Android第一行代码——快速入门 Kotlin 编程(8.5 Kotlin 课堂:泛型和委托)

目录

8.5 Kotlin 课堂:泛型和委托

8.5.1        泛型的基本用法

8.5.2        类委托和委托属性

8.5.3        实现一个自己的 lazy 函数

8.6        小结与点评


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 并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常 情况下,是一种约定俗成的泛型写法。

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

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() 方法的泛型指定成数字类型,比如 IntFloatDouble等。但是如果你指定成字符串类型,就肯定会报错,因为它不是一个数字。

        另外,在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型的上界默认是 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 属性赋值时,就会调用 Delegate 类的 setValue()方法,当获取 MyClass 属性的值时,就会调用 Delegate 类的 getValue() 方法。是不是很好理解?

        不过,其实还存在一种情况可以不用在 Delegate 类中实现 setValue()方法,那就是 MyClass中的 p 属性是使用 val 关键字声明的。这一点也很好理解,如果 属性是使用 val 关键 字声明的,那么就意味着 属性是无法在初始化之后被重新赋值的,因此也就没有必要实现 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 对象,当我们调用 属性的时候,其实调用的是 Delegate 对象的 getValue() 方法,然后 getValue() 方法中又会调用 lazy 函数传入的 Lambda 表达式,这样表达式中的代码就可以得到执行了,并且调用 属性后得到的值就是 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 多媒体方面的知识吧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值