Непосредственная модификация элементов List <T>

У меня есть эта структура:

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;

Почему?

Ответ 1

Компилятор С# предоставит вам следующую ошибку:

Невозможно изменить возвращаемое значение 'System.Collections.Generic.List.this [int]', потому что это не переменная

Причина в том, что структуры являются типами значений, поэтому при доступе к элементу списка вы фактически получаете доступ к промежуточной копии элемента, который был возвращен индексом списка.

От MSDN:

Сообщение об ошибке

Невозможно изменить возвращаемое значение 'выражение', потому что это не Переменная

Была сделана попытка изменить значение тип, который был результатом промежуточное выражение. Поскольку значение не сохраняется, значение будет не изменится.

Чтобы устранить эту ошибку, результат выражения в промежуточное значение или использовать ссылку тип для промежуточного выражения.

Решения:

  • Используйте массив. Это дает вам прямой доступ к элементам (вы не получаете доступ к копии)
  • Когда вы создаете карту класса, вы все равно можете использовать Список для хранения вашего элемента. Затем вы получите ссылку на объект Map вместо промежуточной копии, и вы сможете изменить объект.
  • Если вы не можете изменить Map из struct в класс, вы должны сохранить элемент списка во временной переменной:

 

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;

Ответ 2

Поскольку это struct, при использовании List <T> вы создаете копии.

При использовании структуры лучше сделать их неизменными. Это позволит избежать таких эффектов.

При использовании массива у вас есть прямой доступ к структурам памяти. Используя List <T> .get_Item, вы работаете над возвращаемым значением, то есть копией структуры.

Если бы это был класс, вы получили бы копию указателя на класс, но вы этого не заметили бы, поскольку указатели скрыты на С#.

Также использование List <T> .ToArray не решает его, потому что оно создаст копию внутреннего массива и вернет это.

И это не единственное место, где вы получите такие эффекты при использовании структуры.

Решение, предоставленное Divo, является очень хорошим решением. Но вы должны помнить, что работаете таким образом, не только при использовании List <T> , но и везде, где вы хотите изменить поле внутри структуры.

Ответ 3

Я не разработчик XNA, поэтому я не могу прокомментировать, насколько строго требуется использование структур для классов для XNA. Но это кажется гораздо более естественным местом для использования класса, чтобы получить семантику pass-by-ref, которую вы ищете.

Я думал, что вы можете использовать бокс. Создав интерфейс, скажем, "IMap" в вашем случае, имплантируется структурой "Карта", а затем с помощью List<IMap> в списке будут храниться объекты 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 [2].Size = 4", поэтому он должен быть достаточно эффективным, если только вы берете эти объекты IMAP и возвращаетесь к Map (копируя его из List<IMap>) в других частях вашего кода.

Хотя это достигнет вашей цели иметь прямой доступ на чтение и запись к структурам в списке < > , бокс, структура действительно заполняет структуру в классе (System.Object), и поэтому я думаю, что возможно, имеет смысл сделать ваш "Карта" классом в первую очередь?

Mike

Ответ 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

list [ 2 ].Size = 0;

на самом деле:

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

Используйте class вместо struct.

Ответ 6

Я решил напрямую заменить результат на копию и переназначить результат, например:

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