Android
Kotlin

【Kotlin】華麗にNull回避

KotlinはNull安全なプログラム言語だといいますが、当然ながらNullを取りうる変数objに対して、obj!!なんてやっちゃったら、場合によっては落ちます。

ただKotlinにはJavaにはないスコープ関数や拡張関数、エルビス演算子などがあります。それらを活用すればあの忌々しいNullを華麗にかわすことができます。今回、比較的よく使うNull回避の実装をまとめておきます。

全てのオブジェクトがNullでない場合のみ処理を実行する

あるオブジェクトがNullでない場合のみ、とある処理を実行したいケースはよくあると思います。普通は次のように書くと思います。

if (obj != null) {
    doSomething(obj)
}

この書き方自体問題があるわけではないのですが、次のようなケースの場合にちょっとした不都合があります。

  1. objがインスタンス変数の場合、スマートキャストが効かない
  2. Nullチェックしたい変数が複数ある場合、if条件にタラタラと書かなきゃならない

1についてですが、マルチスレッド環境下の場合だと、他のスレッドからインスタンス変数の内容を書き換えられてしまう可能性があります。そのためKotlinはスマートキャストによってobjを非Nullとみなすことができません。

ですので、if内に書く処理内容がobjを非Nullを期待している場合、objを使用する際は、保証できるならobj!!と書くか、あらかじめローカル変数に移しておくとかしないといけません。

2については、大した問題ではないようにも思えますが、if条件を長々と書くのもアレですし、何よりNullチェックする全ての変数において 1 の問題がありますので、実は結構やっかいです。

というわけで、私は次のような拡張関数を定義して、上記のNull問題を回避しています。

inline fun <T> ensureNotNull(p1: T?, f: (p1: T) -> Unit) {
    if (p1 != null) f(p1)
}

これを使って先程のif文を修正すると、こんな感じになります。

ensureNotNull(obj) { o ->
    doSomething(o)
}

これならobjに対してスマートキャストが効くので便利です。

これだけなら?.let?.alsoでもいいのですが、関数名に「Nullじゃないよ」という訴求力!があるので、何がしたいのか自明です。また、複数の変数に対してNullチェックをかける場合にも展開が容易です。複数の場合だと、次のようになります。

inline fun <T1, T2> ensureNotNull(p1: T1?, p2: T2?, f: (p1: T1, p2: T2) -> Unit) {
    if (p1 != null && p2 != null) f(p1, p2)
}

inline fun <T1, T2, T3> ensureNotNull(p1: T1?, p2: T2?, p3: T3?, f: (p1: T1, p2: T2, p3: T3) -> Unit) {
    if (p1 != null && p2 != null && p3 != null) f(p1, p2, p3)
}
...

例えば2つの変数が非Nullであることを保証して処理を実行したい場合は、次のように書けます。

ensureNotNull(arg1, arg2) { a1, a2 ->
    doSomething(a1, a2)
}

実用的な例では、AndroidアプリのFragment内での利用なんかがあると思います。Fragmentから外部APIを非同期で実行し、受け取った結果をもって何らかの処理する場合です。非同期処理が終わった頃にはviewがNullになってたりする可能性があるので、そこは華麗に回避してあげなければなりません。

api.call(...) { result ->
    // 結果を受け取ったときには、すでにactivityやviewがNullかもしれない!
    ensureNotNull(activity, view) { a, v ->
       doSomething()
    }
}

またFragmentの親クラスやミックスインするInterface等で、次のようなメソッドを用意しておいてもいいかもしれません。

abstract class BaseFragment : Fragment() {
    protected inline fun ensureAlive(f: (activity: Activity, view: View) -> Unit) {
        if (!isDetached && activity != null && view != null) f(activity!!, view!!)
    }
}

いろいろ応用は効くと思います。

拡張関数の解読

ジェネリック関数に慣れてないと、Kotlinの拡張関数は意味不明に見えるかもしれないので、簡単に解説しておきます。

inline fun <T> ensureNotNull(p1: T?, f: (p1: T) -> Unit) {
    if (p1 != null) f(p1)
}

1行目の宣言部の説明です。

  • 1行目: <T> ・・・ 今からTという型パラメータ使いますよー
  • 1行目: (p1: T?, ・・・ 1個目の引数はNull許容のT型で、名前はp1ですよー
  • 1行目: f: (p1: T) -> Unit) ・・・ 2個目の引数は関数型の引数で、名前はfですよー。fの関数定義なんですが、引数が非NullのT型でp1という名前でして、戻り値はナシ(Unit)ですからねー

inlineを付けているのは、簡単に言うとinline関数じゃないと関数fの中からreturnができなくなるからです。もっと知りたければ自分でググって見て下さい。

エルビス演算子でフォールバック代入

なんか値が入ってたらそれを使うけど、Nullだったら既定値を使いたい、ってな場合です。

private fun assignName(name: String?) {
    val name = name ?: "[匿名さん]"

    doSomething(name)
}

なお、右辺のnameは引数のnameで左辺のnameは新しく定義した変数扱いになります。別名を考えなくていいので便利です。Kotlin最高。

エルビス演算子で速攻リターン

(さっきとほとんど同じ例なんですが)、Nullだったら早々にreturnしたい場合にもエルビス演算子が便利です。

fun makeAQuickReturn(str: String?) {
    str ?: return

    doSomething(str)
}

エルビス演算子で例外発生

またまた同じような例ですが、AndroidのFragmentに必須パラメータが渡ってきてない場合なんかも。

val url = arguments?.getString(PARAM_URL) ?: throw Exception("URLは必須です")

Nullかもしれない値の三項演算的なこと

nullじゃなかったら、その値を使って、そうじゃなかったらnullにままでいい、というようなケースです。kotlinのif ~ elseは値を返すので三項演算子的に書けますが、letを使った方がスッキリします。

fun decorate(name: String?) {
    val decoratedName = name?.let { "★$it★" } ?: null
    ...
}

userIdがNullじゃなかったら検索しにいこう、とか。

val user = userId?.let { User.find(it) } ?: null

空文字じゃなければ実行、を代入と同時に

Nullじゃなくて空文字の例なんですが、空文字チェックと変数への代入を同時に行いたい場合がしばしばあると思います。例えば、次のような例です。

val name = doc.selectFirst(".input-title").attr("name")
if (name.isNotBlank()) {
    doSomething(name)
    ...
}

一時変数に落とすのが少々煩わしいです。
拡張関数を定義しておけば楽になります。

inline fun <T: CharSequence> T.isNotBlank(f: (t: T) -> Unit) {
    if (isNotBlank()) f(this)
}

これを使うと、先程の例はこのようになります。

doc.selectFirst(".input-title").attr("name").isNotBlank { name ->
    doSomething(name)
    ...
}

1行しか変わりませんが、然るべきスコープ内でしか有効でない一時変数を楽に定義できるので、相当気持ちいいです。

空リストでなければ実行、を代入と同時に

さっきの空文字の例のリスト版です。

inline fun <T: List<*>> T.isNotEmpty(f: (T) -> Unit) {
    if (isNotEmpty()) f(this)
}
doc.select("div.list").isNotEmpty { list ->
    doSomething(list)
}

おしまい

とりあえず思い付いたものを列挙してみました。何かまた思い付いたら追記していきたいと思います。