6个能让你的Kotlin代码库更有意思的“魔法糖”
语法糖会导致分号的悲剧。—— Alan J. Perlis
我们不断地失去一些东西。其中一些东西相对来说会更重要,现在重新拣起来还不算太晚。Kotlin 语言为程序员的生活带来了大量新的概念和特性,它们在日常开发中使用起来会很困难。我在生产环境中使用了两年 Kotlin 之后,才感受到它带来的快乐和满足。这是怎么发生的?原因就在那些小小的语法糖中。
我会在本文中与你分析我最喜欢的 Kotlin 语法糖,它们是在我需要写简洁而鲁棒 Android 应用程序组件时发现的。为了让这篇文章读起来更轻松,我把它分成三个部分。在这第一部分中,你会看到密封类和 when() 控制流函数。愉快的开始吧!
拥抱“模式匹配”的密封类
最近我的工作中有机会使用 Swift。我不仅要审核代码,还要将其中一些组件翻译成 Kotlin 实现。我读的代码越多,就越感到惊讶。最对我来说,最吸引人的特性是枚举。可惜 Kotlin 的枚举并不太灵活,我不得不挖掘合适的替代品: 密封类 。
密封类在编程界并不是什么新鲜玩意儿。事实上,密封类是一个非常知名的语言概念。Kotlin 引入了 sealed 关键字,它可用于类声明,表示对类层次结构的限制。某个值可以是有限类型中的一个,但它不能是其它类型。简单地说,你可以使用密封类来代替枚举,甚至做更多事情。
来看看下面的示例代码。
sealed class Response data class Success(val body: String): Response() data class Error(val code: Int, val message: String): Response() object Timeout: Response()
乍一看,这些代码除只是声明了一些简单的继承关系,但步步深入,就会提示一个谅人的真相。为 Response 类添加的 sealed 关键字到底起到了什么作用呢?提示这个问题最好的方法是使用 IntelliJ IDEA Kotlin Bytecode 工具。
第一 步。查看 Kotlin 字节码 (Kotlin Bytecode)
第二步。将 Kotlin 字节码反编译成 Java 代码
经过这样非常简单地翻译,你可以看到 Kotlin 代码对应的 Java 代码呈现。
public abstract class Response { private Response() { } // $FF: synthetic method public Response(DefaultConstructorMarker $constructor_marker) { this(); } }
你可能已经猜到了,密封类专们用于继承,所以它们是抽象的。不过他们变得与枚举相似的?在这里,Kotlin 编译器做了大量的工作,让你可以在 when() 函数中将 Response 的子类用作分支。此外,Kotlin 提供了很大的灵活性来允许对密封类的继承结构可以被当作数据声明甚至对象来使用。
fun sugar(response: Response) = when (response) { is Success -> ... is Error -> ... Timeout -> ... }
它不仅提供了非常彻底的表达式,还提供了自动类型转换,因此你可以在不需要额外的转换的情况下使用 Response 实例。
fun sugar(response: Response) = when (response) { is Success -> println(response.body) is Error -> println("${response.code} ${response.message}") Timeout -> println(response.javaClass.simpleName) }
你能想象一下,如果没有一个 sealed 的功能,或者根本没有 Kotlin ,它可能看起来是那么的丑陋和复杂?如果你忘记了 Java 语言的一些特性,请再次使用 IntelliJ IDEA Kotlin Bytecode ,但要坐下来使用 - 这可能会让你晕倒。
public final void sugar(@NotNull Response response) { Intrinsics.checkParameterIsNotNull(response, "response"); String var3; if (response instanceof Success) { var3 = ((Success)response).getBody(); System.out.println(var3); } else if (response instanceof Error) { var3 = "" + ((Error)response).getCode() + ' ' + ((Error)response).getMessage(); System.out.println(var3); } else { if (!Intrinsics.areEqual(response, Timeout.INSTANCE)) { throw new NoWhenBranchMatchedException(); } var3 = response.getClass().getSimpleName(); System.out.println(var3); } }
总结一下,我很高兴在这种情况下使用 sealed 关键字,因为它让我以类似于 Swift 的方式塑造我的 Kotlin 代码。
使用 when()函数来排列
由于你已经看到了 when()在 sealed 类中的用法,我决定再分享更多强大的功能。 想象一下,你必须实现一个接受两个 enums 并产生一个不可变状态的函数。
enum class Employee { DEV_LEAD, SENIOR_ENGINEER, REGULAR_ENGINEER, JUNIOR_ENGINEER } enum class Contract { PROBATION, PERMANENT, CONTRACTOR, }
enum class Employee 描述了在公司 XYZ 中可以找到的所有角色, enum class Contract 包含所有类型的雇佣合同。 基于这两个 enums ,你应该返回一个正确的 SafariBookAccess 。 而且,你的函数必须产生给定 enum 的所有排列的状态。 第一步,我们来创建状态生成函数的签名。
fun access(employee: Employee, contract: Contract): SafariBookAccess
现在是时候定义 SafariBooksAccess 结构体了,因为你已了解 sealed 关键字,这是使用它最适合的时机。封装 SafariBookAccess 并不是必须的,但它是封装不同情景下的 SafariBookAccess 的不同状态的好方式。
sealed class SafariBookAccess data class Granted(val expirationDate: DateTime) : SafariBookAccess() data class NotGranted(val error: AssertionError) : SafariBookAccess() data class Blocked(val message: String) : SafariBookAccess()
那么隐藏在 access() 函数后面的主要意图是什么?全排列!让我们罗列下。
fun access(employee: Employee, contract: Contract): SafariBookAccess { return when (employee) { SENIOR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Granted(DateTime()) CONTRACTOR -> Granted(DateTime()) } REGULAR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Granted(DateTime()) CONTRACTOR -> Blocked("Access blocked for $contract.") } JUNIOR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Blocked("Access blocked for $contract.") CONTRACTOR -> Blocked("Access blocked for $contract.") } else -> throw AssertionError() } }
这个代码很完美,但你能让它更像 Kotlin 吗?当你每天对同事的 PR/MR 进行审查时会有什么建议吗?你可能会写一些这样的评论:
- 太多 when() 函数。使用 Pair 来避免嵌套。
- 改变枚举参数的顺序,定义 Pair() 对象来让它更易读。
- 合并重复的 return。
- 改为一个表达式函数。
fun access(contract: Contract, employee: Employee) = when (Pair(contract, employee)) { Pair(PROBATION, SENIOR_ENGINEER), Pair(PROBATION, REGULAR_ENGINEER), Pair(PROBATION, JUNIOR_ENGINEER) -> NotGranted(AssertionError("Access not allowed on probation contract.")) Pair(PERMANENT, SENIOR_ENGINEER), Pair(PERMANENT, REGULAR_ENGINEER), Pair(PERMANENT, JUNIOR_ENGINEER), Pair(CONTRACTOR, SENIOR_ENGINEER) -> Granted(DateTime(1)) Pair(CONTRACTOR, REGULAR_ENGINEER), Pair(CONTRACTOR, JUNIOR_ENGINEER) -> Blocked("Access for junior contractors is blocked.") else -> throw AssertionError("Unsupported case of $employee and $contract") }
现在它看起来更整洁,但 Kotlin 还有语法糖可以完全省略对 Pair 的定义。棒!
fun access(contract: Contract, employee: Employee) = when (contract to employee) { PROBATION to SENIOR_ENGINEER, PROBATION to REGULAR_ENGINEER -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT to SENIOR_ENGINEER, PERMANENT to REGULAR_ENGINEER, PERMANENT to JUNIOR_ENGINEER, CONTRACTOR to SENIOR_ENGINEER -> Granted(DateTime(1)) CONTRACTOR to REGULAR_ENGINEER, PROBATION to JUNIOR_ENGINEER, CONTRACTOR to JUNIOR_ENGINEER -> Blocked("Access for junior contractors is blocked.") else -> throw AssertionError("Unsupported case of $employee and $contract") }
这个结构让我的生活变得轻松,也让 Kotlin 代码读写变得容易,我希望你也觉得这很有用。但它是不是不能用于三元组呢?答案是肯定的。