Kotlin 在 1.6.20 版本中加入了 Context Receivers 的想法。在这篇文章中,我想玩弄它们以了解它们的用处。
请注意,如果您想一起玩,则需要使用context-receivers标志进行编译。
Context Receivers背后的主要思想是将附加参数传递给函数,而无需显式执行。
简化的模型样本
让我们从一个简单的例子开始来展示它是如何工作的。我们想对两个 ba 之间的简单传输操作进行建模让我们从一个简单的例子开始来展示它是如何工作的。我们想模拟两个银行账户之间的简单转账操作。账户余额存储在数据库中,相应的贷记/还款操作必须是交易性的。
让我们专注于AccountService.transfer()功能。它需要一个Transaction包含多个操作的实例:
class AccountService {
fun transfer(tx: Transaction, vararg operations: () -> Unit) {
tx.start()
try {
operations.forEach { it.invoke() }
tx.commit()
} catch (e: Exception) {
tx.rollback()
}
}
}
我们可以将上面的代码称为:
val service = AccountService()
val transaction = Transaction()
val repo = AccountRepo()
service.transfer(
transaction,
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)
使用扩展函数改进代码
我们可以利用扩展函数对上面的代码稍加改进。我们可以将后者迁移到扩展函数,而不是将其定义Transaction为函数的参数。transfer()
class AccountService {
fun Transaction.transfer(vararg operations: () -> Unit) {
start() // 1
try {
operations.forEach { it.invoke() }
commit() // 1
} catch (e: Exception) {
rollback() // 1
}
}
}
- 隐式this引用Transaction对象
在 的上下文中AccountService,我们现在可以transfer()在现有的 上调用函数Transaction。
with(service) { // 1
transaction.transfer( // 2
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)
}
- 将service实例纳入范围
- 所以调用对象是有效transfer的transaction
可以从两个不同的角度分析新的调用代码:
- 输入的原始字符数更少、更简洁
- 但是,语义完全不同。新代码意味着在的Context中AccountService,我们可以调用transfer()现有Transaction对象。
我认为语义是错误的;它应该是相反的。它应该是相反的。在的Context中Transaction,我们应该能够调用transfer()现有AccountService对象:
with(transaction) {
service.transfer(
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)
}
恕我直言,与错误语义的成本相比,简洁的价值很小。不幸的是,对于当前的语言结构,修复语义意味着我们需要将transfer()函数移动到Transaction. 这将是糟糕的建模,因为传输是服务的责任。
Context Receivers
正如我在介绍中提到的,Context Receivers背后的想法是以某种方式“传递”函数参数,而不是明确地说明它们。
context(Foo, Bar, Baz)
fun myfunction() {}
要调用这样的函数,需要将每个Context类型的对象带入“范围”。我们可以通过with函数来实现:
val foo = Foo()
val bar = Bar()
val baz = Baz()
with(foo) { // 1
with(bar) { // 2
with (baz) { // 3
myfunction() // 4
}
}
}
- 引入foo范围
- 引入bar范围
- 引入baz范围
- 调用函数
虽然上面的代码可以编译,但它只有在我们使用Context对象时才适用。调用语法与带有接收器的 lambda 之一相同:
context(Foo, Bar, Baz)
fun myfunction() {
println(this@Foo)
println(this@Bar)
println(this@Baz)
}
我们使用Context Receivers能够使用想要的代码编写代码:
class AccountService {
context(Transaction)
fun transfer(vararg operations: () -> Unit) {
start() // 1
try {
operations.forEach { it.invoke() }
commit() // 1
} catch (e: Exception) {
rollback() // 1
}
}
}
- 隐式this引用Transaction对象。我们不需要进一步限定类名,因为没有其他Context对象
我们现在可以使用正确的语义相应地调用代码:
with(transaction) { // 1
service.transfer( // 2
{ repo.credit(account1, 10.5) },
{ repo.debit(account2, 10.5) }
)
}
- 引入transaction范围
- 使用transaction范围内的对象
讨论
Context Receivers允许我们使用正确的调用代码语义来实现 API。没有他们是不可能的。
我在 Scala 中只涉足了一点,但我总是发现 Scala 2 的implicit实现很差。要将对象带入范围,您只需要import在文件顶部放置一个,这可能与使用它的位置相距甚远。它使理解代码变得更加困难,并增加了维护成本。Scala 3 使隐含性更加明确,并解决了我的一些不满。
我相信 Kotlin 的实现要理智得多。您可以使用 实现Context Receivers的范围界定with,这会将Context靠近调用站点并使用代码块发出信号。
然而,并不是所有的独角兽和彩虹。特别是,我有点担心Context Receivers会被滥用。当然,每种语言的每个新功能通常都是这种情况。然而,我觉得这个滥用的可能性是巨大的。只有未来会告诉我们。
与此同时,我很想看到Context Receivers的更多不同用法以及它们可以解锁哪些模式。
为了帮助需要入门和深入的学习 Kotlin ,我近日整理发布一份《高级Kotlin强化实战学习手册(附Demo)》供Android开发者进阶学习 Kotlin,内容涵盖 Kotlin 入门教程、Kotlin实战避坑指南、Kotlin Jetpack 实战三大模块。
第一章 Kotlin入门教程
● Kotlin 概述
● Kotlin 与 Java 比较
● 巧用 Android Studio
● 认识 Kotlin 基本类型
● 走进 Kotlin 的数组
● 走进 Kotlin 的集合
● 集合问题
● 完整代码
● 基础语法
第二章 Kotlin 实战避坑指南
● 方法入参是常量,不可修改
● 不要 Companion 、INSTANCE ?
● Java 重载,在 Kotlin 中怎么巧妙过渡一下?
● Kotlin 中的判空姿势
● Kotlin 复写 Java 父类中的方法
● Kotlin “狠”起来,连TODO 都不放过!
● is、as` 中的坑
● Kotlin 中的 Property 的理解
● also 关键字
● takeIf 关键字
● takeIf 关键字
● 单例模式的写法
第三章 项目实战《Kotlin Jetpack 实战》
● 从一个膜拜大神的 Demo 开始
● Kotlin 写 Gradle 脚本是一种什么体验?
● Kotlin 编程的三重境界
● Kotlin 高阶函数
● Kotlin 泛型
● Kotlin 扩展
● Kotlin 委托
● 协程“不为人知”的调试技巧
● 图解协程:suspend
如有需要可以点击文末微信卡片免费领取