BETA

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

投稿日:2020-03-26
最終更新:2020-03-26

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())  
    }  
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@tolinerのあれこれ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう