背景:本着"program to an interface, not an implementation"和Haskell type classes的精神,作为编码实验,我正在考虑创建一个主要成立的API意味着什么关于接口和扩展方法的组合。我有两条准则:
尽可能避免类继承。接口应实现为sealed class
es。
(这有两个原因:首先,因为子类化引发了一些关于如何在派生类中指定和强制执行基类'契约的讨厌问题。其次,这就是Haskell类型类的影响,多态不需要子类化。)子>
尽可能避免使用实例方法。如果可以使用扩展方法,则首选这些方法 (这是为了帮助保持接口紧凑:通过其他实例方法的组合可以完成的一切都成为扩展方法。接口中剩下的是核心功能,特别是状态改变方法。)子>
问题:我遇到了第二条准则的问题。考虑一下:
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)的任何有争议的改进都表示赞赏。
答案 0 :(得分:6)
你的指导原则很好:它已经说“尽可能”。因此,任务实际上是在一些更详细的信息中详细说明“尽可能”的位。
我使用这个简单的二分法:如果添加方法的目的是隐藏子类之间的差异,请使用扩展方法;如果目的是突出差异,请使用虚拟方法。
你的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)
对我来说,预期的输出是正确的。您将变量类型化(可能使用该错误)作为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!"
问题:关于扩展方法与(虚拟)实例方法的使用,哪些是精确/更准确的指南?什么时候使用扩展方法“编程到接口”远吗?在什么情况下实际需要实例方法?
开发应用程序时的个人意见:
当我写一些我可能或其他人可能会消费的内容时,我会使用实例方法。这是因为它是实际类型的要求。考虑具有方法FlyingObject
的接口/类Fly()
。这是飞行物体的基本基本方法。创建扩展方法确实没有意义。
我使用(很多)Extension方法,但这些方法从不是使用它们扩展的类的要求。例如,我在int
上有一个扩展方法,它创建了一个SqlParameter
(另外它是内部的)。将该方法作为int的基类的一部分仍然没有意义,它实际上与int是什么或做什么无关。扩展方法是创建一个消耗类/结构的可重用方法的视觉上很好的方法。
答案 2 :(得分:0)
我注意到C#扩展方法可以通过以下方式与C ++非成员非友元函数非常相似:Scott Meyers和Herb Sutter都声称在C ++中,封装有时会增加通过不使一个函数成为一个类成员:
“在可能的情况下,更喜欢将函数编写为非成员非朋友。” - Summary of Herb Sutter's GotW #84
(萨特在他的article about the Interface Principle证明了这种方法。)
早在1991年,Scott Meyers甚至制定了一个算法,用于决定一个函数应该是一个成员函数,一个朋友函数,还是一个非成员非朋友函数:
如果(
f
需要虚拟)
使f
成为C
的成员函数;
其他如果(f
为operator>>
或operator<<
)
使f
成为非成员函数;
如果(f
需要访问C
的非公开成员) 让f
成为C
的朋友;
其他如果(f
需要在其最左侧参数上进行类型转换)
使f
成为非成员函数;
如果(f
需要访问C
的非公开成员) 让f
成为C
的朋友;
其他如果(f
可以通过C
的公共接口实现)
使f
成为非成员函数;
的其他强>
使f
成为C
的成员函数;
其中一些显然特定于C ++,但是为C#语言找到类似的算法应该相当容易。 (首先,friend
可以使用internal
访问修饰符在C#中近似;“非成员函数”可以是扩展方法或其他静态方法。)
该算法未说明的是f
“何时或为何需要虚拟”。 @dasblinkenlight's answer在某种程度上解释了这一点。
有关Stack Overflow的相关问题: