简体   繁体   English

Kotlin 中的密封类是什么?

[英]What are sealed classes in Kotlin?

I'm a beginner in Kotlin and recently read about Sealed Classes .我是 Kotlin 的初学者,最近阅读了Sealed Classes But from the doc the only think I actually get is that they are exist.但是从文档中我真正得到的唯一想法是它们存在。

The doc stated, that they are "representing restricted class hierarchies".该文档指出,它们“代表受限的类层次结构”。 Besides that I found a statement that they are enums with superpower.除此之外,我发现了一个声明,他们是具有超能力的枚举。 Both aspects are actually not clear.这两个方面其实都不清楚。

So can you help me with the following questions:那么你能帮我解决以下问题吗:

  • What are sealed classes and what is the idiomatic way of using ones?什么是密封类以及使用它们的惯用方式是什么?
  • Does such a concept present in other languages like Python, Groovy or C#?这样的概念是否存在于其他语言(如 Python、Groovy 或 C#)中?

UPDATE: I carefully checked this blog post and still can't wrap my head around that concept.更新:我仔细检查了这篇博客文章,但仍然无法理解这个概念。 As stated in the post如帖子中所述

Benefit益处

The feature allows us to define class hierarchies that are restricted in their types, ie subclasses.该功能允许我们定义受其类型限制的类层次结构,即子类。 Since all subclasses need to be defined inside the file of the sealed class, there's no chance of unknown subclasses which the compiler doesn't know about.由于所有子类都需要在密封类的文件中定义,因此不可能有编译器不知道的未知子类。

Why the compiler doesn't know about other subclasses defined in other files?为什么编译器不知道其他文件中定义的其他子类? Even IDE knows that.连IDE都知道。 Just press Ctrl+Alt+B in IDEA on, for instance, List<> definition and all implementations will be shown even in other source files.只需在 IDEA 中按Ctrl+Alt+B ,例如List<>定义,所有实现都会显示在其他源文件中。 If a subclass can be defined in some third-party framework, which not used in the application, why should we care about that?如果一个子类可以在一些第三方框架中定义,而在应用程序中没有使用,我们为什么要关心它?

Say you have a domain (your pets) where you know there is a definite enumeration (count) of types.假设您有一个域(您的宠物),您知道其中有一个明确的枚举(计数)类型。 For example, you have two and only two pets (which you will model with a class called MyPet ).例如,您有两个而且只有两个宠物(您将使用名为MyPet的类MyPet )。 Meowsi is your cat and Fido is your dog. Meowsi 是你的猫,Fido 是你的狗。

Compare the following two implementations of that contrived example:比较该人为示例的以下两个实现:

sealed class MyPet
class Meowsi : MyPet()
class Fido : MyPet()

Because you have used sealed classes, when you need to perform an action depending on the type of pet, then the possibilities of MyPet are exhausted in two and you can ascertain that the MyPet instance will be exactly one of the two options:因为你使用了密封类,当你需要根据宠物的类型执行一个动作时,那么MyPet的可能性就用尽了两种,你可以确定MyPet实例恰好是两个选项之一:

fun feed(myPet: MyPet): String {
    return when(myPet) {
       is Meowsi -> "Giving cat food to Meowsi!"
       is Fido -> "Giving dog biscuit to Fido!" 
    }
}

If you don't use sealed classes, the possibilities are not exhausted in two and you need to include an else statement:如果您不使用密封类,那么可能性不会一分为二,您需要包含一个else语句:

open class MyPet
class Meowsi : MyPet()
class Fido : MyPet()

fun feed(myPet: MyPet): String {
    return when(myPet) {
       is Mewosi -> "Giving cat food to Meowsi!"
       is Fido -> "Giving dog biscuit to Fido!" 
       else -> "Giving food to someone else!" //else statement required or compiler error here
    }
}

In other words, without sealed classes there is not exhaustion (complete coverage) of possibility.换句话说,没有密封类就没有穷尽(完全覆盖)的可能性。

Note that you could achieve exhaustion of possiblity with Java enum however these are not fully-fledged classes.请注意,您可以使用 Java enum实现穷尽所有可能性,但是这些并不是成熟的类。 For example, enum cannot be subclasses of another class, only implement an interface (thanks EpicPandaForce ).例如, enum不能是另一个类的子类,只能实现一个接口(感谢EpicPandaForce )。

What is the use case for complete exhaustion of possibilities?完全耗尽可能性的用例是什么? To give an analogy, imagine you are on a tight budget and your feed is very precious and you want to ensure you don't end up feeding extra pets that are not part of your household.打个比方,假设您预算紧张,您的饲料非常宝贵,您希望确保最终不会喂养不属于您家庭的额外宠物。

Without the sealed class, someone else in your home/application could define a new MyPet :如果没有sealed类,您家/应用程序中的其他人可以定义一个新的MyPet

class TweetiePie : MyPet() //a bird

And this unwanted pet would be fed by your feed method as it is included in the else statement:这个不需要的宠物将由您的feed方法喂养,因为它包含在else语句中:

       else -> "Giving food to someone else!" //feeds any other subclass of MyPet including TweetiePie!

Likewise, in your program exhaustion of possibility is desirable because it reduces the number of states your application can be in and reduces the possibility of bugs occurring where you have a possible state where behaviour is poorly defined.同样,在您的程序中,尽可能排除可能性是可取的,因为它减少了您的应用程序可能处于的状态数量,并减少了在您具有行为定义不明确的可能状态时发生错误的可能性。

Hence the need for sealed classes.因此需要sealed类。

Mandatory else必填 其他

Note that you only get the mandatory else statement if when is used as an expression.请注意,如果将when用作表达式,您只会得到强制的else语句。 As per the docs:根据文档:

If [ when ] is used as an expression, the value of the satisfied branch becomes the value of the overall expression [... and] the else branch is mandatory, unless the compiler can prove that all possible cases are covered with branch conditions如果 [ when ] 用作表达式,则满足分支的值变为整个表达式 [... 并且] else分支是强制性的,除非编译器可以证明所有可能的情况都被分支条件覆盖

This means you won't get the benefit of sealed classes for something like this):这意味着您不会因为这样的事情获得密封类的好处):

fun feed(myPet: MyPet): Unit {
    when(myPet) {
        is Meowsi -> println("Giving cat food to Meowsi!") // not an expression so we can forget about Fido
    }
}

To get exhaustion for this scenario, you would need to turn the statement into an expression with return type.为了使这种情况筋疲力尽,您需要将语句转换为具有返回类型的表达式。

Some have suggested an extension function like this would help: 有人建议这样的扩展功能会有所帮助:

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

Then you can do:然后你可以这样做:

fun feed(myPet: MyPet): Unit {
    when(myPet) {
        is Meowsi -> println("Giving cat food to Meowsi!") 
    }.exhaustive // compiler error because we forgot about Fido
}

Others have suggested that an extension function pollutes the namespace and other workarounds (like compiler plugins) are required. 其他人建议扩展函数会污染命名空间,并且需要其他解决方法(如编译器插件)。

See here for more about this problem.有关此问题的更多信息,请参见此处

If you've ever used an enum with an abstract method just so that you could do something like this:如果您曾经使用带有abstract methodenum ,以便您可以执行以下操作:

public enum ResultTypes implements ResultServiceHolder {
    RESULT_TYPE_ONE {
        @Override
        public ResultOneService getService() {
            return serviceInitializer.getResultOneService();
        }
    },
    RESULT_TYPE_TWO {
        @Override
        public ResultTwoService getService() {
            return serviceInitializer.getResultTwoService();
        }
    },
    RESULT_TYPE_THREE {
        @Override
        public ResultThreeService getService() {
            return serviceInitializer.getResultThreeService();
        }
    };

When in reality what you wanted is this:当实际上你想要的是这样的:

val service = when(resultType) {
    RESULT_TYPE_ONE -> resultOneService,
    RESULT_TYPE_TWO -> resultTwoService,
    RESULT_TYPE_THREE -> resultThreeService
}

And you only made it an enum abstract method to receive compile time guarantee that you always handle this assignment in case a new enum type is added;并且您仅将其设为枚举抽象方法以接收编译时保证在添加新枚举类型的情况下您始终处理此分配; then you'll love sealed classes because sealed classes used in assignments like that when statement receive a "when should be exhaustive" compilation error which forces you to handle all cases instead of accidentally only some of them.那么你会喜欢密封类,因为像when语句这样的赋值中使用的密封类收到“何时应该是详尽的”编译错误,这迫使你处理所有情况,而不是意外地只处理其中的一些情况。

So now you cannot end up with something like:所以现在你不能得到类似的结果:

switch(...) {
   case ...:
       ...
   default:
       throw new IllegalArgumentException("Unknown type: " + enum.name());
}

Also, enums cannot extend classes, only interfaces;此外,枚举不能扩展类,只能扩展接口; while sealed classes can inherit fields from a base class.而密封类可以从基类继承字段。 You can also create multiple instances of them (and you can technically use object if you need the subclass of the sealed class to be a singleton).您还可以创建它们的多个实例(如果您需要密封类的子类是单例,您可以在技术上使用object )。

Sealed classes are easier to understand when you understand the kinds of problems they aim to solve.当您了解它们旨在解决的问题类型时,密封类更容易理解。 First I'll explain the problems, then I'll introduce the class hierarchies and the restricted class hierarchies step by step.先说明问题,然后逐步介绍类层次结构和受限类层次结构。

We'll take a simple example of an online delivery service where we use three possible states Preparing , Dispatched and Delivered to display the current status of an online order.我们将举一个在线交付服务的简单示例,其中我们使用三种可能的状态PreparingDispatchedDelivered来显示在线订单的当前状态。


Problems问题

Tagged class标记类

Here we use a single class for all the states.在这里,我们对所有状态使用一个类。 Enums are used as type markers.枚举用作类型标记。 They are used for tagging the states Preparing , Dispatched and Delivered :它们用于标记状态PreparingDispatchedDelivered

class DeliveryStatus(
    val type: Type,
    val trackingId: String? = null,
    val receiversName: String? = null) {
    enum class Type { PREPARING, DISPATCHED, DELIVERED }
}

The following function checks the state of the currently passed object with the help of enums and displays the respective status:以下函数在枚举的帮助下检查当前传递的对象的状态并显示相应的状态:

fun displayStatus(state: DeliveryStatus) = when (state.type) {
    PREPARING -> print("Preparing for dispatch")
    DISPATCHED -> print("Dispatched. Tracking ID: ${state.trackingId ?: "unavailable"}")
    DELIVERED -> print("Delivered. Receiver's name: ${state.receiversName ?: "unavailable"}")
}

As you can see, we are able to display the different states properly.如您所见,我们能够正确显示不同的状态。 We also get to use exhaustive when expression, thanks to enums.多亏了枚举,我们还可以when表达式中使用穷举。 But there are various problems with this pattern:但是这种模式存在各种问题:

Multiple responsibilities多重责任

The class DeliveryStatus has multiple responsibilities of representing different states. DeliveryStatus类具有表示不同状态的多项职责。 So it can grow bigger, if we add more functions and properties for different states.所以它可以变得更大,如果我们为不同的状态添加更多的功能和属性。

More properties than needed超过需要的属性

An object has more properties than it actually needs in a particular state.一个对象具有比它在特定状态下实际需要的更多的属性。 For example, in the function above, we don't need any property for representing the Preparing state.例如,在上面的函数中,我们不需要任何属性来表示Preparing状态。 The trackingId property is used only for the Dispatched state and the receiversName property is concerned only with the Delivered state. trackingId属性仅用于Dispatched状态,并且receiversName属性仅与Delivered状态有关。 The same is true for functions.函数也是如此。 I haven't shown functions associated with states to keep the example small.我没有展示与状态相关的函数以保持示例较小。

No guarantee of consistency不保证一致性

Since these unused properties can be set from unrelated states, it's hard to guarantee the consistency of a particular state.由于可以从不相关的状态设置这些未使用的属性,因此很难保证特定状态的一致性。 For example, one can set the receiversName property on the Preparing state.例如,可以在Preparing状态上设置receiversName属性。 In that case, the Preparing will be an illegal state, because we can't have a receiver's name for the shipment that hasn't been delivered yet.在这种情况下, Preparing将是非法状态,因为对于尚未交付的货件,我们无法获得收件人的姓名。

Need to handle null values需要处理null

Since not all properties are used for all states, we have to keep the properties nullable.由于并非所有属性都用于所有状态,因此我们必须保持属性可为空。 This means we also need to check for the nullability.这意味着我们还需要检查可空性。 In the displayStatus() function we check the nullability using the ?: (elvis) operator and show unavailable , if that property is null .displayStatus()函数中,我们使用?: (elvis) 运算符检查可空性,如果该属性为null ,则显示unavailable This complicates our code and reduces readability.这使我们的代码复杂化并降低了可读性。 Also, due to the possibility of a nullable value, the guarantee for consistency is reduced further, because the null value of receiversName in Delivered is an illegal state.此外,由于存在可空值的可能性,因此进一步降低了对一致性的保证,因为DeliveredreceiversNamenull值是非法状态。


Introducing Class Hierarchies介绍类层次结构

Unrestricted class hierarchy: abstract class不受限制的类层次结构: abstract class

Instead of managing all the states in a single class, we separate the states in different classes.我们不是在单个类中管理所有状态,而是将不同类中的状态分开。 We create a class hierarchy from an abstract class so that we can use polymorphism in our displayStatus() function:我们从abstract class创建一个类层次结构,以便我们可以在displayStatus()函数中使用多态:

abstract class DeliveryStatus
object Preparing : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
class Delivered(val receiversName: String) : DeliveryStatus()

The trackingId is now only associated with the Dispatched state and receiversName is only associated with the Delivered state. trackingId现在仅与Dispatched状态相关联, receiversName仅与Delivered状态相关联。 This solves the problems of multiple responsibilities, unused properties, lack of state consistency and null values.这解决了多重职责、未使用的属性、缺乏状态一致性和空值的问题。

Our displayStatus() function now looks like the following:我们的displayStatus()函数现在如下所示:

fun displayStatus(state: DeliveryStatus) = when (state) {
    is Preparing -> print("Preparing for dispatch")
    is Dispatched -> print("Dispatched. Tracking ID: ${state.trackingId}")
    is Delivered -> print("Delivered. Received by ${state.receiversName}")
    else -> throw IllegalStateException("Unexpected state passed to the function.")
}

Since we got rid of null values, we can be sure that our properties will always have some values.由于我们摆脱了null值,我们可以确保我们的属性总是有一些值。 So now we don't need to check for null values using the ?: (elvis) operator.所以现在我们不需要使用?: (elvis) 运算符检查null值。 This improves code readability.这提高了代码的可读性。

So we solved all the problems mentioned in the tagged class section by introducing a class hierarchy.所以我们通过引入类层次结构解决了标记类部分提到的所有问题。 But the unrestricted class hierarchies have the following shortcomings:但是不受限制的类层次结构有以下缺点:

Unrestricted Polymorphism无限制多态性

By unrestricted polymorphism I mean that our function displayStatus() can be passed a value of unlimited number of subclasses of the DeliveryStatus .不受限制的多态性是指我们的函数displayStatus()可以传递一个无限数量的DeliveryStatus子类的值。 This means we have to take care of the unexpected states in displayStatus() .这意味着我们必须处理displayStatus()中的意外状态。 For this, we throw an exception.为此,我们抛出异常。

Need for the else branch需要else分支

Due to unrestricted polymorphism, we need an else branch to decide what to do when an unexpected state is passed.由于不受限制的多态性,我们需要一个else分支来决定在传递意外状态时要做什么。 If we use some default state instead of throwing an exception and then forget to take care of any newly added subclass, then that default state will be displayed instead of the state of the newly created subclass.如果我们使用一些默认状态而不是抛出异常,然后忘记处理任何新添加的子类,那么将显示该默认状态而不是新创建的子类的状态。

No exhaustive when expression表达when没有穷尽

Since the subclasses of an abstract class can exist in different packages and compilation units, the compiler doesn't know all the possible subclasses of the abstract class .由于abstract class的子abstract class可以存在于不同的包和编译单元中,编译器并不知道abstract class所有可能的子abstract class So it won't flag an error at compile time, if we forget to take care of any newly created subclasses in the when expression.所以它不会在编译时标记错误,如果我们忘记在when表达式中处理任何新创建的子类。 In that case, only throwing an exception can help us.在这种情况下,只有抛出异常才能帮助我们。 Unfortunately, we'll know about the newly created state only after the program crashes at runtime.不幸的是,只有在程序在运行时崩溃后,我们才会知道新创建的状态。


Sealed Classes to the Rescue密封类救援

Restricted class hierarchy: sealed class受限类层次结构: sealed class

Using the sealed modifier on a class does two things:class上使用sealed修饰符有两件事:

  1. It makes that class an abstract class .它使该类成为abstract class Since Kotlin 1.5, you can use a sealed interface too.从 Kotlin 1.5 开始,您也可以使用sealed interface
  2. It makes it impossible to extend that class outside of that file.这使得无法将该类扩展到该文件之外。 Since Kotlin 1.5 the same file restriction has been removed.自 Kotlin 1.5 起,相同的文件限制已被删除。 Now the class can be extended in other files too but they need to be in the same compilation unit and in the same package as the sealed type.现在该类也可以在其他文件中扩展,但它们需要与sealed类型位于相同的编译单元和相同的包中。
sealed class DeliveryStatus
object Preparing : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
class Delivered(val receiversName: String) : DeliveryStatus()

Our displayStatus() function now looks cleaner:我们的displayStatus()函数现在看起来更简洁:

fun displayStatus(state: DeliveryStatus) = when (state) {
    is Preparing -> print("Preparing for Dispatch")
    is Dispatched -> print("Dispatched. Tracking ID: ${state.trackingId}")
    is Delivered -> print("Delivered. Received by ${state.receiversName}")
}

Sealed classes offer the following advantages:密封类具有以下优点:

Restricted Polymorphism限制性多态性

By passing an object of a sealed class to a function, you are also sealing that function, in a sense.通过将sealed class的对象传递给函数,从某种意义上说,您也在密封该函数。 For example, now our displayStatus() function is sealed to the limited forms of the state object, that is, it will either take Preparing , Dispatched or Delivered .例如,现在我们的displayStatus()函数被密封到state对象的有限形式,也就是说,它将采用PreparingDispatchedDelivered Earlier it was able to take any subclass of DeliveryStatus .早些时候,它能够采用DeliveryStatus任何子类。 The sealed modifier has put a limit on polymorphism. sealed修饰符限制了多态性。 As a result, we don't need to throw an exception from the displayStatus() function.因此,我们不需要从displayStatus()函数中抛出异常。

No need for the else branch不需要else分支

Due to restricted polymorphism, we don't need to worry about other possible subclasses of DeliveryStatus and throw an exception when our function receives an unexpected type.由于受限制的多态性,我们无需担心DeliveryStatus其他可能子类并在我们的函数收到意外类型时抛出异常。 As a result, we don't need an else branch in the when expression.因此,我们不需要when表达式中的else分支。

Exhaustive when expression表达when穷尽

Just like all the possible values of an enum class are contained inside the same class , all the possible subtypes of a sealed class are contained inside the same package and the same compilation unit .就像enum class所有可能值都包含在同一个类一样sealed class所有可能子类型都包含在同一个包和同一个编译单元中 So, the compiler knows all the possible subclasses of this sealed class.所以,编译器知道这个sealed类的所有可能的子类。 This helps the compiler to make sure that we have covered(exhausted) all the possible subtypes in the when expression.这有助于编译器确保我们已经涵盖(用尽) when表达式中所有可能的子类型。 And when we add a new subclass and forget to cover it in the when expression, it flags an error at compile time.当我们添加一个新的子类并忘记在when表达式中覆盖它when ,它会在编译时标记一个错误。

Note that in the latest Kotlin versions, your when is exhaustive for the when expressions as well the when statements .请注意,在最新的 Kotlin 版本中,您的whenwhen表达式when语句都是详尽无遗的。

Why in the same file?为什么在同一个文件中?

The same file restriction has been removed since Kotlin 1.5.自 Kotlin 1.5 以来,相同的文件限制已被删除。 Now you can define the subclasses of the sealed class in different files but the files need to be in the same package and the same compilation unit.现在你可以在不同的文件中定义sealed class的子sealed class ,但文件需要在同一个包和同一个编译单元中。 Before 1.5, the reason that all the subclasses of a sealed class needed to be in the same file was that it had to be compiled together with all of its subclasses for it to have a closed set of types.在 1.5 之前, sealed class所有子sealed class需要在同一个文件中的原因是它必须与其所有子类一起编译才能拥有一组封闭的类型。 If the subclasses were allowed in other files, the build tools like Gradle would have to keep track of the relations of files and this would affect the performance of incremental compilation.如果其他文件中允许使用子类,Gradle 等构建工具将不得不跟踪文件的关系,这会影响增量编译的性能。

IDE feature: Add remaining branches IDE 功能: Add remaining branches

When you just type when (status) { } and press Alt + Enter , Enter , the IDE automatically generates all the possible branches for you like the following:当您只输入when (status) { }并按Alt + Enter , Enter 时,IDE 会自动为您生成所有可能的分支,如下所示:

when (state) {
    is Preparing -> TODO()
    is Dispatched -> TODO()
    is Delivered -> TODO()
}

In our small example there are just three branches but in a real project you could have hundreds of branches.在我们的小示例中,只有三个分支,但在实际项目中,您可能有数百个分支。 So you save the effort of manually looking up which subclasses you have defined in different files and writing them in the when expression one by one in another file.因此,您无需手动查找在不同文件中定义的子类并将它们一个一个地写入另一个文件中的when表达式中。 Just use this IDE feature.只需使用此 IDE 功能。 Only the sealed modifier enables this.只有sealed修饰符才能启用此功能。


That's it!就是这样! Hope this helps you understand the essence of sealed classes.希望这可以帮助您理解密封类的本质。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM