Jason. Cała informatyka w jednym miejscu!

Po krótkiej przerwie, otwieramy kolejny dział wiedzy z Kotlina! Teraz czas na "genericsy"! Tak, tak. Jeden z tematów wysoce zaawansowanych, przy których już bez kitu trzeba dłużej posiedzieć i zrozumieć sens działania. Jako pierwszy artykuł na ten temat, prezentuję dzisiaj czym jest klasa sparametryzowana w języku Kotlin! Czytać uważnie, bo tu będą się dziać skomplikowane rzeczy!

KLASA SPARAMETRYZOWANA W JĘZYKU KOTLIN POCZĄTKIEM DO PROGRAMOWANIA GENERYCZNEGO

Ucząc się Javy, mogliście dojść do tematu związanego z typami generycznymi albo programowaniem generycznym (nazywany również "programowaniem uogólnionym"). Tak się składa, że na ten temat również zdążyłem wysłać artykuł kiedy pisałem regularnie o języku Java, dlatego też skieruję Was tam, gdzie trzeba, a tutaj tylko szybko przypomnę i przejdę zaraz do rzeczy.

JAK MAM ROZUMIEĆ COŚ "UOGÓLNIONEGO"?

Muszę uwzględnić czytelników, którzy o typie generycznym nie mają zielonego pojęcia (już nie piszę o pojęciu "klasa sparametryzowana" w języku Kotlin). Typ generyczny oznacza najprostszymi słowy, "bycie" typem uniwersalnym. Oznacza to, że taki typ nie ma na sobie żadnej ścisłej informacji do jakiej grupy klas może należeć. Łopatologicznie można stwierdzić, że jest on "niewiadomego pochodzenia".

Jest taki sobie element o nazwie "T". Konwencja nazewnicza "trzyma się" literek T oraz E. T jest literą uniwersalną (ang. "type"), a E używane jest przeważnie podczas programowania kolekcji i interfejsów (ang. "element"). Można też spotkać się z parą K i V podczas programowania niedawno poznanej mapy.

Taki "T" jest nazywany "parametrem typu" i jak napisałem, nie określa on sam w sobie jakiejkolwiek konkretnej informacji ani do czego to należy, ani którym klasom może się to przydać (choć można wymusić ograniczenie, aby w miejsce T, miała prawo pojawić się tylko podana klasa albo jej pochodna - szczegóły niżej). Dlatego jeśli klasa sparametryzowana w języku Kotlin zostanie użyta do utworzenia instancji, należy wewnątrz pary nawiasów kątowych określić typ, czyli KONKRETNĄ klasę, chyba że na podstawie wartości parametru typu generycznego, kompilator określi sobie typ samodzielnie.

PRZYKŁAD KLASY SPARAMETRYZOWANEJ

Popatrzmy na typ generyczny w akcji. Najpierw przyjrzyjmy się najprostszej możliwej strukturze. Oto jak prezentuje się klasa sparametryzowana w języku Kotlin:

class Generic<T>

Czy już widzicie jeden "wystający" szczegół? Literka "T" w nawiasach kątowych!!! To ma być dla Was dożywotni znak rozpoznawczy typów generycznych. Jeśli widzicie w klasie, bądź funkcji parę nawiasów kątowych, ma się Wam zapalać lampka, że jest to wykorzystywanie typów generycznych.

Choć parametr typu występuje, w chwili obecnej nie ma żadnego wpływu na klasę. Dodajmy daną składową do konstruktora podstawowego:

class Generic<T>(val t: T)

Teraz jak utworzymy sobie kilka instancji, możemy porozmawiać o tym temacie dalej. Zanim skorzystacie z konstruktorów, dodajcie sobie na uboczu tę klasę stworzoną na potrzeby dydaktyczne:

class Point(var x: Int, var y: Int)

Teraz bez przeszkód możecie wstawić dwie poniższe instrukcje:

val genericA = Generic(6)
val genericB = Generic(Point(3, 6))

Sztuczka przydatności uogólnienia polega na tym, że po określeniu typu w momencie wywoływania konstruktora, parametr typu T niejako "wciela się" w typ skonkretyzowany. W naszym przypadku, będą to typy danych "Int" oraz "Point". Możecie to dostrzec w większej okazałości próbując skorzystać z właściwości, która odzwierciedla parametr typu T:

println(genericA.t.inc())
println(genericB.t.x)

W momencie skorzystania z danej składowej "t", mamy dostęp do funkcjonalności już SKONKRETYZOWANEJ, w wyniku wcześniejszego podania (bądź ustalenia przez kompilator) typu. W pierwszym przykładzie wywołujemy przykładowo "inc", jedna z metod obiektu typu "Int" zwracająca aktualną wartość poddaną inkrementacji, z kolei zaś w drugim przykładzie, przedostajemy się do danej składowej obiektu punktu. Ale właściwość "wcielająca się" w konkretny typ jest ta sama!

OGRANICZENIE DO KONKRETNEJ KLASY

Jest możliwość, aby klasa sparametryzowana w języku Kotlin posiadała nałożone ograniczenie na parametr typu T, aby transformacja na typ konkretny była dostępna tylko dla określonej hierarchii klas, to znaczy dla podanej klasy oraz jej klas pochodnych. Załóżmy, że klasę "Point" czynimy otwartą:

open class Point(var x: Int, var y: Int)

i dodatkowo tworzymy sobie odrębną klasę pochodną, która dziedziczy od punktu:

class Entity(x: Int, y: Int, model: String) : Point(x, y)

Trzeba jeszcze nałożyć poprawkę w naszej klasie sparametryzowanej:

class Generic<T : Point>(val t: T)

Czy jesteście w stanie ją dostrzec? "Siedzi" ona wewnątrz nawiasów kątowych. Zaraz po nazwie naszego parametru typu, wstawiamy dwukropek (tak, ten sam, który nakładamy podczas dziedziczenia czy implementowania interfejsu do specyficznej klasy) i podajemy nazwę klasy, od której ma się zaczynać dopuszczanie obiektów zarówno podanego typu, jak również wszystkich klas pochodnych (czyli "Entity" w naszym przykładzie). Oznaczać to będzie, że nasza pierwsza instancja będzie już powodować błąd kompilacji, gdyż "Int" nie należy do hierarchii klasy "Point". Drugi obiekt nadal będzie dopuszczalny. Gdy utworzycie sobie jeszcze jeden tym razem typu "Entity", to również zostanie uznany za prawidłowy, bo "Entity" jest klasą potomną klasy "Point". A to dlatego, że klasa sparametryzowana w języku Kotlin nie wpuszcza do środka jedynie klas niezwiązanych z klasą "Point".

POWÓD WPROWADZENIA TYPÓW GENERYCZNYCH

Na koniec, odpowiedź na pytanie które może Was intrygować: "po co to w ogóle?". W celu podniesienia poziomu bezpieczeństwa. Dawniej, kiedy nie było jeszcze typów generycznych, "pakowanie" elementów do kolekcji traciło swój oryginalny typ danych przez co stawał się "Any" (albo "Object" w Javie), typ danych "wszystkiego". Na przykład instancja klasy "Pączek" stawała się "Any". Instancja klasy "Samochód" stawała się "Any". Wszystko inne można było "upchnąć" do jednej kolekcji i kompilatora nie zastanawiało jakie zagrożenia mogą się z tym związać. Powodowało to horrendalne problemy przy późniejszym przedostawaniu się do danych składowych i metod, bo trzeba było robić kilkanaście weryfikacji celem upewnienia się czy to TEN obiekt. Ponadto, człowiek narażał się na błędy podczas wykonywania programu.

W momencie wprowadzenia typów uogólnionych, nadało to sens umieszczania wielu elementów do kolekcji, gdyż jest dopuszczalne wstawianie obiektów tylko ściśle określonego typu! W ten sposób, wspomniane pączki można było umieścić tylko do kolekcji typu "Pączek" i tak dalej, dzięki czemu błędy zostaną wykryte podczas kompilacji! Stąd poniższy kod:

val donuts: List<Donut> = listOf(Donut("malinowy"), Donut("wiśniowy"), Donut("toffi"))

zostanie zrealizowany bez zarzutów, gdyż "trzymamy się" poprawnego typu danych. A to:

val donuts: List<Donut> = listOf(Donut("malinowy"), Car("Opel"), Person("Maria", "Skłodowska-Curie"))

już stanie się powodem do zgłoszenia błędu kompilacji, bo nie można umieszczać argumentów wszystkich typów jak się chce.

Klasa sparametryzowana w języku Kotlin

Klasa sparametryzowana w języku Kotlin to klasa wykorzystująca parametr typu generycznego do programowania uogólnionego. Programista jest w stanie nałożyć ograniczenie na parametr typu od której klasy (wraz z klasami dziedziczącymi od niej) ma występować dopuszczenie typu obiektu.


Klasa sparametryzowana dysponująca parametrem typu uogólnionego, stanowi potężną broń w naszych dłoniach. Jedyny haczyk jest taki, że trzeba naprawdę solidnie rozumieć typy generyczne dlaczego pisze się to tak, a nie inaczej, a także jakie manewry mogą powodować jakie konsekwencje. Gdy dojdziemy do reszty materiału, dopiero zaczną się "robić kłęby dymu" nad głowami!

PODOBNE ARTYKUŁY