8.5--Kotlin 课堂:泛型和委托

8.5.1 泛型的基本用法

泛型并不是什么新鲜的事物。Java 早在1.5版本中就引入了泛型的机制,Kotlin 自然也就支持了泛型功能。但是Kotlin 中的泛型和Java 中的泛型有同有异(有相同之处,也有不同之处)。我们先学习和Java 中相同的部分吧。

首先解释一下什么是泛型。在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。

举个例子,List 是一个可以存放数据的列表,但是List 并没有限制我们只能存放整型数据或字符串数据,因为它没有指定一个具体的类型,而是使用泛型来实现的。也正是如此,我们才可以使用List<Int>、List<String>之类的语法来构建具体类型的列表。

那么要怎养才能定义自己的泛型实现呢?这里我们来学习一下基本的语法。

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是<T>,当然括号内的T 并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常情况下,T 是一种约定俗成的泛型写法。

如果我们定义一个泛型类,就可以这么写:

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

此时的MyClass 类就是一个泛型类,MyClass 中的方法允许使用 T 类型的参数和返回值。

我们在调用MyClass 类和 method() 方法的时候,就可以将泛型指定成具体的类型,如下所示:

fun main() {
    val myClass = MyClass<Int>()
    val result = myClass.method(123)
    println(result)
}

这里我们将MyClass 类的泛型指定成Int 类型,于是 method() 方法就可以接收一个 Int 类型的参数,并且它的返回值也变成了Int 类型。

而如果我们不想定义一个泛型类,只是想定义一个泛型方法,应该要怎么写呢?也很简单,只需要将定义泛型的语法结构写在方法上面就可以了,如下所示:

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

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

fun main() {
    val myClass = MyClass()
    val result = myClass.method<Int>(123)
    println(result)
}

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

fun main() {
    val myClass = MyClass()
    val result = myClass.method(123)
    println(result)
}

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

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

这种写法就表明,我们只能将method() 方法的泛型指定成数字类型,比如Int 、Float、Double 等。但是如果你指定成字符串类型,就肯定会报错,因为它不是一个数字。

另外,在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型的上界默认是Any? 。而如果想要让泛型的类型不可为空,只需要将泛型的上界手动指定成Any 就可以了。

接下来,我们尝试对本小节所学的泛型知识进行应用,回想一下,在6.5.1小节学习高阶函数的时候,我们编写了一个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
}

这样就完成了,为什么指定<T> 就可以让所有的函数都调用它呢??上面有讲过不手动指定上界的话默认是Any?  ,这个Any 就是所有类的父类!

 

8.5.2 类委托和委托属性

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另一个辅助对象去处理。

委托功能分为了俩贵司:类委托委托属性 。

首先来看类委托 ,它的核心思想在于将一个类的具体实现(内部的各种函数方法)委托给另一个类去完成。在前面的章节中,我们曾经使用过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 中 类委托 使用的关键字是by ,我们只需要在接口声明的后面使用by 关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式代码了,如下所示:

class MySet<T>(val helperSet: HashSet<T>):Set<T> by helperSet{

}

这两段代码实现的效果是一模一样的,但是借助了类委托 的功能之后,代码明显简化了太多,另外,如果我们要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托 所带来的便利,如下所示:

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

这里我们新增了一个helloWord() 方法,并且重写了isEmpty() 方法,让它永远返回false。当然这是一种错误的做法,这里仅仅是为了演示一下而以。现在我们的MySet 就成为了一个全新的数据结构类,它不仅永远不会为空,而且还能打印helloWord(),至于其他Set接口中的功能,则和HashSet 保持一致。这就是Kotlin 类委托 所能实现的功能。

掌握了类委托 之后,接下来我们开始学习委托属性 。它的基本原理也非常容易理解,真正的难点在于如何灵活地进行应用。

类委托 的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性 的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。

我们看一下委托属性 的语法结构:

class MyClass{
    var p by Delegate()
}

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

因此,我们还得对Delegate 类进行具体实现才行(Ps: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 关键字进行声明。Ps:因为是p 属性是var变量所以要有setValue() 如果是val 常量就可以不写setValue() 了。

gaValue() 方法要接收两个参数:

第一个参数用于声明该Delegate 类的委托功能可以在什么类中使用,这里写成MyClass 表示仅可在MyClass 类中使用;

第二个参数KProperty<*> 是Kotlin 中的一个属性操作类,可用于获取各种属性相关的值,当前场景下用不着,但是必须在方法参数上声明。另外,<*> 这种泛型的写法表示你不知道或者不关心泛型的具体类型,知识为了通过语法编译而且,有点类似于Java 中<?> 的写法。至于返回值可以声明成任何类型,根据具体的实现逻辑去写就行了,上述代码知识一种示例写法;。

 

setValue() 方法也是相似的,只不过它要接收三个参数:

第一个参数用于声明该Delegate 类的委托功能可以在什么类中使用,这里写成MyClass 表示仅可在MyClass 类中使用;

第二个参数KProperty<*> 是Kotlin 中的一个属性操作类,可用于获取各种属性相关的值;

第三个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue() 方法的返回值保持一致;

 

接下来我们就通过一个示例来学习一下委托功能的具体应用。

 

8.5.3 实现一个自己的lazy 函数

在8.4.2 小节初始化 uriMatcher 变量的时候,我们使用了一种懒加载技术。把想要延迟执行的代码放到by lazy 代码块中,这样代码块中的代码在一开始的时候就不会执行,只有当uriMatcher 的变量首次被调用的时候,代码块中的代码才会执行。

那么学习了Kotlin 的委托功能之后,我们就可以对by lazy 的工作原理进行解密了,它的基本语法结构如下:

val p by lazy { ... }

现在再来看这段代码,是不是觉得更有头绪了呢?实际上,by lazy 并不是连在一起的关键字,只有 by 才是Kotlin 中的关键字,lazy 在这里知识一个高阶函数而且。在lazy 函数中会创建并返回一个Delegate 对象,当我们调用p 属性的时候,其实调用的是Delegate 对象的getValue() 方法,然后getValue() 方法中又会调用lazy 函数传入的Lambda 表达式,这样表达式中的代码就可以得到执行了,并且调用p 属性后得到的值就是Lambda 表达式中最后一行代码的返回值。

我们来自己实现一个lazy 函数。

新建一个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
    }
}

这段代码很简单,就是通过构造方法中的 函数类型参数 给value 全局变量赋值。如果value 已经有值,直接返回value,如果value 是空的,证明还没有值,我们就给它进行一次初始化,调用函数类型参数。

由于懒加载技术是不会对属性进行赋值的,因此这里我们就不实现setValue() 方法了。

代码写到这里,委托属性的功能已经就完成了。虽然我们可以立即使用它,不过为了让它的方法更加类似于lazy 函数,最好再定义一个顶层函数。这个函数直接卸载Later.kt 文件中就可以了,但是要写在Later 类外面,因为只有不定义在任何类当中的函数才是顶层函数。代码如下所示:

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

我们将这个顶层函数也定义成了泛型函数,并且它也接收一个函数类型参数。这个顶层函数的作用很简单:创建Later 类的实例,并将接收的函数类型参数传给Later 类的构造函数。

over~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值