Force compilation error with sealed classes

后端 未结 6 1855
我寻月下人不归
我寻月下人不归 2020-12-28 12:12

With sealed classes you can use exhaustive when expressions and omit the else clause when the expression returns a result:

sealed c         


        
相关标签:
6条回答
  • 2020-12-28 12:39

    We can create an extension property on type T with a name that helps explain the purpose

    val <T> T.exhaustive: T
        get() = this
    

    and then use it anywhere like

    when (sealedClass) {
            is SealedClass.First -> doSomething()
            is SealedClass.Second -> doSomethingElse()
        }.exhaustive
    

    It is readable, shows exactly what it does an will show an error if all cases are not covered. Read more here

    0 讨论(0)
  • 2020-12-28 12:43

    In inspiration by Voddan's answer, you can build a property called safe you can use:

    val Any?.safe get() = Unit
    

    To use:

    when (sealedClass) {
        is SealedClass.First -> doSomething()
        is SealedClass.Second -> doSomethingElse()
    }.safe
    

    I think it provides a clearer message than just appending .let{} or assigning the result to a value.


    There is an open issue on the Kotlin tracker which considers to support 'sealed whens'.

    0 讨论(0)
  • 2020-12-28 12:49

    A discussion triggered me to look for a more general solution and found one, for Gradle builds. It doesn't require changing the source code! The drawback is that compilation may become noisy.

    build.gradle.kts

    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
        val taskOutput = StringBuilder()
        logging.level = LogLevel.INFO
        logging.addStandardOutputListener { taskOutput.append(it) }
        doLast {
            fun CharSequence.hasInfoWithError(): Boolean =
                "'when' expression on sealed classes is recommended to be exhaustive" in this
            if (taskOutput.hasInfoWithError()) {
                throw Exception("kotlinc infos considered as errors found, see compiler output for details.")
            }
        }
    }
    

    build.gradle

    tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
        def taskOutput = new StringBuilder()
        logging.level = LogLevel.INFO
        logging.addStandardOutputListener(new StandardOutputListener() {
            void onOutput(CharSequence text) { taskOutput.append(text) }
        })
        doLast {
            def hasInfoWithError = { CharSequence output ->
                output.contains("'when' expression on sealed classes is recommended to be exhaustive")
            }
            if (hasInfoWithError(taskOutput)) {
                throw new Exception("kotlinc infos considered as errors found, see compiler output for details.")
            }
        }
    }
    
    

    Notes:

    • Change implementation of hasInfoWithError to generalize to other i:s.
    • Put this code in subprojects { } or allprojects { } to apply project-wide.

    References:

    • Issue tracking making missing sealed case as warning:
      (which together with kotlinOptions.allWarningsAsErrors would solve the issue)
      https://youtrack.jetbrains.com/issue/KT-37651
    • Discussion triggering this solution:
      https://youtrack.jetbrains.com/issue/KT-12380#focus=streamItem-27-4017839.0-0
    • Repo with working example:
      https://github.com/TWiStErRob/repros/tree/master/kotlin/fail-on-sealed-when-info
    0 讨论(0)
  • 2020-12-28 12:55

    The way to enforce exhaustive when is to make it an expression by using its value:

    sealed class SealedClass {
        class First : SealedClass()
        class Second : SealedClass()
        class Third : SealedClass()
    }
    
    fun test(sealedClass: SealedClass) {
        val x = when (sealedClass) {
            is SealedClass.First -> doSomething()
            is SealedClass.Second -> doSomethingElse()
        }  // ERROR here
    
        // or
    
        when (sealedClass) {
            is SealedClass.First -> doSomething()
            is SealedClass.Second -> doSomethingElse()
        }.let {}  // ERROR here
    }
    
    0 讨论(0)
  • 2020-12-28 13:02

    Consider using the recent library by JakeWharton that allows to just use @Exhaustive annotation.

    sealed class RouletteColor {
      object Red : RouletteColor()
      object Black : RouletteColor()
      object Green : RouletteColor()
    }
    
    fun printColor(color: RouletteColor) {
      @Exhaustive
      when (color) {
        RouletteColor.Red -> println("red")
        RouletteColor.Black -> println("black")
      }
    }
    

    Usage:

    buildscript {
      dependencies {
        classpath 'app.cash.exhaustive:exhaustive-gradle:0.1.1'
      }
      repositories {
        mavenCentral()
      }
    }
    
    apply plugin: 'org.jetbrains.kotlin.jvm' // or .android or .multiplatform or .js
    apply plugin: 'app.cash.exhaustive'
    

    Lib: https://github.com/cashapp/exhaustive

    0 讨论(0)
  • 2020-12-28 13:04

    Our approach avoids to have the function everywhere when auto-completing. With this solution you also have the when return type in compile time so you can continue using functions of the when return type.

    Do exhaustive when (sealedClass) {
      is SealedClass.First -> doSomething()
      is SealedClass.Second -> doSomethingElse()
    }
    

    You can define this object like so:

    object Do {
        inline infix fun<reified T> exhaustive(any: T?) = any
    }
    
    0 讨论(0)
提交回复
热议问题