Delen via


Extensieleden (C#-programmeerhandleiding)

Met extensieleden kunt u methoden toevoegen aan bestaande typen zonder een nieuw afgeleid type te maken, opnieuw te compileren of anderszins het oorspronkelijke type te wijzigen.

Vanaf C# 14 zijn er twee syntaxis die u gebruikt om extensiemethoden te definiëren. C# 14 voegt containers toe extension , waarbij u meerdere extensieleden definieert voor een type of een exemplaar van een type. Voor C# 14 voegt u de this wijzigingsfunctie toe aan de eerste parameter van een statische methode om aan te geven dat de methode wordt weergegeven als lid van een exemplaar van het parametertype.

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. Beide vormen van uitbreidingsmethoden worden gecompileerd naar dezelfde IL (Tussentaal). Consumenten van extensieleden hoeven niet te weten welke syntaxis is gebruikt om extensiemethoden te definiëren.

De meest voorkomende extensieleden zijn de linQ-standaardqueryoperators 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, OrderByAverage, enzovoort. U kunt deze extra 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. Zie Lambda Expressionsvoor meer informatie.

int[] numbers = [10, 45, 15, 39, 21, 26];
IOrderedEnumerable<int> result = numbers.OrderBy(g => g);
foreach (int i in result)
{
    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.

Extensieleden declareren

Vanaf C# 14 kunt u extensieblokken declareren. Een extensieblok is een blok in een niet-geneste, niet-generische, statische klasse die extensieleden voor een type of een exemplaar van dat type bevat. In het volgende codevoorbeeld wordt een extensieblok voor het string type gedefinieerd. Het extensieblok bevat één lid: een methode waarmee de woorden in de tekenreeks worden geteld:

namespace CustomExtensionMembers;

public static class MyExtensions
{
    extension(string str)
    {
        public int WordCount() =>
            str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

Voor C# 14 declareert u een extensiemethode door de this modifier toe te voegen aan de eerste parameter:

namespace CustomExtensionMethods;

public static class MyExtensions
{
    public static int WordCount(this string str) =>
        str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}

Beide vormen van extensies moeten worden gedefinieerd in een niet-geneste, niet-generische statische klasse.

En deze kan vanuit een toepassing worden aangeroepen met behulp van de syntaxis voor toegang tot exemplaarleden:

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

Hoewel extensieleden nieuwe mogelijkheden toevoegen aan een bestaand type, schenden extensieleden het principe van inkapseling niet. De toegangsdeclaraties voor alle leden van het uitgebreide type zijn van toepassing op extensieleden.

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 is van toepassing op zowel het extensieblok als this de syntaxis voor extensieleden. De voorgaande 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 roept u extensieleden waarschijnlijk veel vaker aan dan u ze implementeert. Omdat extensieleden worden aangeroepen alsof ze worden gedeclareerd als leden van de uitgebreide klasse, is er geen speciale kennis vereist om ze te gebruiken vanuit clientcode. Als u extensieleden voor een bepaald type wilt inschakelen, voegt u gewoon 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;

Bindingsuitbreidingsleden tijdens het compileren

U kunt extensieleden gebruiken om een klasse of interface uit te breiden, maar niet om gedrag te overschrijven dat is gedefinieerd in een klasse. Een extensielid met dezelfde naam en handtekening als een interface of klasseleden wordt nooit aangeroepen. Tijdens het compileren hebben extensieleden altijd een lagere prioriteit dan instantieleden (of statische) leden 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 gebonden aan de lidmethode. Wanneer de compiler een aanroep van een lid tegenkomt, wordt eerst gezocht naar een overeenkomst in de leden van het type. Als er geen overeenkomst wordt gevonden, wordt gezocht naar extensieleden die zijn gedefinieerd voor het type. Het bindt aan het eerste extensielid dat wordt gevonden. In het volgende voorbeeld ziet u de regels die de C#-compiler volgt om te bepalen of de C#-compiler moet worden gekoppeld aan een exemplaarlid van het type of aan een uitbreidingslid. De statische klasse Extensions bevat extensieleden die zijn gedefinieerd voor elk type dat wordt geïmplementeerd IMyInterface:

public interface IMyInterface
{
    void MethodB();
}

// Define extension methods for IMyInterface.

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

De equivalente extensies kunnen worden gedeclareerd met behulp van de C# 14-extensielidsyntaxis:

public static class Extension
{
    extension(IMyInterface myInterface)
    {
        public void MethodA(int i) =>
            Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

        public void MethodA(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 void MethodB() =>
            Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
    }
}

AKlassen en BC alle implementeren de interface:

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
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)");
    }
}

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.

// 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 System.Collections.Generic.IEnumerable<T> op dat type implementeert. Een voorbeeld hiervan met behulp van een matrix van Int32 vindt u eerder in dit artikel.

Layer-Specific 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.

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

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

U kunt een equivalente FullName eigenschap in C# 14 en hoger declareren met behulp van de syntaxis van het nieuwe extensieblok:

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

Vooraf gedefinieerde typen uitbreiden

In plaats van nieuwe objecten te maken wanneer herbruikbare functionaliteit moet worden gemaakt, kunt u vaak een bestaand type uitbreiden, zoals een .NET- of CLR-type. Als u bijvoorbeeld geen extensiemethoden gebruikt, kunt u een Engine of Query klasse maken om een query uit te voeren op een SQL Server die mogelijk wordt aangeroepen vanaf meerdere locaties in onze code. U kunt de System.Data.SqlClient.SqlConnection klasse echter uitbreiden met behulp van extensiemethoden om die query uit te voeren vanaf elke locatie waar u verbinding hebt 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 als waarden aan methoden worden doorgegeven. 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. Met deze techniek kunt u extensiemethoden schrijven die de status van de struct wijzigen die 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 over deze regels) zijn toegestaan als de eerste parameter van een ref extensiemethode of als ontvanger van een extensieblok. 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++;
}

De equivalente extensieblokken worden weergegeven in de volgende code:

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

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

Er zijn verschillende extensieblokken vereist om de parametermodi voor de ontvanger te onderscheiden van waarde- en verw-parametermodi.

In het volgende voorbeeld ziet u het verschil dat van toepassing is op ref de ontvanger:

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

U kunt dezelfde techniek toepassen door extensieleden toe te voegen aan ref 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
    }
}

Het voorgaande voorbeeld kan ook worden gemaakt met behulp van extensieblokken in C# 14:

public static class AccountExtensions
{
    extension(ref Account account)
    {
        // ref keyword can also appear before the this keyword
        public void Deposit(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
        }
    }
}

U kunt deze uitbreidingsmethoden als volgt openen:

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

Het verdient de voorkeur 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. Extensiemethoden zijn een cruciale optie voor het maken van herbruikbare functionaliteit in het hele .NET-ecosysteem. Extensieleden hebben de voorkeur wanneer de oorspronkelijke bron niet onder uw beheer valt, wanneer een afgeleid object ongepast of onmogelijk is, of wanneer de functionaliteit een beperkt bereik heeft.

Zie Overname voor meer informatie over afgeleide typen.

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 bevatten in één naamruimte met de naam Extensions, 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