这本C#字典的意义是什么&燃气轮机;优化?
我用的是C#4.0。Visual Studio中的“优化代码”已启用 在类中考虑以下代码:这本C#字典的意义是什么&燃气轮机;优化?,c#,C#,我用的是C#4.0。Visual Studio中的“优化代码”已启用 在类中考虑以下代码: Dictionary<int, int> dictionary = new Dictionary<int, int>(); public void IncrementDictionary(int key) { if (!dictionary.ContainsKey(key)) { dictionary[key] = 1; } else {
Dictionary<int, int> dictionary = new Dictionary<int, int>();
public void IncrementDictionary(int key) {
if (!dictionary.ContainsKey(key)) {
dictionary[key] = 1;
} else {
dictionary[key]++;
}
}
注意:在实际的生产代码中,使用此选项,优化器/编译器还会创建:int-key2=key
并在最后一行中使用键2
好的,var
已被Dictionary
替换,这是预期的。而if
语句被简化为添加return
,而不是使用else
但是为什么要对原始词典进行新的引用呢?我猜这可能是为了避免出现竞争条件,如果您有:
dictionary[i]=dictionary[i]+1
它不是原子的。指定给的词典
在您获得值并递增后可能会更改
想象一下这个代码:
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
}
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_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.get_项
IL_0011:ldc.i4.1
IL_0012:添加
IL_0013:callvirt System.Collections.Generic.Dictionary.set_项
IL_0018:没有
IL_0019:ret
我不完全确定(我不是IL wiz),但我认为dup
调用本质上是将堆栈上的同一字典引用加倍,因此不管如何,get和set调用都将指向同一字典。也许这就是ILSpy将其表示为C#代码的方式(至少在行为上是相同的)。我想。如果我错了,请纠正我,因为,就像我说的,我还不知道我喜欢我的手背
编辑:必须运行,但最后的要点是:
++
和++=
不是原子操作,实际上执行的指令比C#中描述的要复杂得多。因此,为了确保在同一个字典实例上执行每个get/increment/set步骤(正如您从C#代码中期望和要求的那样),需要对字典进行本地引用,以避免两次运行字段“get”操作,这可能导致指向新实例。ILSpy如何描述索引+=操作所涉及的所有工作取决于它。您的编辑弄乱了您试图显示的内容,但原因是字典[key]++
临时复制字典
是因为它无法知道字典
的索引getter是否会更改字段字典
。该规范指出,即使在索引get期间更改了字段字典
,索引put仍将在同一对象上执行
顺便说一句,.net确实应该(现在仍然应该)提供一种方法,通过这种方法类可以将属性公开为
ref
。Dictionary
可以提供方法ActOnValue(TKey-key,ActionByRef-action)
和ActOnValue(TKey-key,ActionByRef-action,ref-TParam-param)
(假设ActionByRef
类似于action
,但参数声明为ref
)。如果是这样,就可以对集合对象执行读-修改-写操作,而无需索引到集合中两次。不幸的是,没有以这种方式公开属性的标准约定,也没有任何语言支持。重要提示:没有创建第二个字典。相反,第二次引用了现有的字典。@Jon:是的,我错了。你发布的ILSpy不是CIL代码中发生的事情的精确表示。这可能是CIL-to-C反编译器中的错误或缺陷。你能发布CIL以便我们确定吗?@SimpleCoder用Reflector反编译了它,看起来几乎一样,但它保留了其他
@SimpleCoder我想你可能混淆了你的代码块:它们都显示了ILSpy结果。:)现在比较起来有点难!不,dictionary2
仍指向原始字典。对Increment()
控制dictionary2
:它是对原始字典的本地引用。您关于不两次解析属性getter的观点非常相关,实际上不是优化,而是调用dictionary[key]++时直观地期望的结果(即,如果属性有一些副作用,它应该只发生一次)…我们开始增加值…
实际上,不会(dictionary2=dictionary)
执行之前,因为它在括号中?@ChrisSinclair:即使在单线程的情况下,编译器也无法知道字典
“index get”函数是否会更改任何对象的任何字段(包括this.Dictionary
)。这不完全是“竞争条件”,但这是个问题。好的,谢谢。你说的很有道理——我从来没有想过。
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;
}
public void IncrementDictionary()
{
dictionary[0]++;
}
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