在 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++;
考虑以下 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
var d = new Dictionary<int, int>();
d[0] = 0;
d[0]++;
在功能上等同于...
int i = 0;
i++;