Menu
Coddy logo textTech

Varianza con in e out

Fa parte della sezione Programmazione orientata agli oggetti del percorso Kotlin di Coddy. Lezione 40 di 57.

Un List<String> può essere passato dove è atteso un List<Any>, perché un List fornisce solo gli elementi. Un MutableList<String> non può esserlo: il codice che lo vede come MutableList<Any> potrebbe aggiungere un Int a una lista di stringhe:

fun describeAll(items: List<Any>) = items.joinToString { it::class.simpleName ?: "?" }
fun addNumber(items: MutableList<Any>) { items.add(42) }

All'interno di main:

val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a"))   // error: MutableList<String> is not a MutableList<Any>

Output:

String, String
[x, 42]

out T rende una classe generica covariante: produce solo valori di T, come tipi restituiti, e non li accetta mai. Quindi un Producer<Cat> è considerato un 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}"

All'interno di main:

val cats: Producer<Cat> = Producer(Cat("Tom"))
println(greet(cats))

Output:

hello Tom

in T la rende controvariante: consuma solo valori di T, come parametri. Un consumatore che gestisce qualsiasi Animal può sostituire un consumatore di 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) }

All'interno di main:

println(feedCats(listOf(Cat("Tom"), Cat("Kit")), AnimalFeeder()))

Output:

[fed Tom, fed Kit]

Il compilatore impone la direzione: un parametro di tipo out non può essere il tipo di un parametro di metodo e uno in non può essere restituito. La regola pratica è produttore out, consumatore in. La libreria standard fa lo stesso: List<out E>, Comparator<in T>. Senza nessuna delle due parole chiave, un parametro di tipo è invariante, come in MutableList<E>, che legge e scrive:

class Producer<out T>(private val item: T) {
    fun get(): T = item                  // corretto: T è in uscita
    // fun set(x: T) { }                 // errore: T è dichiarato out
}
interface Feeder<in T> {
    fun feed(animal: T): String          // corretto: T è in ingresso
    // fun last(): T                     // errore: T è dichiarato in
}

La varianza può anche essere scritta nel punto in cui viene usato un tipo. MutableList<in Int> accetta qualsiasi lista mutabile che possa contenere valori Int, come una MutableList<Number>, mentre MutableList<out Number> consente solo la lettura:

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() }

All'interno di main:

val numbers = mutableListOf<Number>(0.5)
fillWith(numbers, 3)
println(numbers)
println(total(mutableListOf(1, 2, 3)))

Output:

[0.5, 1, 2, 3]
6.0
challenge icon

Sfida

Facile

Due interfacce spostano gli elementi: Source<out T> produce elementi con next() (restituendo null quando è vuota) e Sink<in T> li consuma con accept(item). Sono fornite. Scrivi ListSource<T>(items), che restituisce gli elementi della lista uno alla volta e poi null; Collector<T>, che conserva gli elementi accettati in una lista items di sola lettura; e Counter, un Sink<Any> che conta tutto ciò che accetta. Poi scrivi pipe(from, vararg to), che sposta ogni elemento della sorgente in ogni sink e restituisce quanti elementi ha spostato.

Il codice fornito invia una riga di parole a un Collector<String> e una riga di numeri a un Collector<Number>, in entrambi i casi anche a un unico Counter. Funziona grazie a out e in: un ListSource<Int> è un Source<Number> e un Sink<Any> è un Sink<String>. Stampa i conteggi, entrambe le raccolte, counted: 7 e after the end: null.

Il tuo codice va inserito in ListSource.kt, Collector.kt, Counter.kt, Pipe.kt, Source.kt e Sink.kt. Main.kt contiene il codice di input/output fornito e non può essere modificato.

Provalo tu

fun main() {
    // Codice di input/output fornito: lascialo così com'è
    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()}")
}
quiz iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Programmazione orientata agli oggetti

Esercitati da solo: Playground Kotlin