在 C# 中重载扩展方法,它有效吗?

Posted

技术标签:

【中文标题】在 C# 中重载扩展方法,它有效吗?【英文标题】:Extension methods overloading in C#, does it work? 【发布时间】:2011-01-08 06:02:14 【问题描述】:

拥有一个有方法的类,像这样:

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

是否可以用另一个更广泛的方法重载该方法,如下所示:

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

当我尝试时发生的事情是我有无限递归。有没有办法让它工作?我希望仅在无法调用其他方法时才调用扩展方法。

【问题讨论】:

你得到的额外是一个非常糟糕的架构;) SCNR 【参考方案1】:

这是不可能的(另请参阅 Monkeypatching For Humans)- 可能使用 DLR 和 method_missing

【讨论】:

对不起,这个答案不正确。有可能的。请参阅此线程中的许多其他答案。【参考方案2】:

让我们看一下规范。首先,我们必须了解方法调用的规则。粗略地说,您从尝试调用方法的实例所指示的类型开始。您沿着继承链向上寻找可访问的方法。然后,您执行类型推断和重载解析规则,如果成功则调用该方法。只有在没有找到这样的方法时,您才会尝试将该方法作为扩展方法处理。因此,从第 7.5.5.2 节(扩展方法调用)看,尤其是粗体声明:

在一种形式的方法调用(§7.5.5.1)中

expr.identifier()

expr.identifier(args)

expr.identifier<typeargs>()

expr.identifier<typeargs>(args)

如果调用的正常处理找不到适用的方法,则尝试将构造作为扩展方法调用处理

除此之外的规则有点复杂,但对于您向我们展示的简单案例来说,它非常简单。如果没有适用的实例方法,则将调用扩展方法WindowExtensions.Display(Window, object)。如果Window.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 --- 应该是:--- w.Display( **nab**); // implicit cast so instance【参考方案3】:

嗯,我相信这有点棘手。如果您将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:不,不应该; window.Display(new Button()) // window is Window 应该调用 Window.Display(Button) @Keith: 嗯.. 实际上window.Display(new Button()) 对我来说工作得很好——它只是调用实例方法。 这里的诀窍是他从扩展方法中调用window.Display() 来获得递归。 @Jason:我认为他得到了递归,因为通常window.Display(new Button()); 会命中实例方法,但在扩展方法中它会选择自己。我在 C# 编译器中看到过与之前类似的问题,但我重新创建了这个问题,所以可能是错误的。【参考方案4】:

这是可能的,尽管您必须小心重载的参数 - 通常最好避免使用 object 类型,因为这通常会导致代码混乱。您可能会犯 C# 选择重载的有趣方式。它将选择一个“更接近”的匹配类型,该类型可以隐式转换为具有完全匹配的“进一步”匹配(参见this question)。

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

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

您希望您的代码相当清晰,尤其是在一年左右返回此代码时,调用的是什么重载。

扩展方法提供了一种非常优雅的方式来扩展对象的功能。您可以添加它最初没有的行为。但是你必须小心它们,因为它们很容易产生令人困惑的代码。最好避免已使用的方法名称,即使它们是明显的重载,因为它们不会出现在智能感知中的类之后。

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

我会更改扩展方法的名称:

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 提供的示例,您会发现 extension method 并没有选择自己作为最佳匹配,而是 compiler 从扩展方法中选择要调用的实例方法。 那么它不应该得到递归错误,但无论哪种方式,我认为建议都是:参数类型可以相互派生的重载总是一个坏主意。【参考方案5】:

这应该可行,编译器几乎总是会选择具有可接受签名的实例方法,而不是具有确切签名的扩展方法。

据此article:

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

你确定你明确地传递了一个按钮吗?

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

【讨论】:

+1,但我认为这里的问题是“几乎总是首选”。在扩展方法中,它更喜欢自己,隐式转换从Buttonobject。因此递归。

以上是关于在 C# 中重载扩展方法,它有效吗?的主要内容,如果未能解决你的问题,请参考以下文章

Dart 有像 c# 这样的扩展方法吗?

C# 中的 .Any 和 .Count 更有效的是啥(扩展方法)[重复]

如何使用 c# 扩展方法扩展类? [复制]

你真的了解扩展方法吗?

Kotlin扩展函数与重载操作符解析

Kotlin扩展函数与重载操作符解析