将 IEnumerable<T> 转换为 IEnumerator<T> 有什么作用

What does casting an IEnumerable<T> to IEnumerator<T> do

我正在培训初学者一些 C# 的核心概念。在为随机数创建 IEnumerable 时,他的代码中的一个错误建议将 IEnumerable<T> 转换为 IEnumerator<T>

我知道这些接口都没有相互实现,所以让我印象深刻的是 ReSharper、编译器和运行时都没有抛出异常。

这是一些示例代码:

public class SomeEnumerable : IEnumerable<int>
{
    private readonly IEnumerable<int> _numbers;

    public SomeEnumerable()
    {
        _numbers = Enumerable.Range(0,10);
    }

    public IEnumerator<int> GetEnumerator()
    {
        return (IEnumerator<int>) _numbers;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

出乎意料的是,GetEnumerator-方法编译得非常好。当初学者错误地输入私有字段“_numbers”时,ReSharper 甚至建议添加我已经包含的显式转换。

现在考虑下面的测试代码:

var someEnumerable = new SomeEnumerable();
Console.WriteLine($"Number of elements: {someEnumerable.Count()}");

Count()-方法调用枚举器,输出是这样的:

Number of elements: 0

奇怪的是,如果您更改 class 的构造函数,以便它使用 IList<T> 作为私有字段

public SomeEnumerable()
{
    _numbers = Enumerable.Range(0,10).ToArray();
}

代码抛出正确的 InvalidCastException。

我知道 Enumerable.Range() 在其实现中使用了 yield 语句,并且编译器可以在 GetEnumerator 方法被实现后立即施展魔法,所以也许就是这样一个提示。

因此我的问题是:

  1. 为什么转换是合法的,为什么没有警告? (编译time/Resharper)
  2. 为什么可以在没有收到警告的情况下将 IEnumerable 转换为 IEnumerator 而不能将 IList 转换为 IEnumerator (ReSharper)
  3. 为什么 Count/foreach/etc 调用 GetEnumerator。产生空结果?
  4. 如果有充分的理由让它起作用,框架的哪一部分利用了它?

一些说明: 我知道不应该这样做,我也知道这样永远行不通。但我感兴趣的是,为什么代码生成的 IL 编译和执行都很好,但会产生如此奇怪的结果

Why is the cast legal and why isn't there a warning?

转换的全部 要点 是你告诉编译器,“我知道你不认为这种类型实际上是另一种类型,但我知道的比你多,让我把它当作另一种类型来对待,如果我错了就在运行时抛出异常。

Why can you cast an IEnumerable to an IEnumerator but not an IList to an IEnumerator without getting a warning (ReSharper)

不能将任何 IEnumerable<T> 转换为 IEnumerator<T>。这个特定对象恰好实现了这两个接口。其他对象,例如您编写的 SomeEnumerable,将只实现一个或另一个,并且像这样的转换将失败(在运行时)。

Why does invoking GetEnumerator by Count/foreach/etc. yield an empty result?

该对象不希望您将其转换为另一种类型并开始对其调用方法。你违反了类型的合同。如果您希望它正常运行,请调用 GetEnumerator ,它会给您一个合适的枚举器。实际上,您得到的枚举器尚未正确初始化,因为您破坏了初始化其数据的方法。

If there is a good reason for this to work, which part of the framework makes use of that?

它只是一个看起来像这样的类型:

public class SomeClass: IEnumerable<int>, IEnumerator<int>
{ 
    //...
}

如果你愿意,你可以这样写class。

查看由 Enumerable.Range(0,10) 编辑的 return 类型:

System.Linq.Enumerable+<RangeIterator>d__110

此类型由编译器生成,是一个小型状态机,用于跟踪 yield 语句期间的迭代。此 yield 语句由 Enumerable.Range 语句在内部执行。

运行 ...

Enumerable.Range(0,10).GetType().GetInterfaces()

...returns

typeof(IEnumerable<Int32>) 
typeof(IEnumerable) 
typeof(IEnumerator<Int32>) 
typeof(IDisposable) 
typeof(IEnumerator) 

所以你已经明白了:它实现了 IEnumerable<Int32>IEnumerator<Int32>。这就是演员成功的原因。转换总是转换对象的 acutal 类型,而不是它的 compile-time 类型。 _number 的编译时类型是 IEnumerable<int>,但它的实际类型仍然是生成的类型。

知道了,为什么创建数组会导致无效转换异常就很清楚了:它没有实现 IEnumerator<int>.

那么为什么 new SomeEnumerable() return 0 元素呢?我不得不在这里推测一下,但我认为这是因为状态机在这里被使用了两次,第一次作为枚举器,然后作为可枚举器。在第二次使用中,它的内部指针已经在最后一次迭代中。

这段代码编译没有问题

using System;
using System.Collections.Generic;
public class C {
    public void M() {
        IEnumerable<double> x = null;
        var y = (IEnumerator<double>)x;
    }
}

这段代码编译也没有问题

public class C {
    public void M() {
        IMagic1<double> x = null;
        var y = (IMagic2<int>)x;
    }
}

public interface IMagic1<T> {
   IMagic2<T> Magic();
}

public interface IMagic2<T> {
    void Magic2();
}

因为你通过强制转换告诉编译器你知道得更多,所以两者都能编译。

这会产生运行时错误:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace xyz
{

    public class C {
        public void M() {
            IMagic1<double> x = new X1();
            var y = (IMagic2<int>)x;
        }
    }

    public class X1 : IMagic1<double> {
        public IMagic2<double> Magic() {
            return new X2();
        }        
    }

    public class X2 : IMagic2<double> {
        public void Magic2() {

        }        
    }

    public interface IMagic1<T> {
       IMagic2<T> Magic();
    }

    public interface IMagic2<T> {
        void Magic2();
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            new C().M();
        }
    }
}

这会产生运行时错误:

public class Program
{
    public static void Main(string[] args)
    {
        var x = (IEnumerator<double>)(new double[] {0}).AsEnumerable();
    }
}

这不是:

public class Program
{
    public static void Main(string[] args)
    {
        var x = (IEnumerator<int>)Enumerable.Range(10,1);
    }
}

所以你的问题似乎是 Enumerable.Range 返回一个实现了 IEnumerable 和 IEnumerator 的对象。