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

edit4:, так как это, похоже, больше перешло в дискуссию, чем конкретный вопрос.

В программировании на C++, как правило, считается хорошей практикой "предпочитать не-членские функции, отличные от друга" вместо методов экземпляра. Это было рекомендовано Скоттом Мейерсом в этой классической статье доктора Доббса, и повторил Херб Саттер и Андрей Александреску в Стандарты кодирования С++ (пункт 44); общий аргумент состоит в том, что если функция может выполнять свою работу исключительно, полагаясь на открытый интерфейс, открытый классом, она фактически увеличивает инкапсуляцию, чтобы она была внешней. Хотя это смущает "упаковку" класса в некоторой степени, выгоды, как правило, считаются заслуживающими внимания.

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

  • Интерфейсы позволяют классу формально указать свой публичный контракт, методы и свойства, которые они раскрывают миру.
  • Любой другой класс может выбрать один и тот же интерфейс и выполнить тот же контракт.
  • Методы расширения могут быть определены на интерфейсе, предоставляя любую функциональность, которая может быть реализована через интерфейс для всех исполнителей автоматически.
  • И лучше всего, из-за синтаксиса "синтаксиса экземпляра" и поддержки IDE, их можно вызывать так же, как и любой другой метод экземпляра, устраняя когнитивные издержки!

Таким образом, вы получаете преимущества инкапсуляции функций "не-член, не друг" с удобством членов. Кажется, лучшее из обоих миров для меня; сама библиотека .NET предоставляет яркий пример в LINQ. Однако, везде, где я смотрю, я вижу, что люди предупреждают о чрезмерном использовании метода расширения; даже сама страница MSDN заявляет:

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

( edit: Даже в текущей библиотеке .NET я вижу места, где было бы полезно иметь расширения вместо методов экземпляра - например, все служебные функции List<T> (Sort, BinarySearch, FindIndex и т.д.) было бы невероятно полезно, если бы они были подняты до IList<T> - получение бесплатных бонусных функций, как это добавляет намного больше преимуществ для реализации интерфейса.)

Так какой вердикт? Являются ли методы расширений методом инкапсуляции и повторного использования кода, или я просто обманываю себя?

( edit2: В ответ на Tomas - в то время как С# действительно начинал с менталитета OO (чрезмерно, imo) OO, он, кажется, обдумывает более многопараметрическое программирование с каждым новым выпуском; Цель этого вопроса заключается в том, полезно ли использование методов расширения для управления изменением стиля (в сторону более общего/функционального С#).)

edit3: методы расширенного расширения

Единственная реальная проблема, выявленная до сих пор при таком подходе, заключается в том, что вы не можете специализировать методы расширения, если вам нужно. Я думал о проблеме, и я думаю, что придумал решение.
Предположим, что у меня есть интерфейс MyInterface, который я хочу расширить -

Я определяю свои методы расширения в статическом классе MyExtension и свяжу его с другим интерфейсом, назовите его MyExtensionOverrider. Методы MyExtension определяются в соответствии с этим шаблоном:

public static int MyMethod(this MyInterface obj, int arg, bool attemptCast=true)
{
    if (attemptCast && obj is MyExtensionOverrider)
    {
        return ((MyExtensionOverrider)obj).MyMethod(arg);
    }
    // regular implementation here
}

Интерфейс переопределения отражает все методы, определенные в MyExtension, за исключением параметров this или attemptCast:

public interface MyExtensionOverrider
{
    int MyMethod(int arg);
    string MyOtherMethod();
}

Теперь любой класс может реализовать интерфейс и получить функциональные возможности по умолчанию:

public class MyClass : MyInterface { ... }

Любой, кто хочет переопределить его с помощью конкретных реализаций, может дополнительно реализовать интерфейс переопределения:

public class MySpecializedClass : MyInterface, MyExtensionOverrider
{
    public int MyMethod(int arg) 
    { 
        //specialized implementation for one method
    }
    public string MyOtherMethod() 
    {   // fallback to default for others
        MyExtension.MyOtherMethod(this, attemptCast: false); 
    }
}

И там мы идем: методы расширения, предоставляемые на интерфейсе, с возможностью полной расширяемости, если это необходимо. Полностью общий интерфейс сам по себе не должен знать о расширении/переопределении, а несколько пар расширения/переопределения могут быть реализованы без вмешательства друг в друга.

Я вижу три проблемы с этим подходом -

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

Тем не менее, несмотря на все это, я думаю, что это лучшее, что мы можем получить, пока не будет поддерживаться языковая поддержка функций интерфейса. Мысли?

Ответы

Ответ 1

Мне обычно нравятся методы расширения, особенно на интерфейсах, но у меня есть две проблемы:

Во-первых, если реализация имеет более эффективный способ достижения цели метода расширения, нет общего способа ее выражения. Например, Enumerable.Count() явно знает о ICollection/ICollection<T> и специальных случаях. Альтернативой для этого было бы, если бы интерфейсы могли фактически содержать реализации напрямую, ссылаясь только на другие методы интерфейса, а не на объявление полей. Затем методы могут быть переопределены в соответствующих реализациях. Это означает, что вам нужно владеть интерфейсом, конечно... но в некоторых случаях он был бы чище, чем существующие методы расширения. (Избегая возможности вводить поля, я считаю, что вы обошли некоторые проблемы с реализацией, которые представили бы множественное наследование классов.)

Во-вторых, мне не нравится способ обнаружения методов расширения. Невозможно сказать: "Я хочу методы расширения из класса X", не перетаскивая также методы расширения из других классов в одном и том же пространстве имен. Я хотел бы, чтобы вы могли написать:

using static System.Linq.Enumerable;

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

(Кстати, я буду больше говорить обо всех этих моментах в NDC 2010 в четверг. Надеюсь, разговор будет записан. )

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

Кстати, неплохо было бы писать методы внутри типа, но сказать: "Ограничьте меня только использованием общедоступного API".

Ответ 2

Я думаю, что С# следует немного другой логике - как и в Java, аксиома в С# заключается в том, что все является объектом, и вся функциональность должна быть инкапсулирована в классе (как методы). С# не является строгим - существуют типы значений, которые на самом деле не являются объектами, и есть статические члены, которые также не принадлежат ни одному объекту.

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

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

Было бы целесообразно проводить различие между членами, которые полагаются на частное государство напрямую и "производными" членами, которые реализуются с точки зрения публичных операций, но я не думаю, что эти методы расширения для этого великолепны. Возможно, можно было бы пометить такие методы некоторым атрибутом (например, UsesOnlyPublic) и написать некоторое правило FxCop, чтобы убедиться, что этот метод не нарушает политику...

Ответ 3

Методы расширения, похоже, прямо адресуют принцип инкапсуляции, который вы цитируете. Но я вижу опасность в их чрезмерном использовании. Без этой языковой функции у вас есть в основном два варианта реализации ваших "нечленовских друзей", которые дополняют данный интерфейс: статические служебные функции или класс обертки/прокси.

Я думаю, что проблема с подходом метода расширения заключается в том, что в основном это удобная синтаксическая конструкция вокруг статических функций полезности. Он просто предоставляет вам семантику метода для статической функции полезности.

Но при отсутствии методов расширения, я думаю, большинство из них согласилось бы, что классы wrapper/proxy - лучший способ реализовать ваши функции "не-член-не-друг". Я думаю, что это действительно сводится к органическому росту вашей кодовой базы объектно-ориентированным способом. Класс, который вы делаете сегодня как простая оболочка/прокси, может завтра превратиться в компонент первого класса в вашей системе. Это настоящий класс, поэтому он может иметь собственные элементы и потенциально увеличиваться в масштабах наряду с расширяющимися вариантами использования.

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