我是Kotlin的初学者,最近阅读了Sealed Classes。但是从文档来看,我实际得到的唯一想法是它们存在。
该文件指出,他们“代表受限制的阶级等级制度”。除此之外,我发现了一个声明,他们是超级大国的枚举。这两个方面实际上都不清楚。
那么你可以帮助我解决以下问题:
更新 我仔细检查了这个blog post,仍然无法绕过这个概念。正如帖子中所述
效益
该功能允许我们定义类层次结构 限制其类型,即子类。因为所有子类都需要 要在密封类的文件中定义,没有机会 编译器不知道的未知子类。
为什么编译器不知道其他文件中定义的其他子类?即使IDE知道这一点。只需按下IDEA中的Ctrl+Alt+B
,例如List<>
定义,即使在其他源文件中也会显示所有实现。如果某个子类可以在某个第三方框架中定义,而在应用程序中没有使用,我们为什么要关心它呢?
答案 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)
当您了解密封类旨在解决的问题时,它们会更容易理解。首先,我将解释问题,然后逐步介绍类层次结构和受限类层次结构。
我们将以一个简单的在线交付服务示例为例,其中我们使用三种可能的状态Preparing
,Dispatched
和Delivered
来显示在线订单的当前状态。
标记的课程
在这里,我们对所有状态使用单个类。枚举用作类型标记。它们用于标记状态Preparing
,Dispatched
和Delivered
:
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
。这会使我们的代码复杂化并降低了可读性。另外,由于可能会为空,因此null
中receiversName
的{{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
修饰符有两件事:
class
。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()
对象的有限形式,也就是说,它将采用state
,Preparing
或Dispatched
。先前,它可以采用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个响应..,对于每个子响应,你可以做一些不同的事情或分配不同的值。像枚举,提供修复选项,但更强的容量。