Menu
CoddyTech

使用 in 和 out 的型变

本课属于 Coddy Kotlin 学习旅程的“面向对象编程”单元。第 40 课,共 57 课。

可以在需要 List<Any> 的地方传入 List<String>,因为 List 只会提供元素。不能传入 MutableList<String>:如果代码把它当作 MutableList<Any> 来处理,就可能向字符串列表中添加一个 Int:

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

在 main 中:

val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a"))   // 错误:MutableList<String> 不是 MutableList<Any>

输出:

String, String
[x, 42]

out T 会让泛型类具有协变性:它只会生成 T 值作为返回类型,不会接收它们。因此,Producer<Cat> 可以视为 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}"

在 main 中:

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

输出:

hello Tom

in T 会使其具有逆变性:它只会将 T 值作为参数接收。能够处理任何 Animal 的消费者,可以替代处理 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) }

在 main 中:

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

输出:

[fed Tom, fed Kit]

编译器会强制执行方向规则:out 类型参数不能作为方法参数的类型,而 in 类型参数不能作为返回值。一个简单的记法是:生产者用 out,消费者用 in。标准库也采用相同的规则:List<out E>、Comparator<in T>。如果不使用这两个关键字中的任何一个,类型参数就是不变型,例如 MutableList<E>,它既能读取也能写入:

class Producer<out T>(private val item: T) {
    fun get(): T = item                  // 没问题:T 用于输出
    // fun set(x: T) { }                 // 错误:T 被声明为 out
}
interface Feeder<in T> {
    fun feed(animal: T): String          // 没问题:T 用于输入
    // fun last(): T                     // 错误:T 被声明为 in
}

也可以在使用类型的地方指定型变。MutableList<in Int>接受任何能存储 Int 值的可变列表,例如 MutableList<Number>;而 MutableList<out Number>只允许读取:

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

在 main 中:

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

输出:

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

挑战

简单

两个接口用于传递项目:Source<out T>通过next()生成项目(为空时返回null),Sink<in T>则通过accept(item)接收项目。这两个接口已提供。请编写ListSource<T>(items),让它逐个返回列表中的项目,然后返回null;编写Collector<T>,将接收的项目保存在只读的items列表中;再编写Counter,这是一个会统计所有接收项目的Sink<Any>。最后,编写pipe(from, vararg to),将来源中的每个项目传递给每个接收器,并返回传递的项目数。

提供的代码会将一行单词传递给Collector<String>,将一行数字传递给Collector<Number>,并且两次都传递给同一个Counter。这得益于out和in:ListSource<Int>可以作为Source<Number>使用,而Sink<Any>可以作为Sink<String>使用。代码会输出计数、两个集合、counted: 7和after the end: null。

你的代码要写在ListSource.kt、Collector.kt、Counter.kt、Pipe.kt、Source.kt和Sink.kt中。Main.kt包含提供的输入和输出代码,不能编辑。

亲手试试

fun main() {
    // 提供的输入/输出代码:保持原样。
    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 icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Kotlin 在线编辑器