Bővítménymetelyek (C# programozási útmutató)

A bővítménymetelyek lehetővé teszik, hogy új származtatott típus létrehozása, újrafordítás vagy az eredeti típus egyéb módosítása nélkül "hozzáadja" a metódusokat a meglévő típusokhoz. A bővítménymetelyek statikus metódusok, de úgy hívják őket, mintha a kiterjesztett típushoz tartozó példánymetodusok lennének. A C#, F# és Visual Basic nyelven írt ügyfélkódok esetében nincs nyilvánvaló különbség a bővítménymetódus meghívása és a típusban definiált metódusok között.

A leggyakoribb kiterjesztési módszerek a LINQ standard lekérdezési operátorok, amelyek lekérdezési funkciókat adnak hozzá a meglévő System.Collections.IEnumerable és System.Collections.Generic.IEnumerable<T> a típusok számára. A szabványos lekérdezési operátorok használatához először hozza őket hatókörbe egy using System.Linq irányelvvel. Ezután úgy tűnik, hogy minden implementált IEnumerable<T> típus rendelkezik példánymetelyekkel, például GroupBy, OrderBy, Averagestb. Ezeket a további metódusokat az IntelliSense utasításkiegészítésében láthatja, ha a "pont" szöveget írja be egy IEnumerable<T> olyan típusú példány után, mint például List<T> vagy Array.

OrderBy példa

Az alábbi példa bemutatja, hogyan hívhatja meg a standard lekérdezési operátor OrderBy metódust egész számok tömbjén. A zárójelben szereplő kifejezés egy lambda kifejezés. Számos szabványos lekérdezési operátor paraméterként használja a Lambda-kifejezéseket, de ez nem követelmény a bővítménymetelyekhez. További információ: Lambda Expressions.

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

A bővítménymetódusok statikus metódusokként vannak definiálva, de a példánymetódus szintaxisával vannak meghívva. Az első paraméter határozza meg, hogy a metódus melyik típuson működik. A paraméter ezt a módosítót követi. A bővítménymetelyek csak akkor tartoznak hatókörbe, ha a névteret explicit módon importálja a forráskódba egy using direktívával.

Az alábbi példa egy, az osztályhoz definiált bővítménymetódust System.String mutat be. Ez egy nem beágyazott, nem általános statikus osztályon belül van definiálva:

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

A WordCount kiterjesztési módszer az using irányelv hatálya alá helyezhető:

using ExtensionMethods;

Egy alkalmazásból pedig az alábbi szintaxissal hívható meg:

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

Meghívja a bővítménymetódust a kódban a példánymetódus szintaxisával. A fordító által létrehozott köztes nyelv (IL) lefordítja a kódot a statikus metódus hívására. A beágyazás elvét nem igazán sértik meg. A bővítménymetelyek nem férnek hozzá a privát változókhoz az általuk kiterjesztett típusban.

MyExtensions Az osztály és a WordCount metódus is , staticés az összes többi static taghoz hasonlóan elérhető. A WordCount metódus más metódusokhoz hasonlóan static az alábbiak szerint hívható meg:

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

Az előző C# kód:

  • Deklarál és hozzárendel egy új string nevet s , amelynek értéke : "Hello Extension Methods".
  • Argumentumként megadott shívásokMyExtensions.WordCount.

További információ: Egyéni bővítménymetódus implementálása és meghívása.

Általánosságban elmondható, hogy valószínűleg sokkal gyakrabban fogja meghívni a bővítménymetszeteket, mint a saját implementálását. Mivel a bővítménymetódusokat példánymetódus-szintaxissal hívjuk meg, az ügyfélkódból való használatához nincs szükség speciális ismeretekre. Ha engedélyezni szeretné a bővítménymetelyeket egy adott típushoz, csak adjon hozzá egy using irányelvet ahhoz a névtérhez, amelyben a metódusok definiálva vannak. A szabványos lekérdezési operátorok használatához például adja hozzá ezt az using irányelvet a kódhoz:

using System.Linq;

(Előfordulhat, hogy hozzá kell adnia egy hivatkozást a System.Core.dll.) Megfigyelheti, hogy a standard lekérdezési operátorok mostantól további módszerekként jelennek meg az IntelliSense-ben a legtöbb IEnumerable<T> típushoz.

Kötéskiterjesztési módszerek fordításkor

A bővítménymetelyekkel kiterjeszthet egy osztályt vagy felületet, de nem bírálhatja felül őket. A rendszer soha nem hív meg olyan bővítménymetódusokat, amelynek neve és aláírása megegyezik az illesztő- vagy osztálymetódus nevével. Fordításkor a bővítménymetelyek mindig alacsonyabb prioritással rendelkeznek, mint a típusban definiált példánymetelyek. Más szóval, ha egy típusnak van egy metódusa, amelynek neve Process(int i)ugyanaz, és ön rendelkezik ugyanazzal az aláírással rendelkező bővítménymetódussal, a fordító mindig a példánymetódushoz fog kapcsolódni. Amikor a fordító metódushívással találkozik, először egyezést keres a típus példánymetódusaiban. Ha nem talál egyezést, megkeresi a típushoz definiált bővítménymetódusokat, és a talált első bővítménymetódushoz köti.

Példa

Az alábbi példa azokat a szabályokat mutatja be, amelyeket a C#-fordító követ annak meghatározásához, hogy a metódushívást egy példánymetódushoz vagy egy bővítménymetódushoz kell-e kötni. A statikus osztály Extensions minden implementált típushoz definiált bővítménymetelyeket IMyInterfacetartalmaz. Osztályok A, Bés C minden implementálja a felületet.

A MethodB bővítménymetódus soha nem lesz meghívva, mert neve és aláírása pontosan egyezik az osztályok által már implementált metódusokkal.

Ha a fordító nem talál egy egyező aláírással rendelkező példánymetódust, akkor egy megfelelő bővítménymetódushoz fog kapcsolódni, ha létezik ilyen.

// 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()
 */

Gyakori használati minták

Gyűjteményfunkciók

A múltban gyakori volt a "Gyűjteményosztályok" létrehozása, amely egy adott típus felületét implementálta System.Collections.Generic.IEnumerable<T> , és olyan funkciókat tartalmazott, amelyek az ilyen típusú gyűjteményeken jártak el. Bár nincs semmi baj az ilyen típusú gyűjteményobjektumok létrehozásával, ugyanez a funkció a bővítmény használatával érhető el a System.Collections.Generic.IEnumerable<T>. A bővítmények előnye, hogy lehetővé teszik a funkciók meghívását bármely gyűjteményből, például egy System.Array vagy System.Collections.Generic.List<T> az adott típuson implementálható System.Collections.Generic.IEnumerable<T> gyűjteményből. Erre az Int32 tömböt használó példa a cikk korábbi részében található.

Rétegspecifikus funkciók

Hagymaarchitektúra vagy más rétegzett alkalmazásterv használata esetén gyakran előfordul, hogy tartományi entitások vagy adatátviteli objektumok készlete van, amelyek az alkalmazáshatárokon keresztüli kommunikációra használhatók. Ezek az objektumok általában nem, vagy csak minimális funkciókat tartalmaznak, amelyek az alkalmazás minden rétegére vonatkoznak. A bővítménymetódusok az egyes alkalmazásrétegekre jellemző funkciók hozzáadására használhatók anélkül, hogy az objektumot le kellene tölteni olyan metódusokkal, amelyek nem szükségesek vagy más rétegekben kellenek.

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

Előre definiált típusok kiterjesztése

Ahelyett, hogy új objektumokat hoznánk létre, amikor újrahasználható funkciókat kell létrehozni, gyakran kiterjeszthetünk egy meglévő típust, például egy .NET- vagy CLR-típust. Ha például nem használunk bővítménymetelyeket, létrehozhatunk egy Engine vagy Query több osztályt is, hogy végrehajtsunk egy lekérdezést egy SQL Serveren, amelyet a kód több helyről is meghívhat. Ehelyett azonban bővíthetjük az System.Data.SqlClient.SqlConnection osztályt bővítménymetelyekkel, hogy a lekérdezést bárhonnan végrehajtsuk, ahol van kapcsolat egy SQL Serverrel. Más példák lehetnek az osztály általános funkcióinak hozzáadására, az System.String objektum adatfeldolgozási képességeinek kiterjesztésére, valamint System.Exception az System.IO.Stream objektumok adott hibakezelési funkciókhoz való hozzáadására. Az ilyen típusú használati eseteket csak a képzelet és a jó érzék korlátozza.

Az előre definiált típusok kiterjesztése nehézkes lehet a típusok esetében struct , mert az érték adja át őket a metódusok számára. Ez azt jelenti, hogy a szerkezet minden módosítása a szerkezet egy példányán történik. Ezek a módosítások nem láthatók, ha a bővítménymetódus kilép. A módosító hozzáadható az ref első argumentumhoz, így bővítménymetódusként ref is használható. A ref kulcsszó szemantikai különbségek nélkül jelenhet meg a this kulcsszó előtt vagy után. A ref módosító hozzáadása azt jelzi, hogy az első argumentumot hivatkozással adja át a függvény. Ez lehetővé teszi olyan bővítménymetelyek írását, amelyek megváltoztatják a kibővítendő szerkezet állapotát (vegye figyelembe, hogy a privát tagok nem érhetők el). A bővítménymetódus első paramétere ref csak az strukturáláshoz korlátozott értéktípusok vagy általános típusok (további információkért lásd struct a korlátozást) engedélyezett. Az alábbi példa bemutatja, hogyan lehet bővítménymetódussal ref közvetlenül módosítani egy beépített típust anélkül, hogy újra kellene hozzárendelni az eredményt, vagy át kellene adni egy függvényen a ref kulcsszóval:

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
    }
}

Ez a következő példa a felhasználó által definiált ref strustruktúratípusok bővítménymetszeteit mutatja be:

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
    }
}

Általános irányelvek

Bár továbbra is célszerűbb funkciókat hozzáadni egy objektum kódjának módosításával vagy egy új típus származtatásával, amikor ésszerű és lehetséges, a bővítménymetódusok kulcsfontosságú lehetőséggé váltak az újrahasználható funkciók létrehozásához az egész .NET-ökoszisztémában. Azokban az esetekben, amikor az eredeti forrás nem az Ön felügyelete alatt áll, ha egy származtatott objektum nem megfelelő vagy lehetetlen, vagy ha a funkciónak nem szabad a megfelelő hatókörön kívülre kerülnie, a bővítménymetódusok kiváló választásnak számítanak.

A származtatott típusokról további információt az Öröklés című témakörben talál.

Ha bővítménymetódust használ egy olyan típus kiterjesztéséhez, amelynek forráskódját nem ön felügyeli, azzal a kockázattal jár, hogy a típus implementációjának változása a bővítménymetódus megszakadását okozza.

Ha egy adott típushoz bővítménymetó módszereket implementál, jegyezze meg a következő pontokat:

  • A bővítménymetódus nem hívható meg, ha ugyanazzal az aláírással rendelkezik, mint a típusban definiált metódus.
  • A bővítménymetelyek a névtér szintjén kerülnek hatókörbe. Ha például több olyan statikus osztálya van, amely egyetlen névtérben Extensionstartalmaz bővítménymetszeteket, azokat az using Extensions; irányelv fogja hatókörbe helyezni.

Az ön által implementált osztálytárak esetében ne használjon bővítménymetelyeket a szerelvény verziószámának növelésének elkerülése érdekében. Ha jelentős funkciókat szeretne hozzáadni egy olyan kódtárhoz, amelynek a forráskódja a tulajdonosa, kövesse a .NET szerelvény-verziószámozásra vonatkozó irányelveit. További információ: Szerelvény verziószámozása.

Lásd még