【问题标题】:Why is the C# compiler happy with double IEnumerable<T> and foreach T?为什么 C# 编译器对双 IEnumerable<T> 和 foreach T 感到满意?
【发布时间】:2013-12-10 21:05:25
【问题描述】:

我知道这段代码不起作用(并且以一种可行的方式编写它没有问题)。 我想知道编译器如何构建没有任何错误。如果你在哪里运行它,你会得到运行时错误? (假设数据不为空)

using System;
using System.Collections.Generic;

public class Class1
{
    public void Main()
    {
        IEnumerable<IEnumerable<Foo>> data = null;

        foreach(Foo foo in data){
            foo.Bar();
        }
    }

}

public class Foo {
    public void Bar() { }
}

【问题讨论】:

  • Resharper 用Suspicious cast 警告捕获它。仍然可以编译。
  • @user2864740 这有点像.Cast&lt;Foo&gt;,但仍然只是那样,不完全是发生了什么。
  • 我猜这是因为 IEnumerable 是一个接受任何类的泛型类。由于 IEnumerable 是一个类,所以编译器没有错。
  • @user2864740 IT 会在运行时抛出 System.InvalidCastException

标签: c# .net foreach ienumerable


【解决方案1】:

这是因为foreach 不会在您的特定情况下进行编译时检查。如果你构建了工作代码,你会得到a InvalidCastException at run-time

using System.Collections.Generic;

public class Test
{
    internal class Program
    {
        public static void Main()
        {
            var item = new Foo();
            var inner = new List<Foo>();
            var outer = new List<List<Foo>>();

            inner.Add(item);
            outer.Add(inner);

            IEnumerable<IEnumerable<Foo>> data = outer;

            foreach (Foo foo in data)
            {
                foo.Bar();
            }
        }

    }


    public class Foo
    {
        public void Bar()
        {
        }
    }
}

foreach (Foo foo in data)相当于调用

IEnumerator enumerator = ((IEnumerable)data).GetEnumerator();
Foo foo; //declared here in C# 4 and older
while(enumerator.MoveNext())
{
    //Foo foo; //declared here in C# 5 and newer

    foo = (Foo)enumerator.Current; //Here is the run time error in your code.

    //The code inside the foreach loop.
    {
        foo.Bar();
    }
}

所以你看它并不关心你传入的是什么类型,只要foo = (Foo)enumerator.Current;调用成功。


它没有抛出任何编译时错误的原因是IEnumerable&lt;T&gt;covariant。这意味着我可以通过任何基于Foo 或更多派生自Foo 的类。因此,如果我可以创建一个继承自 Foo 的第二类,该类也将支持 IEnumerable&lt;Foo&gt; 并让我的列表包含它,这将导致强制转换失败。

//This code compiles fine in .NET 4.5 and runs without throwing any errors.
internal class Program
{
    public static void Main()
    {
        var item = new Baz();
        var inner = new List<Baz>();
        inner.Add(item);

        IEnumerable<IEnumerable<Foo>> data = inner;

        foreach (Foo foo in data)
        {
            foo.Bar();
        }
    }
}

public class Foo
{
    public void Bar()
    {
    }
}

public class Baz : Foo, IEnumerable<Foo>
{
    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }

    IEnumerator<Foo> IEnumerable<Foo>.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

但是,如果您将 Foo 标记为 sealed,编译器现在知道不再存在派生类,然后将引发编译器错误

【讨论】:

  • +1。确切地。还要注意,如果他把它改成foreach (var foo in data),那么编译器会做类型检查。
  • +1。这个答案很到位。我也喜欢上面的评论,建议在foreach 声明中使用var。尝试强制转换为字符串时抛出错误的原因是编译器知道这些类型不兼容。作为计数器,这有效:foreach(Foo2 foo in data) 其中Foo2 是另一个类定义
  • @Xenolightning 是的,没错。而且编译器知道这些类型不兼容,因为string 被标记为sealed
  • Foo 类未密封。可以从中派生出一个实现IEnumerable&lt;Foo&gt;CrazyFoo。那么IEnumerable&lt;IEnumerable&lt;Foo&gt;&gt; 类型的data 可能实际上会产生CrazyFoo 实例,而原来的foreach 将是有效的。
  • IEnumerable&lt;out T&gt; 的协方差不是绝对必要的。同样在 C# 3 和更早版本中,当泛型类型不能协变时,可以从 Foo 派生并实现所需的确切(封闭泛型)接口。编辑:只需将您的inner 设为Foo 列表,然后仍添加Baz 项目,在C# 2 中应该没问题。
【解决方案2】:

由于您明确指定了类型,foreach 的每次迭代都将尝试(在运行时)将当前项目转换为 Foo。这和写这个没什么不同:

IEnumerable<IEnumerable<Foo>> data = null;
foreach (object item in data)
{
    Foo foo = (Foo)item;
    foo.Bar();
}

或者更直接地说,这个:

IEnumerable<Foo> data = null;
Foo foo = (Foo)data;

编译器不会抱怨它不会抱怨转换到接口或从接口转换的原因:它无法证明转换无效(有关示例,请参见 here为什么)。

请注意,相比之下,如果您使用具体类而不是接口IEnumerable,那么您收到编译时错误。例如:

IEnumerable<List<Foo>> data = null;

foreach(Foo foo in data){  // compile-time error here: "cannot convert List<Foo> to Foo"
    foo.Bar();
}

【讨论】:

  • 不一样。你的代码会抛出NullRefenceException,当他抛出System.InvalidCastException
  • @MarcinJuraszek 这只是(Foo)data(抛出异常)和data as Foo(返回null)之间的区别。这是一个重要的区别,但与问题无关。
  • 一个类型可以在foreach(Foo foo in data) 中使用而不实现IEnumerable&lt;Foo&gt;。我不知道如果Foo 是未实现IEnumerable&lt;Foo&gt; 的密封类型,那么这样的事情必然是可能的,但是前面提到的“foreach”语句将使用编译时鸭子类型来查找@987654336 @ 方法返回一个类型,该类型具有返回 boolMoveNext 方法和返回 FooCurrent 属性。
【解决方案3】:

您可以通过将Foo 类标记为sealed 来使编译时错误发生:

public sealed class Foo
{
    public void Bar() { }
}

否则,编译器无法确定是否可以转换。

【讨论】:

    【解决方案4】:

    非常好的问题!这是我从@Jon Skeet 读到的一个未解决的问题: Do "type-safe" and "strongly typed" mean the same thing? 我还阅读了 msdn 中的一些博客。 @slaks 博客也不错 http://blog.slaks.net/2011/09/c-is-not-type-safe.html

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-25
      相关资源
      最近更新 更多