foreach Dictionary<>.Values 或 foreach Dictionary<>

foreach Dictionary<>.Values or foreach Dictionary<>

我想知道在 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<Y>?我在 the documentation 中没有看到任何关于创建临时列表的信息。

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

如果上面问题的答案是肯定的,那么第二种风格应该更有效,使第一种风格几乎无用,所以我认为我在某些方面应该是错误的。

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

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

调用 GetEnumerator() 时,您将获得嵌套的 Dictionary<,>.ValueCollection.Enumerator 结构的实例。它通过 Dictionary<,>.

private 数组 entries 直接访问条目

您可以看到 source code.

所以您上面的 "Style one" 是一种很好且清晰的做事方式,没有性能开销。

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

但是,"Style one" 和 "Style two" 得到的顺序是一样的;两者都以相同的方式访问 Dictionary<,> 的私有 entries 数组。

Values 不是在创建 List<T>,不是。它甚至没有将整组值拉入一个单独的数据结构中。它所做的只是创建一个可以迭代值的枚举器。它所做的事情与直接迭代字典时发生的事情完全相同;不同之处在于,它不是为每对对象构造一个 KeyValuePair 对象,而是只给你一对中的一半。除此之外,迭代过程是一样的。

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

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

您可以在 reference source

中查看确切的详细信息

在这两种情况下,您得到的都是 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