Kotlinで特定のブロック内でのみ有効な拡張関数を定義する

Kotlinのドキュメントに、Type-Safe Buildersという記事があります。 これは、Groovyでよく使われている builders パターンというものを、Kotlinで表現した際にどうかるかを示したドキュメントです。

kotlinlang.org

Groovy使ったことないですが、build.gradleの定義にも使われているこういう書き方を指します。

buildscript {

    ext {
        
        kotlinVersion = '1.0.0'

    }

}

要は、独自のブロックを定義して、その中で初期化処理を実行しましょうということです。 Kotlinのブロックについては、親子関係を持つ独自のブロックを作るという内容で「逆引きKotlin」に追加されています。

親子関係のある独自のブロックを作りたい - 逆引きKotlin - 逆引きKotlin

今回は、この機能を利用した「特定のブロック内でのみ有効な拡張関数」を定義する方法について紹介したいと思います。

元ネタはこちらです。

discuss.kotlinlang.org

やりたいこと

例として、ブロックを抜けたら指定したリソースを開放するブロックusingを定義します。

開放するリソースを指定する拡張関数autoClose()を、usingブロック内でのみ有効化します。

using {

    val in = Files.newInputStream("in.txt").autoClose()
    val out = Files.newOutputStream("out.txt").autoClose()

    // ごにょごにょ

}

定義

ポイントは、関数のレシーバーに指定したクラス内で拡張関数を定義することです。 こうすることで、クラス内でのみ拡張関数が有効となります。

usingに渡されたブロックは、レシーバーを指定したことによりレシーバー内のスコープとなり、拡張関数が有効となる仕組みです。

// usingブロック
fun <R> using(block: ResourceHolder.() -> R): R {
    ResourceHolder().use {
        return it.block()
    }
}

// レシーバークラス
class ResourceHolder : Closeable {

    val resources = mutableListOf<Any>()

    // Closeable インターフェースを実装したクラスに対する拡張関数
    fun <T: Closeable> T.autoClose(): T {
        resources.add(this)
        return this
    } 

    // AutoCloseable インターフェースを実装したクラスに対する拡張関数
    fun <T: AutoCloseable> T.autoClose(): T {
        resources.add(this)
        return this
    } 

    override fun close() {
        resources.reverses().forEach {
            when (it) {
                is Closeable -> it.close()
                is AutoCloseable -> it.close()
            }
        }
    }

}

使い方

使い方は「やりたいこと」に書いたとおりです。

定義した拡張関数 autoClose() は、usingブロック以外からは呼び出すことが出来ず、コンパイルエラーとなります。

// テスト用のクラス
class TestCloseable : Closeable {
    override fun close() {
        println("TestCloseable")
    }
}
class TestAutoCloseable : AutoCloseable {
    override fun close() {
        println("TestAutoCloseable")
    }
}

@Test
fun usingTest() {

    TestCloseable().autoClose() // コンパイルエラー!

    using {

        TestCloseable().autoClose()
        TestAutoCloseable().autoClose()
        TestCloseable().autoClose()
        TestAutoCloseable().autoClose()
        TestCloseable().autoClose()
        TestCloseable().autoClose()
        TestAutoCloseable().autoClose()

    }

    // 出力
    // TestAutoCloseable
    // TestCloseable
    // TestCloseable
    // TestAutoCloseable
    // TestCloseable
    // TestAutoCloseable
    // TestCloseable

}

まとめ

特定のブロック内でのみ有効な拡張関数を定義する方法について紹介しました。

上記のようなリソース開放の他、トランザクションブロック内ではStringを直接SQLとして実行できるようにする、という使い方もできます。

拡張関数は便利だけど、使わせる範囲を明示的に指定したい場合に使ってみてください。