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

14c9795d267f5b85abb98ca5e8780646?s=47 Taro Nagasawa
February 27, 2018

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

14c9795d267f5b85abb98ca5e8780646?s=128

Taro Nagasawa

February 27, 2018
Tweet

Transcript

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

  2. 長澤 太郎 • エムスリー株式会社 • 日本Kotlinユーザグループ代表

  3. もくじ 1. ジェネリクスとは 2. 変位とは 3. 変位の指定 4. ジェネリック制約 5.

    型消去とreified型
  4. 1. ジェネリクスとは 2. 変位とは 3. 変位の指定 4. ジェネリック制約 5. 型消去とreified型

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

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

    Anyは、あらゆる型の スーパタイプ ※ただしNullableは除く
  7. 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()) }
  8. 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()) } イイ感じにオブジェクトをラップできている!
  9. 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()) } 取り出し(アンラップ)はキャストが要る
  10. 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()) } 取り出し(アンラップ)はキャストが要る キャストは 危険!
  11. あらゆる型に対応させたい=固定の型で定義したくない V.S. キャストしたくない=Anyを使いたくない

  12. あらゆる型に対応させたい=固定の型で定義したくない V.S. キャストしたくない=Anyを使いたくない ジェネリクス

  13. ジェネリッククラス class Box<T>(val value: T) • 型パラメータが宣言されているクラス • 型パラメータ=仮の型。名前を付けられるが、大文字1字が慣 習となっている。

  14. ジェネリッククラスのインスタンス生成 • 型引数を指定する ◦ 型パラメータが指定の型で置き換わるイメージ ◦ 型推論により省略できる場合が多い val box1: Box<String>

    = Box<String>("Hello") val box2: Box<Int> = Box<Int>(3) repeat(box2.value) { val message: String = box1.value println(message.toUpperCase()) }
  15. 型パラメータ?型引数? 仮の宣言 実際の指定 関数 仮引数 parameter 実引数 argument ジェネリック クラス

    型パラメータ type parameter 型引数 type argument
  16. 1. ジェネリクスとは 2. 変位とは 3. 変位の指定 4. ジェネリック制約 5. 型消去とreified型

  17. 変位とは • variance: 「変位」や「分散」などと訳される • ジェネリック型において、サブタイピングの関係を記述する • 次の3種類がある ◦ 不変

    ◦ 共変 ◦ 反変
  18. クラス ≠ 型 • 1つのクラスにつき、2つの型がある場合の例 ◦ Stringクラス ◦ String型とString?型 •

    1つのクラスにつき、無数の型がある場合の例 ◦ (先ほど独自に定義した)Boxクラス ◦ Box<Int>型、Box<String>型、Box<Box<Int>>型.........
  19. 不変(invariant) • 非変とも。不変(immutable)との混同をおそれて • サブタイプの関係が成り立たない • デフォルト(特に指定がない場合は不変となる) val box1: Box<Int>

    = Box(123) val box2: Box<Number> = box1 // コンパイルエラー Int Number Box<Int> Box<Number>
  20. 不変だと扱いづらい場合がある // Box<Number>をIntに変換する関数 fun toInt(box: Box<Number>): Int = box.value.toInt() val

    floatBox: Box<Float> = Box(12.3f) val int: Int = toInt(floatBox) // NG
  21. 共変(covariant) • 型パラメータと同じサブタイピング関係が成り立つ • 型プロジェクションにおいてoutキーワードを用いる val box1: Box<Int> = Box(123)

    val box2: Box<out Number> = box1 // OK Int Number Box<Int> Box<out Number>
  22. 型プロジェクション • 型の射影(projection) • RDBにおける射影とは、カラムの選択 → レコードのある側面にだけ注目している • 型のある側面にだけ注目することで、サブタインピングの関係 を変更できる

    →逆に言うと、ある側面を隠すということ
  23. 例えば、変更可能なコンテナを考える class MutableBox<T>(var value: T) val box1: MutableBox<Int> = MutableBox(123)

    val box2: MutableBox<out Number> = box1 box2.value = 0.5
  24. 例えば、変更可能なコンテナを考える class MutableBox<T>(var value: T) val box1: MutableBox<Int> = MutableBox(123)

    val box2: MutableBox<out Number> = box1 box2.value = 0.5 この操作は安全か?
  25. 例えば、変更可能なコンテナを考える 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が削除されている → 型プロジェクションにより「ある側面を隠した」
  26. 反変(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>
  27. 不変・共変・反変 まとめ キーワード サブタイピング 可能な 操作 不変 invariant (なし) 入出力

    共変 covariant out 出力 反変 contravariant in 入力 型Aが型Bのサブタイプであるとき... Box<A> Box<B> Box<A> Box<out B> Box<A> Box<in B>
  28. 1. ジェネリクスとは 2. 変位とは 3. 変位の指定 4. ジェネリック制約 5. 型消去とreified型

  29. 型プロジェクション(2回目) • 型の射影 • キーワード out や in を使う •

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

    ジェネリック型を使う際に指定するので 「使用場所変位指定」と言うこともある val box1: Box<Int> = Box(123) val box2: Box<out Number> = box1 型プロジェクションにより「入力」すなわち「変更」 が禁止される。 そもそもBoxクラスはイミュータブルなので 変更できないのは自明。このout宣言は冗長な のでは?
  31. 宣言場所変位指定 • クラスやインタフェースにおいて、型パラメータを宣言する場所 で変位を指定することができる • outやinキーワードを使用する • 型プロジェクションは、自動的に危険な操作を隠してくれるが、 宣言場所変位指定では、指定した変位に対して、危険な操作 を公開するとコンパイルエラーとなる

    class Box<out T>(val value: T) val box1: Box<Int> = Box(123) val box2: Box<Number> = box1
  32. 1. ジェネリクスとは 2. 変位とは 3. 変位の指定 4. ジェネリック制約 5. 型消去とreified型

  33. ジェネリック制約 • 型引数として指定できる型に制約を設けることが可能 • 制約とは、具体的には上限境界 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()
  34. 複数の上限境界 • 複数の上限境界を指定するには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}!" }
  35. 複数の上限境界 • 複数の上限境界を指定するには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}!" } ジェネリック関数
  36. 1. ジェネリクスとは 2. 変位とは 3. 変位の指定 4. ジェネリック制約 5. 型消去とreified型

  37. 型消去 Kotlin Java風 コンパイル val box: Box<String> = Box("Hello") val

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

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

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

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

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

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

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

    <in Nothing>的に振る舞う val list: MutableList<*> = mutableListOf<Int>() val first: Any? = list.get(0) // Any?として生産 list.add(123) // コンパイルエラー, Nothingとして消費 Any? あらゆる型のスーパタイプ Nothing あらゆる型のサブタイプ
  45. 型消去が不便なところ fun <T> Any.isA(clazz: Class<T>): Boolean = clazz.isInstance(this) 5.isA(Number::class.java)

  46. 型消去が不便なところ fun <T> Any.isA(clazz: Class<T>): Boolean = clazz.isInstance(this) 5.isA(Number::class.java) 5.isA<Number>()

    こう書きたくない?
  47. 具象型(reified type)パラメータ付き関数 inline fun <reified T> Any.isA(): Boolean = this

    is T 5.isA<Number>()
  48. 具象型(reified type)パラメータ付き関数 inline fun <reified T> Any.isA(): Boolean = this

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

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