Eine Erweiterungsmethode fügt einem bestehenden Typ eine Methode hinzu, ohne ihn zu ändern, von ihm zu erben oder ihn zu kapseln. Du schreibst eine gewöhnliche statische Methode, markierst ihren ersten Parameter mit this, und Aufrufer verwenden sie mit Punktsyntax, als hätte der Typ sie schon immer gehabt:
Ausgabe:
Extension methods let yo...
12
short
Extension...
title.Truncate(24) wird genau zu StringExtensions.Truncate(title, 24) kompiliert. Die Punktsyntax ist nur eine Bequemlichkeit: Die Methode ist weiterhin statisch, steht weiterhin in StringExtensions und kann nur die öffentlichen Member von string verwenden.
Die Regeln für die Deklaration
- Die Methode muss
staticsein und in einer Klasse stehen, diestatic, nicht generisch und nicht in eine andere Klasse verschachtelt ist. Eine nicht statische oder generische Klasse ergibt CS1106,Extension method must be defined in a non-generic static class; eine verschachtelte ergibt CS1109,Extension methods must be defined in a top level static class. thissteht nur am ersten Parameter, und dessen Typ ist der erweiterte Typ. Weitere Parameter sind normal.- Die Methode selbst darf generisch sein (
this IEnumerable<T> source), auch wenn die Klasse es nicht darf. - Sie sieht nur, was jeder externe Code sieht: öffentliche (und innerhalb derselben Assembly interne) Member. Private Felder bleiben privat.
Die Konvention ist, die Klasse nach dem zu benennen, was sie erweitert (StringExtensions, EnumerableExtensions), und sie in einem Namespace wie MyApp.Extensions zu gruppieren.
Interfaces und IEnumerable<T> erweitern
Wer ein Interface erweitert, fügt die Methode jedem Typ hinzu, der es implementiert. Eine Erweiterung von IEnumerable<T> gibt die Methode daher auf einen Schlag Arrays, Listen, Sets, Schlüsselsammlungen von Dictionaries und LINQ-Ergebnissen:
Ausgabe:
9.5
50
65
f0 f3 f6
EveryNth ist generisch: T wird aus dem Empfänger abgeleitet, es funktioniert also hier für Strings und für jeden anderen Elementtyp. Weil es yield return verwendet, ist es wie die eingebauten LINQ-Operatoren verzögert und lässt sich mit ihnen verketten.
LINQ ist auf Erweiterungsmethoden gebaut
Where, Select, OrderBy, Sum, First und der Rest von LINQ sind Erweiterungsmethoden auf IEnumerable<T>, definiert in der statischen Klasse System.Linq.Enumerable. Deshalb muss using System.Linq; am Anfang einer Datei stehen, bevor list.Where(...) kompiliert, und deshalb funktioniert LINQ mit jedem Collection-Typ, ohne dass einer davon eine Methode Where implementiert:
Ausgabe:
72, 88, 95
72, 88, 95
Die beiden Zeilen sind derselbe Code. Die Erweiterungssyntax macht aus verschachtelten Aufrufen, die sich von innen nach außen lesen, eine Kette, die sich in der Reihenfolge liest, in der die Schritte passieren. Das ist der Hauptgrund, warum es Erweiterungsmethoden gibt: Sie kamen mit C# 3 zusammen mit LINQ. Die LINQ-Seite behandelt die Operatoren.
Der Namespace muss importiert sein
Eine Erweiterungsmethode ist nur verfügbar, wenn der Namespace ihrer statischen Klasse mit using importiert ist. Ohne ihn scheitert der Aufruf, als gäbe es die Methode nicht:
error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)
Füge das using für den Namespace der Erweiterungen hinzu (oder ab C# 10 einmal ein global using für das Projekt):
Ausgabe:
EUR 49.90
Die Kehrseite ist nützlich: Eine Erweiterungsmethode, die du nicht überall haben willst, kann in einem Namespace liegen, den nur manche Dateien importieren.
Instanzmethoden haben Vorrang
Hat ein Typ bereits eine passende Instanzmethode, verwendet der Compiler diese und berücksichtigt die Erweiterung nie. Eine Erweiterung mit derselben Signatur ist an jeder Aufrufstelle mit Punktsyntax toter Code:
Ausgabe:
Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call
Die Erweiterung mit einem Argument hat gegen die Instanzmethode verloren; die mit zwei Argumenten wurde verwendet, weil keine Instanzmethode einen String nimmt. Das bedeutet auch: Ein Bibliotheksupdate, das eine Instanzmethode mit dem Namen deiner Erweiterung hinzufügt, leitet deine Aufrufe stillschweigend auf die Version der Bibliothek um. Wähle Erweiterungsnamen, die wahrscheinlich nicht kollidieren.
Aufruf auf null
Da der Aufruf in Wahrheit ein statischer Aufruf ist, wirft ein null-Empfänger an der Aufrufstelle nicht. Die Methode bekommt null als erstes Argument und entscheidet, was das bedeutet:
Ausgabe:
True
AL
ArgumentNullException: fullName
Dass missing.IsBlank() true zurückgibt, ist bequem, kann aber Leser überraschen, die erwarten, dass ein Member-Aufruf auf null wirft. Akzeptiere null nur in Methoden, deren Name es offensichtlich macht (IsBlank, OrEmpty); wirf anderswo ArgumentNullException, wie es LINQ tut.
Enums erweitern
Enums können keine eigenen Methoden haben, was sie zu einem natürlichen Ziel macht:
enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }
static class OrderStatusExtensions
{
public static bool IsFinal(this OrderStatus s) =>
s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}
// usage: if (order.Status.IsFinal()) { ... }
Erweiterungsmember in C# 14
Bis C# 14 konnten nur Methoden Erweiterungen sein. C# 14 hat extension-Blöcke eingeführt, die Member für einen Empfänger gruppieren und auch Erweiterungs-Properties und statische Member erlauben:
// C# 14 and later
public static class StringExtensions
{
extension(string s)
{
public bool IsBlank => string.IsNullOrWhiteSpace(s); // extension property
public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
}
}
Klassische Erweiterungsmethoden mit this-Parameter bleiben gültig und sind das, was fast jeder bestehende Code und jede Bibliothek verwendet.
Wann du eine schreibst
Erweiterungsmethoden passen, wenn dir der Typ nicht gehört (string, DateTime, Framework-Interfaces), wenn du dieselbe Hilfsmethode an jeder Implementierung eines Interfaces haben willst oder wenn sich eine Kette besser liest als verschachtelte Aufrufe. Wenn dir die Klasse gehört, füge stattdessen eine echte Instanzmethode hinzu: Sie kann privaten Zustand verwenden und steht dort, wo Leser sie suchen. Und vermeide es, object zu erweitern, denn damit landet deine Methode in der Vervollständigungsliste jedes Werts im Programm.
Häufig gestellte Fragen
Was ist eine Erweiterungsmethode in C#?
Eine statische Methode, die sich so aufrufen lässt, als wäre sie eine Instanzmethode eines anderen Typs. Du schreibst sie in eine statische Klasse und setzt this vor ihren ersten Parameter: public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);. Dann funktioniert name.IsBlank() mit jedem String, ohne string zu ändern oder davon zu erben.
Warum wird meine Erweiterungsmethode nicht gefunden?
Erweiterungsmethoden sind nur sichtbar, wenn ihr Namespace importiert ist. Liegt die statische Klasse in MyApp.Extensions, füge using MyApp.Extensions; in der aufrufenden Datei hinzu; sonst meldet der Compiler CS1061 und sagt, der Typ enthalte keine Definition und keine zugängliche Erweiterungsmethode dieses Namens. Prüfe außerdem, dass die Klasse static, nicht generisch und nicht verschachtelt ist.
Kann eine Erweiterungsmethode eine Instanzmethode überschreiben?
Nein. Der Compiler sucht zuerst nach einer passenden Instanzmethode und berücksichtigt Erweiterungsmethoden nur, wenn keine existiert. Eine Erweiterung mit demselben Namen und denselben Parametern wie eine Instanzmethode wird an jeder Aufrufstelle mit Instanzsyntax stillschweigend ignoriert.
Kann man in C# eine Erweiterungsmethode auf null aufrufen?
Ja. Der Aufruf wird zu einem statischen Methodenaufruf mit null als erstem Argument kompiliert, beim Aufruf selbst wird also keine NullReferenceException geworfen. Die Methode entscheidet, was zu tun ist: null als gültige Eingabe behandeln (wie eine Hilfsmethode im Stil von IsNullOrEmpty) oder ArgumentNullException werfen, wie es die Methoden von LINQ tun.
Gibt es in C# Erweiterungs-Properties?
Nicht vor C# 14. Bis dahin können nur Methoden Erweiterungen sein, eine gewünschte Property wird also als Methode geschrieben (GetFullName()). C# 14 hat Erweiterungsblöcke eingeführt, die neben Methoden auch Erweiterungs-Properties und statische Member deklarieren können.