这个问题很奇怪。似乎是“当我知道如何以简单的方式做事时,为什么我会以困难的方式做事?”您不会,所以问为什么会是一件奇怪的事情。当您控制要扩展的类型时,通常不会将实现放入扩展方法中。在这种情况下,并未发明扩展方法。它们是在相反的情况下发明的,在这种情况下,进行扩展的人员无法控制类型。
但是,让我们以原始海报为准,看看即使在我们控制所有定义的情况下,将功能放入扩展方法中也是合理的情况。对我来说尚不清楚为什么原始海报正在寻找使用此模式的借口,但让我们不必担心。
这是一个遵循逻辑:
假设我们有一个通用类型,希望以协变或逆变方式使用。
C#仅在接口和委托上支持通用差异,因此变体部分必须进入接口。
C#中的接口尚不能支持实现。 (这是我在C#中缺少的少数Java功能之一,并且已被提议用于该语言的未来修订版。在我撰写本文时,该功能处于C#8的测试版本中。)
特征的签名将阻止所需的差异注释。
可以将所需功能实现为扩展方法。
在那种情况下,即使我们控制类和接口,也可以将功能实现为扩展方法是合理的。
那可能很抽象。让我们看一个例子。我们希望实现一个可变的不可变堆栈。这是我们的首次尝试:
public abstract class Stack<T>
{
private Stack() {}
public static readonly Stack<T> Empty = new EmptyStack();
private sealed class EmptyStack : Stack<T>
{
public override bool IsEmpty => true;
public override T Peek() => throw new Exception("Empty stack");
public override Stack<T> Pop() => throw new Exception("Empty stack");
}
private sealed class Regular : Stack<T>
{
private readonly T head;
private readonly Stack<T> tail;
public Stack(T head, Stack<T> tail)
{
this.head = head;
this.tail = tail;
}
public override bool IsEmpty => false;
public override T Peek() => head;
public override Stack<T> Pop() => tail;
}
public abstract bool IsEmpty { get; }
public abstract T Peek();
public abstract Stack<T> Pop();
public Stack<T> Push(T head) => new Regular(head, this);
}
好,第一个问题:我们不能在类类型上使用方差。它必须是一个接口。什么界面?
public interface IStack<out T>
{
bool IsEmpty { get; }
IStack<T> Pop();
T Peek();
到目前为止,协方差没有问题。如果有
IStack<Mammal>
,则可以将其用作
IStack<Animal>
,因为当我们偷看一堆狮子,老虎和熊时,每次都会得到一只动物。
但是
Push
呢?我们不能写
IStack<T> Push(T t);
因为现在T在无效位置使用。
但是,让我们考虑一下。假设我们有一个
IStack<Turtle>
。我们可以把长颈鹿推上去吗?听起来不对;您不能将长颈鹿放入海龟列表中,那么为什么您应该能够将长颈鹿推到一堆海龟上呢?
但这确实有效:一堆乌龟就是一堆动物,我们可以将长颈鹿推上去。所以我们需要的是:
public interface IStack<out T>
{
...
IStack<U> Push<U>(U u) where T : U;
}
在C#中是非法的;没有这样的“向后”约束。 (再次,Java可以做得更好的少数领域之一。)
好的,我们在界面中没有想要的
Push
。那我们该如何推入堆栈呢?我们可以通过仅对类型进行较小的更改来做到这一点:
public abstract class Stack<T> : IStack<T>
{
private Stack() {}
public static readonly Stack<T> Empty = new EmptyStack();
private sealed class EmptyStack : Stack<T>
{
public override bool IsEmpty => true;
public override T Peek() => throw new Exception("Empty stack");
public override IStack<T> Pop() => throw new Exception("Empty stack");
}
private sealed class Regular : Stack<T>
{
private readonly T head;
private readonly IStack<T> tail;
public Stack(T head, IStack<T> tail)
{
this.head = head;
this.tail = tail;
}
public override bool IsEmpty => false;
public override T Peek() => head;
public override IStack<T> Pop() => tail;
}
public abstract bool IsEmpty { get; }
public abstract T Peek();
public abstract IStack<T> Pop();
public static IStack<T> Push(T head, IStack<T> tail) =>
new Regular(head, tail);
}
超。呼叫站点是什么样的?
IStack<Turtle> s1 = Stack<Turtle>.Empty;
IStack<Turtle> s2 = Stack<Turtle>.Push(someTurtle, s1);
IStack<Animal> s3 = Stack<Turtle>.Push(anotherTurtle, s2);
IStack<Animal> s4 = Stack<Animal>.Push(someGiraffe, s3);
完全可以。我们用一堆乌龟作为一堆动物,然后将长颈鹿推上去。但是,噢,我的天哪,看看那个呼叫站点有多可怕!
我们需要的是一种将类型实参移出调用站点的方法,但是我们可以使用扩展方法来做到这一点!
public static IStack<T> Push<T>(this IStack<T> s, T t) =>
Stack<T>.Push(t, s);
现在我们的呼叫站点是什么样的?
IStack<Turtle> s1 = Stack<Turtle>.Empty;
IStack<Turtle> s2 = s1.Push(someTurtle);
IStack<Animal> s3 = s2.Push(anotherTurtle);
IStack<Animal> s4 = s3.Push(someGiraffe);
好多了。更好的是:
var s3 = Stack<Turtle>.Empty.Push(someTurtle).Push(anotherTurtle);
var s4 = s3.Push((Animal)someGiraffe);
(强制转换的要求有点不幸; C#类型推断不会推断“我有一只乌龟和长颈鹿,开发人员可能是动物”。相反,它将推断“我有一只乌龟和长颈鹿,但我没有知道选择哪个。”强制转换可帮助编译器解决歧义。
因此,回答您的问题:为什么可以在修改类时实施扩展方法?如果修改类使您陷入呼叫站点必须丑陋的情况,那么您会这样做,但是添加巧妙的扩展方法可以带来愉悦,流畅,令人愉悦的用户体验。