はじめに
Kotlinには「スコープ関数(Scope-Function)」という関数があります。
いくつか種類がありますが、どの関数も似ていて使い分けや使いどころがわからなかったのでまとめました。
注意
私は業務でKotlinを使っておらず、iOSアプリ開発でSwift(スコープ関数がない言語)を使っています。
そのため、Kotlinをメインで使っている人とは考え方が異なると思います。
スコープ関数を使い慣れていないため、考え方が変わることも大いにありえます。
前提条件
本記事を読むための前提条件です。
- Kotlinのスコープ関数を理解している
理解していない場合、公式ドキュメントをご参照ください
環境
- Kotlin:1.3.61
結論
まず私の結論を述べます。
-
with
・run
・apply
は使わない - nullableな変数を処理したい
- 戻り値を使う →
let
を使う - 戻り値を使わない
- スマートキャストできる(= 変数が不変) →
if
を使う - スマートキャストできない(= 変数が可変) →
let
を使う
- スマートキャストできる(= 変数が不変) →
- 戻り値を使う →
- 変数のプロパティを変更したい・メソッドを実行したい
- 戻り値を使う →
also
を使う - 戻り値を使わない → スコープ関数を使わない
- 戻り値を使う →
ざっくりいうと、「できる限り可読性を損なわず、スコープ関数のメリットである「前の処理からメソッドチェーンで繋げられる(with以外)」「戻り値がある(メソッドチェーンで後ろに処理を繋げられる)」を活かせる場面で使う」ということです。
with
・ run
・ apply
を使わない理由
with
・ run
・ apply
を使わない理由を説明します。
with
は他のスコープ関数と統一性がないので使わない
with
でできることはすべて run
でもできます。(後述しますが、私は run
も使いません)
逆に、 with
は拡張関数ではないので run
のようにメソッドチェーンすることができません。
var holderView = getHolderView()
with(holderView) {
tag = item
setOnClickListener(mOnClickListener)
}
getHolderView().run {
tag = item
setOnClickListener(mOnClickListener)
}
他のスコープ関数と使い方を統一するためにも、拡張関数ではない with
は使わないようにします。
run
・ apply
は this.
の省略がわかりづらいので使わない
私がKotlinに慣れていないのもありますが、 this.
が省略されていると処理が理解しづらいです。
holder.view.run {
tag = item // `tag` って何?
setOnClickListener(mOnClickListener)
}
holder.view.let {
it.tag = item
it.setOnClickListener(mOnClickListener)
}
this.
を省略した場合、同名の変数が追加されると意図しない動作に変わるので、不具合の温床にしないためにも run
・ apply
は使わないようにします。
var tag = 1 // 変数を追加
holder.view.run {
tag = item // 変数で定義した `tag` に代入される
setOnClickListener(mOnClickListener)
}
this
と異なり、 it
だと別名を付けられるのもメリットです。
getMock(type)?.let { mock ->
clearer.clear(arrayOf(mock), options)
}
let
・ also
の使いどころ
消去法で残った let
・ also
の使いどころを紹介します。
let
はnullableな変数に使う
Kotlinには三項演算子がなく、nullかどうかで戻り値を切り替える場合には let
が有用です。
エルビス演算子( ?:
)と併用することで、スマートに記述できます。
Twitterで教えていただいたのですが、 let
は最後に評価した値が戻り値になるため、エルビス演算子と併用すべきではなさそうです。
おとなしくif-elseを使うのが無難です。
var foo: Int? = 3
val bar = if(foo != null) foo * 2 else 0 // 6
var foo: Int? = 3
val bar = foo?.let { it * 2 } ?: 0 // 6
// `it.getInt()` が `null` の場合、 `0` が返ってしまう
hoge?.let { it.getInt() } ?: 0
ifと異なりメソッドチェーンが使えるので、処理をより短く記述できることがあります。
override fun clear(type: KClass<*>, options: MockKGateway.ClearOptions) {
val mock = getMock(type)
if (mock != null) {
clearer.clear(arrayOf(mock), options)
}
}
override fun clear(type: KClass<*>, options: MockKGateway.ClearOptions) {
getMock(type)?.let {
clearer.clear(arrayOf(it), options)
}
}
ただ、こちらの例はスコープ関数の戻り値を使っていないため、個人的には冗長でも変数に代入してif文で記述したいです。
どうしても戻り値を使わずにスコープ関数を使いたい場合、個人的には自身を返す also
を使いたいですが、 let
の方がなんとなくnullableの変数を扱う感がある ので、このままとします。
変数が可変である(= var
で定義されている)場合、スマートキャストが使えません。
その場合、戻り値を使わなくても let
を使うのが有用です。
// `arguments` が可変のため、以下のビルドエラーが発生する
// Smart cast to 'Bundle' is impossible, because 'arguments' is a mutable property that could have been changed by this time
if (arguments != null) {
columnCount = arguments.getInt(ARG_COLUMN_COUNT)
}
arguments?.let {
columnCount = it.getInt(ARG_COLUMN_COUNT)
}
ただし、変数が不変(= val
で定義されている)場合はスマートキャストを使いましょう。
そうすることで、戻り値なしで let
が使われていると、「この変数は可変なんだな」と推測できるようになります。
also
はプロパティの変更やメソッドの実行に使う
also
はメソッドチェーンによるプロパティの変更やメソッドの実行に使います。
val ret = obj
.javaClass
.getDeclaredField("ret")
ret.isAccessible = true
ret.get(obj)
obj
.javaClass
.getDeclaredField("ret")
.also { it.isAccessible = true }
.get(obj)
let
でもいいましたが、戻り値を使わない場合、個人的にはスコープ関数を使いたくありません。
スコープ関数に慣れていない人が少しでも読みやすいようにしたいです。
holder.view.also {
it.tag = item
it.setOnClickListener(mOnClickListener)
}
holder.view.tag = item
holder.view.setOnClickListener(mOnClickListener)
ある変数に対する処理をまとめる場合、変数の初期化から始めると戻り値が使われ、処理がまとまって可読性も上がると思います。
val button = Button(this).also {
it.text = "Foo"
it.setOnClickListener {
startActivity(Intent(this, NextActivity::class.java))
}
}
参考:https://qiita.com/SYABU555/items/c6d828b4c29c545a58f3#tips
SwiftのInitialization Closureみたいで読みやすいです。
おまけ:Swiftにスコープ関数があったらどうか?
私が有用だと考えている let
と also
について、Swiftにもあったら便利でしょうか。
let
はif-letや三項演算子があるので不要そうですが、 also
は有用だと感じます。
Twitterで教えていただいたのですが、Thenというライブラリを使えば、Swiftでも also
と同様のことを実現できます。
let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.text = "Hello, World!"
return label
}()
let label = UILabel().then {
$0.textAlignment = .center
$0.textColor = .black
$0.text = "Hello, World!"
}
Builderパターンと似ていますが、インスタンスの初期化時以外にも使える点が異なります。
おわりに
おそらくかなり偏った考えだと思います。
みなさんのスコープ関数の使いどころもぜひ教えてください!
参考リンク
- Scope Functions - Kotlin Programming Language
- https://twitter.com/kotlin/status/1425088549075001354?s=20
- Kotlin スコープ関数 用途まとめ - Qiita
- Kotlinのスコープ関数の定義を読み解く - Qiita
- Kotlin スコープ関数の使い分け - Qiita
- Kotlinのif let - Qiita
-
Kotlinアンチパターン
p.23-31 - ウホーイさんはTwitterを使っています 「スコープ関数のメリットって以下の2つが大きいと思う ・メソッドチェーンできる(with以外) ・戻り値がある 実は↓は上2つのメリットのおまけに過ぎないかもしれない ・複数の処理を実行できる」 / Twitter