【问题标题】:Changing the value of an element in a list of structs更改结构列表中元素的值
【发布时间】:2010-09-08 06:26:30
【问题描述】:

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

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

现在我想改变一个元素:

MyList[1].Name = "bob"

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

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

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

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

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

【问题讨论】:

    标签: c# struct value-type


    【解决方案1】:

    从 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 后,运行正常。

    【讨论】:

      【解决方案2】:

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

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

      只要方法返回结构的实例,您就会得到该结构的副本(除非您使用 ref/out)。

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

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

      【讨论】:

        【解决方案3】:

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

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

        Struct obItem = MyList[1];
        

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

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

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

        下面的代码sn-p说明了我刚才说的内容

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

        【讨论】:

        • 还是不行。该列表必须声明为接口类型,在这种情况下,其中的所有项目都将被装箱。对于每个值类型,都有一个盒装等效具有类类型语义。如果您想使用一个类,请使用一个类,但要注意其他令人讨厌的警告。
        • @Supercat - 就像我上面提到的......它会导致拳击。我不建议通过接口参考进行修改——只是说如果你必须有一个集合结构+想要就地修改它们,它会起作用。这是一个 hack.. 基本上你正在为值类型制作 ref-type 包装器。
        • 与其使用List&lt;Object&gt; 或让结构体实现一个setter 接口(whoswe 语义是可怕的,如上所述),另一种方法是定义class SimpleHolder&lt;T&gt; { public T Value; },然后使用List&lt;SimpleHolder&lt;MyStruct&gt;&gt;。如果Value 是一个字段而不是一个结构,那么像obList2[1].Value.Name="George"; 这样的语句就可以正常工作。
        【解决方案4】:
        MyList[1] = new MyStruct("bob");
        

        C# 中的结构几乎总是应设计为不可变的(即,一旦创建,就无法更改其内部状态)。

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

        【讨论】:

        • 这不是完整的答案,Gishu 的答案要完整得多。
        • Jolson 所说的——结构并不是“不可变的”。是正确的。 -1 cos 说结构是不可变的确实是错误的。
        • 对 Andrew 说句公道话——我不解释他是说结构是“不可变的”,他是说它们应该被用作 if 它们是不可变的;如果所有字段都是只读的,当然你可以使它们不可变。
        • 你真的用这个structs in C# should almost always be designed to be immutable (that is, have no way to change their internal state once they have been created).拯救了我的一天。我的班级中有一些嵌套结构,更改它的值并没有改变。将struct 更改为class 解决了我的问题。谢谢你,兄弟。
        • 只是想抛弃“结构应该被视为不可变”的设计概念是过时的,并且没有考虑大量有效的用例。结构的一个常见用途是提高效率。在紧密循环中复制大型结构数组中的整个结构(再次以这种方式存储以提高效率)与结构的常见目的背道而驰。将结构视为可变的是有效的。也就是说,这个问题的答案涉及使用“不安全”上下文并检索指向所需结构数据的指针。有人可能会说这违反了 c# 的“方式”,有人会误会。
        【解决方案5】:

        具有暴露字段或允许通过属性设置器进行突变的结构没有任何问题。然而,为了响应方法或属性 getter 而改变自身的结构是危险的,因为系统将允许在临时结构实例上调用方法或属性 getter;如果方法或 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 可能做了什么其他事情,或者他们可能期望什么之后再做。

        【讨论】:

        • 你说,“……如果NameMyStruct 的暴露字段,我提供的更新它的方法将起作用……”不完全是。由于您为参考class 案例提出了线程安全的幽灵,因此在相同的基础上判断ValueType 代码是公平的,并且列表中的索引位置可以在您的操作过程中发生变化,使得myList[1] 没有更长对应于您获取的struct 实例。要解决此问题,您需要某种锁定或同步来了解整个集合实例。 class 版本也同样存在同样的问题。
        • @GlennSlayden:一个将项目公开为 byrefs 以进行就地编辑的集合可以很容易地允许以线程安全的方式编辑项目,如果所有将完成的添加和删除都在任何之前完成byrefs 暴露。如有必要,可以构造一个集合以允许在最后添加项目而不影响任何 byref,但这需要仅通过添加新对象或数组来完成任何扩展——当然可能,但大多数集合是如何实现的。
        • 我没有说这是不可能的,我当然知道很多方法可以解决它,我只是想指出你的例子中的竞争条件。也许是因为我在这个不起眼的地区的好奇的微观专业知识导致种族像南极冰川上的陨石一样突出。至于“将项目公开为 byrefs 以进行就地编辑的集合:”是的,完全正确;我自己说得再好不过了。去过那里,做到了,效果很好。哦,我差点忘了,无锁并发也是。
        • 顺便说一句,我最初误读了您的评论,并认为您在暗示一种在托管集合内部实际持久 byrefs 的方法。几分钟的闲逛让我确信你不可能在暗示它,所以我重新仔细阅读......但现在我不能停止想知道:(ref T)[].NET,对吧?甚至C++/CLI 也不允许cli::arrayinterior_ptr&lt;T&gt;——这是必须的——因为后者是非原始本机类型。所以......不可能,对吧?
        • 为了澄清 supercat 的原始评论,以便那些迟到的人受益,我们正在谈论一种类型的 .NET 集合,它合成托管指针即时,以便通过专门设计的ref return API原位公开其元素。
        【解决方案6】:

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

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

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

        【讨论】:

        • 嗯。为什么我可以修改分配在堆栈上的结构的字段,即使结构是数组 MyStruct[] 的一部分?为什么会这样?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-14
        相关资源
        最近更新 更多