# Kotlin 型実践入門(修正版) みなさんこんにちは! タップル誕生で Android エンジニアをやっている [@mzkii](https://github.com/mzkii) です. サイバーエージェントでは,エンジニアに対して国内外のテックカンファレンスへの参加を推奨しています. 先日開催された「Kotlin Fest 2019」にもエンジニア達が参加し、そこで弊社のAndroidエンジニアである佐藤([@stsn_jp](https://twitter.com/stsn_jp))が「[Kotlin型実践入門](https://speakerdeck.com/satoshun/kotlin-fest-2019-kotlinxing-shi-jian-ru-men)」というタイトルで発表しましたので,今日はそのセッションの内容について解説します. https://kotlin.connpass.com/event/129860/ <img src="https://i.imgur.com/SCwu2th.jpg" width="640"> ## Smart Cast 多くの場合,Kotlin ではコンパイル時に必要に応じて型推論が行われるため,明示的にキャスト演算子を使用する必要はありません. Smart Cast とは,if 式 / when 式 / is 演算子 / as 演算子などを使用した後に型推論してくれる機能のことを指します.この機能によって,Java 特有の冗長な記述を減らすことが可能です. 例えば,if 式で型チェックを行ったとします.するとブロック内では Smart Cast によって明示的に型を指定する必要がなくなります. ```kotlin /* * if ブロック内では String であることが保証されているため * length メソッドが呼び出せる */ fun demo(x: Any) { if (x is String) { print(x.length) } } ``` また Smart Cast は複雑な条件式においても有効です.例えば,以下のように複数の条件式が組み合わさっている場合でも効果を発揮します. ```kotlin /* * x が String と保証されているため * length メソッドが if 式の中で呼び出せる */ if (x is String && x.length > 0) { print(x.length) } ``` ただし,型チェックからその変数が使用されるまでの間に,変数に何らかの変更が加わる可能性がある場合,Smart Cast は機能しないことに注意して下さい. 例えば,以下のクラスメソッドがあった場合,一見型チェック後の obj は String として扱えそうですが,コンパイルレベルでは String であることが保証されないため,コンパイルに失敗します. ```kotlin /* * 型チェックと,length メソッドを呼び出す間に * obj に対して変更が加わる可能性があるため * 明示的にキャストしないとコンパイルエラー */ class Hoge { private var obj: Any = "a" fun test() { if (obj is String) { print((obj as String).length) } } } ``` この場合,val としてローカル変数を新しく切り出したり, ```kotlin /* * val によって変更されないことが保証されるので OK */ class Hoge { private var obj: Any = "a" fun test() { val obj = obj if (obj is String) { print((obj.length) } } } ``` スコープ関数を使ったりすることで Smart Cast が使えるようになります. ```kotlin /* * let スコープの中では it は毎回同じ obj を返すので OK */ class Hoge { private var obj: Any = "a" fun test() { obj.let { if (it is String) { print(obj.length) } } } } ``` また,Kotlin の Smart Cast については以下の公式ドキュメントが参考になります. https://kotlinlang.org/docs/reference/typecasts.html#smart-casts ## Contracts Smart Cast は Kotlin の便利な機能の一つですが,Smart Cast が効いた変数に対して,別の関数で使用してしまうと型推論が無効になってしまいます. 例えば Kotlin 1.3 以前では,Smart Cast が効いた変数に対して,isNullOrEmpty() を使用することは出来ませんでした. ```kotlin fun foo(s: String?) { if (!s.isNullOrEmpty()) { print(s.length) // error!! } } ``` こうした問題を解決するために,Kotlin 1.3 では Contracts と呼ばれる機能が追加されました. Contracts を使うことによって,関数の呼び出し結果を元にして,Smart Cast させる事が可能です.例えば,Kotlin 1.3 以降の CharSequence?.isNullOrEmpty() は以下のように定義されています. ```kotlin public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 } ``` 早速 contract が出てきました。 returns(false) implies (this@isNullOrEmpty != null) の意味は,「isNullOrEmpty が false を返す時, isNullOrEmpty は NonNull である」という意味になります。 したがって, isNullOrEmpty を使った先程の例では Smart Cast が効きコンパイルが通ります. ```kotlin fun foo(s: String?) { if (!s.isNullOrEmpty()) { print(s.length) // OK } } ``` また,Kotlin には assertTrue というメソッドが用意されていますが,こちらの内部実装を見ると Contracts が使用されていることが分かります. ```kotlin /** Asserts that the given [block] returns `true`. */ fun assertTrue(message: String? = null, block: () -> Boolean): Unit = assertTrue(block(), message) /** Asserts that the expression is `true` with an optional [message]. */ fun assertTrue(actual: Boolean, message: String? = null) { contract { returns() implies actual } return asserter.assertTrue(message ?: "Expected value to be true.", actual) } ``` contract { returns() implies actual } によって, assertTrue が正常に return する時に,actual は True であることがコンパイルレベルで保証されます. (※ asserter.assertTrue が False の時は,例外が発生するため,正常に終了できる == actual は True) したがって以下のように記述することが可能です. ```kotlin assertTrue(obj != null) print(obj.length) ``` また,Contract は自分で実装することも可能です.checkObj は引数 obj が NonNull か否かを Boolean で返す関数です. ```kotlin @UseExperimental(ExperimentalContracts::class) fun checkObj(obj: Any?): Boolean { contract { returns(true) implies (obj != null) } return obj != null } if (checkObj(obj)) { obj.javaClass // obj is not null } ``` 現在 Contract は Experimental な機能であるため, @UseExperimental(ExperimentalContracts::class) が必要になります. また,Kotlin の Contract については以下の公式ドキュメントが参考になります. https://kotlinlang.org/docs/reference/whatsnew13.html#contracts ## Any,Unit,Nothing Kotlin では新しく Any,Unit,Nothing という特殊な型が登場しました. ここでは,これらの型を使ってどのようなメリットがあるのか解説します. ### Any Any はすべてのクラスのスーパークラスであり,Java における java.lang.Object クラスに相当します. java.lang.Object と違うところは, - スレッドを操作するための java.lang.Object.wait メソッドや java.lang.Object.notify メソッドが使えない - Object の実行時クラスを取得するための java.lang.Object.getClass メソッドが Kotlin では拡張関数で定義されている ```kotlin public inline val <T: Any> T.javaClass: Class<T> ``` などが挙げられます. ### Unit Unit は戻り値が空であることを表し,Java における void 型に相当します.Kotlin では,メソッドが値を返さない場合は Unit を省略することができます. ```kotlin fun empty(): Unit { println("hoge") } fun empty() { println("hoge") } ``` ### Nothing Nothing 型は特殊な型になっていて,すべての Kotlin におけるクラスのサブクラスを表します. 使い道の例としては,絶対に正常終了することのない関数の戻り値として使うことができ,値が存在しないことを表します.例えば,以下の2つの関数が存在したとして,それぞれの戻り値の型が String と Unit だとします. ```kotlin fun getName(): String { return "name" } fun fail(): Unit { throw RuntimeException() } val name: Any = if (isFriend()) { getName() else { fail() } ``` この場合,変数 name の型は String ではなく Any になってしまいます. なぜかというと,String と Unit というそれぞれ異なった型が返却されるため,name の型はそれらの親の型になってしまうからです. この場合 name の型は Any なのですが,Nothing 型の特徴をうまく使えば name を String にすることができます. ```kotlin fun getName(): String { return "name" } fun fail(): Nothing { throw RuntimeException() } val name: String = if (isFriend()) { getName() else { fail() } ``` fail の戻り値を Unit から Nothing にすることで,Nothing は String のサブクラスとなるので, 変数 name の型は,Nothing の親クラスである String として扱うことができます. ### Nothing? Kotlin では Nothing? 型も用意されています.Nothing は Unit と同じく値として存在しないので,実質的には null のみを許容する型になります. ```kotlin // kotlintest/kotlintest interface Show<in A> { fun supports(a: Any?): Boolean fun show(a: A): String } object NullShow : Show<Nothing?> { override fun supports(a: Any?): Boolean = a == null // Nothing? なので null のみを許容する override fun show(a: Nothing?): String = "<null>" } ``` ## ジェネリクス Java と同様に,Kotlin でもクラスは型パラメータを持つことができます. Java において,総称型(ジェネリクス) は不変でしたが,Kotlin では型パラメータに対して 共変性(out 修飾子) と不変性(in 修飾子) を持たせることで,柔軟に親子関係を制限することが出来ます. 例えば,Number 型が存在したとして,Int は Number を継承している関係(Number <-- Int) の時,任意のクラス A<T> に対して A<Number> <-- A<Int> の関係であるならば,クラス A を共変と言います. ```kotlin // 共変 val a: A<Int> = A<Int>() val b: A<Number> = a ``` 逆に,Int は Number を継承している関係(Number <-- Int)の時,任意のクラス A<T> に対して A<Int> <-- A<Number> の関係であるならば,クラス A を反変と言います. ```kotlin // 反変 val a: A<Number> = A<Number>() val b: A<Int> = a ``` A<Int> と A<Number> が関連付けられていない場合は,クラス A を不変と言います. ```kotlin // 不変 val a: A<Int> = A<Int>() ``` 具体的に,例を挙げて説明したいと思います.例えば,以下のインターフェースがあるとします. ```kotlin interface Mapper<T> { fun map(s: String): T } ``` これを継承した IntMapper クラスを作ります. ```kotlin class IntMapper : Mapper<Int> { override fun map(s: String): Int = s.toInt() } ``` ```kotlin fun hoge(mapper: Mapper<Number>) { mapper.map("10") } val mapper = IntMapper() hoge(mapper) ``` この時,Mapper<Number> を引数に取る hoge に対して,IntMapper() のインスタンスを渡すことは出来るのでしょうか? 答えは NO です.Int と Number クラスには何の関係性も定義されていないため,hoge の呼び出し箇所でコンパイルエラーになります. この場合は共変性を利用します.Mapper インターフェースに対して out 修飾子を使って改めて定義してみます. ```kotlin interface Mapper<out T> { fun map(s: String): T } ``` こうすることで,Mapper<Number> <-- IntMapper の関係性が出来るので, hoge メソッドを呼び出せるようになります. ## reified reified とは,inline function 内で型変数にアクセスするための修飾子のことです. 以下のコード例は,reified 修飾子を使ったサンプルです. ```kotlin inline fun <reified T> hoge(obj: Any) { println(T::class.java) if (obj is T) { println("obj is T") } } hoge<MainActivity>(requireActivity()) ``` hoge 呼び出し時に型情報を渡し,関数内で渡されたインスタンスが T であるかをチェックすることができます. また,拡張関数とジェネリクスを組み合わせることで,メソッド呼び出し時に柔軟に型制限できるようになります.例えば,以下の A というクラスが存在したとして,そのクラスに対して `isNull` という拡張関数を定義します. ```kotlin class A<T>(val value: T) fun <T: Any> A<T?>.isNull(): Boolean { return value != null } ``` こうすることで,プリミティブ型など絶対に null でないことが分かりきっているケースにおいては,isNull メソッドを呼び出せないように制限することができます. ```kotlin /* * コンストラクタで渡されたクラスの型は * nullable ではないため,このメソッドは呼び出せない */ val a1: A<Int?> = A(null) a1.isNull() // true val a2: A<Int> = A(10) a2.isNull() ``` ## sealed class sealed class は内部的には abstract クラスになっていて,制限されたクラスの階層を表すために使用されます.例えば,以下のような Either クラスが存在するとします. ```kotlin sealed class Either { object Left : Either() object Right : Either() } ``` この sealed class のインスタンスを obj とすれば,when 式内で型チェックをする場合に else 句を省略することが可能です. ```kotlin /* * sealed class によって * Either クラスのサブクラスには Left と Right のみ * 存在することがコンパイルレベルで保証される */ when (obj) { Left -> println("is Left") Right -> println("is right") // else -> println("else") } ``` ちなみに,sealed class があるなら sealed interface が存在しても良いのではないかと思いつく方もいると思いますが,sealed interface だと容易に継承ができてしまい,型による制限を破ってしまうため用意されていません. ## 関数型 Kotlin では,関数を第1級オブジェクトとして扱うことが出来るので,変数・関数の引数・戻り値などに関数型を使うことが出来ます.例えば,Int 型を受け取り String 型で返す関数を受け取る関数 hoge は以下のように定義します. ```kotlin fun hoge(body: (Int) -> String) { body(10) } hoge { it.toString() } ``` Kotlin では,Function0 ~ Function22 までの関数インターフェースと,FunctionN インターフェースが用意されていて,上記のコードはコンパイル時に Function 関数インターフェースに変換されます. また関数は変数として扱うことも出来ます.以下の例では,Int 型変数を2つ受け取り Int 型を返す関数を変数 a に代入して呼び出しています. ```kotlin fun add(a1: Int, a2: Int): Int { return a1 + a2 } add(10, 20) val a: (Int, Int) -> Int = ::add add(10, 20) ``` ## SAM 変換 SAMとは,Single Abstract Method のことで,一つの抽象メソッドを持つインターフェースを指します.Kotlin においては,Java 定義の SAM インターフェースを引数に取る関数に対してラムダ式を渡すと,SAM インターフェースに変換されるので,簡潔に書くことができます. 以下のように Runnable インターフェースが存在したとして,それを引数に取る SamTest.foo() メソッドについて考えてみます. ```java public interface Runnable { public abstract void run(); } public class SamTest { public static void foo(Runnable a, Runnable b) { // do nothing } ``` ```kotlin fun test() { SamTest.foo({}) {} } ``` この例では,Runnable インターフェースに対して SAM 変換が機能するので, run メソッドの実装は省略することができます. ただし,以下のように引数として Runnable を受け取ることはできません. ```kotlin fun test(r: Runnable) { SamTest.foo(r) {} // コンパイルエラー ``` なぜかというと,foo メソッドのように複数の SAM を引数に取る場合は,一つでも実態を使ってしまうと意図した変換が行われないからです.この場合,test メソッドの引数の r は既に実体化されているので foo メソッドの呼び出し部分でコンパイルエラーになる訳です. また,Kotlin 1.3.40 から SAM 変換の問題点を解決するために,[新しい型推論アルゴリズム](https://blog.jetbrains.com/kotlin/2019/06/kotlin-1-3-40-released/)を開発しています.現在はまだ experimental ですが,設定で有効化することもできます. ## 最後に Kotlin の型についてざっくりとおさらいしてみました. Smart Cast や型推論,Sealed Class などの Kotlin 独自の言語機能を使えば,Java のような冗長な記述がなくなりとても書きやすくなります. また,Kotlin Fest 2019 のセッション資料は[公式](https://docs.google.com/document/d/e/2PACX-1vSrhqi78mkNdsW0GN3otMfb8pznZ0_yTcich85KcsDnuWkz-FbydNIwO_0mXBAEPdbHY_Iupg2taZVb/pub)でまとめられていますのでぜひご覧ください. この記事を読んで Kotlin に興味を持った方はチャレンジしてもらえると幸いです! 来年も Kotlin Fest を楽しみましょう!Have a Nice Kotlin! ## おまけ Kotlin Fest 2019 では,昨年に引き続き弊社もブースを出展し, 今年は寿司打大会を開催し多くの参加者にチャレンジして頂きました! 参加者の皆様ありがとうございました! https://twitter.com/kotlin_fest/status/1165112495138361344?s=20 そして上位に入賞された皆様おめでとうございます:tada: https://twitter.com/ca_developers/status/1165160567843115008?s=20