Menu

Interfejs w C#: deklaracja, implementacja, metody domyślne i przykłady

Jak działają interfejsy w C#: deklarowanie, implementowanie w klasach i strukturach, używanie interfejsu jako typu, implementowanie kilku naraz, jawna implementacja, interfejsy frameworka, które implementuje się najczęściej (IComparable<T>, IEnumerable<T>, IDisposable), oraz domyślne metody interfejsów z C# 8.

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

Interfejs to kontrakt: nazwana lista składowych, które typ obiecuje dostarczyć. Mówi, co typ potrafi, a nic o tym, jak. Klasy i struktury implementują interfejsy, a kod napisany dla interfejsu działa z każdą implementacją, także z tymi napisanymi później.

Deklarowanie i implementowanie interfejsu

Interfejs deklaruje składowe bez ciał. Zgodnie z konwencją jego nazwa zaczyna się od I. Klasa implementuje go, wymieniając go po dwukropku i dostarczając publiczną składową dla każdej z nich:

Wynik:

15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30

BestPrice nic nie wie o PercentOff ani FixedOff. Zależy tylko od IDiscount, więc dodanie później klasy BuyOneGetOne nie wymaga w nim żadnej zmiany. To rozdzielenie to cały powód istnienia interfejsów.

Kilka zasad dotyczących deklaracji:

  • Składowe interfejsu są domyślnie publiczne i składowe implementujące też muszą być publiczne (chyba że implementuje się je jawnie, o czym niżej).
  • Interfejs może deklarować metody, właściwości, indeksery i zdarzenia. Nie może deklarować pól instancji ani konstruktorów.
  • Klasa, która pomija składową, się nie kompiluje: błąd CS0535, "'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'".
  • Nie da się utworzyć interfejsu przez new. Tworzysz klasę implementującą i możesz ją trzymać w zmiennej typu interfejsu: IDiscount d = new FixedOff(5);.

Interfejs jako typ

Zmienną, parametr, pole albo typ zwracany można zadeklarować jako interfejs. Przyjmuje wtedy każdy obiekt, który go implementuje, i udostępnia tylko składowe interfejsu. Tak pisze się kod, któremu nie zależy na tym, jaką implementację dostanie, i tak testy podmieniają ją na atrapę:

public class Checkout
{
    private readonly IPaymentGateway gateway;          // not StripeGateway, not PayPalGateway
    public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }

    public bool Pay(decimal amount) => gateway.Charge(amount);
}

// Production: new Checkout(new StripeGateway(apiKey))
// Unit test:  new Checkout(new FakeGateway(alwaysSucceeds: true))

Na tym zbudowane są kontenery wstrzykiwania zależności w ASP.NET Core: usługi rejestruje się i pobiera według interfejsu.

Implementowanie kilku interfejsów

Klasa ma najwyżej jedną klasę bazową, ale może implementować dowolną liczbę interfejsów. Najpierw klasa bazowa, potem interfejsy, oddzielone przecinkami:

Wynik:

INVOICE Studio rent March: 950.00
True
True

Interfejsy mogą też dziedziczyć po innych interfejsach: interface IRepository<T> : IReadRepository<T> dodaje składowe do tych odziedziczonych, a klasa implementująca IRepository<T> musi dostarczyć oba zestawy.

Jawna implementacja interfejsu

Czasem dwa interfejsy deklarują składową o tej samej nazwie i różnym znaczeniu albo składowa interfejsu nie ma sensu na publicznej powierzchni klasy. Zaimplementuj ją jawnie, poprzedzając nazwą interfejsu i pomijając modyfikator dostępu:

Wynik:

Desk lamp,34.90
Desk lamp for 34.90

Do jawnie zaimplementowanej składowej można dotrzeć tylko przez interfejs. Framework używa tego, żeby nie zaśmiecać popularnych typów: tablice implementują IList.Add jawnie (rzuca NotSupportedException, bo tablice mają stały rozmiar), więc metoda nie pojawia się, gdy wpisujesz array..

Interfejsy frameworka, które będziesz implementować

Implementacja standardowego interfejsu podłącza twój typ do istniejącego kodu frameworka. Trzy pojawiają się bez przerwy.

IComparable<T> nadaje typowi naturalny porządek, z którego korzystają List<T>.Sort(), Array.Sort i Max(). CompareTo zwraca liczbę ujemną, zero albo liczbę dodatnią:

Wynik:

1.4, 2.9, 2.10, 10.0

Sortowanie tych samych wersji jako stringów dałoby 1.4, 10.0, 2.10, 2.9. Bez IComparable<T> metoda Sort() rzuca InvalidOperationException, bo nie ma jak porównać dwóch obiektów Version.

IEnumerable<T> sprawia, że typu można używać w foreach i z LINQ. Najłatwiej zaimplementować GetEnumerator przez yield return, co omawia strona o IEnumerable i yield.

IDisposable oznacza typ, który trzyma coś, co trzeba zwolnić (uchwyt pliku, połączenie, timer). Jego jedyną metodę, Dispose, wywołuje instrukcja using na końcu bloku, nawet gdy zostanie rzucony wyjątek:

Wynik:

open sales.txt
  write to sales.txt: March total: 12400
close sales.txt
after using

Domyślne metody interfejsów (C# 8)

Dodanie składowej do opublikowanego interfejsu kiedyś psuło każdą implementującą go klasę. Od C# 8 składowa interfejsu może mieć ciało, które klasy implementujące dziedziczą, o ile nie dostarczą własnego:

public interface ILogger
{
    void Write(string message);

    // New in version 2 of the library. Existing implementers keep compiling.
    void Error(string message) => Write("ERROR: " + message);
}

public class ConsoleLogger : ILogger
{
    public void Write(string message) => Console.WriteLine(message);
}

ILogger log = new ConsoleLogger();
log.Error("disk full");            // ERROR: disk full

var direct = new ConsoleLogger();
// direct.Error("x");              // does not compile: the default method belongs to the interface

Ostatnia linia to część, która zaskakuje: metoda domyślna nie staje się składową samej klasy, więc można ją wywołać tylko przez typ interfejsu. Metody domyślne nie mogą też używać pól instancji (interfejsy ich nie mają); działają przez pozostałe składowe interfejsu. Wymagają .NET Core 3.0 lub nowszego i nie są dostępne w .NET Framework.

C# 8 pozwolił też na składowe statyczne w interfejsach, a C# 11 dodał składowe static abstract, dzięki którym kod generyczny może wywołać metodę statyczną lub operator na T. Na tym zbudowana jest generyczna matematyka w .NET 7 (INumber<T>, where T : INumber<T>).

Interfejs czy klasa abstrakcyjna?

Używaj interfejsu dla możliwości, którą mogą dzielić niepowiązane typy, gdy mają w tym uczestniczyć struktury albo gdy klasa potrzebuje kilku takich ról. Używaj klasy abstrakcyjnej, gdy implementacje są wariantami jednej rzeczy i dzielą stan lub ustalony algorytm. Strona o klasach abstrakcyjnych ma tabelę porównawczą. Wiele projektów używa obu: interfejsu dla konsumentów i abstrakcyjnej klasy bazowej, która implementuje nudne części za implementujących.

Typowe błędy

  • Brakująca lub niepubliczna implementacja. Każda składowa interfejsu potrzebuje publicznej składowej o pasującej sygnaturze albo jawnej implementacji. Inaczej CS0535 (albo CS0737, gdy metoda istnieje, ale nie jest publiczna).
  • Interfejsy z jedną implementacją "na wszelki wypadek". Interfejs zasługuje na swoje miejsce, gdy jest kilka implementacji albo atrapa w testach. W przeciwnym razie to kolejny plik do przeglądania.
  • Grube interfejsy. Interfejs z dwudziestoma składowymi zmusza każdą implementację do napisania dwudziestu składowych. Podziel go na mniejsze role (IReader, IWriter), które klasy łączą.
  • Rzutowanie interfejsu z powrotem na klasę. ((StripeGateway)gateway).Refund() niweczy rozdzielenie. Jeśli kod wywołujący potrzebuje Refund, to miejsce tej metody jest w interfejsie.
  • Oczekiwanie, że metodę domyślną interfejsu wywoła się na klasie. Dociera się do niej przez typ interfejsu.

Najczęściej zadawane pytania

Czym jest interfejs w C#?

Interfejs to nazwany zestaw składowych (metod, właściwości, zdarzeń, indekserów), które typ obiecuje dostarczyć, bez stanu instancji. interface IDiscount { decimal Apply(decimal price); } mówi "wszystko, co jest IDiscount, potrafi zastosować się do ceny". Klasy i struktury implementują go po dwukropku, a kod może potem pracować z każdą z nich przez typ interfejsu.

Jak zaimplementować interfejs w C#?

Wymień go po dwukropku w deklaracji klasy (po klasie bazowej, jeśli jest) i dostarcz publiczną składową dla każdej składowej interfejsu: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. Brakująca składowa to błąd CS0535. Visual Studio i Rider potrafią wygenerować szkielety przez szybką poprawkę.

Czy klasa w C# może implementować wiele interfejsów?

Tak, tyle, ile potrzeba, oddzielonych przecinkami: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. To odpowiedź C# na wielodziedziczenie: klasa ma jedną klasę bazową, ale może pełnić wiele ról. Jeśli dwa interfejsy deklarują tę samą składową, jedna publiczna metoda może spełnić oba albo możesz użyć jawnej implementacji, aby każdy dostał własną.

Czym jest jawna implementacja interfejsu w C#?

To implementacja składowej z nazwą interfejsu na początku i bez modyfikatora dostępu: string IExportable.Format() { ... }. Taką składową można wtedy wywołać tylko przez zmienną typu interfejsu, a nie przez klasę. Służy do rozwiązywania konfliktów nazw między dwoma interfejsami i do usuwania rzadko używanych składowych interfejsu z publicznej powierzchni klasy.

Czym są domyślne metody interfejsów w C#?

Od C# 8 składowa interfejsu może mieć ciało: void Error(string m) => Write("ERROR: " + m);. Klasy implementujące dostają ją za darmo i mogą dostarczyć własną wersję. Funkcja istnieje po to, by autorzy bibliotek mogli dodawać składowe do opublikowanego interfejsu bez psucia wszystkich implementacji. Do metody domyślnej dociera się przez typ interfejsu, a nie przez klasę.

Dlaczego nazwy interfejsów w C# zaczynają się od I?

To konwencja nazewnicza .NET (IEnumerable, IDisposable, IComparable<T>): wielka litera I, a po niej nazwa w PascalCase, często przymiotnik zakończony na "-able". Kompilator tego nie wymaga, ale trzymanie się konwencji pozwala od razu rozpoznać typy interfejsów, zwłaszcza w deklaracji klasy, gdzie klasa bazowa i interfejsy dzielą jedną listę.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ