Menu
Coddy logo textTech

Variance avec in et out

Fait partie de la section Programmation orientée objet du Journey Kotlin de Coddy. Leçon 40 sur 57.

Un List<String> peut être transmis là où un List<Any> est attendu, car un List ne fait que fournir des éléments. Un MutableList<String> ne le peut pas : le code qui le considérerait comme un MutableList<Any> pourrait ajouter un Int à une liste de chaînes :

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

Dans main :

val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a"))   // erreur : MutableList<String> n'est pas un MutableList<Any>

Sortie :

String, String
[x, 42]

out T rend une classe générique covariante : elle produit uniquement des valeurs de type T, en tant que types de retour, et n'en reçoit jamais. Ainsi, un Producer<Cat> est considéré comme 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}"

Dans main :

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

Sortie :

hello Tom

in T la rend contravariante : elle ne fait que consommer des valeurs de T, en tant que paramètres. Un consommateur capable de gérer n’importe quel Animal peut remplacer un consommateur 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) }

À l’intérieur de main :

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

Sortie :

[fed Tom, fed Kit]

Le compilateur impose la direction : un paramètre de type out ne peut pas être le type d’un paramètre de méthode, et un paramètre in ne peut pas être renvoyé. La règle générale est producteur out, consommateur in. La bibliothèque standard fait de même : List<out E>, Comparator<in T>. Sans aucun de ces mots-clés, un paramètre de type est invariant, comme dans MutableList<E>, qui permet à la fois de lire et d’écrire :

class Producer<out T>(private val item: T) {
    fun get(): T = item                  // OK : T sort
    // fun set(x: T) { }                 // erreur : T est déclaré out
}
interface Feeder<in T> {
    fun feed(animal: T): String          // OK : T entre
    // fun last(): T                     // erreur : T est déclaré in
}

La variance peut également s’écrire là où un type est utilisé. MutableList<in Int> accepte toute liste mutable pouvant recevoir des valeurs Int, comme une MutableList<Number>, tandis que MutableList<out Number> autorise uniquement la lecture :

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

Dans main :

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

Sortie :

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

Défi

Facile

Deux interfaces déplacent des éléments : Source<out T> produit des éléments avec next() (en renvoyant null lorsqu'il est vide) et Sink<in T> les consomme avec accept(item). Elles sont fournies. Écrivez ListSource<T>(items), qui renvoie les éléments de la liste un par un, puis null ; Collector<T>, qui conserve les éléments acceptés dans une liste items en lecture seule ; et Counter, un Sink<Any> qui compte tout ce qu'il accepte. Écrivez ensuite pipe(from, vararg to), qui déplace chaque élément de la source vers chaque puits et renvoie le nombre d'éléments déplacés.

Le code fourni achemine une ligne de mots vers un Collector<String> et une ligne de nombres vers un Collector<Number>, dans les deux cas également vers un même Counter. Cela fonctionne grâce à out et in : un ListSource<Int> est un Source<Number>, et un Sink<Any> est un Sink<String>. Il affiche les nombres, les deux collections, counted: 7 et after the end: null.

Votre code doit être placé dans ListSource.kt, Collector.kt, Counter.kt, Pipe.kt, Source.kt et Sink.kt. Main.kt contient le code d'entrée/sortie fourni et ne peut pas être modifié.

Essayez vous-même

fun main() {
    // Code d'entrée/sortie fourni : le garder tel quel
    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 iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation orientée objet

Entraînez-vous par vous-même : Playground Kotlin