我猜这可能是为了避免竞争条件,如果你有:
dictionary[i] = dictionary[i] + 1
它不是原子的。在您获得值并递增后dictionary
,分配给的对象可能会发生变化。
想象一下这段代码:
public Dictionary<int, int> dictionary = new Dictionary<int, int>();
public void Increment()
{
int newValue = dictionary[0] + 1;
//meanwhile, right now in another thread: dictionary = new Dictionary<int, int>();
dictionary[0] = newValue; //at this point, "dictionary" is actually pointing to a whole new instance
}
使用他们拥有的局部变量赋值,它看起来更像是为了避免这种情况:
public void IncrementFix()
{
var dictionary2 = dictionary;
//in another thread: dictionary = new Dictionary<int, int>();
//this is OK, dictionary2 is still pointing to the ORIGINAL dictionary
int newValue = dictionary2[0] + 1;
dictionary2[0] = newValue;
}
请注意,它并不完全满足所有线程安全要求。例如,在这种情况下,我们开始增加值,但dictionary
类上的引用已更改为全新的实例。但是如果您需要更高级别的线程安全,那么您需要实现自己的主动同步/锁定,这通常超出了编译器优化的范围。然而,据我所知,这并没有真正给处理增加任何重大影响(如果有的话),并且避免了这种情况。如果dictionary
是财产,情况尤其如此不是您示例中的字段,在这种情况下,不解析属性获取器两次肯定是一种优化。(无论如何,您的实际代码是否使用字典的属性而不是您发布的字段?)
编辑:好吧,对于一个简单的方法:
public void IncrementDictionary()
{
dictionary[0]++;
}
从 LINQPad 报告的 IL 是:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld UserQuery.dictionary
IL_0007: dup
IL_0008: stloc.0
IL_0009: ldc.i4.0
IL_000A: ldloc.0
IL_000B: ldc.i4.0
IL_000C: callvirt System.Collections.Generic.Dictionary<System.Int32,System.Int32>.get_Item
IL_0011: ldc.i4.1
IL_0012: add
IL_0013: callvirt System.Collections.Generic.Dictionary<System.Int32,System.Int32>.set_Item
IL_0018: nop
IL_0019: ret
我不完全确定(我不是 IL wiz),但我认为该dup
调用本质上将堆栈上的相同字典引用加倍,因此无论 get 和 set 调用都指向同一个字典。也许这正是 ILSpy 将其表示为 C# 代码的方式(至少与行为相同)。我认为。如果我错了,请纠正我,因为就像我说的那样,我还不知道我的手背上的 IL。
编辑:必须运行,但它的最后要点是:++
并且+=
不是原子操作,实际上执行指令比 C# 中描述的要复杂得多。因此,为了确保每个 get/increment/set 步骤都在同一个字典实例上执行(正如您在 C# 代码中所期望和要求的那样),对字典进行了本地引用以避免运行字段“ get" 操作两次,这可能导致指向一个新实例。ILSpy 如何描述索引 += 操作所涉及的所有工作取决于它。