Variancia con in y out
Parte de la sección Programación orientada a objetos del Journey de Kotlin de Coddy. Lección 40 de 57.
Se puede pasar un List<String> donde se espera un List<Any>, porque un List solo proporciona elementos. Un MutableList<String> no puede: el código que lo ve como MutableList<Any> podría añadir un Int a una lista de cadenas:
fun describeAll(items: List<Any>) = items.joinToString { it::class.simpleName ?: "?" }
fun addNumber(items: MutableList<Any>) { items.add(42) }Dentro de main:
val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a")) // error: MutableList<String> no es un MutableList<Any>Salida:
String, String
[x, 42]out T hace que una clase genérica sea covariante: solo produce valores de T, como tipos de retorno, y nunca los recibe. Después, un Producer<Cat> cuenta como 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}"Dentro de main:
val cats: Producer<Cat> = Producer(Cat("Tom"))
println(greet(cats))Salida:
hello Tomin T lo hace contravariante: solo consume valores de T, como parámetros. Un consumidor que maneja cualquier Animal puede sustituir a un consumidor de 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) }Dentro de main:
println(feedCats(listOf(Cat("Tom"), Cat("Kit")), AnimalFeeder()))Salida:
[fed Tom, fed Kit]El compilador impone la dirección: un parámetro de tipo out no puede ser el tipo de un parámetro de método, y uno in no puede devolverse. La regla práctica es productor out, consumidor in. La biblioteca estándar hace lo mismo: List<out E>, Comparator<in T>. Sin ninguna de las dos palabras clave, un parámetro de tipo es invariante, como en MutableList<E>, que tanto lee como escribe:
class Producer<out T>(private val item: T) {
fun get(): T = item // bien: T sale
// fun set(x: T) { } // error: T está declarado out
}
interface Feeder<in T> {
fun feed(animal: T): String // bien: T entra
// fun last(): T // error: T está declarado in
}La varianza también puede escribirse donde se utiliza un tipo. MutableList<in Int> acepta cualquier lista mutable que pueda recibir valores Int, como una MutableList<Number>, y MutableList<out Number> solo permite la lectura:
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() }Dentro de main:
val numbers = mutableListOf<Number>(0.5)
fillWith(numbers, 3)
println(numbers)
println(total(mutableListOf(1, 2, 3)))Salida:
[0.5, 1, 2, 3]
6.0Desafío
FácilDos interfaces mueven elementos: Source<out T> produce elementos con next() (devuelve null cuando está vacío) y Sink<in T> los consume con accept(item). Se proporcionan. Escribe ListSource<T>(items), que devuelve los elementos de la lista uno por uno y después null; Collector<T>, que conserva los elementos aceptados en una lista de solo lectura items; y Counter, un Sink<Any> que cuenta todo lo que acepta. Después escribe pipe(from, vararg to), que mueve cada elemento del origen a cada destino y devuelve cuántos elementos ha movido.
El código proporcionado canaliza una línea de palabras hacia un Collector<String> y una línea de números hacia un Collector<Number>, y en ambas ocasiones también hacia un Counter. Esto funciona gracias a out y in: un ListSource<Int> es un Source<Number>, y un Sink<Any> es un Sink<String>. Imprime los conteos, ambas colecciones, counted: 7 y after the end: null.
Tu código va en ListSource.kt, Collector.kt, Counter.kt, Pipe.kt, Source.kt y Sink.kt. Main.kt contiene el código de entrada/salida proporcionado y no se puede editar.
Pruébalo tú mismo
fun main() {
// Código de entrada/salida suministrado: déjalo tal como está
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()}")
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación orientada a objetos
1Clases y objetos
Trabajar con archivosDefinir clasesConstructores primariosBloques init y validaciónConstructores secundariosRepaso - Tarjeta de biblioteca4Herencia
Clases abiertas y subclasesSobrescritura de miembrossuper y constructoresClases abstractasRepaso: directorio del personal2Propiedades y encapsulación
Accesores personalizadosModificadores de visibilidadSetters privadoslazy y lateinitRepaso: cuenta bancaria5Interfaces
Declaración de interfacesMétodos predeterminadosInterfaces múltiplesInterfaces como tiposRepaso: métodos de pago8Genéricos
Funciones genéricasClases genéricasRestricciones de tiposVariancia con in y outRepaso - Cola de prioridad11Proyecto: Gestión de biblioteca
Libros y miembrosPréstamo de librosPractica por tu cuenta: Playground de Kotlin