1. Krótko o klasycznych różnicach
Jeśli ktoś mówi: „Interfejs to po prostu zbiór sygnatur”, zapytaj: „A na jakiej wersji C# piszesz?” Od C# 8 i nowszych interfejsy stały się dużo potężniejsze. Czas porównać je z klasami abstrakcyjnymi — nie tylko pod kątem klasycznych właściwości, ale też wszystkich nowych bajerów platformy .NET.
Jeśli cofnąć się kilka lat — do C# 7 — wszystko było proste. Klasa abstrakcyjna może definiować pola i częściowo zaimplementowane metody, interfejs — tylko sygnatury (metod, właściwości, zdarzeń, indeksatorów).
Dziedziczenie klasy abstrakcyjnej to relacja "jest" (Is-a), interfejsy realizują wielokrotne dziedziczenie zachowania ("potrafi", can-do).
| Charakterystyka | Klasa abstrakcyjna | Interfejs (do C# 8) |
|---|---|---|
| Relacja | is-a | can-do |
| Dziedziczenie | Tylko jedno | Wielokrotne |
| Pola | Może zawierać | Nie może |
| Implementacja metod | Może | Nie może |
| Konstruktory | Może | Nie może |
| Modyfikatory dostępu | Różne (public, protected, ...) | Tylko domyślnie public |
Jak widać, kiedyś klasy abstrakcyjne były prawdziwymi "starszymi braćmi" interfejsów — mocniejsze i bardziej elastyczne. Ale wszystko się zmienia!
2. Interfejsy z domyślną implementacją
Od C# 8 (a tym bardziej w C# 14 i .NET 9) interfejsy dostały nową supermoc — metody z domyślną implementacją, nazywane "Default Interface Methods" (DIM).
Jak to wygląda?
public interface IAnimal
{
void SayHello();
// Metoda z domyślną implementacją!
void Walk()
{
Console.WriteLine("Idę...");
}
}
Wow! Nagle okazało się, że interfejs może zawierać implementację metody. I to nie jedną, a ile dusza zapragnie. Ale jest haczyk: takie metody muszą być jawnie zadeklarowane z ciałem, a cała reszta (pola, prywatne metody, konstruktory) — nadal nie można.
3. Nowoczesne możliwości interfejsów
Nowe bajery, o których powinien wiedzieć każdy .NET-deweloper:
- Metody z domyślną implementacją.
- Prywatne metody w interfejsie (tylko do pomocniczych celów, dostępne tylko dla innych metod tego samego interfejsu).
- Metody statyczne (od C# 8).
- Właściwości z domyślną implementacją.
- Pola statyczne (od C# 14 — "static interface members").
- Abstrakcyjne statyczne członki ("abstract static members" — tak, teraz interfejs może wymagać od implementacji określonych metod statycznych!).
Przykład pełnego nowoczesnego interfejsu:
public interface ILogger
{
static int LoggerCount { get; set; } // C# 14
void Log(string message); // Sygnatura (kontrakt)
// Domyślna implementacja
void LogWarning(string warning)
{
Log("[WARNING]: " + warning);
}
// Prywatna metoda pomocnicza w interfejsie (C# 8+)
private void FormatAndLog(string level, string msg)
{
Log($"{level}: {msg}");
}
// Metoda statyczna w interfejsie (C# 8+)
static void PrintLoggerInfo()
{
Console.WriteLine("Interfejs ILogger — twój najlepszy pomocnik!");
}
}
Wyobraź sobie — kiedyś to było po prostu niemożliwe, to jakby ktoś pozwolił kotu pracować jako ochroniarz serwera.
4. Klasy abstrakcyjne: co nowego?
Klasy abstrakcyjne... jakby to powiedzieć... nie bardzo ewoluowały przez ostatnią dekadę. Nadal mogą zawierać:
- Pola (w tym prywatne, chronione i statyczne).
- Metody zaimplementowane i abstrakcyjne.
- Konstruktory (tak, można tworzyć klasy abstrakcyjne z logiką inicjalizacji).
- Właściwości, zdarzenia, indeksatory.
- Członkowie statyczni i instancyjni.
Przykład klasy abstrakcyjnej:
public abstract class Animal
{
public string Name { get; set; }
public abstract void Speak();
public virtual void Walk()
{
Console.WriteLine($"{Name} idzie na łapkach!");
}
protected void Eat()
{
Console.WriteLine($"{Name} je karmę.");
}
}
Klasa abstrakcyjna nadal jest świetnym miejscem na wspólną logikę, stan i zachowanie dla hierarchii klas.
5. Nowoczesne porównanie: tabela z nowymi możliwościami
| Charakterystyka | Klasa abstrakcyjna | Interfejs (C# 14+, .NET 9) |
|---|---|---|
| Relacja | is-a (jest) | can-do (potrafi) |
| Dziedziczenie | Tylko jedno | Wielokrotne |
| Pola | Tak, dowolne | Tylko statyczne* (C# 14+) |
| Konstruktory | Tak | Nie |
| Implementacja metod | Tak (virtual/abstract) | Tak (default, static, abstract static) |
| Właściwości z implementacją | Tak | Tak (default implementation) |
| Prywatni członkowie | Tak | Tak (tylko metody, C# 8+) |
| Członkowie statyczni | Tak | Tak (C# 8+, z ograniczeniami) |
| Pola statyczne | Tak | Tak * (C# 14+) |
| Modyfikatory dostępu | Dowolne | Domyślnie public lub private |
* — W interfejsach pola statyczne używane są raczej w wyjątkowych przypadkach i to bardzo świeża funkcja języka.
6. Gdzie użyć interfejsu, a gdzie klasy abstrakcyjnej: nowoczesne rekomendacje
Interfejsy (a teraz także z domyślną implementacją) — narzędzie do tworzenia kontraktów między komponentami. Ich kluczowa cecha: wielokrotność. Twoja klasa może implementować nawet dziesięć różnych interfejsów, co czyni ją uniwersalnym żołnierzem.
Klasa abstrakcyjna to nadal twój wybór, jeśli:
- Potrzebny jest wspólny stan (pola), logika i zachowanie, które dziedziczą inne klasy.
- Potrzebna jest standardowa, ale nadpisywalna logika (użyj virtual).
- Chcesz scentralizować inicjalizację przez konstruktor.
W prawdziwych projektach często spotyka się taki schemat: "czyste kontrakty" formułuje się w interfejsach, a jeśli potrzebny jest wspólny kod lub infrastruktura dla podklas — tworzy się abstrakcyjną klasę bazową.
. ┌────────────────────────┐
│ Interfejs │
│ (kontrakt: co potrafi)│
└─────────┬──────────────┘
│
┌──────────────┼──────────────┐
│ │ │
Implementacja 1 Implementacja 2 ... Implementacja N
MyLogger CloudLogger FileLogger
(można łączyć z dziedziczeniem klasy abstrakcyjnej)
7. Scenariusze — kiedy co wygrywa
Wielokrotna implementacja:
Załóżmy, że masz interfejs IDrivable i klasę abstrakcyjną Vehicle. Teraz klasa Car może dziedziczyć bazę — Vehicle — i jednocześnie implementować kilka interfejsów (IDrivable, IRepairable, IInsurable). Gdybyś miał klasę abstrakcyjną Repairable, musiałbyś wybierać — albo Vehicle, albo Repairable! Interfejsy tutaj wygrywają.
Wspólna logika i stan:
Załóżmy, że cały "autotransport" ma pole "numer". To powinno być pole klasy abstrakcyjnej. W interfejsie pola (poza statycznymi) nie można.
Ewolucja API:
Jedna z rewolucyjnych historii z Default Interface Methods — teraz interfejsy można rozwijać bez ryzyka popsucia istniejących użytkowników.
Na przykład, dodano do interfejsu nową metodę z domyślną implementacją — wszystko działa, wszystkie stare implementacje interfejsu nie padły! Kiedyś to bolało (albo, jeśli zapomnieć o bólu, i tak było niemożliwe).
8. Przykłady w praktyce
W naszej aplikacji edukacyjnej stopniowo pojawia się logowanie. Stwórzmy własny interfejs ILogger z domyślną implementacją:
public interface ILogger
{
void Log(string message);
// Domyślna implementacja dostępna dla wszystkich implementacji interfejsu!
void LogInfo(string info)
{
Log("[INFO] " + info);
}
// Statyczna metoda interfejsu
static void PrintHelp()
{
Console.WriteLine("Użyj ILogger do logowania zdarzeń");
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
// Gdzieś w kodzie:
ILogger logger = new ConsoleLogger();
logger.LogInfo("System uruchomiony!"); // działa dzięki domyślnej implementacji
// Wywołanie statycznej metody interfejsu
ILogger.PrintHelp();
Gdybyśmy dodali nową metodę z domyślną implementacją do interfejsu, wszystkie istniejące implementacje (np. ConsoleLogger) automatycznie dostałyby tę nową metodę — żadnej paniki i psucia kodu.
9. Błędy i niuanse: praktyka i pułapki
Warto wiedzieć, że nie wszystko jest tak kolorowe, jak się wydaje. Na przykład, jeśli twój interfejs zawiera domyślną implementację, ale użytkownik odwołuje się do obiektu przez typ klasy, a nie przez typ interfejsu, domyślna implementacja jest dostępna tylko przez interfejs.
ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // Nie skompiluje się: LogInfo nie jest zdefiniowane w klasie!
ILogger log2 = log;
log2.LogInfo("Hello"); // Wszystko OK!
To trochę jak szczególny przypadek jawnej implementacji interfejsu. Czasem takie zachowanie jest wygodne do ukrycia "zbędnego" API, czasem — zaskakujące dla początkujących.
GO TO FULL VERSION