C#中的扩展方法重载,是否有效?

7

有一个类,其中包含一个方法,如下所示:

class Window {
    public void Display(Button button) {
        // ...
    }
}

可以通过另一个更广泛的方法来重载该方法吗,例如:

class WindowExtensions {
    public void Display(this Window window, object o) {
        Button button = BlahBlah(o);
        window.Display(button);
    }
}

当我尝试时,发生了无限递归。有没有办法使它正常工作?我希望只有在其他方法无法调用时才调用扩展方法。


2
你额外得到的是一个极其糟糕的架构 ;) SCNR - Thorsten79
5个回答

9
让我们来看一下规范。首先,我们必须理解方法调用的规则。大致上,你从实例所指示的类型开始。你往上找可访问的方法,然后进行类型推断和重载解析规则,如果成功就调用该方法。只有在找不到这样的方法时,才会尝试将该方法作为扩展方法处理。所以从§7.5.5.2(扩展方法调用)中可以看到,特别是粗体语句:

在以下方法调用形式中之一:

expr.identifier()

expr.identifier(args)

expr.identifier<typeargs>()

expr.identifier<typeargs>(args)

如果对调用的常规处理未找到适用的方法,则尝试将构造处理为扩展方法调用

其后面的规则有点复杂,但对于你提供给我们的简单情况来说,非常简单。如果没有适用的实例方法,则会调用扩展方法WindowExtensions.Display(Window, object)。如果窗口Display的参数是按钮或可以隐式转换为按钮,则实例方法是适用的。否则,将调用扩展方法(因为从object派生的所有内容都可以隐式转换为object)。

所以,除非你遗漏了重要的部分,否则你想做的事情将会起作用。

因此,请考虑以下示例:

class Button { }
class Window {
    public void Display(Button button) {
        Console.WriteLine("Window.Button");
    }
}

class NotAButtonButCanBeCastedToAButton {
    public static implicit operator Button(
        NotAButtonButCanBeCastedToAButton nab
    ) {
        return new Button();
    }
}

class NotAButtonButMustBeCastedToAButton {
    public static explicit operator Button(
        NotAButtonButMustBeCastedToAButton nab
    ) {
        return new Button();
    }
}

static class WindowExtensions {
    public static void Display(this Window window, object o) {
        Console.WriteLine("WindowExtensions.Button: {0}", o.ToString());
        Button button = BlahBlah(o);
        window.Display(button);
    }
    public static Button BlahBlah(object o) {
        return new Button();
    }
}

class Program {
    static void Main(string[] args) {
        Window w = new Window();
        object o = new object();
        w.Display(o); // extension
        int i = 17;
        w.Display(i); // extension
        string s = "Hello, world!";
        w.Display(s); // extension
        Button b = new Button();
        w.Display(b); // instance
        var nab = new NotAButtonButCanBeCastedToAButton();
        w.Display(b); // implicit cast so instance
        var nabexplict = new NotAButtonButMustBeCastedToAButton();
        w.Display(nabexplict); // only explicit cast so extension
        w.Display((Button)nabexplict); // explictly casted so instance
    }
}

这将会打印出来。
WindowExtensions.Button: System.Object
Window.Button
WindowExtensions.Button: 17
Window.Button
WindowExtensions.Button: Hello, world!
Window.Button
Window.Button
Window.Button
WindowExtensions.Button: NotAButtonButMustBeCastedToAButton
Window.Button
Window.Button

在控制台中。


w.Display(b); // implicit cast so instance

should have been:

w.Display( **nab**); // implicit cast so instance
- Pete

2

虽然有可能,但是在重载参数上需要小心 - 最好避免使用object类型,因为这经常导致混乱的代码。您可能会遇到C#选择重载的有趣方式。它会选择可以隐式转换的类型中更接近的匹配,而不是具有精确匹配的“更远”的匹配(请参见此问题)。

Button myButton = // get button
Window currentWindow = // get window

// which method is called here?
currentWindow.Display( myButton );

您希望代码相当清晰,特别是在一年或更长时间后重新查看此代码时,需要知道调用了哪个重载。

扩展方法提供了一种非常优雅的方式来扩展对象的功能。您可以添加它原本没有的行为。但是您必须小心使用它们,因为它们很容易创建混乱的代码。最佳实践是避免使用已经被使用的方法名称,即使它们是明确的重载,因为它们不会在Intellisense中显示在类后面。

这里的问题似乎是扩展方法可以隐式将您的按钮转换为对象,因此选择自己作为最佳匹配项,而不是实际的显示方法。您可以将扩展方法显式调用为普通静态调用,但是无法强制其调用基础类的方法。

我建议更改扩展方法的名称:

object somethingToMakeIntoAButton = // get object
Window currentWindow = // get window

// which method is called here?
currentWindow.DisplayButton( somethingToMakeIntoAButton );

接着...

class WindowExtensions 
{
    public void DisplayButton(this Window window, object o) 
    {
        Button button = BlahBlah(o);

        // now it's clear to the C# compiler and human readers
        // that you want the instance method
        window.Display(button);
    }
}

或者,如果扩展方法的第二个参数是一个不能隐式转换为 Button 的类型(比如 intstring),这种混淆也不会发生。


更改方法名称是一个好建议。但是,如果您尝试运行Jason提供的示例,您会发现扩展方法并没有选择自己作为最佳匹配项,而是编译器从扩展方法中选择要调用的实例方法。 - AlexD
那么它就不应该出现递归错误,但无论如何,我认为建议仍然适用:当参数类型可以从彼此推导时,重载总是一个坏主意。 - Keith

1

应该可以工作,编译器几乎总是会选择具有可接受签名的实例方法,而不是具有完全相同签名的扩展方法。

根据这个文章

使用扩宽转换的可接受签名的实例方法几乎总是优先于具有精确签名匹配的扩展方法。如果这导致绑定到实例方法时您真正想要使用扩展方法,则可以使用共享方法调用约定显式调用扩展方法。这也是消除两种方法都不太具体的方法的方式。

你确定你在明确传递一个Button吗?

还是void Display(Button button)在递归调用自己?


2
+1,但我认为问题在于“几乎总是优先使用”。在扩展方法内部,它会优先选择自身,并使用从Buttonobject的隐式转换。因此产生了递归。 - Keith

0

嗯,我认为这有点棘手。如果您将Button作为方法参数传递:

Button button = BlahBlah(o);
window.Display(button);

然后有一个适合的类方法,它总是优先于扩展方法。

但是,如果您传递的对象不是Button,那么就没有适合的类方法,而会调用扩展方法。

var o = new object();
window.Display(o);

从我所看到的,你的示例应该可以正常工作,扩展方法将在Window实例上调用Display方法。无限循环可能是由其他代码引起的。

有没有可能你的示例中包含Display方法的Window类和作为扩展方法参数的Window类实际上是两个不同的类?


除了 window.Display(new Button()); 会实际调用扩展方法,这就是他出现无限递归的原因。 - Keith
@Keith:不应该这样; window.Display(new Button()) // window is Window 应该调用 Window.Display(Button) - jason
@Keith:嗯...实际上,对我来说window.Display(new Button())很好用 - 它只是调用实例方法。 - AlexD
这里的诀窍是他在扩展方法中调用 window.Display() 实现递归。 - Pavel Minaev
@Jason:我认为他理解了递归,因为通常window.Display(new Button());会触发实例方法,但在扩展方法内部它会选择自己。我以前在C#编译器中见过类似的问题,但我已经重新创建了这个问题,所以可能是错误的。 - Keith

0

抱歉,这个答案是不正确的。请查看本帖中的其他许多答案。 - jason

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接