В С#, почему работает словарь [0] ++?
Рассмотрим следующий код С#:
var d = new Dictionary<int, int>();
d[0] = 0;
d[0]++;
Каково значение d [0] после выполнения этого кода? Я ожидал бы d [0] == 0, потому что свойство Item словаря < > возвращает тип значения int
, предположительно в стеке, который затем увеличивается. Удивительно, однако, когда вы действительно запускаете этот код, вы найдете d [0] == 1.
Приведенный выше пример ведет себя так, как если бы индексчик возвращал ссылочный тип, но теперь рассмотрим следующее:
var d = new Dictionary<int, int>();
d[0] = 0;
var a = d[0];
a++;
Каково значение d [0] после выполнения этого кода? На этот раз мы получим d [0] == 0, как и ожидалось, поэтому индексатор определенно не возвращает ссылку.
Кто-нибудь знает, почему мы видим это поведение?
Ответы
Ответ 1
Спецификация С# 7.6.9 Операторы приращения и уменьшения постфикса:
Обработка времени выполнения операции увеличения или уменьшения постфиксации формы x ++ или x-- состоит из следующих шагов:
- если x классифицируется как доступ к свойствам или индексам:
- Выражение экземпляра (если x не является статическим) и список аргументов (если x является доступом индексатора), связанный с x, оцениваются, а результаты используются в последующих вызовах get и set accessor.
- Вызывается get accessor из x и возвращается возвращаемое значение.
- Выбранный оператор вызывается с сохраненным значением x в качестве аргумента.
- Аксессор доступа к объекту x запускается со значением, возвращаемым оператором в качестве аргумента значения.
- Сохраненное значение x становится результатом операции.
И это на самом деле не имеет никакого отношения к типу значений по сравнению с ссылочным типом семантики, так как --
и ++
не должны изменять экземпляр, а возвращать новый экземпляр с новым значением.
public static class Test {
public static void Main() {
TestReferenceType();
TestValueType();
}
public static void TestReferenceType() {
var d=new Dictionary<int,BoxedInt>();
BoxedInt a=0;
d[0]=a;
d[0]++;
d[1]=2;
BoxedInt b=d[1];
b++;
Console.WriteLine("{0}:{1}:{2}:{3}",a,d[0],d[1],b);
}
public static void TestValueType() {
var d=new Dictionary<int,int>();
int a=0;
d[0]=a;
d[0]++;
d[1]=2;
int b=d[1];
b++;
Console.WriteLine("{0}:{1}:{2}:{3}",a,d[0],d[1],b);
}
public class BoxedInt {
public int Value;
public BoxedInt(int value) {
Value=value;
}
public override string ToString() {
return Value.ToString();
}
public static implicit operator BoxedInt(int value) {
return new BoxedInt(value);
}
public static BoxedInt operator++(BoxedInt value) {
return new BoxedInt(value.Value+1);
}
}
}
Оба метода тестирования будут печатать ту же строку 0:1:2:3
. Как вы можете видеть, даже с типом ссылки вам нужно вызвать set accessor, чтобы наблюдать обновленное значение в словаре.
Ответ 2
Ваш код работает таким образом, потому что индексщик d
возвращает < значение int
(который является тип значения). Ваш код в основном идентичен этому:
var d = new Dictionary<int, int>();
d[0] = 0;
d[0] = d[0] + 1; // 1. Access the indexer of `d`
// 2. Increment it (returned another int, which is of course a value type)
// 3. Store the new int to d[0] again (d[0] now equals to 1)
d[0]
возвращен 0
в вашем втором примере кода из-за семантики значения, в частности, этой строки:
var a = d[0]; // a is copied by value, not a **reference** to d[0],
// so they are two separate integers from here
a++; // a is incremented, but d[0] is not, because they are **two** separate integers
Я нашел Jon Skeet объяснение о различии ссылочных типов и типов значений чрезвычайно полезным.
Ответ 3
Второй пример не будет работать так, как вы думаете, потому что int не является ссылочным типом.
Когда вы берете значение из словаря, вы выделяете новую переменную в этом случае a
.
Первый пример скомпилирован:
d[0] = d[0] + 1;
Однако второй пример скомпилирован:
int a = d[0];
a++;
Продумать это. Индексаторы работают подобно свойствам и свойствам являются функции getter/setter. В этом случае d[0] =
вызовет свойство setter индексатора. d[0] + 1
вызовет свойство getter индексатора.
int не является ссылочным объектом. Второй пример будет работать с классами, но не целыми числами. Свойства также не возвращают ссылку на переменную. Если вы изменяете возвращаемое значение из индексатора, вы изменяете совершенно новую переменную, а не ту, которая хранится в словаре. Поэтому первый пример работает так, как предполагалось, но второй - нет.
В первом примере обновляется индексатор снова, в отличие от второго.
Второй пример будет работать как первый, если вы сделали это так.
int a = d[0];
a++;
d[0] = a;
Это скорее концепция понимания свойств и индексаторов.
class MyArray<T>
{
private T[] array;
public MyArray(T[] _array)
{
array = _array;
}
public T this[int i]
{
get { return array[i];
set { array[i] = value; }
}
}
Теперь рассмотрим следующее для нормального массива
int[] myArray = new int[] { 0, 1, 2, 3, 4 };
int a = myArray[2]; // index 2 is 1
// a is now 1
a++; // a is now 2
// myArray[2] is still 1
Вышеприведенный код аналогичен тому, что int не является ссылочным типом, а индексы не возвращают ссылку, поскольку она не передается по ref, а как возвращаемое значение нормальной функции.
Теперь рассмотрим следующее для MyArray
var myArray = new MyArray<int>(new int[] { 0, 1, 2, 3, 4 });
int a = myArray[2]; // index 2 is 1
// a is now 1
a++; // a is now 2
// myArray[2] is still 1
Вы ожидали, что myArray [2] создаст ссылку на myArray [2]?
Вы не должны. То же самое касается Dictionary
Это индекс для Dictionary
// System.Collections.Generic.Dictionary<TKey, TValue>
[__DynamicallyInvokable]
public TValue this[TKey key]
{
[__DynamicallyInvokable]
get
{
int num = this.FindEntry(key);
if (num >= 0)
{
return this.entries[num].value;
}
ThrowHelper.ThrowKeyNotFoundException();
return default(TValue);
}
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
set
{
this.Insert(key, value, false);
}
}
Я думаю, теперь должно быть ясно, почему первый работает так, как это делает, и почему вторая делает тоже.
Ответ 4
The great thing about generics like the Dictionary<TKey, TValue > that you use in your example is that a specialized Dictionary is generated using the type parameters that you specify. When you declare a Dictionary<int, int > the 'key' and 'value' that make up the KeyValuePair used by the Dictionary are literal 'int' types a.k.a value types. The 'int' values are not boxed. So...
var d = new Dictionary<int, int>();
d[0] = 0;
d[0]++;
Функционально эквивалентно...
int i = 0;
i++;