【问题标题】:foreach Dictionary<>.Values or foreach Dictionary<>foreach Dictionary<>.Values 或 foreach Dictionary<>
【发布时间】:2015-09-09 14:42:56
【问题描述】:

我想知道这两种在C#中迭代Dictionary集合的方式的细节:

Dictionary<X, Y> xydic = new Dictionary<X, Y>();

样式一:

foreach (Y y in xydic.Values) { use y }

样式二:

foreach (var it in xydic) { Y y = it.Value; use y... }

多年来,我一直是 C++ 开发人员(现在我在一个 C# 项目中工作),我不知道 Dictionary 集合如何工作、内存布局或元素如何迭代的细节,所以我我想知道:

xydic.Values 创建一个临时的List&lt;Y&gt;?我在the documentation 中没有看到任何有关创建临时列表的信息。

如果创建了一个临时列表,这是否意味着该集合被迭代了两次:第一次创建List&lt;Y&gt;,第二次迭代列表本身?

如果上述问题的答案是肯定的,那么第二种样式应该更有效,使第一种样式几乎无用,所以我认为我在某种程度上应该是错误的。

我觉得这个问题应该在某个地方得到回答,但我找不到答案。

【问题讨论】:

  • 在 C# 中,您不必担心内存问题。垃圾收集器将为您完成这项工作。如果你真的想看看Dictionary 类是如何执行.Values 或其他函数的,你可以用IlSpy app 打开它,然后看代码
  • 您可以查看source 以了解其具体实现方式。
  • @Jasper 我更关心的是Values 是否创建了一个临时列表(这意味着该集合被迭代了两次)。当我提到我不知道内存布局时,如果我知道,我可以自己推断出答案:)
  • @PaperBirdMaster 你为什么不简单地检查来源 - referencesource.microsoft.com/#mscorlib/system/collections/…
  • 创建一个临时列表(意味着集合被迭代两次):) :) 典型的 C++ 问题 :) :) 在 C# 中你有垃圾收集器 - 不要过多考虑内存分配 - 只是让 GC 完成它的工作

标签: c# foreach iteration containers


【解决方案1】:

检索Dictionary&lt;,&gt;.Values 属性是一个O(1) 操作(documented)。嵌套类型Dictionary&lt;,&gt;.ValueCollection 是字典的简单包装器,因此在创建它时没有迭代。

在调用GetEnumerator() 时,您会得到一个嵌套的、嵌套的Dictionary&lt;,&gt;.ValueCollection.Enumerator 结构的实例。它直接通过Dictionary&lt;,&gt;private数组entries访问条目。

你可以看到source code

因此,您上面的“样式一”是一种很好且清晰的做事方式,没有性能开销。

请注意,获取值的顺序是任意的。您不知道底层数组 entries 是如何组织的,一旦 Dictionary&lt;,&gt; 在您开始 foreaching 之前进行了多次插入和删除。

但是,“样式一”和“样式二”得到的顺序是一样的;两者都以相同的方式访问Dictionary&lt;,&gt; 的私有entries 数组。

【讨论】:

    【解决方案2】:

    Values 没有创建List&lt;T&gt;,没有。它甚至没有将整个值集拉到一个单独的数据结构中。它所做的只是创建一个可以迭代值的枚举器。当你直接迭代字典时,它会做同样的事情;不同之处在于,不是为每对对象构造一个KeyValuePair 对象,而是只给你这对对象的一半。除此之外,迭代过程都是一样的。

    【讨论】:

      【解决方案3】:

      所有 3 种方法(KeysValues 和字典迭代)的行为相同 - 迭代字典中项目的内部集合。没有创建额外的列表/数组。

      唯一的“额外”工作是检查字典是否在迭代开始后被修改(整数比较)。

      您可以在reference source查看确切的详细信息

      【讨论】:

        【解决方案4】:

        在这两种情况下,您都会得到 iterator 而不是临时集合。 Iterator 是使用内部状态机来记住当前项是什么,并使用 MoveNext 方法获取下一个项。

        如果您查看 IL 代码,您会看到更多关于 foreach 内部发生的细节。这是

        的 IL
        void Main()
        {
            var dictionary = new Dictionary<int, string> { { 1, "one" }, { 2, "two" }};
        
            foreach (var item in dictionary.Values)
            {
                Console.WriteLine(item);
            }
        }
        

        IL 代码:

        IL_0000:  nop         
        IL_0001:  newobj      System.Collections.Generic.Dictionary<System.Int32,System.String>..ctor
        IL_0006:  stloc.1     
        IL_0007:  ldloc.1     
        IL_0008:  ldc.i4.1    
        IL_0009:  ldstr       "one"
        IL_000E:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.String>.Add
        IL_0013:  nop         
        IL_0014:  ldloc.1     
        IL_0015:  ldc.i4.2    
        IL_0016:  ldstr       "two"
        IL_001B:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.String>.Add
        IL_0020:  nop         
        IL_0021:  ldloc.1     
        IL_0022:  stloc.0     // dictionary
        IL_0023:  nop         
        IL_0024:  ldloc.0     // dictionary
        IL_0025:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.String>.get_Values
        IL_002A:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.String>+ValueCollection.GetEnumerator
        IL_002F:  stloc.2     
        IL_0030:  br.s        IL_0043
        IL_0032:  ldloca.s    02 
        IL_0034:  call        System.Collections.Generic.Dictionary<System.Int32,System.String>+ValueCollection+Enumerator.get_Current
        IL_0039:  stloc.3     // item
        IL_003A:  nop         
        IL_003B:  ldloc.3     // item
        IL_003C:  call        System.Console.WriteLine
        IL_0041:  nop         
        IL_0042:  nop         
        IL_0043:  ldloca.s    02 
        IL_0045:  call        System.Collections.Generic.Dictionary<System.Int32,System.String>+ValueCollection+Enumerator.MoveNext
        IL_004A:  brtrue.s    IL_0032
        IL_004C:  leave.s     IL_005D
        IL_004E:  ldloca.s    02 
        IL_0050:  constrained. System.Collections.Generic.Dictionary<,>+ValueCollection.Enumerator
        IL_0056:  callvirt    System.IDisposable.Dispose
        IL_005B:  nop         
        IL_005C:  endfinally  
        IL_005D:  ret         
        

        【讨论】:

        • 很抱歉为最近登陆 C# 世界的可怜的 C++ 开发人员添加了一个愚蠢而幼稚的问题...什么是 IL? :D
        • IL 是中间语言。它独立于 CPU,这允许在任何平台上运行 .NET 代码。 .NET 代码(C#、VB.NET 或 F#)首先编译为 IL 代码,由 CLR(公共语言运行时)和 JITed(即时编译器)运行为机器代码。
        • 酷,知道了我会阅读更多有关该主题的内容,非常感谢!
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-30
        • 2018-02-24
        相关资源
        最近更新 更多