[Kotlin]僕たちだってスコープ関数!takeIfとtakeUnless

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を返すインライン関数です。

[Kotlin]インライン関数 - inlineの一言で何が変わるのか
inline修飾子が付いた関数はインライン関数として動きます。インライン関数は主...

letalsoと同じように、ラムダ内ではレシーバを「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に書き換えることを考えるくらいには。

タイトルとURLをコピーしました