49 votes

Forcer l'erreur de compilation avec les classes scellées

Avec les classes scellées, vous pouvez utiliser des when et omettre l'expression else lorsque l'expression renvoie un résultat :

sealed class SealedClass {
  class First : SealedClass()
  class Second : SealedClass()
}

fun test(sealedClass: SealedClass) : String =
    when (sealedClass) {
      is SealedClass.First -> "First"
      is SealedClass.Second -> "Second"
    }

Maintenant, si je devais ajouter un Third à SealedClass le compilateur se plaindra que l'option when l'expression dans test() n'est pas exhaustive, et je dois ajouter une clause de Third o else .

Je me demande cependant si ce contrôle peut également être appliqué lorsque test() ne renvoie rien :

fun test(sealedClass: SealedClass) {
    when (sealedClass) {
      is SealedClass.First -> doSomething()
      is SealedClass.Second -> doSomethingElse()
    }
}

Cet extrait n'est pas cassé si Third est ajouté. Je peux ajouter un return déclaration avant when mais cela peut facilement être oublié et peut être rompu si le type de retour de l'une des clauses n'est pas Unit .

Comment puis-je m'assurer que je n'oublie pas d'ajouter une branche à mon when clauses ?

2 votes

J'ai posé la même question dans Slack il y a longtemps, et AFAIR, il n'y a pas de solution autre que d'en faire une expression renvoyant quelque chose. Mais IntelliJ devrait émettre un avertissement si vous oubliez une clause, cependant (ma question concernait un enum, pas une classe scellée, mais cela devrait être la même chose).

1 votes

Hmm, oui. J'ai remarqué que je peux le faire revenir Any? et de forcer une valeur de retour, mais cela semble toujours un peu compliqué.

0 votes

Mon préféré est le bloc run. Il est lisible, ne change pas le type de retour de la fonction de Unit à Any ?, et n'est pas plus ou moins oubliable que l'ajout d'un type de retour return ou Any ?

27voto

voddan Points 16

La manière d'imposer l'exhaustivité when est d'en faire une expression en utilisant sa valeur :

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 votes

Si vous n'avez pas réellement besoin de la valeur de retour, je trouve que c'est une bonne pratique d'utiliser un nom de variable dédié "jetable" pour indiquer que vous ne l'utilisez que pour appliquer une procédure exhaustive. when . De cette façon, vous lui donnez un sens sémantique, par exemple : val exhaustive = when(...)

27voto

Niek Haarman Points 13931

En s'inspirant de la réponse de Voddan, vous pouvez construire une propriété appelée safe que vous pouvez utiliser :

val Any?.safe get() = Unit

A utiliser :

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

Je pense que cela fournit un message plus clair que le simple fait d'annexer .let{} ou en assignant le résultat à une valeur.


Il existe un problème ouvert sur le tracker Kotlin qui envisage de supporter les "sealed whens".

6 votes

L'inconvénient est que cela pollue l'autocomplétion car cette méthode d'extension apparaîtra sur chaque déclaration.

4 votes

@Tunga remplacez Any ? par Unit ? et cela ne polluera pas autant.

26voto

Notre approche évite d'avoir la fonction partout lors de l'autocomplétion. Avec cette solution, vous disposez également du type de retour when au moment de la compilation, ce qui vous permet de continuer à utiliser les fonctions du type de retour when.

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

Vous pouvez définir cet objet comme suit :

object Do {
    inline infix fun<reified T> exhaustive(any: T?) = any
}

4 votes

Une approche intéressante !

10voto

Max Points 6217

Nous pouvons créer une propriété d'extension sur le type T avec un nom qui permet d'expliquer le but recherché

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

et ensuite l'utiliser n'importe où comme

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

Il est lisible, montre exactement ce qu'il fait et indique une erreur si tous les cas ne sont pas couverts. Plus d'informations ici

2voto

TWiStErRob Points 1233

A discussion m'a incité à chercher une solution plus générale et j'en ai trouvé une, pour les constructions Gradle. Il n'est pas nécessaire de modifier le code source ! L'inconvénient est que la compilation peut devenir bruyante.

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 :

  • Modifier la mise en œuvre de hasInfoWithError de généraliser à d'autres i: s.
  • Mettez ce code dans subprojects { } o allprojects { } à appliquer à l'ensemble du projet.

Références :

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X