【问题标题】:How does foreach call GetEnumerator()? Via IEnumerable reference or via...?foreach 如何调用 GetEnumerator()?通过 IEnumerable 引用还是通过...?
【发布时间】:2010-09-09 19:16:39
【问题描述】:
    static void Main(string[] args)
    {
        List<int> listArray = new List<int>();
        listArray.Add(100);
        foreach (int item in listArray)
            Console.WriteLine(item);
    }

a) 当foreach 语句调用listArray's IEnumerable&lt;int&gt;.GetEnumerator() 实现时,它是通过listArray.GetEnumerator()IEnumerable&lt;int&gt;.GetEnumerator()IEnumerable.GetEnumerator() 调用它吗?

b) 同样,当foreach 引用listArray's IEnumerable&lt;int&gt;.GetEnumerator() 返回的对象时,它是通过IEnumerator 还是IEnumerator&lt;int&gt; 引用类型引用该对象?

谢谢

编辑:

我的一些问题会引用这段文字:

o 对类型 X 执行成员查找 带有标识符 GetEnumerator 并且没有 类型参数。如果成员查找 不产生匹配,或者它 产生歧义,或产生 不是方法组的匹配项, 检查可枚举接口为 如下面所描述的。推荐 如果成员发出警告 查找产生任何东西,除了 方法组或不匹配。

o 使用 生成的方法组和 空参数列表。如果过载 决议导致不适用 方法,导致歧义,或 结果是一个最好的方法,但是 该方法是静态的还是非静态的 public,检查一个可枚举的 界面如下所述。它是 建议发出警告 如果重载决议产生 除了明确的公众之外的任何东西 实例方法或不适用 方法。

o 如果返回类型 E 的 GetEnumerator 方法不是一个类, 结构或接口类型,错误是 生产,没有进一步的步骤 采取了。

o 成员查找在 E 上执行 带有标识符 Current 和 no 类型参数。如果成员查找 不产生匹配,结果是 错误,或者结果是什么 除了公共实例属性 允许读取,产生错误 并且没有采取进一步的步骤。

o 成员查找在 E 上执行 标识符 MoveNext 并且没有类型 论据。如果成员查找 不产生匹配,结果是 错误,或者结果是什么 除了方法组,错误是 生产,没有进一步的步骤 采取了。

o 重载解决方案在 方法组为空 参数列表。如果重载决议 导致没有适用的方法, 导致歧义,或导致 一个最好的方法,但那个方法 是静态的或不公开的,或者它的 返回类型不是 bool,错误是 生产,没有进一步的步骤 采取了。

o 集合类型是 X, 枚举类型为 E,元素 type 是当前的类型 财产。

  • 否则,检查可枚举接口: o 如果恰好存在一种类型 T 使得存在隐式 从 X 到接口的转换 System.Collections.Generic.IEnumerable, 那么集合类型就是这个 接口,枚举类型是 界面 System.Collections.Generic.IEnumerator, 元素类型为T。

  • 否则,如果有多个这样的类型 T,则 产生错误并且没有进一步的步骤 被拍了。

  • 否则,如果存在从 X 到 System.Collections.IEnumerable 接口,则集合类型为 这个接口,枚举类型是 界面 System.Collections.IEnumerator 和 元素类型是对象。

  • 否则,将产生错误并且没有进一步的步骤 拍摄。

1)

引用 Eric Lippert 的话:

选项(1)是正确的。请注意,这 表示返回的枚举数是 一个未装箱的可变结构。

这是一个可变结构的事实 如果你这样做会产生非常真实的效果 像路过一样愚蠢的事情 结构就好像它是一个 参考类型;它将被复制 价值,而非参考。

来自http://en.csharp-online.net/ECMA-334:_15.8.4_The_foreach_statement

foreach (V v in x) 嵌入语句

然后扩展为:

{
   E e = ((C)(x)).GetEnumerator();
   try {
      V v;
      while (e.MoveNext()) {
         v = (V)(T)e.Current;
         embedded-statement
      }
   }
   finally {
      … // Dispose e
   }
}

变量 e 对 or 不可见 可访问表达式 x 或 嵌入式声明或任何其他来源 程序代码。

listArray的情况下,返回的枚举数被保存(即它的值被保存)在变量e中(因此变量e是一个可变结构)。但是根据上面的摘录,e是无法访问我的源代码,那么我将如何传递这个结构(除非我编写的代码手动执行 foreach 语句自动执行的操作)?

2)

成员查找在 E 上执行,标识符为 Current,没有类型参数。如果 成员查找不产生匹配,结果是错误,或者结果是任何东西 除了允许读取的公共实例属性之外,会产生错误并且不会 采取进一步措施。

似乎如果我们在类(X)本身中实现GetEnumerator,那么Current也应该在类(E)本身中实现(因此E不应该显式实现@ 987654344@ ),因为在成员查找(在 E 上,标识符为 Current )不产生匹配的情况下,编译器不会费心检查 IEnumerator&lt;T&gt; / IEnumerator 接口?

3)

如果只有一种类型 T 存在从 X 到 接口 System.Collections.Generic.IEnumerable,那么集合类型就是这个 接口,枚举类型为接口 System.Collections.Generic.IEnumerator,元素类型为T。

根据上述,如果foreach 必须检查IEnumerable&lt;T&gt; 接口,那么foreach 将始终使用IEnumerator&lt;T&gt; 版本的Current?因此,如果E 显式实现了IEnumerator&lt;T&gt; 版本的Current,并且如果它还在类本身中实现了另一个版本的Current,那么foreach 将始终调用IEnumerable&lt;T&gt; 版本的Current

4)

GetEnumerator 方法被记录为返回以下之一:

http://msdn.microsoft.com/en-us/library/x854yt9s.aspx

其中一个(如复数形式)是什么意思?您提供的链接说 GetEnumerator (由 List&lt;T&gt; 实现)仅返回 struct 类型。

5)

g.集合类型为X,枚举类型为E,元素类型为Current属性的类型

也许是一个无用的问题——根据上面,foreach 不会检查某些用户定义的集合实际存储的元素类型,而是假设元素的类型与Current 返回的类型相同财产?

【问题讨论】:

  • 您是否考虑过编写一个简单的测试来找出自己?编写一个实现 IEnumerable.GetEnumerator() 和 GetEnumerator() 的类并传回不同的枚举器是非常简单的。
  • 我已更新我的答案以解决您的其他问题。

标签: c#


【解决方案1】:

(a) 当 foreach 语句调用 listArray 的 IEnumerable.GetEnumerator() 实现时,它是通过 (1) listArray.GetEnumerator() 还是 (2) IEnumerable.GetEnumerator() 或 (3) IEnumerable.GetEnumerator() 调用它?

选项(1)是正确的。 请注意,这意味着返回的枚举数是一个未装箱的可变结构。 GetEnumerator 方法被记录为返回以下之一:

http://msdn.microsoft.com/en-us/library/x854yt9s.aspx

如果你做一些愚蠢的事情,比如像传递引用类型一样传递结构体,那么这是一个可变结构体这一事实会产生非常实际的影响;它将按值复制,而不是按引用复制。

(1) 但是根据上面的摘录,我的源代码无法访问 e,那么我如何才能传递这个结构(除非我编写的代码手动执行 foreach 语句自动执行的操作)?

你是对的。我不清楚。我的观点是,如果您编写的代码与 foreach 一样,并且您自己弄乱了枚举器对象,那么您必须小心。 CLR 团队意识到绝大多数人会使用 foreach 循环,因此不会面临意外错误使用枚举器的危险。

(2) 似乎如果我们在 X 类本身中实现 ​​GetEnumerator,那么 Current 也应该在 E 类本身中实现,因为在成员查找没有的情况下编译器不会费心检查显式接口成员不匹配?

正确。

(3) 如果foreach 必须检查IEnumerable&lt;T&gt; 接口,那么foreach 将始终使用IEnumerator&lt;T&gt; 版本的Current?因此,如果 E 显式实现了 Current 的 IEnumerator&lt;T&gt; 版本,并且它还在类本身中实现了 Current 的另一个版本,那么 foreach 将始终调用 Current 的 IEnumerable&lt;T&gt; 版本?

正确。如果您达到我们正在查看界面的程度,那么我们将使用该界面。

(4) “其中之一”是什么意思

我的意思是它会返回结构的一个实例。

(5) 根据上面的说法,foreach 并不检查某个用户定义的集合实际存储的是什么类型的元素,而是假设元素的类型与 Current 属性返回的类型相同?

正确。它确实检查转换是否成功。例如,如果你说

foreach(int x in myObjects)

其中 myObjects 为您提供一个 Current 类型为 object 的枚举器,然后循环假定每个对象都可以成功转换为 int,如果不正确,则在运行时抛出异常。但如果你说类似的话:

foreach(string x in myInts)

然后编译器会注意到,如果 Current 返回一个 int 则集合永远不会包含字符串,并且将无法编译程序。

(b) 同样,当foreach引用listArray的IEnumerable.GetEnumerator()返回的对象时,是通过IEnumerator还是IEnumerator引用类型来引用这个对象?

该问题的前提是第一个问题的答案是选项 (2)。由于这个问题是基于虚假的,所以无法明智地回答。

【讨论】:

    【解决方案2】:

    语言规范第 8.8.4 节详细说明了 foreach 的行为。一言以蔽之

    foreach(表达式中的 T t)

    • 如果 expression 是一个数组*,请使用 IEnumerable 接口(*请参阅下面 Eric Lippert 的评论。)
    • 如果 表达式 有 GetEnumerator 方法,则使用该方法
    • 如果 表达式 可转换为 IEnumerable&lt;T&gt;,则使用该接口和 IEnumerator&lt;T&gt;(和相关方法)
    • 如果 表达式 可转换为 IEnumerable,则使用该接口和 IEnumerator(和相关方法)

    还有各种错误情况和我正在掩饰的事情。但是,简而言之,如果您的集合是通用的,它将使用通用接口选项。

    【讨论】:

    • 请注意,第一个要点可能具有误导性。在使用 foreach 枚举数组(或字符串!)时,C# 编译器生成的代码实际上不使用 IE 接口。相反,它会生成与您通常使用的相同的“for(int i = 0; i 出于类型分析的目的,但实际上并没有以这种方式生成代码
    【解决方案3】:

    来自 C# 3.0 语言规范(第 8.8.4 节):

    foreach 语句的编译时处理首先确定表达式的集合类型、枚举类型和元素类型。该确定过程如下:

    1. 如果表达式的 X 类型是数组类型,则存在从 X 到 System.Collections.IEnumerable 接口的隐式引用转换(因为 System.Array 实现了此接口)。集合类型为 System.Collections.IEnumerable 接口,枚举类型为 System.Collections.IEnumerator 接口,元素类型为数组类型 X 的元素类型。
    2. 否则,判断X类型是否有合适的GetEnumerator方法:

      一个。对带有标识符 GetEnumerator 且没有类型参数的类型 X 执行成员查找。如果成员查找没有产生匹配,或者产生歧义,或者产生不是方法组的匹配,则如下所述检查可枚举接口。如果成员查找产生除方法组以外的任何内容或不匹配,建议发出警告。

      b.使用生成的方法组和空参数列表执行重载决议。如果重载决议导致没有适用的方法,导致歧义,或者导致单个最佳方法但该方法是静态的或不是公共的,请检查可枚举接口,如下所述。如果重载决议产生除了明确的公共实例方法或没有适用的方法之外的任何内容,建议发出警告。

      c。如果 GetEnumerator 方法的返回类型 E 不是类、结构或接口类型,则会产生错误并且不采取进一步的步骤。

      d。成员查找在 E 上执行,标识符为 Current,没有类型参数。如果成员查找未产生匹配,则结果为错误,或者结果为除允许读取的公共实例属性之外的任何内容,则会产生错误并且不采取进一步的步骤。

      e。成员查找在 E 上执行,标识符为 MoveNext,没有类型参数。如果成员查找未产生匹配,则结果是错误,或者结果是除方法组之外的任何内容,则产生错误并且不采取进一步的步骤。

      f。在具有空参数列表的方法组上执行重载解决方案。如果重载决议导致没有适用的方法,导致歧义,或导致单个最佳方法但该方法是静态的或不是公共的,或者它的返回类型不是 bool,则会产生错误并且不采取进一步的步骤。

      g.集合类型为X,枚举类型为E,元素类型为Current属性的类型。

    总之,编译器就好像 foreach 是以下代码一样,进行多态调用并查看已定义的可枚举接口定义(如果有)以确定正确的类型和方法:

    var iterator = listArray.GetEnumerator();
    while(iterator.MoveNext())
    {
       var item = iterator.Current;
       Console.WriteLine(item);
    }
    

    【讨论】:

    • 当然还有处理枚举数的代码。另外请注意,如果集合是数组或字符串,这不是生成的代码。
    猜你喜欢
    • 1970-01-01
    • 2014-07-20
    • 2013-02-23
    • 2020-04-01
    • 2012-03-15
    • 2011-09-17
    • 1970-01-01
    • 1970-01-01
    • 2010-12-05
    相关资源
    最近更新 更多