14
6

More than 3 years have passed since last update.

スコープ関数の使いどころ(Kotlin)

Last updated at Posted at 2020-04-16

はじめに

Kotlinには「スコープ関数(Scope-Function)」という関数があります。
いくつか種類がありますが、どの関数も似ていて使い分けや使いどころがわからなかったのでまとめました。

注意

私は業務でKotlinを使っておらず、iOSアプリ開発でSwift(スコープ関数がない言語)を使っています。
そのため、Kotlinをメインで使っている人とは考え方が異なると思います。

スコープ関数を使い慣れていないため、考え方が変わることも大いにありえます。

前提条件

本記事を読むための前提条件です。

  • Kotlinのスコープ関数を理解している
    理解していない場合、公式ドキュメントをご参照ください

環境

  • Kotlin:1.3.61

結論

まず私の結論を述べます。

  • withrunapply は使わない
  • nullableな変数を処理したい
    • 戻り値を使う → let を使う
    • 戻り値を使わない
      • スマートキャストできる(= 変数が不変) → if を使う
      • スマートキャストできない(= 変数が可変) → let を使う
  • 変数のプロパティを変更したい・メソッドを実行したい
    • 戻り値を使う → also を使う
    • 戻り値を使わない → スコープ関数を使わない

ざっくりいうと、「できる限り可読性を損なわず、スコープ関数のメリットである「前の処理からメソッドチェーンで繋げられる(with以外)」「戻り値がある(メソッドチェーンで後ろに処理を繋げられる)」を活かせる場面で使う」ということです。

withrunapply を使わない理由

withrunapply を使わない理由を説明します。

with は他のスコープ関数と統一性がないので使わない

with でできることはすべて run でもできます。(後述しますが、私は run も使いません)
逆に、 with は拡張関数ではないので run のようにメソッドチェーンすることができません。

×withはメソッドチェーンできない
var holderView = getHolderView()
with(holderView) {
    tag = item
    setOnClickListener(mOnClickListener)
}
○runはメソッドチェーンできる
getHolderView().run {
    tag = item
    setOnClickListener(mOnClickListener)
}

他のスコープ関数と使い方を統一するためにも、拡張関数ではない with は使わないようにします。

runapplythis. の省略がわかりづらいので使わない

私がKotlinに慣れていないのもありますが、 this. が省略されていると処理が理解しづらいです。

×runはthisを省略できる
holder.view.run {
    tag = item // `tag` って何?
    setOnClickListener(mOnClickListener)
}
○letはitを省略できない
holder.view.let {
    it.tag = item
    it.setOnClickListener(mOnClickListener)
}

this. を省略した場合、同名の変数が追加されると意図しない動作に変わるので、不具合の温床にしないためにも runapply は使わないようにします。

×thisの省略は不具合の温床になる
var tag = 1 // 変数を追加
holder.view.run {
    tag = item // 変数で定義した `tag` に代入される
    setOnClickListener(mOnClickListener)
}

this と異なり、 it だと別名を付けられるのもメリットです。

○itは別名を付けられる
getMock(type)?.let { mock ->
    clearer.clear(arrayOf(mock), options)
}

letalso の使いどころ

消去法で残った letalso の使いどころを紹介します。

let はnullableな変数に使う

Kotlinには三項演算子がなく、nullかどうかで戻り値を切り替える場合には let が有用です。
エルビス演算子( ?: )と併用することで、スマートに記述できます。

Twitterで教えていただいたのですが、 let は最後に評価した値が戻り値になるため、エルビス演算子と併用すべきではなさそうです。
おとなしくif-elseを使うのが無難です。

○if-elseによるスマートキャストでも悪くはない
var foo: Int? = 3
val bar = if(foo != null) foo * 2 else 0 // 6
△letを使うとスマートに書けるが、ラムダ結果がnullになるとエルビス演算子側で処理されてしまう
var foo: Int? = 3
val bar = foo?.let { it * 2 } ?: 0 // 6

// `it.getInt()` が `null` の場合、 `0` が返ってしまう
hoge?.let { it.getInt() } ?: 0

ifと異なりメソッドチェーンが使えるので、処理をより短く記述できることがあります。

△if文だと変数に代入する必要がある
override fun clear(type: KClass<*>, options: MockKGateway.ClearOptions) {
    val mock = getMock(type)
    if (mock != null) {
        clearer.clear(arrayOf(mock), options)
    }
}
○letを使うと短く書ける
override fun clear(type: KClass<*>, options: MockKGateway.ClearOptions) {
    getMock(type)?.let {
        clearer.clear(arrayOf(it), options)
    }
}

引用:https://github.com/mockk/mockk/blob/0dd309232fc75906dc52e75c3f0dc89d9f2cc63e/mockk/jvm/src/main/kotlin/io/mockk/impl/instantiation/JvmConstructorMockFactory.kt#L185-L189

ただ、こちらの例はスコープ関数の戻り値を使っていないため、個人的には冗長でも変数に代入して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)
}
○letを使うとビルドエラーにならない
arguments?.let {
    columnCount = it.getInt(ARG_COLUMN_COUNT)
}

ただし、変数が不変(= val で定義されている)場合はスマートキャストを使いましょう。
そうすることで、戻り値なしで let が使われていると、「この変数は可変なんだな」と推測できるようになります。

also はプロパティの変更やメソッドの実行に使う

also はメソッドチェーンによるプロパティの変更やメソッドの実行に使います。

△スコープ関数を使わないと冗長になる
val ret = obj
    .javaClass
    .getDeclaredField("ret")
ret.isAccessible = true
ret.get(obj)
○alsoを使うとメソッドチェーンで書ける
obj
    .javaClass
    .getDeclaredField("ret")
    .also { it.isAccessible = true }
    .get(obj)

引用:https://github.com/mockk/mockk/blob/b0fe0ad2284ab3677a8eb477a68b8c6f28a3af32/mockk/jvm/src/test/kotlin/io/mockk/gh/Issue149Test.kt#L38-L42

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にスコープ関数があったらどうか?

私が有用だと考えている letalso について、Swiftにもあったら便利でしょうか。
let はif-letや三項演算子があるので不要そうですが、 also は有用だと感じます。

Twitterで教えていただいたのですが、Thenというライブラリを使えば、Swiftでも also と同様のことを実現できます。

InitializationClosureを使う
let label: UILabel = {
  let label = UILabel()
  label.textAlignment = .center
  label.textColor = .black
  label.text = "Hello, World!"
  return label
}()
Thenを使うとalsoと同じように書ける
let label = UILabel().then {
  $0.textAlignment = .center
  $0.textColor = .black
  $0.text = "Hello, World!"
}

Builderパターンと似ていますが、インスタンスの初期化時以外にも使える点が異なります。

おわりに

おそらくかなり偏った考えだと思います。
みなさんのスコープ関数の使いどころもぜひ教えてください!

参考リンク

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6