在阅读
有关密封类的
文档后,我可能不是一个人,我想:“好吧。 也许有一天会派上用场。” 后来,在工作中,我成功完成了几个任务,成功地使用了该工具,我想:“不错。 您应该经常考虑该应用程序。” 最后,我在《
有效的Java》(Joshua Bloch,第3版) (是的,关于Java的书)中遇到了关于任务类别的描述。
让我们看一个应用程序,并根据语义和性能对其进行评估。
我认为使用UI的每个人都曾经通过某些
状态满足UI与
服务交互的实现,其中某些
类型标记是属性之一。 在此类实现中处理下一个状态的机制通常直接取决于指定的标记。 例如,这样的
State类的实现:
class State( val type: Type, val data: String?, val error: Throwable? ) { enum class Type { LOADING, ERROR, EMPTY, DATA } }
我们列出了这种实现的缺点(自己尝试)摘自“将类层次结构优先于标记的类”一书的第23章。 我建议结识她。
- 仅针对某些类型初始化的属性的内存消耗。 大量因素可能很重要。 如果创建默认对象来填充属性,则情况会更加恶化。
- 语义负载过大。 该类的用户需要监视什么类型,哪些属性可用。
- 业务逻辑类中的复杂支持。 假设一个对象可以对其数据执行某些操作的实现。 这样的类看起来像收割机 ,添加新类型或操作可能会变得困难。
处理新状态可能如下所示:
fun handleState(state: State) { when(state.type) { State.Type.LOADING -> onLoading() State.Type.ERROR -> state.error?.run(::onError) ?: throw AssertionError("Unexpected error state: $state") State.Type.EMPTY -> onEmpty() State.Type.DATA -> state.data?.run(::onData) ?: throw AssertionError("Unexpected data state: $state") } } fun onLoading() {} fun onError(error: Throwable) {} fun onEmpty() {} fun onData(data: String) {}
请注意,对于ERROR和DATA之类的状态,编译器无法确定使用属性的安全性,因此用户必须编写冗余代码。 语义上的更改只能在运行时检测到。
密封类
通过简单的重构,我们可以将
State分为一组类:
sealed class State // stateless - singleton object Loading : State() data class Error(val error: Throwable) : State()
在用户方面,我们进行状态处理,其中属性的可访问性将在语言级别上确定,并且滥用会在编译阶段引起错误:
fun handleState(state: State) { when(state) { Loading -> onLoading() is Error -> onError(state.error) Empty -> onEmpty() is Data -> onData(state.data) } }
由于副本中仅存在重要的属性,因此我们可以讨论节省内存,以及重要的是改善语义。 密封类的用户无需根据
类型标记手动实施用于处理属性的规则;属性的可用性通过分隔成类型来确保。
这一切免费吗?
尝试过Kotlin的Java开发人员一定已经查看了反编译的代码,以了解Kotlin Java术语是什么样的。 带
when的表达式看起来像这样:
public static final void handleState(@NotNull State state) { Intrinsics.checkParameterIsNotNull(state, "state"); if (Intrinsics.areEqual(state, Loading.INSTANCE)) { onLoading(); } else if (state instanceof Error) { onError(((Error)state).getError()); } else if (Intrinsics.areEqual(state, Empty.INSTANCE)) { onEmpty(); } else if (state instanceof Data) { onData(((Data)state).getData()); } }
由于存在关于“错误代码的迹象”和“性能影响”的成见,具有大量
instanceof的分支可能会令人震惊。 必须以某种方式比较执行速度,例如使用
jmh 。
基于
“正确测量Java代码的速度”一文 ,准备了处理四个状态(LOADING,ERROR,EMPTY,DATA)的
测试,其结果如下:
Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 940739,966 ± 5350,341 ops/s CompareSealedVsTagged.tagged thrpt 500 1281274,381 ± 10675,956 ops/s
可以看出,密封的实现的速度慢了约25%(假设滞后时间不会超过10-15%)。
如果在四种类型上我们有四分之一的滞后,且类型(检查实例的数量)增加,则滞后只会增加。 为了进行检查,我们将类型的数量增加到16(假设我们已经设法获得了如此广泛的层次结构):
Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 149493,062 ± 622,313 ops/s CompareSealedVsTagged.tagged thrpt 500 235024,737 ± 3372,754 ops/s
加上生产率的下降,密封销售的滞后增加到了约35%-奇迹没有发生。
结论
在本文中,我们并没有发现America,分支分支中的密封实现实际上比链接比较慢。
但是,需要表达一些想法:
- 在大多数情况下,我们希望使用良好的代码语义,由于来自IDE和编译器检查的其他提示而加快了开发速度-在这种情况下,您可以使用密封类
- 如果您不能在一项任务中牺牲性能,则应忽略密封的实现,并用例如tagget实现来代替它。 也许您应该完全放弃Kotlin,转而使用低级语言