takeIfやtakeUnlessも、letやalsoのようなスコープ関数の一種です。
今回はこの2つのマイナーなスコープ関数にスポットを当て、仕様や使い方をご紹介します。しかし先に言ってしまうと「takeUnlessは使うな」がほぼ正解です。その理由も含めて見ていきましょう。
takeIf
inline fun <T> T.takeIf(predicate: (T) -> Boolean): T?
//predicateの結果がTrueならレシーバ「T」を、そうでなければnullを返す
takeIf
は結果がBooleanになるラムダ式(predicate)を取り、その結果がTrueであればレシーバ(呼び出し元)をそのまま、Falseであればnullを返すインライン関数です。

let
やalso
と同じように、ラムダ内ではレシーバを「it」で参照します。
val num: Int = 10
//変数ansはnull許容型
val ans: Int? = num.takeIf{it == 10}
println(ans) //10
ifで置き換える
これと全く同じコードをif式で表現するとこのようになります。
val num: Int = 10
val ans: Int? = if(num == 10) num else null
println(ans) //10
これだけ見るとtakeIf
は、はっきり言って「すごく微妙」です。記述量も可読性も、if式とほとんど差がありません。
takeIfの利点
nullチェック
しかしtakeIf
が適している場面もあります。例えば以下のようなコードではどうでしょう?
//null許容型を含むMap
val map: Map<Int, String?> = mapOf(1 to null, 2 to "53万", 3 to "たったの5")
//1~3の乱数
val num: Int = (1..3).shuffled().first()
//nullチェックをtakeIfで行う
val ans: String = map[num].takeIf{it != null}?:"0"
println("私の戦闘力は${ans}です") //私の戦闘力は53万です
レシーバである変数mapのvalueはnull許容型です。takeIf
はそれを「it」で参照し、値のnullチェックを行っています。
結果がfalseであった場合はエルビス演算子の右辺(この場合0)が出力されるため、変数ansに代入される値はnull非許容型です。
これをif式で記述すると、
val ans = if(map[num] != null) map[num].toString() else "0"
あまりスマートとは言えません。map[num]
が複数回参照されていることや、toString()
などの無駄なひと手間が原因です。
takeIf
を使うことで、(場合によっては)ifよりも記述量を減らし、可読性も保ったコードを書くことが可能になります。
takeUnless
inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T?
//predicateの結果がTrueならnullを、そうでなければレシーバ「T」を返す
上のtakeIf
と対をなすのがtakeUnless
です。ラムダ式がtrueの時はnullを返し、falseのときはレシーバを返します。
この関数はおすすめしません。「ラムダがfalseのとき、レシーバを返す」というのは人間から見て自然ではないからです。
val num: Int = 10
val ans: Int? = num.takeIf{it == 10}
val ans2: Int? = num.takeUnless{it != 10}
//どちらも10
どちらが自然に読めるでしょうか。大半の人は上のtakeIf
を選ぶはずです。
結果を反転させるなら他にも様々な方法があるし、そのような方法を取った方が迷わなくて済みます。あるいはそれが面倒と感じるなら、そこはifで攻めるべき場所かもしれません。
できるだけtakeUnless
は封印しておくべき関数です。もし他人が書いたコードにこの関数が混じっていたら、そっとtakeIf
に書き換えることを考えるくらいには。