74

我有一个结构列表,我想更改一个元素。例如 :

MyList.Add(new MyStruct("john");
MyList.Add(new MyStruct("peter");

现在我想改变一个元素:

MyList[1].Name = "bob"

但是,每当我尝试这样做时,都会出现以下错误:

无法修改 System.Collections.Generic.List.this[int]' 的返回值,因为它不是变量

如果我使用类列表,则不会出现问题。

我想答案与结构是一种值类型有关。

那么,如果我有一个结构列表,我应该将它们视为只读吗?如果我需要更改列表中的元素,那么我应该使用类而不是结构吗?

4

6 回答 6

50

不完全的。将类型设计为类或结构不应由您将其存储在集合中的需要驱动:) 您应该查看所需的“语义”

您看到的问题是由于值类型语义。每个值类型变量/引用都是一个新实例。当你说

Struct obItem = MyList[1];

发生的情况是创建了一个新的结构实例,并且所有成员都被一一复制。这样您就有了 MyList[1] 的克隆,即 2 个实例。现在如果你修改obItem,它不会影响原来的。

obItem.Name = "Gishu";  // MyList[1].Name still remains "peter"

现在在这里忍受我 2 分钟(这需要一段时间才能吞下.. 这对我来说确实如此:) 如果您确实需要将结构存储在集合中并像您在问题中指出的那样进行修改,您将不得不您的结构公开一个接口(但是这将导致装箱)。然后,您可以通过引用装箱对象的接口引用来修改实际结构。

下面的代码片段说明了我刚才所说的

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 宇宙仍然是平衡的 :)
Setter 方法不起作用
obList2[1] 返回一个其状态将被修改的副本。列表中的原始结构保持不变。所以 Set-via-Interface 似乎是唯一的方法。

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);
}
于 2008-09-09T10:56:06.587 回答
43
MyList[1] = new MyStruct("bob");

C# 中的结构几乎总是被设计为不可变的(也就是说,一旦它们被创建就无法改变它们的内部状态)。

在您的情况下,您要做的是替换指定数组索引中的整个结构,而不是尝试仅更改单个属性或字段。

于 2008-09-09T10:27:56.700 回答
16

结构并不是“不可变的”。

真正的根本问题是结构是值类型,而不是引用类型。因此,当您从列表中拉出对结构的“引用”时,它正在创建整个结构的新副本。因此,您对其所做的任何更改都会更改副本,而不是列表中的原始版本。

像 Andrew 所说,您必须替换整个结构。就这一点而言,尽管我认为您必须首先问自己为什么要使用结构(而不​​是类)。确保您没有围绕过早的优化问题进行操作。

于 2008-09-09T22:22:50.867 回答
7

具有暴露字段或允许通过属性设置器进行突变的结构没有任何问题。然而,响应方法或属性获取器而发生变异的结构是危险的,因为系统将允许在临时结构实例上调用方法或属性获取器;如果方法或 getter 对结构进行更改,这些更改最终将被丢弃。

不幸的是,正如您所注意到的,.net 中内置的集合在暴露其中包含的值类型对象方面确实很弱。您最好的选择通常是执行以下操作:

  MyStruct temp = myList[1];
  temp.Name = "阿尔伯特";
  我的列表 [1] = 温度;

有点烦人,而且根本不是线程安全的。仍然是对类类型 List 的改进,其中做同样的事情可能需要:

  myList[1].Name = "阿尔伯特";

但它可能还需要:

  myList[1] = myList[1].Withname("Albert");

或许

  myClass temp = (myClass)myList[1].Clone();
  temp.Name = "阿尔伯特";
  我的列表 [1] = 温度;

或者可能是其他一些变化。除非检查 myClass 以及将事物放入列表的其他代码,否则真的无法知道。如果不检查无权访问的程序集中的代码,则完全有可能无法知道第一种形式是否安全。相比之下,如果 Name 是 MyStruct 的一个公开字段,那么我为更新它提供的方法将起作用,无论 MyStruct 包含什么其他内容,或者无论在代码执行之前 myList 可能做了什么其他事情,或者他们可能期望什么之后做。

于 2011-12-19T07:59:41.553 回答
2

除了其他答案之外,我认为解释编译器抱怨的原因可能会有所帮助。

当您调用 时MyList[1].Name,与数组不同,它MyList[1]实际上是在后台调用索引器方法。

每当方法返回结构的实例时,您都会获得该结构的副本(除非您使用 ref/out)。

因此,您将获得一个副本并在副本Name上设置属性,该副本即将被丢弃,因为该副本没有存储在任何地方的变量中。

教程更详细地描述了正在发生的事情(包括生成的 CIL 代码)。

于 2019-07-05T02:39:00.327 回答
0

从 C#9 开始,我不知道有任何方法可以通过引用从通用容器中提取结构,包括List<T>. 正如杰森奥尔森的回答所说:

真正的根本问题是结构是值类型,而不是引用类型。因此,当您从列表中拉出对结构的“引用”时,它正在创建整个结构的新副本。因此,您对其所做的任何更改都会更改副本,而不是列表中的原始版本。

所以,这可能是非常低效的。SuperCat 的答案,即使它是正确的,通过将更新的结构复制回列表来加剧效率低下。

如果您对最大化结构的性能感兴趣,请使用数组而不是List<T>. 数组中的索引器返回对结构的引用,并且不像List<T>索引器那样复制整个结构。此外,数组比List<T>.

如果您需要随着时间的推移增加数组,那么创建一个类似 的泛型类List<T>,但在下面使用数组。

有一个替代解决方案。创建一个包含该结构的类并创建公共方法来调用该结构的方法以获得所需的功能。使用 aList<T>并为 T 指定类。该结构也可以通过 ref 返回方法或 ref 属性返回,该方法返回对该结构的引用。

这种方法的优点是它可以与任何通用数据结构一起使用,例如Dictionary<TKey, TValue>. 当从 a 中拉出一个结构时Dictionary<TKey, TValue>,它也会将该结构复制到一个新实例中,就像List<T>. 我怀疑所有 C# 通用容器都是如此。

代码示例:

public struct Mutable
{
   private int _x;

   public Mutable(int x)
   {
      _x = x;
   }

   public int X => _x; // Property

   public void IncrementX() { _x++; }
}

public class MutClass
{
   public Mutable Mut;
   //
   public MutClass()
   {
      Mut = new Mutable(2);
   }

   public MutClass(int x)
   {
      Mut = new Mutable(x);
   }

   public ref Mutable MutRef => ref Mut; // Property

   public ref Mutable GetMutStruct()
   {
      return ref Mut;
   }
}

private static void TestClassList()
{
   // This test method shows that a list of a class that holds a struct
   // may be used to efficiently obtain the struct by reference.
   //
   var mcList = new List<MutClass>();
   var mClass = new MutClass(1);
   mcList.Add(mClass);
   ref Mutable mutRef = ref mcList[0].MutRef;
   // Increment the x value defined in the struct.
   mutRef.IncrementX();
   // Now verify that the X values match.
   if (mutRef.X != mClass.Mut.X)
      Console.Error.WriteLine("TestClassList: Error - the X values do not match.");
   else
      Console.Error.WriteLine("TestClassList: Success - the X values match!");
}

控制台窗口上的输出:

TestClassList: Success - the X values match!

对于以下行:

ref Mutable mutRef = ref mcList[0].MutRef;

我最初无意中在等号后遗漏了 ref。编译器没有抱怨,但它确实生成了结构的副本,并且在运行时测试失败了。添加 ref 后,它运行正确。

于 2021-07-27T02:31:37.157 回答