[英]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:那么你能帮我解决以下问题吗:
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
类。
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] theelse
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. 其他人建议扩展函数会污染命名空间,并且需要其他解决方法(如编译器插件)。
If you've ever used an enum
with an abstract method
just so that you could do something like this:如果您曾经使用带有abstract method
的enum
,以便您可以执行以下操作:
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.我们将举一个在线交付服务的简单示例,其中我们使用三种可能的状态Preparing
、 Dispatched
和Delivered
来显示在线订单的当前状态。
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
:它们用于标记状态Preparing
、 Dispatched
和Delivered
:
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.此外,由于存在可空值的可能性,因此进一步降低了对一致性的保证,因为Delivered
的receiversName
的null
值是非法状态。
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.不幸的是,只有在程序在运行时崩溃后,我们才会知道新创建的状态。
Restricted class hierarchy: sealed class
受限类层次结构: sealed class
Using the sealed
modifier on a class
does two things:在class
上使用sealed
修饰符有两件事:
abstract class
.它使该类成为abstract class
。 Since Kotlin 1.5, you can use a sealed interface
too.从 Kotlin 1.5 开始,您也可以使用sealed interface
。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
对象的有限形式,也就是说,它将采用Preparing
、 Dispatched
或Delivered
。 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 版本中,您的when
对when
表达式和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.