Kotlin作为一门工程化的语言,拥有一些令开发者心旷神怡的特性。本章将带领大家一步步了解Kotlin中一个比较重要的特性——扩展(Extensions)。Kotlin的扩展其实是多态的一种表现形式,在深入了解扩展之前,让我们先探讨一下多态的不同技术手段。
## 多态的不同方式
熟悉Java的读者对多态应该不会陌生,它是面向对象程序设计(OOP)的一个重要特征。当我们用一个子类继承一个父类的时候,这就是子类型多态(Subtype polymorphism)。另一种熟悉的多态是参数多态(Parametric polymorphism),我们在后面所讨论的泛型就是其最常见的形式。此外,也许你还会想到C++中的运算符重载,我们可以用特设多态(Ad-hoc polymorphism)来描述它。相比子类型多态和参数多态,可能你对特设多态会感到有些许陌生。其实这是一种更加灵活的多态技术,在Kotlin中,一些有趣的语言特性,如运算符重载、扩展都很好地支持这种多态。在本节接下来的内容中,我们将通过具体的例子来进一步展现各种多态的特点,并介绍更加深入的Kotlin语言特性。
### 子类型多态
无论在前端、移动端还是后台开发中,数据持久化操作都是必不可少的。在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的所有方法。这种用子类型替换超类型实例的行为,就是我们通常说的子类型多态。
### 参数多态
在完成数据库的创建之后,现在我们要把客户(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)
}
```
以上的多态形式我们可以称之为参数多态,其实最常见的参数多态的形式就是泛型。
参数多态在程序设计语言与类型论中是指声明与定义函数、复合类型、变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用,所以它建立在运行时的参数基础上,并且所有这些都是在不影响类型安全的前提下进行的。
### 对第三方类进行扩展
进一步思考,假使当对应的业务类ClassA、ClassB是第三方引入的,且不可被修改时,如果我们要想给它们扩展一些方法,比如将对象转化为Json,利用之前介绍的多态技术就会显得比较麻烦。
幸运的是,Kotlin支持扩展的语法,利用扩展我们就能给ClassA、ClassB添加方法或属性,从而换一种思路来解决上面的问题。
```
fun ClassA.toJson(): String = {
……
}
```
如上我们给ClassA类扩展了一个将对象转换为Json的toJson方法。需要注意的是,扩展属性和方法的实现运行在ClassA实例,它们的定义操作并不会修改ClassA类本身。这样就为我们带来了一个很大的好处,即被扩展的第三方类免于被污染,从而避免了一些因父类修改而可能导致子类出错的问题发生。
当然,在Java中我们可以依靠其他的办法比如设计模式来解决,但相较而言依靠扩展的方案显得更加方便且合理,这其实也是另一种被称为特设多态的技术。下节我们就来了解下这种多态,然而再介绍Kotlin中另外一种同样可服务于它的语言特性——运算符重载。
### 特设多态与运算符重载
除了子类型多态、参数多态以外,还存在一种更灵活的多态形式——特设多态(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)
```
我们将会展示如何利用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