kotlin委托必须重视的几个点(代码片段)

丶笑看退场 丶笑看退场     2022-12-05     573

关键词:

委托模式是实现继承的一个很好的替代方式,也是Kotlin语言的一种特性,可以很优雅的实现委托模式。在开发过程中也少不了使用它,但常常都会被低估。所以今天就让它得到重视,去充分的掌握kotlin委托特性以及原理。

一、委托类

我们先完成一个委托类,常常用于实现类的委托模式,它的关键是通过by关键字:

interface Base
  fun print()


class BaseImpl(val x: Int): Base
  override fun print()  print(x) 


class Derived(b: Base): Base by b

fun main()
  val b = BaseImpl(10)
  Deriived(b).print()


//最后输出了10

在这个委托模式中Derived相当于是个包装,虽然也实现了base,但并不关心它怎么实现,通过by这个关键字,将接口的实现委托给了它的参数db。

相当于Java代码的结构:

class Derived implements Base
  Base b;
  public Derived(Base b) this.b = b

二、 委托属性

前面讲到Kotlin委托类是委托的是接口方法,委托属性委托的,则是属性的gettersetter方法。kotlin支持的委托属性语法:

class Example 
    var prop: String by Delegate()

属性对应的get()set()会被委托给它的getValuesetValue方法。当然属性的委托不是随便写的,对于与val属性它必须要提供一个getValue函数,如果是var属性的则要另外提供setValue属性。先来看个官方提供的委托属性Delegate

class Delegate 
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String 
        return "$thisRef, thank you for delegating '$property.name' to me!"
    
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) 
        println("$value has been assigned to '$property.name' in $thisRef.")
    

我们可以看到对于var修饰的属性,必须要有getValuesetValue方法,同时这两个方法必须有operator关键字的修饰。

再来看第一个参数thisRef,它的类型是要这个属性所有者的类型,或者是它的父类。当我们不确定属性会属于哪个类,就可以将thisRef的类型定义为Any?了。

接着看另一个参数property,它的类型是必须要KProperty<*>或其超类型,它的值则是前面的字段的名字prop

最后一个参数,它的类型必须是委托属性的类型,或者是它的父类。也就是说例子中的 value: String 也可以换成 value: Any

我们来测试下到底是不是这样的:

fun main() 
    println(Test().prop)
    Test().prop = "Hello, World"

则会看到输出:

Example@5197848c, thank you for delegating 'prop' to me!
Hello, World has been assigned to 'prop' in Example@17f052a3.

2.1 自定义委托

在知道了委托属性怎么写之后,也可以根据需求来实现自己的属性委托。但是每次写都要写那么多模板代码,也是很麻烦的,所以官方也提供了接口类给我们快速实现:

interface ReadOnlyProperty<in R, out T> 
    operator fun getValue(thisRef: R, property: KProperty<*>): T


interface ReadWriteProperty<in R, T> 
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)

现在被委托的类只要实现这个接口的其中一个就可以了。对于 val 变量使用 ReadOnlyProperty,而 var 变量实现ReadWriteProperty。我们现在就用ReadWriteProperty接口来实现一个自定义委托:

class Owner 
  var text: String by StringDelegate()



class StringDelegate(private var s: String = "Hello"): ReadWriteProperty<Owner, String> 
    override operator fun getValue(thisRef: Owner, property: KProperty<*>): String 
        return s
    
    override operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) 
        s = value
    

三、委托进阶

3.1 懒加载委托

懒加载委托,也就是我们再对一些资源进行操作的时候,希望它在被访问的时候采取触发,避免不必要的消耗。官方已经帮我们提供了一个lazy()方法来快速创建懒加载委托:

val lazyData: String by lazy 
    request()


fun request(): String 
    println("执行网络请求")
    return "网络数据"


fun main() 
    println("开始")
    println(lazyData)
    println(lazyData)


//结果:
开始
执行网络请求
网络数据
网络数据

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

那么懒加载委托又是怎么实现的呢? 现在来看下它的源代码:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) 
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    

在这个里面lazy()方法会接收一个LazyThreadSafetyMod类型的参数,如果不传这个参数的话,就会默认使用SynchronizedLazyImpl方式。看解释就可以知道它是用来多线程同步的,而另外两个则不是多线程安全的。

  • LazyThreadSafetyMode.PUBLICATION:初始化方法可以被多次调用,但是值只是第一次返回时的返回值,也就是只有第一次的返回值可以赋值给初始化的值。

  • LazyThreadSafetyMode. NONE:如果初始化将总是发生在与属性使用位于相同的线程,这种情况下可以使用,但它没有同步锁。

我们现在主要来看下SynchronizedLazyImpl里面做了什么事情:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable 
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() 
            val _v1 = _value
            //判断是否已经初始化过,如果初始化过直接返回,不在调用高级函数内部逻辑
            //如果这两个值不相同,就说明当前的值已经被加载过了,直接返回
            if (_v1 !== UNINITIALIZED_VALUE) 
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            

            return synchronized(lock) 
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) 
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                 else 
                  //调用高级函数获取其返回值
                    val typedValue = initializer!!()
                  //将返回值赋值给_value,用于下次判断时,直接返回高级函数的返回值
                    _value = typedValue
                    initializer = null
                    typedValue
                
            
        
			......

通过上面代码,可以发现SynchronizedLazyImpl覆盖了lazy接口的返回值,并且重写了属性的访问器,具体逻辑是与Java的双重校验类似的。Lazy接口又是怎么变成委托属性的?

Lazy.kt文件中发现它声明了Lazy接口的getValue扩展属性,也就在最终赋值的时候会被调用,而我们在自定义委托中说过,对于val属性,我们需要提供一个getValue函数。

## Lazy.kt
//此扩展允许使用 Lazy 的实例进行属性委托
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

有了这个懒加载委托后,我们实现单例会变的更加简单:

class SingletonDemo private constructor() 
    companion object 
        val instance: SingletonDemo by lazy
        SingletonDemo() 
    

3.2 Delegates.observable 观察者委托

如果你要观察一个属性的变化过程,可以将属性委托给Delegates.observable,它有三个参数:被赋值的属性、旧值和新值:

var name: String by Delegates.observable("<no name>") 
        prop, old, new ->
        println("$old -> $new")
    

返回了一个ObservableProperty 对象,继承自 ReadWriteProperty。再来看下它的内部实现:

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)
        

initialValue是初始值,而另外个参数onChange是属性值被修改时的回调处理器。

3.3 by map 映射委托

一个常见的用例是在一个映射(map)里存储属性的值,它可以使用Map/MutableMap来实现属性委托:

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


fun main(args: Array<String>) 
    val map = mutableMapOf(
        "name" to "哈哈"
    )
    val user = User(map)
    println(user.name)
    map["name"] = "LOL"
    println(user.name)


//输出:
哈哈
LoL

不过在使用过程中会有个问题,如果Map中不存在委托属性名的映射值的时候,会再取值时抛异常:Key $key is missing in the map

## MapAccessors.kt
public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = (getOrImplicitDefault(property.name) as V1)

@kotlin.internal.InlineOnly
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) 
    this.put(property.name, value)


## MapWithDefault.kt
internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V 
    if (this is MapWithDefault)
        return this.getOrImplicitDefault(key)

    return getOrElseNullable(key,  throw NoSuchElementException("Key $key is missing in the map.") )

所以在使用的时候要注意,必须要有映射值。

3.4 两个属性之间的直接委托

从 Kotlin 1.4 开始,我们可以直接在语法层面将“属性 A”委托给“属性 B”,就如下示例:

class Item 
    var count: Int = 0
    var total: Int by ::count

上面代码total的值与count完全一致,因为我们把total这个属性的getter和setter都委托给了count。可以用代码来解释下具体的逻辑:

class Item 
    var count: Int = 0

    var total: Int
        get() = count

        set(value: Int) 
            count = value
        

在写法上,委托名称可以使用":"限定符,比如this::delegateMyClass::delegate

这种用法在字段发生改变,又要保留原有的字段时非常有用。可以定义一个新字段,然后将其委托给原来的字段,这样就不用担心新老字段数值不一样的问题了。

3.5 提供委托

如果要在绑定属性委托之前再做一些额外的判断工作要怎么办?我们可以定义provideDelegate来实现:

class StringDelegate(private var s: String = "Hello")                                                      
    operator fun getValue(thisRef: Owner, property: KProperty<*>): String 
        return s
                           
    operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) 
            s = value
    



class SmartDelegator 

    operator fun provideDelegate(
        thisRef: Owner,
        prop: KProperty<*>
    ): ReadWriteProperty<Owner, String> 
	//根据属性委托的名字传入不同的初始值
        return if (prop.name.contains("log")) 
            StringDelegate("log")
         else 
            StringDelegate("normal")
        
    


class Owner 
    var normalText: String by SmartDelegator()
    var logText: String by SmartDelegator()


fun main() 
    val owner = Owner()
    println(owner.normalText)
    println(owner.logText)


//结果:
normal
log

这里我们创建了一个新的SmartDelegator,通过对成员方法provideDelegate再套了一层,然后在里面进行一些逻辑判断,最后才把属性委托getStringDelegate

这种拦截属性与其委托之间的绑定的能力,大大缩短了要实现相同功能,还要必须传递属性名的逻辑。

四、委托栗子

4.1 简化Fragment / Activity 传参

平时在针对Fragment传参,每次都要写一大段代码是不是很烦,现在有了委托这个法宝就来一起简化它,正常模式如下:

class BookDetailFragment : Fragment(R.layout.fragment_book_detail) 

    private var bookId: Int? = null
    private var bookType: Int? = null

    companion object 

        const val EXTRA_BOOK_ID = "bookId"
        const val EXTRA_BOOK_TYPE = "bookType";

        fun newInstance(bookId: Int, bookType: Int?) = BookDetailFragment().apply 
            Bundle().apply 
                putInt(EXTRA_BOOK_ID, bookId)
                if (null != bookType) 
                    putInt(EXTRA_BOOK_TYPE, bookType)
                
            .also 
                arguments = it
            
        
    

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)

        arguments?.let 
            bookId = it.getInt(EXTRA_book_ID, 123)
            bookType = it.getInt(EXTRA_BOOK_TYPE, 1)
        
    


写了那么一大段,终于写好了传参的基本方法,在获取值的时候还要处理参数为空的情况,现在我们就抽取委托类用属性委托的方式重新实现上面功能:

class BookDetailFragment : Fragment(R.layout.fragment_book_detail) 

    private var bookId: Int by argument()

    companion object 
        fun newInstance(bookId: Int, bookType: Int) = BookDetailFragment().apply 
            this.bookId = bookId
        
    

    override fun onViewCreated(root: View, savedInstanceState: Bundle?) 
      Log.d("tag", "BOOKID:" + bookId);
    

看上去减少了大量代码,是不是很神奇,下面实现思路如下所示:

class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> 

    override fun getValue(thisRef: Fragment, property: KProperty<*>): T 
      //对Bunndle取值还要进行单独处理
        return thisRef.arguments?.getValue(property.name) as? T
            ?: throw IllegalStateException("Property $property.name could not be read")
    

    override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) 
        val arguments = thisRef.arguments ?: Bundle().also  thisRef.arguments = it 
        if (arguments.containsKey(property.name)) 
            // The Value is not expected to be modified
            return
        
      	//对Bunndle设值还要进行单独处理
        arguments[property.name] = value
    


fun <T> Fragment.argument(defaultValue: T? = null) = FragmentArgumentProperty(defaultValue)

4.2 简化SharedPreferences存取值

如果我们现在存取值可以这样做是不是很方便:

private var spResponse: String by PreferenceString(SP_KEY_RESPONSE, "")

// 读取,展示缓存
display(spResponse)

// 更新缓存
spResponse = response

答案是可以的,还是用委托属性来改造,下面就是具体的实现示例:

class PreDelegate<T>(
        private val name: String,
        private val default: T,
        private val isCommit: Boolean = false,
        private val prefs: SharedPreferences = App.prefs) 

    operator fun 查看详情  

[javascript]记录完成轮播过程中的几个点(代码片段)

记录几个坑之前的轮播:完整代码:GitHub效果预览:GitHub最近完成的轮播:完整代码:GitHub效果预览:GitHub在完成轮播中解决两个问题:1.setInterval()会和按钮绑定的事件发生冲突。表现为:setInterval()设置了每3S轮播一次,在没有... 查看详情

databinding——使用kotlin委托优化(代码片段)

简介DataBinding是Google在Jetpack中推出的一款数据绑定的支持库,利用该库可以实现在页面组件中直接绑定应用程序的数据源。使其维护起来更加方便,架构更明确简洁。启用DataBindingDataBinding库与AndroidGradle插件捆绑在一起。... 查看详情

深入kotlin-委托(代码片段)

委托委托不是java中的概念。它仅仅是一种设计模式,但是kotlin从语言层级上,通过by关键字提供了对委托模式的支持。类委托interfaceMyInteface//1 funmyPrint()classMyInterfaceImpl(valname:String):MyInterface//2 overridefunmyPring() println(name) 查看详情

kotlin委托(代码片段)

1、类委托类的委托是一个类中定义的方法实际是调用另一个类的对象的方法来实现的。interfaceIDemofunaction1()funaction2()//被委托的类classDemoImpl:IDemooverridefunaction1()println("doaction1")overridefunaction2()println("doaction2")/ 查看详情

kotlin委托(代码片段)

1、类委托类的委托是一个类中定义的方法实际是调用另一个类的对象的方法来实现的。interfaceIDemofunaction1()funaction2()//被委托的类classDemoImpl:IDemooverridefunaction1()println("doaction1")overridefunaction2()println("doaction2")/ 查看详情

kotlin委托(代码片段)

1、类委托类的委托是一个类中定义的方法实际是调用另一个类的对象的方法来实现的。interfaceIDemofunaction1()funaction2()//被委托的类classDemoImpl:IDemooverridefunaction1()println("doaction1")overridefunaction2()println("doaction2")/ 查看详情

kotlin对象枚举委托(代码片段)

目录一、Kotlin对象1.kotlin伴生对象2.kotlin对象和单例模式二、Kotlin枚举1.kotlin定义枚举2.kotlin使用枚举三、Kotlin委托1.kotlin类委托2.kotlin属性委托3.kotlinMap委托4.kotlin延迟属性5.kotlin属性监听附Github源码一、Kotlin对象1.kotlin伴生对象clas... 查看详情

kotlin学习之委托机制(代码片段)

类的委托    委托模式可以很好的替代实现继承,kotlin本身支持需要零样板代码,一个类Derived可以继承Base并委托它所有的public方法到一个指定的类:interfaceBasefunprint()classBaseImpl(valx:Int):Baseoverridefunprint()print(x)classDerived(b:B... 查看详情

kotlin委托工厂map存储属性值(代码片段)

1.委托工厂(1)概念?委托工厂顾名思义:生产委托对象的工厂类。(2)定义?该类实现了operator修饰的provideDelegate方法,返回ReadWriteProperty/ReadOnlyProperty,该类就可提供对应类型的委托对象。/***委托工厂类(生产委托对象)*/classP... 查看详情

python必须熟知的几个模块(代码片段)

文章目录1.datetime模块1.date类2.time类3.datetime类2.json模块3.re模块1.datetime模块datetime模块中定义了很多类,如:date,time,datetime,timedelta,tzinfo。1.date类方法作用datetime.date.max、dateti 查看详情

kotlin学习记录(代码片段)

关于kotlin中的委托:1.属性委托  我们知道kotlin为我们提供了几种标准的委托,如下所示  延迟属性(lazyproperties):其值只在?次访问时计算;  可观察属性(observableproperties):监听器会收到有关此属性变更的通知;  把... 查看详情

[系列]go使用defer函数要注意的几个点(代码片段)

概述defer函数大家肯定都用过,它在声明时不会立刻去执行,而是在函数return后去执行的。它的主要应用场景有异常处理、记录日志、清理数据、释放资源等等。这篇文章不是分享defer的应用场景,而是分享使用defer需要注意的点... 查看详情

databinding——使用kotlin委托优化(代码片段)

简介DataBinding是Google在Jetpack中推出的一款数据绑定的支持库,利用该库可以实现在页面组件中直接绑定应用程序的数据源。使其维护起来更加方便,架构更明确简洁。启用DataBindingDataBinding库与AndroidGradle插件捆绑在一起。... 查看详情

js总结之关于this应该知道的几个点(代码片段)

JS中的this对每位前端工程师都不陌生,经常看到对象这里this那里this,那什么是this?答案就是上下文对象,即被调用函数所处的环境,也就是说,this在函数内部指向了调用函数的对象。通俗的讲,就是谁调用了函数。??情况1this... 查看详情

kotlin实战之委托总结(代码片段)

...是觉得将自己的云笔记分享出来吧~特别说明,kotlin系列文章均以Java差异为核心进行提炼,与Java相同部分不再列出。随着kotlin官方版本的迭代,文中有些语法可能会发生变化,请务必留意,语言领悟精髓... 查看详情

kotlin中委托的概念和原理(代码片段)

kotlin中委托的概念和原理问题背景kotlin的日常使用过程中,经常会使用到委托机制,问题来了,委托机制究竟是什么呢?委托模式:多个对象接收并处理同一请求,他们将请求委托给另一个对象统一处理请求。比如调用A类的metho... 查看详情

Sprite Kit 碰撞发生在精灵之外的几个点

】SpriteKit碰撞发生在精灵之外的几个点【英文标题】:SpriteKitcollisionhappensseveralpointsoutsidethesprite【发布时间】:2014-02-1002:48:01【问题描述】:我设置了一堆SKSpriteNode。我的英雄节点不应该撞墙,如果撞墙,它会触发碰撞。这工作... 查看详情