在 Kotlin 中,如何将扩展方法添加到另一个 class,但仅在特定上下文中可见?

In Kotlin, how do I add extension methods to another class, but only visible in a certain context?

在 Kotlin 中,我想将扩展方法添加到 class,例如添加到 class Entity。但我只想在 Entity 在交易中时看到这些扩展,否则隐藏。例如,如果我定义这些 classes 和扩展:

interface Entity {}

fun Entity.save() {}
fun Entity.delete() {}

class Transaction {
    fun start() {}
    fun commit() {}
    fun rollback() {}
}

我现在可以随时不小心调用 save()delete(),但我只希望它们在交易的 start() 之后可用,而不是在 commit() 之后可用或 rollback()?目前我可以这样做,这是错误的:

someEntity.save()       // DO NOT WANT TO ALLOW HERE
val tx = Transaction()
tx.start()
someEntity.save()       // YES, ALLOW
tx.commit()
someEntity.delete()     // DO NOT WANT TO ALLOW HERE

如何让它们在正确的上下文中出现和消失?

注意: 这个问题是作者(Self-Answered Questions)有意写下并回答的,以便对常见的 Kotlin 主题进行地道的回答存在于 SO 中。还要澄清一些为 Kotlin alpha 编写的非常古老的答案,这些答案对于当今的 Kotlin 来说并不准确。也欢迎其他回答,这个回答的方式有很多种!

基础知识:

在 Kotlin 中,我们倾向于使用传递给其他 classes 的 lambda 来为它们提供 "scope" 或在执行 lambda 之前和之后发生的行为,包括错误处理。因此,您首先需要更改 Transaction 的代码以提供范围。这是修改后的 Transaction class:

class Transaction(withinTx: Transaction.() -> Unit) {
    init {
        start()
        try {
            // now call the user code, scoped to this transaction class
            this.withinTx()
            commit()
        }
        catch (ex: Throwable) {
            rollback()
            throw ex
        }

    }
    private fun Transaction.start() { ... }

    fun Entity.save(tx: Transaction) { ... }
    fun Entity.delete(tx: Transaction) { ... }

    fun Transaction.save(entity: Entity) { entity.save(this) }
    fun Transaction.delete(entity: Entity) { entity.delete(this) }

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

这里我们有一个事务,在创建时需要一个 lambda 在事务中进行处理,如果没有抛出异常,它会自动提交事务。 (Transaction class 的构造函数就像 Higher-Order Function

我们还将 Entity 的扩展函数移到了 Transaction 中,这样如果不在此 class 的上下文中,就不会看到或调用这些扩展函数。这包括 commit()rollback() 的方法,它们现在只能从 class 本身内部调用,因为它们现在是 class 范围内的扩展函数。

由于接收到的 lambda 是 Transaction 的扩展函数,它在 class 的上下文中运行,因此可以看到扩展。 (参见:Function Literals with Receiver

这段旧代码现在无效,编译器给我们一个错误:

fun changePerson(person: Person) {
    person.name = "Fred" 
    person.save() // ERROR: unresolved reference: save()
}

现在您将编写代码以存在于 Transaction 块中:

fun actsInMovie(actor: Person, film: Movie) {
    Transaction {   // optional parenthesis omitted
        if (actor.winsAwards()) {
            film.addActor(actor)
            save(film)
        } else {
            rollback()
        }
    }
}

传入的 lambda 被推断为 Transaction 上的扩展函数,因为它没有正式声明。

要在交易中将这些 "actions" 链接在一起,只需创建一系列可在交易中使用的扩展函数,例如:

fun Transaction.actsInMovie(actor: Person, film: Movie) {
    film.addActor(actor)
    save(film)
}

像这样创建更多,然后在传递给事务的 lambda 中使用它们...

Transaction { 
   actsInMovie(harrison, starWars)
   actsInMovie(carrie, starWars)
   directsMovie(abrams, starWars)
   rateMovie(starWars, 5)
}

现在回到最初的问题,我们的交易方法和实体方法只在正确的时间出现。作为使用 lambda 或匿名函数的副作用,我们最终会探索有关代码组成方式的新想法。

请参阅 了解主要主题和基础知识,这里是更深的水域...

相关高级主题:

我们不会解决您可能 运行 到这里的所有问题。很容易让一些扩展函数出现在另一个 class 的上下文中。但是同时为两件事情做这项工作并不是那么容易。例如,如果我想让 Movie 方法 addActor() 只出现在 Transaction 块内,那就更难了。 addActor() 方法不能同时有两个接收者。所以我们要么有一个接收两个参数 Transaction.addActorToMovie(actor, movie) 的方法,要么我们需要另一个计划。

实现此目的的一种方法是使用我们可以扩展系统的中间对象。现在,下面的示例可能合理也可能不合理,但它显示了如何仅根据需要进行这种额外级别的公开功能。这是代码,我们在其中更改 Transaction 以实现接口 Transactable 以便我们现在可以随时 delegate to the interface

当我们添加新功能时,我们可以创建 Transactable 的新实现来公开这些函数并保存临时状态。然后一个简单的辅助函数可以很容易地访问这些隐藏的新 classes。所有添加都可以在不修改核心原classes.

的情况下完成

核心classes:

interface Entity {}

interface Transactable {
    fun Entity.save(tx: Transactable)
    fun Entity.delete(tx: Transactable)

    fun Transactable.commit()
    fun Transactable.rollback()

    fun Transactable.save(entity: Entity) { entity.save(this) }
    fun Transactable.delete(entity: Entity) { entity.save(this) }
}


class Transaction(withinTx: Transactable.() -> Unit) : Transactable {
    init {
        start()
        try {
            withinTx()
            commit()
        } catch (ex: Throwable) {
            rollback()
            throw ex
        }
    }

    private fun start() { ... }

    override fun Entity.save(tx: Transactable) { ... }
    override fun Entity.delete(tx: Transactable) { ... }

    override fun Transactable.commit() { ... }
    override fun Transactable.rollback() { ... }
}


class Person : Entity { ... }
class Movie : Entity { ... }

后来决定补充:

class MovieTransactions(val movie: Movie, 
                        tx: Transactable, 
                        withTx: MovieTransactions.()->Unit): Transactable by tx {
    init {
        this.withTx()
    }

    fun swapActor(originalActor: Person, replacementActor: Person) {
        // `this` is the transaction
        // `movie` is the movie
        movie.removeActor(originalActor)
        movie.addActor(replacementActor)
        save(movie)
    }

    // ...and other complex functions
}

fun Transactable.forMovie(movie: Movie, withTx: MovieTransactions.()->Unit) {
    MovieTransactions(movie, this, withTx)
}

现在使用新功能:

fun castChanges(swaps: Pair<Person, Person>, film: Movie) {
    Transaction {
        forMovie(film) {
            swaps.forEach { 
                // only available here inside forMovie() lambda
                swapActor(it.first, it.second) 
            }
        }
    }
}

或者,如果您不介意它位于顶层而不是 class 中,那么整个事情可能只是 Transactable 上的顶层扩展函数包的命名空间。

有关使用中介 classes 的其他示例,请参阅:

  • 在 Klutter TypeSafe 配置模块中,一个中间对象用于存储 "which property" 的状态,可以对其进行操作,因此它可以传递并改变其他可用的方法。 config.value("something").asString() (code link)
  • 在 Klutter Netflix Graph 模块中,一个中间对象用于转换到 DSL 语法的另一部分 connect(node).edge(relation).to(otherNode)。 (同一模块中的 code link) The test cases 显示更多用途,包括 get()invoke() 等运算符如何仅在上下文中可用。