在 C# 中,为什么 dictionary[0]++ 有效?

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

考虑以下 C# 代码:

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

这段代码执行后d[0]的值是多少?我希望 d[0] == 0,因为 Dictionary<> returns 的 Item 属性 a value type int,大概在堆栈,然后递增。然而,令人惊讶的是,当您实际 运行 这段代码时,您会发现 d[0] == 1.

上面的示例表现得好像索引器正在返回引用类型,但现在考虑以下内容:

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

这段代码执行后d[0]的值是多少?这次我们得到了预期的 d[0] == 0,所以索引器肯定没有返回引用。

有人知道我们为什么会看到这种行为吗?

您的代码以这种方式工作,因为 d returns 的索引器 value reference[= int 的 28=](这是一个值类型)。您的代码与此基本相同:

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 关于引用类型和值类型的区别的 explanation 非常有帮助。

C# 规范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.

这实际上与值类型与引用类型语义无关,因为 --++ 不应更改实例,但 return 具有新值的新实例。

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 访问器来观察字典中的更新值。

第二个例子不会像你想的那样工作,因为 int 不是引用类型。 在这种情况下,当您从字典中取出值时,您正在分配一个新变量 a.

第一个例子编译成这样:

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

但是第二个例子编译成这样:

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

对此进行详细说明。索引器的操作类似于属性,而属性是 getter/setter 函数。在这种情况下 d[0] = 将调用索引器的 setter 属性。 d[0] + 1 将调用索引器的 getter 属性。

int 不是引用对象。第二个示例适用于 类,但不适用于整数。属性也不 return 对变量的引用。如果您修改索引器中的 return 值,那么您正在修改一个全新的变量,而不是实际存储在字典中的变量。这就是第一个示例按预期工作而第二个示例不工作的原因。

与第二个不同,第一个示例再次更新索引器。

如果您这样做,第二个示例将像第一个示例一样工作。

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 不是引用类型并且索引器不是 return 引用,因为它不是由 ref 传递的,而是作为普通函数 return 值。

现在考虑以下 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);
    }
}

我想现在应该很清楚了,为什么第一个是这样的,为什么第二个也是这样。

您在示例中使用的 Dictionary 等泛型的优点在于,使用您指定的类型参数生成专用字典。当您声明 Dictionary 时,构成 Dictionary 使用的 KeyValuePair 的 'key' 和 'value' 是文字 'int' 类型 a.k.a 值类型。 'int' 值未装箱。所以...

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

在功能上等同于...

int i = 0;
i++;