如何在列表和数组中定义索引器。
List<MyStruct> lists=new List<MyStruct>();
其中MyStruct
是一个结构。现在考虑一下
MyStruct[] arr=new MyStruct[10];
arr[0]
提供对第一个结构项的引用。但是lists[0]
给了我一份副本。
是否有任何理由这样做。
此外,由于Int32
是结构List<Int32> list1 =new List<Int32>();
,我如何能够访问list1[0]
或指定list1[0]=5
,因为无法执行lists[0]._x=5
答案 0 :(得分:9)
虽然它们看起来一样,但是数组索引器和列表索引器正在完全分开。
List<T>
索引器被声明为带有参数的属性:
public T this[int index] { get; set; }
这会被编译为get_Item
和set_Item
方法,这些方法在访问参数时会像任何其他方法一样被调用。
数组索引器在CLR中有直接支持;有一个特定的IL指令ldelema
(加载元素地址),用于获取指向数组第n个元素的托管指针。然后,该指针可以被任何其他IL指令使用,这些指令将指针直接改变该地址处的东西。
例如,stfld
(存储字段值)指令可以使用指定'this'实例的托管指针来存储该字段,或者您可以使用指针直接调用该方法中的方法。阵列。
在C#用语中,数组索引器返回变量,但列表索引器返回值。
答案 1 :(得分:5)
最后一点:
lists[0]._x=5
实际上只是对你早期观点的重述:
arr[0]
提供了对第一个结构项的引用。但是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
它永远不会将结构作为值进行实际会话 - 没有副本等
答案 2 :(得分:2)
List<T>
有一个普通的索引器,其行为类似于属性。访问通过访问器功能,这些是按值的。
T this[int index]
{
get{return arr[index];}
set{arr[index]=value;}}
}
数组是特殊类型,它们的索引器是字段式的。运行时和C#编译器都具有数组的特殊知识,并且可以实现此行为。您不能在自定义类型上拥有类似行为的数组。
幸运的是,这在实践中很少成为问题。由于您只在极少数特殊情况下使用可变结构(高性能或本机互操作),并且在那些中您通常更喜欢数组,因为它们的开销很低。
您对属性与字段的行为相同。使用字段时会获得一种引用,但在使用属性时会获得副本。因此,您可以写入值类型字段的成员,但不能写入值类型属性的成员。
答案 3 :(得分:2)
当我检查lambda表达式类型时,我也碰到了这个。将lambda编译为表达式树时,您可以检查每个节点的expression type。事实证明,ArrayIndex
索引器有一个特殊的节点类型Array
:
Expression<Func<string>> expression = () => new string[] { "Test" }[0];
Assert.Equal(ExpressionType.ArrayIndex, expression.Body.NodeType);
List
索引器的类型为Call
:
Expression<Func<string>> expression = () => new List<string>() { "Test" }[0];
Assert.Equal(ExpressionType.Call, expression.Body.NodeType);
这只是为了说明我们可以用lambda表达式推断底层架构。
答案 4 :(得分:0)
你的问题不在于List&lt;&gt;,它与结构本身有关。
以此为例:
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的新对象。 你不能用列表直接修改它的原因是因为列表索引器是一个函数(而不是数组索引器),它不知道如何在列表中“设置”新结构。
将关键字struct
修改为class
,您会发现它的工作正常(每次修改时都不会创建一个全新的对象)。
答案 5 :(得分:0)
.net语言的一个不幸的限制是,除了返回一个值之外,它们没有任何属性做任何事情的概念,然后可以使用,但是调用者认为合适。如果有一个标准的编译器支持的方法将属性公开为委托调用者,那将是非常有用的(如果我有一种请求语言功能的方法,我会寻求这个),例如:
MyListOfPoints[4].X = 5;
可以由编译器翻译成:
MyListOfPoints.ActOnItem(4, (ref Point it) => it.X = 5);
如果ActOnItem
采用泛型类型的额外ref参数,并将其传递给也采用该类型参数的委托,则此类代码可能相对有效,并且不会产生任何GC压力。这样做将允许被调用的函数是静态的,从而无需为每个封闭函数的执行创建闭包或委托。如果ActOnItem
有一种方法可以接受可变数量的通用'ref'参数,那么甚至可以处理如下构造:
SwapItems(ref MyListOfPoints[4].X, ref MyListofPoints[4].Y);
使用'ref'参数的任意组合,但即使只是能够处理属性“参与”赋值左侧的情况,或者使用单个属性调用函数 - ish'ref'参数,会有所帮助。
请注意,能够以这种方式执行操作将提供超出访问结构字段的能力的额外好处。这也意味着暴露该属性的对象将收到消费者完成它的通知(因为消费者的委托将返回)。想象一下,例如,一个人有一个控件,显示一个项目列表,每个项目都有一个字符串和一个颜色,一个人希望能够做类似的事情:
MyControl.Items(5).Color = Color.Red;
一个易于阅读的语句,以及更改第五个列表项颜色的最自然的阅读方式,但尝试使这样的语句工作需要Items(5)
返回的对象有一个链接MyControl
,并在更改时发送某种通知。相当复杂。相比之下,如果支持上面所述的通话方式,那么这样的事情就会简单得多。 ActOnItem(index, proc, param)
方法会知道,一旦proc
返回,就必须重新绘制index
指定的项目。重要的是,如果Items(5)
是一个呼入过程并且不支持任何直接读取方法,那么可以避免以下情况:
var evil = MyControl.Items(5); MyControl.items.DeleteAt(0); // Should the following statement affect the item that used to be fifth, // or the one that's fifth now, or should it throw an exception? How // should such semantics be ensured? evil.Color = Color.Purple;
MyControl.Items(5)
的值仅在涉及它的呼叫期间保持绑定到MyControl
。在那之后,它只是一个超然的价值。