Galin Iliev

Software Architecture & Development

Разширяващи методи (Extension Methods)

Разширяващите методи са една от новите възможности в C# 3.0 и позволяват разширяването на съществуващ тип с допълнителни методи без да се налага промяна в сорса на типа или прекомпилиране на асемблито, което го съдържа. Терминът разширяване в този случай означава, че разширяващите методи могат да се извикват от инстанция на разширения тип, въпреки че тези методи не са дефинирани във въпросният клас.

Разширяващите методи могат да се извикват само когато се включи пространството от имена с ключовата дума using.

Най-широко разпространетото използване на разширяващите методи към момента е в LINQ, където се добавят допълнителни методи към типовете IEnumerable<T> и IQuerable<T>.

Разширяващите методи се дефинират като статични, но се извикват чрез инстанция на разширеният тип. Техният първи параметър показва кой тип се разширява и се предхожда от ключовата дума this. Освен това първият параметър преоставя достъп до инстанцията, която нормално се достъпва с this. Малко объркващо звучи, но ето един пример (от документацията на VS Orcas March CTP):

public static int WordCount(this System.String str)

{

    return str.Split(null).Length;

}

Единствената разлика от дефинирането на нормален статичен метод е ключовата дума this преди първият параметър. Това обаче позволява много удобно да извикаме разширяващият метод от инстанция на класа, който в горният случай е string. Този пример преброява думите в даден текстов низ. Ето как може да се използва:

string test = "Hello from C# tutorial";

int wordCount = test.WordCount(); // wordCount = 4

Разширяващите методи могат да се използват, за да се добави функционалност към клас или интерфейс, но не и да се замени функционалност (както се прави с override). Разширяващ метод със същото име и параметри както метод на клас/интерфейс никога няма да бъде извикан. По време на компилация, разширяващите методи имат по-нисък приоритет от методите, които са дефинирани в самият тип. Ако типа не дефинира метод, който се извиква, компилатора търси разширяващ метод с подадените параметри и използва първият, който бъде намерен. За да разберем по-подробно по какъв начин компилатора решава кой метод да бъде използван ще разгледаме следващият пример.

Нека да дефинираме интерфейс IMyInterface, който дефинира един метод MethodB. След това дефинираме три класа, който имплементират посоченият метод по различен начин и два от тях дефинират допълнителен метод MethodA с различни параметри.

public interface IMyInterface

{

    void MethodB();

}

 

class A : IMyInterface

{

    public void MethodB() { Console.WriteLine("A.MethodB()"); }

}

class B : IMyInterface

{

    public void MethodB() { Console.WriteLine("B.MethodB()"); }

    public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }

}

class C : IMyInterface

{

    public void MethodB() { Console.WriteLine("C.MethodB()"); }

    public void MethodA(object obj) { Console.WriteLine("C.MethodA(object obj)"); }

}

Класът B дефинира MethodA, който приема един параметър от тип int, а класът C дефинира MethodA, който приема един параметър от тип object. За да разберем кои методи имат по-голям приоритет ще дефинираме три разширяващи метода за интерфейса IMyInterface:

// Define extension methods for any type that implements IMyInterface.

public static class Extensions

{

    public static void MethodA(this IMyInterface myInterface, int i)

    {

        Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

    }

    public static void MethodA(this IMyInterface myInterface, string s)

    {

        Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, string s)");

    }

    // This method is never called, because the three classes

    public static void MethodB(this IMyInterface myInterface)

    {

        Console.WriteLine("Extension.MethodB(this IMyInterface myInterface, int i)");

    }

}

Първият метод - MethodA – разширява интерфейса IMyInterface като изисква един параметър от тип int. Обърнете внимание, че класът A не дефинира метод MethodA;класът B дефинира MethodA със същите параметри, а класът C дефинира MethodA,който приема параметър от тип object.

Вторият метод - MethodA – разширява интерфейса IMyInterface като изисква един параметър от тип string. Забележете, че никой от класовете не дефинира MethodA с такъв параметър.

Третият метод - MethodB – разширява интерфейса IMyInterface като не изисква параметри за да се извика. Важното тук е, че интерфейса и всички класове имат метод MethodB, който не приема параметри.

Нека да изпълним един малък пример, за да видим кои методи се изпълняват:

static void Main(string[] args)

{

    A a = new A();

    B b = new B();

    C c = new C();

    TestMethodBinding(a, b, c);

}

 

static void TestMethodBinding(A a, B b, C c)

{

    // A has no methods, so each call resolves to

    // the extension methods whose signatures match.

    a.MethodA(1);  // Extension.MethodA(object, int)

    a.MethodA("hello");  // Extension.MethodA(object, string)

    a.MethodB();

 

    // B itself has a method with this signature.

    b.MethodA(1);  // B.MethodA(int)

    b.MethodB();

 

    // B has no matching method, but E does.

    b.MethodA("hello");  // Extension.MethodA(object, string)

 

    // In each case C has a matching instance method.

    c.MethodA(1);  // C.MethodA(object)

    c.MethodA("hello");  // C.MethodA(object)

    c.MethodB();

}

В този пример създаваме три инстанции съответно на класовете A, B, C и ги подаваме на метода TestMethodBinding, който извиква съответните методи на всеки клас.

За класът А:

·         Всяко извикване на MethodA се свързва с дефинираните разширяващи методи, защото клас A  не дефинира MethodA.

·         Извикването на MethodB се изпълнява от дефинираният метод в тялото на класа.

За класът B:

·         Извикването на MethodA с параметър 1 се изпълнява от дефинираният метод в тялото на класа.

·         Извикването на MethodB също се изпълнява от дефинираният метод в тялото на класа.

·         Извикването на MethodA с параметър "hello" се изпълнява от разширяващият метод, защото няма подходящ метод в тялото на класа, който да приема параметър от този тип.

За класът C:

·         Извикването на MethodA с параметър 1 се изпълнява от дефинираният метод в тялото на класа, защото типът int може да се преобразува към object.

·         Извикването на MethodA с параметър "hello" се изпълнява изпълнява от дефинираният метод в тялото на класа, защото типът string може да се преобразува към object.

·         Извикването на MethodB също се изпълнява от дефинираният метод в тялото на класа.

Извод: Разширяващите методи имат най-нисък приоритет и се изпълняват само ако няма метод със същото име в тялото на разширяваният клас/интрефейс и същите параметри или параметрите не могат да се преубразуват до изискваните.

Разширяващите методи са мощен инструмент за добавяне на функционалност към компилирани обекти, но трябва са се използва внимателно.

Основното предимство се разкрива в следната ситуация: Нека си представив, че нашата компания използва външни библиотеки (3rd party libraries), но те нямат малка функционалност, от която ние имаме нужда. Да приемем, че разполагаме със сорс кода на библиотеката. В този случай можем да променим сорса в съответствие с нашите нужди (и в зависимост от лиценза) и да прекомпилираме библиотеката. Дотук добре, но при следваща версия на библиотеката промените, които сме направили трябва да се прилагат отново. А това може да отнеме същото време както първоначалното модифициране. В този случай е много удачно да използваме разширяващи медоти.

Проекти с примерите

Проект за VS 2008 (6.5KB)

 

Следваща част: Ламбда изрази

Content