NET 3.5: Практическое использование extension methods (исходники)

Источник: GOT DOT NET

Что это такое

Наверняка все из нас сталкивались с желанием "дописать" что-либо к существующему классу. А если это класс библиотеки .NET, да к тому же ещё и sealed? Приходиться использовать классы типа "helper" ("помощник"), единственное назначение которых реализовать специальные методы необходимые для облегчения работы в какой-либо области. Любой более-менее серьёзный проект содержит такие классы. Очень часто такие классы обрастают новыми методами и кочуют из проекта в проект.

Так было раньше. Посмотрим, что мы можем теперь. В качестве примера возьмём класс System.String. Посмотрите на следующий код:

using System;
using System.Text;
namespace Samples
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "The quick brown fox jumped over the lazy dog.";
            int i = StringHelper.WordCount(s); // <== Использование StringHelper
            System.Console.WriteLine("Word count of s is {0}", i);
        }
    }
    public static class StringHelper
    {
        public static int WordCount(String str)
        {
            return str.Split(new char[] {' ', '.','?'}, StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

Это старый вариант. Для подсчёта слов в строке мы используем метод WordCount() класса StringHelper.

Теперь та же функциональность, но с использованием "методов-расширителей":

using System;
using System.Text;
namespace Samples
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "The quick brown fox jumped over the lazy dog.";
            int i = s.WordCount();// <== Использование StringExtension
            System.Console.WriteLine("Word count of s is {0}", i);
        }
    }
    public static class StringExtension
    {
        public static int WordCount(this String str)
        {
            return str.Split(new char[] {' ', '.','?'}, StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

Если Вы ещё не сталкивались с этой новой возможностью, данный код Вас должен слегка обескуражить.

И действительно, откуда у System.String появился метод WordCount()?!

Ну а теперь обратим внимание на StringExtension. Объявление метода WordCount() слегка изменилось. Перед параметром str добавилось ключевое слово this. Это всё, что нужно чтобы наш метод стал "методом-расширителем". Теперь в любом месте кода, из которого видим класс StringExtension, мы можем использовать быстрый вариант вызова метода подсчёта строк.

Казалось бы, всё чего мы добились, это более короткого способа вызова нашего метода. Это конечно так, но неужели этого мало? В конечном счёте, чем меньше нам (программистам) необходимо написать в коде, тем быстрее мы работаем. В общем, на этом можно было бы поставить точку и закончить статью. Но область применения этой технологии намного шире, что я и постараюсь показать в дальнейшем.

А пока, несколько условий, ограничивающих применение "методов-расширителей".

  • Методы-расширения должны быть определены в статическом классе. Кроме того сам метод должен быть маркирован как статический.
  • Первый параметр должен иметь модификатор this и указывать тип, к которому применяется данный метод-расширение.
  • Метод-расширение не будет вызван, если его сигнатура (название, параметры и возвращаемое значение) совпадают с уже определённым в типе, к которому он должен применяться.

Следует помнить, что значение экземпляра типа может быть равно "null". И соответственно, правило:

В методах-расширителях не стоит забывать про проверку на "null".

Ну и наконец, одна приятная "мелочь" - методы-расширения могут быть применены как к классам (class), так и к интерфейсам (interface) и перечислениям (enum).

Практическое применение

С основами разобрались. Теперь посмотрим как это можно применить.

Библиотеки расширений

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

Пока же советую обратить внимание на UberUtils.

Применение к интерфейсам

Реализация LINQ в .NET 3.5 базируется именно на методах-расширителях. Причём применительно к интерфейсам.

А применение методов-расширителей к интерфейсам даёт нам ещё один бонус. Часть общих методов можно вынести за пределы интерфейса и реализовать через методы-расширители. В результате реализация интерфейса будет более простой и понятной.

К сожалению, в качестве действительно удачного примера в данном случае подойдёт только большой (для статьи) проект с несколькими реализациями интерфейса. Например, реализация LINQ :)

Параметр - указатель типа == null

Т.к. ссылка на экземпляр типа передаётся как параметр, мы можем использовать методы-расширители даже если текущее значение экземпляра равно "null".

Что это нам даёт? Вот пример:

using System;
namespace ExtensionMethods
{
    // Расширение
    public static class IsNullOrEmptyExtension
    {
        public static bool IsNullOrEmpty(this string s)
        {
            return string.IsNullOrEmpty(s);
        }
        public static bool IsNullOrEmpty(this Array array)
        {
            return ((array == null) / (array.Length == 0));
        }
    }
    // Пример использования
    class Program
    {
        static void Main(string[] args)
        {
            // Старый способ проверки массива
            if ( (args!=null) & (args.Length!=0) )
            {
                // Старый способ проверки строки
                if (String.IsNullOrEmpty(args[0]))
                {
                    // ...
                }
            }
            // Новый способ проверки массива
            if (args.IsNullOrEmpty())
            {
                // Новый способ проверки строки
                if (args[0].IsNullOrEmpty())
                {
                    // ...
                }
            }
        }
    }
}

Более короткий вариант написания, и соответственно, наш код пишется чуть быстрее.

Параметр - указатель типа - константа

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

Например, как вам такой код:

DateTime filterDate;
// Дата 3-х недельной давности
filterDate = 3.Weeks().Ago;
// Дата на 5 лет вперёд от текущей
filterDate = 5.Years().FromNow;

Как несложно догадаться, реализуется это использованием расширения для System.Integer. О деталях реализации советую подумать самому, ну а для особо нетерпеливых укажу, что полный код расширения можно найти в статье "C# 3.0 - Hair extensions for wanna-be rubyists".

Универсальный вызов обработчиков событий

Довольно интересных результатов можно добиться, комбинируя generics методы и методы-расширители.

Следующий пример представляет собой универсальное расширение для вызова обработчика событий (EventHandler) с проверкой на null.

using System;
namespace ExtensionMethods
{
    // Расширение для EventHandler<EventArgs>
    public static class EventHandlerExtension
    {
        public static void Fire<TEventArgs>(this EventHandler<TEventArgs> eventHandler, object sender, TEventArgs e)
            where TEventArgs: EventArgs
        {
            if (eventHandler != null)
                eventHandler(sender, e);
        }
    }
    // Пример использования
    public class Test
    {
        public event EventHandler<EventArgs> OnLoadData;
        public event EventHandler<EventArgs> OnInitData;
        // Старый вариант
        public void FireAll_Old()
        {
            if (OnLoadData != null)
            {
                OnLoadData(this, null);
            }
            if (OnInitData != null)
            {
                OnInitData(this, null);
            }
        }
        // Новый вариант
        public void FireAll_New()
        {
            OnInitData.Fire(this, null);
            OnLoadData.Fire(this, null);
        }
    }
}

Идея была опубликована в статье "Firing events with Extension Methods".

Использование в качестве делегатов

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

using System;
namespace ExtensionMethods
{
    // Класс
    public class Some
    {
        public int Val
        {
            get { return 1; }
        }
    }
    // Расширение
    public static class TestExtension
    {
        public static int UseAsDelegate(this Some some)
        {
            return some.Val;
        }
    }
    // Тест
    class App
    {
        delegate int d1();
        static void Main()
        {
            Some some = new Some();
            //
            d1 d = some.UseAsDelegate;
        }
    }
}

Локализация имён методов

Довольно часто в бизнес-приложениях приходится использовать встроенную систему скриптов. Как известно, C# поддерживает Unicode имена для классов, методов и т.д. Ну а в условиях глобализации приложения иногда желательно поддерживать локализованные имена методов в скриптах.

При использовании методов-расширений, это достигается довольно просто. Создаём сборку с локализованными именами необходимых методов. В скрипте прописываем строку вида:

// Подключение русских имён методов
using AppName.Ru;

и можем использовать локализованные имена.

Конечно, этот метод не позволяет локализовать имена классов и свойств, но это всё же лучше чем ничего.

Усложнение читабельности кода

Как ни странно, я ещё нигде не слышал о таком использовании методов-расширителей. Хотя одно из следствий их неправильного использования именно ухудшение читабельности кода. Кстати, это один из наиболее часто приводимых аргументов против их использования вообще. Моё личное мнение - плюсы применения очень перевешивают все минусы. Кроме того, возможность неправильного применения существует практически у всего. Однако это не причина для того, чтобы это "всё" запретить.

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

Примеров не привожу специально, ибо о деталях область применения обязывает умолчать. Да и не входит это в рамки темы статьи.

Несколько слов в заключение... или продолжение

Конечно, данная статья не может претендовать на всеобъемленность. Более того, в ней приведены только наиболее интересные (на мой взгляд) сценарии использования. А посему, буду рад новым идеям, комментариям и пожеланиям. И наиболее интересное из полученного, обещаю включить в новые версии статьи. Кстати, советую обратить внимание на комбинации с generic типами и константными указателями типа.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=16137