Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

ジェネリック制約 ● 型引数として指定できる型に制約を設けることが可能 ● 制約とは、具体的には上限境界 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()

Slide 34

Slide 34 text

複数の上限境界 ● 複数の上限境界を指定するには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}!" }

Slide 35

Slide 35 text

複数の上限境界 ● 複数の上限境界を指定するには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}!" } ジェネリック関数

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

スタープロジェクション ● 型が決まっているが、不明なとき or 興味がないときに使用す る ● AND 的に振る舞う val list: MutableList<*> = mutableListOf() val first: Any? = list.get(0) // Any?として生産 list.add(123) // コンパイルエラー, Nothingとして消費 Any? あらゆる型のスーパタイプ Nothing あらゆる型のサブタイプ

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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