玛卡巴卡的卡巴卡玛 发表于 2025-4-15 17:52:01

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

第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操作的相干类:
class CustomerDatabaseHelper(context:Context): SQLiteOpenHelper(context, "kotlinDemo.db", cursorFactory , db.version){
    override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {}
    override fun onCreate(db: SQLiteDatabase) {
      val sql = "CREATE TABLE if not exists $tableName ( id integer PRIMARY KEY autoincrement, uniqueKey VARCHAR(32))" // 此处省略其他参数
      db.execSQL(sql)
    }
} 然后我们就可以使用父类DatabaseHelper的所有方法。这种用子范例替换超范例实例的举动,就是我们通常说的子范例多态。
7.1.2 参数多态

在完成数据库的创建之后,现在我们要把客户(Customer)存入客户端数据库中。大概会写这样一个方法:
fun persist(customer: Customer) {
    db.save(customer.uniqueKey, customer)
} 如果代码成功实行,我们就成功地将customer以键值对的方式存入数据库(上述例子中以uniqueKey对应customer,便于查询等操作)​。
但是,随着需求的变动,我们大概还会持久化多种范例的数据。如果每种范例都写一个presist方法,多少有些烦琐,通常我们会抽象一个方法来处置惩罚不同范例的持久化。因为我们接纳键值对的方式存储,以是需要获取不同范例对应的uniqueKey:
interface KeyI {
    val uniqueKey : String
}

class ClassA(override val uniqueKey: String) : KeyI {
    …
}

class ClassB(override val uniqueKey: String) : KeyI {
    …
} 这样,class A、B都已经具备uniqueKey。我们可以将persist举行如下改写:
fun <T: KeyI> persist(t: T) {
    db.save(t.uniqueKey, t)
} 以上的多态情势我们可以称之为参数多态,实在最常见的参数多态的情势就是泛型。
参数多态在程序设计语言与范例论中是指声明与定义函数、复合范例、变量时不指定其具体的范例,而把这部门范例作为参数使用,使得该定义对各种具体范例都实用,以是它建立在运行时的参数基础上,并且所有这些都是在不影响范例安全的前提下举行的。
7.1.3 对第三方类举行扩展

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

除了子范例多态、参数多态以外,还存在一种更机动的多态情势——特设多态(Ad-hoc polymorphism)。
大概你对特设多态这个概念并不是很了解,我们来举一个具体的例子。当你想定义一个通用的sum方法时,大概会在Kotlin中这么写:
fun <T> sum(x: T, y: T) : T = x + y 但编译器会报错,因为某些范例T的实例不一定支持加法操作,而且如果针对一些自定义类,我们更希望可以或许实现各自定制化的“加法语义上的操作”​。如果把参数多态做的事情打个比方:它提供了一个工具,只要一个东西能“切”​,就用这个工具来切割它。然而,实际中不是所有的东西都能被切,而且材料也不一定类似。更加合理的方案是,你可以根据不同的原材料来选择不同的工具来切它。再换种思绪,我们可以定义一个通用的Summable接口,然后让需要支持加法操作的类来实现它的plusThat方法。就像这样子:
interface Sumable<T> {
    fun plusThat(that: T): T
}

data class Len(val v: Int) : Sumable<Len> {
    override fun plusThat(that: Len) = Len(this.v + that.v)
} 可以发现,当我们在自定义一个支持plusThat方法的数据结构如Len时,这种做法并没有什么问题。然而,如果我们要针对不可修改的第三方类扩展加法操作时,这种通过子范例多态的技术手段也会遇到问题。
于是,你又想到了Kotlin的扩展,我们要引出另一种叫作“特设多态”的技术了。相比更通用的参数多态,特设多态提供了“量身定制”的能力。参考它的定义,特设多态可以理解为:一个多态函数是有多个不同的实现,依靠于实在参而调用相应版本的函数。
针对以上的例子,我们完全可以接纳扩展的语法来办理问题。别的,Kotlin原生支持了一种语言特性来很好地办理问题,这就是运算符重载。借助这种语法,我们可以完善地实现需求。代码如下:
data class Area(val value: Double)

operator fun Area.plus(that: Area): Area {
    return Area(this.value + that.value)
}
fun main(args: Array<String>) {
    println(Area(1.0) + Area(2.0)) // 运行结果:Area(value=3.0)
} 下面我们来具体先容下Kotlin中运算符重载的语法。相信你已经注意到了operator关键字,以及Kotlin中内置可重载的运算符plus。先来看看operator,它的作用是:将一个函数标记为重载一个操作符或者实现一个约定。
注意,这里的plus是Kotlin规定的函数名。除了重载加法,我们还可以通过重载减法(minus)、乘法(times)、除法(div)、取余(mod)(Kotlin1.1版本开始被rem替代)等函数来实现重载运算符。别的,你可以再追念一下第2章中遇到的一些基础语法,它们也是使用这种神奇的语言特性来实现的,如:
a in b // 转换为 b.contains(a)
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方法,代码如下:
fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
    val tmp = this
    this = this
    this = tmp
} MutableList<T>是Kotlin尺度库Collections中的List容器类,这里作为recievier type,exchange是扩展函数名。其余和Kotlin声明一个普通函数并无区别。
Kotlin的this要比Java更机动,这里扩展函数体里的this代表的是接收者范例的对象。
这里需要注意的是:Kotlin严格区分了接收者是否可空。如果你的函数是可空的,你需要重写一个可空范例的扩展函数。
我们可以非常方便地对该函数举行调用,代码如下:
val list = mutableListOf(1,2,3)
list.exchange(1,2) 1.扩展函数的实现机制

扩展函数的使用云云方便,会不会对性能造成影响呢?为了办理这个疑惑,我们有必要对Kotlin扩展函数的实现举行探究。我们以之前的MutableList<Int>.exchange为例,它对应的Java代码如下:
import java.util.List;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
    mv = {1, 1, 1},
    bv = {1, 0, 0},
    k = 2,
    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"},
    d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "production sources for module FPKotlin"}
)
public final class ExSampleKt {
    public static final void exchange(@NotNull List $receiver, int fromIndex, int toIndex) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      int tmp = ((Number)$receiver.get(fromIndex)).intValue();
      $receiver.set(fromIndex, $receiver.get(toIndex));
      $receiver.set(toIndex, Integer.valueOf(tmp));
    }
} 联合以上Java代码可以看出,我们可以将扩展函数近以理解为静态方法。而熟悉Java的读者应该知道静态方法的特点:它独立于该类的任何对象,且不依靠类的特定实例,被该类的所有实例共享。别的,被public修饰的静态方法本质上也就是全局方法。
综上所述,我们可以得出结论:扩展函数不会带来额外的性能消耗。
2.扩展函数的作用域

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

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

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

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

已知我们有如下类:
class Son {
    fun foo() = println("son called member foo")
} 它包罗一个成员方法foo(),假如我们哪天心血来潮,想对这个方法做特殊实现,使用扩展函数大概会写出如下代码:
fun Son.foo() = println("son called extention foo")

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

前面的例子中提到过,我们发现Kotlin中的this比在Java中更机动。以扩展函数为例,当在扩展函数里调用this时,指代的是接收者范例的实例。那么如果这个扩展函数声明在一个object内部,我们怎样通过this获取到类的实例呢?参考如下代码:
class Son{
    fun foo(){
      println("foo in Class Son")
    }
}

object Parent {
    fun foo() {
      println("foo in Class Parent")
    }

    @JvmStatic
    fun main(args: Array<String>) {
      fun Son.foo2() {
            this.foo()
            this@Parent.foo()
      }

      Son().foo2()
    }
} 这里我们可以用this@类名来强行指定调用的this。别的值得一提的是:如果Son扩展函数在Parent类内,我们将无法对其调用。
class Son{
    fun foo(){
      println("foo in Class Son")
    }
}

class Parent {
    fun foo() {
      println("foo in Class Parent")
    }

    fun Son.foo2() {
      this.foo()
      this@Parent.foo()
    }
}

object Test {
    @JvmStatic
    fun main(args: Array<String>) {
      Son().foo2()
    }
} 这是为什么呢?来看看Parent对应的Java代码,以下为核心部门:
public final class Parent {
    public final void foo() {
      String var1 = "foo in Class Parent";
      System.out.println(var1);
    }

    public final void foo2(@NotNull Son $receiver) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      $receiver.foo();
      this.foo();
    }
} 纵然我们设置访问权限为public,它也只能在该类或者该类的子类中被访问,如果我们设置访问权限为private,那么在子类中也不能访问这个扩展函数。原因在7.2.2节中已经先容。
7.2.4 尺度库中的扩展函数:run let also takeIf

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

先来看下run方法,它是使用扩展实现的,定义如下:
public inline fun <T, R> T.run(block: T.() -> R): R = block() 简单来说,run是任何范例T的通用扩展函数,run中实行了返回范例为R的扩展函数block,终极返回该扩展函数的结果。
在run函数中我们拥有一个单独的作用域,可以或许重新定义一个nickName变量,并且它的作用域只存在于run函数中。
fun testFoo() {
    val nickName = "Prefert"

    run {
      val nickName = "YarenTang"
      println(nickName) // YarenTang
    }
    println(nickName)   // Prefert
} 这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。
例如现在有这么一个场景:用户点击领取新人嘉奖的按钮,如果用户此时没有登录则弹出loginDialog,如果已经登录则弹出领取嘉奖的getNewAccountDialog。我们可以使用以下代码来处置惩罚这个逻辑:
run {
    if (!islogin) loginDialog else getNewAccountDialog
}.show() 2.let

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

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

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

在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLaoderUtils等。以NetworkUtils为例,该类中我们通常会放入Android常常需要使用的网络相干方法。比如,我们现在有一个判断手机网络是否可用的方法:
public class NetworkUtils {
    public static boolean isMobileConnected(Context context) {
      if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager)
                  context
                            .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mMobileNetworkInfo = mConnectivityManager
                  .getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
            if (mMobileNetworkInfo != null) {
                return mMobileNetworkInfo.isAvailable();
            }
      }
      return false;
    }
} 在需要调用的地方,我们通常会这样写:
Boolean isConnected = NetworkUtils.isMobileConnected(context); 固然用起来比没封装之前优雅了许多,但是每次都要传入context,造成的烦琐我们先不计较,告急是大概会让调用者忽视context和mobileNetwork间的强接洽。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。我们盼望的是下面这样的使用方式
Boolean isConnected = context.isMobileConnected(); 由于Context是Andorid SDK自带的类,我们无法对其举行修改,在Java中现在只能通过继承Context新增静态成员方法来实现。如果你阅读过前面的内容,应该知道在Kotlin中,我们通过扩展函数就能简单地实现:
fun Context.isMobileConnected(): Boolean {
    val mNetworkInfo = connectivityManager.activeNetworkInfo
    if (mNetworkInfo != null) {
      return mNetworkInfo.isAvailable
    }
    return false
} 我们只需将以上代码放入对应文件中即可。这时我们已经摆脱了类的束缚,使用方式如下:
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)样板代码。举一个最常见的例子:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;

public class LoginActivity extends AppCompatActivity {
    Button loginButton;
    EditText nameEditText;
    EditText passwordEditText;
    …
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_login);

      loginButton =findViewById(R.id.btn_login);
      nameEditText = findViewById(R.id.et_name);
      passwordEditText = findViewById(R.id.et_password);
      …
    }
} 在一个登录界面中,至少包罗登录按钮(loginButton)、登录名输入框(nameEditText)、暗码输入框(passwordEditText),实际情况只会更复杂。在老版本SDK中,findBiewById获取到的范例是View,我们还需要举行范例欺压转换。
loginButton =(Button)findViewById(R.id.btn_login); 荣幸的是,在Kotlin中我们可以使用扩展函数来简化这个烦琐的过程:
fun <T : View> Activity._view(@IdRes id: Int): T {   
    return findViewById(id) as T
} 现在,在兼容老版本的情况下,我们可以将代码改为这样:
loginButton =_view(R.id.btn_login);
nameEditText = _view(R.id.et_name);
passwordEditText = _view(R.id.et_password); 现在调用起来是比力方便了,但是部门极简主义的读者大概会想:当前我们照旧需要创建loginButton、nameEditText、passwordEditText的实例,但是这些实例似乎只充当了一个“临时变量”的角色,我们依靠它举行一些点击事件绑定(onlick)、赋值操作后似乎就没什么用处了。能不能将其也省略掉,直接对R.id.*操作呢?答案是可以,在Kotlin中我们可以使用高阶函数,做如下改动(此处以简化onclick为例子)​:
fun Int.onClick(click: ()->Unit){
       // _view 为我们之前定义的简化版findViewById
    val tmp = _view <View>(this).apply {
      setOnClickListener{
            click()
      }
    }
} 我们就可以这样绑定登录按钮的点击事件:
R.id.btn_login.onClick { println("Login…") }  大概有强迫症的读者会受不了R.id.xx这样的写法,并且每次都要写R.id前缀,某种情况下也会造成烦琐。那还有更简便的写法吗?答案是肯定的,Kotlin为我们提供了扩展插件,gradle默认就集成了:
apply plugin: 'kotlin-android-extensions' 回到最原始的LoginActivity,我们只用额外import kotlinx.android.synthetic.main.activity_login.*,即可直接用视图中组件的id名称来操作视图:
btn_login.setOnClickListener {
    println("MainKotlinActivity onClick Button")
} 这时候,或许还会有读者疑惑:固然是省略了R.id.几个字符,但是引入是否会造成性能问题?值得引入、使用kotlin-android-extensions吗?照旧用惯常的做法,让我们先对其反编译,看看其对应Java代码中是怎样实现的:
public final class MainActivity extends BaseActivity {
    private HashMap _$_findViewCache;

    protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131296283);
    ((TextView)this._$_findCachedViewById(id.label)).setText((CharSequence)"Dive Into Kotlin");
    ((TextView)this._$_findCachedViewById(id.label)).setOnClickListener((OnClickListener)null.INSTANCE);
    ((Button)this._$_findCachedViewById(id.btn)).setOnClickListener((OnClickListener)null.INSTANCE);
      }

      public View _$_findCachedViewById(int var1) {
            if(this._$_findViewCache == null) {
               this._$_findViewCache = new HashMap();
            }

            View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
            if(var2 == null) {
               var2 = this.findViewById(var1);
               this._$_findViewCache.put(Integer.valueOf(var1), var2);
            }

            return var2;
      }

      public void _$_clearFindViewByIdCache() {
            if(this._$_findViewCache != null) {
                this._$_findViewCache.clear();
            }

    }
} 你会惊喜地发现,在第一次使用控件的时候,在缓存集合中举行查找,有就直接使用,没有就通过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例子,帮助各人理解。已知我们有以下类:
class Base {
    public void fun() {
    System.out.println(("I'm Base foo!"));
    }
}
class Extended extends Base {
    @Override
    public void fun() {
    System.out.println(("I'm Extended foo!"));
    }
}

Base base = new Extended();
base.fun(); 对于以上代码,我们声明一个名为base的变量,它具有编译时范例Base和运行时范例Extended。当我们调用时,base.foo()将动态调理该方法,这意味着运行时范例(Extended)的方法被调用。
当我们调用重载方法时,调理变为静态并且仅取决于编译时范例。
void foo(Base base) {
    ...
}
void foo(Extended extended) {
    ...
}
public static void main(String[] args) {
    Base base = new Extended();
    foo(base);
} 在这种情况下,纵然base本质上是Extended的实例,终极照旧会实行Base的方法。
2.扩展函数始终静态调理

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

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

扩展函数在开发中为我们提供了非常多的便利,但是在实际应用中,我们大概会将这个特性滥用。
在上一节中,我们提到过一些常用的方法封装到Utils类中,此中就包括ImageLoaderUtsils。这里以此中加载网络图片为例:
fun Context.loadImage(url:String,imageView :ImageView){ GlideApp.with(this)
         .load(url)
         .placeholder(R.mipmap.img_default)
         .error(R.mipmap.ic_error)
         .into(imageView)
}

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

(1)多态

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

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

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

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

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Kotlin核心编程 下水篇:kotlin核心(第7章)