在C#编译器中键入Duck

本文关键字:Duck 编译器 | 更新日期: 2023-09-27 17:59:12

注意这是一个关于如何在C#中实现或模拟鸭子类型的问题,而不是。。。

几年来,我一直认为某些C#语言特性依赖于语言本身定义的数据结构(对我来说,这总是一个奇怪的鸡和蛋的场景)。例如,我的印象是foreach循环只能用于实现IEnumerable的类型。

从那时起,我开始理解C#编译器使用鸭子类型来确定对象是否可以在foreach循环中使用,从而寻找GetEnumerator方法而不是IEnumerable方法。这是很有意义的,因为它去除了鸡肉和鸡肉;鸡蛋难题。

我有点困惑,为什么using块和IDisposable的情况似乎不是这样。编译器不能使用duck类型并查找Dispose方法,有什么特别的原因吗?这种不一致的原因是什么?

也许IDisposable背后还有其他事情?

讨论为什么有一个对象的Dispose方法没有实现IDisposable超出了这个问题的范围:)

在C#编译器中键入Duck

IDisposable没有什么特别之处,但迭代器有一些特别之处。

在C#2之前,在foreach上使用这种鸭子类型是实现强类型迭代器的唯一方法,也是在不装箱的情况下迭代值类型的唯一方法。我怀疑,如果C#和.NET一开始就有泛型,那么foreach将需要IEnumerable<T>,而不是鸭子类型。

现在编译器在我能想到的其他几个地方使用这种鸭子类型:

  • 集合初始化器寻找合适的Add重载(以及必须实现IEnumerable的类型,只是为了表明它确实是某种集合);这允许灵活地添加单个项、键/值对等
  • LINQ(Select等)-这就是LINQ实现其灵活性的方式,允许对多种类型使用相同的查询表达式格式,而不必更改IEnumerable<T>本身
  • C#5等待表达式要求GetAwaiter返回一个具有IsCompleted/OnCompleted/GetResult的awaiter类型

在这两种情况下,这使得将该功能添加到现有的类型和接口中变得更容易,而这些类型和接口在早期并不存在该概念

考虑到IDisposable从第一个版本就已经在框架中了,我认为鸭子键入using语句没有任何好处。我知道你明确地试图从讨论中忽略使用Dispose而没有实现IDisposable的原因,但我认为这是一个关键点。在语言中实现一个功能需要有充分的理由,我认为鸭子类型是一个超越支持已知接口的功能。如果这样做没有明显的好处,它就不会出现在语言中。

没有鸡和蛋:foreach可以依赖IEnumerable,因为IEnumerable不依赖foreach。允许在未实现IEnumerable的集合上使用foreach的原因可能很大程度上是历史性的:

在C#中,它不是严格必要的用于要从中继承的集合类IEnumerable和IEnumerator按顺序排列与foreach兼容;只要因为该类具有所需的GetEnumerator、MoveNext、Reset和当前成员,它将与前臂。省略接口有允许您定义Current的返回类型比对象更具体,从而提供类型安全性。

此外,并不是所有的鸡和蛋问题实际上都是问题:例如,函数可以调用自己(递归!),或者引用类型可以包含自己(就像链表一样)。

那么,当using出现时,当他们可以简单地说:实现IDisposable时,为什么他们会使用像鸭子键入这样棘手的东西来指定呢?从根本上说,通过使用duck类型,您正在围绕类型系统进行最终运行,这只有在类型系统不足以(或不切实际)解决问题时才有用。

您所问的问题不是鸡和蛋的问题。它更像是语言编译器的实现方式。类似C#和VB.NET编译器的实现方式不同。如果你写了一个helloworld的简单代码,并用编译器和检查IL代码进行编译,它们会有所不同。回到您的问题,我想解释一下C#编译器为IEnumerable生成了什么IL代码。

IEnumerator e = arr.GetEnumerator();
while(e.MoveNext())
{
   e.Currrent;
}

因此,针对foreach的情况对C#编译器进行了调整。