LoginSignup
3
5

More than 5 years have passed since last update.

【Kotlin】一度だけ実行できる関数、一度だけ値をセットできるプロパティ

Posted at

一度だけ実行される関数

他の方が書かれた記事「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に参加してきました。)

3
5
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
3
5