Индексаторы в List vs Array

как индексаторы определяются в списке и массивах.

List<MyStruct> lists=new List<MyStruct>(); здесь MyStruct - это структура. теперь рассмотреть MyStruct[] arr=new MyStruct[10];

arr[0] дает ссылку на первый элемент структуры.Но!--4--> дает мне копию. Есть ли причина, почему это делается так. Также с Int32 структура List<Int32> list1 =new List<Int32>(); как это возможно для меня, чтобы открыть list1[0] и назначить list1[0]=5 где как нельзя сделать lists[0]._x=5

6 ответов


хотя они выглядят одинаково, индексатор массива и индексатор списка делают совершенно разные вещи.

на List<T> индексатор объявляется как свойство с параметром:

public T this[int index] { get; set; }

это компилируется в get_Item и set_Item методы, которые вызываются как любой другой метод при доступе к параметру.

индексатор массива имеет прямую поддержку в CLR; существует определенная инструкция IL ldelema (адрес элемента нагрузки) для получения управляемый указатель на n-й элемент массива. Затем этот указатель может использоваться любой другой инструкцией IL, которая принимает указатель для непосредственного изменения вещи по этому адресу.

например,stfld (store field value) инструкция может принимать управляемый указатель, указывающий экземпляр "this" для хранения поля, или вы можете использовать указатель для вызова методов непосредственно на вещи в массиве.

на языке C# индексатор массива возвращает переменная, но индексатор списка возвращает стоимостью.


конечная точка:

lists[0]._x=5

на самом деле это просто повторение вашей предыдущей точки зрения:

arr[0] дает ссылку на первый элемент структуры.Но!--5--> дает мне копию.

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

var throwawayCopy = lists[0];
throwawayCopy._x = 5;
// (and never refer to throwawayCopy again, ever)

поскольку это почти наверняка не то, что вы намеревались, компилятор не позволяет вам. Однако, изменяемые структуры зло. Лучшим вариантом здесь было бы не используйте изменяемые структуры. Они кусаются.


принимая это на уровень ниже, к простому, но конкретному примеру:

using System;
struct Evil
{
    public int Yeuch;
}
public class Program
{
    public static void Main()
    {
        var pain = new Evil[] { new Evil { Yeuch = 2 } };
        pain[0].Yeuch = 27;
        Console.WriteLine(pain[0].Yeuch);
    }
}

это компилируется (глядя на последние 2 строки здесь) как:

L_0026: ldloc.0 <== pain
L_0027: ldc.i4.0 <== 0
L_0028: ldelema Evil <== get the MANAGED POINTER to the 0th element
                           (not the 0th element as a value)
L_002d: ldc.i4.s 0x1b <== 27
L_002f: stfld int32 Evil::Yeuch <== store field

L_0034: ldloc.0 <== pain
L_0035: ldc.i4.0 <== 0
L_0036: ldelema Evil <== get the MANAGED POINTER to the 0th element
                           (not the 0th element as a value)
L_003b: ldfld int32 Evil::Yeuch <== read field
L_0040: call void [mscorlib]System.Console::WriteLine(int32) <== writeline
L_0045: ret 

это не фактическое переговоры на struct значение - никаких копий и т. д.


List<T> имеет нормальный индексатор, который ведет себя как собственность. Доступ идет через функции доступа, и по значению.

T this[int index]
{
    get{return arr[index];}
    set{arr[index]=value;}}
}

массивы являются специальными типами, и их индексатор похож на поле. Как среда выполнения, так и компилятор C# имеют специальные знания о массивах, и это позволяет такое поведение. Вы не можете иметь массив как поведение на пользовательских типах.

к счастью, это редко является проблемой на практике. Поскольку вы используете изменяемые структуры только в rare особые случаи (высокая производительность или собственное взаимодействие), и в тех, которые вы обычно предпочитаете массивы в любом случае из-за их низких накладных расходов.


вы получаете то же самое поведение со свойствами против полей. Вы получаете своего рода ссылку при использовании поля, но копию при использовании свойства. Таким образом, можно записывать в члены полей типа значения, но не в члены свойств типа значения.


я столкнулся с этим, когда я проверял типы лямбда-выражений. Когда лямбда компилируется в дерево выражений, вы можете проверить выражение типа для каждого узла. Оказывается, существует специальный тип узла ArrayIndex на Array индексатор:

Expression<Func<string>> expression = () => new string[] { "Test" }[0];
Assert.Equal(ExpressionType.ArrayIndex, expression.Body.NodeType);

а List индексатор типа Call:

Expression<Func<string>> expression = () => new List<string>() { "Test" }[0];
Assert.Equal(ExpressionType.Call, expression.Body.NodeType);

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


ваша проблема не в List, а в самих структурах.

возьмите это, например:

public class MyStruct
{
    public int x;
    public int y;
}

void Main()
{
    var someStruct = new MyStruct { x = 5, y = 5 };

    someStruct.x = 3;
}

здесь вы не изменяете значение x исходной структуры, вы создаете новый объект с y = y и x = 3. Причина, по которой вы не можете напрямую изменить это со списком, заключается в том, что индексатор списка является функцией (в отличие от индексатора массива), и он не знает, как "установить" новую структуру в списке.

изменить ключевое слово struct to class и вы увидите, что он работает просто отлично (с классами, вы не создаете новый объект каждый раз, когда вы его мутировать).


одним из неудачных ограничений языков .net является то, что у них нет концепции свойства, делающего что-либо, кроме возврата значения, которое затем может использоваться, как считает нужным вызывающий. Было бы очень полезно (и если бы у меня было средство ходатайствовать о языковых функциях, я бы искал это) , если бы были стандартные поддерживаемые компилятором средства предоставления свойств в качестве вызывающих делегатов, такие как:

  MyListOfPoints[4].X = 5;

может быть переведен компилятором в что-то вроде:

  MyListOfPoints.ActOnItem(4, (ref Point it) => it.X = 5);

такой код может быть относительно эффективным и не создавать давления GC, если ActOnItem взял дополнительный параметр ref универсального типа и передал его делегату, который также принял параметр этого типа. Это позволит вызываемой функции быть статической, устраняя необходимость создания замыканий или делегатов для каждого выполнения заключительной функции. Если бы был способ для ActOnItem чтобы принять переменное число общих параметров "ref", это было бы даже возможно обрабатывать такие конструкции, как:

  SwapItems(ref MyListOfPoints[4].X, ref MyListofPoints[4].Y);

с произвольными комбинациями параметров "ref", но даже просто имея возможность обрабатывать случаи, когда свойство было" вовлечено " в левую часть назначения, или функция была вызвана с одним параметром свойства-ish 'ref', было бы полезно.

обратите внимание, что возможность делать вещи таким образом предложит дополнительную выгоду за пределами возможности доступа к полям структур. Это также означает, что объект, предоставляющий свойство, получит уведомление о том, что потребитель сделал с ним (поскольку делегат потребителя вернется). Представьте, например, что у вас есть элемент управления, который показывает список элементов, каждый со строкой и цветом, и вы хотите сделать что-то вроде:

  MyControl.Items(5).Color = Color.Red;

простое заявление для чтения и самый естественный способ чтения для изменения цвета Пятого элемента списка, но попытка сделать такую работу заявления потребует, чтобы объект возвращается Items(5) есть ссылка на MyControl, и отправить ему какое-то уведомление, когда он изменился. Довольно сложно. Напротив, если бы стиль сквозного вызова, указанный выше, был поддержан, такая вещь была бы намного проще. The ActOnItem(index, proc, param) метод будет знать, что после proc вернулся, он должен был бы перерисовать элемент, указанный index. Это имеет некоторое значение, если Items(5) были call-through proc и не поддерживали никакого метода прямого чтения, можно было избежать сценариев например:

  var evil = MyControl.Items(5);
  MyControl.items.DeleteAt(0);
  // Should the following statement affect the item that used to be fifth,
  // or the one that's fifth now, or should it throw an exception?  How
  // should such semantics be ensured?
  evil.Color = Color.Purple;

значение MyControl.Items(5) останется привязанным к MyControl только на время вызова через вовлечение его. После этого это будет просто отдельная ценность.