Kowariancja i kontrawariancja: in i out
Część sekcji Programowanie obiektowe ścieżki Kotlin w Coddy. Lekcja 40 z 57.
W miejsce oczekiwanego List<Any> można przekazać List<String>, ponieważ List tylko udostępnia elementy. Nie można tak zrobić z MutableList<String>: kod, który widzi ją jako MutableList<Any>, mógłby dodać Int do listy ciągów znaków:
fun describeAll(items: List<Any>) = items.joinToString { it::class.simpleName ?: "?" }
fun addNumber(items: MutableList<Any>) { items.add(42) }Wewnątrz main:
val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a")) // błąd: MutableList<String> nie jest MutableList<Any>Wynik:
String, String
[x, 42]out T sprawia, że klasa generyczna jest kowariantna: tylko wytwarza wartości T jako typy zwracane i nigdy ich nie przyjmuje. Wtedy Producer<Cat> jest uznawane za Producer<Animal>:
open class Animal(val name: String)
class Cat(name: String) : Animal(name)
class Producer<out T>(private val item: T) {
fun get(): T = item
}
fun greet(p: Producer<Animal>) = "hello ${p.get().name}"Wewnątrz main:
val cats: Producer<Cat> = Producer(Cat("Tom"))
println(greet(cats))Wynik:
hello Tomin T sprawia, że typ jest kontrawariantny: przyjmuje tylko wartości T jako parametry. Konsument obsługujący dowolne Animal może zastąpić konsumenta typu Cat:
open class Animal(val name: String)
class Cat(name: String) : Animal(name)
interface Feeder<in T> {
fun feed(animal: T): String
}
class AnimalFeeder : Feeder<Animal> {
override fun feed(animal: Animal) = "fed ${animal.name}"
}
fun feedCats(cats: List<Cat>, feeder: Feeder<Cat>) = cats.map { feeder.feed(it) }Wewnątrz main:
println(feedCats(listOf(Cat("Tom"), Cat("Kit")), AnimalFeeder()))Wynik:
[fed Tom, fed Kit]Kompilator wymusza ten kierunek: parametr typu out nie może być typem parametru metody, a parametr typu in nie może być zwracany. Praktyczna zasada to: producent — out, konsument — in. Biblioteka standardowa działa tak samo: List<out E>, Comparator<in T>. Jeśli nie użyjesz żadnego z tych słów kluczowych, parametr typu jest niezmienniczy, jak w przypadku MutableList<E>, która umożliwia zarówno odczyt, jak i zapis:
class Producer<out T>(private val item: T) {
fun get(): T = item // poprawnie: T wychodzi
// fun set(x: T) { } // błąd: T jest zadeklarowany jako out
}
interface Feeder<in T> {
fun feed(animal: T): String // poprawnie: T wchodzi
// fun last(): T // błąd: T jest zadeklarowany jako in
}Kowariancję można też zapisać w miejscu użycia typu. MutableList<in Int> akceptuje dowolną mutowalną listę, do której można dodawać wartości Int, na przykład MutableList<Number>, a MutableList<out Number> pozwala tylko na odczyt:
fun fillWith(target: MutableList<in Int>, n: Int) {
for (i in 1..n) target.add(i)
}
fun total(source: MutableList<out Number>) = source.sumOf { it.toDouble() }Wewnątrz main:
val numbers = mutableListOf<Number>(0.5)
fillWith(numbers, 3)
println(numbers)
println(total(mutableListOf(1, 2, 3)))Wynik:
[0.5, 1, 2, 3]
6.0Wyzwanie
ŁatwyDwa interfejsy służą do przekazywania elementów: Source<out T> udostępnia elementy za pomocą next() (zwracając null, gdy jest pusty), a Sink<in T> je przyjmuje za pomocą accept(item). Są one dostarczone. Napisz ListSource<T>(items), która zwraca elementy listy jeden po drugim, a następnie null; Collector<T>, który przechowuje przyjęte elementy na liście tylko do odczytu items; oraz Counter, czyli Sink<Any>, który zlicza wszystkie przyjęte elementy. Następnie napisz pipe(from, vararg to), która przenosi każdy element źródła do każdego ujścia i zwraca liczbę przeniesionych elementów.
Dostarczony kod przekazuje wiersz słów do Collector<String>, a wiersz liczb do Collector<Number>, za każdym razem również do jednego Counter. Działa to dzięki out i in: ListSource<Int> jest typem Source<Number>, a Sink<Any> jest typem Sink<String>. Kod wypisuje liczniki, obie kolekcje, counted: 7 i after the end: null.
Twój kod umieść w plikach ListSource.kt, Collector.kt, Counter.kt, Pipe.kt, Source.kt i Sink.kt. Main.kt zawiera dostarczony kod wejścia/wyjścia i nie można go edytować.
Spróbuj swoich sił
fun main() {
// Dostarczony kod wejścia/wyjścia: pozostaw go bez zmian
val input = generateSequence(::readLine).toList()
val words = ListSource(input[0].split(" "))
val ints = ListSource(input[1].split(" ").map { it.toInt() })
val counter = Counter()
val allWords = Collector<String>()
val numbers = Collector<Number>()
println("words moved: " + pipe(words, allWords, counter))
println("numbers moved: " + pipe(ints, numbers, counter))
println(allWords.items)
println(numbers.items)
println("counted: ${counter.count}")
println("after the end: ${words.next()}")
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Klasy i obiekty
Praca z plikamiDefiniowanie klasKonstruktory główneBloki init i walidacjaKonstruktory dodatkowePodsumowanie — karta biblioteczna4Dziedziczenie
Klasy otwarte i podklasyNadpisywanie składowychsuper i konstruktoryKlasy abstrakcyjnePodsumowanie – katalog pracowników7Klasy specjalne
Klasy danychKlasy wyliczenioweHierarchie zapieczętowaneequals, hashCode, toStringPodsumowanie — śledzenie zamówień10Wzorce projektowe, część 2
Delegowanie za pomocą byWzorzec dekoratoraWzorzec budowniczegoWzorzec stanu2Właściwości i hermetyzacja
Własne akcesoryModyfikatory widocznościPrywatne setterylazy i lateinitPowtórka – konto bankowe5Interfejsy
Deklarowanie interfejsówMetody domyślneWiele interfejsówInterfejsy jako typyPowtórzenie – metody płatności8Typy generyczne
Funkcje generyczneKlasy generyczneOgraniczenia typówKowariancja i kontrawariancja: in i outPodsumowanie — kolejka priorytetowa11Projekt: Zarządzanie biblioteką
Książki i czytelnicyWypożyczanie książekPoćwicz samodzielnie: Edytor online Kotlin