Перегрузка методов расширения в C #, это работает? - PullRequest
6 голосов
/ 22 января 2010

Наличие класса, у которого есть метод, например:

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

возможно ли перегрузить метод другим, более широким, например:

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

Что случилось, когда я попробовал, так это то, что у меня бесконечная рекурсия. Есть ли способ заставить эту работу? Я хочу, чтобы метод расширения вызывался только тогда, когда другой метод не может быть вызван.

Ответы [ 5 ]

9 голосов
/ 22 января 2010

Давайте перейдем к спецификации. Во-первых, мы должны понять правила для вызова методов. Грубо говоря, вы начинаете с типа, указанного экземпляром, для которого вы пытаетесь вызвать метод. Вы идете вверх по цепочке наследования в поисках доступного метода. Затем вы делаете вывод типа и правила разрешения перегрузки и вызываете метод, если это удается. Только если такой метод не найден, вы пытаетесь обработать метод как метод расширения. Так из §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

на консоли.

2 голосов
/ 22 января 2010

Это возможно, хотя вы должны быть осторожны с параметрами перегрузок - обычно рекомендуется избегать типов 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 (скажем, int или string), эта путаница также не произошла бы.

1 голос
/ 22 января 2010

Это должно сработать, компилятор почти всегда выбирает метод экземпляра с приемлемой сигнатурой, а не метод расширения с точной сигнатурой.

Согласно этой статье :

Метод экземпляра с приемлемым подпись с использованием расширяющегося преобразования почти всегда будет предпочтительнее, чем метод расширения с точным подпись матча. Если это приводит к привязка к методу экземпляра, когда вы действительно хотите использовать расширение метод, вы можете явно вызвать метод расширения с использованием общего соглашение о вызове метода. Это также способ избавиться от двух методы, когда ни один из них не является более конкретным.

Вы уверены, что явно передаете кнопку?

Или void Display(Button button) рекурсивно вызывает себя?

0 голосов
/ 22 января 2010

Ну, я считаю, что это немного сложно. Если вы передадите Button в качестве параметра метода:

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

тогда есть подходящий метод класса, который всегда имеет приоритет над методом расширения.

Но если вы передадите объект, который не является Button, подходящего метода класса не будет, и метод расширения будет вызван.

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

Итак, из того, что я вижу, ваш пример должен работать правильно, и метод расширения вызовет метод Display для экземпляра Window. Бесконечный цикл может быть вызван каким-то другим кодом.

Есть ли вероятность, что класс Window, содержащий метод Display в вашем примере, и класс Window, являющийся параметром метода расширения, на самом деле являются двумя разными классами?

0 голосов
/ 22 января 2010

Это невозможно (см. Также Обезьяны для людей ) - возможно, с DLR и method_missing.

...