Какова мотивация «Использовать методы расширения экономно?» - PullRequest
11 голосов
/ 26 марта 2010

Я считаю, что это очень естественный способ расширить существующие классы, особенно когда вам просто нужно "приварить" некоторые функциональные возможности к существующему классу.

Microsoft говорит: «В общем, мы рекомендуем вам применять методы расширения экономно и только тогда, когда это необходимо». И все же методы расширения составляют основу Linq; На самом деле, именно из-за Linq были созданы методы расширения.

Существуют ли конкретные критерии проектирования, в которых использование методов расширения предпочтительнее наследования или композиции? По каким критериям они не поощряются?

Ответы [ 7 ]

17 голосов
/ 26 марта 2010

Мы показываем предлагаемые новые языковые возможности (по крайней мере, те, которые имеют приличный шанс увидеть свет) для MVP C # для ранней обратной связи. Некоторые отзывы, которые мы получаем очень часто по многим функциям, таковы: «Хорошо Я будет использовать эту функцию разумно и в соответствии с принципами разработки этой функции, но мои коллеги по глупости собираются сойти с ума схожу с ума от этой штуки и напишу массу не поддерживаемого, непонятного кода, который я собираюсь застрять в отладке в течение следующих десяти лет, так что, насколько я хочу, пожалуйста, никогда не реализуйте эту функцию! "

Хотя я несколько преувеличиваю из-за комедийного эффекта, к этому мы относимся очень серьезно; мы хотим, чтобы дизайн новых функций поощрял их использование, но препятствовал их неправильному использованию.

Мы обеспокоены тем, что методы расширения будут использоваться, например, для произвольного расширения «объекта» произвольно и, таким образом, для создания больших шаров слабо типизированной грязи, которые будет трудно понять, отладить и поддерживать.

Меня лично особенно волнуют "милые" методы расширения, которые вы видите в "свободном" программировании. Вещи как

6.Times(x=>Print("hello"));

Тьфу. циклы for универсально понятны и идиоматичны в C #; не становись милым .

11 голосов
/ 26 марта 2010

Проблема в том, что методы расширения не обязательно понятны.

Когда вы видите что-то подобное в коде:

 myObject.Foo();

Естественный инстинкт состоит в том, что Foo() - это метод, определенный в классе myObject, или родительский класс класса myObject.

С методами расширения это просто не так. Метод может быть определен почти ЛЮБОЙ, почти для любого типа (вы можете определить метод расширения в System.Object, хотя это плохая идея ...). Это действительно увеличивает стоимость обслуживания вашего кода, так как снижает вероятность обнаружения.

Каждый раз, когда вы добавляете неочевидный код или даже уменьшаете «очевидность» реализации, возникают затраты, связанные с долгосрочным обслуживанием.

6 голосов
/ 26 марта 2010

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

Давайте сначала посмотрим на пример перегрузки операторов. Когда вы видите код:

var result = myFoo + myBar;

Вы хотели бы надеяться, что myFoo и myBar являются числовыми типами, а operator + выполняет сложение. Это логично, интуитивно понятно, легко понять. Но как только перегрузка оператора входит в картину, вы больше не можете быть уверены в том, что на самом деле делает myFoo + myBar - оператор + может быть перегружен на , что означает что-то . Вы не можете просто прочитать код и понять, что происходит, не читая весь код, лежащий в основе типов, участвующих в выражении. Теперь operator + уже перегружен для таких типов, как String или DateTime, однако существует интуитивно понятное толкование того, что означает добавление в этих случаях. Что еще более важно, в этих распространенных случаях это добавляет много выразительной силы в код. Так что это стоит потенциальной путаницы.

Так, что все это имеет отношение к методам расширения? Ну, методы расширения создают похожую ситуацию. Без методов расширения, когда вы видите:

var result = myFoo.DoSomething();

Вы можете предположить, что DoSomething() является либо методом myFoo, либо одним из его базовых классов. Это просто, легко понять, даже интуитивно понятно. Но с помощью методов расширения DoSomething() может быть определено где угодно - и, что еще хуже, определение зависит от набора using операторов в файле кода и может включать в себя потенциально много классов, любой из который может содержать реализацию DoSomething().

Теперь не поймите меня неправильно. Как перегрузка операторов, так и методы расширения являются полезными и мощными языковыми функциями. Но помните - с великой силой приходит большая ответственность . Вы должны использовать эти функции, когда они улучшают ясность или возможности реализации. Если вы начнете использовать их без разбора, они добавят путаницы, сложности и, возможно, дефектов в то, что вы пытаетесь создать.

3 голосов
/ 26 марта 2010

Я следую очень простому правилу с методами расширения и вспомогательными классами.

Каждый раз, когда у меня есть метод, который может быть передан статическому вспомогательному классу, я буду оценивать его полезность в общем объеме. Действительно ли я хочу, чтобы этот метод был доступен? Является ли значение ясным и полезным для других?

Если я могу ответить да на этот вопрос, я создам его как метод расширения. Где у меня есть общая библиотека, которая, помимо прочего, содержит все мои методы Extension в 1 пространстве имен, где каждый файл класса определен как TypeNameExtension, поэтому становится очень ясно, чего ожидать внутри этого класса, чтобы вы могли легко найти код.

Если я поставлю под сомнение необходимость использования вспомогательного метода в качестве общедоступного использования, я объявлю метод private static и оставлю его внутри класса-владельца.

2 голосов
/ 26 марта 2010

У меня есть растущая библиотека методов расширения, которые работают с классами BCL для упрощения общих проблем, которые на первый взгляд выглядят запутанными. Честно говоря, я думаю, что методы расширения делают вещи намного более читабельными. Например, возьмем проверку оператора if, чтобы увидеть, находится ли число между двумя числами:

if (x <= right && x >= left )
{
   // Do Something
}

Что если right меньше left в случае аварии? И что именно это делает? Когда переменные просто похожи на x и left, тогда их легко выполнить. Но что, если они являются свойствами длинного класса? Оператор if может стать довольно большим, что запутывает довольно простую вещь, которую вы пытаетесь сделать.

Итак, я написал это:

public static bool Between<T>(this T test, T minValue, T maxValue)
    where T : IComparable<T>
{
    // If minValue is greater than maxValue, simply reverse the comparision.
    if (minValue.CompareTo(maxValue) == 1)
        return (test.CompareTo(maxValue) >= 0 && test.CompareTo(minValue) <= 0);
    else
        return (test.CompareTo(minValue) >= 0 && test.CompareTo(maxValue) <= 0);
}

Теперь я могу улучшить свое утверждение if в этом:

if (x.Between(left, right))
{
   // Do Something
}

Это "необыкновенно"? Я так не думаю ... но я думаю, что это значительно улучшает читабельность и позволяет провести дополнительную проверку входных данных теста на безопасность.

Я думаю, что опасность, которую Microsoft хочет избежать, заключается в том, что люди пишут тонну расширенных методов, которые переопределяют Equals и ToString.

2 голосов
/ 26 марта 2010

Когда Борланд изобрел их (помощники класса Delphi), руководство уже было: «следует использовать только в исключительных ситуациях».

Они стали намного более приемлемыми (как часть LINQ), но они остаются странным способом добавления функциональности. Я думаю, у Рида есть хороший ответ на вопрос, почему.

1 голос
/ 26 марта 2010

Методы расширения можно рассматривать как ориентированные на аспект. Я полагаю, что Microsoft говорит, что если у вас есть исходный код, вы должны изменить исходный класс. Это связано с ясностью и простотой обслуживания. Причина, по которой методы расширения были необходимы, заключается в том, что они расширяют функциональность существующих объектов. В случае LINQ, если вы углубитесь в то, как он работает, объекты могут быть созданы с различными типами, которые не могут быть известны заранее. Использование методов расширения позволяет добавлять некоторые варианты поведения.

Например, имеет смысл расширять объекты платформы .NET, потому что у вас нет источника, но может быть какое-то поведение для добавления к объекту. Расширение строки, классов DateTime и FrameworkElement допустимо. Также может быть приемлемо создать методы расширения для классов, которые пересекают границы приложения. Например, если вы хотите использовать класс DTO и иметь определенное поведение пользовательского интерфейса, создайте этот метод расширения только в пользовательском интерфейсе.

У динамических языков парадигма иная, но я думаю, что вы говорите о C # и VB.net.

...