Metody rozšíření (Průvodce programováním v C#)

Metody rozšíření umožňují „přidávat“ metody ke stávajícím typům bez vytváření nového odvozeného typu, rekompilace nebo jiné změny původního typu. Rozšiřující metody jsou statické metody, ale volají se, jako by se jednalo o metody instance rozšířeného typu. Pro klientský kód napsaný v jazyce C#, F# a Visual Basic neexistuje žádný zjevný rozdíl mezi voláním metody rozšíření a metodami definovanými v typu.

Nejběžnějšími rozšiřujícími metodami jsou standardní operátory dotazů LINQ, které přidávají funkce dotazu do existujících System.Collections.IEnumerable a System.Collections.Generic.IEnumerable<T> typů. Pokud chcete použít standardní operátory dotazů, nejprve je přineste do oboru direktivou using System.Linq . Pak se zdá, že jakýkoli typ, který implementuje IEnumerable<T> , má metody instance, jako GroupByje , OrderBy, Averagea tak dále. Tyto další metody můžete zobrazit v doplňování příkazů IntelliSense při zadání tečky za instancí IEnumerable<T> typu, například List<T> nebo Array.

Příklad OrderBy

Následující příklad ukazuje, jak volat standardní metodu operátoru OrderBy dotazu na pole celých čísel. Výraz v závorkách je výraz lambda. Mnoho standardních operátorů dotazu bere výrazy lambda jako parametry, ale to není požadavek na rozšiřující metody. Další informace najdete v tématu Výrazy lambda.

class ExtensionMethods2
{

    static void Main()
    {
        int[] ints = [10, 45, 15, 39, 21, 26];
        var result = ints.OrderBy(g => g);
        foreach (var i in result)
        {
            System.Console.Write(i + " ");
        }
    }
}
//Output: 10 15 21 26 39 45

Metody rozšíření jsou definovány jako statické metody, ale jsou volány pomocí syntaxe metody instance. První parametr určuje typ, se kterým metoda pracuje. Parametr se řídí tímto modifikátorem. Metody rozšíření jsou v oboru pouze v případech, kdy explicitně importujete obor názvů do zdrojového kódu pomocí direktivy using .

Následující příklad ukazuje rozšiřující metodu definovanou System.String pro třídu. Definuje se uvnitř nenořené, ne generické statické třídy:

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this string str)
        {
            return str.Split(new char[] { ' ', '.', '?' },
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

Metodu WordCount rozšíření lze rozšířit do oblasti působnosti této using směrnice:

using ExtensionMethods;

A může být volána z aplikace pomocí následující syntaxe:

string s = "Hello Extension Methods";
int i = s.WordCount();

V kódu vyvoláte metodu rozšíření pomocí syntaxe metody instance. Zprostředkující jazyk (IL) vygenerovaný kompilátorem přeloží váš kód do volání statické metody. Princip zapouzdření není ve skutečnosti porušován. Metody rozšíření nemají přístup k privátním proměnným v typu, který rozšiřují.

Třída MyExtensions i metoda jsou statica lze k němu přistupovat stejně jako ke všem ostatním static členůmWordCount. Metodu WordCount lze vyvolat stejně jako jiné static metody následujícím způsobem:

string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);

Předchozí kód jazyka C#:

  • Deklaruje a přiřadí nový string pojmenovaný s s hodnotou "Hello Extension Methods".
  • Volání MyExtensions.WordCount daného argumentu s.

Další informace naleznete v tématu Jak implementovat a volat vlastní rozšiřující metodu.

Obecně platí, že budete pravděpodobně volat rozšiřující metody mnohem častěji než implementovat vlastní. Vzhledem k tomu, že metody rozšíření jsou volány pomocí syntaxe metody instance, není vyžadována žádná zvláštní znalost, abyste je mohli použít v klientském kódu. Chcete-li povolit rozšiřující metody pro určitý typ, stačí přidat direktivu using pro obor názvů, ve kterém jsou metody definovány. Pokud chcete například použít standardní operátory dotazu, přidejte do kódu tuto using direktivu:

using System.Linq;

(Můžete také přidat odkaz na System.Core.dll.) Všimněte si, že standardní operátory dotazů se teď v IntelliSense zobrazují jako další metody dostupné pro většinu IEnumerable<T> typů.

Vytváření vazeb na metody rozšíření v době kompilace

Metody rozšíření můžete použít k rozšíření třídy nebo rozhraní, nikoli však k jejich přepsání. Metoda rozšíření se stejným názvem a signaturou, jako má rozhraní nebo metoda třídy, nebude nikdy volána. V době kompilace mají metody rozšíření vždy nižší prioritu než metody instance definované v samotném typu. Jinými slovy, pokud typ má pojmenovanou Process(int i)metodu a máte rozšiřující metodu se stejným podpisem, kompilátor bude vždy svázat s metodou instance. Pokud kompilátor narazí na vyvolání metody, nejprve vyhledá shodu v metodách instance tohoto typu. Pokud se nenajde žádná shoda, vyhledá všechny rozšiřující metody definované pro daný typ a vytvoří vazbu na první metodu rozšíření, kterou najde.

Příklad

Následující příklad znázorňuje pravidla, které u kompilátoru jazyka C# určují, zda vytvořit vazbu volání metody s metodou instance v rámci typu, nebo s metodou rozšíření. Statická třída Extensions obsahuje rozšiřující metody definované pro libovolný typ, který implementuje IMyInterface. Třídy A, Ba C všechny implementovat rozhraní.

Rozšiřující MethodB metoda se nikdy nevolá, protože jeho název a podpis přesně odpovídají metodám, které již byly implementovány třídami.

Pokud kompilátor nemůže najít metodu instance s odpovídajícím podpisem, vytvoří vazbu na odpovídající metodu rozšíření, pokud existuje.

// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
    public interface IMyInterface
    {
        // Any class that implements IMyInterface must define a method
        // that matches the following signature.
        void MethodB();
    }
}

// Define extension methods for IMyInterface.
namespace Extensions
{
    using System;
    using DefineIMyInterface;

    // The following extension methods can be accessed by instances of any
    // class that implements IMyInterface.
    public static class Extension
    {
        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 in ExtensionMethodsDemo1, because each
        // of the three classes A, B, and C implements a method named MethodB
        // that has a matching signature.
        public static void MethodB(this IMyInterface myInterface)
        {
            Console.WriteLine
                ("Extension.MethodB(this IMyInterface myInterface)");
        }
    }
}

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
    using System;
    using Extensions;
    using DefineIMyInterface;

    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)");
        }
    }

    class ExtMethodDemo
    {
        static void Main(string[] args)
        {
            // Declare an instance of class A, class B, and class C.
            A a = new A();
            B b = new B();
            C c = new C();

            // For a, b, and c, call the following methods:
            //      -- MethodA with an int argument
            //      -- MethodA with a string argument
            //      -- MethodB with no argument.

            // A contains no MethodA, so each call to MethodA resolves to
            // the extension method that has a matching signature.
            a.MethodA(1);           // Extension.MethodA(IMyInterface, int)
            a.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // A has a method that matches the signature of the following call
            // to MethodB.
            a.MethodB();            // A.MethodB()

            // B has methods that match the signatures of the following
            // method calls.
            b.MethodA(1);           // B.MethodA(int)
            b.MethodB();            // B.MethodB()

            // B has no matching method for the following call, but
            // class Extension does.
            b.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // C contains an instance method that matches each of the following
            // method calls.
            c.MethodA(1);           // C.MethodA(object)
            c.MethodA("hello");     // C.MethodA(object)
            c.MethodB();            // C.MethodB()
        }
    }
}
/* Output:
    Extension.MethodA(this IMyInterface myInterface, int i)
    Extension.MethodA(this IMyInterface myInterface, string s)
    A.MethodB()
    B.MethodA(int i)
    B.MethodB()
    Extension.MethodA(this IMyInterface myInterface, string s)
    C.MethodA(object obj)
    C.MethodA(object obj)
    C.MethodB()
 */

Běžné vzory použití

Funkce kolekce

V minulosti bylo běžné vytvořit "Třídy kolekce", které implementovaly System.Collections.Generic.IEnumerable<T> rozhraní pro daný typ a obsažené funkce, které fungovaly na kolekcích tohoto typu. I když není nic špatného při vytváření tohoto typu objektu kolekce, stejné funkce lze dosáhnout pomocí rozšíření na objektu System.Collections.Generic.IEnumerable<T>. Rozšíření mají výhodu, která umožňuje volat funkce z jakékoli kolekce, jako System.Array je například kolekce nebo System.Collections.Generic.List<T> která implementuje System.Collections.Generic.IEnumerable<T> tento typ. Příklad použití pole Int32 najdete dříve v tomto článku.

Funkce specifické pro vrstvu

Při použití architektury onionu nebo jiného návrhu vrstvené aplikace je běžné mít sadu entit domény nebo objektů přenosu dat, které je možné použít ke komunikaci přes hranice aplikace. Tyto objekty obecně neobsahují žádné funkce nebo pouze minimální funkce, které platí pro všechny vrstvy aplikace. Rozšiřující metody lze použít k přidání funkcí, které jsou specifické pro každou aplikační vrstvu bez načtení objektu dolů s metodami, které nejsou potřeba nebo požadovány v jiných vrstvách.

public class DomainEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

static class DomainEntityExtensions
{
    static string FullName(this DomainEntity value)
        => $"{value.FirstName} {value.LastName}";
}

Rozšíření předdefinovaných typů

Místo vytváření nových objektů, když je potřeba vytvořit opakovaně použitelné funkce, můžeme často rozšířit existující typ, například typ .NET nebo CLR. Pokud například nepoužíváme rozšiřující metody, můžeme vytvořit Engine nebo Query třídu pro provádění dotazu na SQL Serveru, který může být volána z více míst v našem kódu. Můžeme ale místo toho rozšířit System.Data.SqlClient.SqlConnection třídu pomocí rozšiřujících metod k provedení dotazu odkudkoli, kde máme připojení k SQL Serveru. Dalšími příklady mohou být přidání běžných funkcí do System.String třídy, rozšíření možností zpracování dat objektu System.IO.Stream a System.Exception objektů pro konkrétní funkce zpracování chyb. Tyto typy případů použití jsou omezeny pouze vaší představivostí a dobrým smyslem.

Rozšíření předdefinovaných typů může být obtížné u struct typů, protože jsou předány hodnotami metodám. To znamená, že všechny změny struktury se provádějí v kopii struktury. Tyto změny se po ukončení metody rozšíření nezobrazí. Modifikátor můžete přidat ref do prvního argumentu, aby se jedná o rozšiřující metodu ref . Klíčové ref slovo se může zobrazit před klíčovým slovem nebo za ho this bez jakýchkoli sémantických rozdílů. Přidání modifikátoru ref označuje, že první argument je předán odkazem. To umožňuje psát rozšiřující metody, které mění stav rozšířené struktury (upozorňujeme, že soukromé členy nejsou přístupné). Jako první parametr ref metody rozšíření jsou povoleny pouze typy hodnot nebo obecné typy omezené na strukturu (viz struct omezení pro další informace). Následující příklad ukazuje, jak pomocí ref rozšiřující metody přímo upravit předdefinovaný typ, aniž by bylo nutné znovu přiřadit výsledek nebo předat funkci pomocí klíčového ref slova:

public static class IntExtensions
{
    public static void Increment(this int number)
        => number++;

    // Take note of the extra ref keyword here
    public static void RefIncrement(this ref int number)
        => number++;
}

public static class IntProgram
{
    public static void Test()
    {
        int x = 1;

        // Takes x by value leading to the extension method
        // Increment modifying its own copy, leaving x unchanged
        x.Increment();
        Console.WriteLine($"x is now {x}"); // x is now 1

        // Takes x by reference leading to the extension method
        // RefIncrement changing the value of x directly
        x.RefIncrement();
        Console.WriteLine($"x is now {x}"); // x is now 2
    }
}

Tento další příklad ukazuje ref rozšiřující metody pro uživatelem definované typy struktur:

public struct Account
{
    public uint id;
    public float balance;

    private int secret;
}

public static class AccountExtensions
{
    // ref keyword can also appear before the this keyword
    public static void Deposit(ref this Account account, float amount)
    {
        account.balance += amount;

        // The following line results in an error as an extension
        // method is not allowed to access private members
        // account.secret = 1; // CS0122
    }
}

public static class AccountProgram
{
    public static void Test()
    {
        Account account = new()
        {
            id = 1,
            balance = 100f
        };

        Console.WriteLine($"I have ${account.balance}"); // I have $100

        account.Deposit(50f);
        Console.WriteLine($"I have ${account.balance}"); // I have $150
    }
}

Obecné pokyny

I když je stále vhodnější přidat funkce úpravou kódu objektu nebo odvozením nového typu, kdykoli je to rozumné a možné, metody rozšíření se staly zásadní možností pro vytváření opakovaně použitelných funkcí v ekosystému .NET. Pro tyto situace, kdy původní zdroj není pod kontrolou, pokud je odvozený objekt nevhodný nebo nemožné nebo pokud by funkce neměla být vystavena nad rámec příslušného oboru, jsou metody rozšíření skvělou volbou.

Další informace o odvozených typech naleznete v tématu Dědičnost.

Při použití rozšiřující metody rozšíření typu, jehož zdrojový kód nemáte pod kontrolou, můžete riskovat, že změna implementace typu způsobí přerušení metody rozšíření.

Pokud implementujete rozšiřující metody pro daný typ, mějte na paměti následující body:

  • Metoda rozšíření není volána, pokud má stejný podpis jako metoda definovaná v typu.
  • Dále jsou metody rozšíření přeneseny do rozsahu na úrovni oboru názvů. Pokud máte například více statických tříd, které obsahují rozšiřující metody v jednom oboru názvů s názvem Extensions, budou všechny přeneseny do oboru působnosti direktivou using Extensions; .

Chcete-li zamezit zvýšení čísla verze sestavení, neměli byste pro implementovanou knihovnu metody rozšíření používat. Pokud chcete do knihovny, pro kterou vlastníte zdrojový kód, přidat významné funkce, postupujte podle pokynů .NET pro správu verzí sestavení. Další informace naleznete v tématu Správa verzí sestavení.

Viz také