编译器会优化集合初始化吗?

8

编译器会优化这段代码还是集合会在每次方法调用后被初始化?

private string Parse(string s)
{
    var dict = new Dictionary<string, string>
    {
        {"a", "x"},
        {"b", "y"}
    };

    return dict[s];
}

如果答案是否定的,我建议使用以下解决方案:在C#中创建常量字典

嗯,虚拟机可能会在方法作用域之后处理它,尽管这是不确定的。 - DevEstacion
你能解释一下为什么你认为编译器会对此进行优化,以及如何进行优化吗? - CodeCaster
1
@CodeCaster 为什么:编译器可能依赖于特定的 .NET 类,这种情况很容易被检测到。如何做:将其改为静态,例如。 - astef
3个回答

13
当遇到这样的问题时,如果您不确定答案是什么,查看“引擎盖”通常是个好主意。
启用优化器后编译器生成的是 IL 如下:
Parse:
IL_0000:  newobj      System.Collections.Generic.Dictionary<System.String,System.String>..ctor
IL_0005:  stloc.1     // <>g__initLocal0
IL_0006:  ldloc.1     // <>g__initLocal0
IL_0007:  ldstr       "a"
IL_000C:  ldstr       "x"
IL_0011:  callvirt    System.Collections.Generic.Dictionary<System.String,System.String>.Add
IL_0016:  ldloc.1     // <>g__initLocal0
IL_0017:  ldstr       "b"
IL_001C:  ldstr       "y"
IL_0021:  callvirt    System.Collections.Generic.Dictionary<System.String,System.String>.Add
IL_0026:  ldloc.1     // <>g__initLocal0
IL_0027:  stloc.0     // dict
IL_0028:  ldloc.0     // dict
IL_0029:  ldarg.1     
IL_002A:  callvirt    System.Collections.Generic.Dictionary<System.String,System.String>.get_Item
IL_002F:  ret    

正如您所看到的,它每次调用newobj来分配Dictionary<K,V>,加载本地变量并每次调用Dictionary.Add在两个本地变量上(这是语法糖等效于调用Add)。 它没有深入了解类型以缓存对象的创建。


2
此检查的结果可能随时更改。最好的方法是假设编译器永远不允许这样做。 - usr
@usr 你说得对,这就是为什么我说编译器没有与类型的亲密了解。虽然说“检查可能随时更改”是一个相当普遍的陈述,但对于下面的任何答案都可能成立。 - Yuval Itzchakov

6
不会的。编译器本身并不知道Dictionary是什么,对于它来说,Dictionary只是一个普通的类,所以它不知道在这种特殊情况下可以重用实例。

以下是正确的做法:
public class Something
{
    private static readonly Dictionary<string, string> _dict = new Dictionary<string, string>
    {
        {"a", "x"},
        {"b", "y"}
    }

    private string Parse(string s)
    {
        return _dict[s];
    }
}

这种方法有效是因为您知道对象的作用,并且知道它永远不会被修改。

请记住以下语法:

var dict = new Dictionary<string, string>
{
    {"a", "x"},
    {"b", "y"}
}

这只是一种语法糖,其实就等同于:

var dict = new Dictionary<string, string>();
dict.Add("a", "x");
dict.Add("b", "y");

使这个方法适用于任何类,唯一的要求是该类需要:
  • 实现 IEnumerable
  • 有一个公共的 Add 方法。
你建议使用 switch 语句,但是 Dictionary 的方法可以更加灵活。例如,你可能想要使用不同的相等比较器(如 StringComparer.OrdinalIgnoreCase),最好让 Dictionary 使用合适的比较器来处理,而不是像 switch(value.ToLowerInvariant()) 这样的代码。

在许多情况下,编译器依赖于特定的.NET类。为什么不使用Dictionary呢? - astef
@astef为什么应该这样做呢?Dictionary有什么特别之处吗?为什么不用HashSet,或者OrderedDictionary,或者ConditionalWeakTable呢?这将是无穷无尽的。最好只有一些特殊的已知情况(例如Task<T>)。 - Lucas Trzesniewski
Add 方法有什么特别之处,以至于要用初始化器来替换它?老实说,我不知道。只是在问一下 :) - astef
@astef 这只是为了开发者方便而约定的惯例。当编译器看到一个初始化程序时,它会用 Add 调用替换它,但方法本身并没有什么特别之处。它可以做任何事情。编译器不关心,它只为你提供语法糖。 - Lucas Trzesniewski

2

不,就目前而言,C# 不会以任何方式“优化”此代码 - 而且我严重怀疑这种情况是否会发生。

虽然你可能会认为这段特定的代码可以进行优化,但实际上它是一个边界情况,不太容易在编译时确定。举个反例 - 如果你的字符串文字之一是另一个类的成员呢?如果你有一个自定义字典(现有字典类没有特殊处理),它的构造函数做了一些奇怪的事情呢?


“如果你的一个字符串字面量是另一个类的成员,会有什么问题吗?” - 我看不出有什么问题,你能解释一下吗?你提到了自定义字典,但普通字典呢?它已经与编译器耦合在一起了(请参见初始化器)。 - astef
想象一下,如果你有new Dictinary<String, String> { { "x", SOME_CLASS.MEMBER } }。现在当SOME_CLASS.MEMBER更改时,对Parse的后续调用应该使用新值——因此无法进行优化。 - decPL
我认为你误解了C#中的引用类型。可能的优化是将字典作为静态字段 - 它只会被初始化一次。在这种情况下,你看到什么问题? - astef
我需要吗?那么这里有一个快速而简单的示例来说明我的观点:http://pastebin.com/pYvXe1bc - decPL
现在我明白你的意思了。当然,这种运行时值定义的情况无法进行优化。不过,它可以很容易地被检测到。 - astef
我不同意 - 如果值是只读的呢?如果它是一个返回const字面量的固定lambda表达式呢?显然这是可行的 - 没有什么魔法,但有很多情况需要考虑 - 只是为了做一些对普通开发人员来说不直观的事情。正如我之前提到的 - 编译器不能试图超越开发人员的思考 - 如果你在每个方法调用中实例化一个字典,那么你必须有你的理由。 - decPL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接