Menu
Coddy logo textTech

in と out による変性

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 40/57。

List<String> は要素を取り出すだけなので、List<Any> が期待される場所に渡すことができます。一方、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

チャレンジ

簡単

2つのインターフェースが項目を受け渡します。Source<out T> は next() で項目を生成し、空の場合は null を返します。一方、Sink<in T> は accept(item) で項目を受け取ります。これらは提供済みです。リストの項目を1つずつ返し、その後に null を返す ListSource<T>(items)、受け取った項目を読み取り専用の items リストに保持する Collector<T>、そして受け取ったすべてのものを数える Sink<Any> である Counter を作成してください。続いて、ソースのすべての項目をすべてのシンクに移し、移した項目数を返す pipe(from, vararg to) を作成してください。

提供されたコードは、単語の行を Collector<String> に、数値の行を Collector<Number> に渡し、どちらの場合もさらに1つの 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プレイグラウンド