Kotlin核心编程 下水篇:kotlin核心(第7章)

打印 上一主题 下一主题

主题 1659|帖子 1659|积分 4977

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
第7章 多态和扩展

上一章我们了解了Kotlin中集合的构造及操作,相信你们已经能感受到函数式操作的强大。Kotlin作为一门工程化的语言,拥有一些令开发者心旷神怡的特性。本章将带领各人一步步了解Kotlin中一个比力告急的特性——扩展(Extensions)。Kotlin的扩展实在是多态的一种表现情势,在深入了解扩展之前,让我们先探究一下多态的不同技术手段。
7.1 多态的不同方式

熟悉Java的读者对多态应该不会陌生,它是面向对象程序设计(OOP)的一个告急特性。当我们用一个子类继承一个父类的时候,这就是子范例多态(Subtype polymorphism)。另一种熟悉的多态是参数多态(Parametric polymorphism),我们在第5章所讨论的泛型就是其最常见的情势。别的,大概你还会想到C++中的运算符重载,我们可以用特设多态(Ad-hoc polymorphism)来形貌它。相比子范例多态和参数多态,大概你对特设多态会感到有些许陌生。实在这是一种更加机动的多态技术,在Kotlin中,一些风趣的语言特性,如运算符重载、扩展都很好地支持这种多态。在本节接下来的内容中,我们将通过具体的例子来进一步展现各种多态的特点,并先容更加深入的Kotlin语言特性。
7.1.1 子范例多态

无论在前端、移动端照旧背景开发中,数据持久化操作都是必不可少的。在Android中,原生就支持Sqlite的操作,一般我们会继承Sqlite操作的相干类:
  1. class CustomerDatabaseHelper(context:Context): SQLiteOpenHelper(context, "kotlinDemo.db", cursorFactory , db.version){
  2.     override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {}
  3.     override fun onCreate(db: SQLiteDatabase) {
  4.         val sql = "CREATE TABLE if not exists $tableName ( id integer PRIMARY KEY autoincrement, uniqueKey VARCHAR(32))" // 此处省略其他参数
  5.         db.execSQL(sql)
  6.     }
  7. }
复制代码
然后我们就可以使用父类DatabaseHelper的所有方法。这种用子范例替换超范例实例的举动,就是我们通常说的子范例多态。
7.1.2 参数多态

在完成数据库的创建之后,现在我们要把客户(Customer)存入客户端数据库中。大概会写这样一个方法:
  1. fun persist(customer: Customer) {
  2.     db.save(customer.uniqueKey, customer)
  3. }
复制代码
如果代码成功实行,我们就成功地将customer以键值对的方式存入数据库(上述例子中以uniqueKey对应customer,便于查询等操作)​。
但是,随着需求的变动,我们大概还会持久化多种范例的数据。如果每种范例都写一个presist方法,多少有些烦琐,通常我们会抽象一个方法来处置惩罚不同范例的持久化。因为我们接纳键值对的方式存储,以是需要获取不同范例对应的uniqueKey:
  1. interface KeyI {
  2.     val uniqueKey : String
  3. }
  4. class ClassA(override val uniqueKey: String) : KeyI {
  5.     …
  6. }
  7. class ClassB(override val uniqueKey: String) : KeyI {
  8.     …
  9. }
复制代码
这样,class A、B都已经具备uniqueKey。我们可以将persist举行如下改写:
  1. fun <T: KeyI> persist(t: T) {
  2.     db.save(t.uniqueKey, t)
  3. }
复制代码
以上的多态情势我们可以称之为参数多态,实在最常见的参数多态的情势就是泛型
参数多态在程序设计语言与范例论中是指声明与定义函数、复合范例、变量时不指定其具体的范例,而把这部门范例作为参数使用,使得该定义对各种具体范例都实用,以是它建立在运行时的参数基础上,并且所有这些都是在不影响范例安全的前提下举行的。
7.1.3 对第三方类举行扩展

进一步思考,倘使当对应的业务类ClassA、ClassB是第三方引入的,且不可被修改时,如果我们要想给它们扩展一些方法,比如将对象转化为Json,使用之前先容的多态技术就会显得比力贫苦。
荣幸的是,Kotlin支持扩展的语法,使用扩展我们就能给ClassA、ClassB添加方法或属性,从而换一种思绪来办理上面的问题。
  1. fun ClassA.toJson(): String = {
  2.     ……
  3. }
复制代码
如上我们给ClassA类扩展了一个将对象转换为Json的toJson方法。需要注意的是,扩展属性和方法的实现运行在ClassA实例,它们的定义操作并不会修改ClassA类本身。这样就为我们带来了一个很大的好处,即被扩展的第三方类免于被污染,从而避免了一些因父类修改而大概导致子类堕落的问题发生。
当然,在Java中我们可以依靠其他的办法比如设计模式来办理,但相较而言依靠扩展的方案显得更加方便且合理,这实在也是另一种被称为特设多态的技术。下节我们就来了解下这种多态,然而再先容Kotlin中别的一种同样可服务于它的语言特性——运算符重载
7.1.4 特设多态与运算符重载

除了子范例多态、参数多态以外,还存在一种更机动的多态情势——特设多态(Ad-hoc polymorphism)。
大概你对特设多态这个概念并不是很了解,我们来举一个具体的例子。当你想定义一个通用的sum方法时,大概会在Kotlin中这么写:
  1. fun <T> sum(x: T, y: T) : T = x + y
复制代码
但编译器会报错,因为某些范例T的实例不一定支持加法操作,而且如果针对一些自定义类,我们更希望可以或许实现各自定制化的“加法语义上的操作”​。如果把参数多态做的事情打个比方:它提供了一个工具,只要一个东西能“切”​,就用这个工具来切割它。然而,实际中不是所有的东西都能被切,而且材料也不一定类似。更加合理的方案是,你可以根据不同的原材料来选择不同的工具来切它。再换种思绪,我们可以定义一个通用的Summable接口,然后让需要支持加法操作的类来实现它的plusThat方法。就像这样子:
  1. interface Sumable<T> {
  2.     fun plusThat(that: T): T
  3. }
  4. data class Len(val v: Int) : Sumable<Len> {
  5.     override fun plusThat(that: Len) = Len(this.v + that.v)
  6. }
复制代码
可以发现,当我们在自定义一个支持plusThat方法的数据结构如Len时,这种做法并没有什么问题。然而,如果我们要针对不可修改的第三方类扩展加法操作时,这种通过子范例多态的技术手段也会遇到问题。
于是,你又想到了Kotlin的扩展,我们要引出另一种叫作“特设多态”的技术了。相比更通用的参数多态,特设多态提供了“量身定制”的能力。参考它的定义,特设多态可以理解为:一个多态函数是有多个不同的实现,依靠于实在参而调用相应版本的函数。
针对以上的例子,我们完全可以接纳扩展的语法来办理问题。别的,Kotlin原生支持了一种语言特性来很好地办理问题,这就是运算符重载。借助这种语法,我们可以完善地实现需求。代码如下:
  1. data class Area(val value: Double)
  2. operator fun Area.plus(that: Area): Area {
  3.     return Area(this.value + that.value)
  4. }  
  5. fun main(args: Array<String>) {
  6.     println(Area(1.0) + Area(2.0)) // 运行结果:Area(value=3.0)
  7. }
复制代码
下面我们来具体先容下Kotlin中运算符重载的语法。相信你已经注意到了operator关键字,以及Kotlin中内置可重载的运算符plus。先来看看operator,它的作用是:将一个函数标记为重载一个操作符或者实现一个约定。
注意,这里的plus是Kotlin规定的函数名。除了重载加法,我们还可以通过重载减法(minus)、乘法(times)、除法(div)、取余(mod)(Kotlin1.1版本开始被rem替代)等函数来实现重载运算符。别的,你可以再追念一下第2章中遇到的一些基础语法,它们也是使用这种神奇的语言特性来实现的,如:
  1. a in b // 转换为 b.contains(a)
  2. f(a)   // 转换为 f.invoke(a)
复制代码
我们将在第9章中展示怎样使用Kotlin运算符重载的语法,来简化经典的设计模式。
7.2 扩展:为别的类添加方法 属性

在上一节中,你已经了解到,扩展是Kotlin实现特设多态的一种非常告急的语言特性。在本节中,我们将继续探究这种技术。
7.2.1 扩展与开放封闭原则

开发者而言,业务需求总是在不断变动的。熟悉设计模式的读者应该知道,在修改现有代码的时候,我们应该遵循开放封闭原则,即:软件实体应该是可扩展,而不可修改的。也就是说,对扩展开放,而对修改是封闭的
开放封闭原则概念

开放封闭原则(OCP,Open Closed Principle)是所有面向对象原则的核心。软件设计本身所追求的目的就是封装变革、降低耦合,而开放封闭原则正是对这一目的的最直接体现。其他的设计原则,许多时候是为实现这一目的服务的,例如以替换原则实现最佳的、精确的继承层次,就能包管不会违反开放封闭原则。
实际情况并不乐观,比如在举行Android开发的时候,为了实现某个需求,我们引入了一个第三方库。但某一天需求发生了变动,当前库无法满足,且库的作者临时没有升级的操持。这时候大概你就会开始尝试对库源码举行修改。这就违反了开放封闭原则。随着需求的不断变动,问题大概就会如滚雪球般增长。
Java中一种惯常的应对方案是让第三方库类继承一个子类,然后添加新功能。然而,正如我们在第3章中评论过的那样,强行的继承大概违反“里氏替换原则”​。
更合理的方案是依靠扩展这个语言特性。Kotlin通过扩展一个类的新功能而无须继承该类,在大多数情况下都是一种更好的选择,从而我们可以合理地遵循软件设计原则。
7.2.2 使用扩展函数 属性

扩展函数的声明非常简单,它的关键字是<Type>。别的我们需要一个“接收者范例(recievier type)”​(通常是类或接口的名称)来作为它的前缀。
以MutableList<Int>为例,我们为其扩展一个exchange方法,代码如下:
  1. fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
  2.     val tmp = this[fromIndex]
  3.     this[fromIndex] = this[toIndex]
  4.     this[toIndex] = tmp
  5. }
复制代码
MutableList<T>是Kotlin尺度库Collections中的List容器类,这里作为recievier type,exchange是扩展函数名。其余和Kotlin声明一个普通函数并无区别。
Kotlin的this要比Java更机动,这里扩展函数体里的this代表的是接收者范例的对象
这里需要注意的是:Kotlin严格区分了接收者是否可空。如果你的函数是可空的,你需要重写一个可空范例的扩展函数。
我们可以非常方便地对该函数举行调用,代码如下:
  1. val list = mutableListOf(1,2,3)
  2. list.exchange(1,2)
复制代码
1.扩展函数的实现机制

扩展函数的使用云云方便,会不会对性能造成影响呢?为了办理这个疑惑,我们有必要对Kotlin扩展函数的实现举行探究。我们以之前的MutableList<Int>.exchange为例,它对应的Java代码如下:
  1. import java.util.List;
  2. import kotlin.Metadata;
  3. import kotlin.jvm.internal.Intrinsics;
  4. import org.jetbrains.annotations.NotNull;
  5. @Metadata(
  6.     mv = {1, 1, 1},
  7.     bv = {1, 0, 0},
  8.     k = 2,
  9.     d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"},
  10.     d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "production sources for module FPKotlin"}
  11. )
  12. public final class ExSampleKt {
  13.     public static final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) {
  14.         Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
  15.         int tmp = ((Number)$receiver.get(fromIndex)).intValue();
  16.         $receiver.set(fromIndex, $receiver.get(toIndex));
  17.         $receiver.set(toIndex, Integer.valueOf(tmp));
  18.     }
  19. }
复制代码
联合以上Java代码可以看出,我们可以将扩展函数近以理解为静态方法。而熟悉Java的读者应该知道静态方法的特点:它独立于该类的任何对象,且不依靠类的特定实例,被该类的所有实例共享。别的,被public修饰的静态方法本质上也就是全局方法。
综上所述,我们可以得出结论:扩展函数不会带来额外的性能消耗。
2.扩展函数的作用域

既然扩展函数不会带来额外的性能消耗,那我们就可以放心地使用它。它的作用域范围是怎么样的呢?一般来说,我们习惯将扩展函数直接定义在包内,例如之前的exchange例子,我们可以将其放在com.example.extension包下:
  1. package com.example.extensionfun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
  2.     val tmp = this[fromIndex]
  3.     this[fromIndex] = this[toIndex]
  4.     this[toIndex] = tmp
  5. }
复制代码
我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。除此之外,实际开发时我们也大概会将扩展函数定义在一个Class内部统一管理。
  1. class Extends {
  2.         fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
  3.             val tmp = this[fromIndex]
  4.             this[fromIndex] = this[toIndex]
  5.             this[toIndex] = tmp
  6.     }
  7. }
复制代码
当扩展函数定义在Extends类内部时,情况就与之前不一样了:这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)​。你大概会猜想,是不是它被声明为private方法了?那我们尝试在exchange方法前加上public关键字:
  1. public fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) { … }
复制代码
结果不尽如人意,此时我们依旧无法调用到(实际上Kotlin中成员方法默认就是用public修饰的)​。是什么原因呢?借助IDEA我们可以查看到它对应的Java代码,这里展示关键部门:
  1. public static final class Extends {
  2.     public final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) {
  3.         Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
  4.         int tmp = ((Number)$receiver.get(fromIndex)).intValue();
  5.         $receiver.set(fromIndex, $receiver.get(toIndex));
  6.         $receiver.set(toIndex, Integer.valueOf(tmp));
  7.     }
  8. }
复制代码
我们看到,exchange方法上已经没有static关键字的修饰了。以是当扩展方法在一个Class内部时,我们只能在该类和该类的子类中举行调用。别的你大概还会想到:如果我用private修饰这个扩展函数,又会有什么结果?这个问题留给读者自行探究。
3.扩展属性

与扩展函数类似,我们还能为一个类添加扩展属性。比如我们想给MutableList<Int>添加判断一个判断和是否为偶数的属性sumIsEven:
  1. val MutableList<Int>.sumIsEven: Boolean
  2.     get() = this.sum() % 2 == 0
复制代码
但是,如果你准备给这个属性添加上默认值,并且写出如下代码:
  1. // 编译错误:扩展属性不能有初始化器
  2. val MutableList<Int>.sumIsEven: Boolean = false
  3.     get() = this.sum() % 2 == 0
复制代码
以上代码编译不能通过,这是为什么呢?
实在,与扩展函数一样,其本质也是对应Java中的静态方法(我们反编译成Java代码后可以看到一个getSumIsEven的静态方法,与扩展函数类似)​。由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器的原因。它们的举动只能由显式提供的getters和setters定义。
   幕后字段
  在Kotlin中,如果属性中存在访问器使用默认实现,那么Kotlin会主动提供幕后字段filed,其仅可用于自定义getter和setter中。
  7.2.3 扩展的特殊情况

颠末上一节,我们对Kotlin的扩展函数已经有了根本的认识,相信大部门读者已经被扩展函数所吸引,并且已经想好怎样使用扩展函数举行实战。但在此之前,照旧让我们先看一些扩展中特殊的情况,或者说是扩展的范围之处。
1.类似Java的静态扩展函数

在Kotlin中,如果你需要声明一个静态的扩展函数,开发者必须将其定义在伴生对象(companion object)上。以是我们需要这样定义带有伴生对象的类:
  1. class Son {
  2.     companion object {
  3.         val age = 10
  4.     }
  5. }
复制代码
Son类中已经有一个伴生对象,如果我们现在不想在Son中定义扩展函数,而是在Son的伴生对象上定义,可以这么写:
  1. fun Son.Companion.foo() {
  2.     println("age = $age")
  3. }
复制代码
这样,我们就能在Son没有实例对象的情况下,也能调用到这个扩展函数,语法类似于Java的静态方法。
  1. object Test {
  2.     @JvmStatic
  3.     fun main(args: Array<String>) {
  4.         Son.foo()
  5.     }
  6. }
复制代码
一切看起来都很顺利,但是当我们想让第三方类库也支持这样的写法时,我们发现,并不是所有的第三方类库中的类都存在伴生对象,我们只能通过它的实例来举行调用,但这样会造成许多不必要的贫苦。
2.成员方法优先级总高于扩展函数

已知我们有如下类:
  1. class Son {
  2.     fun foo() = println("son called member foo")
  3. }
复制代码
它包罗一个成员方法foo(),假如我们哪天心血来潮,想对这个方法做特殊实现,使用扩展函数大概会写出如下代码:
  1. fun Son.foo() = println("son called extention foo")
  2. object Test {
  3.     @JvmStatic
  4.     fun main(args: Array<String>) {
  5.         Son().foo()
  6.     }
  7. }
复制代码
在我们的预期中,我们希望调用的是扩展函数foo(),但是输出结果为:son called member foo。这表明:当扩展函数和现有类的成员方法同时存在时,Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么照旧调用到了旧的方法?
但是换一个角度思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成肴杂。对于第三方类库来说甚至是一场劫难:我们把不应该更改的方法改变了。以是在使用时,我们必须注意:同名的类成员方法的优先级总高于扩展函数
3.类的实例与接收者的实例

前面的例子中提到过,我们发现Kotlin中的this比在Java中更机动。以扩展函数为例,当在扩展函数里调用this时,指代的是接收者范例的实例。那么如果这个扩展函数声明在一个object内部,我们怎样通过this获取到类的实例呢?参考如下代码:
  1. class Son{
  2.     fun foo(){
  3.         println("foo in Class Son")
  4.     }
  5. }
  6. object Parent {
  7.     fun foo() {
  8.         println("foo in Class Parent")
  9.     }
  10.     @JvmStatic
  11.     fun main(args: Array<String>) {
  12.         fun Son.foo2() {
  13.             this.foo()
  14.             this@Parent.foo()
  15.         }
  16.         Son().foo2()
  17.     }
  18. }
复制代码
这里我们可以用this@类名来强行指定调用的this。别的值得一提的是:如果Son扩展函数在Parent类内,我们将无法对其调用。
  1. class Son{
  2.     fun foo(){
  3.         println("foo in Class Son")
  4.     }
  5. }
  6. class Parent {
  7.     fun foo() {
  8.         println("foo in Class Parent")
  9.     }
  10.     fun Son.foo2() {
  11.         this.foo()
  12.         this@Parent.foo()
  13.     }
  14. }
  15. object Test {
  16.     @JvmStatic
  17.     fun main(args: Array<String>) {
  18.         Son().foo2()
  19.     }
  20. }
复制代码
这是为什么呢?来看看Parent对应的Java代码,以下为核心部门:
  1. public final class Parent {
  2.     public final void foo() {
  3.         String var1 = "foo in Class Parent";
  4.         System.out.println(var1);
  5.     }
  6.     public final void foo2(@NotNull Son $receiver) {
  7.         Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
  8.         $receiver.foo();
  9.         this.foo();
  10.     }
  11. }
复制代码
纵然我们设置访问权限为public,它也只能在该类或者该类的子类中被访问,如果我们设置访问权限为private,那么在子类中也不能访问这个扩展函数。原因在7.2.2节中已经先容。
7.2.4 尺度库中的扩展函数:run let also takeIf

本节让我们看看Kotlin尺度库中使用扩展实现的几个函数。
Kotlin尺度库中有一些非常实用的扩展函数,除了之前我们打仗过的apply、with函数之外,我们再来了解下let、run、also、takeIf。
1.run

先来看下run方法,它是使用扩展实现的,定义如下:
  1. public inline fun <T, R> T.run(block: T.() -> R): R = block()
复制代码
简单来说,run是任何范例T的通用扩展函数,run中实行了返回范例为R的扩展函数block,终极返回该扩展函数的结果。
在run函数中我们拥有一个单独的作用域,可以或许重新定义一个nickName变量,并且它的作用域只存在于run函数中。
  1. fun testFoo() {
  2.     val nickName = "Prefert"
  3.     run {
  4.         val nickName = "YarenTang"
  5.         println(nickName) // YarenTang
  6.     }
  7.     println(nickName)     // Prefert
  8. }
复制代码
这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。
例如现在有这么一个场景:用户点击领取新人嘉奖的按钮,如果用户此时没有登录则弹出loginDialog,如果已经登录则弹出领取嘉奖的getNewAccountDialog。我们可以使用以下代码来处置惩罚这个逻辑:
  1. run {
  2.     if (!islogin) loginDialog else getNewAccountDialog
  3. }.show()
复制代码
2.let

在第5章先容可空范例的时候,我们打仗了let方法,来看看它的定义:
  1. public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
复制代码
我们在第6章对apply举行过先容,let和apply类似,唯一不同的是返回值:apply返回的是原来的对象,而let返回的是闭包里面的值。细心的读者应该察觉到,我们在第5章先容可空范例的时候,大量使用了let语法,简单回顾一下:
  1. data class Student(age: Int)
  2. class Kot {
  3.     val student: Student? = getStu()
  4.     fun dealStu() {
  5.         val result = student?.let {
  6.             println(it.age)
  7.             it.age
  8.         }
  9.     }
  10. }
复制代码
由于let函数返回的是闭包的最后一行,当student不为null的时候,才会打印并返回它的年龄。与run一样,它同样限制了变量的作用域。
3.also

also是Kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版。
  1. public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
复制代码
与apply一致,它的返回值是该函数的接收者:
  1. class Kot {
  2.     val student: Student? = getStu()
  3.     var age = 0
  4.     fun dealStu() {
  5.         val result = student?.also { stu ->
  6.             this.age += stu.age
  7.             println(this.age)
  8.             println(stu.age)
  9.             this.age
  10.         }
  11.     }
  12. }
复制代码
我将它的隐式参数指定为stu,假设student?不为空,我们会发现返回了student,并且总年龄age增加了。
值得注意的是:如果使用apply,由于它内部是一个扩展函数,this将指向stu而不是Kot,此处我们将无法调用到Kot下的age。
4.takeIf

如果我们不仅仅只想判空,还想加入条件,这时let大概显得有点不敷。让我们来看看takeIf。
  1. public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null
复制代码
这个函数也是在Kotlin1.1中新增的。当接收器满足某些条件时它才会实行。如果我们想对成年的学生操作,可以这样写:
  1. val result = student.takeIf { it.age >= 18 }.let { ... }
复制代码
我们发现,这与第6章集合中的filter异曲同工,不过takeIf只操作单条数据。与takeIf相反的还有takeUnless,即接收器不满足特定条件才会实行。
除了这些函数外,Kotlin尺度库中还有许多方便的扩展函数,由于篇幅限制,这些兴趣留给读者自行探索。
7.3 Android中的扩展应用

纸上得来终觉浅,本节我们将通过几个Android中的例子,带领读者一起来感受实际开发中扩展函数带来的便捷。
7.3.1 优化Snackbar

让我们看一下实际项目中常用的例子:Snackbar。几年前,它被添加到Android支持库中,以代替作为用户和应用程序之间的消息通报接口恒久服务的Toast。它办理了一些问题并引入了一种全新的外观,根本使用方式如下:
  1. Snackbar.make(parentView, message_text, duration)
  2.     .setAction(action_text, click_listener)
  3.     .show();
复制代码
但是实际中使用它的API会给代码增加不必要的复杂性:我们不希望每次都定义我们想要显示消息的时间,并且在添补一堆参数后,为什么我们还要额外调用show()?
著名的开源项目Anko拥有Snackbar的辅助函数,使其更易于使用并使代码更简便:
  1. snackbar(parentView, action_text, message_text) { click_listener }
复制代码
此中一些参数是可选的,以是我们一般这么使用:
  1. snackbar(parentView, "message")
复制代码
anko中的snackbar的部门源码如下:
  1.   inline fun View.snackbar(message: Int, @StringRes actionText: Int, noinline action: (View) -> Unit) = Snackbar
  2.       .make(this, message, Snackbar.LENGTH_SHORT)
  3.       .setAction(actionText, action)
  4.       .apply { show() }
复制代码
但是这样就够了吗?我们想让它更短,对于大多数情况,我们需要的唯一参数是消息。以是我们的调用方式是:
  1. snackbar("message")
复制代码
因为我们关心的仅仅是在屏幕底部显示我们的消息,以是需要消除视图参数。荣幸的是,借助扩展函数,在Activity中获取根视图可以通过使用Anko中的find(android.R.id.content)来完成。改良后的Activity的扩展方法如下所示
  1. inline fun Activity.snackbar(message: String) = snackbar(find (R.id.content), message)
复制代码
除了在Activity中,我们通常还在哪里使用Snackbar呢?Android UI中还有两个告急的组件:Fragment和View。在Fragment中,有一个它地点的Activity的引用。以是实现我们的办理方案要容易得多:
  1. inline fun Fragment.snackbar(message: String) = snackbar(activity.find (R.id.content), message)
复制代码
而View并不一定附加在Activity上,我们要做出防御式判断,即:在我们尝试显示Snackbar之前,我们必须确保View的context属性隐藏了一个Activity实例:
  1. inline fun View.snackbar(message: String) {
  2.     val activity = context
  3.     if (activity is Activity) snackbar(activity.find(android.R.id.content), message)
  4.     else throw IllegalStateException("视图必须要承载在Activity上.")
  5. }
复制代码
  小练习
  上述例子联合Anko的find(R.id)使用,你是否可以或许改写为不依靠Anko呢?动手尝试一下吧。
  7.3.2 用扩展函数封装Utils

在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLaoderUtils等。以NetworkUtils为例,该类中我们通常会放入Android常常需要使用的网络相干方法。比如,我们现在有一个判断手机网络是否可用的方法:
  1. public class NetworkUtils {
  2.     public static boolean isMobileConnected(Context context) {
  3.         if (context != null) {
  4.             ConnectivityManager mConnectivityManager = (ConnectivityManager)
  5.                     context
  6.                             .getSystemService(Context.CONNECTIVITY_SERVICE);
  7.             NetworkInfo mMobileNetworkInfo = mConnectivityManager
  8.                     .getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
  9.             if (mMobileNetworkInfo != null) {
  10.                 return mMobileNetworkInfo.isAvailable();
  11.             }
  12.         }
  13.         return false;
  14.     }
  15. }
复制代码
在需要调用的地方,我们通常会这样写:
  1. Boolean isConnected = NetworkUtils.isMobileConnected(context);
复制代码
固然用起来比没封装之前优雅了许多,但是每次都要传入context,造成的烦琐我们先不计较,告急是大概会让调用者忽视context和mobileNetwork间的强接洽。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。我们盼望的是下面这样的使用方式
  1. Boolean isConnected = context.isMobileConnected();
复制代码
由于Context是Andorid SDK自带的类,我们无法对其举行修改,在Java中现在只能通过继承Context新增静态成员方法来实现。如果你阅读过前面的内容,应该知道在Kotlin中,我们通过扩展函数就能简单地实现:
  1. fun Context.isMobileConnected(): Boolean {
  2.     val mNetworkInfo = connectivityManager.activeNetworkInfo
  3.     if (mNetworkInfo != null) {
  4.         return mNetworkInfo.isAvailable
  5.     }
  6.     return false
  7. }
复制代码
我们只需将以上代码放入对应文件中即可。这时我们已经摆脱了类的束缚,使用方式如下:
  1. val isConnected = context.isMobileConnected();
复制代码
值得一提的是,在Android中对Context的生命周期需要举行很好地把控。这里我们应该使用ApplicationContext,防止出现生命周期不一致导致的内存泄漏或者其他问题。
除了上述方法,我们还有许多这样通用的代码,我们可以将它们放入不同的文件下。包括上面提到的Snackbar,我们也可以为其创建一个SnackbarUtils,这样会提供非常多的便利。但是需要注意的是,我们不能滥用这个特性(具体的原因在7.4.2节会再先容)​。
7.3.3 办理烦人的findViewById
对于Android开发者来说,对findViewById()这个方法一定不会陌生:在我们对视图控件操作前,我们需要通过findViewById方法来找到其对应的实例。因为一个界面里视图控件的数量大概会非常多,以是在Android开发早期我们通常都会看到一大片的findViewById(R.id.view_id)样板代码。举一个最常见的例子:
  1. import android.support.v7.app.AppCompatActivity;
  2. import android.os.Bundle;
  3. import android.widget.Button;
  4. import android.widget.EditText;
  5. public class LoginActivity extends AppCompatActivity {
  6.     Button loginButton;
  7.     EditText nameEditText;
  8.     EditText passwordEditText;
  9.     …
  10.     @Override
  11.     protected void onCreate(Bundle savedInstanceState) {
  12.         super.onCreate(savedInstanceState);
  13.         setContentView(R.layout.activity_login);
  14.         loginButton =  findViewById(R.id.btn_login);
  15.         nameEditText = findViewById(R.id.et_name);
  16.         passwordEditText = findViewById(R.id.et_password);
  17.         …
  18.     }
  19. }  
复制代码
在一个登录界面中,至少包罗登录按钮(loginButton)、登录名输入框(nameEditText)、暗码输入框(passwordEditText),实际情况只会更复杂。在老版本SDK中,findBiewById获取到的范例是View,我们还需要举行范例欺压转换。
  1. loginButton =  (Button)findViewById(R.id.btn_login);
复制代码
荣幸的是,在Kotlin中我们可以使用扩展函数来简化这个烦琐的过程:
  1. fun <T : View> Activity._view(@IdRes id: Int): T {   
  2.     return findViewById(id) as T
  3. }
复制代码
现在,在兼容老版本的情况下,我们可以将代码改为这样:
  1. loginButton =  _view(R.id.btn_login);
  2. nameEditText = _view(R.id.et_name);
  3. passwordEditText = _view(R.id.et_password);
复制代码
现在调用起来是比力方便了,但是部门极简主义的读者大概会想:当前我们照旧需要创建loginButton、nameEditText、passwordEditText的实例,但是这些实例似乎只充当了一个“临时变量”的角色,我们依靠它举行一些点击事件绑定(onlick)、赋值操作后似乎就没什么用处了。能不能将其也省略掉,直接对R.id.*操作呢?答案是可以,在Kotlin中我们可以使用高阶函数,做如下改动(此处以简化onclick为例子)​:
  1. fun Int.onClick(click: ()->Unit){
  2.        // _view 为我们之前定义的简化版findViewById
  3.     val tmp = _view <View>(this).apply {
  4.         setOnClickListener{
  5.             click()
  6.         }
  7.     }
  8. }
复制代码
我们就可以这样绑定登录按钮的点击事件:
  1. R.id.btn_login.onClick { println("Login…") }
复制代码
 大概有强迫症的读者会受不了R.id.xx这样的写法,并且每次都要写R.id前缀,某种情况下也会造成烦琐。那还有更简便的写法吗?答案是肯定的,Kotlin为我们提供了扩展插件,gradle默认就集成了:
  1. apply plugin: 'kotlin-android-extensions'
复制代码
回到最原始的LoginActivity,我们只用额外import kotlinx.android.synthetic.main.activity_login.*,即可直接用视图中组件的id名称来操作视图:
  1. btn_login.setOnClickListener {
  2.     println("MainKotlinActivity onClick Button")
  3. }
复制代码
这时候,或许还会有读者疑惑:固然是省略了R.id.几个字符,但是引入是否会造成性能问题?值得引入、使用kotlin-android-extensions吗?照旧用惯常的做法,让我们先对其反编译,看看其对应Java代码中是怎样实现的:
  1. public final class MainActivity extends BaseActivity {
  2.     private HashMap _$_findViewCache;
  3.     protected void onCreate(@Nullable Bundle savedInstanceState) {
  4.         super.onCreate(savedInstanceState);
  5.         this.setContentView(2131296283);
  6.     ((TextView)this._$_findCachedViewById(id.label)).setText((CharSequence)"Dive Into Kotlin");
  7.     ((TextView)this._$_findCachedViewById(id.label)).setOnClickListener((OnClickListener)null.INSTANCE);
  8.     ((Button)this._$_findCachedViewById(id.btn)).setOnClickListener((OnClickListener)null.INSTANCE);
  9.         }
  10.         public View _$_findCachedViewById(int var1) {
  11.             if(this._$_findViewCache == null) {
  12.                this._$_findViewCache = new HashMap();
  13.             }
  14.             View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
  15.             if(var2 == null) {
  16.                var2 = this.findViewById(var1);
  17.                this._$_findViewCache.put(Integer.valueOf(var1), var2);
  18.             }
  19.             return var2;
  20.         }
  21.         public void _$_clearFindViewByIdCache() {
  22.             if(this._$_findViewCache != null) {
  23.                 this._$_findViewCache.clear();
  24.             }
  25.     }
  26. }
复制代码
你会惊喜地发现,在第一次使用控件的时候,在缓存集合中举行查找,有就直接使用,没有就通过findViewById举行查找,并添加到缓存集合中。其还提供了$clearFindViewById Cache()方法用于清除缓存,在我们想要彻底替换界面控件时可以使用。
   注意
  Fragment的onDestroyView()方法中默认调用了$clearFindViewByIdCache()清除缓存,而Activity没有。当然,我们并没有完全离开findViewById,只是Kotlin的扩展插件使用缓存的方式让我们开发更方便、更快捷。还有许多场景都使用了扩展函数,由于篇幅的限制,这里不再先容。感兴趣的读者可以了解一下Google推出的Android扩展库Android KXT(https://github.com/android/android-ktx)。
  7.4 扩展不是万能的

如果你已经看过任何KotlinConf演讲,大概常常看到,扩展函数被称为Kotlin最有魅力的特性之一。确实云云,扩展可以让你的代码更加通透。但在使用的同时,有一些值得注意的地方。
7.4.1 调理方式对扩展函数的影响

Kotlin是一种静态范例语言,我们创建的每个对象不仅具有运行时,还具有编译时范例,开发职员必须明白指定(在Kotlin中可以推断)​。在使用扩展函数时,要清楚地了解静态和动态调理之间的区别。
1.静态与动态调理

由于这个内容大概大部门读者都没有打仗过,以是我们举一个Java例子,帮助各人理解。已知我们有以下类:
  1. class Base {
  2.     public void fun() {
  3.     System.out.println(("I'm Base foo!"));
  4.     }
  5. }
  6. class Extended extends Base {
  7.     @Override
  8.     public void fun() {
  9.     System.out.println(("I'm Extended foo!"));
  10.     }
  11. }
  12. Base base = new Extended();
  13. base.fun();
复制代码
对于以上代码,我们声明一个名为base的变量,它具有编译时范例Base和运行时范例Extended。当我们调用时,base.foo()将动态调理该方法,这意味着运行时范例(Extended)的方法被调用。
当我们调用重载方法时,调理变为静态并且仅取决于编译时范例
  1. void foo(Base base) {
  2.     ...
  3. }
  4. void foo(Extended extended) {
  5.     ...
  6. }
  7. public static void main(String[] args) {
  8.     Base base = new Extended();
  9.     foo(base);
  10. }
复制代码
在这种情况下,纵然base本质上是Extended的实例,终极照旧会实行Base的方法。
2.扩展函数始终静态调理

大概你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,因此你可以重载它,但不能覆盖它。这大概是成员扩展函数之间最告急的区别:前者是动态调理的,后者总是静态调理的。
为了便于理解,我们举一个例子:
  1. open class Base
  2. class Extended: Base()
  3. fun Base.foo() = "I'm Base.foo!"
  4. fun Extended.foo() = "I'm Extended.foo!"
  5. fun main(args: Array<String>) {
  6.     val instance: Base = Extended()
  7.     val instance2 = Extended()
  8.     println(instance.foo())
  9.     println(instance2.foo())
  10. }
复制代码
正如我们所说,由于只考虑了编译时范例,第1个打印将调用Base.foo(),而第2个打印将调用Extended.foo()。
  1. I'm Base.foo!
  2. I'm Extended.foo!
复制代码
3.类中的扩展函数

如果我们在类的内部声明扩展函数,那么它将不是静态的。如果该扩展函数加上open关键字,我们可以在子类中举行重写(override)。这是否意味着它将被动态调理?这是一个比力尴尬的问题:当在类内部声明扩展函数时,它同时具有调理接收器和扩展接收器。
调理接收器和扩展接收器的概念
扩展接收器(extension receiver):与Kotlin扩展密切相干的接收器,表现我们为其定义扩展的对象。
调理接收器(dispatch receiver):扩展被声明为成员时存在的一种特殊接收器,它表现声明扩展名的类的实例。
  1. class X {
  2.     fun Y.foo() = " I’m Y.foo"
  3. }
复制代码
在上面的例子中,X是调理接收器而Y是扩展接收器。如果将扩展函数声明为open,则它的调理接收器只能是动态的,而扩展接收器总是在编译时剖析。这样说你大概还不是很明白,我们照旧举一个例子帮助理解:
  1. open class Base
  2. class Extended : Base()
  3. open class X {     
  4.     open fun Base.foo() {
  5.         println("I’m Base.foo in X")
  6.     }
  7.     open fun Extended.foo() {
  8.         println("I’m Extended.foo in X")
  9.     }
  10.     fun deal(base: Base) {
  11.          base.foo()  
  12.     }
  13. }
  14. class Y : X() {
  15.     override fun Base.foo() {
  16.         println("I’m Base.foo in Y")
  17.      }
  18.     override fun Extended.foo() {
  19.         println("I’m Extended.foo in Y")
  20.      }
  21. }
  22. X().deal(Base())      // 输出 I’m Base.foo in X
  23. Y().deal(Base())      // 输出I’m Base.foo in Y — 即 dispatch receiver 被动态处理
  24. X().deal(Extended())  // 输出I’m  Base.foo in X — 即 extension receiver 被静态处理
  25. Y().deal(Extended())  // 输出I’m  Base.foo in Y
复制代码
聪明的你大概会注意到,Extended扩展函数始终没有被调用,并且此举动与我们之前在静态调理例子中所看到的一致。决定两个Base类扩展函数实行哪一个,直接因素是实行deal方法的类的运行时范例。
通过以上例子,我们可以总结出扩展函数几个需要注意的地方:
• 如果该扩展函数是顶级函数或成员函数,则不能被覆盖;
• 我们无法访问其接收器的非公共属性;
• 扩展接收器总是被静态调理。
7.4.2 被滥用的扩展函数

扩展函数在开发中为我们提供了非常多的便利,但是在实际应用中,我们大概会将这个特性滥用。
在上一节中,我们提到过一些常用的方法封装到Utils类中,此中就包括ImageLoaderUtsils。这里以此中加载网络图片为例:
  1. fun Context.loadImage(url:String,imageView :ImageView){ GlideApp.with(this)
  2.            .load(url)
  3.            .placeholder(R.mipmap.img_default)
  4.            .error(R.mipmap.ic_error)
  5.            .into(imageView)
  6. }
  7. // ImageActivity.kt 中使用
  8. ...
  9. this.loadImage(url, imgView)
  10. ...
复制代码
大概你在用的时候并没有感觉出什么奇怪的地方,但是实际上,我们并没有以任何方式扩展现有类。上述代码仅仅为了在函数调用的时候省去参数,这是一种滥用扩展机制的举动。
我们知道,Context作为“God Object”​,已经承担了许多责任。我们基于Context扩展,还很大概产生ImageView与传入上下文周期不一致导致的许多问题。
精确的做法应该是在ImageView上举行扩展:
  1. fun ImageView.loadImage(url:String){ GlideApp.with(this.context)
  2.              .load(url)
  3.              .placeholder(R.mipmap.img_default)
  4.              .error(R.mipmap.ic_error)
  5.              .into(this)
  6.     }
复制代码
这样在调用的时候,不仅省去了更多的参数,而且ImageView的生命周期也得到了包管。
实际项目中,我们还需要考虑网络哀求框架替换及维护的问题,一般会对图片哀求框架举行二次封装:
  1. object ImageLoader {
  2.     fun with(context: Context, url: String, imageView: ImageView) {
  3.         GlideApp.with(context)
  4.                 .load(url)
  5.                 .placeholder(R.mipmap.img_default)
  6.                 .error(R.mipmap.ic_error)
  7.                 .into(imageView)
  8.     }
  9.     ...
  10. }
复制代码
以是,固然扩展函数可以或许提供许多便利,我们照旧应该注意在恰当的地方使用它,否则会造成不必要的贫苦。
7.5 本章小结

(1)多态

多态在语言使用中发挥了不可或缺的作用。计算机科学中的多态是在1967年由克里斯托弗·斯特雷奇(Christopher Strachey)提出的一个概念。他同时指定,特设多态以及参数多态是主要的多态表现情势。
(2)特设多态

与参数多态相对,是为了应对特殊情况下所做的特殊处置惩罚。在Kotlin中,扩展以及重载都是特设多态的一种。它符合面向对象设计根本原则之一——开放封闭原则
(3)扩展函数

作为Kotlin的特色之一,为我们提供了非常多的便利。由于扩展函数的实质对应Java中的静态方法,我们在使用的时候应该以Java中静态方法对其举行规范。
(4)扩展函数接收器

扩展函数有两个接收器的概念:调理接收器扩展接收器。调理方式不同,会影响扩展函数,常见的影响有:
• 如果它是顶级函数或成员函数,则不能被覆盖。
• 我们无法访问其接收器的非公共属性。
扩展接收器总是被静态调理
(5)精确使用扩展函数

扩展函数固然方便,但是方便之下带来了滥用的情况。在新特性面前,我们不能过于喜新厌旧,应联合面向对象思想和设计模式来举行规范。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

玛卡巴卡的卡巴卡玛

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表