更改结构列表中元素的值

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

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

现在我想改变一个元素:

MyList[1].Name = "bob"

但是,无论何时我尝试这样做,都会得到以下错误:

无法修改 System.Collections.Generic.List.this [ int ]‘因为它不是 一个变量

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

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

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

107556 次浏览
MyList[1] = new MyStruct("bob");

C # 中的 structs 几乎总是被设计成不可变的(也就是说,一旦它们被创建,就没有办法改变它们的内部状态)。

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

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

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

Struct obItem = MyList[1];

会发生的情况是,创建一个 struct 的新实例,并逐个复制所有成员。这样您就有了一个 MyList [1]的克隆,即两个实例。 现在,如果修改 obItem,它不会影响原始的。

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

现在请忍耐我2分钟(这需要一段时间才能咽下去. . 对我来说是这样的:) 如果您确实需要将 struct 存储在集合中并像您在问题中指出的那样进行修改,那么您必须让 struct 公开一个接口(然而,这将导致拳击)。然后,可以通过引用装箱对象的接口引用修改实际的结构。

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

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

问得好。
更新: @Hath-你让我跑步检查我是否忽略了那么简单的事情。(如果 setter 属性没有 nt,而方法没有-the。网络世界仍然是平衡的:)
Setter 方法不起作用
ObList2[1]返回一个其状态将被修改的副本。List 中的原始结构保持不变。因此,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);
}

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

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

就像 Andrew 说的,你必须替换整个结构。在这一点上,我认为你必须问问自己为什么你首先要使用一个结构(而不是一个类)。确保您不是在过早的优化问题周围做这件事。

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

不幸的是,正如您注意到的,内置到。Net 在暴露其中包含的值类型对象方面真的很弱。你最好的选择通常是这样做:

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

有点烦人,而且丝毫不安全。相对于类类型的 List,这仍然是一个改进,在这种情况下,做同样的事情可能需要:

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 可能做了什么其他事情,或者在代码执行之后他们可能期望用它做什么。

除了其他的答案,我认为解释编译器为什么抱怨是有帮助的。

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

任何时候一个方法返回一个 struct 的实例,您都会得到该 struct 的一个副本(除非您使用 ref/out)。

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

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

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

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

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

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

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

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

这种方法的优点是可以与任何通用数据结构(如 Dictionary<TKey, TValue>)一起使用。当从 Dictionary<TKey, TValue>中提取 struct 时,它也会将 struct 复制到一个新实例,就像 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。编译器没有抱怨,但是它确实生成了 struct 的一个副本,测试在运行时失败了。添加了引用之后,它运行正确。

在.Net 5.0中,可以使用 CollectionsMarshal.AsSpan()(来源GitHub 的问题)获取 List<T>的底层数组作为 Span<T>

var listOfStructs = new List<MyStruct> { new MyStruct() };
Span<MyStruct> spanOfStructs = CollectionsMarshal.AsSpan(listOfStructs);
spanOfStructs[0].Value = 42;
Assert.Equal(42, spanOfStructs[0].Value);


struct MyStruct { public int Value { get; set; } }

这是因为 Span<T>索引器使用了一个称为 ref return 的 C # 7.0特性。索引器使用 ref T返回类型声明,它提供了类似数组索引的语义,返回对实际存储位置的引用。

相比之下,List<T>索引器不返回 ref,而是返回位于该位置的内容的副本。

请记住,这仍然是不安全的: 如果 List<T>重新分配数组,CollectionsMarshal.AsSpan以前返回的 Span<T>将不会反映对 List<T>的任何进一步更改。(这就是该方法隐藏在 System.Runtime.InteropServices.CollectionsMarshal类中的原因。)

来源