Kotlinの初級~中級の狭間 スコープ関数、コルーチン、Null安全


Kotlin では、より質の高い Android アプリをより速く記述できます。Kotlin は最新の静的型付けのプログラミング言語で、プロフェッショナルな Android デベロッパーの 60% 以上に使用されており、生産性、デベロッパーの満足度、コードの安全性の向上に貢献しています。
Kotlin で Android アプリを開発する

かつてJavaでAndroidアプリを開発していたがKotlinを使ってみると手に馴染んだ。

公式Doc

Kotlinはコード量も少ないし、モダンだ。Kotlinはかなり普及しているように思える。Android開発で詰まったときにググると、もはやJavaよりKotlinの記事の方が多いと思う。
だからこれからAndroidアプリを開発する人はKotlinでやるべきだと思う。

私が学習に使ったウェブサイトはここ。
JavaプログラマのためのKotlin入門

このページをかじりながらコードを書いていけば、
基本中の基本はできるようになると思う。

ただNull安全や高階関数、コルーチンはなんとなくの理解だったのでちゃんと整理しておいた方が良いと思った。

この記事はKotlinにやや慣れてきたがなんとなくここは理解がはっきりしないなと思うようなところをかいつまんでまとめていく。

項目 説明
この記事の対象者 Kotlinの初学者、基本中の基本を学んだ人
この記事は何か Kotlinの初学者のためにややこしいところを整理する
この記事を読んで分かること Null安全、ラムダ式、高階関数、コルーチン

なお簡単にKotlinを試したいひとは公式のTry Kotlinでコードを試せる。

Null安全

Javaで散々苦しめられたというNullPointerExceptionであるが、
KotlinはNull安全が売りになっている。
私はJavaの歴が特に深いわけではないので、いまいちピンとこなかったが、
Kotlinでコードを組んでいく内に、Nullを意識してコードを組んでいる感じで
これは便利だと思った。

もし職場でなかなか周囲がJavaの呪縛にはまっているようなら、
Null安全を推していけば、賛同が得られるのではないかと思う。

Kotlin's type system is aimed at eliminating the danger of null references from code, also known as the The Billion Dollar Mistake.

Nullable types and non-null types

KotlinでNullPointExceptionが出るのは以下のパターンのみ。
* throw NullPointerException()
* !!演算子の使用
* superとthisの初期化が関係するところ
* Javaと同居するとき

それ以外は発生しない。

Kotlinにはnon-null型とNullable型があり、
?を使って宣言する。

var a : String = "abc" //not-null
val la = a.length //OK
var b : String? = "abc" //Nullable
val lb = b.length //コンパイルエラー

b.lengthというのはNull安全でないアクセスなので、そもそもコンパイルされない。
駄目なNullの使い方には、コンパイル前に開発者に指摘してあげる。

開発者は変数にNullが入ってくるかどうか良く分かんないし、?付けちゃえってやってしまいそうだが、
そんなことをするコンパイルが通らないので、
変数を宣言するときにnon-null型とNullable型のどっちにするかちゃんと考えなくてはいけない。
でもそのおかげでNullPointerExceptionは起きなくなる。

じゃあb.lengthしたときはどうするんだ、となるけれど、
そういう場合はnull判定をその都度やる。

val l = if (b != null) b.length else -1

基本はこの考えだが、いちいちif文を使ってるのは格好悪い。
なのでKotlinにはifを使わなくてもいい仕組みがちゃんと揃っている。

「!」とか「?」とか「let」がその仕組みである。
次の3行は全て同じ意味。

val l = if (b != null) b.length else null
val l = b?.length
val l = b?.length ?: null //これは冗長

この喜怒哀楽なKotlinのコードを初めて見るとき人は尻込みする物であるが、
慣れれば便利になる。
慣れのためにも、以下のチートシートは、使えるようになるまで覚えよう。

演算子とか 意味 使用例
: 型名? Nullableの宣言 var b : String? = "abc"
?. Safeコール
bがnullならnullを返す
b?.length
?.let{} let演算子
bがnullなら{}の式は実行されない
letは次に出てくる高階関数
item?.let{println(it)}
?: エルビス演算
nullなら右の式が実行される
val parent = node.getParent() ?: return null
!! Nullの場合NullPointerExceptionを返す
nullableに無理矢理lengthするときに使いがちだがなるべく使わないこと
val l = b!!.length
as? Safeキャスト
キャスト出来なければnullが変える
?を付けないキャストは例外が発生
val aInt: Int? = a as? Int
bob?.department?.head?.name

みたいに連結したりもできる

直感的に、?でnullと評価されたらその文の後は評価されなかったりする。
またvalとvarで違ったりする(varはあまり使わないが)
この辺りは公式ドキュメントを見ると良い。

またスマートキャストという機能があり、
一度nullでないことを確認すると、non-nullとして扱えたりする。

関数、ラムダ式

ラムダ式はKotlinだけでなく色々な言語に使われているモダンな式なので、
この際ちゃんと整理しておこう。

ラムダ式は即席の関数だ。

val sum = { x: Int, y: Int -> x + y }
val sum: (Int, Int) -> Int = { x, y -> x + y } //上と同じ

このように引数->式のような形にする。

ラムダ式と並べてよく出てくる無名関数は、

fun(x: Int, y: Int): Int = x + y

こうなる。

関数で他に抑えておきたいことはいくつかある。

  • 有用な物を返さない戻り値はUnit型と言われる。関数の返り値の型宣言でUnitは省略できる。
  • 可変長引数はvarargを使う。
  • クロージャ:関数の中で定義された変数と関数の結果がセットで保存される
  • レシーバ付き関数リテラル:1.sum(2) こんな感じで、レシーバー引数に取り関数宣言できる
  • 高階関数とは、パラメータとして関数を受け取ったり、関数を返したりする。

スコープ関数

letとかalsoとかいうやつで、これを使うとコード量がぐっと少なくなり、Kotlinっぽくなる。
初めは戸惑うが便利だし、これをやっておかないと他の人のコードがろくに読めない。
なのでしっかり整理しておきたい。
letやalsoが一番使うので重点的に。

下記の定義はインライン関数である。
インライン関数とは、関数を引数に持つ関数。

スコープ
関数
定義 レシーバー
の変化
(戻り値)
よく使われる
用途
使用例
let public inline fun T.let(f: (T) -> R): R = f(this) 変化する nullabeに対して(前述) val s = "hoge".let { it.toUpperCase() }
println(s) // HOGE
with public inline fun with(receiver: T, f: T.() -> R): R = receiver.f() 変化する val s = with("hoge") { this.toUpperCase() }
println(s) // HOGE
run public inline fun T.run(f: T.() -> R): R = f() 変化する val s = "hoge".run { toUpperCase() }
println(s) //HOGE
apply public inline fun T.apply(f: T.() -> Unit): T { f(); return this } 変化しない val s = "hoge".apply { toUpperCase() }
println(s) //hoge
also public inline fun T.also(block: (T) -> Unit): T { block(this); return this } 変化しない val s = "hoge".also { it.toUpperCase() }
println(s) //hoge

なんでレシーバー.let{処理}みたいな後ろに{}がある変な書き方になるかというと、
{}はラムダ式であり、Kotlinでは関数を引数に取る関数を書くとき、
()なしで後ろに{処理}を書くことができるから。
つまり{処理}は引数で、この引数は関数なのだ。

関数を引数に取る関数は上述のようにインライン関数と呼ばれ、inline修飾子がつく。
だから上の定義はinline修飾子がすべて入っている。

上の定義が理解できたら凄く良いと思う。
まだまだ私は理解に時間が掛かります。

コルーチン

コルーチンは非同期処理とかマルチスレッドのための仕組みで、
Javaではコールバックばかりで見づらいコードになっていたが、
Kotlinではすっきりする。

    launch{//処理}

コルーチンとは、

簡単にいうと、ひとつの非同期処理ブロック内で、処理を途中で中断して、その中断している間に別の処理を実行することができる仕組みです。

KotlinによるAndroidの非同期処理

コルーチンはスレッドと同じく非同期処理であるが、
非同期処理は並行処理と違うということを念頭に置きたい。
JavaやKotlinでは別のプログラムがまったく並行に行われるのではなく、
そう見えるだけだ。
これが非同期処理である。

非同期処理では、HTTP通信するとか、重い処理をしている間に、
他のスレッド/コルーチンで処理がなされているように見える。

重い処理をしている間に、そのスレッド/コルーチンの処理を「中断」して、他のスレッド/コルーチンの処理を進める。重い処理が終わったら別のスレッドから戻り続きの処理を続ける(再開)。

ここの中断は後述のsuspend修飾子に繋がっていく……。

使用例はどうかというと、基本の形はこうだと思う。

fun main() = runBlocking {// this: CoroutineScope
    launch { 
        delay(1000L)
        println("World!")
    }

    println("Hello")
}
Hello
World!

このdelayは実はsuspend関数なのである。
delayでlaunchによって始められたコルーチンの処理はいったん「中断」

メインに戻って処理を行う。

1秒経つとdelayは終了し、launchで始められたコルーチンが「再開」
される。

さて、このrunBlockingは何かというと、
これはスコープ内の全てのコルーチンが終わるまで待ってくれる。
そのスコープの外の処理をブロックしてくれるのだ。

runBlockingがないと

Hello

で終わってしまう。launchで始めたコルーチンは独立しているが、
それをまたずにプログラムが終わってしまうからだ。

delayは標準関数だったが、suspend修飾子を使って関数を作れば、コルーチンを中断させられる。

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

コルーチンは他にも下記のような重要な点がある。
* コルーチンの返値を変数に格納できる。
* コルーチンの処理が終わるまで待ちたいときはjoin()を使う。
* コルーチンが終わるのを待ちつつ、その結果を受け取りないなら、async/awaitを使う。
* suspend関数はコルーチンか、他のsuspend関数からしか呼び出せない。
* GlobalScope.launch{}はトップレベルのコルーチンを作成する
* コルーチンは軽量でほとんどコストがないから、何百個でも作れる

所感

  • Kotlinのおぼつかないところが整理できた。
  • まだまだ押さえ切れていないところが多いので継続勉強したい。

他にやることリスト

理解度確認クイズ

  • KotlinはどのようにしてNull安全になっていますか?
  • Kotlinのスコープ関数を実際に使えますか?
  • コルーチンはどのようなもので、Javaのスレッドと何が違いますか?
  • コルーチンを実際に使えますか?

参考文献

Kotlin で Android アプリを開発する
JavaプログラマのためのKotlin入門
リファレンス

Null安全

Null safety
Kotlin : as, !, ? 周りのチートシート
Kotlinでのnullの基本的な扱いかた
null を扱うさまざまな演算子・関数

ラムダ式

Kotlin文法 - 関数とラムダ

スコープ関数

Kotlin スコープ関数 用途まとめ
Kotlinスコープ関数 apply,also,let,run,withの使い分け

コルーチン

Kotlin のコルーチン入門
Kotlin コルーチンを 理解しよう 2019 - KotlinFest2019 -
KotlinによるAndroidの非同期処理
Kotlin の Coroutine を概観する

リンク

普段はこちらでブログ記事を投稿しています。
https://kurage-solution.work/