【问题标题】:Duck typing in the C# compiler在 C# 编译器中输入鸭子
【发布时间】:2011-09-16 04:09:00
【问题描述】:

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

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

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

我有点困惑,为什么using 块和IDisposable 似乎不是这种情况。编译器是否有任何特殊原因不能使用鸭子类型并寻找Dispose 方法?这种不一致的原因是什么?

也许 IDisposable 的幕后还有其他事情发生?

讨论为什么你会曾经拥有一个带有未实现 IDisposable 的 Dispose 方法的对象,这超出了这个问题的范围:)

【问题讨论】:

  • +1 有趣。你能提供关于foreach的观点的来源吗?
  • @harpo 我刚刚“发现”了 MattDavey 所做的事情。我在下面发布了一些代码作为答案,尽管从技术上讲它本身并不是答案。
  • Eric Lippert 在被问到这个问题后不久就写了一篇博文:blogs.msdn.com/b/ericlippert/archive/2011/06/30/…
  • 这里是关于 foreach 的一个很好的解释,stackoverflow.com/a/398996/1160036。简而言之,对象不需要实现IEnumerable接口,它们只需要一个GetEnumerator()方法,该方法返回一个具有MoveNext()Current的对象。
  • @MattDavey 该链接无效。

标签: c# compiler-construction idisposable duck-typing


【解决方案1】:

这里的IDisposable 没有什么特别之处 - 但是迭代器有些特别之处。

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

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

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

在这两种情况下,都可以更轻松地将功能添加到现有类型和接口中,而这个概念在早期并不存在。

鉴于IDisposable 从第一个版本开始就在框架中,我认为鸭子键入using 语句没有任何好处。我知道您在讨论中明确试图打折使用Dispose 而不实施IDisposable 的原因,但我认为这是一个关键点。在语言中实现一个特性需要有充分的理由,我认为鸭子类型是一个超越支持已知接口的特性。如果这样做没有明显的好处,它就不会出现在语言中。

【讨论】:

  • 表达式创建(来自 lambda)是另一个例子;但是,这在任何地方都没有严格记录
  • @Jon Skeet 感谢乔恩的回答!编译器是否也使用带有集合初始值设定项的鸭子类型? (即寻找 Add() 方法而不是 ICollection)
  • 在迭代器是结构体的情况下,将鸭子类型与“for each”一起使用允许编译器避免装箱。虽然我认为结构实现 IEnumerator(通用或非通用)是不好的,但有时避免在每次循环开始时创建一个新对象是有利的。让 GetEnumerator 方法返回一个结构,而 IEnumerable.GetEnumerator 方法返回一个实现 IEnumerator 的类,可能会实现两全其美。
  • 顺便提一下,至少在VB中,如果GetEnumerator的返回类型是IEnumerable,编译器会检查在运行时看它是否实现了IDisposable;如果返回类型是其他类型,编译器将在编译时检查,但不会在运行时检查。即使枚举器是实现 IDisposable 的派生类型,如果 declared 类型未实现 IDisposable,编译器也不会尝试在运行时调用它。
  • 如果类有一个不需要 IDisposable 的枚举器,但派生类的枚举器需要它,这可能会产生问题。使用除 IEnumerable<T> 之外的任何东西作为 GetEnumerator 方法的返回类型可能会阻止派生类指定新的枚举方法。
【解决方案2】:

没有鸡和蛋:foreach 可以依赖于IEnumerable,因为IEnumerable 不依赖于foreach。在未实现IEnumerable 的集合上允许使用foreach 的原因可能是largely historic

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

此外,并非所有先有鸡还是先有蛋的问题实际上都是问题:例如,函数可以调用自身(递归!)或引用类型可以包含自身(如链表)。

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

【讨论】:

  • 我所指的先有鸡还是先有蛋的场景是语言特性可能依赖于语言本身定义的类型。例如。 IEnumerable依赖于C#,而C#(foreach)依赖于IEnumerable...
  • 嗯,从这个意义上说,很多东西都是鸡+蛋:用于积分加法的语言功能+取决于语言中定义的int。这只是一张大图的一部分:-)
  • 再想一想,依赖接口的不是C#语言,而是C#编译器,所以我们有一个三角依赖:)
  • @MattDavey 不是+,更好的例子是delegete。这只是System.Delegate 的另一个名称,一个普通的类,您可以使用其静态CombineRemove 方法调用它。但是现在你有 +=-= 运营商依赖它。
【解决方案3】:

您要问的问题不是先有鸡还是先有蛋的情况。它更像是语言编译器的实现方式。就像 C# 和 VB.NET 编译器的实现方式不同。如果您编写一个简单的 hello world 代码并使用编译器编译它并检查 IL 代码,它们会有所不同。回到你的问题,我想解释一下 C# 编译器为IEnumerable 生成了哪些 IL 代码。

IEnumerator e = arr.GetEnumerator();

while(e.MoveNext())
{
   e.Currrent;
}

所以 C# 编译器针对foreach 的情况进行了调整。

【讨论】:

    猜你喜欢
    • 2013-05-10
    • 1970-01-01
    • 2011-05-25
    • 1970-01-01
    • 2023-03-11
    • 2020-04-01
    • 2015-05-01
    • 2012-02-22
    • 1970-01-01
    相关资源
    最近更新 更多