Kotlin语法基础篇十三:by关键字(手写一个我们自己的lazy函数吧~)

前言

在上一篇文章我们详细的介绍了Kotlin泛型的协变、逆变、类型投影、星投影。这篇文章我们来讲解Kotlin中另外一个比较重要的知识点委托。委托的运用主要分为两种:委托属性和委托类。关于委托属性,它是一种很好的懒加载模式;而委托类,它是替代继承的一个很好的方式。在Kotlin中我们使用by关键字来表示对一个类或属性进行委托,下面我们开始本篇文章的讲解。

1.委托的概念

委托是一种设计模式,它的基本概念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。例如我们下面这段代码就是一个简单的示例:

kotlin 复制代码
class NewList<out T>(private val list: MutableList<T>) {

    fun isEmpty() = list.isEmpty()

    fun add(item: @UnsafeVariance T) = list.add(item)

    fun remove(item: @UnsafeVariance T) = list.remove(item)

    fun myMethod() {
        // some logic
    }

}

我们在类NewList中定义了几个简单的方法,我们并没有让NewList自己去实现这些方法的逻辑。而是交给了其构造函数中的属性参数list去处理。

2.委托属性

委托属性的语法:val/var <属性名>: <类型> by <表达式>

by关键字后面的表达式是该委托,属性对应的get()set()会被委托给它的getValue()setValue()方法。属性的委托不必实现任何的接口,但是需要提供getValue()setValue()方法。下面我们来看一个具体的示例:

kotlin 复制代码
class Delegate {
    operator fun getValue(thisRef:Any?, prop:KProperty<*>) : String {
        return "$thisRef, name = ${prop.name}"
    }
}

class Example {
    val p : String by Delegate()
    val q : String by Delegate()
}

fun main() {
    val e = Example()
    println(e.p)
    println(e.q)
}

// 输出
com.study.myapplication.bean.Example@4de8b406, name = p
com.study.myapplication.bean.Example@4de8b406, name = q

我们来分析一下这段代码。首先我们创建了一个名为Delegate的类,然后在Delegate类内部我们创建了一个使用operator关键字修饰的方法getValue(),关于操作符重载的相关知识我们已经在有趣的操作符重载这篇文章中详细的介绍了,这里就不展开讲解了。getValue()方法中有两个参数:thisRefprop。而thisRef代表我们被委托的对象实例,prop中存储了被委托属性的相关信息。 在main()函数中访问p属性就是相当于调用Delegate中的getValue()方法,如下示例代码:

kotlin 复制代码
fun main() {
    val e = Example()
    println(e)
    
    // 对属性e的访问,等价于下面的调用
    val delegate = Delegate()
    delegate.getValue(e, e::p)
}

其中e::p获取的是KProperty的引用,关于::的用法我们在后面的文章会详细说明。如果我们仅仅只是委托一个属性的get()方法,那么该委托属性在声明时只能是val类型。只有同时委托get()set()我们才能将委托的属性声明为var类型。这和属性声明时的语法是类似的。如果我们强行将上述示例中被委托的属性声明为var,那么编译器会提示"Type 'Delegate' has no method 'setValue(Example, KProperty<*>, String)' and thus it cannot serve as a delegate for var (read-write property)

意思就是我们只重载get()被委托的属性是只读的,需要我们去重载set(),被委托的属性才能被声明为var类型。下面我们就在Delegate类中去重载set()

kotlin 复制代码
class Delegate {
    operator fun getValue(thisRef:Any?, prop:KProperty<*>) : String {
        return "$thisRef, name = ${prop.name}"
    }
    operator fun setValue(thisRef:Any?, prop: KProperty<*>, value: String) {
        println( "$thisRef, name = ${prop.name}, value = $value")
    }
}

class Example {
    var p : String by Delegate()
    val q : String by Delegate()
}

fun main() {
    val e = Example()
    println(e.p)
    e.p = "by this property"
}

// 输出
com.study.myapplication.bean.Example@1810399e, name = p
com.study.myapplication.bean.Example@1810399e, name = p, value = by this property

可以看到我们在给被委托属性p赋值时就是调用了Delegate类中的setValue()方法。并且在Example类中我们将被委托属性p声明为了var类型。

3.委托属性的真相

上面我们一直在介绍当我们访问被委托属性的get()就是调用了委托类中的getValue()方法;访问其set()就是访问了委托类中的setValue()方法。对于上述示例,这样一个简单属性委托:

csharp 复制代码
class Example {
    var prop : String by Delegate()
}

我们来看一下它反编译成Java代码的情况,为了方便阅读,这里对代码进行了一些调整:

kotlin 复制代码
class Example {
    private val prop$delegate = Delegate()
    var prop: String
        get() = prop$delegate.getValue(this, this::prop)
        set(value: String) = prop$delegate.setValue(this, this::prop, value)
}

相信看到这,你已经可以熟练的掌握了属性委托的实现原理了。

4.动手实现一个我们自己的lazy函数吧

Kotlin的内置API中,为我们提供了一个lazy()高阶函数,我们经常在开发中这么去写:

vbnet 复制代码
val property : String by lazy { "property lazy" }

下面我们就来分析一下lazy()函数,然后我们自己去实现它。lazy()函数拥有一个函数类型的参数,因为我们在调用lazy()函数的时候使用了Lambda表达式。在高阶函数和Lambda表达式的文章中我们已经详细介绍了,在KotlinLambda表达式就是函数类型的实例。lazy()函数中的函数类型的参数拥有一个返回值,该返回值的类型决定了我们被委托属性的类型,我们可以在lazy()函数的Lambda表达式中设置任意的类型作为被委托属性的值,我们就可以推断出lazy()函数是个泛型函数

猜想一 ->lazy()函数应该长这样:

kotlin 复制代码
fun <T> easyLazy(block:() -> T) { }

猜想二 ->lazy()函数需要返回一个委托类的对象,委托类中的对象重载的get()的返回值是我们在lazy()函数中设置的类型,那么我们这个委托类应该长这样:

kotlin 复制代码
class Later<T>(private val block: () -> T) {
    private var value: Any? = null
    operator fun getValue(thisRef:Any?, prop:KProperty<*>) : T {
        if(value == null) {
            value = block()
        }
        return value as T
    }
}

猜想三 -> lazy()函数需要完成属性的委托,lazy()函数本身的返回值需要是个委托类:

kotlin 复制代码
fun <T> easyLazy(block:() -> T) : Later<T> { 
    return Later(block)
}

这样我们一个简单的eazyLazy()函数就完成了,下面我们写个简单的示例来运用它:

kotlin 复制代码
fun main() {
    val name : String by easyLazy { "by name" }
    println(name)
}
// 输出
by name

就是这么简单,当然内置API中的lazy()函数要比这里复杂许多,其还提供了三种不同的使用模式,感兴趣的读者可以自己去阅读下内置的lazy()函数的实现,这里就不展开介绍了,实现的原理都是一样的。

5.委托类

委托模式已经证明是实现继承的一个很好的替代方式。下面我们就来看一个简单的示例:

kotlin 复制代码
class MyList<E>(private val list:MutableList<E>) : MutableList<E> by list {
    override val size: Int get() = list.size
}

类委托的语法结构可能看上去不是那么好理解。但是我们要是理解了其真实的实现原理就比较好理解了,下面我们就来分析一下上面这个类委托的示例。首先我们定义了一个MyList的泛型类,并且让MyList类实现了MutableList接口。在MyList的主构造函数中我们定义了一个类型为:MutableList名为:list的属性,然后紧接着我们又在继承类后添加了by list。而by list表示list将会在MyList中内部存储,并且编译器将生成MutableList中所有的方法转发给list。下面我们来看一下MyList反编译成Java的代码:

kotlin 复制代码
public final class MyList implements List, KMutableList {
   private final List list;

   public int getSize() {
      return this.list.size();
   }

   public final int size() {
      return this.getSize();
   }

   public MyList(@NotNull List list) {
      Intrinsics.checkNotNullParameter(list, "list");
      super();
      this.list = list;
   }

   public boolean add(Object element) {
      return this.list.add(element);
   }
   
   public boolean contains(Object element) {
      return this.list.contains(element);
   }

   public Object get(int index) {
      return this.list.get(index);
   }

   public boolean isEmpty() {
      return this.list.isEmpty();
   }

   public boolean remove(Object element) {
      return this.list.remove(element);
   }
   
   // ... 省略
}

为了方便阅读,这里将一些方法进行了省略。而在实际开发中,当MutableList不能满足我们的现有的需求时我们完全可以使用委托来实现,我们可以在MyList中新增一些我们自定义的方法,让MyList成为一个新的数据结构,而这也正是我们使用类委托的优势所在。

总结

到这里关于属性委托和类委托的使用我们就介绍完了,事实上两者在语法上并不是很难理解,而难的是我们如何将它们很好的运用在我们的实际开发中。比如委托类,我们需要在什么样的条件下去使用它。

只有掌握了语法的真实含义,我们才能更好的在实际开发中去运用它。下篇文章我们继续讲解Kotin中的基础知识集合,我们下期再见~

相关推荐
酷爱码1 小时前
css中的 vertical-align与line-height作用详解
前端·css
沐土Arvin1 小时前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年1 小时前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖6661 小时前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
L耀早睡2 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer2 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿2 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹3 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹3 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年3 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net