Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In C#, why does dictionary[0]++ work?

Consider the following C# code:

var d = new Dictionary<int, int>();
d[0] = 0;
d[0]++;

What is the value of d[0] after this code executes? I would expect d[0] == 0, because the Item property of Dictionary<> returns a value type int, presumably on the stack, which then gets incremented. Surprisingly, however, when you actually run this code you find d[0] == 1.

The example above behaves as if the indexer is returning a reference type, but now consider the following:

var d = new Dictionary<int, int>();
d[0] = 0;
var a = d[0];
a++;

What is the value of d[0] after this code executes? This time we get d[0] == 0 as expected, so the indexer is definitely not returning a reference.

Does anybody know why we see this behavior?

like image 739
Mike Avatar asked Feb 02 '15 01:02

Mike


People also ask

What does != Mean in C?

The not-equal-to operator ( != ) returns true if the operands don't have the same value; otherwise, it returns false .

What is operators in C?

C operators are one of the features in C which has symbols that can be used to perform mathematical, relational, bitwise, conditional, or logical manipulations. The C programming language has a lot of built-in operators to perform various tasks as per the need of the program.

What is the use of in C?

In C/C++, the # sign marks preprocessor directives. If you're not familiar with the preprocessor, it works as part of the compilation process, handling includes, macros, and more.


3 Answers

C# Specification 7.6.9 Postfix increment and decrement operators:

The run-time processing of a postfix increment or decrement operation of the form x++ or x-- consists of the following steps:

  • if x is classified as a property or indexer access:
    • The instance expression (if x is not static) and the argument list (if x is an indexer access) associated with x are evaluated, and the results are used in the subsequent get and set accessor invocations.
    • The get accessor of x is invoked and the returned value is saved.
    • The selected operator is invoked with the saved value of x as its argument.
    • The set accessor of x is invoked with the value returned by the operator as its value argument.
    • The saved value of x becomes the result of the operation.

And this actually have nothing to do with value type vs. reference type semantic, as -- and ++ should not change instance but return new instance with new value.

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);
        }
    }
}

Both test method will print same string 0:1:2:3. As you can see, even with reference type you have to call set accessor to observe updated value in the dictionary.

like image 147
user4003407 Avatar answered Oct 17 '22 18:10

user4003407


Your code works this way because the the indexer of d returns the value reference of the int (which is a value type). Your code is basically identical to this:

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] returned 0 in your second code example is because of value type semantics, specifically this line:

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

I found Jon Skeet's explanation on the difference of reference types and value types extremely helpful.

like image 5
rexcfnghk Avatar answered Oct 17 '22 18:10

rexcfnghk


The second example will not work as you think it will, because int is not a reference type. When you take the value out of the dictionary you're allocating a new variable in this case a.

The first example is compiled to this:

d[0] = d[0] + 1;

However the second example is compiled to this:

int a = d[0];
a++;

To elaborate on this. Indexers operates like properties and properties are getter/setter functions. In this case d[0] = will call the setter property of the indexer. d[0] + 1 will call the getter property of the indexer.

An int is not a reference object. The second example would work with classes, but not integers. Properties does not return a reference to the variable either. If you modify the return value from the indexer then you're modifying a completely new variable and not actually the one stored in the dictionary. That's why the first example works as intended, but the second doesn't.

The first example updates the indexer again, unlike the second.

The second example would work like the first if you did it like this.

int a = d[0];
a++;
d[0] = a;

This is more of a concept to understand properties and indexers.

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; }
    }
}

Now consider the following for a normal array

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

The code above is like that, because int is not a reference type and indexers does not return a reference since it's not passed by ref, but as a normal function return value.

Now consider the following for 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

Did you expect myArray[2] to create a reference to myArray[2]? You shouldn't. The same goes for Dictionary

This is the indexer for 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);
    }
}

I think it should be clear by now, why the first works like this does and why the second does too.

like image 2
Bauss Avatar answered Oct 17 '22 16:10

Bauss