Прямое изменение элементов 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;

Почему?

Что значит не работает? Он компилируется? Выдает ли исключение во время выполнения? Работает, но свойство соответственно не меняется?

Andrei Rînea 06.01.2009 03:02

Извините, я никогда раньше не сталкивался с этой проблемой ..

Andrei Rînea 06.01.2009 03:18

@Peter Mortensen: нет необходимости добавлять «C#» к заголовку. SO справляется с этим просто отлично, так как это тег

Default 03.09.2012 19:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
35
3
48 628
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Ответ принят как подходящий

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

Cannot modify the return value of 'System.Collections.Generic.List.this[int]' because it is not a variable

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

От MSDN:

Error Message

Cannot modify the return value of 'expression' because it is not a variable

An attempt was made to modify a value type that was the result of an intermediate expression. Because the value is not persisted, the value will be unchanged.

To resolve this error, store the result of the expression in an intermediate value, or use a reference type for the intermediate expression.

Решения:

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

 

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 и т. д.

Joan Venge 06.01.2009 03:18

Это просто ограничение C#, CLR имеет возможность возвращать ссылки на структуры (управляемые указатели), но язык C# этого не реализует.

Pop Catalin 06.01.2009 04:04

Спасибо, а для массивов он возвращает ссылки?

Joan Venge 06.01.2009 04:25

Да, msdn.microsoft.com/en-us/library/…, но управляемые указатели также могут быть возвращены из методов, C# этого не делает.

Pop Catalin 06.01.2009 05:17

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

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

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

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

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

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

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

Как сделать его неизменным?

Joan Venge 06.01.2009 03:10

Не предоставляйте никаких способов изменения значений внутри структуры. Как .NET делает со строками и объектами DateTime, все операции должны возвращать новый экземпляр, а не изменять текущий экземпляр.

Joel Mueller 06.01.2009 03:11

Но как насчет методов экземпляра?

Joan Venge 06.01.2009 03:13

Измените код для поля Размер на (что-то вроде): public int Size {get; частный набор; }

GvS 06.01.2009 03:15

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

GvS 06.01.2009 03:17

Поскольку OP требует, чтобы это была структура, нельзя ли использовать «небезопасный» код для прямого доступа к структуре?

Mike Rosenblum 06.01.2009 03:58

Это интересно. Как вы думаете, это будет единственный способ получить дескриптор структуры? Но даже в этом случае почему те, что в массиве, работают так, как ожидалось, а не в List <T>? Похоже, команда .NET упустила из виду?

Joan Venge 06.01.2009 04:24

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

Dirk Vollmar 06.01.2009 15:01
list [ 2 ].Size = 0;

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

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

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

Спасибо, но я использую платформу XNA, и большинство вещей реализовано в виде структуры, например Vector3 и т. д.

Joan Venge 07.01.2009 19:07

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

У меня была одна мысль, что ты можешь использовать бокс. Создав интерфейс, скажем, IMap в вашем случае, который будет имплантирован вашей структурой Map, а затем используя 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>) в других частях вашего кода.

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

Майк

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

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

Я постоянно возвращаюсь к этому вопросу, пытаясь вычислить нормали в буфере вершин в 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();
}

Другие вопросы по теме