• Status: Experimental in 1.6.20-M1
  • Discussion: KEEP-259

介绍

Kotlin 在 1.6.20-M1 版本中添加了一个实验性的新特性 context receivers,以支持上下文相关的声明(context-dependent declarations)

这个特性是为了方便在 Kotlin 中面向上下文编程,而在此之前主要通过扩展函数和作用域函数实现。关于面向上下文编程可以看看这篇文章

引入 context receivers 的目的1

  • Remove all limitations of member extensions for writing contextual abstractions
    • Support top-level (non-member) contextual functions and properties
    • Support adding contextual function and properties to 3rd party context classes
    • Support multiple contexts
  • Make blocks of code with multiple receivers representable in Kotlin’s type system
  • Separate the concepts of extension and dispatch receivers from the concept of context receivers
    • Context receivers should not change the meaning of unqualified this expression
    • Multiple contexts should not be ordered during resolution, resolution ambiguities shall be reported
  • Design a scalable resolution algorithm with respect to the number of receivers
    • Call resolution should not be exponential in the number of context receivers

另外需要注意,context receivers 在 1.6.20 版本中还是实验性的,需要显式开启 -Xcontext-receivers 才能使用。比如在 Gradle 中使用:

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "17"
    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
}

使用场景

举个例子,我们来实现一个简单的银行交易逻辑,其中存款和取款必须是事务性的,在失败时进行回滚

/ox-hugo/account-service.svg

我们在 AccountService#transfer 中处理交易逻辑,通过 Transaction 进行事务性操作,所以 transfer 需要传入一个事务实例。常规实现:

class AccountService {
    fun transfer(tx: Transaction, vararg operations: () -> Unit) {
        tx.start()
        try {
            operations.forEach { it.invoke() }
            tx.commit()
        } catch (e: Exception) {
            tx.rollback()
        }
    }
}

然后在调用时将 Transaction 实例和 lambda 作为参数传入:

val service = AccountService()
val transaction = Transaction()
val repo = AccountRepo()
service.transfer(
    transaction,
    { repo.credit(account1, 10.5) },
    { repo.debit(account2, 10.5) }
)

上面的实现在 Java 中很常见,但是在 Kotlin 我们可以用扩展函数来优化一下:

class AccountService {
    fun Transaction.transfer(vararg operations: () -> Unit) {
        start()
        try {
            operations.forEach { it.invoke() }
            commit()
        } catch (e: Exception) {
            rollback()
        }
    }
}

现在 transfer 中的 this 指向了 Transaction 实例,那么就不再需要通过参数传入了,不过需要在 AccountService 实例的上下文中使用 transfer,比如使用 with:

with(service) {
    transaction.transfer(
        { repo.credit(account1, 10.5) },
        { repo.debit(account2, 10.5) }
    )
}

使用扩展函数虽然减少了一些代码,但是存在其他问题:在语义上和原来的版本是不同的——transfer 是 Transaction 类的方法,这一点也不符合实际的逻辑,Transaction 应该只包含事务性的操作

那么有没有办法将 Transaction 上下文引入 transfer 的同时,不改变 transfer 的所属类呢?Context receivers 特性就可以实现这一点

我们在 transfer 的声明上通过 context() 指定上下文,那么在函数内就会引入一个指向 Transaction 的隐式的 this

class AccountService {
    context(Transaction)
    fun transfer(vararg operations: () -> Unit) {
        start()
        try {
            operations.forEach { it.invoke() }
            commit()
        } catch (e: Exception) {
            rollback()
        }
    }
}

然后在 Transaction 实例的上下文中调用 transfer 服务,在语义上也符合逻辑

with(transaction) {
    service.transfer(
        { repo.credit(account1, 10.5) },
        { repo.debit(account2, 10.5) }
    )
}

思考

由于 context receivers 还处于实验阶段,使用体验和实际案例都还比较欠缺,要感受到它带来的好处恐怕还需要一段时间。不过在实现 DSL 上它肯定比继承更加适合,唯一需要担心的是滥用 context 导致代码晦涩难懂

关于 context receivers 的详细说明建议看 KEEP 上的相关提案:context-receivers