我正在培训初学者一些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()
- 方法调用枚举器,输出为:
元素数量:0
奇怪的是,如果更改类的构造函数,以便它为私有字段使用IList<T>
public SomeEnumerable()
{
_numbers = Enumerable.Range(0,10).ToArray();
}
代码抛出正确的InvalidCastException。
我知道Enumerable.Range()
在其实现中使用yield
语句,并且只要GetEnumerator
方法实现,编译器就可以做一些魔术,所以也许这是一个提示。
因此我的问题是:
IEnumerable
向IEnumerator
而不是IList
投降IEnumerator
而不会收到警告(ReSharper)一些澄清: 我知道不应该这样做,我知道它永远不会以这种方式工作。但我感兴趣的是为什么代码导致IL编译和执行完全正常但产生如此奇怪的结果
答案 0 :(得分:4)
为什么演员合法,为什么没有警告?
演员的整个点就是你告诉编译器,“我知道你不认为这种类型实际上是另一种类型,但我知道的比你多,让我把它看作是这个其他类型,如果我错了就在运行时抛出异常。
为什么你可以在没有警告的情况下将IEnumerable投射到IEnumerator而不是IList到IEnumerator(ReSharper)
您无法将任何let startDate = moment('2017-02-02 15:36:00').toDate();
投射到IEnumerable<T>
。这个特定对象恰好实现了两个接口。其他对象,例如你编写的IEnumerator<T>
,只会实现其中一个,而像这样的强制转换将会失败(在运行时)。
为什么要通过Count / foreach / etc调用GetEnumerator。产生空结果?
该对象不期望您将其转换为其他类型并开始在其上调用方法。你违反了合同类型。如果您希望它正常运行,请致电SomeEnumerable
,它会为您提供适当的调查员。实际上,由于您破坏了初始化其数据的方法,因此您将获得一个未正确初始化的枚举器。
如果有充分理由这样做,框架的哪一部分会使用它?
它只是一种类似于:
的类型GetEnumerator
如果你愿意,你可以写一个这样的课。
答案 1 :(得分:2)
查看Enumerable.Range(0,10)
返回的类型:
System.Linq.Enumerable+<RangeIterator>d__110
这种类型由编译器生成,是一个小型状态机,用于跟踪yield
语句中的迭代。此yield
语句由Enumerable.Range
语句在内部执行。
跑步......
Enumerable.Range(0,10).GetType().GetInterfaces()
...返回
typeof(IEnumerable<Int32>)
typeof(IEnumerable)
typeof(IEnumerator<Int32>)
typeof(IDisposable)
typeof(IEnumerator)
所以你有它:它实现了IEnumerable<Int32>
和IEnumerator<Int32>
。这就是演员成功的原因。转换始终会转换对象的 acutal 类型,而不是编译时类型。 _number
的编译时类型为IEnumerable<int>
,但它的实际类型仍然是生成的类型。
知道这一点很明显,为什么创建数组会导致无效的强制转换异常:它没有实现IEnumerator<int>
。
那么为什么new SomeEnumerable()
会返回0
个元素?我必须在这里推测一下,但我认为这是因为状态机在这里已经使用了两次,首先是枚举器,然后是可枚举的。在第二种用法中,它的内部指针已经在最后一次迭代。
答案 2 :(得分:1)
此代码编译没有问题
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'CONSTRAINT customers_pk PRIMARY KEY,
first_name VARCHAR2(10) NOT NULL,
last_na' at line 2
此代码编译也没有问题
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();
}
}
所以你的问题似乎是Enumerable.Range返回一个实现IEnumerable和IEnumerator的对象。