一度だけ実行される関数
他の方が書かれた記事「Kotlinで一度しか行わない処理を実現する」のコメントで、一度だけ実行される関数の改善案を提示しました。
そのコメントで提示したのはスニペットでしたが、一般化してユーティリティー関数にしたので公開します。
使用例
まず使用例です。
class Dog {
// 一度だけ実行できる関数(を返すプロパティ)を定義する。
val go = once {
// 一度だけ実行したい処理を記述する。
"Gone!"
}
}
fun main(vararg args: String) {
val dog = Dog()
// 一度目の呼び出し
dog.go()
?.let { println(it.result) } // > Gone!
?: println("Do nothing") // ここにはこない
// 二度目の呼び出し
dog.go()
?.let { println(it.result) } // ここにはこない
?: println("Do nothing") // > Do nothing
}
今回作成したユーティリティー関数 once
は、「一度だけ実行できる関数」を生成します。それを使って「一度だけ実行できる関数(を返すプロパティ)」を定義します。
once
のブロックに「一度だけ実行したい処理」を書きます。
定義した関数を呼び出すと(定義したプロパティから関数を取得して実行すると)、一度目は null
でないオブジェクトが返ります。そのオブジェクトの result
プロパティに、「一度だけ実行したい処理」の返値が入っています。
二度目以降の呼び出しでは null
が返ります。
ユーティリティー関数 once
の実装
今回作成したユーティリティー関数は、次のような実装になっています。
/**
* 処理が一度だけ実行できる関数を生成する。
*
* 生成された関数は、二度目以降の呼び出しでは何もしない。
*
* 生成された関数を実行した際の返値は、
* 一度目の呼び出しでは処理の結果を保持する[OnceResult]型のオブジェクト(非 null )。
* 二度目以降の呼び出しでは null 。
*
* @param block 一度だけ実行したい処理。
*/
fun <R> once(block: () -> R): () -> OnceResult<R>? = run {
var isDone = false;
// run の返値となるラムダ式
{
if (isDone) {
null
} else {
isDone = true
val result = block()
OnceResult(result)
}
}
}
/**
* [once]で生成した関数が返す、
* 実行した処理の返値を保持するためのクラス。
*/
data class OnceResult<R>(val result: R)
関数 once
は、「一度だけ実行できる関数」を生成します。
「一度だけ実行できる関数」を実行すると、
- 一度目の実行:ラムダ式で渡した処理が実行され、その返値を保持した
OnceResult<R>
オブジェクトが返ります。 - 二度目以降の実行:何も処理されず、
null
が返ります。
返値が OnceResult<R>
でラップされているのは、「一度だけ実行できるようにしたい処理」の返値が nullable であっても良いようにするためです。
もし OnceResult<R>?
ではなく R?
としたら、一度目の実行で「一度だけ実行できるようにしたい処理」が null
を返したのか、それとも二度目以降の実行なのか、を区別できません。
引数を持たせる
次のようにすれば、引数を持たせることもできます。
fun <R, A0> once(block: (A0) -> R): (A0) -> OnceResult<R>? = run {
var isDone = false;
// 返値となるラムダ式
{ a1 ->
if (isDone) {
null
} else {
isDone = true
val result = block(a1)
OnceResult(result)
}
}
}
欠点は、引数の数が異なるオーバーロードがあると、「一度だけ実行できる関数(を返すプロパティ)」を定義する際に型パラメーターを明示しなければならなくなることです。
class Dog {
// 引数なし
val go = once<String> {
// 一度だけ実行したい処理を記述する。
"Gone!"
}
// 引数1つ
val goTo = once<String, Double> {
// 一度だけ実行したい処理を記述する。
"Gone! (distance: ${it}m})"
}
}
fun main(vararg args: String) {
go()
goTo(100.0)
}
一度だけ値をセットできるプロパティ
ついでに「一度だけ値をセットできるプロパティ」を定義するためのデリゲートも作成しました。
使用例
使用例です。
class Cat {
// 一度だけ値をセットできるプロパティを定義する。
var name: String by WriteOnce()
}
fun main(vararg args : String ) {
val cat = Cat()
cat.name = "Nyanko" // 一度目のセット
println("name: ${cat.name}") // > name: Nyanko
cat.name = "Mike" // 二度目のセット。 IllegalStateException がスローされる。
}
プロパティ定義は var
にして、 by WriteOnce()
をつけます。( WriteOnce
が今回実装したクラスです。)
プロパティ定義では型を明示する必要があります。
WriteOnce
のコンストラクターが型推論できるようにするためです。
( var name by WriteOnce<String>()
のように書くこともできますが、これだと name
の型が明示されないので、コードの可読性が下がります。)
二度以上値をセットしようとした場合、および値がセットされていないのに値をゲットしようとした場合は、 IllegalStateException
がスローされます。
デリゲートの実装
デリゲートの実装は次のようになっています。
/**
* 一度だけ値をセットできるプロパティのデリゲート。
*
* 二度以上値をセットしようとした場合、および値をセットせずにゲットしようとした場合は
* [IllegalStateException]がスローされる。
*/
class WriteOnce<T> {
// T が nullable であっても良いように、 T? 型ではなく Value<T>? 型にしている。
// Either (https://arrow-kt.io/docs/datatypes/either/) のようなものにするのも良いだろう。
/**
* セットされた値を保持するオブジェクト。
* まだセットされていなければ null 。
*/
private var value: Value<T>? = null
/** 値がセットされたかどうか */
val isSet: Boolean
get() = value != null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
value.let {
if (it == null) {
throw IllegalStateException()
} else {
it.value
}
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (this.value == null) {
this.value = Value(value)
} else {
throw IllegalStateException()
}
}
/** セットされた値を保持するためのクラス。 */
private data class Value<T>(
val value: T
)
}
Nullable な値をセットできるようにするため、値を Value<T>
でラップして保持しています。
値がセットされたかどうかを判別するためのプロパティ isSet
も用意しました。
これを使用するには次のようにします。
class Cat {
private val nameDelegate = WriteOnce<String>()
// 一度だけ値をセットできるプロパティを定義する。
var name: String by nameDelegate
val isNameSet
get() = nameDelegate.isSet
}
fun main(vararg args: String) {
val cat = Cat()
cat.name = "Nyanko"
if (cat.isNameSet) {
println(cat.name)
}
}
さいごに
ことりんかわいい!
(↑Kotlin Fest 2018に参加してきました。)