Программирование: Методы расширений

» Раздел: C#

Если вы находитесь на стадии обучения языку 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. Это не обязывает вас делать тоже самое, но насколько я видел в проектах всегда есть отдельная папочка для методов расширений с вот такими классами.
Вот наверное и вся основная информация по методам расширений. Спасибо за внимание.

Просмотров: 6 355

» Лучшие комментарии


Faion #1 - 5 лет назад -8
Автор, прежде чем учить других, сам хотя бы немного изучи язык :facepalm:
public static IntArrayUtils 
{
    public static int GetLast(this int[] array) 
    {
        var last = 0;
        if (array != null && array.Count != 0)
             last = array[array.Length];
        return last;
    }
}
Две ошибки в примитивном примере. Я уже не говорю про оптимальность.
Несмотря на плюсы, не стоит использовать методы расширений если
Вместо этих если, нужно было сказать, что эти методы расширения допустимо использовать только в том случае, если у вас нету доступа к объекту, который нужно расширить.
И да:
Is Extension method thread safe?
You found an interesting loop hole, it tripped everybody up. No, it is not thread-safe.
While it looks like the EventHandler<> reference is copied through the method argument, this is not what happens at runtime. Extension methods are subject to being inlined, just like a regular instance method. In fact, it is extremely likely to get inlined since it is so small. There's no copy, you have to make one yourself.
Devion #2 - 5 лет назад (отредактировано ) 3
Две ошибки в примитивном примере.
Поправил, писал код в странице, не заметил
Автор, прежде чем учить других, сам хотя бы немного изучи язык :
Прежде чем делать такие заявления, подумай, реально ли списать ошибку на человеческий фактор невнимательности. Если да - выбирай выражения.
Faion:
Я уже не говорю про оптимальность.
Читай статью внимательно. Эта "неоптимальность" сделана умышлено. Когда речь идет о том чтобы объяснить новичку как устроены методы расширения, лучше имхо избавить его от необходимости смотреть "а почему код в функции поменялся", если это не касается темы. Я намеренно не использовал даже простейший оператор ?: только по этой причине, как и оставил объявление переменной (хотя можно было сразу ретурнить) чтобы читающий обращал внимание на изменения способа, а не на код-рыбу, который я использую.
Faion:
Вместо этих если, нужно было сказать, что эти методы расширения допустимо использовать только в том случае, если у вас нету доступа к объекту, который нужно расширить.
Это было написано двумя предложениями ранее.
Faion:
И да
Потокобезопасность - это то что нужно в статье для новичка по методам расширениям. Грац!
Снобам к ознакомлению
Критика к статье приветствуется, но, пожалуйста, не нужно выставлять это в виде собственного превосходства, посматривая как на гавно и убегая в крайности на любую брешь, перестаньте мудачить.
alexprey #3 - 5 лет назад 4
Faion, в книгах тоже бывают опечатки, по твоему тогда и авторы тоже не знаю языка? А так попрошу не горячиться и помочь с написанием статей для новичков, раз ты такой умный
bea_mind #4 - 5 лет назад 0
этот пример выбран специально и устроен так, чтобы не задевать другие темы программирования и не мешать в голове понятия.
Это понятно, но метод, который возвращает значение несуществующего элемента, особенно для несуществующего объекта, выглядит опасно, а если еще кто-то скопирует это себе...
Я уже подзабыл: разве индексирование массивов в шарпе начинается с 1?
alexprey #5 - 5 лет назад 0
bea_mind, я думаю, если еще и юзать nullable тип для возвращения, новичкам будет не легко
bea_mind #6 - 5 лет назад 1
alexprey, никаких nullable здесь быть и не должно. По-моему правильный вариант - если метод будет генерировать исключения и/или иметь вид
public static bool GetLast(this int[] array, out int value).
Devion #7 - 5 лет назад (отредактировано ) 0
bea_mind:
public static bool GetLast(this int[] array, out int value)
Нет уверенности что потенциальный читатель уже знает про out-параметры и про исключения. Откуда такое стремление усложнить подачу материала в целях того чтобы профи устроила правильность? Статья рассчитана на новичка, если освещаются методы расширений, то не надо приплетать еще 30 понятий. В этом и та беда почему по книгам сложно обучаться с нуля - они тут же начинают говорить про кучи, исключения, потокобезопасность и еще 40 не разъясненных понятий, при том не объясняя нифига, а потом думают что доступно изложили материал.
При условии, что в статье предупреждается, что мы экспериментируем не с самым правильным примером кода я думаю это допустимая погрешность.
alexprey #8 - 5 лет назад 0
bea_mind, имхо, с наллеблом было бы намного лучше, ну а вообще, да, согласен с экстравертом, зачем все усложнять?
Это сообщение удалено
Mihahail #10 - 5 лет назад 2
Откуда такое стремление
Переучивать сложнее, чем учить с нуля.
Это я не критикую статью, просто аргумент.
Это сообщение удалено
Mihahail #12 - 5 лет назад 0
nvc123, я не тебя имел ввиду, если что.
alexprey #13 - 5 лет назад 0
nvc123, тоесть предлагаешь бедному новичку сразу дать не объясняя овер дофига тем, чтобы он тупо это все скопипастил не понимая?
Это сообщение удалено
Mihahail #15 - 5 лет назад 0
Вообще-то делить на ноль нельзя.
prog #16 - 5 лет назад (отредактировано ) 0
Extravert, лично я шел в школу с твердой уверенностью что на ноль делить можно и это доставило мне в свое время немало проблем, не говоря уже об уверенности что если из единицы вычесть два, то получится не ноль, а минус единица.
К чему это я? Наверно к тому, что осваивать программирование, как правило, начинают все-же не в первом классе, а уже более зрелые личности, способные к мыслительной деятельности повыше уровнем чем "добавим пять слонов к девяти бананам". Да и синтаксический сахар вроде методов расширения это явно не первое, чему должен учиться человек, начинающий изучать программирование, а значит и статья на эту тему не обязательно должна быть написана в столь аскетичной манере.
Не говоря уже о том, что конкретно методы расширения есть зло, нарушающее принципы ООП и взрывающее мозг почитателям более-менее грамотной архитектуры приложения, а также поборникам чистоты, читаемости и самодостаточности кода. И да, вот еще что, имена методов с большой буквы - ааааа моооииии глаааазааааа!
P.S. так-то статья составлена достаточно грамотно, ничего личного.
alexprey #17 - 5 лет назад 0
имена методов с большой буквы - ааааа моооииии глаааазааааа!
вот только не надо тут холиваров по стилю написания кода.
prog, Mihahail, nvc123, bea_mind, вот вы все молодцы, хоть бы одну статью написали. Человек старается и пытается развить сайт, а вы тут сидите и пинаете палочки. Говорил, и еще раз скажу, хотите показать свою крутоту всем? Так возьмите и напишите статью
prog:
Не говоря уже о том, что конкретно методы расширения есть зло
не понимаешь всю их прелесть, они упрощают твою жизнь.
3 комментария удалено
Mihahail #21 - 5 лет назад 0
Методы расширения было бы лучше реализовывать переопределяя класс array его потомком(ой, ну да, этож c#) с заимплеменченным методом .last()
Тогда это не выглядело бы как костыль, на мой скромный взгляд.
alexprey, я вообще мимокрокодил. Но аргумент "сперва добейся" - не аргумент.
Extravert, не воспринимай близко к сердцу. Статья-то хорошая, её хоть сейчас помещай в учебник.
Мы тут просто начали оффтопить на близкую, но другую тему: как лучше эту фичу использовать в больших серьезных проектах. В статье настолько всё разжевано и понятно, что обсуждать приходится что угодно, кроме статьи.
bea_mind #22 - 5 лет назад 3
Нет уверенности что потенциальный читатель уже знает про out-параметры и про исключения.
Так и нету уверенности, что он знает static, public, class и.т.д.
В этом и та беда почему по книгам сложно обучаться с нуля - они тут же начинают говорить про кучи, исключения, потокобезопасность и еще 40 не разъясненных понятий, при том не объясняя нифига, а потом думают что доступно изложили материал.
Вот про книги не надо. В большинстве из них оговаривается, что конструкции языка тесно связаны, и на ненужные просто не обращать внимания.
Скорее всего вам не повезло с хорошей литературой.
Вообще подобные статьи должны быть нацелены на тех, кто не совсем или в тяжелом случае совершенно ничего не понял, что "это" такое.
А вид иметь примерно такой:
  • полное описание в доступной форме(зачем? когда? как?).
  • реальные примеры и побольше.
  • различные тонкости применения и "подводные камни", опять же с обильным количеством примеров.
Devion #23 - 5 лет назад 0
bea_mind:
Так и нету уверенности, что он знает static, public, class и.т.д.
Есть разница между тем, что ему придется узнать чтобы это осилить, и тем что он может узнать потом. Все в программировании очень сильно переплетается, но нельзя одним махом срубить все сразу.
prog #24 - 5 лет назад (отредактировано ) 0
alexprey, C# вызывает у меня дикий когнитивный диссонанс, как можно было понять из оффтопа в прошлом комментарии. А полезное для сайта я делать пытался, пока время позволяло - переводил статьи по JmonkeyEngine3 и еще вернусь к этому, даже не смотря на полное отсутствие востребованности этой темы.
Что касается методов расширения, расскажу одну маленькую историю из своей жизни: было дело, работал я на C# и выпало мне поддерживать библиотеку, написанную задолго до моего прихода в проект. Так получилось, что эта библиотека понадобилась одной из групп разработчиков на смежном проекте.
И вот, приходят ко мне с вопросом "как пользоваться методом Revitalize для объекта типа LostRequest? Мы его и так и эдак, а ему хоть бы хны - фиолетовый" имена методов и классов, а также пароли и явки взяты произвольно из соображений анонимности. Я то помню что такого метода в нашей библиотеке нет и быть не может, потому в первую очередь спрашиваю не используют ли они какую-то самопальную модификацию, но нет, ничего такого.
Потом вспоминаю про методы расширения и мягко уточняю не пользуются ли они чем-то таким, но и тут все чисто - не писали, не употребляли, не привлекались. В итоге мне пришлось скачать и развернуть у себя их проект, весивший, к слову, пару гигов со всеми необходимыми составляющими. И все только для того, чтобы в другой библиотеке обнаружить этот-самый метод-расширение, расширявший класс из нашей библиотеки.
Мораль - реализация этого расширения через фасад, адаптер, агрегацию или любым другим применимым в данном случае архитектурным паттерном значительно упростила бы жизнь, как минимум, пяти программистам ценой лишних десяти минут работы одного программиста.
alexprey #25 - 5 лет назад 1
prog, что это за программисты такие которые не могут отследить где метод расположен.... Студия такие методы выделяет и отправляет в нужный класс. Просто им было лень и скинули дело на тебя. Проблема фасада в том, что если добавят новйы метод в основной класс, тебе придется расширять новый и т.д. У нас в проекте с большим успехом юзают расширения для простоты написания кода и никто не жалуется.
Mihahail:
alexprey, я вообще мимокрокодил. Но аргумент "сперва добейся" - не аргумент.
Моя цель сподвигнуть других писать статьи) А вообще экстраверт тоже, сперва пришел и начал везде всех расскидывать такими же контр-комментариями, но он хотя бы доказал что он может не только писать заумные комментарии.
Я лично помогал в том году нашему преподавателю разрабатывать учебник для первого курса по плюсам и шарпам. Там было все так же разжованно, а в некоторых местах еще лучше. Большинство все равно так и не понимали как это делать, пока им устно не расскажешь, но в итоге, они все таки освоили язык. Поэтому давай те не будем тут разводить срачи и напишем каждый по несколько статей по основом любого языка.
Вот ты prog, например, ты отлично знаешь джаву, так почему бы не научить других? Я уверен, у тебя бы получились отличные статьи, наверняка.
И на последок, почему то мне кажется вы не дочитываете статью, или просто пробежались по коду и все. Потому что там было сказано, что их их надо использовть с умом, а не тыкать везде и всюду... вот теперь точно все
Это сообщение удалено
prog #27 - 5 лет назад (отредактировано ) 0
alexprey, набрали джуниоров и отправили говнокодить низкоприоритетный проект, назначив в сопровождение пару миддлов-кураторов из основной ветки, если это так важно.
Преимущество адаптеров, врапперов и фасадов перед матодами расширения в том, что в один враппер можно собрать сразу несколько перекликающихся дополнительных методов, плюс там можно хранить дополнительные поля, недостающие исходному классу - методы расширения не дают второго в принципе, а первое дают условно - можно в один класс собирать все методы расширения для одного класса, но это не позволит, например, провернуть трюк с наследованием от враппера и переопределением какого-нибудь дополнительного метода.
Что касается необходимости обновлять врапперы и фасады при добавлении новых методов в базовый класс, то это не совсем так - в фасад, а уж тем более в адаптер методы из базового класса добавляются по мере необходимости их использования в том коде, который работает с фасадом - зачастую задача адаптера скрыть частично или полностью контракт чужого класса, подменив его собственным. А в случае с врапперами так и вовсе нет необходимости реализовывать во враппере контракт базового класса - достаточно той функциональности, которая требуется, а в остальном с базовым классом можно работать и напрямую, при желании завернув это все в еще один слой, состоящий из адаптера или фасада.
В целом, причины избегать методов расширения совпадают с причинами избегать статических методов как таковых - сильно падает гибкость и расширяемость кода при злоупотреблении статическими методами, да и юниттесты в таких условиях писать это ад. Плюс к этому добавляются проблемы вида "а что если в базовом классе появится метод с тем-же именем и функциональностью, которые есть у существующего метода расширения?", "а как себя ведут методы расширения при наследовании?" и другие.
К сожалению, в отличии от простых статических методов, законно занимающих нишу таких фундаментальных вещей, как математические вычисления, такой ниши для методов расширения я не вижу, не считая работы с массивами, но для этого есть коллекции. С натяжкой можно предположить что с помощью методов расширения можно дополнить операции с векторами, а также упомянуть про возможность переопределить операторы, добавив к своим классам возможность использоваться в качестве операндов, но эти две возможности я нахожу весьма сомнительными. Во-первых векторным операциям место в самих классах векторов. Во-вторых использование математических операторов для пользовательских классов имеет существенный недостаток - далеко не всегда очевидно что автор подразумевал, например, под сложением, а классическая реализация через метод класса позволяет постараться дать более-менее красноречивое имя методу.
На этом все, что касается моего отношения к методам расширения.

Что касается статей - я если и буду что-то писать с нуля, то по чему-нибудь более экзотическому, чем миллион раз обсосанные основы языка Java, которые намного лучше описаны в той-же "Think in Java", чем получится у меня. Например, по программной архитектуре или по работе с Lua, в крайнем случае по каким-нибудь специфичным применениям движка JmonkeyEngine3, да и по Action Script практически не паханое поле.
alexprey #28 - 5 лет назад 1
Преимущество адаптеров, врапперов и фасадов перед матодами расширения в том, что в один враппер можно собрать сразу несколько перекликающихся дополнительных методов, плюс там можно хранить дополнительные поля, недостающие исходному классу - методы расширения не дают второго в принципе, а первое дают условно - можно в один класс собирать все методы расширения для одного класса, но это не позволит, например, провернуть трюк с наследованием от враппера и переопределением какого-нибудь дополнительного метода.
это плюсы, а разные решения. Как бы расширение для этого и не нужно.
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 побольше используется и в разных играх, да и прикрутить её не сложно вроде бы.
prog #29 - 5 лет назад (отредактировано ) 0
alexprey, что есть методы расширения я прекрасно понимаю, потому и привел сравнение со статическими методами. Про наследование имелась в виду ситуация, когда есть базовый класс, есть его наследники и есть методы расширения к базовому классу.
Что касается вашего примера, то тут напрашивается что-то вроде класса FormPresetsFactory, инкапсулирующего в себе работу с UI-хелпером и предоставляющего N методов, каждый на отдельный вид форм плюс X protected методов для внутреннего пользования в пределах фабрики, в которых можно собрать повторяющиеся элементы форм. Почему это лучше, чем расширения? Потому что в случае необходимости легко заменить одну фабрику другой, реализующей ту-же функциональность, но совсем другими способами, а в случае с методами расширения придется не только писать дополнительные методы расширения, но и вносить изменения в существующий код, если нужно и старый вариант сохранить и новый добавить.
Кроме того фабрика скрывает работу с UI-хелпером, опуская её на более низкий уровень, а программисту дает возможность работать с абстракциями более высокого уровня. Методы расширения же смешивают высокоуровневые абстракции (пресеты форм и составные элементы) с низкоуровневыми (создание отдельных элементов с помощью UI-хелпера), что не есть хорошо.
alexprey #30 - 5 лет назад 0
alexprey, что есть методы расширения я прекрасно понимаю, потому и привел сравнение со статическими методами. Про наследование имелась в виду ситуация, когда есть базовый класс, есть его наследники и есть методы расширения к базовому классу.
ну а что их сравнивать, если это они и есть?) И что тут не понятно?
prog, UI-хелпер по мне так это такая штука которая и так на высоком уровне, она работает с абстракциями и шаблонами. А делать фабрику для этого нет особо смысла, по крайней мере я не вижу в этом смысл, придется его инициализровать на каждого окна отдельно, подпихивать в UI модель, и потом использовать его. В общем вот что я скажу, на каждый проект надо смотреть по разному, в нашем случае было сделано так, никто не жаловался, в том числе и архитекторы)
Вот в случае с дата провайдерами, тут да я согласен, удобно использовать фабрику и адаптеры, чтобы можно в любой момент поменять способ получения данных.
Variecs #31 - 5 лет назад 4
Отличная статья, как раз намереваюсь подробнее изучать С#, так что очень кстати =)
Msey #32 - 4 года назад 2
>> last = array[array.Length];
Подправь на length-1 в начале статьи
alexprey #33 - 4 года назад 0
Msey, спасибо, исправил