kotlin好好委托,好好by,好好Delegation

162 阅读9分钟

一、首先,委托和代理算不上一个东西

代理模式和委托模式类似,但是两者不能认为是同一种模式。

  • 委托模式 Delegation pattern
  • 代理模式 Proxy Pattern

Kotlin 使用 by 关键字来实现委托(delegat) 模式

虽然委托和代理算不上一个东西,但是实际上很多时候,大多数人都把他说成本一个东西。


二、关于by

阅读编写kotlin代码的时候,by几乎无处不在。

by的两种用途

在kotlin中,by关键字主要有3种用途,

  • 类委托:  一个类的方法不在该类中定义,而是直接委托给另一个对象来处理。
  • 属性委托:  一个类的属性不在该类中定义,而是直接委托给另一个对象来处理。
  • 局部变量委托:  一个局部变量不在该方法中定义,而是直接委托给另一个对象来处理。

by关键字后面的表达式可能有各种各样的写法,但一定是返回一个委托的实例 .
.

by这个语法糖帮我们干了什么事

Kotlin委托其实就是使用by语法后,编译器会帮我们生成了委托类的代码。

如果是接口的委托,by语法糖这样做

class RealSubject : Subject by Delegate()

编译器就会帮我们生成委托模式的代码:

class RealSubject : Subject {

  private val delegate: Subject = Delegate()

  override fun buy() {
    delegate.buy()
  }
} 

.
.

如果是属性的委托,by语法糖这样做

var name: String by PropertyDelegate()

编译器就会帮我们把属性的get、set方法委托出去:

val delegate = PropertyDelegate()
var name: String
  get() = delegate.getValue()
  set(value) = delegate.setValue(value)

by关键字后面的表达式可能有各种各样的写法,但一定是返回一个委托的实例。

.
.

二、 属性委托

委托属性的语法

如下:

val/var <属性名>: <类型> by <表达式>

委托属性这个点比较简单,所以我们放在最开始的时候讲。

  • by lazy本身是一种属性委托。属性委托的关键字是by
  • lazy只能修饰val,即不可变变量(仅仅说的是lazy)
  • kotlin中,当且仅当变量被第一次调用的时候,委托方法才会执行。所以这也经常被用于kotlin的单例

属性代理是借助于代理设计模式,把这个模式应用于一个属性时,它可以将访问器的逻辑代理给一个辅助对象。

可以简单理解为属性的 setter、getter 访问器内部实现是交给一个代理对象来实现,相当于使用一个代理对象来替换了原来简单属性字段读写过程,而暴露外部属性操作还是不变的,照样是属性赋值和读取,只是 setter、getter 内部具体实现变了。

1.2 基本语法格式

class Student{
    var name: String by Delegate()
}

class Delegate{
    operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T{
        ...
    }
    operator fun <T> setValue(thisRef: Any?, property: KProperty<*>, value: T){
        ...
    }
}

代码块
123456789101112

.
.

属性代理的理解/自定义属性代理

如果单纯用by,不是by lazy,相当于把属性的 setter、getter 访问器内部实现是交给一个代理对象来实现。

属性代理是借助于代理设计模式,把这个模式应用于一个属性时,它可以将访问器的逻辑代理给一个辅助对象。

可以简单理解为属性的 setter、getter 访问器内部实现是交给一个代理对象来实现,相当于使用一个代理对象来替换了原来简单属性字段读写过程,而暴露外部属性操作还是不变的,照样是属性赋值和读取,只是 setter、getter 内部具体实现变了。

class Student{
   var name: String by Delegate()
}

class Delegate{
   operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T{
       ...
   }
   operator fun <T> setValue(thisRef: Any?, property: KProperty<*>, value: T){
       ...
   }
}

属性 name 将它访问器的逻辑委托给了 Delegate 对象,通过 by 关键字对表达式 Delegate() 求值获取这个对象。任何符合属性代理规则都可以使用 by 关键字。属性代理类必须要遵循 getValue(),setValue()方法约定,getValue、setValue方法可以是普通方法也可以是扩展方法,并且是方法是支持运算符重载。如果是 val 修饰的属性只需要具备 getValue() 方法即可。

属性代理基本流程就是代理类中的 getValue() 方法包含属性getter访问器的逻辑实现,setValue()方法包含了属性setter访问器的逻辑实现。当属性 name 执行赋值操作时,会触发属性 setter 访问器,然后在setter 访问器内部调用 delegate 对象的 setValue() 方法;执行读取属性 name 操作时,会在 getter 访问器中调用 delegate 对象的 getValue 方法. .
.

by lazy一个例子

class Cat() {
    // by lazy 当且仅当变量被第一次调用的时候,委托方法才会执行。
    val name by lazy {
        println("只调用一次")
        "大脸猫"
    }
}

fun main() {
    val lazyCat = Cat()
    // lazyCat.name的时候,委托方法执行
    val name = lazyCat.name
    // lazyCat1.name执行的时候,委托方法上面执行过了,不再执行了
    val name1 = lazyCat.name
    
    println("====")
    println("lazy-name:$name")
    println("lazy-name:$name1")
}

输出:
只调用一次
====
lazy-name:大脸猫
lazy-name:大脸猫

Kotlin 标准库中提供了几种委托

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
1.1 延迟属性 lazy

lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

val lazyProp: String by lazy {
    println("Hello,第一次调用才会执行我!")
    "西哥!"
}

// 打印lazyProp 3次,查看结果
fun main() {
    println(lazyProp)
    println(lazyProp)
    println(lazyProp)
}

打印结果如下:

Hello,第一次调用才会执行我!
西哥!
西哥!
西哥!

可以看到,只有第一次调用,才会执行lambda表达式中的逻辑,后面调用只会返回lambda表达式的最终值。

1.1.1 lazy 也可以接受参数

lazy延迟初始化是可以接受参数的,提供了如下三个参数:

/**
 * Specifies how a [Lazy] instance synchronizes initialization among multiple threads.
 */
public enum class LazyThreadSafetyMode {

    /**
     * Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
     */
    SYNCHRONIZED,

    /**
     * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
     * but only the first returned value will be used as the value of [Lazy] instance.
     */
    PUBLICATION,

    /**
     * No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
     *
     * This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
     */
    NONE,
}

三个参数解释如下:

  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步锁,使lazy延迟初始化线程安全
  • LazyThreadSafetyMode. PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程

使用如下:

val lazyProp: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    println("Hello,第一次调用才会执行我!")
    "西哥!"
}

如果你指定的参数为LazyThreadSafetyMode.SYNCHRONIZED,则可以省略,因为lazy默认就是使用的LazyThreadSafetyMode.SYNCHRONIZED

1.2 可观察属性 Observable

如果你要观察一个属性的变化过程,那么可以将属性委托给Delegates.observableobservable函数原型如下:

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
            ReadWriteProperty<Any?, T> =
        object : ObservableProperty<T>(initialValue) {
            override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
        }

接受2个参数:

  • initialValue:初始值
  • onChange:属性值被修改时的回调处理器,回调有三个参数property,oldValue,newValue,分别为:被赋值的属性、旧值与新值。

使用如下:

var observableProp: String by Delegates.observable("默认值:xxx"){
    property, oldValue, newValue ->
    println("property: $property: $oldValue -> $newValue ")
}
// 测试
fun main() {
    observableProp = "第一次修改值"
    observableProp = "第二次修改值"
}

打印如下:

property: var observableProp: kotlin.String: 默认值:xxx -> 第一次修改值
property: var observableProp: kotlin.String: 第一次修改值 -> 第二次修改值

可以看到,每一次赋值,都能观察到值的变化过程。

1.2.1 vetoable 函数

vetoable 与 observable一样,可以观察属性值的变化,不同的是,vetoable可以通过处理器函数来决定属性值是否生效

来看这样一个例子:声明一个Int类型的属性vetoableProp,如果新的值比旧值大,则生效,否则不生效。

代码如下:

var vetoableProp: Int by Delegates.vetoable(0){
    _, oldValue, newValue ->
    // 如果新的值大于旧值,则生效
    newValue > oldValue
}

测试代码:

fun main() {
    println("vetoableProp=$vetoableProp")
    vetoableProp = 10
    println("vetoableProp=$vetoableProp")
    vetoableProp = 5
    println("vetoableProp=$vetoableProp")
    vetoableProp = 100
    println("vetoableProp=$vetoableProp")
}

打印如下:

vetoableProp=0
 0 -> 10
vetoableProp=10
 10 -> 5
vetoableProp=10
 10 -> 100
vetoableProp=100

可以看到10 -> 5 的赋值没有生效。

3.3 属性存储在映射中

还有一种情况,在一个映射(map)里存储属性的值,使用映射实例自身作为委托来实现委托属性,如:

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

测试如下:

fun main() {
    val user = User(mapOf(
        "name" to "西哥",
        "age"  to 25
    ))
   println("name=${user.name} age=${user.age}")
}

打印如下:

name=西哥 age=25

使用映射实例自身作为委托来实现委托属性,可以使用在json解析中,因为json本身就可以解析成一个map。

三、 kotlin委托模式/类委托 (重点)

委托,也就是委托模式,它是23种经典设计模式种的一种,又名代理模式,在委托模式中,有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。

委托模式是一项技巧,其他的几种设计模式如:策略模式、状态模式和访问者模式都是委托模式的具体场景应用。

  • 委托模式中,有三个角色,约束委托对象被委托对象
    • 举个例子,游戏代练公司。
    • 游戏代练公司就是委托对象,代练就是被委托对象
    • 委托对象(游戏公司) 让 被委托对象干活,具体干什么要看任务要求,也就是看约束
    • 委托对象就是安排工作的人,具体怎么安排,要看约束的规定
    • 但是,代练除了完成任务之外,平时自己还可以干点别的,人任务的前前后后随便玩
  • 委托模式的本质就是,就是一个对象将消息委托给另一个对象来处理。

其实可以简单地理解为,所谓的类委托代理模式代理接口委托模式,就是我们后面要说的东西。

一句话说委托

委托模式的本质就是,在实现类中,用委托对象的方法 代替实现类中的方法,并适当增加一些自己的逻辑

Kotlin 类委托的语法格式如下:

class <类名>(b : <基础接口>) : <基础接口> by <基础对象>

比如:class DelegateGamePlayer(private val iGamePlayer: IGamePlayer): IGamePlayer by iGamePlayer

.
.

来个例子吧

// 约束类 游戏的公司的业务,约束死的,这是规定
interface IGamePlayer {
    // 打排位赛
    fun rank()
    // 升级
    fun upgrade()
}

// 委托对象,这里 代练公司
// 这短短的一行代码,就是kotlin的委托模式精髓
class DelegateGamePlayer(private val iGamePlayer: IGamePlayer): IGamePlayer by iGamePlayer

// 被委托对象,本场景中的游戏代练
class RealGamePlayer(private val name: String): IGamePlayer{
    override fun rank() {
        println("打排位之前先买了瓶可乐")
        println("$name 开始排位赛")
        println("打排位之后可乐喝完了")
    }

    override fun upgrade() {
        println("还没升级,没钱买泡面")
        println("$name 升级了")
        println("升级之后请室友吃海底捞")
    }
}

fun main(){
    /*val realGamePlayer = RealGamePlayer("张三")
    val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)*/

    val delegateGamePlayer = DelegateGamePlayer(RealGamePlayer("张三"))
    delegateGamePlayer.rank()
    delegateGamePlayer.upgrade()
}

输出:
打排位之前先买了瓶可乐
张三 开始排位赛
打排位之后可乐喝完了
还没升级,没钱买泡面
张三 升级了
升级之后请室友吃海底捞

现在明白了吧

也许你会说,这玩意,不是跟继承差不多吗,怎么说呢,是有像,但是又不一样。毕竟这是一个设计模式,第二,你不是用by,也能实现委托模式,但是呢,by是kotlin的一个语法糖,你用kotlin,又不用语法糖,何必呢。

其实这个文章,看到这里就可以了。

.
.

不用by的委托

当然也不是说不用by就实现不了委托,这不可能。

  • java的委托
  • 不用by的kotlin的委托

先开看看,用Java实现一个委托,

package delegate;

interface DelegateApiJava {
    void doSomething();
}

class ImplJava implements DelegateApiJava {

    private DelegateApiJava delegateApiJava;

    public ImplJava(DelegateApiJava delegateApiJava) {
        this.delegateApiJava = delegateApiJava;
    }

    @Override  
    public void doSomething() {
        if (this.delegateApiJava != null) {
            System.out.println("before");
            delegateApiJava.doSomething();
            System.out.println("after");
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        ImplJava implJava = new ImplJava(new DelegateApiJava() {
            @Override
            public void doSomething() {
                System.out.println("doSomething");
            }
        });

        implJava.doSomething();
    }
}

// 输出(before和after就是自己的逻辑,doSomething就是代理对象的实现)
before
doSomething
after

.
.
上面的例子,如果换成Kotlin,在没有使用by的情况下,是这样的

internal interface DelegateApiJava {
    fun doSomething()
}

internal class ImplJava(private val delegateApiJava: DelegateApiJava?) : DelegateApiJava {
    override fun doSomething() {
        if (delegateApiJava != null) {
            println("before")
            delegateApiJava.doSomething()
            println("after")
        }
    }
}
object Demo {
    @JvmStatic
    fun main(args: Array<String>) {
        val implJava = ImplJava(object : DelegateApiJava {
            override fun doSomething() {
                println("doSomething")
            }
        })
        implJava.doSomething()
    }
}

嗯。还是很麻烦。 .

四、使用场景:

A.延迟加载属性(lazy property): 属性值只在初次访问时才会计算,

B.可观察属性(observable property): 属性发生变化时, 可以向监听器发送通知,

C.将多个属性保存在一个 map 内, 而不是保存在多个独立的域内.


参考:

这两个文章值得一看

mp.weixin.qq.com/s?src=11&ti…

mp.weixin.qq.com/s?src=11&ti…