扩展是Kotlin实现特设多态的一种非常重要的语言特性。在本节中,我们将继续探讨这种技术。
[TOC]
### 扩展与开放封闭原则
对开发者而言,业务需求总是在不断变动的。熟悉设计模式的读者应该知道,在修改现有代码的时候,我们应该遵循开放封闭原则,即:**软件实体应该是可扩展,而不可修改的**。也就是说,对扩展开放,而对修改是封闭的。
*****
**开放封闭原则概念**
**开放封闭原则(OCP, Open Closed Principle)是所有面向对象原则的核心**。软件设计本身所追求的目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其他的设计原则,很多时候是为实现这一目标服务的,例如以替换原则实现最佳的、正确的继承层次,就能保证不会违反开放封闭原则。
*****
实际情况并不乐观,比如在进行Android开发的时候,为了实现某个需求,我们引入了一个第三方库。但某一天需求发生了变动,当前库无法满足,且库的作者暂时没有升级的计划。这时候也许你就会开始尝试对库源码进行修改。这就违背了开放封闭原则。随着需求的不断变更,问题可能就会如滚雪球般增长。
Java中一种惯常的应对方案是让第三方库类继承一个子类,然后添加新功能。然而,正如我们谈论过的那样,强行的继承可能违背“里氏替换原则”。
**更合理的方案是依靠扩展这个语言特性。Kotlin通过扩展一个类的新功能而无须继承该类,在大多数情况下都是一种更好的选择,从而我们可以合理地遵循软件设计原则**。
### 使用扩展函数、属性
扩展函数的声明非常简单,它的关键字是`<Type>`。此外我们需要一个“接收者类型(recievier type)”(通常是类或接口的名称,也就是谁可以调用这个函数)来作为它的前缀。
以`MutableList<Int>`为例,我们为其扩展一个exchange方法,代码如下:
```
fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
val tmp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = tmp
}
fun main(args: Array<String>) {
val list = mutableListOf(1, 2, 3)
list.exchange(1, 2)
}
```
`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.extension
fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
val tmp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = tmp
}
```
我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理。
```
class Extends {
fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
val tmp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = 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修饰这个扩展函数,又会有什么结果?这个问题留给读者自行探究。
#### 扩展属性
与扩展函数类似,我们还能为一个类添加扩展属性。比如我们想给`MutableList<Int>`添加判断一个判断和是否为偶数的属性sumIsEven:
```
val MutableList<Int>.sumIsEven: Boolean
get() = this.sum() % 2 == 0
```
这样就可以像调用扩展函数一样调用它了:
```
val list = mutableListOf(2,2,4)
list.sumIsEven
```
但是,如果你准备给这个属性添加上默认值,并且写出如下代码:
// 编译错误:扩展属性不能有初始化器
```
val MutableList<Int>.sumIsEven: Boolean = false
get() = this.sum() % 2 == 0
```
以上代码编译不能通过,这是为什么呢?
其实,与扩展函数一样,其本质也是对应Java中的静态方法(我们反编译成Java代码后可以看到一个getSumIsEven的静态方法,与扩展函数类似)。**由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器的原因。它们的行为只能由显式提供的getters和setters定义**。
*****
**幕后字段**
在Kotlin中,如果属性中存在访问器使用默认实现,那么Kotlin会自动提供幕后字段filed,其仅可用于自定义getter和setter中。
*****
### 扩展的特殊情况
前面,我们对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,那么在子类中也不能访问这个扩展函数。
### 标准库中的扩展函数:run、let、also、takeIf
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
在介绍可空类型的时候,我们接触了let方法,来看看它的定义:
```
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
```
**let和apply类似,唯一不同的是返回值:apply返回的是原来的对象,而let返回的是闭包里面的值**。细心的读者应该察觉到,我们在介绍可空类型的时候,大量使用了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 { ... }
```
我们发现,这**与集合中的filter异曲同工,不过takeIf只操作单条数据**。与takeIf相反的还有takeUnless,即接收器不满足特定条件才会执行。
除了这些函数外,Kotlin标准库中还有很多方便的扩展函数。
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android