【问题标题】:"Program to an interface" using extension methods: When does it go too far?使用扩展方法“编程到接口”:什么时候走得太远了?
【发布时间】:2012-07-11 21:38:45
【问题描述】:

背景: 本着"program to an interface, not an implementation"Haskell type classes 的精神,作为一个编码实验,我正在思考创建一个主要基于以下组合的API 意味着什么接口和扩展方法。我有两个指导方针:

  1. 尽可能避免类继承。接口应该被实现为sealed classes。
    (这有两个原因:首先,因为子类化提出了一些关于如何在其派生类中指定和执行基类契约的讨厌问题。第二,和这就是 Haskell 类型类的影响,多态性不需要子类化。)

  2. 尽可能避免使用实例方法。如果可以使用扩展方法完成,则首选这些方法。
    (这是为了帮助保持接口紧凑:通过其他实例方法的组合可以完成的所有事情都成为扩展方法。剩下的接口是核心功能,尤其是状态改变方法。)

问题:我对第二条准则有疑问。考虑一下:

interface IApple { }
static void Eat(this IApple apple)
{
    Console.WriteLine("Yummy, that was good!");
}

interface IRottenApple : IApple { }
static void Eat(this IRottenApple apple)
{
    Console.WriteLine("Eat it yourself, you disgusting human, you!");
}

sealed class RottenApple : IRottenApple { }
IApple apple = new RottenApple();
// API user might expect virtual dispatch to happen (as usual) when 'Eat' is called:
apple.Eat(); // ==> "Yummy, that was good!"

显然,对于预期的结果 ("Eat it yourself…"),Eat 应该是常规实例方法。

问题:关于使用扩展方法与(虚拟)实例方法的更精确/更准确的指南是什么?什么时候使用扩展方法来“编程到接口”走得太远了?在什么情况下实际需要实例方法?

我不知道是否有任何明确的通用规则,所以我不期待一个完美的、普遍的答案。对上述准则 (2) 的任何有充分理由的改进表示赞赏。

【问题讨论】:

  • 我认为一个更好的起点是IEnumerable<T>,而不是一个人为的例子。
  • 好吧,你已经 kindof 违反了规则 #1,使 IRottenApple 扩展 IApple...(尽管它只是声明 class 继承)
  • @John:我不这么认为。我相信 interface 继承避免了许多子类化问题。 (例如:是否必须从派生类调用重写的方法?当您禁止子类化时,这个问题就消失了。)

标签: c# interface extension-methods api-design


【解决方案1】:

您的指导方针已经足够好:它已经说“尽可能”。因此,任务实际上是在更多细节中拼出“尽可能”这一点。

我使用这个简单的二分法:如果添加方法的目的是隐藏子类之间的差异,则使用扩展方法;如果目的是突出差异,请使用虚拟方法。

您的Eat 方法是引入子类之间差异的方法的一个示例:吃(或不吃)苹果的过程取决于它是哪种苹果。因此,您应该将其实现为实例方法。

尝试隐藏差异的方法示例是ThrowAway

public static void ThrowAway(this IApple apple) {
    var theBin = RecycleBins.FindCompostBin();
    if (theBin != null) {
        theBin.Accept(apple);
        return;
    }
    apple.CutUp();
    RecycleBins.FindGarbage().Accept(apple);
}

如果扔掉苹果的过程是相同的,不管苹果是什么品种,那么该操作是在扩展方法中实现的主要候选对象。

【讨论】:

  • +1。用于隐藏与突出显示的区别。旁注我认为您应该说“使用接口方法”。而不是“使用虚拟方法”。
  • +1 和 :-D。但是,通过多态性进行抽象的目的不就是让不同的实现从外表上看看起来一样吗?也就是说,适当的抽象不是总是鼓励诸如ThrowAway之类的差异隐藏方法吗?
  • 关于我之前的评论:您答案的最后一段为区分您的两种情况提供了很好的指导。我在上面的评论中提到了 “不同的实现”,而您给出的示例是实现总是相同的。感谢您的精彩回答。
【解决方案2】:

对我来说,预期的输出是正确的。您将变量类型转换为(可能使用错误)作为 IApple。

例如:

IApple apple = new RottenApple();
apple.Eat();  // "Yummy, that was good!"
IRottenApple apple2 = new RottenApple();
apple2.Eat(); // "Eat it yourself, you disgusting human, you!"
var apple3 = new RottenApple();
apple.Eat();  // "Eat it yourself, you disgusting human, you!"

问题:关于使用扩展方法与(虚拟)实例方法的更精确/更准确的指南是什么?什么时候使用扩展方法来“编程到接口”走得更远?在什么情况下实际需要实例方法?

只是我在开发应用程序时的个人意见:

当我编写我可能或其他人可能使用的东西时,我会使用实例方法。这是因为它是对实际类型的要求。考虑带有方法Fly() 的接口/类FlyingObject。这是飞行物体的基本基本方法。创建扩展方法真的没有意义。

我使用(很多)扩展方法,但这些都不是使用它们扩展的类的要求。例如,我在int 上有一个扩展方法,它创建了一个SqlParameter(另外它是内部的)。将该方法作为 int 基类的一部分仍然没有任何意义,它实际上与 int 是什么或做什么无关。扩展方法是创建使用类/结构的可重用方法的视觉好方法。

【讨论】:

  • 我的例子让我担心的是,代码表明apple.Eat 会打印一个投诉,因为这就是虚拟调度(通常)情况下会发生的情况;但是,正如您正确指出的那样,扩展方法不能那样工作。 -- 假设吃苹果不是IApple 类型的基本操作,因此Eat 被正确地实现为扩展方法。但是我的指导方针(2)有什么问题呢?为什么会导致我陷入如此意外的境地?我将如何调整它,以便在有人遇到它之前检测并防止这些
  • 如果 Apple 和 RottenApple 放在一个篮子里会怎样 ?他们都很好吃!
  • @stakx 我认为扩展方法实际上只是静态方法只是一个基本的理解。因此,当编译器尝试搜索方法签名时,它会尽可能地找到匹配的类型。 IRottenAppleIApple 更接近匹配,因此使用 IRottenApple 的方法将在 IApple 上使用。
  • @David B:这正是我的问题。 @Erik:你的解释对我来说似乎不正确。编译器通过查看包含this 对象的变量的静态(编译时)类型来选择要调用的扩展方法。如果该变量被声明为IApple,编译器将从不考虑为更多派生类型定义的扩展方法(反之亦然)。
猜你喜欢
  • 2013-09-17
  • 1970-01-01
  • 2017-11-24
  • 1970-01-01
  • 2012-12-28
  • 2011-03-12
  • 2011-01-14
  • 1970-01-01
  • 2010-10-24
相关资源
最近更新 更多