kotlin基础之泛型和委托

一、泛型的基本用法

在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。
举个例子,List是一个可以存放数据的列表,但是List并没有限制我们只能存放整型数据或字符串数据,因为它没有指定一个具体的类型,而是使用泛型来实现的。也正是如此,我们才可以使用List< Int > 、List< String >之类的语法来构建具体类型的列表。
泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是< T >。当然括号里的T并不是固定要求的,使用任何英文字母或单词都可以,但是通常情况下,T是一种约定俗成的泛型写法。
如果我们要定义一个泛型类,就可以这么写:

class MyClass<T>{
fun method(param:T):T{
return param
}
}

此时的MyClass就是一个泛型类,MyClass中的method方法允许使用T类型的参数和返回值。
我们在调用MyClass类和method()方法的时候,就可以将泛型指定成具体的类型,如下所示:

var myclass=MyClass<Int>()
val result=myclass.method(123)

这里我们将MyClass类的泛型指定成了Int类型,于是method()方法就可以接收一个Int类型的参数,并且返回值也变成了Int类型。
而如果我们不想定义一个泛型类,只是想定义一个泛型方法,应该怎么做?只需要将定义泛型的语法结构写在方法上面就可以了,如下所示:

class MyClass{
fun <T> method(param :T){
return param
}
}

此时的调用方式也需要进行调整:

val myClass=MyClass()
val result=myClass.method<Int>(123)

可以看到,现在是在调用method()方法的时候指定泛型类型了。另外Kotlin还拥有非常出色的类型推导机制,例如我们传入了一个Int类型的参数,它能够自动推导出泛型的类型就是Int类型,因此这里也可以进行省略泛型的指定:

val myClass=MyClass()
val result=myClass.method(123)

Kotlin还允许我们对泛型的类型进行限制。目前你还可以将method()方法的泛型指定成任意类型,但是如果这并不是你想要的话,还可以通过指定上界的方式来对泛型的类型进行约束,比如这里将method()方法的泛型上界设置为Number类型,如下所示:

class MyClass{
fun <T :Number> method(param:T):T{
return param
}
}

这种写法就表明了,我们只能将method方法指定成了数字类型(因为调用method方法的类型是Number类的子类),比如Int、Float、Double等。但是如果你指定成字符串类型,就肯定会报错,因为它不是一个数字。
另外,在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型的上界默认是Any?。而如果想要让泛型的类型不可为空,只需要让泛型的上界手动指定成Any就可以了。(Any相当于Java中的object,是所有类的父类)
接下来我们来对本节泛型内容进行应用,我们编写一个build函数,代码如下所示:

fun StringBuilder.build(block:StringBuilder.()->Unit):StringBuilder{
block()
return this
}

这里运用到高阶函数的知识,可查看:参考链接
这个函数的作用和apply函数基本一样,只是build函数只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面。这时我们可以通过泛型知识进行扩展,让build函数实现和apply函数一样的功能。
只需要使用< T >将build函数定义成泛型函数,再将原来所有强制指定StringBuilder的地方都替换成< T>就可以了。新建一个build.kt文件,并编写如下代码:

     fun <T> T.build(block:T.()->Unit):T{
        block()
        return this
    }

现在就可以像使用apply函数一样去使用build函数了,比如这里使用build函数简化Cursor的遍历:

contentResolver.query(uri,null,null,null)?.build{
while(moveToNext){
...
}
close()
}

二、类委托和委托属性

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。Kotlin中也是支持委托功能的,并且将委托功能分为了两种:类委托和委托属性。
首先类委托,它的核心思想是将一个类的具体实现委托给另外一个类去完成。在前面的章节中,我们曾经使用过Set这种数据结构,它和List有点类似,只是它所存储的数据是无序的,并且不能存储重复的数据。Set是一个接口,如果使用它的话,需要使用它具体的实现类,比如HashSet。而借助于委托模式,我们可以实现一个自己的实现类。比如,定义一个MySet,并让它实现Set接口,代码如下所示:

class MySet <T>(val helperSet:HashSet<T>):Set<T>{
    override val size: Int
        get() = helperSet.size

    override fun contains(element: T)=helperSet.contains(element)

    override fun containsAll(elements: Collection<T>)=helperSet.containsAll(elements)

    override fun isEmpty()=helperSet.isEmpty()

    override fun iterator()=helperSet.iterator()
}

可以看到,MySet类的构造方法中接收了一个HashSet参数,这就相当于一个辅助对象。然后再Set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这其实就是一种委托模式。
这种写法的好处是什么?当我们只让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入自己独有的方法,那么MySet类就会成为一个全新的类,这就是委托模式的意义所在。
但是这种写法也有弊端,当我们实现这个接口,但接口中待实现的方法要是有几十个甚至上百个方法的话,每个都去这样调用辅助对象中的相应方法去实现,那么太麻烦了,kotlin可以通过类委托的功能来解决。
Kotlin中使用委托的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以,免去之前写的一大堆模版式代码了,如下所示:

class MySet <T>(val helperSet:HashSet<T>):Set<T> by helperSet{
    fun helloWorld()= println("Hello World")
    override fun isEmpty()=false
}

这里新增了一个helloWorld()方法,并且重写了isEmpty()方法,让它永远返回false。现在我们的MySet就成为了一个全新的类,它不仅永远不会为空,而且还能打印helloWorld(),至于其他Set接口中的功能,则和HashSet保持一致。 这就是Kotlin的类委托所能实现的功能。
接下来我们开始学习委托属性,类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
委托属性的语法结构,如下所示:

class MyClass{
 var p by Delegate()
}

可以看到,这里使用by关键字连接了左边的p属性和右边的Delegate实例,这是什么意思呢?这种写法就代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给P属性赋值的时候会自动调用Delegate类的setValue()方法。
因此,我们还需要对Delegate类进行具体的实现才行

class Delegate{
var propValue:Any?=null
operator fun getValue(myClass:MyClass,prop:KProperty<*>):Any?{
return propValue
}
operator fun setValue(myClass:MyClass,prop:KProperty<*>,value:Any?){
propValue=value
}
}

这是一种标准的代码实现模版,在Delegate类中我们必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。
getValue()方法要接收两个参数:第一个参数用于声明该Delegate类的委托功能可以在什么类中使用,这里写成MyClass表示仅可在MyClass类中使用;第二个参数KProperty<*> 是Kotlin中的一个属性操作类,可用于获取各种属性相关的值。另外< * >这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,有点类似于Java中<?>的学法。至于返回值可以声明成任何类型,根据具体的实现逻辑去写就行了。
setValue()方法也是类似,只不过它要接收3个参数。前两个参数和getValue()方法是相同的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法返回值的类型保持一致。
整个委托属性的工作流程大概就是这样实现的,现在当我们给MyClass的p属性赋值时,就会调用Delegate类的getValue()方法。

三、实现一个自己的lazy函数

在初始化一些特殊变量的时候,一般我们会把想要延迟执行的代码放到by lazy代码块中,这样代码块在一开始就不会执行,只有当变量首次被调用的时候,代码块中的代码才会执行。这就是一种懒加载技术。
学习了Kotlin委托功能后,就可以对by lazy的工作原理进行解密了,它的基本语法结构如下:

val p by lazy{
...
}

实际上,by lazy并不是连在一起的关键字,只有by才是kotlin中的关键字,lazy在这里只是一个高阶函数。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码就可以得到执行了。并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值
新建一个Later.kt文件,并编写如下代码:

class Later<T>(val block:() ->T){

}

这里我首先定义了一个Later类,并将它指定成泛型类。Later的构造函数中接收一个函数类型参数,这个函数类型参数不接收任何参数,并且返回值类型就是Later类指定的泛型。
接着我们再Later类中实现getValue()方法,代码如下所示:

class Later<T>(val block:()->T){
var value :Any?=null
operator fun getValue(any:Any?,prop:KProperty<*>):T{
if(value==null){
value=block()
}
return value as T
}
}

这里将getValue()方法的第一个参数指定成了Any?类型,表示我们希望Later的委托功能在所有类中都可以使用。然后使用了一个value变量对值进行缓存,如果value为空就调用构造函数中传入的函数类型参数去获取值,否则就直接返回。
由于懒加载技术是不会对属性进行赋值的,因此这里我们就不用实现setValue()方法了。
为了让它的用法更加类似于lazy函数,我们再定义一个顶层函数。这个函数直接写在Later.kt文件中就可以了,但是要定义在Later类的外面,因为只有不定义在任何类当中的函数才是顶层函数。代码如下所示:

fun <T> later(block:() ->T)=Later(block)

我们将这个顶层函数也定义成了泛型函数,并且它也接收了一个函数类型参数。这个顶层函数的作用就是创建Later类的实例,并将接收的函数类型参数传给Later类的构造函数。
我们通过一个例子:
Later.kt文件:

class Later<T>(val block:()->T){
    var value :Any?=null
    operator fun getValue(any:Any?,prop: KProperty<*>):T{
        if(value==null){
            value=block()
        }
        return value as T
    }
}
fun <T> later(block:() ->T)=Later(block)

FirstActivity :

class FirstActivity : BaseActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        val p by later {
            Log.d("TAG", "run codes inside later block")
            "text later"
        }
        button.setOnClickListener {
            Log.d("FirstActivity P", p)
            Toast.makeText(this,"点击了按钮",Toast.LENGTH_SHORT).show()
        }
    }
}

在这里插入图片描述

可以看到,我们在later函数的代码块中打印了一行日志。将这段代码放到一个Activity中,并在按钮的点击事件里调用P属性。
此时later函数接收的是Later类通过构造方法创建的对象,当调用P属性的时候,其实就是调用对象的getValue()方法,getValue()方法里面会调用later函数传入的Lambda表达式,这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。
当Activity启动的时候,later函数中的那行代码日志是不会打印的。只有当你首次点击按钮的时候,日志才会打印出来,说明代码块中的代码成功执行了。而当你再次点击按钮的时候,日志不回再打印出来,因为代码块中的代码只会执行一次。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值