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

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

Taro Nagasawa
February 27, 2018

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

Taro Nagasawa

February 27, 2018
Tweet

More Decks by Taro Nagasawa

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. 単純なコンテナを定義してみよう
    class Box(val value: Any)
    Any
    String Int
    Number
    CharSequence
    Anyは、あらゆる型の
    スーパタイプ
    ※ただしNullableは除く

    View Slide

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

    View Slide

  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())
    }
    イイ感じにオブジェクトをラップできている!

    View Slide

  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())
    }
    取り出し(アンラップ)はキャストが要る

    View Slide

  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())
    }
    取り出し(アンラップ)はキャストが要る
    キャストは
    危険!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  14. ジェネリッククラスのインスタンス生成
    ● 型引数を指定する
    ○ 型パラメータが指定の型で置き換わるイメージ
    ○ 型推論により省略できる場合が多い
    val box1: Box = Box("Hello")
    val box2: Box = Box(3)
    repeat(box2.value) {
    val message: String = box1.value
    println(message.toUpperCase())
    }

    View Slide

  15. 型パラメータ?型引数?
    仮の宣言 実際の指定
    関数
    仮引数
    parameter
    実引数
    argument
    ジェネリック
    クラス
    型パラメータ
    type parameter
    型引数
    type argument

    View Slide

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

    View Slide

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

    View Slide

  18. クラス ≠ 型
    ● 1つのクラスにつき、2つの型がある場合の例
    ○ Stringクラス
    ○ String型とString?型
    ● 1つのクラスにつき、無数の型がある場合の例
    ○ (先ほど独自に定義した)Boxクラス
    ○ Box型、Box型、Box>型.........

    View Slide

  19. 不変(invariant)
    ● 非変とも。不変(immutable)との混同をおそれて
    ● サブタイプの関係が成り立たない
    ● デフォルト(特に指定がない場合は不変となる)
    val box1: Box = Box(123)
    val box2: Box = box1 // コンパイルエラー
    Int
    Number
    Box
    Box

    View Slide

  20. 不変だと扱いづらい場合がある
    // BoxをIntに変換する関数
    fun toInt(box: Box): Int =
    box.value.toInt()
    val floatBox: Box = Box(12.3f)
    val int: Int = toInt(floatBox) // NG

    View Slide

  21. 共変(covariant)
    ● 型パラメータと同じサブタイピング関係が成り立つ
    ● 型プロジェクションにおいてoutキーワードを用いる
    val box1: Box = Box(123)
    val box2: Box = box1 // OK
    Int
    Number
    Box
    Box

    View Slide

  22. 型プロジェクション
    ● 型の射影(projection)
    ● RDBにおける射影とは、カラムの選択
    → レコードのある側面にだけ注目している
    ● 型のある側面にだけ注目することで、サブタインピングの関係
    を変更できる
    →逆に言うと、ある側面を隠すということ

    View Slide

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

    View Slide

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

    View Slide

  25. 例えば、変更可能なコンテナを考える
    class MutableBox(var value: T)
    val box1: MutableBox = MutableBox(123)
    val box2: MutableBox = box1
    box2.value = 0.5 // コンパイルエラー
    ● 実体がIntなのでDoubleの代入は危険
    ○ 禁止すべき操作(Javaの配列では可能)
    ● 実際にはコンパイルエラーとなる
    → setterが削除されている
    → 型プロジェクションにより「ある側面を隠した」

    View Slide

  26. 反変(contravariant)
    ● 型パラメータと逆のサブタイピング関係が成り立つ
    ● inキーワードを用いる
    fun setDefault(box: MutableBox) {
    box.value = 0
    }
    val box: MutableBox = MutableBox(NaN)
    setDefault(box)
    println(box.value) // 0
    Int
    Number
    Box
    Box

    View Slide

  27. 不変・共変・反変 まとめ
    キーワード サブタイピング 可能な
    操作
    不変
    invariant
    (なし) 入出力
    共変
    covariant
    out 出力
    反変
    contravariant
    in 入力
    型Aが型Bのサブタイプであるとき...
    Box Box
    Box Box
    Box Box

    View Slide

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

    View Slide

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

    View Slide

  30. 型プロジェクション(2回目)
    ● 型の射影
    ● キーワード out や in を使う
    ● ジェネリック型を使う際に指定するので
    「使用場所変位指定」と言うこともある
    val box1: Box = Box(123)
    val box2: Box = box1
    型プロジェクションにより「入力」すなわち「変更」
    が禁止される。
    そもそもBoxクラスはイミュータブルなので
    変更できないのは自明。このout宣言は冗長な
    のでは?

    View Slide

  31. 宣言場所変位指定
    ● クラスやインタフェースにおいて、型パラメータを宣言する場所
    で変位を指定することができる
    ● outやinキーワードを使用する
    ● 型プロジェクションは、自動的に危険な操作を隠してくれるが、
    宣言場所変位指定では、指定した変位に対して、危険な操作
    を公開するとコンパイルエラーとなる
    class Box(val value: T)
    val box1: Box = Box(123)
    val box2: Box = box1

    View Slide

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

    View Slide

  33. ジェネリック制約
    ● 型引数として指定できる型に制約を設けることが可能
    ● 制約とは、具体的には上限境界
    class NumberBox(val value: T) {
    fun toInt(): NumberBox =
    NumberBox(value.toInt())
    }
    val box1: NumberBox = NumberBox("") // NG
    val box2: NumberBox = NumberBox(1.2f)
    val box3: NumberBox = box2.toInt()

    View Slide

  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 introduceMyself(t: T): String
    where T: WithName, T: Greeter {
    return "${t.greet()}, I am ${t.name}!"
    }

    View Slide

  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 introduceMyself(t: T): String
    where T: WithName, T: Greeter {
    return "${t.greet()}, I am ${t.name}!"
    }
    ジェネリック関数

    View Slide

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

    View Slide

  37. 型消去
    Kotlin
    Java風
    コンパイル
    val box: Box = Box("Hello")
    val value: String = box.value
    Box box = new Box("Hello");
    String value = (String) box.getValue();

    View Slide

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

    View Slide

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

    View Slide

  40. 型消去
    Kotlin
    Java風
    コンパイル
    val box: Box = Box("Hello")
    val value: String = box.value
    Box box = new Box("Hello");
    String value = (String) box.getValue();
    コンパイルすると型引数が消える!
    コンパイルの時だけに使用される情報、すなわち型安全性の面のみで活
    用される。

    View Slide

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

    View Slide

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

    View Slide

  43. 型チェック
    val myObject: Any = Box(123)
    if (myObject is Box) {
    ...
    }
    val myObject: Any = Box(123)
    if (myObject is Box<*>) {
    ...
    }
    NG
    OK
    スタープロジェクションを使えばOK

    View Slide

  44. スタープロジェクション
    ● 型が決まっているが、不明なとき or 興味がないときに使用す

    ● AND 的に振る舞う
    val list: MutableList<*> = mutableListOf()
    val first: Any? = list.get(0) // Any?として生産
    list.add(123) // コンパイルエラー, Nothingとして消費
    Any?
    あらゆる型のスーパタイプ
    Nothing
    あらゆる型のサブタイプ

    View Slide

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

    View Slide

  46. 型消去が不便なところ
    fun Any.isA(clazz: Class): Boolean =
    clazz.isInstance(this)
    5.isA(Number::class.java)
    5.isA()
    こう書きたくない?

    View Slide

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

    View Slide

  48. 具象型(reified type)パラメータ付き関数
    inline fun Any.isA(): Boolean =
    this is T
    5.isA()
    インライン関数 実行時にも残る型パラメータ

    View Slide

  49. まとめ
    ● 任意の型に対して汎用的に安全なコード部品化を実現する
    →ジェネリクス
    ● クラスや関数は、型パラメータを宣言することができる
    ● 型引数を指定することで、それが型パラメータを置き換える
    ● ジェネリック型を安全かつ柔軟に扱うために変位と呼ばれる性質を
    生かす(不変、共変、反変)
    ● 型プロジェクションや宣言場所変位指定により、変位の指定をする
    ことができる
    ● 型引数に制約を設けることができる
    ● 型引数の情報はコンパイル時に消える→型消去
    ● 具象型パラメータ付き関数の中では、型消去が起こっていないよう
    に見える

    View Slide