vlambda博客
学习文章列表

读书笔记《functional-kotlin》柯特林的代表

Chapter 6. Delegates in Kotlin

在最后两章中,我们学习了函数式编程中的函数和函数类型。我们还了解了 Kotlin 必须提供的各种类型的功能。

本章基于 Kotlin 中的委托。委托是 Kotlin 的绝佳功能,有利于函数式编程。如果您来自 Java 等非 FP 背景,您可能第一次听说委托。因此,在本章中,我们将尝试为您解开问题。

我们将从学习委托的基础知识开始,然后逐步进入 Kotlin 中委托的实现。

以下列表包含本章将涉及的主题:

  • Introduction to delegation
  • Delegates in Kotlin
  • Delegated properties
  • Standard delegates
  • Custom delegates
  • Delegated map
  • Local delegation
  • Class delegation

那么,让我们从代表开始吧。

Introduction to delegation


delegation在编程中的起源是对象组合。对象组合是一种组合简单对象以派生复杂对象的方法。对象组合是许多基本数据结构的关键组成部分,包括标记联合、链表和二叉树。

为了使对象组合更具可重用性(与继承一样可重用),引入了一种新模式——委托模式

这种模式允许一个对象有一个辅助对象,而那个辅助对象 object 被称为 委托。这种模式允许原始对象通过委托给委托助手对象来处理请求。

尽管委托是一种面向对象的设计模式,但并非所有语言都隐式支持委托(例如 Java,它不隐式支持委托)。在这些情况下,您仍然可以使用 delegation,方法是将原始对象作为参数/参数显式传递给方法的委托。

但是有了语言支持(例如在 Kotlin 中),委托变得更容易,并且通常看起来就像使用原始变量本身一样。

Understanding delegation

随着时间的推移,delegation 模式已被证明是一种更好的继承替代方案。继承是代码重用的强大工具,尤其是在 Liskov Substitution 模型的上下文中。此外,对 OOP 语言的直接支持使其更加强大。

但是,继承仍然有一些限制,比如一个类在程序执行过程中不能动​​态改变它的超类;另外,如果你对超类进行小修改,它会直接传播到子类,这不是我们每次都想要的。

另一方面,委派是灵活的。您可以将委托视为多个对象的组合,其中一个对象将其方法调用传递给另一个对象并将其称为委托。正如我之前提到的,授权是灵活的;您可以在运行时更改委托。

例如,考虑 Electronics 类和 Refrigerator 类。通过继承,Refrigerator 应该实现/覆盖 Electronics 方法调用和属性。然而,通过委托,Refrigerator 对象将保留对 Electronics 对象的引用,并通过它传递方法调用。

现在,既然我们知道 Kotlin 提供了对委托的支持,让我们开始在 Kotlin 中进行委托。

Delegates in Kotlin


Kotlin 对委派提供了开箱即用的支持。 Kotlin 为您提供了一些标准的属性委托,以满足最常见的编程需求。大多数时候,您会发现自己使用的是那些标准的委托,而不是创建自己的;但是,Kotlin 还允许您根据需要创建自己的委托。

不仅 delegation 用于属性,Kotlin 还允许您拥有委托类。

所以基本上,Kotlin 中有两种类型的委托,如下所示:

  • Property delegation
  • Class delegation

所以,让我们先看一下属性委托,然后我们将继续进行类委托。

Property delegation (standard delegates)


在上一节中,我们讨论了委托,我们了解到 delegation 是一种方法传递/转发的技术。

对于财产代表,它几乎做同样的事情。属性可以将其 getter 和 setter 调用传递给委托,委托可以代表属性本身处理这些调用。

您可能在想,将 getter 和 setter 调用传递给委托有什么好处?只有您使用的代表才能回答这个问题。 Kotlin 为最常见的用例提供了多个预定义的标准委托。让我们看一下以下列表,其中包含可用的标准委托:

  • The Delegates.notNull function and lateinit
  • The lazy function
  • The Delegates.Observable function
  • The Delegates.vetoble function

The Delegates.notNull function and lateinit

考虑一种情况其中您需要在class 级别,但那里没有变量的初始值。您将在稍后的某个时间获得该值,但在实际使用该属性之前,并且您确信该属性将在使用之前被初始化并且它不会为空。但是,根据 Kotlin 语法,您必须在初始化时初始化属性。快速解决方法是将其声明为 nullable var 属性,并分配一个默认的 null 值。但是正如我们前面提到的,由于您 确信 变量不会为 null 在使用它时,您不愿意将其声明为可为空的。

Delegates.notNull 在这种情况下可以帮助您。看看下面的程序:

var notNullStr:String by Delegates.notNull<String>() 
 
fun main(args: Array<String>) { 
    notNullStr = "Initial value" 
    println(notNullStr) 
} 

关注第一行——var notNullStr:String by Delegates.notNull () ,我们声明了一个非空String var 属性,但我们没有初始化它。相反,我们用 Delegates.notNull<String>() 编写了 ,但它是什么意思呢?让我们检查一下。 by 运算符是 Kotlin 中的保留关键字,用于委托。 by 运算符与两个操作数一起使用,在 by 的左侧 将是需要的属性/类被委派,右侧是代表。

委托—Delegates.notNull 允许您在不初始化属性的情况下暂时离开。它必须在使用之前进行初始化(就像我们在 main 方法的第一行所做的那样),否则它会抛出异常。

所以,让我们通过添加另一个属性来修改程序,在使用它之前我们不会初始化它,看看会发生什么:

var notNullStr:String by Delegates.notNull<String>() 
var notInit:String by Delegates.notNull<String>() 
 
fun main(args: Array<String>) { 
    notNullStr = "Initial value" 
    println(notNullStr) 
    println(notInit) 
} 

输出如下所示:

读书笔记《functional-kotlin》柯特林的代表

所以,notInit 属性导致了异常——属性 notInit 应该在 get 之前初始化

但是,Delegates.notNull() 的变量声明——听起来不是很尴尬吗? Kotlin 团队也有同样的想法。这就是为什么他们在 Kotlin 1.1 中添加了一个简单的关键字——lateinit,以实现相同的目标。由于它只是说明了后期初始化,它应该只是 lateinit

因此,让我们修改最后一个程序,将 by Delegates.notNull() 替换为 lateinit。以下是修改后的程序:

lateinit var notNullStr1:String 
lateinit var notInit1:String 
 
fun main(args: Array<String>) { 
    notNullStr1 = "Initial value" 
    println(notNullStr1) 
    println(notInit1) 
} 

在这个程序中,我们必须重命名变量,因为不能有两个同名的顶级(包级变量,没有任何类/函数)变量。除了变量名,唯一改变的是我们添加了 lateinit,而不是 Delegates.notNull() 中的

所以,现在让我们看一下以下输出,以确定是否有任何变化:

读书笔记《functional-kotlin》柯特林的代表

输出也是相同的,只是它稍微改变了错误消息。它现在说, lateinit 属性 notInit1 尚未初始化

The lazy function

lateinit 关键字仅适用于 var 属性。 Delegates.notNull() 函数也只能与 var 属性一起正常工作。

那么,在使用 val 属性时我们应该怎么做呢? Kotlin provides 你有另一个委托——lazy,这是为 val 属性。但它的工作方式略有不同。

lateinitDelegates.notNull() 不同,您必须指定想要 在声明时初始化变量。那么,有什么好处呢?在实际使用变量之前不会调用初始化。这就是为什么这个委托被称为 lazy;它启用属性的延迟初始化。

下面是一个代码示例:

val myLazyVal:String by lazy { 
    println("Just Initialised") 
    "My Lazy Val" 
} 
 
fun main(args: Array<String>) { 
    println("Not yet initialised") 
    println(myLazyVal) 
} 

所以在这个程序中,我们声明了一个 String val 属性——myLazyVal 和一个 lazy< /代码>委托。我们在 main 函数的第二行中使用(打印)了该属性。

现在,让我们关注变量声明。 lazy 委托接受预期返回属性值的 lambda。

那么,让我们看看输出:

读书笔记《functional-kotlin》柯特林的代表

Notice that the output clearly shows that the property got initialized after the first line of the main method executed, that is, when the property was actually used. This lazy initialization of properties can save your memory by a significant measure. It also comes as a handy tool in some situations, for example, think of a situation where you want to initialize the property with some other property/context, which would be available only after a certain point (but you have the property name); in that situation, you can simply keep the property as lazy, and then you can use it when it's confirmed that the initialization will be successful.

Observing property value change with Delegates.Observable

委托不仅用于初始化 最近/懒惰的属性。正如我们所了解的,委托可以将属性的 getter 和 setter 调用转发给委托。这使委托能够提供比最近/延迟初始化更酷的功能。

Delegates.observable 就是这样一个很酷的特性。考虑一种情况,您需要注意属性的值变化,并在发生这种情况时立即执行一些操作。我们想到的直接解决方案是覆盖 setter,但这看起来很讨厌并使代码变得复杂,而委托可以挽救我们的生命。

看看下面的例子:

var myStr:String by Delegates.observable("<Initial Value>") { 
    property, oldValue, newValue -> 
    println("Property `${property.name}` changed value from "$oldValue" to "$newValue"") 
} 
 
fun main(args: Array<String>) { 
    myStr = "Change Value" 
    myStr = "Change Value again" 
} 

这是一个简单的例子,我们在 Delegates 的帮助下声明了一个 String 属性—myStr .observable(我们将在查看输出后很快描述初始化),然后,在 main 函数中,我们更改了 myStr 两次。

看看下面的输出:

读书笔记《functional-kotlin》柯特林的代表

在输出中,我们可以看到,对于我们更改值的两次,都打印了一个日志,其中包含属性的旧值和新值。该程序中的 Delegates.observable 块负责输出中的日志。所以现在,让我们仔细看看 Delegates.observable 块并了解它是如何工作的:

var myStr:String by Delegates.observable("<Initial Value>") { 
    property, oldValue, newValue -> 
    println("Property `${property.name}` changed value from "$oldValue" to "$newValue"") 
} 

Delegates.observable 函数采用两个参数来创建委托。第一个参数是属性的初始值,第二个参数是当注意到值变化时应该执行的 lambda。

Delegates.observable 的 lambda 需要三个参数:

  • The first one is an instance of KProperty<out R>

Note

KProperty 是 Kotlin stdlib, kotlin.reflect 包中的一个接口,它是一个属性;例如命名的 valvar 声明。此类的实例可通过 :: 运算符获得。如需更多信息,请访问:  https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-property/.

  • The second parameter contains the old value of the property (the last value just before the assignment)
  • The third parameter is the newest value assigned to the property (the new value used in the assignment)

所以,既然我们已经有了 Delegates.observable 的概念,让我们继续使用一个新的委托, Delegates.vetoable

The power of veto – Delegates.vetoable

Delegates.vetoable 是另一个标准 delegate 允许我们 否决 值更改。

Note

否决权,拉丁文表示我禁止,是权力(例如, 被国家官员使用)单方面停止官方行动。这里有更多信息:https://en.wikipedia.org/wiki /否决.

这种否决权使我们能够对财产的每个分配进行逻辑检查,我们可以决定是否继续分配。

下面是一个例子:

var myIntEven:Int by Delegates.vetoable(0) { 
    property, oldValue, newValue -> 
    println("${property.name} $oldValue -> $newValue") 
    newValue%2==0 
} 
 
fun main(args: Array<String>) { 
    myIntEven = 6 
    myIntEven = 3 
    println("myIntEven:$myIntEven") 
} 

在这个程序中,我们创建了一个Int属性——myIntEven;这个属性应该只接受偶数作为赋值。 Delegates.vetoable 委托的工作方式与 Delegates.observable 函数几乎相同,只是在拉姆达。在这里,lambda 应该返回一个布尔值;如果返回的布尔值是 true,则分配将被传递,否则分配将被解除。

回顾一下节目。在使用委托 Delegates.vetoable 声明变量时,我们将 0 作为初始值,然后,在 lambda 中,我们记录了一个 assignment 调用,然后我们将返回 true 如果新值为偶数,如果为奇数,则为 false

这是输出:

读书笔记《functional-kotlin》柯特林的代表

因此,在输出中,我们可以看到两个赋值日志,但是当我们在最后一个赋值之后打印 myIntEven 属性时,我们可以看到最后一个赋值不成功。

很有趣,不是吗?让我们看另一个Delegates.vetoable的例子。看看下面的代码:

var myCounter:Int by Delegates.vetoable(0) { 
    property, oldValue, newValue -> 
    println("${property.name} $oldValue -> $newValue") 
    newValue>oldValue 
} 
 
fun main(args: Array<String>) { 
    myCounter = 2 
    println("myCounter:$myCounter") 
    myCounter = 5 
    myCounter = 4 
    println("myCounter:$myCounter")  
    myCounter++ 
    myCounter-- 
    println("myCounter:$myCounter") 
} 

这个程序有一个属性——myCounter,预计会随着每次赋值而增加。

在 lambda 中,我们检查了 newValue value 是否大于 oldValue value。这是输出:

读书笔记《functional-kotlin》柯特林的代表

输出 显示值增加的那些分配是成功的,但值减少的那些被驳回。

即使我们使用了递增和递减运算符,递增运算符也成功了,但递减运算符却没有。如果没有委托,这个特性就不会那么容易实现。

Delegated map


因此,我们学会了如何使用标准委托,但是 Kotlin 必须提供更多的委托。地图委托是委托附带的那些很棒的功能之一。那么,它是什么?将映射作为单个参数而不是函数/类构造函数中的参数数量传递是自由的。我们来看一下。下面是一个应用地图委托的程序:

data class Book (val delegate:Map<String,Any?>) { 
    val name:String by delegate 
    val authors:String by delegate 
    val pageCount:Int by delegate 
    val publicationDate:Date by delegate 
    val publisher:String by delegate 
} 
 
fun main(args: Array<String>) { 
    val map1 = mapOf( 
            Pair("name","Reactive Programming in Kotlin"), 
            Pair("authors","Rivu Chakraborty"), 
            Pair("pageCount",400), 
            Pair("publicationDate",SimpleDateFormat("yyyy/mm/dd").parse("2017/12/05")), 
            Pair("publisher","Packt") 
    ) 
    val map2 = mapOf( 
            "name" to "Kotlin Blueprints", 
            "authors" to "Ashish Belagali, Hardik Trivedi, Akshay Chordiya", 
            "pageCount" to 250, 
            "publicationDate" to SimpleDateFormat("yyyy/mm/dd").parse("2017/12/05"), 
            "publisher" to "Packt" 
    ) 
 
    val book1 = Book(map1) 
    val book2 = Book(map2) 
 
    println("Book1 $book1 nBook2 $book2") 
} 

程序很简单;我们定义了一个 Book 数据类,并且在构造函数中,我们没有一个一个地获取所有成员值,而是获取了一个映射,然后将所有成员委托给映射委托。

这里需要注意的一件事是提到映射中的所有成员变量,并且键名应该与属性名完全匹配。

这是输出:

读书笔记《functional-kotlin》柯特林的代表

很简单,不是吗?是的,代表团是如此强大。但是您是否对如果我们跳过提及地图中的任何属性会发生什么感到好奇?它会简单地避免您跳过的属性,如果您明确地尝试访问它们,那么它会抛出一个异常——java.util.NoSuchElementException

Custom delegation


到目前为止,在本章中,我们已经看到了 Kotlin 提供的标准 delegations。但是,Kotlin 确实允许我们编写自己的自定义委托,以满足我们的自定义需求。

例如,在程序中,我们使用 Delegates.vetoable 检查 Even,我们只能丢弃赋值,但是没有办法自动将下一个偶数分配给变量。

在下面的程序中,我们使用了 makeEven,一个 自定义委托 如果将奇数传递给赋值,它将自动分配下一个偶数,否则如果是偶数传递给任务,它会传递那个。

看看下面的程序:

var myEven:Int by makeEven(0) { 
    property, oldValue, newValue, wasEven -> 
    println("${property.name} $oldValue -> $newValue, Even:$wasEven") 
} 
 
fun main(args: Array<String>) { 
    myEven = 6 
    println("myEven:$myEven") 
    myEven = 3 
    println("myEven:$myEven") 
    myEven = 5 
    println("myEven:$myEven") 
    myEven = 8 
    println("myEven:$myEven") 
} 

这是输出:

读书笔记《functional-kotlin》柯特林的代表

输出清楚地表明,每当我们为 myEven 分配一个偶数时,它就会被分配,但是当我们分配一个奇数时,下一个偶数 (+1) 已分配。

对于这个委托,我们使用了与 Delegates.observable 几乎相同的 lambda,我们只是添加了一个参数——wasEven:Boolean,如果分配的数字是偶数,则包含 true,否则包含 false

想知道我们是如何创建委托的吗?这是代码:

abstract class MakeEven(initialValue: Int):ReadWriteProperty<Any?,Int> { 
    private var value:Int = initialValue 
 
    override fun getValue(thisRef: Any?, property: KProperty<*>) = value 
 
    override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: Int) { 
        val oldValue = newValue 
        val wasEven = newValue %2==0 
        if(wasEven) { 
            this.value = newValue 
        } else { 
            this.value = newValue +1 
        } 
        afterAssignmentCall(property,oldValue, newValue,wasEven) 
    } 
 
    abstract fun afterAssignmentCall (property: KProperty<*>, oldValue: Int, newValue: Int, wasEven:Boolean):Unit 
} 

要在 var 属性上创建委托,您需要实现 ReadWriteProperty 接口。

该接口有两个要重写的函数——getValuesetValue。这些函数实际上是属性的 getter 和 setter 的委托函数。您可以从 getValue 函数返回您想要的值,然后将其作为属性的返回值转发。每次访问该属性时,都会调用 getValue 函数。同样,每次为属性赋值时,都会调用 setValue 函数,以及我们从 setValue 函数返回的任何内容实际上将是该属性最终分配的值。例如,假设一个属性 a 被赋值 X,但来自 setValue 函数,我们返回了 Y,所以在赋值语句之后,属性 a 实际上会持有 Y 而不是 X

因此,如果您想从委托的 getValue 函数中返回属性的值,则必须将属性的值保存在某个地方(是的,您将无法提取该值来自原始属性,可能是因为原始属性甚至不会存储该值,因为该属性知道它将被委托)。在这个程序中,我们使用了一个可变的 var 属性——value,来存储属性的值。我们从 getValue 函数返回 value

setValue 函数中,我们检查了分配的 newValue 是否为偶数。如果是偶数,我们将 newValue 分配给 value 属性(将从 getValue 函数返回),如果 < code class="literal">newValue 很奇怪,我们将 newValue+1 分配给 value 属性。

MakeEven 类中,我们有一个 abstract 函数——afterAssignmentCall。我们在 setValue函数结束时调用了这个function。此功能用于记录目的。

所以,委托几乎准备好了,但是抽象函数呢?我们需要扩展这个类来应用委托,对吧?但是请记住我们使用它的代码,例如 by makeEven(0) {...},所以那里一定有一个函数,不是吗?是的,有一个函数,下面是定义:

 inline fun makeEven(initialValue: Int, crossinline onAssignment:(property: KProperty<*>, oldValue: Int, newValue: Int, wasEven:Boolean)->Unit):ReadWriteProperty<Any?, Int> 
        =object : MakeEven(initialValue){ 
    override fun afterAssignmentCall(property: KProperty<*>, oldValue: Int, newValue: Int, wasEven: Boolean) 
            = onAssignment(property,oldValue,newValue,wasEven) 
} 

我们创建了一个 MakeEven 的匿名对象并将其作为委托传递,我们将参数 lambda——onAssignment 作为抽象传递函数——afterAssignmentCall

所以,我们必须处理好代表,让我们继续尝试一些更有趣的代表方面。

Local delegates


委托很强大,我们已经看到了,但是想想 common 的情况,我们在方法内部声明并初始化一个属性,然后我们应用一个逻辑,该逻辑将使用该属性或在没有它的情况下继续。例如,下面是这样一个程序:

fun useDelegate(shouldPrint:Boolean) { 
    val localDelegate = "Delegate Used" 
    if(shouldPrint) { 
        println(localDelegate) 
    } 
     
    println("bye bye") 
} 

在这个程序中,我们将使用 localDelegate 属性,只有当 shouldPrint 值为 true,否则我们不会使用它。但它总是会占用内存空间,因为它已被声明和初始化。避免这种内存阻塞的一个选项是在 if 块中包含属性,但它是一个简单的虚拟程序,在这里我们可以轻松地将变量声明移动到 if 块中code class="literal">if 块,而在许多现实生活场景中,将变量声明移动到 if 块内是不可能的。

那么,解决方案是什么?是的,使用 lazy 委托可以挽救我们的生命。但在 Kotlin 1.1 到来之前,这在 Kotlin 中是不可能的。

因此,以下是更新后的程序:

fun useDelegate(shouldPrint:Boolean) { 
    val localDelegate by lazy { 
        "Delegate Used" 
    } 
    if(shouldPrint) { 
        println(localDelegate) 
    } 
     
    println("bye bye") 
} 

虽然我们在这个例子中只使用了 lazy,但从 Kotlin 1.1 开始,我们可以在本地属性中应用任何委托。

Class delegation


类委托是 Kotlin 的另一个有趣 特性。如何?想想以下的情况。

你有一个接口, I,和两个类, A BAB 都实现了 。在您的代码中,您有一个 A 的实例,并且您想创建一个 B 的实例 来自那个 A

在传统的继承中,这是不可能直接实现的;你必须编写一堆讨厌的代码来实现这一点,但是类委托可以拯救你。

通过以下代码:

interface Person { 
    fun printName() 
} 
 
class PersonImpl(val name:String):Person { 
    override fun printName() { 
        println(name) 
    } 
} 
 
class User(val person:Person):Person by person { 
    override fun printName() { 
        println("Printing Name:") 
        person.printName() 
    } 
} 
 
fun main(args: Array<String>) { 
    val person = PersonImpl("Mario Arias") 
    person.printName() 
    println() 
    val user = User(person) 
    user.printName() 
} 

在这个程序中,我们创建了User的实例,它的成员属性——person,是person的一个实例< code class="literal">Person 接口。在 main 函数中,我们将 PersonImpl 的实例传递给用户以创建 User 的实例。

现在,看看 User 的声明。在 color (:) 之后,短语 Person by person 表示类 User 扩展 Person 并期望从提供的 person 复制 Person 行为> 实例。

这是输出:

读书笔记《functional-kotlin》柯特林的代表

输出显示了预期的覆盖工作,我们还可以访问 person 的属性和函数,就像普通属性一样。

一个非常棒的功能,不是吗?

Summary


在本章中,我们了解了委托,并了解了如何以各种方式使用委托来使我们的代码高效和干净。我们了解了代表的不同功能和部分,以及如何使用它们。

下一章将介绍协同程序,这是 Kotlin 的一项开创性功能,可实现无缝异步处理,同时让开发人员的生活变得简单明了。

所以,不要等待太久,现在开始下一章。