今回はスコープ関数。apply/also/let/run/withの紹介とまとめです。
あるインスタンスに対して、連続して処理をするような場合、スコープ関数を使えばコードを簡潔に書くことが可能になります。
「4種類も5種類もあってわけが分からん」という方、大丈夫です。5種類もあると面倒なので、まずはこの5種類を大きく2つに分けてしまいましょう。
スコープ関数を用途別に2つに分ける
こちらではスコープ関数を「どのような用途で使うのか」という観点から解説していきたいと思います。
プロパティを設定するapply,alsoと、メソッドを実行するlet,run,withです。
まずはapplyからスタートしていきます。
apply
applyは主にプロパティ設定用関数として使えるスコープ関数です。
例えば以下のようなクラスがあったとしましょう。
class Book(val name: String){
var price = 0
var author: String? = null
var pageNumber = 0
}
Bookクラスはコンストラクタと、3つの変更可能なプロパティを持ちます。
このクラスのインスタンスを生成してプロパティを設定するには、
fun main(){
val book = Book("既読スルーされた数だけ幸せになれる")
book.price = 827
book.author = "広中祐介"
println(book.price)
}
//827
このようにまずクラスのインスタンスを生成し、そのインスタンスの各プロパティを設定していくというのが通常の手順です。
ただこのコード、applyを使えばもっと簡潔なコードにできます。
fun main(){
val book = Book("既読スルーされた数だけ幸せになれる").apply{
price = 827
author = "広中祐介"
}
println(book.author)
}
//広中祐介
このコードでは、インスタンス生成と同時にapplyを呼び出し、その後にラムダ式でプロパティを設定しています。
戻り値はプロパティが設定されたレシーバになります。
ここで注目すべきは、プロパティ設定時にBookインスタンスを表記していないということです。
apply関数のラムダ式の中では、レシーバ(ここではBookインスタンス)を「this」で参照することもできますが、このthisは省略することが可能です。
同じインスタンスに対して何度も処理を重ねるような場合、このapplyを使うとコードを簡単に記述でき、可読性も高く保つことができます。
- applyは呼び出し元を「this」で参照し、thisは省略可能
- applyの戻り値は呼び出し元のインスタンス
also
Kotlin1.1で追加されたalso。こちらもapplyと似た使い方ですが、明確に違う点があります。
val book = Book("既読スルーされた数だけ幸せになれる").also{
it.price = 827
it.author = "広中祐介"
}
alsoの中のラムダ式で、Bookインスタンスは「it」で参照されています。こちらは省略不可です。
このitはラムダ式の引数が1つだけのときに使えるitであり、つまりalsoは呼び出し元を、ラムダの引数として扱っているということになります。
ラムダの内外でthisの意味が変わらないalso
applyの場合はラムダの中で(省略可能ですが)thisを使うため、クラスメソッドなどの内部処理で使用される場合、thisが指し示すものがラムダ内だけ変わってしまうという場合もあります。
その点alsoは呼び出し元の参照用としてのthisを使用しないため、ラムダの内も外もthisの意味合いが変わらないという利点があります。
逆にこれを利点と思えないような状況であれば、applyでいいやという感じ。
戻り値はapplyと同じく、呼び出し元のインスタンスが返ります。
- alsoは呼び出し元を「it」で参照し、省略不可
- 戻り値は呼び出し元のインスタンス
let
letはapplyやalsoとはまた違った利用方法で、プロパティを設定するのではなく、そのレシーバに対してメソッドを実行するといった使い方が主になってきます。
fun main(){
val cake = "strawberry cake".let{
println(it.toUpperCase()[3]) //大文字にした4文字目を標準出力
println(it[0] in 'm'..'t') //レシーバがmからtまでの文字から始まっているか
it.capitalize() //最初の文字だけ大文字に変換
}
println(cake)
}
//A
//true
//Strawberry cake
letのラムダ内では、呼び出し元は「it」で参照します。itが省略できない点はalsoと同じです。
戻り値はラムダの結果が入ります。最終的に変数cakeの値がcapitalizeされたものであることで、それを確認できると思います。
この戻り値の違いによって、これ以降の関数の用途はapplyやalsoとは異なるものとなります。
letでnull許容型を扱う
letの用途として、nullチェックとして使うという方法は最もポピュラーかもしれません。
fun main(){
val book = Book("オタクはすでに死んでいる")
book.author?.let{
println("著者: $it")
}?:println("著者は未設定です")
}
//著者は未設定です
Book型のauthorの初期値はnullです。ここではnull許容型のauthorプロパティを呼び出し、それがnullでなければラムダの結果が出力され、nullであればエルビス演算子の後のコードが実行されます。
セーフコールに続くletの後のラムダ内では、レシーバがnullでないことが確定できているため、ラムダ内ではレシーバをnull非許容型として扱えるということが利点としてあげられます。
- letは呼び出し元をitで参照し、省略不可
- letの戻り値はラムダの結果
run
runはイメージ的にletのapply版といった感じでしょうか。
letと出来ることはほぼ同じですが、レシーバを「this」として扱うところが異なります。
fun main(){
val book = Book("オタクはすでに死んでいる")
book.author = "岡田 斗司夫"
val otaku = book.author?.run{
"著者: $this"
}?:"著者は未設定です"
println(otaku)
}
//著者: 岡田 斗司夫
働きとしてはletと同じですがapplyとalsoの関係と同じように、runを使用する場合、ラムダの外側で「this」を使用していないかどうかチェックし、もし使用しているのであれば、そこはletを使うべき場面となるかもしれません。
- runは呼び出し元をthisで参照し、省略可
- runの戻り値はラムダの結果
with
withはこれまでのスコープ関数と記述方法が異なるため、若干異質の存在です。ただ働きとしてはrunと同じです。そのため、withはあまり見かけることは無いかもしれません。
val cake = with("strawberry cake"){
this.toUpperCase()
}
println(cake)
//STRAWBERRY CAKE
withは通常の関数のように呼び出し、2つの引数を取ります。第一引数にレシーバ、第二引数にラムダ式を記述しましょう。
戻り値はラムダの結果です。そのため、toUpperCaseされた後の結果である"STRAWBERRY CAKE"が、変数cakeに代入されています。
applyやrunと同じように、withのラムダ内では第一引数をthisで参照し、省略可能です。
- withは呼び出し元をthisで参照し、省略可
- withの戻り値はラムダの結果
おさらい
スコープ関数は種類があってどれも似たような感じに見えてしまい、混乱が起きやすいかもしれません。
まずはザックリと大きく分類して、それから細かな違いに目を向けていけば、理解も楽になると思います。
関数 | 呼び出し元の参照 (thisは省略可能) | 戻り値 |
---|---|---|
apply | this | 呼び出し元のインスタンス |
also | it | 同上 |
let | it | ラムダの結果 |
run | this | 同上 |
with | this | 同上 |
AndroidでKotlinを使う場合、thisの扱いにも注意しなければならない場面も多いので、alsoやletの出番が多くなるかもしれません。
逆にwithはrunで置き換えられるのもあってほとんど見ないかも。