更改结构列表中元素的值

时间:2008-09-09 10:23:30

标签: c# struct value-type

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

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

现在我想改变一个元素:

MyList[1].Name = "bob"

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

  

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

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

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

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

6 个答案:

答案 0 :(得分:41)

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

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

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);
}

答案 1 :(得分:33)

MyList[1] = new MyStruct("bob");
C#中的

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

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

答案 2 :(得分:11)

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

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

像安德鲁州一样,你必须替换整个结构。虽然我认为你必须问自己为什么你首先使用结构(而不​​是类)。确保你没有围绕过早的优化问题做这件事。

答案 3 :(得分:5)

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

不幸的是,正如您所注意到的,.net中内置的集合在暴露其中包含的值类型对象时非常虚弱。你最好的选择通常是做:

  MyStruct temp = myList[1];
  temp.Name = "Albert";
  myList[1] = temp;

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

  myList[1].Name = "Albert";

但它可能还需要:

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

或者

  myClass temp = (myClass)myList[1].Clone();
  temp.Name = "Albert";
  myList[1] = temp;

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

答案 4 :(得分:0)

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

当您调用MyList[1].Name时,与数组不同,MyList[1]实际上在幕后调用了indexer方法。

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

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

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

答案 5 :(得分:0)

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

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

因此,这可能非常低效。 SuperCat 的答案虽然是正确的,但通过将更新后的结构复制回列表来加剧这种低效率。

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

如果您需要随着时间的推移增加数组,然后创建一个通用类,其工作方式类似于 List<T>,但在下面使用数组。

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

这种方法的优点是它可以与任何通用数据结构一起使用,例如 Dictionary<TKey, TValue>。当从 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后,运行正常。