列表与数组中的索引器

如何在列表和数组中定义索引器。

List lists=new List(); MyStruct是一个结构。 现在考虑MyStruct[] arr=new MyStruct[10];

arr[0]给出了对第一个Structure项的引用。但是lists[0]给了我一个副本。 是否有任何理由这样做。 此外,因为Int32是结构List list1 =new List(); 我怎么可能访问list1[0]或分配list1[0]=5 ,因为它不可能做lists[0]._x=5

虽然它们看起来一样,但是数组索引器和列表索引器正在完全分开。

List索引器声明为带有参数的属性:

 public T this[int index] { get; set; } 

这会被编译为get_Itemset_Item方法,这些方法在访问参数时像任何其他方法一样被调用。

数组索引器在CLR中有直接支持; 有一个特定的IL指令ldelema (加载元素地址),用于获取指向数组第n个元素的托管指针。 然后,该指针可以被任何其他IL指令使用,这些指令将指针直接改变该地址处的东西。

例如, stfld (存储字段值)指令可以使用指定’this’实例的托管指针来存储字段,或者您可以使用指针直接在数组中的事物上调用方法。

在C#parlance中,数组索引器返回一个变量 ,但列表索引器返回一个

最后一点:

 lists[0]._x=5 

实际上只是对你早期观点的重述:

arr[0]给出了对第一个Structure项的引用。但是lists[0]给了我一个副本。

如果您编辑了它的副本 ,则更改将丢失到以太,即

 var throwawayCopy = lists[0]; throwawayCopy._x = 5; // (and never refer to throwawayCopy again, ever) 

因为这几乎肯定不是你想要的,编译器不会让你。 然而,可变的结构是邪恶的 。 这里更好的选择是不要使用可变结构 。 他们咬了一口。


把它降低到一个水平,到一个简单而具体的例子:

 using System; struct Evil { public int Yeuch; } public class Program { public static void Main() { var pain = new Evil[] { new Evil { Yeuch = 2 } }; pain[0].Yeuch = 27; Console.WriteLine(pain[0].Yeuch); } } 

这将编译(在这里查看最后两行):

 L_0026: ldloc.0 <== pain L_0027: ldc.i4.0 <== 0 L_0028: ldelema Evil <== get the MANAGED POINTER to the 0th element (not the 0th element as a value) L_002d: ldc.i4.s 0x1b <== 27 L_002f: stfld int32 Evil::Yeuch <== store field L_0034: ldloc.0 <== pain L_0035: ldc.i4.0 <== 0 L_0036: ldelema Evil <== get the MANAGED POINTER to the 0th element (not the 0th element as a value) L_003b: ldfld int32 Evil::Yeuch <== read field L_0040: call void [mscorlib]System.Console::WriteLine(int32) <== writeline L_0045: ret 

它从来没有真正与结构作为一个值进行对话 - 没有副本等

List有一个普通的索引器,其行为类似于属性。 访问通过访问器function,这些是按值的。

 T this[int index] { get{return arr[index];} set{arr[index]=value;}} } 

数组是特殊类型,它们的索引器是字段式的。 运行时和C#编译器都具有数组的特殊知识,并且可以实现此行为。 您不能在自定义类型上拥有类似行为的数组。

幸运的是,这在实践中很少成为问题。 由于您只在极少数特殊情况下使用可变结构(高性能或本机互操作),并且在那些中您通常更喜欢数组,因为它们的开销很低。


您对属性与字段的行为相同。 使用字段时会获得一种引用,但在使用属性时会获得副本。 因此,您可以写入值类型字段的成员,但不能写入值类型属性的成员。

当我检查lambda表达式类型时,我也碰到了这个。 将lambda编译为表达式树时,可以检查每个节点的表达式类型 。 事实certificate, Array索引器有一个特殊的节点类型ArrayIndex

 Expression> expression = () => new string[] { "Test" }[0]; Assert.Equal(ExpressionType.ArrayIndex, expression.Body.NodeType); 

List索引器的类型为Call

 Expression> expression = () => new List() { "Test" }[0]; Assert.Equal(ExpressionType.Call, expression.Body.NodeType); 

这只是为了说明我们可以用lambda表达式推断底层架构。

你的问题不是List <>,而是结构本身。

以此为例:

 public class MyStruct { public int x; public int y; } void Main() { var someStruct = new MyStruct { x = 5, y = 5 }; someStruct.x = 3; } 

在这里,你没有修改原始结构的x的值,你正在创建一个y = y和x = 3的新对象。你不能用列表直接修改它的原因是因为列表indexer是一个函数(与数组索引器相对),它不知道如何在列表中“设置”新结构。

将关键字struct修改为class ,你会发现它工作得很好(每次你改变时你都不会创建一个全新的对象)。

.net语言的一个不幸的限制是,除了返回一个值之外,它们没有任何属性做任何事情的概念,然后可以使用,但是调用者认为合适。 如果有一个标准的编译器支持的方法将属性公开为委托调用者,那将是非常有用的(如果我有一种请求语言function的方法,我会寻求这个),例如:

   MyListOfPoints [4] .X = 5;

可以由编译器翻译成如下内容:

   MyListOfPoints.ActOnItem(4,(ref Point it)=> it.X = 5);

如果ActOnItem采用generics类型的额外ref参数,并将其传递给也采用该类型参数的委托,则此类代码可能相对有效,并且不会产生任何GC压力。 这样做将允许被调用的函数是静态的,从而无需为每个执行封闭函数创建闭包或委托。 如果ActOnItem有一种方法可以接受可变数量的generics’ref’参数,甚至可以处理如下构造:

   SwapItems(ref MyListOfPoints [4] .X,ref MyListofPoints [4] .Y);

使用’ref’参数的任意组合,但即使只是能够处理属性“涉及”赋值左侧的情况,或者使用单个property-ish’ref’参数调用函数,很有帮助。

请注意,能够以这种方式执行操作将提供超出访问结构字段的能力的额外好处。 这也意味着暴露财产的对象将收到消费者完成的通知(因为消费者的委托将返回)。 想象一下,例如,一个人有一个控件,显示一个项目列表,每个项目都有一个字符串和一个颜色,一个人希望能够做到这样的事情:

   MyControl.Items(5).Color = Color.Red;

一个简单的阅读声明,以及改变第五个列表项颜色的最自然的阅读方式,但试图使这样的语句工作将要求Items(5)返回的对象有一个指向MyControl的链接,并发送当它发生变化时会发出某种通知。 相当复杂。 相比之下,如果支持上面所述的呼叫方式,那么这样的事情就会简单得多。 ActOnItem(index, proc, param)方法会知道一旦proc返回,就必须重绘index指定的项目。 重要的是,如果Items(5)是一个call-through proc并且不支持任何直接读取方法,那么可以避免以下情况:

   var evil = MyControl.Items(5);
   MyControl.items.DeleteAt(0);
   //以下语句是否应该影响以前的项目,
   //或现在是第五个,还是应该抛出exception? 怎么样
   //应该确保这样的语义吗?
   evil.Color = Color.Purple;

MyControl.Items(5)的值仅在涉及它的调用期间保持绑定到MyControl 。 在那之后,它只是一个分离的价值。