スコープ関数を選ぶ
Kotlinの便利機能としてよく挙げられるスコープ関数ですが、
結構いろいろあってちょっとキャラ被ってるなってやつもあります。
その中でどれを使えばいいのか迷ったら最初のうちは「let」「also」がいいんじゃないかと思います。
(alsoはKotlin1.1からなので注意)
その理由は**「『自分自身』を名前で区別しやすい」**です。
val str: String = "Let!"
str.let{
println(it) // Let!
}
str.also{
println(it) // Let!
}
letとalsoはその内部で「スコープ関数を呼んだ自分自身」を「it」で表します。
しかしこれはデフォルトでそうなっているだけで明示的に指定することもできます。
letがネストした時などにわかりやすく書けます。
val str: String = "Let!"
str.let{elem ->
println(elem) // Let!
elem.toUpperCase().let{upp ->
println(upp) // LET!
}
}
letとalsoはこの同じルールで記述でき、
Collectionでのfilterなどとも同じなので統一しやすく、
そして2つの役割が明確に違うので使い分けもしやすくなっています。
ではその役割とは? という話ですが、
特にletは挙動をしっかりと考える必要があります。
letの正体を忘れがちになる理由としてよく見る以下のような話があるのではと思っています…
val str: String? = "Let!"
str?.let{it ->
println(it) // strがnullではない時だけ「Let!」が表示される
}
上記は変数strがString?のnullableとなっています。
そしてstr?.let{} と呼び出しているのでstrがnullの場合はletは動かずスキップされます。
動きとしては確かにその通りなのですが重要なことを見落としがちです。
なのでちょっとそこを考えたいと思います。
letとは一体何ものか
letの挙動の本質
まずはletについて公式のドキュメントを引いてみたいと思います。
inline fun T.let(block: (T) -> R): R
Calls the specified function block with this value as its argument and returns its result.
自分自身を引数に指定された関数を呼び出し、結果を返す。
このように書いてあります、つまりletとは
・letを呼び出した自分自身を引数に
・letの中に書かれた処理を関数として実行し
・(最後に評価された)結果を返す
という動作をするものです。
つまり高確率、ほぼ全ての場合において、
letの結果はletを呼び出したナニカとは別のナニカになっている
ことになります。
これをつい忘れて、さらにletやら何か他の関数やらをチェインして書くとバグる可能性があります。
ちなみにちょっとずれますがそもそもprintln(v) という関数の挙動は、
関数的に見たら「標準出力に書き出す作用」ではありません。
関数の役割として見たなら「標準出力に書き出す役割」ですがそれは関数的には**『副作用』**です。
printn(v)は
作用:何らかの引数vを受け取ってUnit型の戻り値を返す
副作用:vを標準出力に書き出す
が正しい作用の表現になります。
つまり、上の例に細かくコメントを書くなら
val str: String? = "Let!"
// strがnullではない時だけletの内部が処理され結果を返し、nullならnullを返します
str?.let{it ->
println(it) // it(今回はString型)の引数を渡され、Unitを返します
// printlnは標準出力にitの中身を書き出すという副作用を起こしますがそれはletとは関係なし
} // null か Unit が帰ってきてるのでここからさらに処理を続けるならその対象はUnit?
というわけで、letの本質を
・letの中でitを標準出力に書き出せる
・letの中でitをDBに書き出せる
・letの中でitをファイルに書き出せる
・上記の動作を?をつけて呼び出すことによりnullではない時にだけ呼び出せる
だけだと間違えてしまうとやらかします。
言ってしまえばletとはmapみたいなものです。
最後に評価された結果が返ってくるので、呼び出した本体とは何の関係もないものも返せます。
val str: String? = "Let!"
val let1 = str?.let{
println(it)
}
println(let1) // kotlin.Unit
val let2 = str?.let{
it.toLowerCase()
}
println(let2) // let!
val let3 = str?.let{
it.toUpperCase()
123
}
println(let3) // 123
ちなみにほぼ書くことはないと思いますが、letは(alsoなどもですが)型を明示的に書けます。
Kotlinは型を明示的に書かなくてもよかったり、
スコープ関数をチェインさせて変数を作らずに書きたくなったりしますがそこが罠になります。
明示的に書いてみると、間違いに気づくこともあります。
val let0 = str?.let<String, String>{
// この辺に処理が書いてあって
// 結果的にStringが返って欲しいものとする……
}
val let1 = str?.let<String, String>{
println(it) // Unitを返してしまうのでコンパイルエラー!
}
val let2 = str?.let<String, Unit>{
println(it) // OK
}
// 戻り値の方にも型を書くとこう。 strがnullの場合nullになるのでUnitではなくUnit?
val let3: Unit? = str?.let<String, Unit>{
println(it) // OK
}
str?.let{
println(it)
}?.let{ // 相手はUnit? になってるけど処理大丈夫?
// it は Unit
}
letの使い道
letは呼び出した元と違うものが返ってくるので、
使い道としてはmap的な動作、データの加工が基本だと思います。
例えばデータクラスをMapに変形したりできます。
fun action(){
val m = getData().let{
mapOf("name" to it.name, "address" to it.addr)
}
}
// 何かデータクラスを返す関数
fun getData(): Data{
return Data()
}
data class Data(val name: String = "MyName", val addr: String = "MyAddress")
あとは、関数Aの結果を関数Bが引数として使い、関数Bの結果を関数Cが引数として……
という記述をする時、
val result1 = funcC( funcB( funcA() ) )
// やっぱり上から下、左から右、外側から内側 の方向に読みたい!
val result2 = funcA().let{funcB(it)}.let{funcC(it)} // . で改行してもOK
のようにも書けます。
alsoとは
letの話が長くなりましたが、alsoです。
これも公式のドキュメントを見てみます。
inline fun T.also(block: (T) -> Unit): T
Calls the specified function block with this value as its argument and returns this value.
自分自身を引数に指定された関数を呼び出し、自分自身を返す。
alsoはletと違い、中で何をやろうとも呼び出した自分自身を返します。
val str: String? = "Also!"
val also1 = str?.also{
println(it)
}
println(also1) // Also!
val also2 = str?.also{
it.toLowerCase()
}
println(also2) // Also!
val also3 = str?.also{
it.toUpperCase()
123
}
println(also3) // Also!
なんらかのクラスの変数を作成しながらフィールドの値をセットしたりするのに使えます。
class Data{
var name: String = ""
var addr: String = ""
}
fun action(){
val data = Data().also{
it.name = "also"
it.addr = "standard"
}
}
言いたいこと
1.スコープ関数はCollectionのmapやfilterと同じ「it」で使えるletとalsoをまずは使うのがいいと思う
2.お手軽に使えるけれども特にletは「letの結果が何になっているか」に注意
3.「letの結果」という概念自体忘れやすいので注意