Подтвердить что ты не робот

В С#, почему работает словарь [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, как и ожидалось, поэтому индексатор определенно не возвращает ссылку.

Кто-нибудь знает, почему мы видим это поведение?

4b9b3361

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