【问题标题】:Why does foreach(var i in array2D) work on multidimensional arrays?为什么 foreach(var i in array2D) 对多维数组起作用?
【发布时间】:2014-04-10 17:57:48
【问题描述】:

给定以下 C# 代码:

   int[,] array2D = new int[10, 10];
   int sum = 0;

   foreach (var i in array2D)
   {
        sum += i;
   }

问题是:是什么导致i 的类型被正确推断为int

这一点也不明显,因为 array2D 是一个矩形数组。它没有实现IEnumerable<int>。它还实现了一个GetEnumerator() 方法,该方法返回一个System.Collections.IEnumerator。因此,我希望i 的类型为object

我的代码使用的是 .net 4.03。

相关的 SO 问题:Why do C# Multidimensional arrays not implement IEnumerable?

【问题讨论】:

    标签: c# .net multidimensional-array foreach


    【解决方案1】:

    数组是使用技巧和黑客实现的东西。实际上 Array 没有实现 IEnumerable<T> CLR 使用 SZArrayHelper 欺骗我们。

    Related question of mine 和 HansPassant 有一个 related answer here

    foreach 代码的 IL 也不同,c# 编译器本身为 2Darrays 和一维数组生成不同的 IL。可悲的是,它甚至没有调用GetEnumerator 方法:(。

    复杂的实现细节

    我没有实现这个,但这是一组非常有趣的工作 在加载器中,还有一些看起来有点 hacky 的 C# 代码。你可能 想知道有人如何仅在某些数组类型上定义泛型方法, 当数组生成没有对应源的类型时 代码。使用 V1 中的 IList,我们将 System.Array 作为基类 Int32[] 和 Object[] 子类化,但我们无法将方法添加到 数组本身,因为方法实现没有意义 在 SZArray 以外的任何东西上。作为内部实现 没有人需要知道的细节,我们目前有一个 SZArrayHelper 定义所有有趣的方法体的类,我们传递一个 数组作为“this”指针的方法。这导致一些非常 奇怪的内部代码,像这样:

    sealed class SZArrayHelper 
    {  
        internal int get_Count<T>() 
        {
              //! Warning: "this" is an array, not an SZArrayHelper. See comments above
             //! or you may introduce a security hole!
              T[] _this = this as T[];
              BCLDebug.Assert(_this!= null, "this should be a T[]");
              return _this.Length;  
        } 
    }
    

    幸运的是,CLR 团队已经完成了所有复杂的邪恶制作 类型系统按照您想要的方式工作,这完全是 你不需要知道的内部细节。有时要建 一个有趣的类型系统以一种有效的方式,你需要玩 一些会让你的计算机科学教授追捕你的游戏 下来。

    我们努力的最终结果是您可以调用通用算法 消耗 IList 并将其传递给通用集合(例如 List)或数组(如String [])并获得有效的元素 无需额外的逻辑就可以访问。你没有 制作任何愚蠢的适配器类或类似的东西。

    以上段落来自Related article,其中谈到了SZArray

    只是他们是神秘的。我想分享我目前所知道的,我知道这并不能提供答案,但我希望这会有所帮助。

    【讨论】:

      【解决方案2】:

      这记录在C# specification under 8.8.4 The Foreach Statement(though sadly not in the online readable version)

      如果表达式的X类型是数组类型,则存在隐式引用转换 从 X 到 System.Collections.IEnumerable 接口(因为 System.Array 实现 这个界面)。集合类型是 System.Collections.IEnumerable 接口, 枚举器类型是 System.Collections.IEnumerator 接口,和 element type 是数组类型X的元素类型。

      我已经强调了记录您所询问的行为的要点。

      数组包含整数,因此 foreach 语句将按照上述规则在整数上循环。

      数组不是“整数数组的数组”,而是“整数的二维数组”。它是一个具有矩形索引的整数集合,但它仍然是一个整数集合。

      请注意,多维数组的元素类型不实现 IEnumerable&lt;T&gt;,但它确实实现了非泛型接口 IEnumerable

      因此,对于集合的定义,数组是整数的集合,即使它是多维的。

      请注意,数组在 C# 编译器中有一个特殊的位置,它会在很多情况下专门处理这些,所以除了它是一个数组之外,你可能无法从反射中推断出任何东西。

      要查看编译器为数组提供的一些特殊处理,请尝试在 LINQPad 中执行此代码,然后单击 IL 选项卡:

      void Main()
      {
      }
      
      public void A()
      {
          int[] a = new int[10];
      
          foreach (var x in a) { }
      }
      
      public void B()
      {
          int[] a = new int[10];
      
          for (int i = 0; i < a.Length; i++) { }
      }
      

      你会得到这个:

      IL_0000:  ret         
      
      A:
      IL_0000:  ldc.i4.s    0A 
      IL_0002:  newarr      System.Int32
      IL_0007:  stloc.0     // a
      IL_0008:  ldloc.0     // a
      IL_0009:  stloc.1     // CS$6$0000
      IL_000A:  ldc.i4.0    
      IL_000B:  stloc.2     // CS$7$0001
      IL_000C:  br.s        IL_0016
      IL_000E:  ldloc.1     // CS$6$0000
      IL_000F:  ldloc.2     // CS$7$0001
      IL_0010:  ldelem.i4   
      IL_0011:  pop         
      IL_0012:  ldloc.2     // CS$7$0001
      IL_0013:  ldc.i4.1    
      IL_0014:  add         
      IL_0015:  stloc.2     // CS$7$0001
      IL_0016:  ldloc.2     // CS$7$0001
      IL_0017:  ldloc.1     // CS$6$0000
      IL_0018:  ldlen       
      IL_0019:  conv.i4     
      IL_001A:  blt.s       IL_000E
      IL_001C:  ret         
      
      B:
      IL_0000:  ldc.i4.s    0A 
      IL_0002:  newarr      System.Int32
      IL_0007:  stloc.0     // a
      IL_0008:  ldc.i4.0    
      IL_0009:  stloc.1     // i
      IL_000A:  br.s        IL_0010
      IL_000C:  ldloc.1     // i
      IL_000D:  ldc.i4.1    
      IL_000E:  add         
      IL_000F:  stloc.1     // i
      IL_0010:  ldloc.1     // i
      IL_0011:  ldloc.0     // a
      IL_0012:  ldlen       
      IL_0013:  conv.i4     
      IL_0014:  blt.s       IL_000C
      IL_0016:  ret         
      

      请注意,在任何地方都没有调用GetEnumerator,这意味着foreach 没有使用GetEnumerator。相反,它被重写为使用索引的 for 循环,因为在 .NET 中实现数组的方式实际上更快。

      【讨论】:

      • 我赞成你的回答,希望反对者发表评论。
      • 对不起,这个答案并没有解释什么。
      • @jdv-JandeVaan 说明多维数组是int数组,不是数组数组,因此枚举按预期返回int。
      • 添加了来自 C# 规范的注释
      • 感谢您扩展您的初始答案。我现在看到您试图解释 foreach 不会枚举矩阵的行。但我没想到会发生这种情况。我知道多维数组不是数组的数组。我最困惑的是 GetEnumerator() 返回一个无类型的枚举器,而 foreach 无论如何都使用了正确的类型。开始看起来像是特殊处理。
      【解决方案3】:

      不是很直观,因为它是一个多维数组,但这是枚举这种数组的预期行为,it's documented

      使用多维数组,可以使用相同的方法进行迭代 通过元素,例如:

      int[,] numbers2D = new int[3, 2] { { 9, 99 }, { 3, 33 }, { 5, 55 } };
      // Or use the short form: 
      // int[,] numbers2D = { { 9, 99 }, { 3, 33 }, { 5, 55 } }; 
      
      foreach (int i in numbers2D)
      {
          System.Console.Write("{0} ", i);
      }
      // Output: 9 99 3 33 5 55
      

      【讨论】:

      • +1,这是正确答案。 C# 规范中的相关引用也可以改进这个答案; 8.4.4(“foreach 语句”)包含详细信息。
      • +1 获取文档链接。然而,机制仍不清楚。我一直明白,标准的 foreach 取决于“GetEnumerator”。
      • 在它上面运行 gettype() 会给我们 System.Int32 [,]
      猜你喜欢
      • 2013-07-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多