Jason. Cała informatyka w jednym miejscu!

Kochani, oto ostatni wycinek wiedzy na temat stosowania typów generycznych. Po przedstawieniu kontrawariancji, przyjrzymy się teraz operacji w drugą stronę, czyli jak utworzyć kowariancję, co ona nam daje i pod jakimi warunkami można ją zrealizować. Zatem na scenę wchodzi słowo kluczowe "out" w języku Kotlin, a Państwa zapraszam do czytanki!

KOWIARANCJA ZA POMOCĄ "OUT" W JĘZYKU KOTLIN

Dobrze to przeczytajcie: ko-wa-rian-cja. Nie mylcie z kontrawariancją, która została opisana w artykule o słówku "in". Jak możecie zacząć coś podejrzewać, jest to przeciwieństwo do wspomnianej kontrawariancji które jak to przy typach generycznych, wymaga starannych wyjaśnień od samego spodu.

Kowariancja daje możliwość wstawienia typu klasy pochodnej do typu klasy bazowej w miejsce parametru typu T. To tak jednym zdaniem po co to jest stosowane. A powód przydatności? Możliwość umieszczenia obiektu typu T, a typem jest klasa pochodna i chcielibyśmy bezkarnie wstawić w miejsce parametru typu T, klasę pochodną. Prościej wytłumaczyć nie można.

PRZYKŁAD KLASY KOWARIANTNEJ

Zerknijcie na przykładowy przypadek. Gracz zbiera surowce za wykonywanie zadań. Pakuje wszystkie znaleziska do jednej torby. Zatem przydałoby się, żeby programistycznie móc upchnąć wszystko do jednej kolekcji. Lista modyfikowalna wydaje się najodpowiedniejszym rodzajem. Przełóżmy tę historyjkę na zapis kodowy:

abstract class Ingredient(val name: String)
class Stone : Ingredient("Kamień")
class Diamond : Ingredient("Diament")

data class Mission<out T : Ingredient>(val reward: T)

class Character<T : Ingredient> {
	val ingredients: MutableList<T> = mutableListOf()

	fun completeMission(mission: Mission<T>) {
		println("Gracz ukończył misję! Nagroda: ${mission.reward.name}")
		ingredients.add(mission.reward)
	}

	fun displayIngredients() {
		println("LISTA ZDOBYTYCH SKŁADNIKÓW")
		println("==========================")

		for (i in ingredients) {
			println(i.name)
		}

		println("==========================")
	}
}

Wyjątkowo więcej kodu, aczkolwiek bardziej sensownego przykładu nie byłem w stanie wynaleźć. Aby dać nieco różnorodności, pozwoliłem sobie skorzystać z klasy danych. Zwróćcie uwagę na to, że wewnątrz jej nawiasów kątowych, znajduje się "out". To "out" w języku Kotlin odgrywa główną rolę w tym przedstawieniu. W zaprezentowanym kodzie źródłowym, umożliwi wstawienie dowolnego typu nagrody dziedziczącej od naszego "składnika". Ponieważ właściwość zwraca typ T, kowariancja jest jak najbardziej dopuszczalna (na samym dole znajduje się pełny zestaw punktów kiedy kowariancja może być dozwolona, a kiedy nie).

A teraz popatrzcie na tworzenie obiektów:

val character = Character<Ingredient>()
val mission1 = Mission(Stone())
val mission2 = Mission(Diamond())

character.completeMission(mission1)
character.displayIngredients()
character.completeMission(mission2)
character.displayIngredients()

Po pierwsze, mamy naszą postać, która "zbiera" wszystkie typy składników. Wynika to jasno z jawnej definicji typu "Ingredient". Po drugie, nasze obie "misje" również wykorzystują typ generyczny i przypisane są typy kolejno "kamień" i "diament". Wynika to z automatycznego wydedukowania typu przez kompilator na podstawie typu wstawianej instancji. Nie to jest najważniejsze. Najważniejszy jest fakt, iż dzięki manipulowaniu typem składników w zmiennej gracza, możemy swobodnie dostosowywać jaki rodzaj może go interesować. Czy diamenty, żabki, muchomorki? To zależy od nas. Jak możecie się domyślić, modyfikując typ "zainteresowania" gracza na "Diamond", kompilator akceptowałby przyznawanie graczowi wyłącznie obiektów typu "diament", bądź jego klas pochodnych. A to z kolei powodowałoby odrzucenie pierwszego wywołania metody ukończenia misji i uniemożliwienie kompilacji z powodu niezgodności typów (nagrodą za misję jest kamień, ale gracza interesują diamenty oraz jej pochodne). Tak to działa!

Zostaje jeszcze pytanie jaki wpływ ma "out" w języku Kotlin w miejscu parametru typu "misji"? Ano taki, aby można było umożliwić wstawienie składnika, który podlega hierarchii naszej klasy abstrakcyjnej, "Ingredient" (klasa pochodna w miejsce klasy bazowej). Bez kowariancji, gracz akceptowałby jedynie obiekty typu "jaki sobie życzy", czyli jaki wstawiliśmy jawnie (w tym przykładzie "Ingredient"), ale ta klasa jest abstrakcyjna! Więc mielibyśmy de facto związane ręce.

Nie dziwcie się, jeśli przykład może nie do końca trzymać się kupy. Sens wykorzystania jest opcjonalny. Może to występować na wyrost, a może też uprzyjemnić pewne kwestie lub nawet przyczynić się do umożliwienia czegoś, bez czego program "nie może żyć" (jak w tym przypadku). Proszę Was również o to, abyście nie byli na siebie wściekli jeśli tego nie rozumiecie. Powtarzam jeszcze raz: typy generyczne stanowią jeden z tych bardziej zagmatwanych tematów oznaczonych jako "zaawansowane" i "trudne".

KIEDY KOWARIANCJA JEST MOŻLIWA?

Kowariancję za pomocą słowa "out" w języku Kotlin można zastosować wyłącznie w następujących okolicznościach:

  • klasa posiada funkcję zwracającą parametr typu T w postaci wartości wynikowej
  • klasa posiada właściwość typu "parametr typu T" utworzoną za pomocą słówka "val" ("var" nie przejdzie, gdyż posiada w sobie mutator)
  • klasa NIE zawiera w sobie funkcji przyjmującej parametr typu T za parametr formalny

Jeśli nie kapujecie powyższych punktów, spójrzcie na to w taki sposób: kowariancja nakazuje, aby parametr typu służył za wartość wyłącznie "wyjściową". Dlatego zwrot wartości "parametr typu T" przejdzie, a parametr formalny nie ("wprowadzenie" parametru). Dlatego "val" będzie akceptowane, a "var" już będzie "ble" ("val" nie pozwala na modyfikację referencji).

Słowo kluczowe "out" w języku Kotlin

Kowariancja typu sparametryzowanego polega na dopuszczeniu wstawiania obiektów będących klasą pochodną klasy parametru typu T, w miejsce T.

Na koniec poinformuję Was, że kowiariancja będąca tematem niniejszego artykułu, została zaimplementowana w kolekcjach występujących w standardowej implementacji Kotlina, takie jak lista, zbiór czy mapa. Dowodem niech będzie fakt, iż do kolekcji takiej jak ta:

val list: List<Any> = listOf(5, 6)

możecie wstawić obiekty typu "klasa pochodna", taka jak "Int". A "Any" to jest "klasa wszystkich klas", tak jak "Object" u Javy. Zatem, kolekcje korzystają z "out" w języku Kotlin.


Żyję nadzieją, że temat zostanie przez Was choć częściowo przyswojony. Artykuł kończy krótki, choć trudny wątek typów generycznych. Od kolejnego materiału, bierzemy się za lambdy!

PODOBNE ARTYKUŁY