Индексаторы в списке против массива
Как индексы определены в списке и массивах.
List<MyStruct> lists=new List<MyStruct>();
где MyStruct
- это структура. Теперь рассмотрим
MyStruct[] arr=new MyStruct[10];
arr[0]
дает ссылку на первый элемент структуры. Но lists[0]
дает мне его копию.
Есть ли причина, почему это делается так.
Кроме того, поскольку Int32
представляет собой структуру List<Int32> list1 =new List<Int32>();
, как мне можно получить доступ к list1[0]
или назначить list1[0]=5
, где это невозможно сделать lists[0]._x=5
Ответы
Ответ 1
Хотя они выглядят одинаково, индексатор массива и индексный указатель выполняют совершенно разные вещи.
Индексатор List<T>
объявляется как свойство с параметром:
public T this[int index] { get; set; }
Это компилируется в методы get_Item
и set_Item
, которые вызываются как любой другой метод при доступе к параметру.
Индексатор массива имеет прямую поддержку внутри CLR; существует конкретная инструкция IL ldelema
(адрес элемента нагрузки) для получения управляемого указателя на n-й элемент массива. Этот указатель затем может использоваться любой из других инструкций IL, которые принимают указатель на прямое изменение объекта по этому адресу.
Например, инструкция stfld
(значение поля сохранения) может принимать управляемый указатель, указывающий экземпляр 'this', чтобы сохранить это поле, или вы можете использовать указатель для вызова методов непосредственно на предмет в массиве.
В языке С# индексный индекс массива возвращает переменную, но индексный индекс возвращает значение.
Ответ 2
Конечная точка:
lists[0]._x=5
- это просто повторение вашего предыдущего пункта:
arr[0]
дает ссылку на первый элемент структуры. Но lists[0]
дает мне его копию.
Если вы отредактировали его копию, это изменение будет потеряно в эфир, т.е.
var throwawayCopy = lists[0];
throwawayCopy._x = 5;
// (and never refer to throwawayCopy again, ever)
Поскольку это почти наверняка не то, что вы намеревались, компилятор не позволяет вам. Однако изменчивые структуры являются злыми. Лучший вариант здесь - не использовать mutable structs. Они кусают.
Сняв этот уровень на простом, но конкретном примере:
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
Он никогда не разговаривает с конструкцией как ценность - без копий и т.д.
Ответ 3
List<T>
имеет нормальный индекс, который ведет себя как свойство. Доступ осуществляется через функции доступа, и они являются значениями.
T this[int index]
{
get{return arr[index];}
set{arr[index]=value;}}
}
Массивы являются специальными типами, а их индекеры похожи на поля. Как среда выполнения, так и компилятор С# обладают специальным знанием массивов и позволяют это поведение. Вы не можете иметь такой массив, как поведение в пользовательских типах.
К счастью, это редко бывает проблемой на практике. Поскольку вы используете только измененные структуры в редких особых случаях (высокопроизводительный или собственный интерфейс), и в тех случаях, когда вы обычно предпочитаете массивы из-за их низких накладных расходов.
Вы получаете то же поведение со свойствами против полей. При использовании поля вы получаете ссылку, но копируете, когда используете свойство. Таким образом, вы можете писать членам полей типа значений, но не членам свойств типа значения.
Ответ 4
Я столкнулся с этим, когда я изучал типы выражений лямбда. Когда лямбда скомпилирована в дерево выражений, вы можете проверить тип выражения для каждого node. Оказывается, существует специальный тип node 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);
Это просто для иллюстрации того, что мы можем рассуждать о базовой архитектуре с помощью лямбда-выражений.
Ответ 5
Ваша проблема заключается не в 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
на class
, и вы увидите, что он работает просто отлично (с классами вы не создаете новый объект каждый раз, когда вы его мутируете).
Ответ 6
Одним из неудачных ограничений .net-языков является то, что у них нет понятия свойства, делающего что-либо, кроме возвращающего значение, которое затем можно использовать, однако вызывающий считает нужным. Было бы очень полезно (и если бы у меня было средство ходатайствовать о языковых возможностях, я бы искал это), если бы были стандартные средства, поддерживающие компилятор, для выставления свойств в качестве делегатов-делегатов, таких как:
MyListOfPoints[4].X = 5;
может быть переведен компилятором во что-то вроде:
MyListOfPoints.ActOnItem(4, (ref Point it) => it.X = 5);
Такой код может быть относительно эффективным и не создавать никакого давления GC, если ActOnItem
взял дополнительный параметр ref generic type и передал его делегату, который также принял параметр этого типа. Это позволит вызываемой функции быть статичной, устраняя необходимость создания закрытий или делегатов для каждого выполнения закрывающей функции. Если бы был ActOnItem
, чтобы принять переменное число общих параметров ref, было бы возможно обрабатывать такие конструкции, как:
SwapItems(ref MyListOfPoints[4].X, ref MyListofPoints[4].Y);
с произвольными комбинациями параметров ref, но даже имея возможность обрабатывать случаи, когда свойство было "вовлечено в" слева от назначения, или вызывалась функция с единственным свойством-ish 'ref' параметр. Было бы полезно.
Обратите внимание, что возможность делать это таким образом обеспечит дополнительную выгоду, выходящую за рамки возможности доступа к полям структур. Это также означает, что объект, подвергающий объект, получит уведомление о том, что потребитель был сделан с ним (поскольку делегат-потребитель вернется). Представьте себе, например, что у одного есть элемент управления, который показывает список элементов, каждый со строкой и цветом, и один хочет иметь возможность сделать что-то вроде:
MyControl.Items(5).Color = Color.Red;
Легкое утверждение для чтения и наиболее естественный способ изменить цвет пятого элемента списка, но для выполнения такой работы с инструкцией потребуется, чтобы объект, возвращаемый Items(5)
, имел ссылку на MyControl
, и отправьте ему какое-то уведомление при его изменении. Довольно сложно. Напротив, если бы поддерживался стиль переадресации, указанный выше, такая вещь была бы намного проще. Метод ActOnItem(index, proc, param)
знал бы, что как только proc
вернется, ему придется перерисовать элемент, указанный index
. Некоторое значение, если Items(5)
был сквозным процессом и не поддерживал какой-либо метод прямого чтения, можно было избежать таких сценариев, как:
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 fifth now, or should it throw an exception? How
// should such semantics be ensured?
evil.Color = Color.Purple;
Значение MyControl.Items(5)
останется привязанным к MyControl
только в течение продолжительного времени, связанного с его вызовом. После этого это будет просто отстраненное значение.