Kotlin Vocabulary | Kotlin 委托代理

image

有时候,完成一些工作的方法是将它们委托给别人。这里不是在建议您将自己的工作委托给朋友去做,而是在说将一个对象的工作委托给另一个对象。

当然,委托在软件行业不是什么新鲜名词。委托 (Delegation) 是一种设计模式,在该模式中,对象会委托一个助手 (helper) 对象来处理请求,这个助手对象被称为代理。代理负责代表原始对象处理请求,并使结果可用于原始对象。

Kotlin 不仅支持类和属性的代理,其自身还包含了一些内建代理,从而使得实现委托变得更加容易。

类代理

这里举个例子,您需要实现一个同 ArrayList 基本相同的用例,唯一的不同是此用例可以恢复最后一次移除的项目。基本上,实现此用例您所需要的就是一个同样功能的 ArrayList,以及对最后移除项目的引用。

实现这个用例的一种方式,是继承 ArrayList 类。由于新的类继承了具体的 ArrayList 类而不是实现 MutableList 接口,因此它与 ArrayList 的实现高度耦合。

如果只需要覆盖 remove() 函数来保持对已删除项目的引用,并将 MutableList 的其余空实现委托给其他对象,那该有多好啊。为了实现这一目标,Kotlin 提供了一种将大部分工作委托给一个内部 ArrayList 实例并且可以自定义其行为的方式,并为此引入了一个新的关键字: by。

让我们看看类代理的工作原理。当您使用 by 关键字时,Kotlin 会自动生成使用 innerList 实例作为代理的代码:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class ListWithTrash <T>(private val innerList: MutableList<T> = ArrayList<T>()) : MutableCollection<T> by innerList {
    var deletedItem : T? = null
    override fun remove(element: T): Boolean {
           deletedItem = element
            return innerList.remove(element)
    }
    fun recover(): T? {
        return deletedItem
    }
}

by 关键字告诉 Kotlin 将 MutableList 接口的功能委托给一个名为 innerList 的内部 ArrayList。通过桥接到内部 ArrayList 对象方法的方式,ListWithTrash 仍然支持 MutableList 接口中的所有函数。与此同时,现在您可以添加自己的行为了。

工作原理

让我们看看这一切是如何工作的。如果您去查看 ListWithTrash 字节码所反编译出的 Java 代码,您会发现 Kotlin 编译器其实创建了一些包装函数,并用它们调用内部 ArrayList 对象的相应函数:

public final class ListWithTrash implements Collection, KMutableCollection {
  @Nullable
  private Object deletedItem;
  private final List innerList;

  @Nullable
  public final Object getDeletedItem() {
     return this.deletedItem;
  }

  public final void setDeletedItem(@Nullable Object var1) {
     this.deletedItem = var1;
  }

  public boolean remove(Object element) {
     this.deletedItem = element;
     return this.innerList.remove(element);
  }

  @Nullable
  public final Object recover() {
     return this.deletedItem;
  }

  public ListWithTrash() {
     this((List)null, 1, (DefaultConstructorMarker)null);
  }

  public int getSize() {
     return this.innerList.size();
  }
   // $FF: 桥接方法
  public final int size() {
     return this.getSize();
  }
  //…...
}

注意: 为了在生成的代码中支持类代理,Kotlin 编译器使用了另一种设计模式——装饰者模式。在装饰者模式中,装饰者类与被装饰类使用同一接口。装饰者会持有一个目标类的内部引用,并且包装 (或者装饰) 接口提供的所有公共方法。

在您无法继承特定类型时,委托模式就显得十分有用。通过使用类代理,您的类可以不继承于任何类。相反,它会与其内部的源类型对象共享相同的接口,并对该对象进行装饰。这意味着您可以轻松切换实现而不会破坏公共 API。

属性代理

除了类代理,您还可以使用 by 关键字进行属性代理。通过使用属性代理,代理会负责处理对应属性 getset 函数的调用。这一特性在您需要在其他对象间复用 getter/setter 逻辑时十分有用,同时也能让您可以轻松地对简单支持字段的功能进行扩展。

让我们假设您有一个 Person 类型,定义如下:

class Person(var name: String, var lastname: String)

该类型的 name 属性有一些格式化需求。当 name 被赋值时,您想要确保将第一个字母大写的同时将其余字母格式化为小写。另外,在更新 name 的值时,您想要自动增加 updateCount 属性。

您可以像下面这样实现这一功能:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Person(name: String, var lastname: String) {
   var name: String = name
       set(value) {
           name = value.toLowerCase().capitalize()
           updateCount++
       }
   var updateCount = 0
}

上述代码当然是可以解决问题的,但若需求发生改变,比如您想要在 lastname 的值发生改变时也增加 updateCount 的话会怎样?您可以复制粘贴这段逻辑并实现一个自定义 setter,但这样一来,您会发现自己为所有属性编写了完全相同的 setter。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Person(name: String, lastname: String) {
   var name: String = name
       set(value) {
           name = value.toLowerCase().capitalize()
           updateCount++
       }
   var lastname: String = lastname
       set(value) {
           lastname = value.toLowerCase().capitalize()
           updateCount++
       }
   var updateCount = 0
}

两个 setter 方法几乎完全相同,这意味着这里的代码可以进行优化。通过使用属性代理,我们可以将 getter 和 setter 委托给属性,从而可以复用代码。

与类代理相同,您可以使用 by 来代理一个属性,Kotlin 会在您使用属性语法时生成代码来使用代理。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Person(name: String, lastname: String) {
   var name: String by FormatDelegate()
   var lastname: String by FormatDelegate()
   var updateCount = 0
}

像这样修改以后,namelastname 属性就被委托给了 FormatDelegate 类。现在让我们来看看 FormatDelegate 的代码。如果您只需要委托 getter,那么代理类需要实现 ReadProperty<Any?, String>;而如果 getter 与 setter 都要委托,则代理类需要实现 ReadWriteProperty<Any?, String>。在我们的例子中,FormatDelegate 需要实现 ReadWriteProperty<Any?, String>,因为您想在调用 setter 时执行格式化操作。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class FormatDelegate : ReadWriteProperty<Any?, String> {
   private var formattedString: String = ""

   override fun getValue(
       thisRef: Any?,
       property: KProperty<*>
   ): String {
       return formattedString
   }

   override fun setValue(
       thisRef: Any?,
       property: KProperty<*>,
       value: String
   ) {
       formattedString = value.toLowerCase().capitalize()
   }
}

您可能已经注意到,getter 和 setter 函数中有两个额外参数。第一个参数是 thisRef,代表了包含该属性的对象。thisRef 可用于访问对象本身,以用于检查其他属性或调用其他类函数一类的目的。第二个参数是 KProperty<*>,可用于访问被代理的属性上的元数据。

回头看一看需求,让我们使用 thisRef 来访问和增加 updateCount 属性:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

override fun setValue(
   thisRef: Any?,
   property: KProperty<*>,
   value: String
) {
   if (thisRef is Person) {
       thisRef.updateCount++
   }
   formattedString = value.toLowerCase().capitalize()
}

工作原理

为了理解其工作原理,让我们来看看反编译出的 Java 代码。Kotlin 编译器会为 namelastname 属性生成持有 FormatDelegate 对象私有引用的代码,以及包含您所添加逻辑的 getter 和 setter。

编译器还会创建一个 KProperty[] 用于存放被代理的属性。如果您查看了为 name 属性所生成的 getter 和 setter,就会发现它的实例存储在了索引为 0 的位置, 同时 lastname 被存储在索引为 1 的位置。

public final class Person {
  // $FF: 合成字段
  static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "lastname", "getlastname()Ljava/lang/String;"))};
  @NotNull
  private final FormatDelegate name$delegate;
  @NotNull
  private final FormatDelegate lastname$delegate;
  private int updateCount;

  @NotNull
  public final String getName() {
     return this.name$delegate.getValue(this, $$delegatedProperties[0]);
  }

  public final void setName(@NotNull String var1) {
     Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
     this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
  }
  //...
}

通过这一技巧,任何调用者都可以通过常规的属性语法访问代理属性。

person.lastname = “Smith” 
// 调用生成的 setter,增加数量
 
println(“Update count is $person.count”)

Kotlin 不仅支持委托模式的实现,同时还在标准库中提供了内建的代理,我们将在另一篇文章中进行详细地介绍。

代理可以帮您将任务委托给其他对象,并提供更好的代码复用性。Kotlin 编译器会创建代码以使您可以无缝使用代理。Kotlin 使用简单的 by 关键字语法来代理属性或类。内部实现上,Kotlin 编译器会生成支持代理所需的所有代码,而不会暴露任何公共 API 的修改。简而言之,Kotlin 会生成和维护所有代理所需的样板代码,换句话说,您可以将您的工作放心地委托给 Kotlin。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容