【21】Kotlin语法进阶——泛型和委托

提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方欢迎指正。

文章目录

  • 一、Kotlin语言中的泛型
    • 1.1 泛型类与泛型方法
    • 1.2 泛型的上界
  • 二、类委托和委托属性
    • 2.1 类委托
    • 2.2 委托属性
  • 三、lazy函数的基本原理

一、Kotlin语言中的泛型

1.1 泛型类与泛型方法

在一般的编程模式下,我们需要为每个变量明确指定一个具体的类型,比如Int、String等。如果我们希望编写的代码能够更加通用,不受特定类型的限制,这个时候就可以使用泛型。
泛型允许我们在编写代码时不指定具体的类型,而是使用一个占位符 < T > 来代替。泛型主要有两种定义方式,一种是定义泛型类,另一种是定义泛型方法。下面是定义一个泛型类的示例代码:

//T代表泛型类型
class MyClass<T> {
	fun method(param: T): T {
		return param
	}
}

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

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

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

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

需要注意:泛型类的< T >结构位于在类名的后面,而泛型方法的< T >结构位于方法名的前面

此时,调用method泛型方法的方式也发生了变化:

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

Kotlin具有非常出色的类型推导机制。当我们给泛型方法传入一个Int类型的参数,它能够自动推导出泛型的类型就是Int类型,因此这里也可以省略泛型的指定:

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

1.2 泛型的上界

在Kotlin语言中,泛型可以通过上界来进一步限制泛型的范围,上界就是泛型参数可以取到的最大类型。在上面的例子中,method()方法的泛型可以是任意类型,我们可以通过指定上界的方式来对泛型的类型进行约束。例如,将method()方法的泛型上界设置为Number类型,如下所示:

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

这种写法就表明method()方法的泛型参数可以取到的最大类型是Number,也就是说参数T只能是Number类的子类(如Integer、Float、Double等),而不可以是其他类型。
在默认情况下,所有的泛型都是可以指定成可null类型的,因为在不手动指定上界时,泛型的默认上界是Any?类型。如果想要让泛型的类型不可为空,只需要让泛型的上界手动指定成Any就可以了(Any相当于Java中的object,是所有类的父类)。
我们尝试将之前在高阶函数学习的build()函数进行改造,build()函数的作用和apply函数基本是一样的,只不过build()函数只能用在StringBuilder类上,因为它是StringBuilder的扩展函数。:

      扩展函数            将函数类型定义到StringBuilder类中
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
    TODO("Code Logic")
    return this
}

现在我们就用刚刚学习的泛型知识,对build()函数进行扩展,让其变成一个泛型方法:

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

现在,你可以在所有类上调用自定义的build函数了,而不是仅局限在StringBuilder类上。

二、类委托和委托属性

委托是一种设计模式,他希望操作对象自己不去处理某段逻辑,而是把工作委托给另外一个辅助对象去处理。通过委托,有助于减少重复代码,提高代码的可读性和可维护性。在Kotlin中,委托功能分为两种:“类委托”和“委托属性”。

2.1 类委托

类委托的基本理念是:将一个类的实现委托给另一个辅助对象去实现。也就是说,我们可以通过使用其他类中已有的代码来实现一个新的类,并将这个新的类与其他类中的代码进行解耦。Set这种数据结构和List很像,只是Set中存储的数据是无序的,并且不能存储重复的数据。Set是一个接口,如果要使用它的话,需要使用它的实现类(比如HashSet)。比如这里定义的MySet,并让他实现Set接口:

package com.example.servicetest

class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
    
    override val size: Int
        get() = TODO("Not yet implemented")

    override fun isEmpty(): Boolean {
        TODO("Not yet implemented")
    }

    override fun iterator(): Iterator<T> {
        TODO("Not yet implemented")
    }

    override fun containsAll(elements: Collection<T>): Boolean {
        TODO("Not yet implemented")
    }

    override fun contains(element: T): Boolean {
        TODO("Not yet implemented")
    }
}

可以看到,在我们的MySet实现了Set接口后,我们还需要实现Set接口中很多个方法。而借助委托模式,我们很容易就可以实现一个自己的实现类:

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

    override val size: Int
        get() = helperSet.size // HashSet参数

    override fun isEmpty(): Boolean {
        return helperSet.isEmpty() // HashSet参数
    }

    override fun iterator(): Iterator<T> {
        return helperSet.iterator() // HashSet参数
    }

    override fun containsAll(elements: Collection<T>): Boolean {
        return helperSet.containsAll(elements) // HashSet参数
    }
    
    override fun contains(element: T): Boolean {
        return helperSet.contains(element) // HashSet参数
    }
}

可以看到,MySet的构造函数中接收了一个HashSet参数,这个HashSet参数就相当于一个辅助对象。然后在MySet中实现Set接口中多个方法时,我们都没有进行自己的实现,而是调用了HashSet参数(辅助对象)中相应的方法实现,这就是一种委托模式。
委托模式的意义在于,我们只让大部分方法的实现,以调用辅助对象中的方法这种形式来实现。而少部分方法的实现,则是由我们自己来重写,我们甚至可以加入一些自己独有的方法。这样,MySet就是一个全新的数据结构类。
当然,若一个接口中待实现的方法很多的话,我们岂不是要重写几十甚至上百个方法?很幸运,在Kotlin中你可以通过类委托来解决。Kotlin中委托使用的关键字是“by”,我们只需要在接口声明后面使用“by”关键字,再接上受委托的辅助对象,就可以免去之前重写的一大堆模板式的代码了:

                                                关键字   辅助对象
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
}

这段使用了by关键字的委托模式代码,和上面调用辅助对象实现接口方法的那段代码,在效果上是一模一样的。但是借助了类委托的功能之后,代码明显简化了。另外,如果我们要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利,如下所示:

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

    fun helloWorld() {
        println("Hello World!")
    }

    override fun isEmpty(): Boolean {
        return false
    }
}

我们这里为了演示,新增了一个helloWorld()方法,然后重写了isEmpty()方法。现在我们的MySet就是一个全新的数据结构类,它isEmpty()方法的返回值永远为false,并且还能调用helloWorld()打印"Hello World!"。至于其他Set接口中的功能,则和HashSet一模一样,这就是Kotlin中的类委托所能实现的功能。

2.2 委托属性

前面我们说过了类委托的基本理念是:“将一个类的实现委托给另一个辅助对象去实现”,而委托属性的核心思想是:“将一个属性(字段)的具体实现委托给另一个类去完成”。下面是委托属性的语法结构:

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类中使用;第二个参数中的 <*> 这种写法表明我们并不关心泛型的具体类型,只是为了通过语法编译。
现在,当我们给MyClass属性p赋值时,就会调用Delegate.setValue()方法;当我们需要获取MyClass属性p的值时,就会调用Delegate.getValue()方法。需要注意一种情况,当你给MyClass属性p声明为val时,p属性就无法在初始化后重新被赋值了。因此也就没有必要实现Delegate类中的setValue()方法了,只需要实现getValue()方法就可以。

三、lazy函数的基本原理

在前面我们学习过by lazy{ }懒加载技术,把想要延迟初始化的代码放到by lazy{ }代码块中。这样,代码块中的代码在一开始的时候就不会立马初始化,只有当变量首次被调用的时候,代码块中的代码才会执行。下面是一个by lazy{ }懒加载技术的基本语法结构:

val p by lazy {
· · ·
}

现在你应该明白了,by lazy并不是连在一起的,by是委托模式的关键字,而lazy则是一个高阶函数。在lazy函数中,会创建并返回一个Delegate对象,然后调用Delegate对象的getValue()方法。getValue()方法中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码就会得到执行,并且Lambda表达式中最后一行代码的返回值就会作为调用p属性后得到的值。
现在我们实现一个自己的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
    }
}

这里,getValue()方法的第一个参数为Any?类型,它表示我们希望Later委托功能在所有类中都可以使用。getValue()方法在第一次被调用时执行,并且其结果被缓存下来,之后的调用不再执行该方法,而是直接返回缓存的结果。由于懒加载技术不会对属性进行赋值,所以这里我们就不需要实现setValue()方法。为了让它的用法更加类似于lazy函数,我们还需要定义一个顶层函数。直接将函数写在Later.kt文件的Later类外面,因为只有不被定义在任何类中的函数才是顶层函数。

在Kotlin中,顶层方法是指直接定义在文件中的方法,而不是定义在类、接口或对象中的方法。顶层方法可以直接使用,无需创建对象。

//顶层方法
fun <T> later(block: () -> T): Later<T> {
    return Later(block)
}

现在我们编写的later懒加载函数就已经完成了,你可以这样使用它:

private val uriMatcher by later {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "book", book_dir)
        matcher.addURI(authority, "book/#", book_item)
        matcher.addURI(authority, "category", category_dir)
        matcher.addURI(authority, "category/#", category_item)
        matcher
    }

这里我们只是简单实现了lazy函数的基本原理,在实际项目中还是使用lazy函数更好一些。

你可能感兴趣的:(奇妙的Kotlin之旅,kotlin,开发语言,android)