Sealed klasy i interfejsy w Kotlinie
Klasy i interfejsy w Kotlinie nie służą tylko do reprezentowania zestawu operacji lub danych; posługując się dziedziczeniem, możemy również reprezentować hierarchie. Na przykład, powiedzmy, że wysyłasz żądanie sieciowe; w rezultacie albo otrzymujesz żądane dane, albo żądanie kończy się niepowodzeniem z informacjami o tym, co poszło nie tak. Te dwa możliwe rezultaty można przedstawić za pomocą dwóch klas implementujących ten sam interfejs:
Alternatywnie mogą dziedziczyć po klasie abstrakcyjnej:
W tym przypadku spodziewamy się, że gdy funkcja zwraca Result
, może być to Success
lub Failure
.
Problem polega na tym, że przy użyciu zwykłego interfejsu lub klasy abstrakcyjnej nie ma gwarancji, że zdefiniowane podklasy są wszystkimi możliwymi podtypami tego interfejsu lub klasy abstrakcyjnej. Ktoś może zdefiniować inną klasę i sprawić, że będzie ona implementować lub rozszerzać Result
. Ktoś może nawet użyć do tego wyrażenia tworzącego obiekt.
Hierarchia, której podklasy nie są znane z góry, nazywana jest hierarchią nieograniczoną. Dla Result
wolelibyśmy zdefiniować hierarchię ograniczoną. Aby osiągnąć ten cel, używamy modyfikatora sealed
przed klasą lub interfejsem03.
Gdy używamy modyfikatora
sealed
przed klasą, sprawia to, że klasa staje się abstrakcyjna, więc nie używamy dodatkowo modyfikatoraabstract
.
Wszystkie podklasy sealed klasy lub interfejsu muszą spełniać kilka wymagań:
- muszą być zdefiniowane w tym samym pakiecie i module, co ich rodzic,
- nie mogą być lokalne ani zdefiniowane za pomocą wyrażenia tworzącego obiekt.
Oznacza to, że używając modyfikatora sealed
, kontrolujesz, jakie podklasy ma klasa lub interfejs. Klienci Twojej biblioteki lub modułu nie mogą dodać własnych bezpośrednich podklas2. Nikt nie może po cichu dodać lokalnej klasy ani wyrażenia obiektu, które rozszerza sealed klasę lub interfejs. Kotlin uczynił to niemożliwym. Hierarchia podklas jest ograniczona.
Sealed interfejsy zostały wprowadzone w nowszych wersjach Kotlina, aby umożliwić klasom uczestnictwo w wielu różnych ograniczonych hierarchiach (można rozszerzać tylko jedną klasę, ale implementować wiele interfejsów). Relacja między sealed klasą i interfejsem jest podobna do relacji między klasą abstrakcyjną a interfejsem. Mocą klas jest to, że mogą przechowywać stan (właściwości nieabstrakcyjne) i kontrolować otwartość swoich elementów (mogą mieć metody i właściwości finalne). Mocą interfejsów jest to, że klasa może dziedziczyć tylko z jednej klasy, ale może implementować wiele interfejsów.
Sealed klasy i wyrażenia when
Kiedy używamy when
jako wyrażenia, zawsze musimy zwrócić jakąś wartość. W większości przypadków jedynym sposobem na osiągnięcie tego jest określenie gałęzi else
.
Jednak istnieją również przypadki, w których Kotlin wie, że rozpatrzyliśmy wszystkie możliwe wartości. Na przykład, gdy używamy wyrażenia when z wartością typu enum i porównujemy tę wartość do wszystkich możliwych wartości enum.
Dla wartości określone typem z modyfikatorem sealed można rozpatrzyć wszystkie możliwości poprzez sprawdzenie wszystkich możliwych podtypów. Do sprawdzenia typu używamy operatora is
. Dzięki modyfikatorowi sealed
nie musimy używać gałęzi else
, gdy rozpatrzymy wszystkie możliwe podtypy.
Ponadto IntelliJ automatycznie sugeruje dodanie pozostałych gałęzi. To sprawia, że sealed klasy i interfejsy są bardzo wygodne w użyciu, gdy musimy uwzględniać ich bezpośrednie podtypy.
Zauważ, że gdy else
nie jest używane, a my dodajemy kolejną podklasę sealed klasy lub interfejsu, należy dostosować użycie tego wyrażenia when
, uwzględniając ten nowy typ. Jest to wygodne w lokalnym kodzie, ponieważ zmusza nas do obsługi nowego typu w wyczerpujących wyrażeniach when
. Jest to jednak problem, gdy sealed klasa lub interfejs jest częścią publicznego API biblioteki lub współdzielonego modułu, gdyż dodanie podtypu jest niekompatybilne wstecznie, ponieważ wszystkie moduły używające wyczerpującego when
muszą obsłużyć nowy możliwy typ.
Sealed vs enum
Enumy reprezentują zbiór możliwych wartości. Sealed klasy lub interfejsy reprezentują zestaw typów. To istotna różnica. Klasa to coś więcej niż wartość. Może mieć wiele instancji i może być nośnikiem danych. Pomyśl o Response
: gdyby była enumem, nie mogłaby przechowywać value
ani error
. Podklasy sealed klasy lub interfejsy mogą przechowywać różne dane, podczas gdy enum to tylko zestaw wartości.
Przypadki użycia
Używamy sealed klas, gdy chcemy wyrazić, że istnieje konkretna liczba podklas danej klasy.
Kluczową korzyścią jest to, że wyrażenie when może łatwo pokryć wszystkie możliwe typy w hierarchii, używając operatora is
. Warunek when zapewnia wtedy, że obsługiwane są wszystkie możliwe podtypy.
Warto także dodać, że gdy używamy modyfikatora sealed
, możemy użyć refleksji, aby znaleźć wszystkie podklasy1:
Podsumowanie
Sealed klasy oraz interfejsy powinny być używane do reprezentowania ograniczonych hierarchii. Wyrażenie when ułatwia obsługę każdego możliwego podtypu. Jest to wygodne i często wykorzystywane w Kotlinie. Jeśli chcemy kontrolować, jakie są podklasy danej klasy, powinniśmy użyć modyfikatora sealed
.
Następnie omówimy ostatni specjalny rodzaj klasy, który służy do definiowania dodatkowych informacji o elementach naszego kodu: adnotacje.
Ograniczone hierarchie są używane do reprezentowania wartości, które mogą przyjmować kilka różnych, ale stałych typów.
Wymaga to zależności kotlin-reflect
. Więcej o refleksji w książce Zaawansowany Kotlin.
Nadal można deklarować klasę abstrakcyjną lub interfejs jako podklasę sealed klasy lub interfejsu, i z niej już klient będzie mógł dziedziczyć w innym module.
Słowo "sealed" można przetłumaczyć jako "zapieczętowany", tak jak pieczętowało się niegdyś koperty przed wysłaniem.