Menu

Metody rozszerzające w C#: składnia, IEnumerable, LINQ i zasady

Jak działają metody rozszerzające w C#: metoda static w klasie static z this przy pierwszym parametrze, wywoływanie jej jak metody instancji, rozszerzanie string, enumów i IEnumerable<T>, jak zbudowano na nich LINQ, dlaczego metody instancji wygrywają, odbiorca null i potrzebna dyrektywa using.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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ć static i znajdować się w klasie, która jest static, 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.
  • this stawia 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ