我有一个结构列表,我想更改一个元素。例如:
1 2
| MyList.Add(new MyStruct("john");
MyList.Add(new MyStruct("peter"); |
现在我要更改一个元素:
但是,每当我尝试执行此操作时,都会出现以下错误:
Cannot modify the return value of
System.Collections.Generic.List.this[int]‘ because it is not
a variable
如果我使用类列表,则不会发生此问题。
我想答案与结构是值类型有关。
因此,如果我有一个结构列表,我应该将它们视为只读吗?如果我需要更改列表中的元素,那么应该使用类而不是结构?
不完全是。将类型设计为类或结构不应由将其存储在集合中的需求驱动:)您应该查看所需的"语义"
您看到的问题是由于值类型语义所致。每个值类型变量/引用都是一个新实例。当你说
1
| Struct obItem = MyList[1]; |
发生的事情是创建了该结构的新实例,并且所有成员都被一个一个地复制。这样您就可以克隆MyList [1],即2个实例。
现在,如果修改obItem,它不会影响原始对象。
1
| obItem.Name ="Gishu"; // MyList[1].Name still remains"peter" |
现在在这里和我一起忍受2分钟(这需要花一会儿时间才能吞下..对我有用:)
如果确实需要将结构存储在集合中并按照问题中的指示进行修改,则必须使结构公开一个接口(但是这将导致装箱)。然后,您可以通过引用盒装对象的接口引用来修改实际的结构。
以下代码段说明了我在上面刚刚说过的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public interface IMyStructModifier
{
String Name { set; }
}
public struct MyStruct : IMyStructModifier ...
List<Object> obList = new List<object>();
obList.Add(new MyStruct("ABC"));
obList.Add(new MyStruct("DEF"));
MyStruct temp = (MyStruct)obList[1];
temp.Name ="Gishu";
foreach (MyStruct s in obList) // =>"ABC","DEF"
{
Console.WriteLine(s.Name);
}
IMyStructModifier temp2 = obList[1] as IMyStructModifier;
temp2.Name ="Now Gishu";
foreach (MyStruct s in obList) // =>"ABC","Now Gishu"
{
Console.WriteLine(s.Name);
} |
HTH。好问题。
更新:@Hath-您要我跑步检查我是否忽略了这么简单的内容。 (如果没有setter属性而方法没有做到这一点-.Net宇宙仍然是平衡的:)
设置器方法不起作用
obList2 [1]返回其状态将被修改的副本。列表中的原始结构保持不变。因此,"通过接口设置"似乎是唯一的方法。
1 2 3 4 5 6 7 8
| List<MyStruct> obList2 = new List<MyStruct>();
obList2.Add(new MyStruct("ABC"));
obList2.Add(new MyStruct("DEF"));
obList2[1].SetName("WTH");
foreach (MyStruct s in obList2) // =>"ABC","DEF"
{
Console.WriteLine(s.Name);
} |
1
| MyList[1] = new MyStruct("bob"); |
C#中的结构几乎应始终设计成不可变的(也就是说,一旦创建,便无法更改其内部状态)。
在您的情况下,您要做的是替换指定数组索引中的整个结构,而不是尝试仅更改单个属性或字段。
结构"不可变"不是很多。
真正的潜在问题是结构是值类型,而不是引用类型。因此,当您从列表中拉出对该结构的"引用"时,它将创建整个结构的新副本。因此,您对此所做的任何更改都将更改副本,而不是列表中的原始版本。
就像安德鲁指出的那样,您必须替换整个结构。尽管如此,我认为您必须问自己,为什么首先要使用结构(而不??是类)。确保您没有围绕过早的优化问题进行操作。
具有公开字段或允许通过属性设置器进行突变的结构没有问题。但是,响应于方法或属性获取器而发生变异的结构很危险,因为系统允许在临时结构实例上调用方法或属性获取器。如果方法或获取方法更改了结构,则这些更改最终将被丢弃。
不幸的是,正如您所注意到的,.net内置的集合在暴露其中包含的值类型对象方面确实微不足道。最好的选择通常是执行以下操作:
1 2 3
| MyStruct temp = myList[1];
temp.Name ="Albert";
myList[1] = temp; |
有点令人讨厌,而且根本不是线程安全的。仍然是对类类型的列表的一种改进,在这种情况下,可能需要执行以下操作:
1
| myList[1].Name ="Albert"; |
,但可能还需要:
1
| myList[1] = myList[1].Withname("Albert"); |
或者也许
1 2 3
| myClass temp = (myClass)myList[1].Clone();
temp.Name ="Albert";
myList[1] = temp; |
或其他一些变化。除非一个人检查了myClass以及将这些东西放到列表中的其他代码,否则一个人实际上是不会知道的。如果不检查无法访问的程序集中的代码,则很可能无法知道第一种形式是否安全。相比之下,如果Name是MyStruct的公开字段,那么我给出的用于更新它的方法将起作用,而不管MyStruct包含的内容是什么,也不管代码执行前myList可能执行的其他操作或期望的代码是什么之后再做。
除了其他答案,我认为解释编译器抱怨的原因可能会有所帮助。
当您调用MyList[1].Name时,与数组不同,MyList[1]实际上是在幕后调用indexer方法。
每当一个方法返回一个结构的实例时,您都将获得该结构的副本(除非您使用ref / out)。
因此,您将获得一个副本,并在副本上设置Name属性,由于该副本未存储在任何地方的变量中,因此该属性将被丢弃。
本教程更详细地描述了正在发生的事情(包括生成的CIL代码)。