KotlinはNull安全なプログラム言語だといいますが、当然ながらNullを取りうる変数objに対して、obj!!
なんてやっちゃったら、場合によっては落ちます。
ただKotlinにはJavaにはないスコープ関数や拡張関数、エルビス演算子などがあります。それらを活用すればあの忌々しいNullを華麗にかわすことができます。今回、比較的よく使うNull回避の実装をまとめておきます。
全てのオブジェクトがNullでない場合のみ処理を実行する1
あるオブジェクトがNullでない場合のみ、とある処理を実行したいケースはよくあると思います。普通は次のように書くと思います。
if (obj != null) {
doSomething(obj)
}
この書き方自体問題があるわけではないのですが、次のようなケースの場合にちょっとした不都合があります。
-
obj
がインスタンス変数の場合、スマートキャストが効かない - 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でない場合のみ処理を実行する2
2019-05-25追記
先程のensureNotNull
の代替案として、次のような関数を定義するのも手です。
fun <T1, T2> safe(t1: T1?, t2: T2?): Pair<T1, T2>? {
return if (t1 == null || t2 == null) null else Pair(t1, t2)
}
fun <T1, T2, T3> safe(t1: T1?, t2: T2?, t3: T3): Triple<T1, T2, T3>? {
return if (t1 == null || t2 == null || t3 == null) null else Triple(t1, t2, t3)
}
このsafe
関数を利用すれば、nullかもしれないオブジェクトのアンラップor即効リターンのコードが簡潔に書けます。
val (context, activity) = safe(context, activity) ?: return
val (context, activity, view) = safe(context, activity, view) ?: return
みたいな感じです。すごく便利。
なお変数1つの場合は普通にval context = context ?: 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
を使った方がスッキリします。
instance?.method()
はinstanceがnullでなければmethodを実行し、nullであればnullを返すので以下のように書けます。
fun decorate(name: String?) {
val decoratedName = name?.let { "★$it★" }
...
}
userId
がNullじゃなかったら検索しにいこう、とか。
val user = userId?.let { User.find(it) }
空文字じゃなければ実行、を代入と同時に
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)
}
おしまい
とりあえず思い付いたものを列挙してみました。何かまた思い付いたら追記していきたいと思います。