1. 上一章:第 12 章
  2. 下一章:第 14 章
Kotlin 图解指南 • 第 13 章

类委托简介

章节封面图片

Roger 在餐厅吃晚餐时,服务员走过来问道:”先生,我能先给您上点喝的吗?”

”我想要一杯汽水,谢谢,”Roger 说。

”请稍等,先生。”服务员走回厨房的柜台,倒了一杯汽水,放在桌上。”先生,您准备好点餐了吗?”

Roger 回答说:”好的,我想要三文鱼配米饭,谢谢。”

“我会帮您下单,”服务员说道。他再次走回厨房。但这一次,他没有亲自准备订单,而是将它交给厨师,由厨师巧妙地准备美味的晚餐。准备完成后,厨师把餐点交给服务员,服务员回来后将餐点放在Roger的桌上。

餐厅和代码中的委托

正如这个故事所示,有时候服务员无需厨师参与就能完成订单——比如Roger点饮料时,但在其他情况下,比如当Roger点主菜时,服务员必须将订单交给厨师来完成。

类似地,在 Kotlin 中,有时候一个对象能够完全独立地完成请求(比如函数调用),但在其他情况下,它可能需要将请求转发给另一个对象

无论这发生在现实生活中还是 Kotlin 中,这都称为委托

手动委托

让我们回顾一下顾客、服务员和厨师之间的关系。

  1. 顾客服务员下订单。
  2. 当订单是主菜时,服务员将主菜准备工作委托给厨师
插图:顾客坐在桌边,服务员和厨师。箭头标注:

请注意,顾客从不直接与厨师交流——他们只与服务员互动,服务员将代表顾客与厨师交流。

与上图相同,但显示顾客和厨师之间有一条线,线上有一个 X。

我们可以在 Kotlin 中对这个顾客-服务员-厨师关系进行建模。首先,让我们把上面的插图替换成方框,这可以将我们的插图转换成简单的 UML 类图

UML 类图。顾客 -> 服务员 -> 厨师。 orders from delegates entrees to Customer Waiter + prepareBeverage(name: String): Beverage? + prepareEntree(name: String): Entree? + acceptPayment(money: Int) Chef + prepareEntree(name: String): Entree?

现在我们准备创建代表服务员和厨师的类,以及一些用于饮料和主菜的枚举类。

class Chef {
    fun prepareEntree(name: String): Entree? = when (name) {
        "Tossed Salad"   -> Entree.TOSSED_SALAD
        "Salmon on Rice" -> Entree.SALMON_ON_RICE
        else             -> null
    }
}

class Waiter(private val chef: Chef) {
    // The waiter can prepare a beverage by himself...
    fun prepareBeverage(name: String): Beverage? = when (name) {
        "Water" -> Beverage.WATER
        "Soda"  -> Beverage.SODA
        else    -> null
    }

    // ... but needs the chef to prepare an entree
    fun prepareEntree(name: String): Entree? = chef.prepareEntree(name)

    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}

enum class Entree { TOSSED_SALAD, SALMON_ON_RICE }
enum class Beverage { WATER, SODA }

在上面这段代码中,Waiter中的 prepareEntree() 函数只是调用 chef对象上的相同函数,并传递它接收到的相同 name参数。这就是手动委托。

delegate 这个词可以是名词也可以是动词,发音略有不同。在上面的代码中,chef 对象被称为 delegate(委托对象),因为 Waiter delegates(委托)prepareEntree() 调用给它。

现在我们有了服务员和厨师的类。但是,我们不会麻烦地为顾客创建一个类。相反,下一段代码将扮演顾客的角色,在 Waiter 对象上调用函数。

val waiter = Waiter(Chef())

val beverage = waiter.prepareBeverage("Soda")
val entree = waiter.prepareEntree("Salmon on Rice")

恭喜你!你已经创建了 WaiterChef 之间的一个简单的手动委托关系。稍后我们将看到,委托可以比这更容易!

不过,在我们继续之前,你可能已经注意到 WaiterChef 都有一个名为 prepareEntree() 的函数,而且这两个函数具有相同的参数类型和返回类型。

指出这两个函数具有相同的签名。 class Chef { fun prepareEntree (name: String): Entree? = when (name) { "Caesar Salad" Entree. CAESAR_SALAD "Salmon on Rice" Entree. SALMON_ON_RICE else null } } class Waiter( private val chef : Chef) { The waiter can prepare a beverage by himself fun prepareBeverage (name: String): Beverage? = when (name) { "Water" Beverage. WATER "Soda" Beverage. SODA else null } but needs the chef to prepare an entree fun prepareEntree (name: String): Entree? = chef .prepareEntree(name) fun acceptPayment (money: Int) = println( "Thank you for paying for your meal" ) } same function name, parameters, and return type fun prepareEntree (name: String): Entree? fun prepareEntree (name: String): Entree?

正如我们在上一章中看到的,当发生这种情况时,我们可以创建一个接口。让我们更新 代码清单 13.1 中的代码,使 WaiterChef 都实现相同的接口。请记住,当我们这样做时,我们还需要在这两个类中将 prepareEntree() 函数标记为 override

interface KitchenStaff {
    fun prepareEntree(name: String): Entree?
}

class Chef : KitchenStaff {
    override fun prepareEntree(name: String): Entree? = when (name) {
        "Tossed Salad"   -> Entree.TOSSED_SALAD
        "Salmon on Rice" -> Entree.SALMON_ON_RICE
        else             -> null
    }
}

class Waiter(private val chef: Chef) : KitchenStaff {
    fun prepareBeverage(name: String): Beverage? = when (name) {
        "Water" -> Beverage.WATER
        "Soda"  -> Beverage.SODA
        else    -> null
    }

    override fun prepareEntree(name: String): Entree? = chef.prepareEntree(name)
    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}

好极了!现在我们在 WaiterChef 之间有了委托关系,此外我们还为他们共享的 prepareEntree() 函数创建了一个接口。

委托更多的函数调用

现在,手动编写一个函数来调用 chef.prepareEntree() 还算可以,但还有很多其他情况服务员可能需要委托给厨师。例如:

  • 获取厨师当天的特色菜列表
  • 准备开胃菜
  • 准备甜点
  • 将顾客的称赞转达给厨师

更新接口以包含这些内容很容易:

interface KitchenStaff {
    val specials: List<String>
    fun prepareEntree(name: String): Entree?
    fun prepareAppetizer(name: String): Appetizer?
    fun prepareDessert(name: String): Dessert?
    fun receiveCompliment(message: String)
}

但是,我们还必须更新 ChefWaiter 类来实现新的属性和函数。至于 Chef 类如何实现这些函数……我们可以发挥想象力。不过,我们很容易看出这如何影响 Waiter 类:

class Waiter(private val chef: Chef) : KitchenStaff {
    // These first two functions are the same as before
    fun prepareBeverage(name: String): Beverage? = when (name) {
        "Water" -> Beverage.WATER
        "Soda"  -> Beverage.SODA
        else    -> null
    }

    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")

    // Manually delegating to the chef for all of these things: 
    override val specials: List<String> get() = chef.specials
    override fun prepareEntree(name: String) = chef.prepareEntree(name)
    override fun prepareAppetizer(name: String) = chef.prepareAppetizer(name)
    override fun prepareDessert(name: String) = chef.prepareDessert(name)
    override fun receiveCompliment(message: String) = chef.receiveCompliment(message)
}

随着越来越多的属性和函数被添加到 KitchenStaff 接口中,手动从 Waiter 委托给 Chef 变得繁琐、难读,而且更容易出错。

当你看上面的 override 函数时,你会发现代码中有很多重复的文字:

  • override fun
  • 函数名在代码行的左侧和右侧
  • 参数名在左侧和右侧

每行的模式都是相同的。具有这种重复模式的代码称为样板代码1 即使使用 Kotlin 的表达式函数,就像我们在这里所做的,样板代码真的开始堆积如山。

大量的样板代码 override val specials : List<String> get () = chef . specials override fun prepareEntree (name: String) = chef .prepareEntree(name) override fun prepareAppetizer (name: String) = chef .prepareAppetizer(name) override fun prepareDessert (name: String) = chef .prepareDessert(name) override fun receiveCompliment (message: String) = chef .receiveCompliment(message) Lots of boilerplate!

谢天谢地,Kotlin 让这种委托变得简单,无需手动编写所有代码!

简单的委托,Kotlin 方式

我们不必手动编写所有委托的样板代码,只需使用 Kotlin 的类委托功能。要做到这一点,我们只需要做两件事:

  1. 指明要委托哪些函数和属性
  2. 指明它们应该被委托给哪个对象

函数和属性由包含它们的接口名称指定,委托对象使用 by 关键字指定。一旦你这样做了,你就可以删除手动委托。

例如,要将 KitchenStaff 中的所有属性和函数委托给 chef 对象,我们只需这样写:

class Waiter(private val chef: Chef) : KitchenStaff by chef {
    fun prepareBeverage(name: String): Beverage? = when (name) { /* ... */ }
    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}

简单吧?此代码的工作方式与代码清单 13.5相同,只是写法不同。通过此更改,所有手动委托给 chef 的属性和函数都完全省略了,不出现在 Waiter 类体中,但它仍在委托它们。

这个类中唯一剩下的函数是 prepareBeverage()acceptPayment()——这两个函数是 Waiter 自己处理的。换句话说,当你查看这个类时,你真的可以专注于 Waiter 类的独特之处,而不必看到它委托给 chef 的所有内容。

只需包含 by chef,Kotlin 就会为我们做很多工作。当你看代码清单 13.6的第一行时,你可以这样理解:

Waiter 实现 KitchenStaff by 将其委托给 chef

或者,图示:

带注释的代码:`Waiter` 实现 `KitchenStaff` `by` 将其委托给 `chef` 对象。(指向每个内容) class Waiter( private val chef : Chef) : KitchenStaff by chef implements delegating to

多个委托对象

好消息——餐厅刚刚开设了新的饮料吧,提供桃子冰茶和柠檬茶等精致饮品。服务员不再自己准备饮料,而是将饮料准备工作委托给调酒师。

显示服务员将工作委托给调酒师。

首先,让我们为调酒师添加一个新的接口和类。我们还将用新的饮料选项更新 Beverage 枚举类。

interface BarStaff {
    fun prepareBeverage(name: String): Beverage?
}

class Bartender: BarStaff {
    override fun prepareBeverage(name: String): Beverage? = when (name) {
        "Water"        -> Beverage.WATER
        "Soda"         -> Beverage.SODA
        "Peach Tea"    -> Beverage.PEACH_ICED_TEA
        "Tea-Lemonade" -> Beverage.TEA_LEMONADE
        else           -> null
    }
}

enum class Beverage { WATER, SODA, PEACH_ICED_TEA, TEA_LEMONADE }

现在,我们希望 Waiter 将饮料准备委托给调酒师。首先,让我们给 Waiter 添加一个 bartender 属性,并手动委托给它,就像我们之前对 chef 做的那样。

class Waiter(
    private val chef: Chef, 
    private val bartender: Bartender
) : KitchenStaff by chef, BarStaff {
    override fun prepareBeverage(name: String) = bartender.prepareBeverage(name)
    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}

在这里,我们让 Kotlin 自动KitchenStaff 中的所有内容委托给 chef,而我们手动prepareBeverage() 委托给 Bartender。这很好,但 Kotlin 还允许我们使用 by 同时委托给多个对象,它的作用就像你期望的那样。只需在 BarStaff 后面添加 by bartender,如下所示:

class Waiter(
    private val chef: Chef, 
    private val bartender: Bartender
) : KitchenStaff by chef, BarStaff by bartender {
    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}

通过这种方式,我们现在有了一个委托给两个不同类的类。就像代码清单 13.8一样,Waiter 类的类体只显示 Waiter 类的独特之处。所有委托样板代码都隐藏在那些简单的 by 声明后面!

重写委托调用

随着时间越来越晚,餐厅越来越拥挤,厨师正忙着为所有顾客准备食物。所以,厨师对服务员说:”沙拉很容易准备。从现在开始,如果你收到沙拉订单,就自己处理。我仍然会处理更精致的餐点。”

”服务员准备沙拉。厨师准备其他餐点。”

正如我们所见,当我们使用 Kotlin 的类委托时,接口中的所有属性和函数都会自动发送到指定的对象。但是,你也可以选择自动委托接口中的一个或多个特定属性或函数。

让我们更新 Kotlin 代码,使 Waiter 可以自己准备沙拉。为此,我们可以在 Waiter 中再次包含 prepareEntree() 函数,并仅在需要时手动委托给 chef。如下所示:

class Waiter(
    private val chef: Chef, 
    private val bartender: Bartender
) : KitchenStaff by chef, BarStaff by bartender {
    override fun prepareEntree(name: String): Entree? = 
        if (name == "Tossed Salad") Entree.TOSSED_SALAD else chef.prepareEntree(name)

    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}

当来自 KitchenStaff 的函数被包含在 Waiter 类中时,Kotlin 将使用该函数而不是将其发送给委托对象。但是,我们仍然可以选择手动委托给它,正如我们在代码清单 13.10中所做的那样。当顾客点除沙拉以外的任何东西时,我们手动委托给 chef

所以,我们可以将 Kotlin 的类委托手动委托结合起来。

处理冲突

当用餐很棒时,有时顾客会告诉服务员向厨师转达赞美。KitchenStaff 有一个名为 receiveCompliment() 的函数。所以,当这个函数在 Waiter 对象上被调用时,它会被发送给它的 chef 对象。在 Chef 类中,我们可以简单地打印出收到了赞美,如下所示:

interface KitchenStaff {
    // ... disregarding other properties and functions ...
    fun receiveCompliment(message: String)
}

class Chef : KitchenStaff { 
    // ... disregarding other properties and functions ...
    override fun receiveCompliment(message: String) =
        println("Chef received a compliment: $message")
}

当然,在 waiter 上调用这个函数会将其发送给 chef

val waiter = Waiter(Chef(), Bartender())

val beverage = waiter.prepareBeverage("Water")
val entree = waiter.prepareEntree("Salmon on Rice")

waiter.receiveCompliment("The salmon entree was fantastic!")

不过,厨师并不是唯一可能收到赞美的人。调酒师泡的桃子冰茶非常棒,有时顾客也想向调酒师转达赞美!让我们更新 BarStaff 接口和 Bartender 类,以便他也能收到赞美。

interface BarStaff {
    fun prepareBeverage(name: String): Beverage?
    fun receiveCompliment(message: String)
}

class Bartender: BarStaff {
    // ... disregarding other properties and functions ...
    override fun receiveCompliment(message: String) =
        println("Bartender received a compliment: $message")
}

现在,both ChefBartender 都包含一个名为 receiveCompliment() 的函数——而且它们具有相同的参数类型和返回类型。

显示它们都有相同的内容。 interface KitchenStaff { val specials : List<String> fun prepareEntree (name: String): Entree? fun prepareAppetizer (name: String): Appetizer? fun prepareDessert (name: String): Dessert? fun receiveCompliment (message: String) } interface BarStaff { fun prepareBeverage (name: String): Beverage fun receiveCompliment (message: String) } same function name, parameter, and return type

所以,当我们的代码调用 waiter.receiveCompliment() 时,Kotlin 应该怎么做?它应该把赞美发送给 chef 对象,还是发送给 bartender 对象?或者两者都发?

这取决于你作为程序员的决定。事实上,在这种情况下,当你尝试对两个具有相同属性或函数的接口使用类委托时,你会收到一个类似这样的编译器错误:

类 ‘Waiter’ 必须重写在 Waiter 中定义的公共 open fun receiveCompliment(message: String): Unit,因为它继承了许多实现。

要解决这个问题,我们需要在 Waiter 类中包含这个函数。例如,我们可以检查 message 是否包含”entree”或”beverage”字样,并相应地手动委托给厨师或调酒师。然后,作为最后的手段,服务员可以直接接收赞美。

class Waiter(
    private val chef: Chef, 
    private val bartender: Bartender
) : KitchenStaff by chef, BarStaff by bartender {
    override fun receiveCompliment(message: String) = when {
        message.contains("entree")   -> chef.receiveCompliment(message)
        message.contains("beverage") -> bartender.receiveCompliment(message)
        else                         -> println("Waiter received compliment: $message")
    }

    override fun prepareEntree(name: String): Entree? = 
        if (name == "Tossed Salad") Entree.TOSSED_SALAD else chef.prepareEntree(name)

    fun acceptPayment(money: Int) = println("Thank you for paying for your meal")
}

现在,顾客可以将赞美发送给厨师、调酒师或服务员!

val waiter = Waiter(Chef(), Bartender())

waiter.receiveCompliment("The salmon entree was fantastic!")
waiter.receiveCompliment("The peach tea beverage was fantastic!")
waiter.receiveCompliment("The service was fantastic!")
Chef received a compliment: The salmon entree was fantastic!
Bartender received a compliment: The peach tea beverage was fantastic!
Waiter received compliment: The service was fantastic!

到目前为止,我们已经使用类委托将属性调用和函数调用从 waiter 对象轻松转发到 chefbartender 对象。虽然服务员、厨师和调酒师用具体例子说明了委托的概念,但委托也经常用于不同的目的——在多个特定类型之间共享通用代码。所以,在结束本章之前,让我们看看它是如何工作的!

用于通用和特定类型的委托

正如我们在上一章中看到的,接口通常代表通用类型,如 FarmAnimal,而实现接口的类通常代表更特定的类型,如 ChickenPigCow。在某些情况下,你可能有一些通用代码希望在许多不同的特定类型之间共享

例如,所有的农场动物都想吃东西,尽管它们吃的具体食物可能因动物种类而异。在下一个代码清单中,我们有三个农场动物,每个都有自己 的 eat() 函数。

class Cow {
    fun eat() = println("Eating grass - munch, munch, munch!")
}

class Chicken {
    fun eat() = println("Eating bugs - munch, munch, munch!")
}

class Pig {
    fun eat() = println("Eating corn - munch, munch, munch!")
}

Cow().eat()     // Eating grass - munch, munch, munch!
Chicken().eat() // Eating bugs - munch, munch, munch!
Pig().eat()     // Eating corn - munch, munch, munch!

你可能再次注意到这里有一些样板代码。事实上,除了它们的名称之外,这些类之间唯一的区别就是字符串中的食物eat() 函数是通用的,我们可以通过委托在这些特定的类之间共享它。这既快速又简单,所以我们从一个简单的"能吃东西"的接口开始吧。

interface Eater {
    fun eat()
}

让我们用一个发出咀嚼声的类来实现这个接口。

class Muncher(private val food: String) : Eater {
    override fun eat() = println("Eating $food - munch, munch, munch!")
}

通过使用类委托,我们可以让所有动物共享这个 eat() 函数实现。注意,现在重复的代码少了很多:

class Cow : Eater by Muncher("grass")
class Chicken : Eater by Muncher("bugs")
class Pig : Eater by Muncher("corn")

Cow().eat()     // Eating grass - munch, munch, munch!
Chicken().eat() // Eating bugs - munch, munch, munch!
Pig().eat()     // Eating corn - munch, munch, munch!

事实证明,这个农场里的猪比通常只是吃草的奶牛和鸡吃晚餐的速度快得多。所以,我们不用与它们共享 Muncher 代码,而可以直接在 Pig 中实现 eat() 函数,如下所示:

class Cow : Eater by Muncher("grass")
class Chicken : Eater by Muncher("bugs")
class Pig : Eater {
    override fun eat() = println("Scarfing down corn - NOM NOM NOM!!!")
} 

Cow().eat()     // Eating grass - munch, munch, munch!
Chicken().eat() // Eating bugs - munch, munch, munch!
Pig().eat()     // Scarfing down corn - NOM NOM NOM!!!

另外,我们可以通过创建另一个类并使用它作为 Pig 的委托对象来实现相同的效果。如下所示:

class Scarfer(private val food: String) : Eater {
    override fun eat() = println("Scarfing down $food - NOM NOM NOM!!!")
}

class Cow : Eater by Muncher("grass")
class Chicken : Eater by Muncher("bugs")
class Pig : Eater by Scarfer("corn")

所以,类委托可用于在不同特定类型(如 CowChickenPig 类)之间共享一些通用代码——比如如何 eat()

这只是在不同类之间共享代码的一种方式。在下一章中,我们将介绍抽象开放类,它们也可用于在多个类之间共享代码。

总结

在本章中,你学到了:

如上所述,下一章将介绍抽象开放类,它们也可用于在多个类之间共享代码。回头见!


  1. 样板代码一词来自印刷报纸行业的旧时代,当时出版集团会当地报纸发送已经准备好的金属印版上的故事。这些金属板看起来就像用于生产蒸汽锅炉的 plating。这些故事也常常是缺乏原创内容的 fluff 文章,这就是该术语获得其含义的方式。(”boilerplate,” Merriam-Webster.com Dictionary, https://www.merriam-webster.com/dictionary/boilerplate. 访问于 2023/1/23)。 ↩︎