跳到主要内容

Kotlin Context Parameters 实战:告别依赖传递的优雅方案

· 阅读需 8 分钟
重庆 Kotlin User Group

在日常开发中,我们经常遇到这样的场景:一个深层调用链中的多个函数都需要访问某个上下文对象(如 Logger、Transaction、Configuration),但又不想把它作为显式参数层层传递。传统做法要么使用全局变量(破坏可测试性),要么在每一层都添加参数(代码冗长且脆弱)。Kotlin 2.4.0 正式稳定的 Context Parameters 特性,为这个问题提供了一个优雅的语言级解决方案。

问题:Logger 到底要传多少层?​

让我们从一个常见的场景开始:

interface Logger {
fun info(message: String)
fun error(message: String, throwable: Throwable? = null)
}

class ConsoleLogger : Logger {
override fun info(message: String) {
println("[INFO] $message")
}
override fun error(message: String, throwable: Throwable?) {
System.err.println("[ERROR] $message: ${throwable?.message}")
}
}

如果只有一个函数需要 Logger,很简单:

fun saveUser(name: String, logger: Logger) {
logger.info("Saving user: $name")
// 保存逻辑...
}

fun main() {
val logger = ConsoleLogger()
saveUser("Alice", logger)
}

但当调用链变深时,问题就出现了:

fun createUser(name: String, logger: Logger) {
validateUser(name, logger) // 需要传递 logger
saveUser(name, logger) // 需要传递 logger
}

fun validateUser(name: String, logger: Logger) {
logger.info("Validating user: $name")
// 校验逻辑...
}

fun saveUser(name: String, logger: Logger) {
logger.info("Saving user: $name")
// 保存逻辑...
}

每一层函数都要带着 logger 参数,即使它本身并不直接使用,只是为了传递给更深层的函数。这不仅让代码冗长,还增加了重构的成本——如果某天需要添加一个新的上下文依赖(比如 Transaction),所有函数签名都要修改。

旧方案:Context Receivers 的尝试​

在 Kotlin 2.2.0 中,JetBrains 引入了实验性的 Context Receivers 来尝试解决这个问题:

context(logger: Logger)
fun validateUser(name: String) {
logger.info("Validating user: $name")
}

context(logger: Logger)
fun saveUser(name: String) {
logger.info("Saving user: $name")
}

context(logger: Logger)
fun createUser(name: String) {
validateUser(name) // 不需要显式传递 logger
saveUser(name) // 不需要显式传递 logger
}

fun main() {
val logger = ConsoleLogger()
with(logger) {
createUser("Alice")
}
}

这个方案的方向是对的,但 Context Receivers 在语法和语义上存在一些争议,最终被重新设计为 Context Parameters。

新方案:Context Parameters 登场​

Kotlin 2.4.0 正式稳定的 Context Parameters 是 Context Receivers 的替代方案,语法更清晰,语义更明确。

基本用法​

context(logger: Logger)
fun validateUser(name: String) {
logger.info("Validating user: $name")
// 校验逻辑...
}

context(logger: Logger)
fun saveUser(name: String) {
logger.info("Saving user: $name")
// 保存逻辑...
}

context(logger: Logger)
fun createUser(name: String) {
validateUser(name) // logger 自动传递
saveUser(name) // logger 自动传递
}

fun main() {
val logger = ConsoleLogger()
with(logger) {
createUser("Alice")
}
}

关键变化:

  • 使用 context(...) 代替 context(...) 接收器
  • 在调用时,使用 with(logger) { ... } 或直接在作用域内调用

多个上下文参数​

Context Parameters 的真正威力在于支持多个上下文依赖:

interface Transaction {
fun commit()
fun rollback()
}

context(logger: Logger, transaction: Transaction)
fun transferMoney(from: String, to: String, amount: Double) {
logger.info("Transferring $amount from $from to $to")
// 执行转账逻辑...
transaction.commit()
logger.info("Transfer completed")
}

context(logger: Logger, transaction: Transaction)
fun processPayment(userId: String, amount: Double) {
logger.info("Processing payment for user $userId")
transferMoney("SYSTEM", userId, amount) // logger 和 transaction 自动传递
}

fun main() {
val logger = ConsoleLogger()
val transaction = FakeTransaction()

with(logger) {
with(transaction) {
processPayment("user_123", 100.0)
}
}
}

在类中使用​

Context Parameters 不仅适用于函数,也可以用于类:

context(logger: Logger)
class UserService {
fun createUser(name: String) {
logger.info("Creating user: $name")
// 业务逻辑...
}

fun deleteUser(id: String) {
logger.info("Deleting user: $id")
// 业务逻辑...
}
}

fun main() {
val logger = ConsoleLogger()
with(logger) {
val service = UserService()
service.createUser("Alice")
service.deleteUser("user_123")
}
}

实战场景​

场景 1:数据库操作与事务管理​

interface Database {
fun query(sql: String): List<Map<String, Any>>
fun execute(sql: String)
}

context(db: Database, logger: Logger)
fun updateUser(userId: String, newName: String) {
logger.info("Updating user $userId to $newName")
db.execute("UPDATE users SET name = '$newName' WHERE id = '$userId'")
logger.info("Update completed")
}

context(db: Database, logger: Logger)
fun getUser(userId: String): Map<String, Any>? {
logger.info("Fetching user $userId")
return db.query("SELECT * FROM users WHERE id = '$userId'").firstOrNull()
}

context(db: Database, logger: Logger)
fun migrateUserData() {
logger.info("Starting data migration")
val users = db.query("SELECT * FROM old_users")
users.forEach { user ->
val id = user["id"] as String
val name = user["name"] as String
updateUser(id, name)
}
logger.info("Migration completed")
}

场景 2:Compose 风格的 DSL​

Context Parameters 非常适合构建 Compose 风格的 DSL:

class HtmlBuilder {
private val content = StringBuilder()

fun tag(name: String, block: HtmlBuilder.() -> Unit = {}) {
content.append("<$name>")
block()
content.append("</$name>")
}

fun text(value: String) {
content.append(value)
}

override fun toString() = content.toString()
}

context(builder: HtmlBuilder)
fun header(level: Int, text: String) {
builder.tag("h$level") {
builder.text(text)
}
}

context(builder: HtmlBuilder)
fun paragraph(text: String) {
builder.tag("p") {
builder.text(text)
}
}

fun html(block: context(HtmlBuilder) () -> Unit): String {
val builder = HtmlBuilder()
builder.tag("html") {
block(builder)
}
return builder.toString()
}

fun main() {
val result = html {
header(1, "Welcome")
paragraph("This is a paragraph.")
header(2, "Section")
paragraph("Another paragraph.")
}
println(result)
}

场景 3:依赖注入的轻量级替代​

对于小型项目或特定模块,Context Parameters 可以作为轻量级的依赖注入方案:

context(config: AppConfig, logger: Logger, db: Database)
class UserRepository {
fun findById(id: String): User? {
logger.info("Finding user by id: $id")
val row = db.query("SELECT * FROM users WHERE id = '$id'").firstOrNull()
return row?.let { mapToUser(it) }
}

fun save(user: User) {
logger.info("Saving user: ${user.name}")
db.execute("INSERT INTO users (id, name, email) VALUES ('${user.id}', '${user.name}', '${user.email}')")
}

private fun mapToUser(row: Map<String, Any>): User {
return User(
id = row["id"] as String,
name = row["name"] as String,
email = row["email"] as String
)
}
}

data class User(val id: String, val name: String, val email: String)

class AppConfig(val maxConnections: Int, val timeout: Long)

fun main() {
val config = AppConfig(maxConnections = 10, timeout = 5000L)
val logger = ConsoleLogger()
val db = FakeDatabase()

with(config) {
with(logger) {
with(db) {
val repo = UserRepository()
repo.save(User("1", "Alice", "alice@example.com"))
val user = repo.findById("1")
println("Found user: $user")
}
}
}
}

与 Context Receivers 的对比​

如果你之前已经使用了实验性的 Context Receivers,需要注意以下迁移要点:

特性Context Receivers (旧)Context Parameters (新)
语法context(Logger) fun foo()context(logger: Logger) fun foo()
命名不支持命名支持命名参数
状态已废弃Kotlin 2.4.0 稳定
编译器参数-Xcontext-receivers默认启用(稳定特性)

重要提示:不要同时启用 -Xcontext-receivers 和 -Xcontext-parameters,它们是互斥的。

实验性特性:显式 Context Arguments​

Kotlin 2.4.0 还引入了一个实验性特性:显式 Context Arguments,用于解决重载歧义问题。

假设有这样的场景:

class EmailSender
class SmsSender

context(emailSender: EmailSender)
fun sendNotification() {
println("Sent email notification")
}

context(smsSender: SmsSender)
fun sendNotification() {
println("Sent SMS notification")
}

在调用时,编译器可能无法推断应该使用哪个 context。这时可以启用显式 context arguments:

kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-context-arguments")
}
}

然后可以显式指定:

context(
defaultEmailSender: EmailSender,
defaultSmsSender: SmsSender
)
fun notifyUser() {
sendNotification(emailSender = defaultEmailSender) // 显式指定
sendNotification(smsSender = defaultSmsSender) // 显式指定
}

这个特性目前还是实验性的,普通场景下不需要使用,但在构建复杂的 DSL 或库时非常有用。

最佳实践​

1. 控制 context 参数的数量​

虽然 Context Parameters 支持多个参数,但建议不要超过 3 个。如果超过,考虑将它们封装成一个上下文对象:

// 不推荐
context(logger: Logger, db: Database, config: AppConfig, cache: Cache, metrics: Metrics)
fun processData() { ... }

// 推荐
data class AppContext(
val logger: Logger,
val db: Database,
val config: AppConfig,
val cache: Cache,
val metrics: Metrics
)

context(ctx: AppContext)
fun processData() {
ctx.logger.info("Processing...")
ctx.db.query(...)
}

2. 优先使用接口类型​

context 参数优先使用接口类型,而不是具体实现,这样便于测试和替换:

// 推荐
context(logger: Logger)
fun doWork() { ... }

// 不推荐
context(logger: ConsoleLogger)
fun doWork() { ... }

3. 在测试中轻松替换​

Context Parameters 的一个巨大优势是测试时可以轻松替换依赖:

class FakeLogger : Logger {
val logs = mutableListOf<String>()

override fun info(message: String) {
logs.add("[INFO] $message")
}

override fun error(message: String, throwable: Throwable?) {
logs.add("[ERROR] $message: ${throwable?.message}")
}
}

@Test
fun testCreateUser() {
val fakeLogger = FakeLogger()
with(fakeLogger) {
createUser("Alice")
}

assert(fakeLogger.logs.contains("[INFO] Creating user: Alice"))
}

4. 避免过度使用​

Context Parameters 虽然强大,但不要滥用。对于简单的场景,显式参数传递可能更清晰:

// 简单场景:显式参数更清晰
fun calculateSum(a: Int, b: Int): Int = a + b

// 复杂场景:Context Parameters 更合适
context(logger: Logger, db: Database, cache: Cache)
fun complexBusinessLogic() { ... }

总结​

Context Parameters 是 Kotlin 2.4.0 带来的一个重要特性,它为解决"依赖传递"问题提供了一个优雅的语言级方案。通过合理使用 Context Parameters,我们可以:

  • ✅ 消除冗余的参数传递
  • ✅ 提高代码的可读性和可维护性
  • ✅ 保持代码的可测试性
  • ✅ 构建更清晰的 DSL

但也要注意:

  • ⚠️ 控制 context 参数的数量(建议不超过 3 个)
  • ⚠️ 优先使用接口类型
  • ⚠️ 避免过度使用,简单场景用显式参数即可

随着 Kotlin 2.4.0 的稳定发布,Context Parameters 已经可以在生产环境中使用。如果你的项目中存在大量的依赖传递问题,不妨试试这个新特性,相信它会为你的代码带来显著的改善。