什么是Kotlin的密封课程?

时间:2018-06-09 08:25:50

标签: kotlin

我是Kotlin的初学者,最近阅读了Sealed Classes。但是从文档来看,我实际得到的唯一想法是它们存在。

该文件指出,他们“代表受限制的阶级等级制度”。除此之外,我发现了一个声明,他们是超级大国的枚举。这两个方面实际上都不清楚。

那么你可以帮助我解决以下问题:

  • 什么是密封类以及使用它们的惯用方法是什么?
  • 这样的概念是否存在于其他语言中,如Python,Groovy或C#?

更新 我仔细检查了这个blog post,仍然无法绕过这个概念。正如帖子中所述

  

效益

     

该功能允许我们定义类层次结构   限制其类型,即子类。因为所有子类都需要   要在密封类的文件中定义,没有机会   编译器不知道的未知子类。

为什么编译器不知道其他文件中定义的其他子类?即使IDE知道这一点。只需按下IDEA中的Ctrl+Alt+B,例如List<>定义,即使在其他源文件中也会显示所有实现。如果某个子类可以在某个第三方框架中定义,而在应用程序中没有使用,我们为什么要关心它呢?

4 个答案:

答案 0 :(得分:21)

假设您有一个域名(您的宠物),您知道有明确的类型枚举(计数)。例如,您有两只且只有两只宠物(您将使用名为MyPet的类进行建模)。 Meowsi是你的猫,Fido是你的狗。

比较该设计示例的以下两种实现:

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

因为你已经使用了密封类,当你需要根据宠物的类型执行一个动作时,MyPet的可能性已经用尽了两个你可以确定MyPet实例将会恰好是两个选项之一:

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

如果你不使用密封课程,那么两种可能性就不会用尽,你需要加上else声明:

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

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

换句话说,没有密封的课程,就没有可能的用尽(完全覆盖)。

请注意,使用Java enum可能会耗尽可能性,但这些不是完全成熟的类。例如,enum不能是另一个类的子类,只能实现一个接口(感谢EpicPandaForce)。

完全耗尽可能性的用例是什么?举一个类比,想象一下预算紧张,你的饲料非常珍贵,你想确保你不会喂养不属于你家庭的额外宠物。

如果没有sealed课程,您家/应用程序中的其他人可以定义新的MyPet

class TweetiePie : MyPet() //a bird

这个不受欢迎的宠物将通过feed语句中包含的else方法提供:

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

同样,在你的程序中,可能性的耗尽是可取的,因为它减少了应用程序所处的状态数,并减少了在行为定义不明确的可能状态下发生错误的可能性。

因此需要sealed类。

答案 1 :(得分:5)

如果您曾使用过enum abstract method,那么您可以执行以下操作:

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();
        }
    };

实际上你想要的是这个:

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

你只是使它成为一个enum抽象方法来接收编译时保证你总是处理这个赋值,以防添加新的枚举类型;然后你会喜欢密封类,因为在when语句这样的赋值中使用的密封类会收到“什么时候应该是详尽无遗的”编译错误,它会强制你处理所有情况而不是偶然只处理其中的一些情况。

所以现在你不能最终得到类似的东西:

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

此外,枚举不能扩展类,只能扩展接口;密封类可以从基类继承字段。您还可以创建它们的多个实例(如果您需要将密封类的子类作为单例,则技术上可以使用object

答案 2 :(得分:0)

当您了解密封类旨在解决的问题时,它们会更容易理解。首先,我将解释问题,然后逐步介绍类层次结构和受限类层次结构。

我们将以一个简单的在线交付服务示例为例,其中我们使用三种可能的状态PreparingDispatchedDelivered来显示在线订单的当前状态。


问题

标记的课程

在这里,我们对所有状态使用单个类。枚举用作类型标记。它们用于标记状态PreparingDispatchedDelivered

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

以下函数在枚举的帮助下检查当前传递对象的状态并显示相应状态:

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"}")
}

如您所见,我们能够正确显示不同的状态。由于枚举,我们还可以使用穷举when表达式。但是这种模式存在各种问题:

多重职责

DeliveryStatus具有代表不同状态的多种职责。如果我们为不同的状态添加更多的函数和属性,则它可以变得更大。

超出所需的属性

在特定状态下,对象比实际需要的属性更多。例如,在上面的函数中,我们不需要任何属性来表示Preparing状态。 trackingId属性仅用于Dispatched状态,而receiversName属性仅与Delivered状态有关。函数也是如此。我没有显示与状态关联的函数来使示例保持较小。

不保证一致性

由于可以从不相关的状态中设置这些未使用的属性,因此很难保证特定状态的一致性。例如,可以在receiversName状态下设置Preparing属性。在这种情况下,Preparing将处于非法状态,因为我们无法获得尚未交付的货件的收货人姓名。

需要处理null个值

由于并非所有属性都用于所有状态,因此我们必须保持属性为空。这意味着我们还需要检查可为空性。在displayStatus()函数中,如果属性为?:,则使用unavailable(elvis)运算符检查可空性,并显示null。这会使我们的代码复杂化并降低了可读性。另外,由于可能会为空,因此nullreceiversName的{​​{1}}值是非法状态,从而进一步降低了一致性保证。


介绍类层次结构

不受限制的类层次结构:Delivered

我们不是在一个类中管理所有状态,而是在不同类中分离状态。我们从abstract class创建一个类层次结构,以便可以在abstract class函数中使用多态:

displayStatus()

abstract class DeliveryStatus object Preparing : DeliveryStatus() class Dispatched(val trackingId: String) : DeliveryStatus() class Delivered(val receiversName: String) : DeliveryStatus() 现在仅与trackingId状态关联,而Dispatched仅与receiversName状态关联。解决了多个职责,未使用的属性,状态一致性不足和空值的问题。

我们的Delivered函数现在如下所示:

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.") } 值,因此可以确定我们的属性将始终具有某些值。因此,现在我们不需要使用null(elvis)运算符检查null的值。这样可以提高代码的可读性。

因此,我们通过引入类层次结构解决了标记的类部分中提到的所有问题。但是无限制的类层次结构具有以下缺点:

不受限制的多态性

通过不受限制的多态性,我的意思是我们的函数?:可以被传递一个displayStatus()的子类数目不限的值。这意味着我们必须注意DeliveryStatus中的意外状态。为此,我们抛出一个异常。

需要displayStatus()分支

由于不受限制的多态性,我们需要一个else分支来决定在传递意外状态时该怎么做。如果我们使用某种默认状态而不是抛出异常,然后忘记照顾任何新添加的子类,则将显示该默认状态,而不是新创建的子类的状态。

没有详尽的else表达式

由于when的子类可以存在于其他文件中,因此编译器并不知道abstract class的所有可能的子类。因此,如果我们忘记照顾abstract class表达式中任何新创建的子类,则它不会在编译时标记错误。在这种情况下,仅抛出异常可以为我们提供帮助。不幸的是,只有在程序在运行时崩溃后,我们才会知道新创建的状态。


抢救的密封类

受限的类层次结构:when

sealed class上使用sealed修饰符有两件事:

  1. 这使该类成为class
  2. 无法将该类扩展到该文件之外。
abstract class

我们的sealed class DeliveryStatus object Preparing : DeliveryStatus() class Dispatched(val trackingId: String) : DeliveryStatus() class Delivered(val receiversName: String) : DeliveryStatus() 函数现在看起来更加简洁:

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 class函数被密封为displayStatus()对象的有限形式,也就是说,它将采用statePreparingDispatched 。先前,它可以采用Delivered的任何子类。 DeliveryStatus修饰符已限制了多态性。结果,我们不需要从sealed函数中引发异常。

不需要displayStatus()分支

由于受限制的多态性,我们不必担心else的其他可能的子类,并且当我们的函数接收到意外的类型时,将引发异常。结果,我们在DeliveryStatus表达式中不需要else分支。

详尽的when表达式

就像{em {em} 中包含when的所有可能值一样,相同文件中也包含enum class的所有可能子类型。 。因此,当编译器遇到sealed class修饰符时,它知道此密封类的所有可能的子类都在同一文件中。这有助于编译器确保我们已经覆盖(耗尽)了sealed表达式中所有可能的子类型。而且,当我们在同一个文件中添加新的子类而忘记在when表达式中覆盖它时,它将在编译时标记一个错误。

请注意,在最新的Kotlin版本中,您的when对于when 表达式以及when 声明都是详尽无遗的

为什么在同一文件中?

when的所有子类都应在同一文件中的原因是,sealed class应该与其所有子类一起编译,以使其具有一组封闭的类型。如果允许在其他文件中使用子类,则诸如Gradle之类的构建工具将必须跟踪文件之间的关系,这将影响增量编译的性能。

IDE功能:sealed class

当您仅键入Add remaining branches并按 Alt + Enter Enter 时,IDE会自动生成所有可能的分支您喜欢以下内容:

when (status) { }

在我们的小示例中,只有三个分支,但是在一个实际项目中,您可能有数百个分支。因此,您省去了手动查找在一个文件中定义的哪些子类并将它们在when (state) { is Preparing -> TODO() is Dispatched -> TODO() is Delivered -> TODO() } 表达式中逐个写入另一个文件中的工作。只需使用此IDE功能。只有when修饰符可以启用此功能。


就是这样!希望这可以帮助您了解密封类的本质。

答案 3 :(得分:-2)

我认为,密封的课程就像一个更强大的枚举。密封类只有固定子类,就像枚举有固定字段一样。但是子类可以覆盖,修饰或增强它的超类。例如,一个密封的http响应,可以有200个响应,300个响应,400个响应..,对于每个子响应,你可以做一些不同的事情或分配不同的值。像枚举,提供修复选项,但更强的容量。