Какова мотивация "Использовать методы расширения?"?

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

Microsoft говорит: "В целом мы рекомендуем использовать методы расширения экономно и только тогда, когда вам нужно". И все же методы расширения составляют основу Linq; на самом деле, Linq был причиной, по которой были созданы методы расширения.

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

Ответ 1

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

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

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

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

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

Тьфу. "для" циклов являются общепринятыми и идиоматическими в С#; не получают симпатичные.

Ответ 2

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

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

 myObject.Foo();

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

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

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

Ответ 3

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

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

var result = myFoo + myBar;

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

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

var result = myFoo.DoSomething();

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

Ответ 6

У меня растущая библиотека методов расширения, которые работают на классах 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:

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

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

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

Ответ 7

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

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

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