Erweiterungsmethoden (C#-Programmierhandbuch)

Mit Erweiterungsmethoden können Sie vorhandenen Typen Methoden hinzufügen, ohne einen neuen abgeleiteten Typ zu erstellen und ohne den ursprünglichen Typ neu kompilieren oder auf andere Weise bearbeiten zu müssen. Erweiterungsmethoden sind statische Methoden, die Sie jedoch wie Instanzmethoden für den erweiterten Typ aufrufen können. Für in C#, F# und Visual Basic geschriebenen Clientcode gibt es keinen sichtbaren Unterschied zwischen dem Aufrufen einer Erweiterungsmethode und den in einem Typ definierten Methoden.

Die häufigsten Erweiterungsmethoden sind die LINQ-Standardabfrageoperatoren, die vorhandenen System.Collections.IEnumerable- und System.Collections.Generic.IEnumerable<T>-Typen Funktionalität hinzufügen. Um die Standardabfrageoperatoren zu verwenden, müssen Sie sie zuerst mit einer using System.Linq-Direktive einbinden. Jeder Typ, der IEnumerable<T> implementiert, scheint Instanzmethoden zu haben, wie z. B. GroupBy, OrderBy, Average. Sie können diese zusätzlichen Methoden in der IntelliSense-Anweisungsvervollständigung sehen, wenn Sie nach einer Instanz eines IEnumerable<T>-Typs, z.B. List<T> oder Array, "dot" eingeben.

OrderBy-Beispiel

Das folgende Beispiel zeigt, wie Sie die Standardabfrageoperator-Methode OrderBy für ein Ganzzahlarray aufrufen können. Der Ausdruck in Klammern ist ein Lambda-Ausdruck. Viele Standardabfrageoperatoren verwenden Lambda-Ausdrücke als Parameter, dies ist jedoch keine Voraussetzung für Erweiterungsmethoden. Weitere Informationen finden Sie unter Lambdaausdrücke.

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

Erweiterungsmethoden werden als statische Methoden definiert, jedoch mithilfe einer Instanzmethodensyntax aufgerufen. Ihr erster Parameter gibt an, für welchen Typ die Methode aufgerufen wird. Der Parameter folgt dem this-Modifizierer. Erweiterungsmethoden befinden sich nur dann im Bereich, wenn Sie den Namespace explizit mit einer using-Direktive in Ihren Quellcode importieren.

Im folgenden Beispiel wird eine für die System.String-Klasse definierte Erweiterungsmethode veranschaulicht. Die Definition erfolgt in einer nicht geschachtelten, nicht generischen statischen Klasse:

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

Die WordCount-Erweiterungsmethode kann mit dieser using-Direktive eingebunden werden:

using ExtensionMethods;

Sie kann darüber hinaus mit dieser Syntax von einer Anwendung aufgerufen werden:

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

Sie rufen die Erweiterungsmethode im Code mit Instanzmethodensyntax auf. Die vom Compiler erstellte Intermediate Language (IL) übersetzt Ihren Code in einen Aufruf der statischen Methode. Es wird nicht wirklich gegen das Prinzip der Kapselung verstoßen. Erweiterungsmethoden können nicht auf private Variablen in dem Typ zugreifen, den sie erweitern.

Sowohl die MyExtensions-Klasse als auch die WordCount-Methode sind static, und auf sie kann wie auf alle anderen static-Member zugegriffen werden. Die WordCount-Methode kann wie andere static-Methoden wie folgt aufgerufen werden:

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

Für den C#-Code oben gilt:

  • Er deklariert ein neues string-Element namens s mit dem Wert "Hello Extension Methods" und weist es zu.
  • Das angegebene Argument s für MyExtensions.WordCount wird aufgerufen.

Weitere Informationen finden Sie unter Vorgehensweise: Implementieren und Aufrufen einer benutzerdefinierten Erweiterungsmethode.

Im Allgemeinen rufen Sie vermutlich sehr viel häufiger Erweiterungsmethoden auf, als eigene Methoden zu implementieren. Da Erweiterungsmethoden mit der Instanzmethodensyntax aufgerufen werden, sind für ihren Einsatz aus dem Clientcode keine besonderen Kenntnisse erforderlich. Um Erweiterungsmethoden für einen bestimmten Typ zu aktivieren, fügen Sie eine using-Direktive für den Namespace, in dem die Methoden definiert werden, hinzu. Um beispielsweise die Standardabfrageoperatoren zu verwenden, fügen Sie diese using-Direktive dem Code hinzu:

using System.Linq;

(Möglicherweise müssen Sie auch einen Verweis auf System.Core.dll hinzufügen.) Wie Sie sehen, werden die Standardabfrageoperatoren in IntelliSense jetzt für die meisten IEnumerable<T>-Typen als zusätzliche Methoden angezeigt.

Binden von Erweiterungsmethoden während der Kompilierung

Sie können Erweiterungsmethoden verwenden, um eine Klasse oder eine Schnittstelle zu erweitern, jedoch nicht, um sie zu überschreiben. Eine Erweiterungsmethode mit dem gleichen Namen und der gleichen Signatur wie eine Schnittstellen- oder Klassenmethode wird nie aufgerufen. Bei der Kompilierung verfügen Erweiterungsmethoden immer über niedrigere Priorität als im Typ selbst definierte Instanzmethoden. Das heißt, wenn ein Typ eine Methode mit dem Namen Process(int i) hat und Sie über eine Erweiterungsmethode mit der gleichen Signatur verfügen, stellt der Compiler immer eine Bindung mit der Instanzmethode her. Wenn der Compiler einen Methodenaufruf erkennt, sucht er zuerst nach einer Entsprechung in den Instanzmethoden des Typs. Wenn keine Übereinstimmung gefunden wird, wird nach Erweiterungsmethoden gesucht, die für den Typ definiert wurden, und eine Bindung mit der ersten gefundenen Erweiterungsmethode hergestellt.

Beispiel

Das folgende Beispiel veranschaulicht die Regeln, denen der C#-Compiler folgt, um zu bestimmen, ob ein Methodenaufruf an eine Instanzmethode für den Typ oder an eine Erweiterungsmethode gebunden werden soll. Die statische Klasse Extensions enthält Erweiterungsmethoden, die für jeden Typ definiert wurden, der IMyInterface implementiert. Die Klassen A, B und C implementieren alle die Schnittstelle.

Die MethodB-Erweiterungsmethode wird nie aufgerufen, da ihr Name und die Signatur genau mit Methoden übereinstimmen, die bereits von den Klassen implementiert wurden.

Wenn der Compiler keine Instanzmethode mit einer entsprechenden Signatur findet, stellt er eine Bindung mit einer entsprechenden Erweiterungsmethode her (sofern vorhanden).

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

Gängige Verwendungsmuster

Auflistungsfunktionen

In der Vergangenheit war es üblich, „Sammlungsklassen“ zu erstellen, mit denen die System.Collections.Generic.IEnumerable<T>-Schnittstelle für einen bestimmten Typ implementiert und Funktionalität für Sammlungen dieses Typs bereitgestellt wurde. Es ist zwar nichts Falsches daran, diese Art von Sammlungsobjekten zu erstellen, aber die gleiche Funktionalität kann durch eine Erweiterung von System.Collections.Generic.IEnumerable<T> erreicht werden. Erweiterungen haben den Vorteil, dass die Funktionalität aus einer beliebigen Sammlung – z. B. System.Array oder System.Collections.Generic.List<T> – aufgerufen werden kann, die System.Collections.Generic.IEnumerable<T> für diesen Typ implementiert. Ein entsprechendes Beispiel mit Verwendung eines Int32-Arrays finden Sie weiter oben in diesem Artikel.

Ebenenspezifische Funktionalität

Bei Verwendung einer Zwiebelarchitektur oder eines anderen geschichteten Anwendungsdesigns ist es üblich, einen Satz mit Domänenentitäten oder Datenübertragungsobjekten für die Kommunikation über Anwendungsgrenzen hinweg zu verwenden. Diese Objekte umfassen in der Regel keine oder nur minimale Funktionalität, die für alle Ebenen der Anwendung gilt. Erweiterungsmethoden können verwendet werden, um Funktionalität hinzuzufügen, die für jede Anwendungsschicht spezifisch ist, ohne das Objekt mit Methoden zu überladen, die in anderen Ebenen nicht benötigt werden oder unerwünscht sind.

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

Erweiterung von vordefinierten Typen

Wenn wiederverwendbare Funktionalität erstellt werden muss, kann statt der Erstellung neuer Objekte häufig ein bereits vorhandener Typ erweitert werden, z. B. ein .NET- oder CLR-Typ. Wenn beispielsweise keine Erweiterungsmethoden verwendet werden, könnten wir eine Engine- oder Query-Klasse zum Ausführen einer Abfrage auf einer SQL Server-Instanz erstellen, die über mehrere Stellen in unserem Code aufgerufen werden kann. Stattdessen können wir jedoch die Klasse System.Data.SqlClient.SqlConnection mithilfe von Erweiterungsmethoden erweitern, damit diese Abfrage von jedem beliebigen Ort ausgeführt werden kann, an dem eine Verbindung mit einer SQL Server-Instanz vorhanden ist. Weitere Beispiele wären das Hinzufügen gemeinsamer Funktionalität zur Klasse System.String, eine Erweiterung der Datenverarbeitungsfunktionalität des System.IO.Stream-Objekts sowie des System.Exception-Objekts für eine spezifische Fehlerbehandlungsfunktionalität. Der Verwendung dieser Anwendungsfälle sind praktisch keine Grenzen gesetzt.

Das Erweitern vordefinierter Typen kann bei struct-Typen schwierig sein, da sie als Wert an Methoden übergeben werden. Dies bedeutet, dass alle Änderungen an der Struktur an einer Kopie der Struktur vorgenommen werden. Diese Änderungen sind nicht sichtbar, nachdem die Erweiterungsmethode beendet wurde. Sie können dem ersten Argument den ref-Modifizierer hinzufügen und es so zu einer ref-Erweiterungsmethode machen. Das Schlüsselwort ref kann vor oder nach dem Schlüsselwort this ohne semantische Unterschiede angezeigt werden. Durch das Hinzufügen des ref-Modifizierers wird angegeben, dass das erste Argument als Verweis übergeben wird. Auf diese Weise können Sie Erweiterungsmethoden schreiben, die den Status der erweiterten Struktur ändern. (Beachten Sie, dass nicht auf private Member zugegriffen werden kann.) Nur Werttypen oder generische Typen, die auf die Struktur beschränkt sind (weitere Informationen siehe struct-Einschränkung), sind als erster Parameter einer ref-Erweiterungsmethode zulässig. Das folgende Beispiel zeigt, wie Sie eine ref-Erweiterungsmethode verwenden, um einen integrierten Typ direkt zu ändern, ohne das Ergebnis neu zuweisen oder es über eine Funktion mit dem Schlüsselwort ref übergeben zu müssen:

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

Im nächsten Beispiel werden ref-Erweiterungsmethoden für benutzerdefinierte Strukturtypen veranschaulicht:

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

Allgemeine Richtlinien

Wenngleich es weiterhin vorzuziehen ist, Funktionalität durch Änderung des Codes eines Objekts oder durch Ableitung eines neuen Typs hinzuzufügen – sofern dies sinnvoll und möglich ist –, sind Erweiterungsmethoden zu einer wichtigen Option zum Bereitstellen wiederverwendbarer Funktionalität im gesamten .NET-Ökosystem geworden. In Fällen, in denen die ursprüngliche Quelle nicht Ihrer Kontrolle unterliegt, ein abgeleitetes Objekt ungeeignet ist oder nicht verwendet werden kann oder die Funktionalität nicht über den anwendbaren Bereich hinaus offengelegt werden soll, stellen Erweiterungsmethoden eine ausgezeichnete Wahl dar.

Weitere Informationen zu abgeleiteten Typen finden Sie unter Vererbung.

Wenn Sie eine Erweiterungsmethode zum Erweitern eines Typs verwenden, dessen Quellcode Sie nicht ändern können, laufen Sie Gefahr, dass eine Änderung an der Implementierung des Typs zu einer Beschädigung Ihrer Erweiterungsmethode führt.

Wenn Sie Erweiterungsmethoden für einen gegebenen Typ implementieren, beachten Sie die folgenden Punkte:

  • Eine Erweiterungsmethode wird nicht aufgerufen, wenn sie die gleiche Signatur wie eine im Typ definierte Methode hat.
  • Erweiterungsmethoden werden auf Namespace-Ebene eingebunden. Wenn Sie z. B. über mehrere statische Klassen verfügen, die Erweiterungsmethoden in einem einzelnen Namespace mit dem Namen Extensions enthalten, werden sie alle mit der using Extensions;-Direktive eingebunden.

Für eine Klassenbibliothek, die Sie implementiert haben, sollten Sie keine Erweiterungsmethoden verwenden, um zu vermeiden, dass die Versionsnummer einer Assembly erhöht wird. Wenn Sie zu einer Bibliothek, deren Quellcode Sie besitzen, wichtige Funktionalität hinzufügen möchten, befolgen Sie die .NET-Richtlinien für Assemblyversionierung. Weitere Informationen dazu finden Sie unter Assemblyversionen.

Weitere Informationen