0Pricing
Kotlin Academy · レッスン

宣言側変性:inとout

共変(out)および反変(in)の型パラメータを正しく使用します。

「宣言側変性:inとout」はCoddyKit上の無料Kotlin Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはKotlin Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Kotlin Academyコースには全4レッスンが含まれています。

変性の問題

Kotlinでは、Listがout Tで宣言されているため、List<String>はList<Any>のサブタイプです。変性アノテーションがなければ、この関係は成立しません。

val strings: List<String> = listOf("a", "b")
val anys: List<Any> = strings  // OK because List<out T>
// MutableList<String> is NOT a subtype of MutableList<Any>:
// val m: MutableList<Any> = mutableListOf("x") // compile error
fun main() { println(anys) }

<code>out</code>による共変性

out Tは、そのクラスがTの値を生成(返却)することだけができ、消費することはできないという意味です。これにより、Producer<Dog>はProducer<Animal>のサブタイプになります。

interface Producer<out T> {
    fun produce(): T
}
class DogProducer : Producer<String> {
    override fun produce() = "Woof!"
}
fun sound(p: Producer<Any>) = println(p.produce())
fun main() {
    val dog = DogProducer()
    sound(dog)  // OK: Producer<String> is a subtype of Producer<Any>
}

<code>in</code>による反変性

in Tは、そのクラスがTの値を消費(パラメーターとして受け取る)することだけができ、生成することはできないという意味です。これにより、Consumer<Animal>はConsumer<Dog>のサブタイプになります。

interface Consumer<in T> {
    fun consume(item: T)
}
class Printer : Consumer<Any> {
    override fun consume(item: Any) = println(item)
}
fun feedDog(consumer: Consumer<String>) = consumer.consume("Dog")
fun main() {
    val printer = Printer()
    feedDog(printer)  // OK: Consumer<Any> is a subtype of Consumer<String>
}

<code>out</code>の制約:in位置では使用不可

out Tでは、Tをパラメーター(in)位置に記述できません。これはコンパイラーによって適用されます。

interface ReadOnly<out T> {
    fun get(): T          // OK: out position
    // fun set(t: T) {}  // Error: T in in-position violates out variance
}
class Box<out T>(private val value: T) : ReadOnly<T> {
    override fun get() = value
}
fun main() {
    val box: ReadOnly<Any> = Box<String>("hello")
    println(box.get())
}

<code>in</code>の制約:out位置では使用不可

in Tでは、Tを戻り値(out)位置に記述できません。

interface WriteOnly<in T> {
    fun set(t: T)        // OK: in position
    // fun get(): T {}  // Error: T in out-position violates in variance
}
class Sink<in T> : WriteOnly<T> {
    override fun set(t: T) = println("Received: $t")
}
fun main() {
    val sink: WriteOnly<String> = Sink<Any>()
    sink.set("hello")
}

Kotlin標準ライブラリの例

Comparable<in T>は反変です。Comparable<Number>は、任意のNumberのサブタイプを比較できます。List<out T>は共変です。猫のリストは動物のリストとして扱えます。

fun sortNumbers(list: List<Number>) = list.sortedWith(compareBy { it.toDouble() })
fun main() {
    val ints: List<Int> = listOf(3, 1, 2)
    val sorted = sortNumbers(ints)  // OK: List<Int> is List<Number>
    println(sorted)
}

不変性:MutableList

MutableList<T>は不変です。Tを生成することも消費することもできるため、異なるTの型の間にサブタイプ関係は成立しません。

fun addNumber(list: MutableList<Number>) { list.add(1.5) }
fun main() {
    val ints = mutableListOf<Int>(1, 2)
    // addNumber(ints) // compile error: MutableList<Int> is not MutableList<Number>
    val nums = mutableListOf<Number>(1, 2)
    addNumber(nums)   // OK
    println(nums)
}

代替手段としての使用箇所変性

クラスを変更できない場合は、使用箇所変性を使います。呼び出し側でoutを指定し、型を共変として投影します。

fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
    for (item in from) to.add(item)
}
fun main() {
    val src = mutableListOf("a", "b", "c")
    val dest = mutableListOf<Any>()
    copy(src, dest)
    println(dest)
}

実践:Repositoryパターン

読み取り専用のRepositoryには共変性を、書き込み専用のシンクには反変性を使い、データフローを適切にモデル化します。

interface Repository<out T> {
    fun findAll(): List<T>
    fun findById(id: Int): T?
}
interface Writer<in T> {
    fun save(entity: T)
}
interface ReadWriteRepo<T> : Repository<T>, Writer<T>
// ReadWriteRepo<User> is neither sub nor super of ReadWriteRepo<Admin>

変性の選択ガイド

Tの使われ方に基づいて変性を選択します。outはプロデューサー(Tだけを返す)、inはコンシューマー(Tだけを受け取る)、不変は両方(MutableList、Channel)を意味します。

// Quick mental model:
// out T: source/producer  — List, Flow, Sequence
// in  T: sink/consumer    — Comparable, Continuation
// invariant: read+write   — MutableList, Channel, MutableStateFlow
fun main() {
    val nums: List<Number> = listOf(1, 2, 3)  // List is out
    println(nums)
}

out関数を持つ不変クラス

不変クラスでも、特定の関数パラメーターに対して使用箇所でoutを指定できます。

class Stack<T>(private val items: MutableList<T> = mutableListOf()) {
    fun push(item: T) = items.add(item)
    fun pop(): T? = items.removeLastOrNull()
}
fun printAll(stack: Stack<out Any>) {
    // can only read, not write
    println(stack.pop())
}
fun main() {
    val s = Stack<String>()
    s.push("hello")
    printAll(s)
}

クイックチェック

型パラメーターのout Tは何を意味しますか。

まとめ

out T(共変性)は、値を生成するときのサブタイピングを可能にします。in T(反変性)は、値を消費するときのサブタイピングを可能にします。不変型(読み取りと書き込みの両方を行う型)には、異なるTの間でサブタイプ関係がありません。

よくある質問

「宣言側変性:inとout」レッスンは無料ですか?

はい。「宣言側変性:inとout」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Kotlin Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Kotlin Academyコースには全4レッスンが含まれています。

「宣言側変性:inとout」で何を学びますか?

共変(out)および反変(in)の型パラメータを正しく使用します。 ブラウザで直接実行するハンズオンコードでKotlin Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Kotlin Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのKotlin Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「宣言側変性:inとout」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このKotlin Academyレッスンでコードを書いて実行できますか?

はい。すべてのKotlin Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. whereによるジェネリック関数と型制約
  2. 宣言側変性:inとout
  3. Star Projectionと*の使いどころ
  4. 型消去とreified型パラメータ
← Kotlin Academyに戻る