【问题标题】:Directly modifying List<T> elements直接修改 List<T> 元素
【发布时间】:2012-09-03 23:01:59
【问题描述】:

我有这个结构:

struct Map
{
    public int Size;

    public Map ( int size )
    {
        this.Size = size;
    }

    public override string ToString ( )
    {
        return String.Format ( "Size: {0}", this.Size );
    }
}

当使用数组时,它可以工作:

Map [ ] arr = new Map [ 4 ] {
    new Map(10),
    new Map(20),
    new Map(30),
    new Map(40)};

arr [ 2 ].Size = 0;

但是使用List的时候,编译不出来:

List<Map> list = new List<Map> ( ) {
    new Map(10),
    new Map(20),
    new Map(30),
    new Map(40)};

list [ 2 ].Size = 0;

为什么?

【问题讨论】:

  • 不起作用是什么意思?它编译吗?它会引发运行时异常吗?它是否运行但属性没有相应改变?
  • 很抱歉以前从未见过这个问题..
  • @Peter Mortensen:无需在标题中添加“C#”。自it's a tag 以来,SO 处理得很好

标签: c# .net


【解决方案1】:

C# 编译器会给你以下错误:

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

原因是结构是值类型,因此当您访问列表元素时,实际上您将访问列表索引器返回的元素的中间副本。

来自MSDN

错误信息

不能修改返回值 '表达',因为它不是 变量

试图修改一个值 类型是一个结果 中间表达。因为 值不持久,该值将 保持不变。

要解决此错误,请将 表达式的结果 中间值,或使用参考 中间表达式的类型。

解决方案:

  1. 使用数组。这使您可以直接访问元素(您不是在访问副本)
  2. 当您将 Map 设为一个类时,您仍然可以使用 List 来存储您的元素。然后,您将获得对 Map 对象的引用而不是中间副本,并且您将能够修改该对象。
  3. 如果您无法将 Map 从 struct 更改为 class,则必须将列表项保存在临时变量中:

 

List<Map> list = new List<Map>() { 
    new Map(10), 
    new Map(20), 
    new Map(30), 
    new Map(40)
};

Map map = list[2];
map.Size = 42;
list[2] = map;

【讨论】:

  • 谢谢,但我使用的是 XNA 框架,并且那里的大多数东西都是作为结构实现的,比如 Vector3 等。
  • 这只是 C# 的限制,CLR 可以返回对结构的引用(托管指针),但 C# 语言没有实现。
  • 谢谢,但是对于数组它返回引用?
  • 是的,msdn.microsoft.com/en-us/library/…,但是托管指针 & 也可以从方法返回,C# 不这样做。
【解决方案2】:

因为它是struct,所以在使用 List 时,您正在创建副本。

使用结构体时,最好使它们不可变。这将避免这样的影响。

使用数组时,您可以直接访问内存结构。使用 List.get_Item 您可以处理返回值,即结构的副本。

如果它是一个类,您将获得指向该类的指针的副本,但您不会注意到这一点,因为指针在 C# 中是隐藏的。

同样使用 List.ToArray 也解决不了,因为它会创建一个内部数组的副本,并返回这个。

这不是你在使用结构时会得到这样效果的唯一地方。

Divo 提供的解决方案是一个非常好的解决方法。但是你必须记住以这种方式工作,不仅在使用 List 时,而且在任何你想更改结构内字段的地方。

【讨论】:

  • 如何让它不可变?
  • 不要提供任何修改结构内部值的方法。就像 .NET 对字符串和 DateTime 对象所做的那样,所有操作都应该返回一个新实例,而不是更改当前实例。
  • 但是实例方法呢?
  • 将 Size 字段的代码更改为(类似于): public int Size { get;私人套装; }
  • 如果需要大量更改 Size(或其他字段),最好去上课,不要使用 sstruct。
【解决方案3】:

我不是 XNA 开发人员,因此我无法评论在 XNA 中使用结构与类的要求有多严格。但这似乎是一个更自然的地方,可以使用类来获得您正在寻找的 pass-by-ref 语义。

我的一个想法是你可以使用拳击。通过在您的情况下创建一个接口,例如“IMap”,由您的“Map”结构植入,然后使用List&lt;IMap&gt;,列表将包含通过引用传递的 System.Object 对象。例如:

interface IMap
{
    int Size { get; set; }
}

struct Map: IMap
{
    public Map(int size)
    {
        _size = size;
    }

    private int _size;

    public int Size
    {
        get { return _size; }
        set { _size = value; }
    }

    public override string ToString()
    {
        return String.Format("Size: {0}", this.Size);
    }

}

然后可以通过以下方式调用:

List<IMap> list = new List<IMap>() { 
    new Map(10), 
    new Map(20), 
    new Map(30), 
    new Map(40)};

    list[2].Size = 4;
    Console.WriteLine("list[2].Size = " + list[2].Size.ToString());

请注意,这些结构只会被装箱一次,当首先传递到 List 时,而不是在使用诸如 'list[2].Size = 4' 之类的代码调用时,所以它应该是相当有效的,除非您在代码的其他部分将这些 IMap 对象转换回 Map(将其从 List&lt;IMap&gt; 中复制出来)。

虽然这将实现您对 List 中的结构进行直接读写访问的目标,但装箱结构实际上是将结构填充到一个类(一个 System.Object)中,因此我认为首先让你的“地图”成为一个类可能更有意义?

迈克

【讨论】:

    【解决方案4】:

    在 XNA 中尝试计算顶点缓冲区的法线时,我不断回到这个问题。

    我想出的最好的 XNA 解决方案是将数据复制(或存储)到一个数组中。

    private void SomeFunction()
    {
        List<VertexBasicTerrain> vertexList = GenerateVertices();
        short[] indexArray = GenerateIndices();
    
        CalculateNormals(vertexList, ref indexArray); // Will not work
    
    
        var vertexArray = vertexList.ToArray();
        CalculateNormals(ref vertexArray, ref indexArray);
    }
    
    // This works (algorithm from Reimers.net)
    private void CalculateNormals(ref VertexBasicTerrain[] vertices, ref short[] indices)
    {
        for (int i = 0; i < vertices.Length; i++)
            vertices[i].Normal = new Vector3(0, 0, 0);
    
        for (int i = 0; i < indices.Length / 3; i++)
        {
            Vector3 firstvec = vertices[indices[i * 3 + 1]].Position - vertices[indices[i * 3]].Position;
            Vector3 secondvec = vertices[indices[i * 3]].Position - vertices[indices[i * 3 + 2]].Position;
            Vector3 normal = Vector3.Cross(firstvec, secondvec);
            normal.Normalize();
            vertices[indices[i * 3]].Normal += normal;
            vertices[indices[i * 3 + 1]].Normal += normal;
            vertices[indices[i * 3 + 2]].Normal += normal;
        }
        for (int i = 0; i < vertices.Length; i++)
            vertices[i].Normal.Normalize();
    }
    
    // This does NOT work and throws a compiler error because of the List<T>
    private void CalculateNormals(List<VertexBasicTerrain> vertices, ref short[] indices)
    {
        for (int i = 0; i < vertices.Length; i++)
            vertices[i].Normal = new Vector3(0, 0, 0);
    
        for (int i = 0; i < indices.Length / 3; i++)
        {
            Vector3 firstvec = vertices[indices[i * 3 + 1]].Position - vertices[indices[i * 3]].Position;
            Vector3 secondvec = vertices[indices[i * 3]].Position - vertices[indices[i * 3 + 2]].Position;
            Vector3 normal = Vector3.Cross(firstvec, secondvec);
            normal.Normalize();
            vertices[indices[i * 3]].Normal += normal;
            vertices[indices[i * 3 + 1]].Normal += normal;
            vertices[indices[i * 3 + 2]].Normal += normal;
        }
        for (int i = 0; i < vertices.Length; i++)
            vertices[i].Normal.Normalize();
    }
    

    【讨论】:

      【解决方案5】:

      我决定直接替换副本上的结果并重新分配结果,例如:

      Map map = arr [ 2 ];
      map.Size = 0;
      arr [ 2 ] = map;
      

      【讨论】:

        【解决方案6】:
        list [ 2 ].Size = 0;
        

        其实是:

        //Copy of object
        Map map = list[2];
        map.Size = 2;
        

        使用class 代替struct

        【讨论】:

        • 谢谢,但我使用的是 XNA 框架,并且那里的大多数东西都是作为结构实现的,比如 Vector3 等。
        猜你喜欢
        • 2022-09-23
        • 2015-10-13
        • 2021-06-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-08-25
        • 2020-05-23
        • 2019-09-22
        相关资源
        最近更新 更多