今年はKotlinがくる…と社内で言い続けて、もう10ヶ月。私達のような受託開発をメインにしている会社では、なかなか実案件に投入するのが難しいところかと思います。
(なにしろ、Ver.1.0にもなってないものを使うのかというところでひっかかる人達もいらっしゃるので…)
そんな中、今回を含めて数回でAndroidアプリの開発現場でKotlinを利用すること得られるメリットを紹介したいと思います。
Kotlinの使い方や「Hello, world.」などは http://kotlinlang.org/docs/tutorials/ を読んでいただければと思います。
今回はその中のNull-Safetyについて紹介します。
Null-Safetyとは
NullPointerException…甘美な響きですね(笑)。
正直な話、「ぬるぽ」はテストフェーズで発生する不具合の原因となる例外の一つです。コーディング時の注意やCheckStyleなどの静的解析で避けられることも多いのですが、なかなかなくなりません。「この世の中から「ぬるぽ」なんてなくなればいいのに…」と思うエンジニアも多いでしょう。(かといって、Objective-Cみたいにnilにメッセージを送っても、落ちもせず動いちゃうのもどうかと思いますが…)
そんな「ぬるぽ」に対して、Kotlinは言語仕様でNull-Safetyを取り入れて対応しています。言語仕様で明確にNullの取扱を決めることで、実行時にNull参照が発生しないようにしていうます。
KotlinがNull-Saftyを実現している仕組みは以下の2点、
- 変数定義時にその変数がNull代入を許可するかを設定する。設定されている変数にNull代入する or される可能性があるコードがある場合はコンパイルエラー
- Null代入を許可している変数を参照する場合、参照前にNullチェックが行われているかをチェックし、行われていない場合はコンパイルエラー
Java時代では「ランタイムエラー」だったNull参照を、KotlinではコンパイルエラーとすることでNull-Safetyを実現しています。
実際にどのようなエラーが発生するかを見ていきます。
まず、最初はNull代入を許可していない変数へ、Nullが代入される可能性があるコードを見てみましょう。
fun main(args: Array<String>) { // 1番目の引数を取る、引数がなければNullとなる val str: String = args.firstOrNull() // 1番目の引数の文字数を表示する println("文字数: ${str.count()}") }
上のコードをKotlinのコンパイラーでコンパイルすると、以下のようなエラーが表示されます。
(IntelliJ IDEA 14 CE+Kotlin pluginでコンパイルしています)
Information:2015/10/06 18:49 - Compilation completed with 1 error and 0 warnings in 4s 629ms /Users/tetsuo/work/IdeaProjects/KotlinTest/src/Main.kt Error:(9, 23) Kotlin: Type mismatch: inferred type is kotlin.String? but kotlin.String was expected
3行目にエラーの内容が出力されています。
要約すると「args.firstOrNull() はString型もしくはNullを返しますが、変数 str はNullを代入を許していないため型が不一致です。」というエラーとなります。
型が不一致というのはちょっと違和感がありますが、コンパイル時にNullが代入される可能性をチェックしていることがわかります。
これを回避するために変数 str をNull代入許可とします。
Null代入を許可するためには変数 str の型の末尾に”?”を付けます。
fun main(args: Array<String>) { // 1番目の引数を取る、引数がなければNullとなる val str: String? = args.firstOrNull() // 1番目の引数の文字数を表示する println("文字数: ${str.count()}") }
これで再度コンパイルしてみます。
すると、エラー内容が以下のように変わりました。
Information:2015/10/07 9:20 - Compilation completed with 1 error and 0 warnings in 5s 44ms /Users/tetsuo/work/IdeaProjects/KotlinTest/src/Main.kt Error:(12, 24) Kotlin: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type kotlin.String?
エラー内容を要約すると、「Null代入を許可した変数に対してメソッドを呼び出す場合、”.”の変わりに”?.”もしくは”!!.”を利用して呼び出してください。」となります。
※なお、”?.”は変数がNullだった場合、メソッド呼び出しをしない演算子。”!!.”は変数がNullでもメソッド呼び出しをする演算子(つまり、「ぬるぽ」がでてもいいよ!!という演算子です。利用しないでくださいね(笑))。
このエラーでKotlinが伝えたいことは「Nullが入る可能性がある変数にアクセスする場合、Nullだった場合にどのようにするかをコードで明示してほしい。」ということです。
つまり、コード上で明示的にチェックを入れることでこのエラーを回避することができます。
なので、”?.”を利用したり、if文を用いることでコンパイルが通るようになります。
fun main(args: Array<String>) { // 1番目の引数を取る、引数がなければNullとなる val str: String? = args.firstOrNull() // 1番目の引数の文字数を表示する if (str != null) { println("文字数: ${str.count()}") } }
実行結果は
Process finished with exit code 0
となり、引数がない場合は何も出力されずに終了します。
ちなみに引数を与えた場合は
文字数: 14 Process finished with exit code 0
となり、正しく動いています。
もちろん、以下のようなコードでもコンパイルエラーは発生しません。(動作はもちろん変わりますけどね。)
fun main(args: Array<String>) { // 1番目の引数を取る、引数がなければNullとなる val str: String? = args.firstOrNull() // 1番目の引数の文字数を表示する println("文字数: ${str?.count()}") }
Javaとの連携時の注意点
KotlinはJavaとのシームレスに連携できるところも利点だと思います。ただ、Null-Safetyは連携するJavaのコードにまではもちろん適用されません。(コンパイル時のチェックですもんね。)
なので、以下のようなJavaコードと連携すると、簡単に「ぬるぽ」が発生します。
public class DamenaClass { public void showStringCount(final String message) { System.out.println("文字数:" + message.length()); } }
fun main(args: Array<String>) { // 1番目の引数を取る、引数がなければNullとなる val str: String? = args.firstOrNull() // Javaクラスと連携する val damenaClass = DamenaClass() damenaClass.showStringCount(str) }
Exception in thread "main" java.lang.NullPointerException at jp.co.techfirm.blog.DamenaClass.showStringCount(DamenaClass.java:8) at main.MainKt.main(Main.kt:15) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
Javaコードを修正できる場合、NullチェックやOptionalを用い、Java側で対応するようにしましょう。
外部ライブラリなどでソースコードが修正できない場合は残念ですが、try-catchを利用して捕獲するようにしましょう。
fun main(args: Array<String>) { // 1番目の引数を取る、引数がなければNullとなる val str: String? = args.firstOrNull() // Javaクラスと連携する val damenaClass = DamenaClass() try { damenaClass.showStringCount(str) } catch (e : Exception) { println("Java側でぬるぽ発生!!") } }
Java側でぬるぽ発生!! Process finished with exit code 0
今回はKotlinのNull-Safetyを説明しました。
次回はData ClassesとExtensionsについて説明する予定です。
勉強させていただいております。ひとつ気になった点があったのでコメントさせていただきます。
DamenaClass#showStringCount をtryで呼び出してcatchで補足するというアイデアは素直に受け入れられません。
なぜならshowStringCountの定義を見るに、おそらくshowStringCountは引数としてnullを許容しない仕様のはずで、ぬるぽが起こるのはshowStringCount自身の責任ではなく呼び出し側のコードの責任だからです。
この例ではtry-catchの代わりにif式(あるいはlet関数など)を使うべきです。
(Javaコード内でのぬるぽはKotlinで防げない、という例であることは承知しています)
これはその通りですね。
今回のケースであれば、渡す側がnullチェックして渡すべきだと思います。
例としてのわかりやすさを優先してしまいました、ごめんなさい。
あえてtry-catchを使う場合があるとしたら、連携するJavaのソースコードが隠蔽されている場合かなと思います。
引数が nullable なのか not null なのか判断できないが、仕様的にnullは許容されるべき場合に安全策として入れる形が考えられます。
(アプリとして落ちることは避けなければいけないので…)
まぁ、普通は隠蔽されているんだから、ちゃんとしたドキュメントがついてくるはずなんですが…
世の中にはいろいろあります…