Kotlin Vocabulary | Kotlin 委托代理

Kotlin Vocabulary | Kotlin 委托代理

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

当然,委托在软件行业不是什么新鲜名词。委托 (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。

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io Funder | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.
🍗