List<T> 和数组索引器有什么区别?

6
在reza发布的《是否可以从List中访问结构体的引用以进行更改?》帖子后,我脑海中浮现出一个问题。
因此,请考虑以下structinterface(肯定不是很有用,但只是为了展示问题):
public interface IChangeStruct
{
    int Value { get; }
    void Change(int value);
}

public struct MyStruct : IChangeStruct
{
    int value;

    public MyStruct(int _value)
    {
        value = _value;
    }

    public int Value
    {
        get
        {
            return value;
        }
    }

    public void Change(int value)
    {
        this.value = value;
    }
}

MyStruct 实现了 IChangeStruct 接口,因此我们可以直接在堆中更改其装箱副本,而无需进行拆箱并替换为新对象。以下代码演示了这一点:

MyStruct[] l1 = new MyStruct[]
{
    new MyStruct(0)
};

Console.WriteLine(l1[0].Value); //0
l1[0].Change(10);
Console.WriteLine(l1[0].Value); //10

现在,让我们将数组改为 List<T>,即:

List<MyStruct> l2 = new List<MyStruct>
{
    new MyStruct(0)
};

Console.WriteLine(l2[0].Value); //0
l2[0].Change(10);
Console.WriteLine(l2[0].Value); //also 0

据我所了解,在第一种情况下,l1 [0]返回了对装箱结构的引用,而在第二种情况下,它是其他东西。
我还尝试进行反汇编,并发现:
1)对于MyStruct []
IL_0030:  ldelema    Utils.MyStruct
IL_0035:  ldc.i4.s   10
IL_0037:  call       instance void Utils.MyStruct::Change(int32)

2) 对于 List<MyStruct>

 IL_007c:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<valuetype Utils.MyStruct>::get_Item(int32)
 IL_0081:  stloc.s    CS$0$0001
 IL_0083:  ldloca.s   CS$0$0001
 IL_0085:  ldc.i4.s   10
 IL_0087:  call       instance void Utils.MyStruct::Change(int32)

但是我似乎不太能很好地解释它。

那么,List<T> 返回了什么呢?或者数组和 List<T> 是如何按索引返回元素的?或者这只适用于值类型,与引用类型无关?

P.S.:我确实明白一个值类型实例不能被改变,但是这个描述的问题让我明白了我从来没有意识到 List<T> 和数组的工作原理。

2个回答

9

.Net可以使用ldelema指令(加载数组元素的地址)来就地处理数组元素。

这使您可以直接操作数组元素而无需复制它们。(这也是为什么您可以将数组元素作为refout参数传递的原因)

List<T>没有这样的能力。相反,list[i]只是list.get_Item(i)的语法糖,它是一个普通的方法调用,返回结构体的副本。


1
数组只有在0索引和单维度的情况下才具备这种能力,对吧?(因为ldelema只能在这些限制下工作) - Earlz

3
数组的索引器以类似于将其作为ref参数传递给以下代码的方式使元素可用。在任何.NET语言中,不存在其他类型可以像这样行事的机制。任何允许索引访问的其他类型都必须公开一对方法,其中一个方法使内部存储的数据副本可用于调用方的代码,另一个方法将给定来自调用方代码的某些数据的副本存储在某种方式中。这种限制在值类型中最为明显,但在某些情况下也可能对引用类型有问题(例如,可以在T []中的元素上执行Interlocked.ComapreExchange,但不能在List<T>中的元素上执行)。
如果要设计自己的集合类型,则可以通过提供ActOnItem成员来减轻索引器的限制,从而允许像MyFancyList.ActOnItem(4, (ref Point it) => {it.X += 4;});这样的代码。提供不同数量的额外ref参数的通用版本族可能会有所帮助,这些参数将从调用方传递(例如,MyFancyList.ActOnItem(4, (ref MyThing it, ref Thing newValue, ref Thing compareValue) => Threading.Interlocked.CompareExchange(ref it, newValue, compareValue);),因为使用此类方法可以避免需要lambda使用被捕获的变量。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接