Upgrade to Pro — share decks privately, control downloads, hide ads and more …

初学者向け「Kotlinでジェネリクスを学ぼう」

Avatar for Taro Nagasawa Taro Nagasawa
February 27, 2018

 初学者向け「Kotlinでジェネリクスを学ぼう」

Avatar for Taro Nagasawa

Taro Nagasawa

February 27, 2018
Tweet

More Decks by Taro Nagasawa

Other Decks in Programming

Transcript

  1. 単純なコンテナを定義してみよう class Box(val value: Any) Any String Int Number CharSequence

    Anyは、あらゆる型の スーパタイプ ※ただしNullableは除く
  2. Boxクラスを使ってみる val box1: Box = Box("Hello") val box2: Box =

    Box(3) repeat(box2.value as Int) { val message: String = box1.value as String println(message.toUpperCase()) }
  3. Boxクラスを使ってみる val box1: Box = Box("Hello") val box2: Box =

    Box(3) repeat(box2.value as Int) { val message: String = box1.value as String println(message.toUpperCase()) } イイ感じにオブジェクトをラップできている!
  4. Boxクラスを使ってみる val box1: Box = Box("Hello") val box2: Box =

    Box(3) repeat(box2.value as Int) { val message: String = box1.value as String println(message.toUpperCase()) } 取り出し(アンラップ)はキャストが要る
  5. Boxクラスを使ってみる val box1: Box = Box("Hello") val box2: Box =

    Box(3) repeat(box2.value as Int) { val message: String = box1.value as String println(message.toUpperCase()) } 取り出し(アンラップ)はキャストが要る キャストは 危険!
  6. クラス ≠ 型 • 1つのクラスにつき、2つの型がある場合の例 ◦ Stringクラス ◦ String型とString?型 •

    1つのクラスにつき、無数の型がある場合の例 ◦ (先ほど独自に定義した)Boxクラス ◦ Box<Int>型、Box<String>型、Box<Box<Int>>型.........
  7. 例えば、変更可能なコンテナを考える class MutableBox<T>(var value: T) val box1: MutableBox<Int> = MutableBox(123)

    val box2: MutableBox<out Number> = box1 box2.value = 0.5 この操作は安全か?
  8. 例えば、変更可能なコンテナを考える class MutableBox<T>(var value: T) val box1: MutableBox<Int> = MutableBox(123)

    val box2: MutableBox<out Number> = box1 box2.value = 0.5 // コンパイルエラー • 実体がIntなのでDoubleの代入は危険 ◦ 禁止すべき操作(Javaの配列では可能) • 実際にはコンパイルエラーとなる → setterが削除されている → 型プロジェクションにより「ある側面を隠した」
  9. 反変(contravariant) • 型パラメータと逆のサブタイピング関係が成り立つ • inキーワードを用いる fun setDefault(box: MutableBox<in Int>) {

    box.value = 0 } val box: MutableBox<Number> = MutableBox(NaN) setDefault(box) println(box.value) // 0 Int Number Box<Int> Box<out Number>
  10. 不変・共変・反変 まとめ キーワード サブタイピング 可能な 操作 不変 invariant (なし) 入出力

    共変 covariant out 出力 反変 contravariant in 入力 型Aが型Bのサブタイプであるとき... Box<A> Box<B> Box<A> Box<out B> Box<A> Box<in B>
  11. 型プロジェクション(2回目) • 型の射影 • キーワード out や in を使う •

    ジェネリック型を使う際に指定するので 「使用場所変位指定」と言うこともある val box1: Box<Int> = Box(123) val box2: Box<out Number> = box1
  12. 型プロジェクション(2回目) • 型の射影 • キーワード out や in を使う •

    ジェネリック型を使う際に指定するので 「使用場所変位指定」と言うこともある val box1: Box<Int> = Box(123) val box2: Box<out Number> = box1 型プロジェクションにより「入力」すなわち「変更」 が禁止される。 そもそもBoxクラスはイミュータブルなので 変更できないのは自明。このout宣言は冗長な のでは?
  13. ジェネリック制約 • 型引数として指定できる型に制約を設けることが可能 • 制約とは、具体的には上限境界 class NumberBox<out T: Number>(val value:

    T) { fun toInt(): NumberBox<Int> = NumberBox(value.toInt()) } val box1: NumberBox<String> = NumberBox("") // NG val box2: NumberBox<Float> = NumberBox(1.2f) val box3: NumberBox<Int> = box2.toInt()
  14. 複数の上限境界 • 複数の上限境界を指定するにはwhereキーワードを使う interface WithName { val name: String }

    interface Greeter { fun greet(): String } class Person(override val name: String): WithName, Greeter { override fun greet(): String = "Hello" } fun <T> introduceMyself(t: T): String where T: WithName, T: Greeter { return "${t.greet()}, I am ${t.name}!" }
  15. 複数の上限境界 • 複数の上限境界を指定するにはwhereキーワードを使う interface WithName { val name: String }

    interface Greeter { fun greet(): String } class Person(override val name: String): WithName, Greeter { override fun greet(): String = "Hello" } fun <T> introduceMyself(t: T): String where T: WithName, T: Greeter { return "${t.greet()}, I am ${t.name}!" } ジェネリック関数
  16. 型消去 Kotlin Java風 コンパイル val box: Box<String> = Box("Hello") val

    value: String = box.value Box box = new Box("Hello"); String value = (String) box.getValue();
  17. val box: Box<String> = Box("Hello") val value: String = box.value

    Box box = new Box("Hello"); String value = (String) box.getValue(); 型消去 Kotlin Java風 コンパイル いわゆる「raw型」=ジェネリクス無視
  18. 型消去 val box: Box<String> = Box("Hello") val value: String =

    box.value Box box = new Box("Hello"); String value = (String) box.getValue(); Kotlin Java風 コンパイル 型引数の情報が失われているので、キャストが必要
  19. 型消去 Kotlin Java風 コンパイル val box: Box<String> = Box("Hello") val

    value: String = box.value Box box = new Box("Hello"); String value = (String) box.getValue(); コンパイルすると型引数が消える! コンパイルの時だけに使用される情報、すなわち型安全性の面のみで活 用される。
  20. 型チェック val myObject: Any = Box<Int>(123) if (myObject is Box<Int>)

    { ... } val myObject: Any = Box<Int>(123) if (myObject is Box<*>) { ... } NG OK
  21. 型チェック val myObject: Any = Box<Int>(123) if (myObject is Box<Int>)

    { ... } val myObject: Any = Box<Int>(123) if (myObject is Box<*>) { ... } NG OK 型消去により、実行時に判断がつかない
  22. 型チェック val myObject: Any = Box<Int>(123) if (myObject is Box<Int>)

    { ... } val myObject: Any = Box<Int>(123) if (myObject is Box<*>) { ... } NG OK スタープロジェクションを使えばOK
  23. スタープロジェクション • 型が決まっているが、不明なとき or 興味がないときに使用す る • <out Any?> AND

    <in Nothing>的に振る舞う val list: MutableList<*> = mutableListOf<Int>() val first: Any? = list.get(0) // Any?として生産 list.add(123) // コンパイルエラー, Nothingとして消費 Any? あらゆる型のスーパタイプ Nothing あらゆる型のサブタイプ
  24. 具象型(reified type)パラメータ付き関数 inline fun <reified T> Any.isA(): Boolean = this

    is T 5.isA<Number>() インライン関数 実行時にも残る型パラメータ
  25. まとめ • 任意の型に対して汎用的に安全なコード部品化を実現する →ジェネリクス • クラスや関数は、型パラメータを宣言することができる • 型引数を指定することで、それが型パラメータを置き換える • ジェネリック型を安全かつ柔軟に扱うために変位と呼ばれる性質を

    生かす(不変、共変、反変) • 型プロジェクションや宣言場所変位指定により、変位の指定をする ことができる • 型引数に制約を設けることができる • 型引数の情報はコンパイル時に消える→型消去 • 具象型パラメータ付き関数の中では、型消去が起こっていないよう に見える