Metoda rozszerzająca dodaje metodę do istniejącego typu bez modyfikowania go, dziedziczenia po nim czy opakowywania go. Piszesz zwykłą metodę static, oznaczasz jej pierwszy parametr słowem this, a kod wywołujący używa jej przez kropkę, jakby typ miał ją od zawsze:
Wynik:
Extension methods let yo...
12
short
Extension...
title.Truncate(24) kompiluje się dokładnie do StringExtensions.Truncate(title, 24). Składnia z kropką to tylko wygoda: metoda nadal jest static, nadal mieszka w StringExtensions i może używać wyłącznie publicznych składowych typu string.
Zasady deklarowania
- Metoda musi być
statici znajdować się w klasie, która jeststatic, niegeneryczna i niezagnieżdżona w innej klasie. Klasa niestatyczna lub generyczna daje CS1106,Extension method must be defined in a non-generic static class; zagnieżdżona daje CS1109,Extension methods must be defined in a top level static class. thisstawia się tylko przy pierwszym parametrze, a typ tego parametru to typ, który rozszerzasz. Kolejne parametry są zwykłe.- Sama metoda może być generyczna (
this IEnumerable<T> source), choć klasa nie może. - Widzi tylko to, co widzi każdy kod z zewnątrz: składowe publiczne (a w obrębie tego samego assembly także internal). Pola prywatne pozostają prywatne.
Zgodnie z konwencją klasę nazywa się od tego, co rozszerza (StringExtensions, EnumerableExtensions), i grupuje w namespace takim jak MyApp.Extensions.
Rozszerzanie interfejsów i IEnumerable<T>
Rozszerzenie interfejsu dodaje metodę do każdego typu, który go implementuje. Rozszerzenie IEnumerable<T> daje więc metodę naraz tablicom, listom, zbiorom, kolekcjom kluczy słowników i wynikom zapytań LINQ:
Wynik:
9.5
50
65
f0 f3 f6
EveryNth jest generyczna: T jest wnioskowany z odbiorcy, więc działa tu dla stringów i dla każdego innego typu elementów. Ponieważ używa yield return, jest leniwa jak wbudowane operatory LINQ i można ją z nimi łączyć w łańcuch.
LINQ jest zbudowany na metodach rozszerzających
Where, Select, OrderBy, Sum, First i reszta LINQ to metody rozszerzające dla IEnumerable<T>, zdefiniowane w klasie static System.Linq.Enumerable. Dlatego using System.Linq; musi stać na początku pliku, zanim list.Where(...) się skompiluje, i dlatego LINQ działa z każdym typem kolekcji, choć żaden z nich nie implementuje metody Where:
Wynik:
72, 88, 95
72, 88, 95
Obie linie to ten sam kod. Składnia rozszerzeń zamienia zagnieżdżone wywołania, które czyta się od środka, w łańcuch czytany w kolejności wykonywania kroków. To główny powód istnienia metod rozszerzających: dodano je w C# 3 razem z LINQ. Operatory omawia strona o LINQ.
Namespace musi być zaimportowany
Metoda rozszerzająca jest w zasięgu tylko wtedy, gdy namespace jej klasy static zaimportowano przez using. Bez tego wywołanie kończy się błędem, jakby metoda nie istniała:
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?)
Dodaj using dla namespace z rozszerzeniami (albo, od C# 10, jeden global using dla całego projektu):
Wynik:
EUR 49.90
Ma to też dobrą stronę: metoda rozszerzająca, której nie chcesz mieć wszędzie, może żyć w namespace importowanym tylko przez niektóre pliki.
Metody instancji wygrywają
Gdy typ ma już pasującą metodę instancji, kompilator jej używa i w ogóle nie bierze pod uwagę rozszerzenia. Rozszerzenie o tej samej sygnaturze jest martwym kodem w każdym miejscu wywołania ze składnią z kropką:
Wynik:
Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call
Rozszerzenie z jednym argumentem przegrało z metodą instancji; to z dwoma argumentami zostało użyte, bo żadna metoda instancji nie przyjmuje stringa. Oznacza to również, że aktualizacja biblioteki, która doda metodę instancji o nazwie twojego rozszerzenia, po cichu przełączy twoje wywołania na wersję z biblioteki. Wybieraj nazwy rozszerzeń, które raczej nie będą kolidować.
Wywołanie na null
Ponieważ to w rzeczywistości wywołanie static, odbiorca null nie powoduje wyjątku w miejscu wywołania. Metoda dostaje null jako pierwszy argument i sama decyduje, co to znaczy:
Wynik:
True
AL
ArgumentNullException: fullName
To, że missing.IsBlank() zwraca true, jest wygodne, ale może zaskoczyć czytelników, którzy oczekują, że wywołanie składowej na null rzuci wyjątek. Akceptuj null tylko w metodach, których nazwa to jasno sugeruje (IsBlank, OrEmpty); w pozostałych rzucaj ArgumentNullException, tak jak LINQ.
Rozszerzanie enumów
Enumy nie mogą mieć własnych metod, dlatego są naturalnym celem rozszerzeń:
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()) { ... }
Składowe rozszerzające w C# 14
Do C# 14 rozszerzeniami mogły być tylko metody. C# 14 dodał bloki extension, które grupują składowe dla jednego odbiorcy i pozwalają też na właściwości rozszerzające i składowe static:
// 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] + "...";
}
}
Klasyczne metody rozszerzające z parametrem this pozostają poprawne i to ich używa niemal cały istniejący kod i biblioteki.
Kiedy ją napisać
Metody rozszerzające pasują, gdy typ nie należy do ciebie (string, DateTime, interfejsy frameworka), gdy chcesz mieć tego samego pomocnika w każdej implementacji interfejsu albo gdy łańcuch czyta się lepiej niż zagnieżdżone wywołania. Jeśli klasa jest twoja, dodaj zamiast tego prawdziwą metodę instancji: może korzystać z prywatnego stanu i jest tam, gdzie czytelnicy jej szukają. Unikaj też rozszerzania object, bo wtedy twoja metoda pojawia się na liście podpowiedzi dla każdej wartości w programie.
Najczęściej zadawane pytania
Czym jest metoda rozszerzająca w C#?
To metoda static, którą można wywołać tak, jakby była metodą instancji innego typu. Piszesz ją w klasie static i stawiasz this przed pierwszym parametrem: public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);. Wtedy name.IsBlank() działa dla każdego stringa, bez zmieniania typu string i bez dziedziczenia po nim.
Dlaczego kompilator nie widzi mojej metody rozszerzającej?
Metody rozszerzające są widoczne tylko wtedy, gdy zaimportowano ich namespace. Jeśli klasa static jest w MyApp.Extensions, dodaj using MyApp.Extensions; do pliku, który ją wywołuje; w przeciwnym razie kompilator zgłasza CS1061, czyli że typ nie zawiera definicji ani dostępnej metody rozszerzającej o tej nazwie. Sprawdź też, czy klasa jest static, niegeneryczna i nie jest zagnieżdżona.
Czy metoda rozszerzająca może zastąpić metodę instancji?
Nie. Kompilator najpierw szuka pasującej metody instancji, a metody rozszerzające bierze pod uwagę dopiero wtedy, gdy takiej nie ma. Rozszerzenie o tej samej nazwie i tych samych parametrach co metoda instancji jest po cichu ignorowane w każdym miejscu wywołania, które używa składni instancji.
Czy w C# można wywołać metodę rozszerzającą na null?
Tak. Wywołanie kompiluje się do wywołania metody static z null jako pierwszym argumentem, więc samo wywołanie nie rzuca NullReferenceException. O tym, co zrobić, decyduje metoda: potraktować null jako poprawne wejście (jak pomocnik w stylu IsNullOrEmpty) albo rzucić ArgumentNullException, tak jak robią to metody LINQ.
Czy w C# istnieją właściwości rozszerzające?
Nie przed C# 14. Do tego czasu rozszerzeniami mogą być tylko metody, więc to, co byłoby właściwością, zapisuje się jako metodę (GetFullName()). C# 14 dodał bloki rozszerzeń, które oprócz metod mogą deklarować właściwości rozszerzające i składowe static.