Добавлен , опубликован
Раздел:
Если вы находитесь на стадии обучения языку C# - данная статья как раз для вас! Она расскажет вам о том, как использовать одну из синтаксических фич языка - методы расширений.
Итак, для чего нужны методы расширений?
Что, ж, я рассмотрю использование подобной конструкции на конкретном примере, правда не самом правильном - этот пример выбран специально и устроен так, чтобы не задевать другие темы программирования и не мешать в голове понятия.

Пример

Так случилось, что в вашем проекте постоянно приходится доставать последний элемент из массивов типа int.
Чтобы доставать последний элемент массива array вам приходится каждый раз писать код на подобии следующего:
var last = 0;
if (array != null && array.Count != 0) 
    last = array[array.Length - 1];
В результате в переменной last хранится либо последний элемент массива, либо, если массив оказался пустым - хранится 0, как значение по умолчанию.
Вы долго и муторно вбиваете этот код при каждом использовании, пока однажды к вам в голову не приходит идея - "а почему бы мне не выделить написанное в отдельный метод"?
И вы создаете метод на подобии вот такого:
public static class IntArrayUtils 
{
    public static int GetLast(int[] array) 
    {
        var last = 0;
        if (array != null && array.Length != 0)
             last = array[array.Length - 1];
        return last;
    }
}
Который вызывается в коде вот так:
var last = IntArrayUtils.GetLast(array);
Согласитесь, уже короче? Но чего -то не хватает. Может, красоты?
Вот тут мы и подошли к методам расширений. Согласитесь, что вот так было бы лучше:
var last = array.GetLast();
Нам бы не пришлось указывать странный метод IntArrayUtils и данный метод выглядел бы в точь-в-точь как родной метод у int[], который, к сожалению мы добавить не можем.

Решение

Что же нужно изменить чтобы достичь такого результата? Все очень просто, к существующему методу мы добавим всего одно слово this перед первым параметром:
public static class IntArrayUtils 
{
    public static int GetLast(this int[] array) 
    {
        var last = 0;
        if (array != null && array.Length != 0)
             last = array[array.Length - 1];
        return last;
    }
}
В результате все экземпляры int[] в нашем коде обзавелись дополнительным методом GetLast().
Ваш код после такой простой манипуляции может вызываться двумя разными способами, которые я уже указывал выше.
Как стандартным, через статический класс:
var last = IntArrayUtils.GetLast(array);
Так и через экземпляр:
var last = array.GetLast();

Заметки

Немного тонкостей о методах расширений:
  • Метод расширения обязан быть частью static класса
  • Несмотря на то, что метод выглядит как метод экземпляра, он является статичным и при компиляции подменяется. Это обязывает вас делать проверку на null, чтобы не допустить ошибок.
  • Метод может иметь собственные Generic типы, как и любой другой метод. Это позволит вам сделать код более универсальным.
  • Увы, вы не сможете расширить этим статические классы, например добавить новый статический метод в класс Math. Только экземпляры, только хардкор.

Примеры использования

Несмотря на все удобство, методы расширений не стоит применять без повода - чрезмерная перенасыщенность ваших классов может плохо сказываться на читаемости и поддерживании вашего кода. Потому настоятельно советую использовать данный "сахар" с умом.
Код желательно обустраивать так, чтобы его было легко читать. Например:
text.SaveAsFile(file);
Среди стандартных библиотек данный способ используется для всех методов у массивов и перечислителей - методы ForEach, Select, Any, Where, Contains и так далее.
Очевидно, что подобное решение отлично подходит для случаев, в которые просто так метод не добавишь, либо этот метод весьма специфичен и требует возможностей static классов. Например, к таким относится удаление объекта, где обычный метод экземпляра не может обнулить экземпляр, через this = null потому что сам является его частью.
Несмотря на плюсы, не стоит использовать методы расширений если:
  • Ваш метод лучше укладывается в отдельном классе с другими тематичными методами и ситуация использования крайне специфична или просто редка для этого типа в целом.
  • Ваш метод перекликается с названием существующих методов и порождает путаницу.
  • Ваш метод выглядит читабельней, не используя данного синтаксиса.
Общепринято складывать методы расширений в отдельный класс (без обычных статических методов) и дописывать данному классу слово Extensions. Например, класс, содержащий методы расширений для типа List обычно называют ExtensionsList или ListExtensions. Это не обязывает вас делать тоже самое, но насколько я видел в проектах всегда есть отдельная папочка для методов расширений с вот такими классами.
Вот наверное и вся основная информация по методам расширений. Спасибо за внимание.
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
2 комментария удалено
20
Методы расширения было бы лучше реализовывать переопределяя класс array его потомком(ой, ну да, этож c#) с заимплеменченным методом .last()
Тогда это не выглядело бы как костыль, на мой скромный взгляд.
alexprey, я вообще мимокрокодил. Но аргумент "сперва добейся" - не аргумент.
Extravert, не воспринимай близко к сердцу. Статья-то хорошая, её хоть сейчас помещай в учебник.
Мы тут просто начали оффтопить на близкую, но другую тему: как лучше эту фичу использовать в больших серьезных проектах. В статье настолько всё разжевано и понятно, что обсуждать приходится что угодно, кроме статьи.
2
Нет уверенности что потенциальный читатель уже знает про out-параметры и про исключения.
Так и нету уверенности, что он знает static, public, class и.т.д.
В этом и та беда почему по книгам сложно обучаться с нуля - они тут же начинают говорить про кучи, исключения, потокобезопасность и еще 40 не разъясненных понятий, при том не объясняя нифига, а потом думают что доступно изложили материал.
Вот про книги не надо. В большинстве из них оговаривается, что конструкции языка тесно связаны, и на ненужные просто не обращать внимания.
Скорее всего вам не повезло с хорошей литературой.
Вообще подобные статьи должны быть нацелены на тех, кто не совсем или в тяжелом случае совершенно ничего не понял, что "это" такое.
А вид иметь примерно такой:
  • полное описание в доступной форме(зачем? когда? как?).
  • реальные примеры и побольше.
  • различные тонкости применения и "подводные камни", опять же с обильным количеством примеров.
27
bea_mind:
Так и нету уверенности, что он знает static, public, class и.т.д.
Есть разница между тем, что ему придется узнать чтобы это осилить, и тем что он может узнать потом. Все в программировании очень сильно переплетается, но нельзя одним махом срубить все сразу.
24
alexprey, C# вызывает у меня дикий когнитивный диссонанс, как можно было понять из оффтопа в прошлом комментарии. А полезное для сайта я делать пытался, пока время позволяло - переводил статьи по JmonkeyEngine3 и еще вернусь к этому, даже не смотря на полное отсутствие востребованности этой темы.
Что касается методов расширения, расскажу одну маленькую историю из своей жизни: было дело, работал я на C# и выпало мне поддерживать библиотеку, написанную задолго до моего прихода в проект. Так получилось, что эта библиотека понадобилась одной из групп разработчиков на смежном проекте.
И вот, приходят ко мне с вопросом "как пользоваться методом Revitalize для объекта типа LostRequest? Мы его и так и эдак, а ему хоть бы хны - фиолетовый" имена методов и классов, а также пароли и явки взяты произвольно из соображений анонимности. Я то помню что такого метода в нашей библиотеке нет и быть не может, потому в первую очередь спрашиваю не используют ли они какую-то самопальную модификацию, но нет, ничего такого.
Потом вспоминаю про методы расширения и мягко уточняю не пользуются ли они чем-то таким, но и тут все чисто - не писали, не употребляли, не привлекались. В итоге мне пришлось скачать и развернуть у себя их проект, весивший, к слову, пару гигов со всеми необходимыми составляющими. И все только для того, чтобы в другой библиотеке обнаружить этот-самый метод-расширение, расширявший класс из нашей библиотеки.
Мораль - реализация этого расширения через фасад, адаптер, агрегацию или любым другим применимым в данном случае архитектурным паттерном значительно упростила бы жизнь, как минимум, пяти программистам ценой лишних десяти минут работы одного программиста.
29
prog, что это за программисты такие которые не могут отследить где метод расположен.... Студия такие методы выделяет и отправляет в нужный класс. Просто им было лень и скинули дело на тебя. Проблема фасада в том, что если добавят новйы метод в основной класс, тебе придется расширять новый и т.д. У нас в проекте с большим успехом юзают расширения для простоты написания кода и никто не жалуется.
Mihahail:
alexprey, я вообще мимокрокодил. Но аргумент "сперва добейся" - не аргумент.
Моя цель сподвигнуть других писать статьи) А вообще экстраверт тоже, сперва пришел и начал везде всех расскидывать такими же контр-комментариями, но он хотя бы доказал что он может не только писать заумные комментарии.
Я лично помогал в том году нашему преподавателю разрабатывать учебник для первого курса по плюсам и шарпам. Там было все так же разжованно, а в некоторых местах еще лучше. Большинство все равно так и не понимали как это делать, пока им устно не расскажешь, но в итоге, они все таки освоили язык. Поэтому давай те не будем тут разводить срачи и напишем каждый по несколько статей по основом любого языка.
Вот ты prog, например, ты отлично знаешь джаву, так почему бы не научить других? Я уверен, у тебя бы получились отличные статьи, наверняка.
И на последок, почему то мне кажется вы не дочитываете статью, или просто пробежались по коду и все. Потому что там было сказано, что их их надо использовть с умом, а не тыкать везде и всюду... вот теперь точно все
Этот комментарий удален
24
alexprey, набрали джуниоров и отправили говнокодить низкоприоритетный проект, назначив в сопровождение пару миддлов-кураторов из основной ветки, если это так важно.
Преимущество адаптеров, врапперов и фасадов перед матодами расширения в том, что в один враппер можно собрать сразу несколько перекликающихся дополнительных методов, плюс там можно хранить дополнительные поля, недостающие исходному классу - методы расширения не дают второго в принципе, а первое дают условно - можно в один класс собирать все методы расширения для одного класса, но это не позволит, например, провернуть трюк с наследованием от враппера и переопределением какого-нибудь дополнительного метода.
Что касается необходимости обновлять врапперы и фасады при добавлении новых методов в базовый класс, то это не совсем так - в фасад, а уж тем более в адаптер методы из базового класса добавляются по мере необходимости их использования в том коде, который работает с фасадом - зачастую задача адаптера скрыть частично или полностью контракт чужого класса, подменив его собственным. А в случае с врапперами так и вовсе нет необходимости реализовывать во враппере контракт базового класса - достаточно той функциональности, которая требуется, а в остальном с базовым классом можно работать и напрямую, при желании завернув это все в еще один слой, состоящий из адаптера или фасада.
В целом, причины избегать методов расширения совпадают с причинами избегать статических методов как таковых - сильно падает гибкость и расширяемость кода при злоупотреблении статическими методами, да и юниттесты в таких условиях писать это ад. Плюс к этому добавляются проблемы вида "а что если в базовом классе появится метод с тем-же именем и функциональностью, которые есть у существующего метода расширения?", "а как себя ведут методы расширения при наследовании?" и другие.
К сожалению, в отличии от простых статических методов, законно занимающих нишу таких фундаментальных вещей, как математические вычисления, такой ниши для методов расширения я не вижу, не считая работы с массивами, но для этого есть коллекции. С натяжкой можно предположить что с помощью методов расширения можно дополнить операции с векторами, а также упомянуть про возможность переопределить операторы, добавив к своим классам возможность использоваться в качестве операндов, но эти две возможности я нахожу весьма сомнительными. Во-первых векторным операциям место в самих классах векторов. Во-вторых использование математических операторов для пользовательских классов имеет существенный недостаток - далеко не всегда очевидно что автор подразумевал, например, под сложением, а классическая реализация через метод класса позволяет постараться дать более-менее красноречивое имя методу.
На этом все, что касается моего отношения к методам расширения.

Что касается статей - я если и буду что-то писать с нуля, то по чему-нибудь более экзотическому, чем миллион раз обсосанные основы языка Java, которые намного лучше описаны в той-же "Think in Java", чем получится у меня. Например, по программной архитектуре или по работе с Lua, в крайнем случае по каким-нибудь специфичным применениям движка JmonkeyEngine3, да и по Action Script практически не паханое поле.
29
Преимущество адаптеров, врапперов и фасадов перед матодами расширения в том, что в один враппер можно собрать сразу несколько перекликающихся дополнительных методов, плюс там можно хранить дополнительные поля, недостающие исходному классу - методы расширения не дают второго в принципе, а первое дают условно - можно в один класс собирать все методы расширения для одного класса, но это не позволит, например, провернуть трюк с наследованием от враппера и переопределением какого-нибудь дополнительного метода.
это плюсы, а разные решения. Как бы расширение для этого и не нужно.
prog:
которые есть у существующего метода расширения?", "а как себя ведут методы расширения при наследовании?" и другие.
тут уже пошла легкая наркомания, похоже ты не докурил основы. 1) Ошибка компиляции, проблема в расширении 2) Ошибка компиляции, какое нахрен наследование у статического класса
prog:
К сожалению, в отличии от простых статических методов, законно занимающих нишу таких фундаментальных вещей, как математические вычисления, такой ниши для методов расширения я не вижу, не считая работы с массивами, но для этого есть коллекции.
Для коллекций тоже иногда нужны расширения под нужды системы. Вот например у нас в проекте используется Join для коллекций, и еще много различных методов для быстрого преобразования и агрегирования данных. уть методов расширения обернуть большую цепочку повторяющихся действий в один метод. Еще один пример, есть UIHelper который помогает создавать UI часть для приложения, в нем храняться нужные контексты и базовые методы, аля DisplayFor и т.д. Для создания простых форм для редактирования данных приходится писать
UI.BeginGroupFor(model => model.ExtensionName);
UI.LabelFor(model => model.ExtensionName, Localization.ExtensionEditor_ExtensionName);
UI.EditorFor(model => model.ExtensionName);
UI.ValidationMessageFor(model => model.ExtensionName);
UI.EndGroup();
Делаем расширяющий метод, и вуаля
UI.GroupFor(model => model.ExtensionName, Localization.ExtensionEditor_ExtensionName);
все просто, красиво и главное потом легко изменить отображение всех форм и т.д. И использовать можно на любой форме, все просто компактно и все данные всегда с собой.
prog:
С натяжкой можно предположить что с помощью методов расширения можно дополнить операции с векторами, а также упомянуть про возможность переопределить операторы, добавив к своим классам возможность использоваться в качестве операндов, но эти две возможности я нахожу весьма сомнительными.
расширение операндов дело тонкой, но в большинстве случаев весьма удобное. Но тут действительно из место внутри нормального класса.

Каждый раз перечитываю и понимаю, что ты так и не понял приколюху методов расширений. Метод расширение - это тот же статический метод, просто он первым аргументом принимает объект класса, который расширяет. Опять же:
java:
public final class ArrayExtension {
     public static int getLast(int[] array) {
         return array[array.size() - 1];
     }
}
...
ArrayExtension.getLast(myArray);

c#:
public static class ArrayExtension 
{
    public static int GetLast(this int[] array)
    {
        return array[array.Length - 1];
    }
}
...
myArray.GetLast();
ArrayExtension.GetLast(myArray);

По архитектуре приложений, даже я бы почитал. А Action Script мне кажется ну тема не особо приличная) Lua побольше используется и в разных играх, да и прикрутить её не сложно вроде бы.
24
alexprey, что есть методы расширения я прекрасно понимаю, потому и привел сравнение со статическими методами. Про наследование имелась в виду ситуация, когда есть базовый класс, есть его наследники и есть методы расширения к базовому классу.
Что касается вашего примера, то тут напрашивается что-то вроде класса FormPresetsFactory, инкапсулирующего в себе работу с UI-хелпером и предоставляющего N методов, каждый на отдельный вид форм плюс X protected методов для внутреннего пользования в пределах фабрики, в которых можно собрать повторяющиеся элементы форм. Почему это лучше, чем расширения? Потому что в случае необходимости легко заменить одну фабрику другой, реализующей ту-же функциональность, но совсем другими способами, а в случае с методами расширения придется не только писать дополнительные методы расширения, но и вносить изменения в существующий код, если нужно и старый вариант сохранить и новый добавить.
Кроме того фабрика скрывает работу с UI-хелпером, опуская её на более низкий уровень, а программисту дает возможность работать с абстракциями более высокого уровня. Методы расширения же смешивают высокоуровневые абстракции (пресеты форм и составные элементы) с низкоуровневыми (создание отдельных элементов с помощью UI-хелпера), что не есть хорошо.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.