Dela via


Tilläggsmetoder (C#-programmeringsguide)

Med tilläggsmetoder kan du "lägga till" metoder i befintliga typer utan att skapa en ny härledd typ, kompilera om eller på annat sätt ändra den ursprungliga typen. Tilläggsmetoder är statiska metoder, men de anropas som om de vore instansmetoder för den utökade typen. För klientkod skriven i C#, F# och Visual Basic finns det ingen uppenbar skillnad mellan att anropa en tilläggsmetod och de metoder som definierats i en typ.

De vanligaste tilläggsmetoderna är LINQ-standardfrågeoperatorer som lägger till frågefunktioner till befintliga System.Collections.IEnumerable och System.Collections.Generic.IEnumerable<T> typer. Om du vill använda standardfrågeoperatorerna tar du först med dem i omfånget med ett using System.Linq direktiv. Sedan verkar alla typer som implementeras IEnumerable<T> ha instansmetoder som GroupBy, OrderBy, Averageoch så vidare. Du kan se dessa ytterligare metoder i IntelliSense-instruktionen när du skriver "dot" efter en instans av en IEnumerable<T> typ som List<T> eller Array.

OrderBy-exempel

I följande exempel visas hur du anropar standardmetoden för frågeoperatorer OrderBy i en matris med heltal. Uttrycket i parenteser är ett lambda-uttryck. Många vanliga frågeoperatorer använder lambda-uttryck som parametrar, men detta är inte ett krav för tilläggsmetoder. Mer information finns i Lambda-uttryck.

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

Tilläggsmetoder definieras som statiska metoder men anropas med hjälp av instansmetodsyntax. Deras första parameter anger vilken typ av metod som används. Parametern följer den här modifieraren. Tilläggsmetoder finns bara i omfånget när du uttryckligen importerar namnområdet till källkoden med ett using direktiv.

I följande exempel visas en tilläggsmetod som definierats för System.String klassen. Den definieras i en icke-kapslad, icke-generisk statisk klass:

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

Tilläggsmetoden WordCount kan omfattas av detta using direktiv:

using ExtensionMethods;

Och det kan anropas från ett program med hjälp av den här syntaxen:

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

Du anropar tilläggsmetoden i koden med instansmetodens syntax. Det mellanliggande språket (IL) som genereras av kompilatorn översätter koden till ett anrop till den statiska metoden. Inkapslingsprincipen överträds egentligen inte. Tilläggsmetoder kan inte komma åt privata variabler i den typ som de utökar.

MyExtensions Både klassen och WordCount metoden är static, och den kan nås som alla andra static medlemmar. Metoden WordCount kan anropas som andra static metoder på följande sätt:

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

Föregående C#-kod:

  • Deklarerar och tilldelar ett nytt string namn s med värdet "Hello Extension Methods".
  • Anropar MyExtensions.WordCount angivet argument s.

Mer information finns i Implementera och anropa en anpassad tilläggsmetod.

I allmänhet kommer du förmodligen att anropa tilläggsmetoder mycket oftare än att implementera dina egna. Eftersom tilläggsmetoder anropas med instansmetodsyntax krävs ingen särskild kunskap för att använda dem från klientkoden. Om du vill aktivera tilläggsmetoder för en viss typ lägger du bara till ett using direktiv för namnområdet där metoderna definieras. Om du till exempel vill använda standardfrågeoperatorerna lägger du till det här using direktivet i koden:

using System.Linq;

(Du kan också behöva lägga till en referens till System.Core.dll.) Du kommer att märka att standardfrågeoperatorerna nu visas i IntelliSense som ytterligare metoder tillgängliga för de flesta IEnumerable<T> typer.

Bindningstilläggsmetoder vid kompileringstid

Du kan använda tilläggsmetoder för att utöka en klass eller ett gränssnitt, men inte för att åsidosätta dem. En tilläggsmetod med samma namn och signatur som ett gränssnitt eller en klassmetod anropas aldrig. Vid kompilering har tilläggsmetoder alltid lägre prioritet än instansmetoder som definierats i själva typen. Med andra ord, om en typ har en metod med namnet Process(int i), och du har en tilläggsmetod med samma signatur, binder kompilatorn alltid till instansmetoden. När kompilatorn stöter på ett metodanrop letar den först efter en matchning i typens instansmetoder. Om ingen matchning hittas söker den efter tilläggsmetoder som har definierats för typen och binder till den första tilläggsmetoden som hittas.

Exempel

I följande exempel visas de regler som C#-kompilatorn följer för att avgöra om ett metodanrop ska bindas till en instansmetod på typen eller till en tilläggsmetod. Den statiska klassen Extensions innehåller tilläggsmetoder som definierats för alla typer som implementerar IMyInterface. Klasser A, Boch C alla implementerar gränssnittet.

Tilläggsmetoden MethodB anropas aldrig eftersom dess namn och signatur exakt matchar metoder som redan implementerats av klasserna.

När kompilatorn inte kan hitta en instansmetod med en matchande signatur binder den till en matchande tilläggsmetod om det finns en sådan.

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

Vanliga användningsmönster

Samlingsfunktioner

Tidigare var det vanligt att skapa "Samlingsklasser" som implementerade System.Collections.Generic.IEnumerable<T> gränssnittet för en viss typ och innehöll funktioner som agerade på samlingar av den typen. Även om det inte är något fel med att skapa den här typen av samlingsobjekt kan samma funktioner uppnås med hjälp av ett tillägg på System.Collections.Generic.IEnumerable<T>. Tillägg har fördelen att tillåta att funktionerna anropas från alla samlingar, till exempel en System.Array eller System.Collections.Generic.List<T> som implementeras System.Collections.Generic.IEnumerable<T> på den typen. Ett exempel på detta med hjälp av en matris med Int32 finns tidigare i den här artikeln.

Lagerspecifika funktioner

När du använder en lökarkitektur eller annan programdesign i flera lager är det vanligt att ha en uppsättning domänentiteter eller dataöverföringsobjekt som kan användas för att kommunicera över programgränser. Dessa objekt innehåller vanligtvis inga funktioner, eller bara minimala funktioner som gäller för alla skikt i programmet. Tilläggsmetoder kan användas för att lägga till funktioner som är specifika för varje programskikt utan att läsa in objektet med metoder som inte behövs eller som önskas i andra lager.

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

Utöka fördefinierade typer

I stället för att skapa nya objekt när återanvändbara funktioner behöver skapas kan vi ofta utöka en befintlig typ, till exempel en .NET- eller CLR-typ. Om vi till exempel inte använder tilläggsmetoder kan vi skapa en Engine eller Query -klass för att utföra arbetet med att köra en fråga på en SQL Server som kan anropas från flera platser i koden. Men vi kan i stället utöka System.Data.SqlClient.SqlConnection klassen med hjälp av tilläggsmetoder för att utföra frågan var vi än har en anslutning till en SQL Server. Andra exempel kan vara att lägga till vanliga funktioner i System.String klassen, utöka databehandlingsfunktionerna för System.IO.Stream objektet och System.Exception objekt för specifika funktioner för felhantering. Dessa typer av användningsfall begränsas endast av din fantasi och ditt goda förnuft.

Det kan vara svårt att utöka fördefinierade typer med struct typer eftersom de skickas med värde till metoder. Det innebär att alla ändringar i struct görs i en kopia av structen. Ändringarna visas inte när tilläggsmetoden avslutas. Du kan lägga till ref modifieraren i det första argumentet, vilket gör det till en ref tilläggsmetod. Nyckelordet ref kan visas före eller efter nyckelordet this utan några semantiska skillnader. ref Att lägga till modifieraren anger att det första argumentet skickas med referens. På så sätt kan du skriva tilläggsmetoder som ändrar tillståndet för den struct som utökas (observera att privata medlemmar inte är tillgängliga). Endast värdetyper eller generiska typer som är begränsade till struct (se struct villkor för mer information) tillåts som den första parametern för en ref tilläggsmetod. I följande exempel visas hur du använder en ref tilläggsmetod för att direkt ändra en inbyggd typ utan att du behöver tilldela om resultatet eller skicka det genom en funktion med nyckelordet ref :

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

I nästa exempel visas ref tilläggsmetoder för användardefinierade structtyper:

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

Allmänna riktlinjer

Det anses fortfarande vara bättre att lägga till funktioner genom att ändra ett objekts kod eller härleda en ny typ när det är rimligt och möjligt att göra det, men tilläggsmetoder har blivit ett viktigt alternativ för att skapa återanvändbara funktioner i hela .NET-ekosystemet. För de tillfällen då den ursprungliga källan inte står under din kontroll, när ett härlett objekt är olämpligt eller omöjligt, eller när funktionen inte ska exponeras utanför dess tillämpliga omfång, är tilläggsmetoder ett utmärkt val.

Mer information om härledda typer finns i Arv.

När du använder en tilläggsmetod för att utöka en typ vars källkod du inte har kontroll över riskerar du att en ändring i implementeringen av typen leder till att tilläggsmetoden bryts.

Kom ihåg följande om du implementerar tilläggsmetoder för en viss typ:

  • En tilläggsmetod anropas inte om den har samma signatur som en metod som definierats i typen.
  • Tilläggsmetoder tas med i omfånget på namnområdesnivå. Om du till exempel har flera statiska klasser som innehåller tilläggsmetoder i ett enda namnområde med namnet Extensionskommer alla att omfattas av using Extensions; direktivet.

För ett klassbibliotek som du implementerade bör du inte använda tilläggsmetoder för att undvika att öka versionsnumret för en sammansättning. Om du vill lägga till betydande funktioner i ett bibliotek som du äger källkoden för följer du .NET-riktlinjerna för versionshantering av sammansättning. Mer information finns i Versionshantering för sammansättning.

Se även