LoginSignup
1
1

More than 1 year has passed since last update.

[Kotlin] 「偽コンストラクタ」の作り方 その2

Posted at

[Kotlin] 「偽コンストラクタ」の作り方 その1 にて、トップレベル関数を利用した偽コンストラクタの作り方を説明しました。ここで、本物のコンストラクタの引数の型を変えたコンストラクタ、と見せかけて実は偽物、といった実装ができることを見てきましたが、トップレベル関数を用いる方法には若干の制約があります。

その1と同じく、コードの検証はKotlin 1.5.31で行っています。

トップレベル関数による偽コンストラクタの制約

保守的なJavaの文法を革新し自由な文法で設計されたKotlinといえども、以下のような実装はできないようです。

Multiplier.kt
class Multiplier private constructor(private val value: Int) {
    private constructor(value: Int) : this(value.toDouble()) { // compile error!!
        println("secondary constructor($value) called")
    }
    init {
        println("Multiplier($value) constructed")
    }

    fun calculate(multiplied: Double) = multiplied * value
}

fun Multiplier(value: Int): Multiplier { // compile error!!
    println("top-level Multiplier($value) called")
    return Multiplier(value = value.toDouble())
}

上のコードでは Multiplier(Int) の本物と偽物が衝突してしまっており、これはコンパイルしてくれません。上のコードでは、セカンダリコンストラクタを private にして1 Multiplier クラスのスコープ外からは偽コンストラクタの Multiplier(Int) だけが可視になるようにしているのですが、 Multiplier のスコープ内部だけであろうと区別できなかったら駄目、ということのようです。

当然、プライマリコンストラクタと同じ型の引数の偽コンストラクタもトップレベル関数では作れません。では本物コンストラクタと同じ型の引数の偽コンストラクタは作れないのでしょうか? 実は、トップレベル関数を使う以外にも偽コンストラクタを作る方法があり、そちらでは本物のコンストラクタと同じ引数の型が許されます。

偽コンストラクタの実装法 (2)

コンパニオンオブジェクトにinvoke演算子を書く、という方法でも偽コンストラクタを定義することができ、しかも本物のコンストラクタと同じ引数にしてもコンパイルエラーになりません。

Multiplier.kt
class Multiplier(private val value: Double) {
    private constructor(value: Int) : this(value.toDouble()) { // do not collide!!
        println("secondary constructor($value) called")
    }
    init {
        println("Multiplier($value) constructed")
    }

    fun calculate(multiplied: Double) = multiplied * value

    companion object {
        operator fun invoke(value: Int): Multiplier { // do not collide!!
            println("companion Multiplier.invoke($value) called")
            return Multiplier(value.toDouble())
        }
    }

    operator fun invoke(v: Int) {
        println("instance Multiplier.invoke($v) (not related to constructors)")
    }
}

上記のように書いても偽コンストラクタを定義することができ、以下のように使えます。

val c = Multiplier(5)
// => companion Multiplier.invoke(5) called
// => Multiplier(5.0) constructed

偽コンストラクタの実装方法としては、本物コンストラクタとの衝突制約なしにトップレベル関数と同じことができるので、この書き方に抵抗がない方は、同一クラス内で作成する偽コンストラクタをすべてこちらの方法で実装することにしてもよいでしょう。 Kotlinの「偽コンストラクタ」の作り方 その1 の DeviceManager の記述は以下のように DeviceManager.getInstance(...) の実装を直接 invoke(...) にすることができるので、記述量を少し減らせます。

DeviceManager.kt
interface DeviceManager {

    fun manage()

    companion object {
        private var instance: DeviceManagerImpl? = null

        operator fun invoke(context: Context): DeviceManager = instance
            ?: synchronized(this) {
                instance ?: run {
                    val deviceManager = DeviceManagerImpl(context)
                    instance = deviceManager
                    deviceManager
                }
            }

        operator fun invoke(): DeviceManager = instance!!
    }
}

private class DeviceManagerImpl(context: Context) : DeviceManager {
    private val nfc = NfcAdapter.getDefaultAdapter(context)

    override fun manage() {
        println("{NFC.isEnabled = ${nfc.isEnabled}")
    }
}

逆に、わけがわからない、という方は、 [Kotlin] 「偽コンストラクタ」の作り方 その1 に書かれた、トップレベル関数による実装でも困ることはあまりないと思います。どうしても本物のコンストラクタと同じ型の引数の偽コンストラクタを作らなければならない、というケースは少ないでしょう。それに、出来上がったクラスに対してFactoryパターンなどのインスタンス生成処理を追加するような開発フェーズの場合は、その1で紹介したトップレベル関数型の偽コンストラクタの実装が必要になるものと思われます2

なお、上のコードではコンパニオンオブジェクトのinvoke演算子の他にインスタンス演算子としてのinvoke演算子の実装を書いていますが、こちらは偽コンストラクタとは無関係で、比較用の蛇足です。

val a = Multiplier(5.0)
// => Multiplier(5.0) constructed
a(3) // or a.invoke(3)
// => instance Multiplier.invoke(3) (not related to constructors)

上記のように書くことができるようになりますが、偽コンストラクタにはなりません。

invoke演算子とは

invoke演算子 (invoke operator) については、別記事: [Kotlin] invoke演算子と多重定義、ラムダ式、関数オブジェクト をご覧ください。

ただし、こちらの記事はインスタンス関数(インスタンス演算子)としてのinvoke演算子について書いています。コンパニオンオブジェクトのinvoke演算子を記述する、という方法は、偽コンストラクタにしか使えない奇手といえます。

コンパニオンオブジェクトのinvoke演算子がなぜ偽コンストラクタになるのか?

Kotlinでクラスやインターフェイスの中に companion object を作成し、さらにその中に関数を定義すると、Javaのクラスメソッドに該当する関数になります。すなわち、クラスのインスタンスとは無関係に静的に使える関数になります。invoke演算子も以下のように静的な関数と同様に呼び出せます。

val b = Multiplier.invoke(5)
// => companion Multiplier.invoke(5) called

さらに invoke は演算子 () の別名ですので、以下のようにも書けます。これが偽コンストラクタになります。

val b = Multiplier(5)
// => companion Multiplier.invoke(5) called

ここまで書いてしまえば簡単な話なのですが、この書式を得るために「コンパニオンオブジェクトにinvoke演算子を定義する」というアイデアにはなかなか思い当たらないのではないでしょうか?

本物コンストラクタと偽コンストラクタの衝突?

ここからはほぼプログラミング言語マニア向けです。トップレベル関数が本物のコンストラクタとまったく同じ名前、かつ引数の型が一致したら衝突とみなされるのに、なぜ演算子多重定義だと共存できるのでしょうか?

上記 Multiplier のセカンダリコンストラクタは private に設定されていますので、セカンダリコンストラクタにアクセスできるのは以下の Multiplier クラス実装のスコープ内部からだけです。

Multiplier.kt
class Multiplier(private val value: Double) {
    private constructor(value: Int) : this(value.toDouble()) { // do not collide!!
        println("secondary constructor($value) called")
    }

    // Multiplier(Int) is accessible from here
    // ...

    companion object {
        operator fun invoke(value: Int): Multiplier { // do not collide!!
            println("companion Multiplier.invoke($value) called")
            return Multiplier(value.toDouble())
        }

        fun functionInCompanionObject1() = Multiplier(5)
        // => Multiplier(5.0) constructed
        // => secondary constructor(5) called

        fun functionInCompanionObject2() = Multiplier.invoke(5)
        // => companion Multiplier.invoke(5) called
        // => Multiplier(5.0) constructed
    }
}

fun topLevelFunc() = Multiplier(5)
// => companion Multiplier.invoke(5) called
// => Multiplier(5.0) constructed

Multiplier.kt のファイル内であっても Multiplier クラスのスコープ外にあるトップレベル関数 topLevelFunc() からはprivateコンストラクタは不可視ですので、 Multiplier(Int) で参照できるのはコンパニオンオブジェクトinvoke演算子だけです。したがって問題なく偽コンストラクタが実行されます。

問題は Multiplier クラススコープ内、ここでは functionInCompanionObject1(), functionInCompanionObject2() ですが、 Multiplier(Int) ならば本物のコンストラクタ、 Multiplier.invoke(Int) ならば偽コンストラクタが実行されるようです。 invoke() の別名ですので、可視の本物コンストラクタもトップレベル関数型偽コンストラクタも存在しなければ Multiplier(Int) でも Multiplier.invoke(Int) でも呼び出せますが、いずれかが存在する場合に呼び出す方法は Multiplier.invoke(Int) という書き方に限られるようです。

ここでは Multiplier クラススコープ内のみで Multiplier(Int) が競合するので、 Multiplier クラススコープ内のみでこの書き分けが必要になります。仮に constructor(value: Int) の可視性を internal や public(=省略)などにしてより広範囲で可視となった場合は、その可視の範囲内でこの書き分けが必要になるようです。いずれもコンパイルエラーにならないところがトップレベル関数型の偽コンストラクタと異なります。

シグナチャと引数の型が一致し、かつ可視である場合に限り演算子 () が別名しか使えなくなる、という言語仕様は、以下のように考えるとすんなり理解できそうに思われます。

/**
 * Extension function
 */
fun Activity.onStart() {
    // ...
}

fun callOnStart(activity: Activity) {
    activity.onStart()
}

上のコードの Activity クラスにメンバ関数 onStart() が存在しないか、またはprivateやprotectedであった場合、 Activity インスタンスで拡張関数 (Extension function) onStart() が使えるようになりますが、もしpublicなメンバ関数 onStart()Activity クラスにすでに存在する場合は、拡張関数 onStart()を後付けで定義しても既存のメンバ関数 onStart() に隠されるため呼び出されることはありません。

ひるがえってinvoke演算子型の偽コンストラクタは、本物のコンストラクタから見るとコンパニオンオブジェクトを利用した後付け実装なので、本物のコンストラクタに隠されてしまう、しかし本物のコンストラクタには invoke という別名がないため、別名を使えばアクセスできる、と考えることができそうです。

とまぁ、このような問題で悩むことはあまり多くないでしょうが、多重定義した演算子で困ることがあった場合は、ひとまず別名を使って解決するかどうか確かめてみるのがよいでしょう。

偽コンストラクタまとめ

記事2つにわたって「偽コンストラクタ」について書いてきました。invoke演算子型の偽コンストラクタはトリッキーですが、この書き方に抵抗がなければ、トップレベル関数型の偽コンストラクタよりも合理的に使えるように思います。

今回、このテーマでKotlinのコードをいろいろといじってみて、相当に複雑な文法の言語だと筆者は感じました。Javaとの比較で言えば、 new を廃止したことで、インスタンスを生成する箇所では必ず new が現れる、という原則がなくなり、コンストラクタによるインスタンス生成とFactoryパターンなどとの違いを必要以上に意識することなくコーディングができるようになって、スッキリと考えやすくなったのではないかと思います。ただしそのためには、偽コンストラクタなどを実装するプログラマーはインスタンスのライフサイクル管理やメモリ管理を適切かつ簡明に行う責務を果たさなければならないでしょう。

FactoryパターンやBuilderパターンなどを含んだインスタンス生成を「なんでも(本物/偽)コンストラクタで行う」というアイデアは、かなり有力なのではないかと筆者は考えます。このテクニックを活用したコーディング流儀やライブラリなどが広まれば面白いと思います。

関連記事

参考文献


  1. [Kotlin] 「偽コンストラクタ」の作り方 その1 に書いたのと同じく、これは説明用のコードで、現実には本物のコンストラクタと同じ機能の偽コンストラクタを作る必要はないでしょう。 

  2. 「拡張invoke演算子」で偽コンストラクタが実装できるなら、すべての偽コンストラクタをinvoke演算子型で実装できるかもしれませんが、 operator fun ClassName.invoke(...) { ... } のように記述しても、インスタンス演算子としてのinvoke演算子の実装を書くことになってしまい、偽コンストラクタになりません。偽コンストラクタにするには、コンパニオンオブジェクトの拡張invoke演算子を記述しなければなりませんが、その方法を筆者はまだ見つけていません。そのような方法が存在しなくてもおかしくない(現バージョンでは)ですが、将来のバージョンでは何かが変わるかもしれません。 

1
1
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
1
1