什么是Kotlin的密封课程?

ore*_*reh 13 kotlin

我是Kotlin的初学者,最近读过有关密封课程的内容.但是从文档来看,我实际得到的唯一想法是它们存在.

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

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

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

更新: 我仔细检查了这篇博客文章,仍然无法理解这个概念.正如帖子中所述

效益

该功能允许我们定义在类型中受限制的类层次结构,即子类.由于所有子类都需要在密封类的文件中定义,因此编译器不了解未知子类的可能性.

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

Dav*_*son 34

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

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

sealed class MyPet
class Meowsi : MyPet()
class Fido : MyPet()
Run Code Online (Sandbox Code Playgroud)

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

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

如果你不使用密封类,那么两种可能性并没有用尽,你需要包括一个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
    }
}
Run Code Online (Sandbox Code Playgroud)

换句话说,没有密封的类,就没有可能性的耗尽(完全覆盖).

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

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

没有sealed课程,你家/应用程序中的其他人可以定义一个新的MyPet:

class TweetiePie : MyPet() //a bird
Run Code Online (Sandbox Code Playgroud)

这个不需要的宠物将由你的feed方法喂养,因为它包含在else声明中:

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

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

因此需要sealed课程.

  • 这个答案终于在我的脑海中理清了关于密封类的问题 (2认同)

Yog*_*ity 10

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

我们将采用一个在线送货服务的简单示例,其中我们使用三种可能的状态PreparingDispatchedDelivered显示在线订单的当前状态。


问题

标记类

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

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

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

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"}")
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我们能够正确显示不同的状态。when多亏了枚举,我们还可以使用详尽的表达式。但这种模式存在各种问题:

多重责任

该类DeliveryStatus具有代表不同状态的多重职责。因此,如果我们为不同的状态添加更多的函数和属性,它就会变得更大。

超出需要的属性

对象在特定状态下拥有的属性多于其实际需要的属性。例如,在上面的函数中,我们不需要任何属性来表示状态Preparing。财产trackingId仅用于国家DispatchedreceiversName财产仅与国家有关Delivered。对于函数来说也是同样的道理。我没有展示与状态相关的函数,以保持示例较小。

不保证一致性

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

需要处理null

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


引入类层次结构

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

我们不是管理单个类中的所有状态,而是将状态分成不同的类。我们从 an 创建一个类层次结构,abstract class以便我们可以在displayStatus()函数中使用多态性:

abstract class DeliveryStatus
object Preparing : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
class Delivered(val receiversName: String) : DeliveryStatus()
Run Code Online (Sandbox Code Playgroud)

trackingId现在仅与状态关联并且DispatchedreceiversNameDelivered。这解决了多重职责、未使用的属性、缺乏状态一致性和空值的问题。

我们的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.")
}
Run Code Online (Sandbox Code Playgroud)

由于我们摆脱了null值,我们可以确定我们的属性将始终具有一些值。所以现在我们不需要null使用?:(elvis) 运算符检查值。这提高了代码的可读性。

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

无限制多态性

通过不受限制的多态性,我的意思是我们的函数displayStatus()可以传递无限数量的DeliveryStatus. 这意味着我们必须处理 中的意外状态displayStatus()。为此,我们抛出一个异常。

需要else分公司

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

没有详尽的when表达

由于 的子类abstract class可以存在于不同的包和编译单元中,因此编译器不知道 的所有可能的子类abstract class。因此,如果我们忘记处理表达式中任何新创建的子类,它不会在编译时标记错误when。在这种情况下,只有抛出异常才能帮助我们。不幸的是,只有在程序运行时崩溃后我们才会知道新创建的状态。


密封课程来救援

受限制的类层次结构:sealed class

sealed在 a 上使用修饰符class有两个作用:

  1. 它使该类成为abstract class. 从 Kotlin 1.5 开始,您也可以使用 a sealed interface
  2. 它使得不可能将该类扩展到该文件之外。自 Kotlin 1.5 起,相同的文件限制已被删除。现在,该类也可以在其他文件中扩展,但它们需要与类型位于同一编译单元和同一包中sealed
sealed class DeliveryStatus
object Preparing : DeliveryStatus()
class Dispatched(val trackingId: String) : DeliveryStatus()
class Delivered(val receiversName: String) : DeliveryStatus()
Run Code Online (Sandbox Code Playgroud)

我们的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}")
}
Run Code Online (Sandbox Code Playgroud)

密封课程具有以下优点:

限制性多态性

sealed class从某种意义上说,通过将 a 的对象传递给函数,您也密封了该函数。例如,现在我们的displayStatus()函数被密封到对象的有限形式state,也就是说,它将要么采用PreparingDispatched要么采用Delivered。早些时候它能够接受 的任何子类DeliveryStatus。修饰符sealed对多态性施加了限制。因此,我们不需要从函数中抛出异常displayStatus()

不需要else分行

由于多态性受到限制,我们不需要担心其他可能的子类,DeliveryStatus并且当我们的函数接收到意外类型时抛出异常。因此,我们不需要表达式else中的分支when

详尽的when表达

就像 an 的所有可能值enum class都包含在同一个类中一样,a 的所有可能子类型sealed class也包含在同一个包和同一个编译单元中。因此,编译器知道该类的所有可能的子类sealed。这有助于编译器确保我们已经覆盖(穷尽)表达式中所有可能的子类型when。当我们添加一个新的子类并忘记在表达式中覆盖它时when,它会在编译时标记一个错误。

请注意,在最新的 Kotlin 版本中,表达式语句when都非常详尽。when when

为什么在同一个文件中?

自 Kotlin 1.5 起,相同的文件限制已被删除。现在您可以在不同的文件中定义 的子类sealed class,但文件需要位于同一包和同一编译单元中。在 1.5 之前,a 的所有子类需要位于同一个文件中的原因sealed class是它必须与其所有子类一起编译才能拥有一组封闭的类型。如果在其他文件中允许子类,那么像 Gradle 这样的构建工具就必须跟踪文件的关系,这会影响增量编译的性能。

IDE功能:Add remaining branches

当您只需键入when (status) { }并按Alt+ Enter,时Enter,IDE 会自动为您生成所有可能的分支,如下所示:

when (state) {
    is Preparing -> TODO()
    is Dispatched -> TODO()
    is Delivered -> TODO()
}
Run Code Online (Sandbox Code Playgroud)

在我们的小示例中,只有三个分支,但在实际项目中,您可能有数百个分支。因此,您可以节省手动查找在不同文件中定义的子类并将它们when一一写入另一个文件中的表达式的精力。只需使用此 IDE 功能即可。只有sealed修饰符才能实现此目的。


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


Epi*_*rce 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();
        }
    };
Run Code Online (Sandbox Code Playgroud)

实际上你想要的是这个:

val service = when(resultType) {
    RESULT_TYPE_ONE -> resultOneService,
    RESULT_TYPE_TWO -> resultTwoService,
    RESULT_TYPE_THREE -> resultThreeService
}
Run Code Online (Sandbox Code Playgroud)

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

所以现在你不能最终得到这样的东西:

switch(...) {
   case ...:
       ...
   default:
       throw new IllegalArgumentException("Unknown type: " + enum.name());
}
Run Code Online (Sandbox Code Playgroud)

此外,枚举不能扩展类,只能扩展接口; 密封类可以从基类继承字段.您还可以创建他们的多个实例(和技术上可以使用object,如果你需要的密封类的子类是单身).