Interfejsy — wspólna umowa bez wspólnego przodka
Różne urządzenia potrafią drukować etykietę. Program potrzebuje tej umiejętności, a nie wiedzy o całej konstrukcji urządzenia.
Czego się dziś nauczysz
- Wyjaśnisz, dlaczego klasa bazowa nie wystarcza, gdy wspólna jest tylko jedna umiejętność.
- Zadeklarujesz interfejs z metodą i właściwością oraz zaimplementujesz go w klasie.
- Zaimplementujesz kilka interfejsów w jednej klasie, także obok klasy bazowej.
- Użyjesz interfejsu jako typu zmiennej, parametru i tablicy.
- Sprawdzisz operatorem
is, czy obiekt spełnia daną umowę. - Rozstrzygniesz, kiedy potrzebny jest interfejs, a kiedy klasa abstrakcyjna.
- Zaimplementujesz gotowy interfejs z biblioteki —
IComparable<T>. - Rozpoznasz komunikaty CS0535, CS0737, CS0115, CS1721 i CS1722.
Jedna baza to za mało
Szkolny inwentarz drukuje etykiety z kodem. Etykietę dostaje komputer z pracowni, książka z biblioteki i legitymacja ucznia. Wszystkie trzy rzeczy trzeba wypisać jedną pętlą.
Z lekcji 45 znasz jedno narzędzie do „wspólnego typu” — klasę bazową. Spróbujmy:
class MaEtykiete
{
public virtual string Etykieta() { return "???"; }
}
class Komputer : Urzadzenie, MaEtykiete // BLAD CS1721
{
}
CS1721
„Klasa Komputer nie może mieć wielu klas bazowych”. W C# klasa ma dokładnie jedną klasę bazową. Komputer jest już urządzeniem i nie może przy okazji być czymś drugim.
Nawet gdyby dziedziczenie po dwóch klasach było dozwolone, pomysł byłby zły. Spójrz na te trzy rzeczy:
| Rzecz | Czy jest urządzeniem? | Czy ma etykietę? |
|---|---|---|
| komputer z pracowni | tak | tak |
| książka z biblioteki | nie | tak |
| legitymacja ucznia | nie | tak |
| drukarka | tak | tak |
| licznik wejść | tak | nie |
Nie ma wspólnego przodka, bo te rzeczy nie mają ze sobą nic wspólnego poza jedną umiejętnością. Książka nie jest urządzeniem, uczeń tym bardziej. Wspólna klasa bazowa dla nich wszystkich musiałaby się nazywać „Rzecz” i nie zawierałaby ani jednego sensownego pola.
Czego szukamy
Potrzebujemy zapisu, który mówi: „nie interesuje mnie, czym to jest — interesuje mnie, że potrafi podać etykietę”. W C# nazywa się to interfejsem. Klasa może mieć jedną bazę, ale dowolnie wiele interfejsów.
Umowa bez wykonania
Interfejs to spis umiejętności bez ani jednej linii wykonania. Deklaruje się go słowem interface:
interface IEtykieta
{
string TekstEtykiety(); // tylko nazwa, typ wyniku i parametry
string Kod { get; } // wlasciwosc tylko do odczytu
}
Trzy rzeczy w tym zapisie różnią się od klasy:
- Nie ma ciał metod — po nawiasach stoi średnik. Interfejs mówi „co”, nigdy „jak”.
- Nie ma modyfikatorów
publicaniabstract. Wszystkie składowe interfejsu są publiczne i abstrakcyjne z definicji, więc się tego nie pisze. - Nie ma pól ani konstruktora. Interfejs nie przechowuje danych, więc nie ma czego ustawiać.
Klasa implementuje interfejs — zapis jest taki sam jak dziedziczenie, po dwukropku:
class Ksiazka : IEtykieta
{
public string Tytul { get; }
public string Autor { get; }
public string Kod { get; } // z interfejsu
public Ksiazka(string tytul, string autor, string kod)
{
Tytul = tytul;
Autor = autor;
Kod = kod;
}
public string TekstEtykiety() // z interfejsu
{
return $"{Kod} {Autor}: {Tytul}";
}
}
Bez słowa override
To najczęstsza pomyłka po lekcji 47. Składowa z interfejsu nie jest przesłonięciem — implementujesz ją zwykłym public. Dopisanie override daje CS0115. Musi być natomiast public: bez tego słowa metoda jest prywatna i kompilator uzna, że interfejs nie został zrealizowany (CS0737).
Kompilator pilnuje kompletności tak samo jak przy klasie abstrakcyjnej:
class Ksiazka : IEtykieta // BLAD CS0535
{
public string Tytul { get; }
// nie ma ani TekstEtykiety(), ani Kod
}
CS0535 — „Ksiazka nie implementuje elementu członkowskiego interfejsu IEtykieta.TekstEtykiety()”. Umowa albo jest dotrzymana w całości, albo program się nie buduje.
Interfejs jako typ zmiennej
Tak jak klasa abstrakcyjna, interfejs nie da się utworzyć przez new, ale świetnie nadaje się na typ zmiennej, parametru i tablicy:
IEtykieta e = new Ksiazka("Pan Tadeusz", "Mickiewicz", "KS-102");
Console.WriteLine(e.TekstEtykiety());
// IEtykieta x = new IEtykieta(); // BLAD CS0144 - interfejsu nie da sie utworzyc
static void Wydrukuj(IEtykieta co) // dziala dla KAZDEJ klasy z ta umowa
{
Console.WriteLine($"[ {co.TekstEtykiety()} ]");
}
Metoda Wydrukuj nie wie i nie chce wiedzieć, czy dostała książkę, komputer czy legitymację. Wie tylko jedno: to coś potrafi podać tekst etykiety. Na tym polega cała siła interfejsów.
Skąd litera I
Nazwy interfejsów w C# zaczyna się wielką literą I: IEtykieta, IComparable, IDisposable. To nie jest wymóg kompilatora, tylko konwencja z lekcji 05 — pozwala na pierwszy rzut oka odróżnić interfejs od klasy. Cała biblioteka .NET jest tak nazwana, więc trzymaj się tego.
Jedna baza, dowolnie wiele umów
Tu jest różnica, dla której interfejsy istnieją: klasa może mieć jedną bazę i dowolnie wiele interfejsów.
interface IEtykieta
{
string TekstEtykiety();
string Kod { get; }
}
interface ISerwisowane
{
int DniDoSerwisu { get; }
void Serwisuj();
}
class Komputer : Urzadzenie, IEtykieta, ISerwisowane
{
// ...
}
Kolejność po dwukropku jest ustalona: najpierw klasa bazowa, potem interfejsy. Odwrotnie daje CS1722. Same interfejsy można wymieniać w dowolnej kolejności.
| Zapis | Poprawny? | Uwaga |
|---|---|---|
class A : Baza | tak | tylko dziedziczenie |
class A : IJedno | tak | tylko interfejs, baza domyślnie object |
class A : Baza, IJedno, IDrugie | tak | to jest typowy zapis |
class A : IJedno, Baza | nie | CS1722 — klasa bazowa musi być pierwsza |
class A : Baza1, Baza2 | nie | CS1721 — jedna klasa bazowa |
interface I : IJedno, IDrugie | tak | interfejs może dziedziczyć po wielu interfejsach |
Ta sama umowa w niepowiązanych klasach
Teraz trzy klasy z zupełnie różnych światów trafiają do jednej tablicy:
IEtykieta[] doWydruku =
{
new Komputer("Dell OptiPlex", "KOM-014"),
new Ksiazka("Pan Tadeusz", "Mickiewicz", "KS-102"),
new Legitymacja("Kasia Nowak", "3TP", "LEG-88")
};
foreach (IEtykieta e in doWydruku)
{
Console.WriteLine(e.TekstEtykiety());
}
Komputer dziedziczy po Urzadzenie, Ksiazka po niczym, Legitymacja po niczym. Nie mają wspólnego przodka poza object — a mimo to mieszczą się w jednej tablicy, bo mają wspólną umowę.
Sprawdzanie, czy klasa spełnia umowę
is i as z lekcji 46 działają także z interfejsami:
foreach (object rzecz in inwentarz)
{
if (rzecz is IEtykieta e)
{
Console.WriteLine(e.TekstEtykiety());
}
if (rzecz is ISerwisowane s && s.DniDoSerwisu < 30)
{
Console.WriteLine($" UWAGA: serwis za {s.DniDoSerwisu} dni");
}
}
Jeden obiekt może pasować do kilku sprawdzeń naraz — komputer wejdzie do obu if, a książka tylko do pierwszego.
Gdy dwa interfejsy mają metodę o tej samej nazwie
Zdarza się, że dwa interfejsy wymagają metody Zamknij(), a klasa musi zrobić w każdym przypadku coś innego. Wtedy pisze się implementację jawną — bez public, z nazwą interfejsu przed nazwą metody:
void IPlik.Zamknij() { ... } i void IOkno.Zamknij() { ... }
Taka metoda jest dostępna tylko przez zmienną odpowiedniego interfejsu, nie przez zmienną klasy. W zadaniach szkolnych rzadko tego potrzebujesz — wystarczy, że wiesz, że taki zapis istnieje i co znaczy.
Sześć różnic i jedna decyzja
Interfejs i klasa abstrakcyjna z lekcji 47 wyglądają podobnie: obie zapowiadają składowe bez ciała i obu nie da się utworzyć. Różnic jest jednak sześć i wszystkie są istotne.
| Klasa abstrakcyjna | Interfejs | |
|---|---|---|
| Ile na klasę | jedna | dowolnie wiele |
| Pola | tak | nie |
| Konstruktor | tak | nie |
| Gotowy kod metod | tak, dowolnie dużo | tylko domyślne implementacje, i to rzadko |
| Składowe niepubliczne | tak (protected, private) | nie — wszystko publiczne |
| Słowo w klasie pochodnej | override | public, bez override |
| Odpowiada na pytanie | czym to jest | co to potrafi |
| Typowa nazwa | rzeczownik: Bilet, Figura | przymiotnik lub umiejętność: IPorownywalny, IEtykieta |
Jak wybrać
| Sytuacja | Wybór |
|---|---|
| Klasy mają wspólne pola i wspólny kod | klasa abstrakcyjna |
| Klasy nie mają nic wspólnego poza jedną umiejętnością | interfejs |
| Klasa już dziedziczy po czymś innym | interfejs — bazy nie da się dołożyć |
| Potrzebny licznik, numerowanie albo wspólny konstruktor | klasa abstrakcyjna |
| Chcesz opisać umiejętność, którą mogą mieć różne hierarchie | interfejs |
| Jedno i drugie | oba naraz — klasa abstrakcyjna, która implementuje interfejs |
Bardzo częste rozwiązanie
Klasa abstrakcyjna i interfejs nie są konkurencją. W bibliotece .NET stale spotkasz zapis abstract class Strumien : IDisposable: interfejs mówi, co obiekt potrafi, a klasa abstrakcyjna dostarcza gotowe pola i połowę kodu dla swoich potomków.
Umowy, które ktoś już napisał
Interfejsów nie pisze się głównie samemu — częściej implementuje się gotowe z biblioteki. Cztery spotkasz najszybciej.
IComparable<T> — „umiem się porównać”
Klasa, która ma jedną metodę CompareTo, zaczyna działać z Array.Sort z lekcji 24:
class Uczen : IComparable<Uczen>
{
public string Nazwisko { get; }
public int Punkty { get; }
public Uczen(string nazwisko, int punkty)
{
Nazwisko = nazwisko;
Punkty = punkty;
}
public int CompareTo(Uczen? inny)
{
if (inny == null)
{
return 1;
}
return inny.Punkty - Punkty; // malejaco po punktach
}
public override string ToString()
{
return $"{Nazwisko} {Punkty} pkt";
}
}
Uczen[] wyniki =
{
new Uczen("Nowak", 34),
new Uczen("Lis", 51),
new Uczen("Wrona", 42)
};
Array.Sort(wyniki); // dziala, bo Uczen umie sie porownac
foreach (Uczen u in wyniki)
{
Console.WriteLine(u);
}
Lis 51 pkt
Wrona 42 pkt
Nowak 34 pkt
Array.Sort został napisany wiele lat przed Twoją klasą i nic o niej nie wie. Potrafi ją posortować, bo wymaga tylko jednej umowy: IComparable<T>. Do tego wracamy w lekcji 51.
IEnumerable<T> — „można mnie przejść pętlą foreach”
To umowa, którą spełniają tablice, List<T> i wszystkie kolekcje z lekcji 31–33. Właśnie dlatego foreach działa z każdą z nich, choć w środku są zupełnie różne.
IDisposable — „mam co zwolnić”
Znasz ją z lekcji 36 i 40: klasa z metodą Dispose() daje się użyć w bloku using, który wywoła zwolnienie także wtedy, gdy poleci wyjątek.
IFormattable, IEquatable<T> i pozostałe
Nie trzeba ich pamiętać. Trzeba pamiętać schemat: gdy jakaś gotowa metoda w .NET „nie chce” Twojej klasy, poszukaj w dokumentacji, jakiego interfejsu wymaga jej parametr. Prawie zawsze wystarczy dopisać jedną metodę.
Inwentarz szkolny
Inwentarz szkolny: dwa interfejsy, jedna klasa abstrakcyjna i cztery klasy, które łączą się w różne kombinacje. Zwróć uwagę, że tablica jest typu object[] — nie ma typu, który obejmowałby wszystkie te rzeczy.
// Program.cs - pelny plik
object[] inwentarz =
{
new Komputer("Dell OptiPlex", "KOM-014", 12),
new Komputer("HP ProDesk", "KOM-015", 95),
new Ksiazka("Pan Tadeusz", "Mickiewicz", "KS-102"),
new Legitymacja("Kasia Nowak", "3TP", "LEG-88"),
new LicznikWejsc()
};
Console.WriteLine("=== etykiety do wydruku ===");
foreach (object rzecz in inwentarz)
{
if (rzecz is IEtykieta e)
{
Console.WriteLine(e.TekstEtykiety());
}
}
Console.WriteLine();
Console.WriteLine("=== plan serwisu ===");
foreach (object rzecz in inwentarz)
{
if (rzecz is ISerwisowane s)
{
if (s.DniDoSerwisu < 30)
{
Console.Write("PILNE: ");
}
s.Serwisuj();
}
}
Console.WriteLine();
Console.WriteLine("=== jedna metoda dla wszystkiego, co ma etykiete ===");
Wydrukuj(new Ksiazka("Lalka", "Prus", "KS-201"));
Wydrukuj(new Legitymacja("Marek Lis", "2TP", "LEG-91"));
Console.WriteLine();
Console.WriteLine("=== co spelnia ktora umowe ===");
foreach (object rzecz in inwentarz)
{
string nazwa = rzecz.GetType().Name;
string etyk = rzecz is IEtykieta ? "IEtykieta" : "-";
string serw = rzecz is ISerwisowane ? "ISerwisowane" : "-";
Console.WriteLine($"{nazwa,-14} {etyk,-11} {serw}");
}
static void Wydrukuj(IEtykieta co)
{
Console.WriteLine($"+-- {co.Kod} " + new string('-', 20));
Console.WriteLine($"| {co.TekstEtykiety()}");
Console.WriteLine("+" + new string('-', 27));
}
interface IEtykieta
{
string Kod { get; }
string TekstEtykiety();
}
interface ISerwisowane
{
int DniDoSerwisu { get; }
void Serwisuj();
}
abstract class Urzadzenie
{
public string Nazwa { get; }
protected Urzadzenie(string nazwa)
{
Nazwa = nazwa;
}
public abstract string Opis();
}
class Komputer : Urzadzenie, IEtykieta, ISerwisowane
{
public string Kod { get; }
public int DniDoSerwisu { get; }
public Komputer(string nazwa, string kod, int dniDoSerwisu) : base(nazwa)
{
Kod = kod;
DniDoSerwisu = dniDoSerwisu;
}
public override string Opis()
{
return $"komputer {Nazwa}";
}
public string TekstEtykiety()
{
return $"{Kod} {Nazwa} (pracownia)";
}
public void Serwisuj()
{
Console.WriteLine($"{Kod}: przeglad za {DniDoSerwisu} dni");
}
}
class LicznikWejsc : Urzadzenie, ISerwisowane
{
public int DniDoSerwisu => 200;
public LicznikWejsc() : base("licznik wejsc") { }
public override string Opis()
{
return "licznik wejsc przy drzwiach";
}
public void Serwisuj()
{
Console.WriteLine($"licznik: przeglad za {DniDoSerwisu} dni");
}
}
class Ksiazka : IEtykieta
{
public string Tytul { get; }
public string Autor { get; }
public string Kod { get; }
public Ksiazka(string tytul, string autor, string kod)
{
Tytul = tytul;
Autor = autor;
Kod = kod;
}
public string TekstEtykiety()
{
return $"{Kod} {Autor}: {Tytul}";
}
}
class Legitymacja : IEtykieta
{
public string Uczen { get; }
public string Klasa { get; }
public string Kod { get; }
public Legitymacja(string uczen, string klasa, string kod)
{
Uczen = uczen;
Klasa = klasa;
Kod = kod;
}
public string TekstEtykiety()
{
return $"{Kod} {Uczen} / {Klasa}";
}
}
=== etykiety do wydruku ===
KOM-014 Dell OptiPlex (pracownia)
KOM-015 HP ProDesk (pracownia)
KS-102 Mickiewicz: Pan Tadeusz
LEG-88 Kasia Nowak / 3TP
=== plan serwisu ===
PILNE: KOM-014: przeglad za 12 dni
KOM-015: przeglad za 95 dni
licznik: przeglad za 200 dni
=== jedna metoda dla wszystkiego, co ma etykiete ===
+-- KS-201 --------------------
| KS-201 Prus: Lalka
+---------------------------
+-- LEG-91 --------------------
| LEG-91 Marek Lis / 2TP
+---------------------------
=== co spelnia ktora umowe ===
Komputer IEtykieta ISerwisowane
Komputer IEtykieta ISerwisowane
Ksiazka IEtykieta -
Legitymacja IEtykieta -
LicznikWejsc - ISerwisowane
Co dzieje się po kolei
Komputerma klasę bazową i dwa interfejsy — dokładnie to, czego nie dałoby się zrobić samym dziedziczeniem.KsiazkaiLegitymacjanie dziedziczą po niczym, a mimo to trafiają do tej samej pętli co komputer, bo spełniają tę samą umowę.LicznikWejscjest urządzeniem i wymaga serwisu, ale nie ma etykiety — dlatego nie pojawia się w pierwszej pętli. Interfejsy dobiera się osobno dla każdej klasy.- Pierwsza i druga pętla przechodzą tę samą tablicę, ale wybierają z niej różne podzbiory. Filtrem jest interfejs, nie klasa.
Wydrukujma parametr typuIEtykieta. Nie zna klasyKsiazkaaniLegitymacja— używa tylkoKodiTekstEtykiety(), bo tyle gwarantuje umowa.- Ostatnia pętla pokazuje pełny obraz: każda klasa spełnia inny zestaw umów, a
isodpowiada na pytanie „czy potrafi”, nie „czym jest”. DniDoSerwisuw klasieKomputerjest zwykłą właściwością z konstruktora, a wLicznikWejscwłaściwością wyliczaną. Interfejs nie narzuca sposobu — tylko to, że wartość ma być dostępna do odczytu.
Sprawdź trzy rzeczy
- Usuń
publicprzedTekstEtykietyw klasieKsiazka. Dostaniesz CS0737 — składowa implementująca interfejs musi być publiczna. - Dopisz
overrideprzedTekstEtykiety. Dostaniesz CS0115: to nie jest przesłonięcie, bo w żadnej klasie bazowej nie ma takiej metody. - Dopisz do
KsiazkainterfejsISerwisowane, ale nic więcej. CS0535 wymieni obie brakujące składowe. Potem dopisz je i sprawdź, że książka pojawiła się w planie serwisu, choć nie zmieniłeś ani jednej linii w pętlach.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
interface IX { } | deklaracja umowy | Bez pól, bez konstruktora, bez ciał metod. |
| składowa interfejsu | nazwa, typ i parametry | Kończy się średnikiem. Domyślnie publiczna i abstrakcyjna. |
class A : IX | klasa realizuje umowę | Musi podać wszystkie składowe (CS0535). |
public w klasie | implementacja składowej | Bez override. Brak public daje CS0737. |
class A : Baza, IX, IY | jedna baza i wiele interfejsów | Klasa bazowa zawsze pierwsza (CS1722). |
interface IX : IY | interfejs rozszerza inny | Tu dziedziczenie wielokrotne jest dozwolone. |
IX zmienna | typ zmiennej lub parametru | Widać tylko składowe z umowy. |
rzecz is IX x | sprawdzenie, czy klasa spełnia umowę | Jeden obiekt może pasować do kilku interfejsów. |
void IX.Metoda() | implementacja jawna | Dla dwóch umów z tą samą nazwą metody. Bez public. |
IComparable<T> | „umiem się porównać” | Metoda CompareTo włącza Array.Sort. |
IEnumerable<T> | „można mnie przejść pętlą” | Spełniają ją tablice i wszystkie kolekcje. |
IDisposable | „mam co zwolnić” | Metoda Dispose() i blok using. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
override przy składowej z interfejsu | CS0115. Interfejs nie ma ciała do przesłonięcia — pisz samo public. |
Brak public przy implementacji | CS0737. Metoda jest prywatna, więc umowa nie jest spełniona. |
| Brakująca składowa interfejsu | CS0535. Kompilator wypisze dokładną nazwę i sygnaturę — przeczytaj komunikat do końca. |
| Inna sygnatura niż w interfejsie | CS0535, bo to jest inna metoda. Zwróć uwagę na typ wyniku i na liczbę parametrów. |
| Interfejs przed klasą bazową po dwukropku | CS1722. Kolejność: najpierw baza, potem interfejsy. |
| Dwie klasy bazowe | CS1721. To właśnie po to są interfejsy. |
| Pole w interfejsie | CS0525. Interfejs nie przechowuje danych — użyj właściwości. |
| Konstruktor w interfejsie | CS0526. Interfejsu nie tworzy się przez new. |
new IEtykieta() | CS0144. Utwórz obiekt klasy, która tę umowę spełnia. |
| Interfejs z dziesięcioma metodami | Umowa, której nikt nie chce podpisać. Lepiej dwa lub trzy małe interfejsy niż jeden wielki. |
| Interfejs zamiast klasy abstrakcyjnej dla wspólnych pól | Skoro klasy mają wspólne dane, potrzebna jest klasa bazowa — interfejs nie przechowuje pól. |
Zadania
Napisz interfejs IOpisywalny z jedną metodą string Opis(). Zaimplementuj go w dwóch niepowiązanych klasach: Ksiazka (tytuł i autor) oraz Rower (marka i liczba biegów). W programie utwórz tablicę IOpisywalny[] z trzema obiektami — dwiema książkami i jednym rowerem — i wypisz wszystkie opisy jedną pętlą foreach. Następnie usuń metodę Opis z klasy Rower, spróbuj skompilować i zapisz w komentarzu numer błędu.
Rozbuduj interfejs IOpisywalny z zadania 1 o właściwość string Kod { get; }. Uzupełnij obie klasy. Dopisz metodę static void Etykieta(IOpisywalny co), która wypisuje w ramce kod i opis, i wywołaj ją dla każdego elementu tablicy. Sprawdź, co się stanie, gdy w klasie Ksiazka usuniesz słowo public przed metodą Opis — zapisz numer błędu w komentarzu.
Napisz klasę abstrakcyjną Sprzet z właściwością Nazwa i abstrakcyjną metodą string Kategoria(). Napisz dwa interfejsy: IWypozyczalny (właściwość bool Dostepny { get; } i metoda void Wypozycz(string komu)) oraz INaprawialny (metoda void Napraw()). Napisz trzy klasy: Laptop dziedziczący po Sprzet i implementujący oba interfejsy, Projektor dziedziczący po Sprzet i implementujący tylko INaprawialny, oraz Ksiazka, która nie dziedziczy po niczym i implementuje tylko IWypozyczalny. Utwórz tablicę object[] ze wszystkimi obiektami i napisz dwie pętle: pierwsza wypożycza wszystko, co da się wypożyczyć, druga naprawia wszystko, co da się naprawić. Użyj is ze zmienną.
Dla każdej z sześciu sytuacji zapisz w komentarzu, czy użyjesz klasy abstrakcyjnej, interfejsu, czy obu, i uzasadnij jednym zdaniem: (a) wszystkie figury geometryczne mają nazwę i liczą pole; (b) niektóre obiekty w grze można zapisać do pliku — są to zarówno postacie, jak i mapy; (c) wszyscy pracownicy mają imię, nazwisko i numer, a pensję liczą inaczej; (d) niektóre obiekty da się posortować; (e) wszystkie bilety numerują się automatycznie i mają cenę zależną od rodzaju; (f) zarówno drukarka, jak i plik dziennika mają metodę zamykania. Następnie zaimplementuj w kodzie punkty (b) i (e): dla (b) napisz interfejs IZapisywalny z metodą string DoTekstu() i dwie niepowiązane klasy, które go spełniają; dla (e) napisz klasę abstrakcyjną z licznikiem i dwie klasy pochodne. Uruchom oba przykłady.
Napisz klasę Zawodnik z właściwościami Nazwisko, Czas (w sekundach, typ double) i Punkty. Zaimplementuj IComparable<Zawodnik> tak, żeby sortowanie ustawiało zawodników od najkrótszego czasu. Utwórz tablicę pięciu zawodników, posortuj ją metodą Array.Sort i wypisz wyniki z numerami miejsc. Przesłoń ToString(), żeby wypisanie zawodnika dawało czytelny wiersz. Na koniec zmień CompareTo tak, żeby sortowało malejąco po punktach, uruchom ponownie i zapisz w komentarzu, ile linii programu poza samą metodą CompareTo musiałeś zmienić.
Zbuduj program obsługujący szkolną wypożyczalnię. Napisz interfejsy: IEtykieta (właściwość Kod { get; }, metoda string TekstEtykiety()), IWypozyczalny (właściwość bool Dostepny { get; }, metody bool Wypozycz(string komu) i void Zwroc()) oraz IPlatny (właściwość decimal OplataZaDzien { get; }). Napisz klasę abstrakcyjną PozycjaInwentarza z automatycznym numerem z licznika statycznego, właściwością Nazwa i abstrakcyjną metodą string Kategoria(); niech ta klasa implementuje IEtykieta, budując kod z kategorii i numeru. Napisz trzy klasy pochodne: Laptop (wypożyczalny, płatny 5 zł za dzień), Ksiazka (wypożyczalna, darmowa) i TablicaInteraktywna (nie wypożyczana — implementuje tylko to, co daje klasa bazowa). W programie utwórz listę co najmniej sześciu pozycji, a następnie: wypisz wszystkie etykiety; wypożycz trzy pozycje różnym osobom i spróbuj wypożyczyć jedną z nich po raz drugi, obsługując odmowę; wypisz tabelkę pozycji z kolumnami numer, kategoria, nazwa, dostępność i opłata dzienna (dla darmowych wpisz kreskę); policz łączny dzienny przychód z aktualnie wypożyczonych pozycji płatnych. Na końcu pliku, w komentarzu, odpowiedz: dlaczego IEtykieta została zaimplementowana w klasie abstrakcyjnej, a nie w każdej klasie pochodnej osobno, oraz co trzeba zmienić w programie, żeby dodać pozycję „mikroskop”, wypożyczalną i płatną 2 zł za dzień.
Co trzeba zapamiętać
- Interfejs to umowa: spis umiejętności bez ani jednej linii wykonania.
- Klasa ma jedną klasę bazową, ale dowolnie wiele interfejsów — to główna różnica.
- Po dwukropku najpierw klasa bazowa, potem interfejsy (inaczej CS1722).
- Składowe interfejsu są publiczne i abstrakcyjne z definicji, więc modyfikatorów się nie pisze.
- W klasie implementującej używasz
public, nieoverride. - Brak choćby jednej składowej to błąd CS0535 — umowa albo jest spełniona w całości, albo program się nie buduje.
- Interfejsu nie da się utworzyć przez
new, ale jest doskonałym typem zmiennej i parametru. - Interfejs odpowiada na pytanie „co to potrafi”, klasa abstrakcyjna na „czym to jest”.
- Interfejs nie ma pól ani konstruktora — gdy klasy mają wspólne dane, potrzebna jest klasa bazowa.
- Klasa abstrakcyjna i interfejs często występują razem: umowa na zewnątrz, wspólny kod w bazie.
- Nazwy interfejsów zaczynają się od
I— to konwencja całej biblioteki .NET. - Gotowe metody .NET współpracują z Twoimi klasami dzięki interfejsom:
IComparable<T>,IEnumerable<T>,IDisposable.
Dokumentacja: Microsoft Learn — temat tej lekcji.