When式で型で条件分岐していても、プロパティがMutableなのでスマートキャストしてくれない! 時の対処法

5 min読了の目安(約4500字TECH技術記事

この記事はQrunchのサービス終了に伴う移行のための投稿です。

Qiita→Qrunch移行のためのQiita版の自己転載です。

最近、コード生成の闇に飲まれているトリナーです。
4桁行の生成されたコードを2桁規模の数生成している中で生まれた知見です。
そのため、サンプルコードが自動生成臭いコードになっているのは仕様ですのでご了承ください。

発生した問題

以下のようなコードを書いたとします。というか書いてます。

class XxxBuilderScope {
    var duplicatedField: DuplicatedField? = null

    fun String.toDuplicatedField() = DuplicatedField.String(this)
    fun Int.toDuplicatedField() = DuplicatedField.Int(this)
    fun Regex.toDuplicatedField() = DuplicatedField.Regex(this)operator fun DuplicatedField?.div(value: Int) = DuplicatedField.Int(value)
    operator fun DuplicatedField?.div(value: String) = DuplicatedField.String(value)
    operator fun DuplicatedField?.div(value: Regex) = DuplicatedField.Regex(value)

    sealed class DuplicatedField {
        data class String(val value: kotlin.String) : DuplicatedField()
        data class Int(val value: kotlin.Int) : DuplicatedField()
        data class Regex(val value: kotlin.text.Regex) : DuplicatedField()
    }

    fun build() {
        val builder = Unit
        when(duplicatedField) {
            is DuplicatedField.String -> println(duplicatedField.value)
            is DuplicatedField.Int -> println(duplicatedField.value)
            is DuplicatedField.Regex -> println(duplicatedField.value)
        }
    }
}

このコードはコンパイルできません。以下の様なエラーがでます。
Error:(26, 50) Kotlin: Smart cast to 'XxxBuilderScope.DuplicatedField.String' is impossible, because 'duplicatedField' is a mutable property that could have been changed by this time
はい。タイトル回収です。
ミュータブルなプロパティは基本的に他スレッドからの書き換えの可能性があるのでスマートキャストできません。
すごーい!kotlincはスレッドセーフティを考えている賢いコンパイラーなんだね!

解決法

一般的な対処法は2通りです。手動キャストするか、valなローカル変数に一度再代入するか
ここでは、後者の方法とKotlin 1.3で新たに追加された構文を使ってエレガントに解決します。
修正されたコードはwhen部分だけ示します。

when(val v = duplicatedField) {
    is DuplicatedField.String -> println(v.value)
    is DuplicatedField.Int -> println(v.value)
    is DuplicatedField.Regex -> println(v.value)
}

ね、簡単でしょ? (cv: チュウニペンギン)


おまけ

サンプルコードを生成するコードの一部(Kotlinpoet)
なおbuild()は別の箇所
こちらOSSです。justincase-jp/AWS-CDK-Kotlin-DSL (宣伝)

private fun TypeSpec.Builder.addPropertyForDuplicatedMethods(name: String, methods: List<KFunction<*>>) {
        val decapitalName = name.decapitalize()
        val capitalName = name.capitalize()

        // sealed class DuplicatedField
        val sealedType = TypeSpec.classBuilder(capitalName)
            .addModifiers(KModifier.SEALED)

        // var duplicatedField: DuplicatedField? = null
        val sealedClassName = ClassName("", capitalName)
        val prop = PropertySpec.builder(decapitalName, sealedClassName.copy(nullable = true))
            .initializer("null")
            .mutable(true)
            .build()
        addProperty(prop)

        methods.forEach { func ->
            val parameterType = func.parameters.single { it.kind == KParameter.Kind.VALUE }.type.classifier as KClass<*>
            // data class String(val value: kotlin.String) : DuplicatedField()
            val constructor = FunSpec.constructorBuilder()
                .addParameter("value", parameterType)
                .build()
            val clazz = TypeSpec.classBuilder(parameterType.simpleName!!)
                .primaryConstructor(constructor)
                .addProperty(
                    PropertySpec.builder("value", parameterType)
                        .initializer("value")
                        .build()
                ).superclass(sealedClassName)
                .build()
            sealedType.addType(clazz)

            // fun String.toDuplicatedField() = DuplicatedField.String(this)
            val converterFunc = FunSpec.builder("to$capitalName")
                .receiver(parameterType)
                .returns(sealedClassName)
                .addStatement("return $capitalName.${parameterType.simpleName}(this)")
                .build()
            addFunction(converterFunc)

            // operator fun DuplicatedField?.div(value: Int) = DuplicatedField.Int(value)
            val operatorFunc = FunSpec.builder("div")
                .receiver(parameterType.asClassName().copy(nullable = true))
                .addParameter("value", parameterType)
                .returns(sealedClassName)
                .addStatement("return $capitalName.${parameterType.simpleName}(value)")
                .build()
            addFunction(operatorFunc)
        }

        addType(sealedType.build())
    }