Kotlin Jetpack 实战|05. Kotlin 泛型

3,132 阅读10分钟

往期文章

《Kotlin Jetpack 实战:开篇》

《00. 写给 Java 开发者的 Kotlin 入坑指南》

《01. 从一个膜拜大神的 Demo 开始》

《02. Kotlin 写 Gradle 脚本是一种什么体验?》

《03. Kotlin 编程的三重境界》

《04. Kotlin 高阶函数》

前言

这是一篇文科生都能读泛型入门教程。(亲测,我女朋友都能看懂。)

本文以故事的形式介绍 Kotlin 泛型及其不变性声明处型变使用处型变,最后再搭配一个实战环节,将泛型应用到我们的 Demo 当中来。

前期准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开: github.com/chaxiu/Kotl…
  • 切换到分支:chapter_05_generics
  • 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓

正文

1. 遥控器的故事:泛型

女朋友:好想要一个万能遥控器啊。

我:要不我教你用 Kotlin 的泛型实现一个吧!

女朋友:切,又想忽悠我学 Kotlin。[白眼]

我:真的很简单,保证你一看就会。

1-1 泛型类

我:这是一个万能遥控器,它带有一个泛型参数

//          类的泛型参数(形参)
//               ↓
class Controller<T>() {
	fun turnOn(obj: T){ ... }
	fun turnOff(obj: T){ ... }
}

我:它用起来也简单,想控制什么,把对应的泛型传进去就行,就跟选模式一样:

//                    电视机作为泛型实参
//                           ↓
val tvController: Controller<TV> =  Controller<TV>()
val tv = TV()
// 控制电视机
tvController.turnOn(tv)
tvController.turnOff(tv)

//                      电风扇作为泛型实参
//                             ↓
val fanController: Controller<Fan> =  Controller<Fan>()
val fan = Fan()
// 控制电风扇
fanController.turnOn(fan)
fanController.turnOff(fan)

借助 Kotlin 的顶层函数,Controller 类甚至都可以省掉,直接用泛型函数:

1-2 泛型函数

//     函数的泛型参数
//   ↓              ↓
fun <T> turnOn(obj: T){ ... }
fun <T> turnOff(obj: T){ ... }

泛型函数用起来也简单:

// 控制电视
val tv = TV()
turnOn<TV>(tv)
turnOff<TV>(tv)

// 控制风扇
val fan = Fan()
turnOn<Fan>(fan)
turnOff<Fan>(fan)

女朋友:我知道怎么用啦!是不是这样?

val boyFriend = BoyFriend()
turnOff<BoyFriend>(boyFriend)

我:……


2. 招聘的故事:泛型的不变性(Invariant)

女朋友:我想招几个大学生做兼职,你推荐几个大学吧。

我:好嘞,不过我要通过 Kotlin 泛型来给你推荐。

女朋友:呃……刚才你讲的泛型还挺简单,这次有什么新花样吗?

我:你看下去就知道了。

我:先来点准备工作:

// 学生
open class Student()
// 女学生
class FemaleStudent: Student()

// 大学
class University<T>(val name: String) {
    // 往外取,代表招聘
    fun get(): T { ... }
    fun put(student: T){ ... }
}

我:你的招聘需求可以用这样的代码描述:

//                                  注意这里
// 女朋友需要一个大学(变量声明)           ↓
lateinit var university: University<Student>

//                      注意这里
// 我随便推荐一个大学         ↓
university = University<Student>("某大学")
val student: Student = university.get()// 招聘

女朋友:原来 Kotlin 也没那么难……

女朋友:能赋值一个"女子大学"吗?

我:不行,会报错。

//                                  注意这里
//                                     ↓
lateinit var university: University<Student>
//                      这是报错的原因
//                           ↓
university = University<FemaleStudent>("女子大学")
val student: Student = university.get()

// 编译器报错!!
/*
Type mismatch.
Required:   University<Student>
Found:      University<FemaleStudent>
*/ 

女朋友:什么鬼。。。

我:虽然 Student 和 FemaleStudent 之间是父子关系,但是 University<Student> 和 University<FemaleStudent> 之间没有任何关系。这叫泛型的不变性。

女朋友:这不合理!女子大学招聘出来的学生,难道就不是学生?

我:招聘当然符合逻辑,但别忘了 University 还有一个 put 方法。

我:你怎么防止别人把一个男学生放到女子大学里去?

我:让我们看看如果可以将“女子大学”当作“普通大学”用,会出现什么问题:

//              声明的类型是:普通大学,然而,实际类型是:女子大学。
//                             ↓                      ↓
var university: University<Student> = University<FemaleStudent>("女子大学")

val maleStudent: Student = Student()
// 男学生被放进女子大学!不合理。
university.put(maleStudent)

女朋友:明白了,原来这就是泛型不变性的原因,确实能避免不少麻烦。

// 默认情况下,编译器只允许这么做
// 声明的泛型参数与实际的要一致
                                    ↓                     ↓
var normalUniversity: University<Student> = University<Student>

                                  ↓                           ↓
var wUniversity: University<FemaleStudent> = University<FemaleStudent>

3. 搞定招聘:泛型的协变(Covariant)

女朋友:如果我把 University 类里面的 put 方法删掉,是不是就可以用“女子大学”赋值了?这样就不用担心把男学生放到女子大学的问题了。

我:这还不够,还需要加一个关键字 out 告诉编译器:我们只会从 University 类往外取,不会往里面放。这时候,University<FemaleStudent> 就可以当作 University<Student> 的子类。

我:这叫做泛型的协变

open class Student()
class FemaleStudent: Student()

//              看这里
//                ↓
class University<out T>(val name: String) {
    fun get(): T { ... }
}

女朋友:我试试,果然好了!

// 不再报错
var university: University<Student> = University<FemaleStudent>("女子大学")
val student: Student = university.get()

我:你不来写代码真浪费了。


4. 填志愿的故事:泛型的逆变(Contravariant)

女朋友:我妹妹刚高考完,马上要填志愿了,你给推荐个大学吧。

我:咱刚看过泛型协变,要不你试试自己解决这个填志愿的问题?正好 University 里有个 put 方法,你就把 put 当作填志愿就行了。

女朋友:那我依葫芦画瓢试试…… 给我妹妹报一个女子大学。

open class Student()
class FemaleStudent: Student()

class University<T>(val name: String) {
    fun get(): T { ... }
    // 往里放,代表填志愿
    fun put(student: T){ ... }
}

val sister: FemaleStudent = FemaleStudent()
val university: University<FemaleStudent> = University<FemaleStudent>("女子大学")
university.put(sister)//填报女子大学

女朋友:完美!

我:厉害。

女朋友:能不能再报一个普通综合大学?

我:不行,你忘记泛型不变性了吗?

val sister: FemaleStudent = FemaleStudent()
//          报错原因:声明类型是:女子大学      赋值的类型是:普通大学
//                               ↓                        ↓  
val university: University<FemaleStudent> = University<Student>("普通大学")
university.put(sister)

// 报错
/*
Type mismatch.
Required:   University<FemaleStudent>
Found:      University<Student>
*/

女朋友:我妹能报女子大学,居然不能报普通的综合大学?这不合理吧!

我:你别忘了 University 还有一个 get 方法吗?普通综合大学 get 出来的可不一定是女学生。

女朋友:哦。那我把 get 方法删了,再加个关键字?

我:对。删掉 get 方法,再加一个关键字:in 就行了。它的作用是告诉编译器:我们只会往 University 类里放,不会往外取。这时候,University<Student> 就可以当作 University<FemaleStudent> 的子类。

我:这其实就叫做泛型的逆变,它们的继承关系反过来了。

//              看这里
//                ↓
class University<in T>(val name: String) {
    fun put(student: T){ ... }
}

val sister: FemaleStudent = FemaleStudent()
// 编译通过
val university: University<FemaleStudent> = University<Student>("普通大学")
university.put(sister)

女朋友:泛型还挺有意思。

我:上面提到的协变逆变。它们都是通过修改 University 类的泛型声明实现的,所以它们统称为:声明处型变,这是 Kotlin 才有的概念,Java 中没有。


5. 使用处型变(Use-site Variance)

女朋友:万一 University 是第三方提供的,我们无法修改,怎么办?能不能在不修改 University 类的前提下实现同样的目的?

我:可以,这就要用到使用处型变了。他们也分为:使用处协变使用处逆变

open class Student()
class FemaleStudent: Student()

// 假设 University 无法修改
class University<T>(val name: String) {
    fun get(): T { ... }
    fun put(student: T){ ... }
}

5-1 使用处协变

我:在泛型的实参前面增加一个 out 关键字,代表我们只会从 University 往外取,不会往里放。这么做就实现了 使用处协变

//                                         看这里
//                                           ↓
fun useSiteCovariant(university: University<out Student>) {
    val femaleStudent: Student? = university.get()

    // 报错: Require Nothing? found Student?
    // university.put(femaleStudent)
}

女朋友:这也挺容易理解的。那使用处逆变呢?加个 in

5-2 使用处逆变

我:对。在泛型的实参前面增加一个 in 关键字,代表我们只会从 University 往里放,不会往外取。这么做就实现了 使用处逆变

//                                               看这里
//                                                 ↓
fun useSiteContravariant(universityIn: University<in FemaleStudent>) {
    universityIn.put(FemaleStudent())

    // 报错: Require FemaleStudent? found Any?
    // val femaleStudent: FemaleStudent? = universityIn.get()
}

女朋友:思想是一样的。

女朋友:如果是从 University 招聘学生,就是往外取,这种情况下就是协变,可以用 University<FemaleStudent> 替代 University<Student>,因为女子大学取出来的女学生,和普通大学取出来的学生,都是学生。

女朋友:如果是 University 要招生,就是往里放,这种情况下,就只能用 University<Student> 替代 University<FemaleStudent>,因为普通大学的招生范围更广,女子大学能接收的学生,普通大学也接收。

我:你总结的真好。顺便提一句:Kotlin 的使用处型变,还有个名字叫:类型投影(Type Projections),这名字真烂。

以上代码的具体细节可以看我这个 GitHub Commit

5-3 Kotlin 和 Java 对比

我:既然你 Kotlin 泛型理解起来毫无压力,那我再给你给加个餐,对比一下 Java 的使用处型变

女朋友:呃…… Java 是啥玩意?

我:没事,你就当看个乐呵。

使用处协变使用处逆变
KotlinUniversity<out Student>University<in FemaleStudent>
JavaUniversity<? extends Student>University<? super FemaleStudent>

我:是不是简单明了?

女朋友:还是 Kotlin 的容易理解:out 代表只能往外取(get),in代表只能往里放(put)。

我:没错。

女朋友:对比起来,Java 的表达方式真是无力吐槽。(-_-)

//     Java 这辣鸡协变语法
//             ↓
University<? extends Student> covariant = new University<FemaleStudent>("女子大学");
Student student = covariant.get();
// 报错
covariant.put(student);

//     Java 这辣鸡逆变语法
//             ↓
University<? super FemaleStudent> contravariant = new University<Student>("普通大学");
contravariant.put(new FemaleStudent())
// 报错
Student s = contravariant.get();

以上代码的具体细节可以看我这个 GitHub Commit


6. Kotlin 泛型实战

我:这里有一个 Kotlin 的 Demo,要不你来看看有哪些地方能用泛型优化的?

女朋友:过分了啊!你让我学 Kotlin 就算了,还想让我帮你写代码?

女朋友:你来写,我来看。

我:呃……听领导的。

6-1 泛型版本的 apply 函数

我:这是上一个章节里的代码,这个 apply 函数其实可以用泛型来简化,让所有的类都能使用。


//  替代               替代              替代
//   ↓                 ↓                 ↓
fun User.apply(block: User.() -> Unit): User{
    block()
    return this
}

user?.apply { this: User ->
    ...
    username.text = this.name
    website.text = this.blog
    image.setOnClickListener { gotoImagePreviewActivity(this) }
}

我:使用泛型替代以后的 apply 函数就是这样:


//   泛型              泛型           泛型
//   ↓  ↓              ↓              ↓
fun <T> T.apply(block: T.() -> Unit): T{
    block()
    return this
}

女朋友:Kotlin 官方的 apply 函数也是这么实现的吗?

我:几乎一样,它只是多了个 contract,你暂时还不懂。

女朋友:呃……还有其他例子吗?

6-2 泛型版本的 HTML 构建器

我:在上一个章节里,我实现了一个简单的 类型安全的 HTML 构建器,其中有不少重复的代码。

女朋友:咱们可以利用泛型消灭重复代码,对吧?

我:没错。

class Body : BaseElement("body") {
    fun h1(block: () -> String): H1 {
        val content = block()
        val h1 = H1(content)
        this.children += h1
        return h1
    }
//            ↑
//      看看这重复的模板代码
//            ↓
    fun p(block: () -> String): P {
        val content = block()
        val p = P(content)
        this.children += p
        return p
    }
}
//            ↑
//      看看这重复的模板代码
//            ↓
class Head : BaseElement("head") {
    fun title(block: () -> String): Title {
        val content = block()
        val title = Title(content)
        this.children += title
        return title
    }
}

我:让我们用泛型来优化:


open class BaseElement(var name: String, var content: String = "") : Element {
    // 在父类增加一个共有的泛型方法
    protected fun <T : BaseElement> initString(element: T, init: T.() -> String): T {
        val content = element.init()
        element.content = content
        children.add(element)
        return element
    }
}

class Body : BaseElement("body") {
    fun h1(block: H1.() -> String) = initString(H1("h1"), block)
    fun p(block: P.() -> String) = initString(P("p"), block)
}

class Head : BaseElement("head") {
    fun title(block: Title.() -> String) = initString(Title(), block)
}

我:还有一个地方有重复代码:

class HTML : BaseElement("html") {
    fun head(block: Head.() -> Unit): Head {
        val head = Head()
        head.block()
        this.children += head
        return head
    }
//            ↑
//      看看这重复的模板代码
//            ↓
    fun body(block: Body.() -> Unit): Body {
        val body = Body()
        body.block()
        this.children += body
        return body
    }
}

我:优化后:

open class BaseElement(var name: String, var content: String = "") : Element {
    // 在父类增加一个共有的泛型方法
    protected fun <T : Element> init(element: T, init: T.() -> Unit): T {
        element.init()
        children.add(element)
        return element
    }
}

class HTML : BaseElement("html") {
    fun head(block: Head.() -> Unit) = init(Head(), block)
    fun body(block: Body.() -> Unit) = init(Body(), block)
}

女朋友:嗯,顺眼了很多!

以上代码的具体细节可以看我这个 GitHub Commit

7. 总结

  • 受限于篇幅,Kotlin 泛型剩余知识点留到以后再讲,本文作为入门暂时够用了。泛型要讲透彻得写一本书,这不是本文的目的。
  • 泛型的思想是一样的,理解了 Kotlin 型变,迁移到 Java 也是一样的。
  • Kotlin 的泛型,由于借鉴了别的语言(C#),所以理解起来其实要比 Java 简单很多。
  • 文章看完了,快去敲代码吧:github.com/chaxiu/Kotl…
  • 找这个分支:chapter_05_generics

8. 思考题:

Kotlin 的声明处型变使用处型变它们分别有哪些优势和劣势?


都看到这了,给点个赞呗!

回目录-->【Kotlin Jetpack 实战】