Extensiemethoden (C#-programmeerhandleiding)

Met extensiemethoden kunt u methoden toevoegen aan bestaande typen zonder een nieuw afgeleid type te maken, opnieuw te compileren of anderszins het oorspronkelijke type te wijzigen. Extensiemethoden zijn statische methoden, maar ze worden aangeroepen alsof het exemplaarmethoden voor het uitgebreide type zijn. Voor clientcode die is geschreven in C#, F# en Visual Basic, is er geen duidelijk verschil tussen het aanroepen van een extensiemethode en de methoden die in een type zijn gedefinieerd.

De meest voorkomende extensiemethoden zijn de standaard-LINQ-queryoperators die queryfunctionaliteit toevoegen aan de bestaande System.Collections.IEnumerable en System.Collections.Generic.IEnumerable<T> typen. Als u de standaardqueryoperators wilt gebruiken, moet u ze eerst binnen het bereik brengen met een using System.Linq richtlijn. Vervolgens lijkt elk type dat wordt geïmplementeerdIEnumerable<T>, exemplaarmethoden te hebben, zoals GroupBy, AverageOrderBy, enzovoort. U kunt deze aanvullende methoden zien in voltooiing van de IntelliSense-instructie wanneer u 'punt' typt na een exemplaar van een IEnumerable<T> type, zoals List<T> of Array.

OrderBy-voorbeeld

In het volgende voorbeeld ziet u hoe u de standaardqueryoperatormethode OrderBy aanroept op een matrix met gehele getallen. De expressie tussen haakjes is een lambda-expressie. Veel standaardqueryoperators gebruiken lambda-expressies als parameters, maar dit is geen vereiste voor extensiemethoden. Zie Lambda-expressies voor meer informatie.

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

Extensiemethoden worden gedefinieerd als statische methoden, maar worden aangeroepen met behulp van de syntaxis van de instantiemethode. De eerste parameter geeft aan op welk type de methode werkt. De parameter volgt de wijzigingsfunctie . Extensiemethoden zijn alleen binnen het bereik wanneer u de naamruimte expliciet in uw broncode importeert met een using instructie.

In het volgende voorbeeld ziet u een extensiemethode die is gedefinieerd voor de System.String klasse. Deze is gedefinieerd in een niet-geneste, niet-algemene statische klasse:

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

De WordCount uitbreidingsmethode kan binnen de werkingssfeer van deze using richtlijn worden gebracht:

using ExtensionMethods;

En het kan worden aangeroepen vanuit een toepassing met behulp van deze syntaxis:

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

U roept de extensiemethode in uw code aan met de syntaxis van de instantiemethode. De tussenliggende taal (IL) die door de compiler wordt gegenereerd, vertaalt uw code in een aanroep van de statische methode. Het principe van inkapseling wordt niet echt geschonden. Extensiemethoden hebben geen toegang tot privévariabelen in het type dat ze uitbreiden.

Zowel de MyExtensions klasse als de WordCount methode zijn staticen kunnen worden geopend als alle andere static leden. De WordCount methode kan als volgt worden aangeroepen, net als andere static methoden:

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

De voorgaande C#-code:

  • Declareert en wijst een nieuwe string naam s toe met een waarde van "Hello Extension Methods".
  • Aanroepen MyExtensions.WordCount gegeven argument s.

Zie Een aangepaste extensiemethode implementeren en aanroepen voor meer informatie.

Over het algemeen zult u waarschijnlijk veel vaker extensiemethoden aanroepen dan het implementeren van uw eigen methoden. Omdat extensiemethoden worden aangeroepen met behulp van de syntaxis van de instantiemethode, is er geen speciale kennis vereist om deze te gebruiken vanuit clientcode. Als u uitbreidingsmethoden voor een bepaald type wilt inschakelen, voegt u een using instructie toe voor de naamruimte waarin de methoden worden gedefinieerd. Als u bijvoorbeeld de standaardqueryoperators wilt gebruiken, voegt u deze using instructie toe aan uw code:

using System.Linq;

(Mogelijk moet u ook een verwijzing toevoegen naar System.Core.dll.) U ziet dat de standaardqueryoperators nu in IntelliSense worden weergegeven als aanvullende methoden die beschikbaar zijn voor de meeste IEnumerable<T> typen.

Binding Extension Methods at Compile Time

U kunt extensiemethoden gebruiken om een klasse of interface uit te breiden, maar niet om ze te overschrijven. Een extensiemethode met dezelfde naam en handtekening als een interface of klassemethode wordt nooit aangeroepen. Tijdens het compileren hebben extensiemethoden altijd een lagere prioriteit dan instantiemethoden die zijn gedefinieerd in het type zelf. Met andere woorden, als een type een methode heeft met de naam Process(int i)en u een extensiemethode met dezelfde handtekening hebt, wordt de compiler altijd verbonden met de instantiemethode. Wanneer de compiler een methodeaanroep tegenkomt, zoekt deze eerst naar een overeenkomst in de instantiemethoden van het type. Als er geen overeenkomst wordt gevonden, wordt gezocht naar extensiemethoden die zijn gedefinieerd voor het type en wordt er een binding gevonden met de eerste extensiemethode die wordt gevonden.

Opmerking

In het volgende voorbeeld ziet u de regels die de C#-compiler volgt bij het bepalen of een methodeaanroep moet worden gekoppeld aan een instantiemethode voor het type of aan een extensiemethode. De statische klasse Extensions bevat extensiemethoden die zijn gedefinieerd voor elk type dat wordt geïmplementeerd IMyInterface. AKlassen, Ben C alle implementeren de interface.

De MethodB extensiemethode wordt nooit aangeroepen omdat de naam en handtekening exact overeenkomen met methoden die al door de klassen zijn geïmplementeerd.

Wanneer de compiler een exemplaarmethode met een overeenkomende handtekening niet kan vinden, wordt deze gekoppeld aan een overeenkomende extensiemethode als deze bestaat.

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

Algemene gebruikspatronen

Verzamelingsfunctionaliteit

In het verleden was het gebruikelijk om 'Verzamelingsklassen' te maken waarmee de System.Collections.Generic.IEnumerable<T> interface voor een bepaald type en ingesloten functionaliteit werd geïmplementeerd die op verzamelingen van dat type heeft gereageerd. Hoewel er niets mis is met het maken van dit type verzamelingsobject, kan dezelfde functionaliteit worden bereikt met behulp van een extensie op de System.Collections.Generic.IEnumerable<T>. Extensies hebben het voordeel dat de functionaliteit kan worden aangeroepen vanuit elke verzameling, zoals een System.Array of System.Collections.Generic.List<T> die op dat type wordt geïmplementeerd System.Collections.Generic.IEnumerable<T> . Een voorbeeld hiervan met behulp van een matrix van Int32 vindt u eerder in dit artikel.

Laagspecifieke functionaliteit

Wanneer u een Onion-architectuur of een ander gelaagd toepassingsontwerp gebruikt, is het gebruikelijk om een set domeinentiteiten of gegevensoverdrachtobjecten te hebben die kunnen worden gebruikt om over toepassingsgrenzen te communiceren. Deze objecten bevatten over het algemeen geen functionaliteit of alleen minimale functionaliteit die van toepassing is op alle lagen van de toepassing. Extensiemethoden kunnen worden gebruikt om functionaliteit toe te voegen die specifiek is voor elke toepassingslaag zonder het object omlaag te laden met methoden die niet nodig of gewenst zijn in andere lagen.

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

Vooraf gedefinieerde typen uitbreiden

In plaats van nieuwe objecten te maken wanneer herbruikbare functionaliteit moet worden gemaakt, kunnen we vaak een bestaand type uitbreiden, zoals een .NET- of CLR-type. Als we bijvoorbeeld geen extensiemethoden gebruiken, kunnen we een Engine of Query klasse maken om een query uit te voeren op een SQL Server die kan worden aangeroepen vanaf meerdere locaties in onze code. We kunnen de System.Data.SqlClient.SqlConnection klasse echter uitbreiden met behulp van extensiemethoden om die query uit te voeren vanaf elke locatie waar we een verbinding hebben met een SQL Server. Andere voorbeelden zijn het toevoegen van algemene functionaliteit aan de System.String klasse, het uitbreiden van de mogelijkheden voor gegevensverwerking van het System.IO.Stream object en System.Exception objecten voor specifieke functionaliteit voor foutafhandeling. Deze soorten gebruiksvoorbeelden worden alleen beperkt door uw verbeelding en goede zin.

Het uitbreiden van vooraf gedefinieerde typen kan lastig zijn met struct typen omdat ze worden doorgegeven aan methoden. Dat betekent dat eventuele wijzigingen in de struct worden aangebracht in een kopie van de struct. Deze wijzigingen zijn niet zichtbaar zodra de extensiemethode wordt afgesloten. U kunt de ref wijzigingsfunctie toevoegen aan het eerste argument, waardoor het een ref extensiemethode is. Het ref trefwoord kan vóór of na het this trefwoord worden weergegeven zonder semantische verschillen. Door de ref wijzigingsfunctie toe te voegen, wordt aangegeven dat het eerste argument wordt doorgegeven door verwijzing. Hiermee kunt u extensiemethoden schrijven waarmee de status van de struct wordt uitgebreid (houd er rekening mee dat privéleden niet toegankelijk zijn). Alleen waardetypen of algemene typen die zijn beperkt tot struct (zie struct beperking voor meer informatie) zijn toegestaan als de eerste parameter van een ref extensiemethode. In het volgende voorbeeld ziet u hoe u een ref extensiemethode gebruikt om een ingebouwd type rechtstreeks te wijzigen zonder dat u het resultaat opnieuw hoeft toe tewijst of doorgeeft aan een functie met het ref trefwoord:

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

In dit volgende voorbeeld ziet u ref extensiemethoden voor door de gebruiker gedefinieerde structtypen:

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

Algemene richtlijnen

Hoewel het nog steeds de voorkeur verdient om functionaliteit toe te voegen door de code van een object te wijzigen of een nieuw type af te leiden wanneer dit redelijk en mogelijk is, zijn extensiemethoden een cruciale optie geworden voor het maken van herbruikbare functionaliteit in het hele .NET-ecosysteem. Voor dergelijke situaties waarin de oorspronkelijke bron niet onder uw beheer valt, wanneer een afgeleid object ongepast of onmogelijk is, of wanneer de functionaliteit niet buiten het toepasselijke bereik mag worden weergegeven, zijn extensiemethoden een uitstekende keuze.

Zie Overname voor meer informatie over afgeleide typen.

Wanneer u een extensiemethode gebruikt om een type uit te breiden waarvan u geen broncode hebt, loopt u het risico dat een wijziging in de implementatie van het type ervoor zorgt dat de extensiemethode wordt verbroken.

Als u extensiemethoden voor een bepaald type implementeert, moet u de volgende punten onthouden:

  • Een extensiemethode wordt niet aangeroepen als deze dezelfde handtekening heeft als een methode die in het type is gedefinieerd.
  • Extensiemethoden worden binnen het bereik gebracht op het niveau van de naamruimte. Als u bijvoorbeeld meerdere statische klassen hebt die extensiemethoden in één naamruimte met de naam Extensionsbevatten, worden ze allemaal binnen het bereik gebracht door de using Extensions; richtlijn.

Voor een klassebibliotheek die u hebt geïmplementeerd, moet u geen extensiemethoden gebruiken om te voorkomen dat het versienummer van een assembly wordt verhoogd. Als u aanzienlijke functionaliteit wilt toevoegen aan een bibliotheek waarvoor u eigenaar bent van de broncode, volgt u de .NET-richtlijnen voor het versiebeheer van assembly's. Zie Assembly-versiebeheer voor meer informatie.

Zie ook