更改结构列表中元素的值

Dar*_*ren 61 c# struct value-type

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

MyList.Add(new MyStruct("john");
MyList.Add(new MyStruct("peter");
Run Code Online (Sandbox Code Playgroud)

现在我想改变一个元素:

MyList[1].Name = "bob"
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

Gis*_*shu 43

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

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

Struct obItem = MyList[1];
Run Code Online (Sandbox Code Playgroud)

会发生什么是创建结构的新实例并逐个复制所有成员.这样你就有了MyList [1]的克隆,即2个实例.现在,如果您修改obItem,它不会影响原始.

obItem.Name = "Gishu";  // MyList[1].Name still remains "peter"
Run Code Online (Sandbox Code Playgroud)

现在忍受我2分钟(这需要一段时间吞下去..它对我来说:)如果你真的需要结构存储在一个集合中并像你在问题中指出的那样修改,你将不得不制作你的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);
}
Run Code Online (Sandbox Code Playgroud)

HTH.好问题.
更新: @Hath - 你让我跑去检查我是否忽略了那么简单的事情.(如果不使用setter属性并且方法没有 - 这将是不一致的.Net Universe仍然是平衡的:)
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);
}
Run Code Online (Sandbox Code Playgroud)

  • 与其使用 List&lt;Object&gt; 或让结构实现 setter 接口(如上所述,谁的语义很糟糕),另一种方法是定义 class SimpleHolder&lt;T&gt; { public T Value; }`,然后使用 `List&lt;SimpleHolder&lt;MyStruct&gt;&gt;`。如果 `Value` 是一个字段而不是一个结构体,那么像 `obList2[1].Value.Name="George";` 这样的语句就可以正常工作。 (2认同)

And*_*rew 37

MyList[1] = new MyStruct("bob");
Run Code Online (Sandbox Code Playgroud)

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

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

  • Jolson 所说的——并不是说结构是“不可变的”。是正确的。-1 cos 说结构是不可变的真的是错误的。 (3认同)
  • 这不是完整的答案,Gishu的答案要完整得多. (2认同)
  • 为了公平对待安德鲁 - 我不解释他说结构是"不可改变的"他说他们应该用作*如果*他们是不可改变的; 当然,如果所有字段都是只读的,你可以使它们不可变. (2认同)
  • 只是想抛弃“结构应该被视为不可变”的设计概念,这是过时的,并且没有考虑大量有效的用例。结构的常见用途是为了提高效率。在紧密循环中复制大型结构数组中的整个结构(再次以这种方式存储以提高效率)与结构的常见目的背道而驰。将结构视为可变是有效的。也就是说,这个问题的答案涉及使用“不安全”上下文并检索指向所需结构数据的指针。有人可能会说这违反了 c#“方式”,但这是错误的。 (2认同)

Dae*_*ire 13

在 .Net 5.0 中,您可以使用CollectionsMarshal.AsSpan()( source , GitHub Issue ) 来获取 a 的底层数组List<T>作为Span<T>. 请注意,在使用过程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; } }
Run Code Online (Sandbox Code Playgroud)

这是可行的,因为Span<T>索引器使用称为 ref returns 的 C# 7.0 功能。索引器是用ref T返回类型声明的,它提供类似于索引到数组的语义,返回对实际存储位置的引用。

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

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

来源


Jas*_*son 11

并不是说结构是"不可变的".

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

像Andrew所说,你必须替换整个结构.虽然我认为你必须问自己为什么你首先使用结构(而不​​是类).确保您没有围绕过早的优化问题来解决这个问题.


sup*_*cat 6

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

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

  MyStruct temp = myList [1];
  temp.Name =“阿尔伯特”;
  myList [1] = temp;

有点烦人,根本不是线程安全的。仍然是对类类型列表的一种改进,在这种情况下,可能需要执行以下操作:

  myList [1] .Name =“ Albert”;

但它可能还需要:

  myList [1] = myList [1] .Withname(“ Albert”);

或许

  myClass temp =(myClass)myList [1] .Clone();
  temp.Name =“阿尔伯特”;
  myList [1] = temp;

或其他一些变化。除非一个人检查了myClass以及将这些东西放在列表中的其他代码,否则,一个人实际上是不会知道的。如果不检查无法访问的程序集中的代码,则很可能无法知道第一种形式是否安全。相比之下,如果Name是MyStruct的公开字段,那么我提供的用于更新它的方法将起作用,而不管MyStruct包含的内容是什么,也不管代码执行前myList可能执行的其他操作或期望的代码是什么之后再做。