Эта статья написана как личное размышление, личное эссе или аргументативное эссе , в котором излагаются личные чувства редактора Википедии или излагается оригинальный аргумент по теме. ( Май 2013 ) |
В объектно-ориентированном программировании метод расширения — это метод, добавленный к объекту после компиляции исходного объекта . Измененный объект часто является классом, прототипом или типом. Методы расширения являются функциями некоторых объектно-ориентированных языков программирования. Синтаксической разницы между вызовом метода расширения и вызовом метода, объявленного в определении типа, нет. [1]
Однако не все языки реализуют методы расширения одинаково безопасным образом. Например, такие языки, как C#, Java (через Manifold, Lombok или Fluent) и Kotlin, не изменяют расширенный класс каким-либо образом, поскольку это может нарушить иерархию классов и помешать диспетчеризации виртуальных методов. Вместо этого эти языки реализуют методы расширения строго статически и используют статическую диспетчеризацию для их вызова.
Методы расширения являются функциями многочисленных языков, включая C# , Java через Manifold или Lombok или Fluent, Gosu , JavaScript , Oxygene , Ruby , Smalltalk , Kotlin , Dart , Visual Basic.NET и Xojo . В динамических языках, таких как Python , концепция метода расширения не нужна, поскольку классы (за исключением встроенных классов) могут быть расширены без какого-либо специального синтаксиса (подход, известный как « monkey-patching », используемый в библиотеках, таких как gevent).
В VB.NET и Oxygene они распознаются по наличию extension
ключевого слова или атрибута " ". В Xojo Extends
ключевое слово " " используется с глобальными методами.
В C# они реализованы как статические методы в статических классах, при этом первый аргумент относится к расширенному классу и ему предшествует this
ключевое слово « ».
В Java методы расширения добавляются через Manifold, jar-файл, добавленный в classpath проекта. Подобно C#, метод расширения Java объявляется статическим в классе @Extension, где первый аргумент имеет тот же тип, что и расширенный класс, и аннотируется с помощью @This
. В качестве альтернативы плагин Fluent позволяет вызывать любой статический метод как метод расширения без использования аннотаций, пока сигнатура метода совпадает.
В Smalltalk любой код может добавить метод к любому классу в любое время, отправив сообщение о создании метода (например, methodsFor:
) классу, который пользователь хочет расширить. Категория методов Smalltalk традиционно называется по имени пакета, который предоставляет расширение, окруженное звездочками. Например, когда код приложения Etoys расширяет классы в основной библиотеке, добавленные методы помещаются в *etoys*
категорию.
В Ruby, как и в Smalltalk, нет специальной языковой функции для расширения, так как Ruby позволяет повторно открывать классы в любое время с помощью class
ключевого слова для добавления новых методов. Сообщество Ruby часто описывает метод расширения как своего рода обезьянью заплатку . Существует также более новая функция для добавления безопасных/локальных расширений к объектам, называемая Refinements, но известно, что она используется реже.
В Swift extension
ключевое слово обозначает конструкцию, подобную классу, которая позволяет добавлять методы, конструкторы и поля к существующему классу, включая возможность реализации нового интерфейса/протокола к существующему классу. [2]
Наряду с методами расширения, позволяющими расширять код, написанный другими, как описано ниже, методы расширения позволяют использовать шаблоны, которые полезны сами по себе. Основной причиной введения методов расширения был Language Integrated Query (LINQ). Поддержка компилятором методов расширения позволяет осуществлять глубокую интеграцию LINQ со старым кодом так же, как и с новым кодом, а также поддержку синтаксиса запросов , который на данный момент является уникальным для основных языков Microsoft .NET .
Console.WriteLine ( new [] { Math.PI , Math.E } .Where ( d = > d > 3 ) .Select ( d = > Math.Sin ( d / 2 ) ) . Sum ( ) ) ; //Вывод: // 1
Однако методы расширения позволяют реализовать функции один раз способами, допускающими повторное использование без необходимости наследования или накладных расходов на вызовы виртуальных методов , или без необходимости для разработчиков интерфейса реализовывать либо тривиальную, либо крайне сложную функциональность.
Особенно полезным сценарием является случай, когда функция работает с интерфейсом, для которого нет конкретной реализации или полезная реализация не предоставлена автором библиотеки классов, например, как это часто бывает в библиотеках, которые предоставляют разработчикам архитектуру плагинов или аналогичную функциональность.
Рассмотрим следующий код и предположим, что это единственный код, содержащийся в библиотеке классов. Тем не менее, каждый реализатор интерфейса ILogger получит возможность писать отформатированную строку, просто включив оператор using MyCoolLogger , без необходимости реализовывать его один раз и без необходимости создавать подкласс библиотеки классов, предоставленной для реализации ILogger.
пространство имен MyCoolLogger ; открытый интерфейс ILogger { void Write ( string text ); } public static class LoggerExtensions { public static void Write ( this ILogger logger , string format , params object [ ] args ) { if ( logger ! = null ) logger.Write ( string.Format ( format , args ) ) ; } }
var logger = new MyLoggerImplementation (); logger.Write ( "{0}: {1}" , "kiddo sais" , " Mam mam mam mam ..." ); logger.Write ( " {0}: {1}" , "kiddo sais" , "Ma ma ma ma..."); logger.Write("{0}: {1}", "kiddo sais", "Mama mama mama"); logger.Write("{0}: {1}", "kiddo sais", "Mamma maamma mamma ..." ) ; logger.Write ( " {0} : {1} " , " kiddo sais " , " Mamma mamma mamma ..." ) ; logger.Write ( " {0} : {1}" , "kiddo sais" , " Elisabeth Lizzy Liz..." ) ; logger.Write ( " {0}: {1}" , "mamma sais" , "ЧТО?!?!!!" ); logger . Write ( "{0}: {1}" , "kiddo sais" , "hi." );
Методы расширения позволяют пользователям библиотек классов воздержаться от объявления аргумента, переменной или чего-либо еще с типом, который исходит из этой библиотеки. Построение и преобразование типов, используемых в библиотеке классов, могут быть реализованы как методы расширения. После тщательной реализации преобразований и фабрик переключение с одной библиотеки классов на другую может быть сделано таким же простым, как изменение оператора using, который делает методы расширения доступными для привязки компилятора.
Методы расширения имеют особое применение в реализации так называемых текучих интерфейсов. Примером является API конфигурации Entity Framework от Microsoft, который позволяет, например, писать код, который напоминает обычный английский настолько близко, насколько это практично.
Можно утверждать, что это вполне возможно и без методов расширения, но на практике методы расширения обеспечивают превосходный опыт, поскольку на иерархию классов накладывается меньше ограничений, чтобы она работала (и читалась) так, как нужно.
Следующий пример использует Entity Framework и настраивает класс TodoList для хранения в таблице базы данных Lists и определяет первичный и внешний ключи. Код следует понимать примерно так: "TodoList имеет ключ TodoListID, его имя набора сущностей — Lists, и у него есть много TodoItem, каждый из которых имеет требуемый TodoList".
public class TodoItemContext : DbContext { public DbSet < TodoItem > TodoItems { get ; set ; } public DbSet < TodoList > TodoLists { get ; set ; } protected override void OnModelCreating ( DbModelBuilder modelBuilder ) { base.OnModelCreating ( modelBuilder ) ; modelBuilder.Entity <TodoList> ( ) .HasKey ( e = > e.TodoListId ) .HasEntitySetName ( " Lists " ) . HasMany ( e = > e.Todos ) .WithRequired ( e = > e.TodoList ) ; } }
Рассмотрим, например, IEnumerable и отметим его простоту — есть только один метод, но он является основой LINQ более или менее. Существует множество реализаций этого интерфейса в Microsoft .NET. Тем не менее, очевидно, было бы обременительно требовать от каждой из этих реализаций реализации целой серии методов, которые определены в пространстве имен System.Linq для работы с IEnumerables, даже несмотря на то, что у Microsoft есть весь исходный код. Хуже того, это потребовало бы от всех, кроме Microsoft, которые рассматривают возможность использования IEnumerable, также реализовывать все эти методы, что было бы очень непродуктивно, учитывая широкое использование этого очень распространенного интерфейса. Вместо этого, реализуя один метод этого интерфейса, LINQ можно использовать более или менее немедленно. Особенно учитывая, что практически в большинстве случаев метод GetEnumerator IEnumerable делегируется частной коллекции, списку или реализации GetEnumerator массива.
public class BankAccount : IEnumerable < decimal > { private List < Tuple < DateTime , decimal >> credits ; // предполагается, что все отрицательные private List < Tuple < DateTime , decimal >> debits ; // предполагается, что все положительные public IEnumerator < decimal > GetEnumerator () { var query = from dc in debits . Union ( credits ) orderby dc . Item1 /* Date */ select dc . Item2 ; /* Amount */ foreach ( var amount in query ) yield return amount ; } } // имея экземпляр BankAccount с именем ba и using System.Linq поверх текущего файла, // теперь можно написать ba.Sum() для получения баланса счета, ba.Reverse() для просмотра последних транзакций в первую очередь, // ba.Average() для получения средней суммы за транзакцию и т. д. - без написания арифметического оператора
Тем не менее, можно добавить дополнительные реализации функции, предоставляемой методом расширения, для повышения производительности или для работы с по-разному реализованными реализациями интерфейсов, например, предоставив компилятору реализацию IEnumerable специально для массивов (в System.SZArrayHelper), которую он будет автоматически выбирать для вызовов методов расширения для ссылок, типизированных как массивы, поскольку их аргумент будет более конкретным (это значение T[]), чем метод расширения с тем же именем, который работает с экземплярами интерфейса IEnumerable (это значение IEnumerable).
С универсальными классами методы расширения позволяют реализовать поведение, которое доступно для всех экземпляров универсального типа, не требуя, чтобы они выводились из общего базового класса, и не ограничивая параметры типа определенной ветвью наследования. Это большой выигрыш, поскольку ситуации, в которых этот аргумент имеет место, требуют неуниверсального базового класса только для реализации общей функции, что затем требует, чтобы универсальный подкласс выполнял упаковку и/или приведения всякий раз, когда используемый тип является одним из аргументов типа.
Следует отметить, что методы расширения должны быть предпочтительными по сравнению с другими способами достижения повторного использования и надлежащего объектно-ориентированного проектирования. Методы расширения могут «загромождать» функции автоматического завершения редакторов кода, таких как IntelliSense в Visual Studio, поэтому они должны быть либо в своем собственном пространстве имен, чтобы разработчик мог выборочно импортировать их, либо они должны быть определены в типе, который достаточно специфичен для того, чтобы метод появлялся в IntelliSense только тогда, когда это действительно необходимо, и, учитывая вышеизложенное, учтите, что их может быть трудно найти, если разработчик ожидает их, но не видит их в IntelliSense из-за отсутствующего оператора using, поскольку разработчик мог не связать метод с классом, который его определяет, или даже с пространством имен, в котором он находится, а с типом, который он расширяет, и с пространством имен, в котором находится этот тип.
В программировании возникают ситуации, когда необходимо добавить функциональность к существующему классу, например, путем добавления нового метода. Обычно программист изменяет исходный код существующего класса , но это заставляет программиста перекомпилировать все двоичные файлы с этими новыми изменениями и требует, чтобы программист мог изменять класс, что не всегда возможно, например, при использовании классов из сторонней сборки . Обычно это обходится одним из трех способов, все из которых несколько ограничены и неинтуитивны [ требуется цитата ] :
Первый вариант в принципе проще, но, к сожалению, он ограничен тем фактом, что многие классы ограничивают наследование определенных членов или полностью запрещают его. Сюда входят запечатанный класс и различные примитивные типы данных в C#, такие как int , float и string . Второй вариант, с другой стороны, не разделяет эти ограничения, но он может быть менее интуитивным, поскольку требует ссылки на отдельный класс вместо использования методов рассматриваемого класса напрямую.
В качестве примера рассмотрим необходимость расширения класса string с помощью нового метода reverse, возвращаемое значение которого представляет собой строку с символами в обратном порядке. Поскольку класс string является запечатанным типом, метод обычно добавляется к новому классу утилиты примерно следующим образом:
string x = "некое строковое значение" ; string y = Utility . Reverse ( x );
Однако это может стать все более сложным для навигации по мере увеличения библиотеки методов и классов утилит, особенно для новичков. Расположение также менее интуитивно понятно, поскольку, в отличие от большинства методов строк, он не будет членом класса строк, а будет находиться в совершенно другом классе. Поэтому лучшим синтаксисом будет следующий:
string x = "некоторое строковое значение" ; string y = x . Reverse ();
Во многих отношениях решение VB.NET похоже на решение C# выше. Однако VB.NET имеет уникальное преимущество в том, что он позволяет передавать члены в расширение по ссылке (C# позволяет только по значению). Позволяет следующее;
Dim x As String = "некое строковое значение" x . Reverse ()
Поскольку Visual Basic позволяет передавать исходный объект по ссылке, можно вносить изменения в исходный объект напрямую, без необходимости создания другой переменной. Это также более интуитивно, поскольку работает согласованно с существующими методами классов.
Однако новая языковая функция методов расширения в C# 3.0 делает возможным последний код. Этот подход требует статического класса и статического метода, как показано ниже.
public static class Utility { public static string Reverse ( this string input ) { char [] chars = input.ToCharArray ( ) ; Array.Reverse ( chars ) ; return new String ( chars ) ; } }
В определении модификатор 'this' перед первым аргументом указывает, что это метод расширения (в данном случае к типу 'string'). В вызове первый аргумент не 'передается', поскольку он уже известен как 'вызывающий' объект (объект перед точкой).
Основное различие между вызовом методов расширения и вызовом статических вспомогательных методов заключается в том, что статические методы вызываются в префиксной нотации , тогда как методы расширения вызываются в инфиксной нотации . Последнее приводит к более читаемому коду, когда результат одной операции используется для другой операции.
HelperClass.Operation2 ( HelperClass.Operation1 ( x , arg1 ) , arg2 )
x . Операция1 ( arg1 ). Операция2 ( arg2 )
В C# 3.0 для класса могут существовать как метод экземпляра, так и метод расширения с одинаковой сигнатурой. В таком сценарии метод экземпляра предпочтительнее метода расширения. Ни компилятор, ни IDE Microsoft Visual Studio не предупреждают о конфликте имен. Рассмотрим этот класс C#, где GetAlphabet()
метод вызывается для экземпляра этого класса:
class AlphabetMaker { public void GetAlphabet () { //Когда этот метод реализован, Console . WriteLine ( "abc" ); //он будет скрывать реализацию } //в классе ExtensionMethods. } static class ExtensionMethods { public static void GetAlphabet ( this AlphabetMaker am ) { // Это будет вызвано только Console.WriteLine ( "ABC" ); //если нет экземпляра } //метод с той же сигнатурой . }
Результат вызова GetAlphabet()
экземпляра, AlphabetMaker
если существует только метод расширения:
АБВ
Результат, если существуют и метод экземпляра, и метод расширения:
азбука