目录
8.5 Kotlin 课堂:泛型和委托
本章的 Kotlin 课堂我们将继续学习一些新的高级知识:泛型和委托。其实在前面的章节中我们已经使用过好几次泛型了,只是还没有系统地介绍过,而委托则是一个全新的主题内容。那么在 这节Kotlin 课堂里,我们就针对这两块主题内容进行学习。
8.5.1 泛型的基本用法
准确来讲,泛型并不是什么新鲜的事物。Java 早在1.5 版本中就引入了泛型的机制,Kotlin 自然也就支持了泛型功能。但是 Kotlin 中的泛型和 Java 中的泛型有同有异。我们在本小节中就先学习泛型的基本用法,也就是和 Java 中相同的部分,然后在第 10 章的Kotlin 课堂中再延伸学习 Kotlin 特有的泛型功能。
首先解释一下什么是泛型。在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好 的扩展性。
举个例子,List 是一个可以存放数据的列表,但是 List 并没有限制我们只能存放整型数据或字符串数据,因为它没有指定一个具体的类型,而是使用泛型来实现的。也正是如此,我们才可以 使用 List<Int>、List<String> 之类的语法来构建具体类型的列表。
那么要怎样才能定义自己的泛型实现呢?这里我们来学习一下基本的语法。
泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是<T> 。当然括号内的 T 并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常 情况下,T 是一种约定俗成的泛型写法。
如果我们要定义一个泛型类,就可以这么写:
class MyClass<T> {
fun method(param: T): T {
return param
}
}
此时的 MyClass 就是一个泛型类,MyClass 中的方法允许使用 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
}
}
此时的调用方式也需要进行相应的调整:
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() 方法的泛型指定成数字类型,比如 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
}
大功告成!现在你完全可以像使用 apply 函数一样去使用 build 函数了,比如说这里我们使用 build 函数简化 Cursor 的遍历:
contentResolver.query(uri, null, null, null, null)?.build {
while (moveToNext()) {
...
}
close()
}
好了,关于 Kotlin 泛型的基本用法就介绍到这里,这部分用法和 Java 中的泛型基本上没什么区别,所以应该还是比较好理解的。接下来我们进入本节 Kotlin 课堂的另一个重要主题——委托。
8.5.2 类委托和委托属性
委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委 托给另外一个辅助对象去处理。这个概念对于Java 程序员来讲可能相对比较陌生,因为 Java 对 于委托并没有语言层级的实现,而像 C# 等语言就对委托进行了原生的支持。
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 就会成为一个全新的数据结构类,这就是委托模式的意义所在。
但是这种写法也有一定的弊端,如果接口中的待实现方法比较少还好,要是有几十甚至上百个方法的话,每个都去这样调用辅助对象中的相应方法实现,那可真是要写哭了。那么这个问题 有没有什么解决方案呢?在 Java 中确实没有,但是在 Kotlin 中可以通过类委托的功能来解决。
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 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 类的 setValue()方法,当获取 MyClass 中 p 属性的值时,就会调用 Delegate 类的 getValue() 方法。是不是很好理解?
不过,其实还存在一种情况可以不用在 Delegate 类中实现 setValue()方法,那就是 MyClass中的 p 属性是使用 val 关键字声明的。这一点也很好理解,如果 p 属性是使用 val 关键 字声明的,那么就意味着 p 属性是无法在初始化之后被重新赋值的,因此也就没有必要实现 setValue() 方法,只需要实现 getValue() 方法就可以了。
好了,关于 Kotlin 的委托功能我们就学到这里。正如前面所说,委托功能本身不难理解,真正的 难点在于如何灵活地进行应用。那么接下来,我们就通过一个示例来学习一下委托功能具体的 应用。
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 表达式中最后一行代码的返回值。
这样看来,Kotlin 的懒加载技术也并没有那么神秘,掌握了它的实现原理之后,我们也可以实现 一个自己的 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 的委托功能在所有类中都可以使用。然后使用了一个 value 变量对值进行缓存,如果 value 为空就调用构造 函数中传入的函数类型参数去获取值,否则就直接返回。
由于懒加载技术是不会对属性进行赋值的,因此这里我们就不用实现 setValue() 方法了。
代码写到这里,委托属性的功能就已经完成了。虽然我们可以立刻使用它,不过为了让它的用法更加类似于 lazy 函数,最好再定义一个顶层函数。这个函数直接写在 Later .kt 文件中就可以了,但是要定义在 Later 类的外面,因为只有不定义在任何类当中的函数才是顶层函数。代码 如下所示:
fun <T> later(block:() -> T) = Later(block)
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
}
}
我们将这个顶层函数也定义成了泛型函数,并且它也接收一个函数类型参数。这个顶层函数的 作用很简单:创建 Later 类的实例,并将接收的函数类型参数传给 Later 类的构造函数。
现在,我们自己编写的 later 懒加载函数就已经完成了,你可以直接使用它来替代之前的 lazy 函数,如下所示:
val uriMatcher by later {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "book", bookDir)
matcher.addURI(authority, "book/#", bookItem)
matcher.addURI(authority, "category", categoryDir)
matcher.addURI(authority, "category/#", categoryItem)
matcher
}
但是如何才能验证 later 函数的懒加载功能有没有生效呢?这里我有一个非常简单方便的验证 方法,写法如下:
val p by later {
Log.d("TAG", "run codes inside later block")
"test later"
}
可以看到,我们在 later 函数的代码块中打印了一行日志。将这段代码放到任何一个 Activity 中,并在按钮的点击事件里调用 p 属性。
你会发现,当 Activity 启动的时候,later 函数中的那行日志是不会打印的。只有当你首次点击按钮的时候,日志才会打印出来,说明代码块中的代码成功执行了。而当你再次点击按钮的时 候,日志也不会再打印出来,因为代码块中的代码只会执行一次。
通过这种方式就可以验证懒加载功能到底有没有生效了,你可以自己测试一下。
另外,必须说明的是,虽然我们编写了一个自己的懒加载函数,但由于简单起见,这里只是大致还原了 lazy 函数的基本实现原理,在一些诸如同步、空值处理等方面并没有实现得很严谨。 因此,在正式的项目中,使用 Kotlin 内置的 lazy 函数才是最佳的选择。
好了,这节Kotlin 课堂的内容就到这里,下面就让我们对本章所学的所有知识做个回顾吧。
8.6 小结与点评
本章的内容不算多,而且很多时候是在使用上一章中学习的数据库知识,所以理解这部分内容 对你来说应该是比较轻松的吧。在本章中,我们一开始先了解了 Android 的权限机制,并且学会了如何在 Android 6.0 以上的系统中使用运行时权限,然后重点学习了 Content rovider 的相关内容,以实现跨程序数据共享的功能。现在你不仅知道了如何访问其他程序中的数据,还学会了怎样创建自己的ContentProvider 来共享数据,收获还是挺大的吧。
不过,每次在创建ContentProvider 的时候,你都需要提醒一下自己,我是不是应该这么做? 因为只有在真正需要将数据共享出去的时候才应该创建ContentProvider ,如果仅仅是用于程序内部访问的数据,就没有必要这么做,所以千万别对它进行滥用。
本章的Kotlin 课堂又是干货满满的一堂课啊。我们学习了泛型和委托这两块主题内容,虽然难度是在渐渐增加的,但是这些都是 Kotlin 中非常重要的功能,你可千万不能掉队。尤其是泛型功 能,在后面的章节里还会频繁用到,一定要好好掌握才行。
在连续学了几章系统机制方面的内容之后,是不是感觉有些枯燥?那么下一章中我们就换换口味,学习一下Android 多媒体方面的知识吧。