原文章出处:[会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)](https://kaixue.io/kotlin-extensions/)
## 开始
Kotlin 有个特别好用的功能叫扩展,你可以**给已有的类去额外添加函数和属性,而且既不需要改源码也不需要写子类**。这就是今天这个视频的主题。另外很多人虽然会用扩展,但只会最基本的使用,比如就只用来写个叫`dp` 的扩展属性来把 dp 值转成像素值:
~~~kotlin
val Float.dp
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
...
val RADIUS = 200f.dp
~~~
稍微高级一点就不太行了,尤其是扩展函数和函数引用混在一起的时候就更是瞬间蒙圈。如果你有这样的问题,本篇文章应该可以帮到你。
## Java 的 Math.pow()
在 Java 里我们如果想做幂运算——也就是几的几次方——要用静态方法`pow(a, n)`
~~~java
Math.pow(2, 10); // 2 的 10 次方
~~~
pow 这个词你可能不认识,其实它不是个完整的词,而是 power 的缩写,power 就是乘方的意思。这个`pow(a, n)` 方法是`Math` 类的一个静态方法,这类方法我们用得比较多的是`max()` 和`min()`
~~~java
Math.max(1, 2); // 2
Math.min(1, 2); // 1
~~~
比较两个数的大小,用静态方法很符合直觉;但是幂运算的话,静态方法就不如成员方法来得更直观了:
~~~java
2.pow(10); // 要是 Java 里能这样写就好了
~~~
但我们只能选择静态方法。为什么?很简单,因为 Integer、Float、Double 这几个类没提供这个方法,所以我们只能用 Math 类的静态方法。
## Kotlin 的扩展函数 Float.pow()
在 Kotlin 里,我们用的不是 Java 的 Integer、Float、Double,而是另外几个名字相同或相像的 Kotlin 自己新创造的类。这几个类同样没有提供`pow()` 这个函数,但好的是,我们**依然可以用看起来像是成员函数的方式来做幂运算**。
~~~kotlin
2f.pow(10) // Kotlin 可以这么写
~~~
为什么?**因为`Float.pow(n: Int)` 是 Kotlin 给`Float` 这个类增加的一个扩展函数**:
~~~kotlin
// kotlin.util.MathJVM.kt
public actual inline fun Float.pow(n: Int): Float
= nativeMath.pow(this.toDouble(), n.toDouble()).toFloat()
~~~
**在声明一个函数的时候在函数名的左边写个类名再加个点,你就能对这个类的对象调用这个函数了。这种函数就叫扩展函数**,Extension Functions。就好像你钻到这个类的源码里,改了它的代码,给它增加了一个新的函数一样。虽然事实上不是,但用起来基本一样。具体区别我等会儿说。
这种用法给我们的开发带来了极大的便利,我们可以用它来做很多事。
举个例子?
* 比如 pow() 吧?
* 再比如,AndroidX 里有个东西叫 ViewModel 对吧?——很多人对 ViewModel 有很大误解,竟然以为这是用来写 MVVM 架构的——AndroidX 的 KTX 库里有一个对于 ComponentActivity 类的扩展函数叫 viewModels():
![](https://img.kancloud.cn/fb/71/fb71c111598033a6ea223c63cb2d4246_884x187.png)
只要引用了对应的 KTX 库,在 Activity 里你可以直接就调用这个函数来很方便地初始化 ViewModel:
~~~kotlin
class MainActivity : AppCompatActivity() {
val model: MyViewModel by viewModels()//委托
...
}
~~~
而不需要重写 Activity 类,上面示例中还用了委托属性——[属性委托](https://developer.android.google.cn/kotlin/common-patterns?hl=zh_cn#delegate)
* 类似的用法可以有很多很多,限制你的是你的想象力。所以**其实对于扩展函数,你更需要注意的是谨慎和克制:需要用了再用,而不要因为它很酷很方便就能用则用。因为这些方便的东西如果太多,就会变成对你和同事的打扰**。
## 扩展函数的写法
扩展函数写在哪都可以,但写的位置不同,作用域就也不同。所谓作用域就是说你能在哪些地方调用到它。
**最简单的写法就是把它写成 Top Level 也就是顶层的,让它不属于任何类,这样你就能在任何类里使用它**。这也和成员函数的作用域很像——哪里能用到这个类,哪里就能用到类里的这个函数:
~~~kotlin
package com.rengwuxian
fun String.method1(i: Int) {
...
}
...
"rengwuxian".method1(1)
~~~
有一点要注意了:**这个函数属于谁?属于函数名左边的类吗?并不是的,它是个 Top-level Function,它谁也不属于,或者说它只属于它所在的 package**。
那它为什么可以被这个类的对象调用呢?——因为它在函数名的左边呀!**在 Kotlin 里,当你给声明的函数名左边加上一个类名的时候,表示你要给这个函数限定一个 Receiver——直译的话叫接收者,其实也就是哪个类的对象可以调用这个函数。虽然说你是个 Top-level Function,不属于任何类——确切地说是,不是任何一个类的成员函数——但我要限制只有通过某个类的对象才能调用你。这就是扩展函数的本质**。
那这……和成员函数有什么区别吗?这种奇怪又绕脑子的知识有什么用吗?
## 成员扩展函数
除了写成 Top Level 的,**扩展函数也可以写在某个类里**:
~~~kotlin
class Example {
fun String.method2(i: Int) {
...
}
}
~~~
然后**你就可以在这个类里调用这个函数,但必须使用那个前缀类的对象来调用它**:
~~~kotlin
class Example {
fun String.method2(i: Int) {
...
}
...
"rengwuxian".method2(1) // 可以调用
}
~~~
看起来……有点奇怪了。这个函数这么写,它到底是属于谁的呀?属于外部的类还是左边前缀的类?
属于谁?这个「属于谁」其实有点模糊的,我需要问再明确点:它是谁的成员函数?当然是外部的类的成员函数了,因为它写在它里面嘛,对吧?那**函数名左边的是什么**?刚才我刚说过,**它是这个函数的 Receiver,对吧?也就是谁可以去调用它**。
所以它既是外部类的成员函数,又是前缀类的扩展函数。
**这种既是成员函数、又是扩展函数的函数,它们的用法跟 Top Level 的扩展函数一样,只是由于它同时还是成员函数,所以只能在它所属的类里面被调用,到了外面就不能用了**:
~~~kotlin
class Example {
fun String.method2(i: Int) {
...
}
...
"rengwuxian".method2(1) // 可以调用
}
"rengwuxian".method2(1) // 类的外部不能调用
~~~
这个……也好理解吧?你**为什么要把扩展函数写在类的里面?不就是为了让它不要被外界看见造成污染吗,是吧?**
## 指向扩展函数的引用
在之前 Lambda 那一期视频里,我说过函数是可以使用双冒号被指向的对吧:
~~~kotlin
Int::toFloat
~~~
我当时也讲了,**其实指向的并不是函数本身,而是和函数等价的一个对象**,这也是为什么你可以对这个引用调用 invoke(),却不能对函数本身调用:
~~~kotlin
(Int::toFloat)(1) // 等价于 1.toFloat()
Int::toFloat.invoke(1) // 等价于 1.toFloat()
1.toFloat.invoke() // 报错
~~~
但是为了简单起见,我们通常可以**把这个「指向和函数等价的对象的引用」称作是「指向这个函数的引用」**,这个问题不大。那么我们基于这个叫法继续说。
**普通函数可以被指向,扩展函数同样也是可以被指向的**:
~~~kotlin
fun String.method1(i: Int) {
}
...
String::method1
~~~
**不过如果这个扩展函数不是 Top-Level 的,也就是说如果它是某个类的成员函数,它就不能被引用了**:
~~~kotlin
class Extensions {
fun String.method1(i: Int) {
...
}
...
String::method1 // 报错
}
~~~
为什么?你想啊,一个成员函数怎么引用:类名加双冒号加函数名对吧?扩展函数呢?也是类名加双冒号加函数名对吧?只不过这次是 Receiver 的类名。**那成员扩展函数呢?还用类名加双冒号加函数名呗?但是……用谁的类名?是这个函数所属的类名,还是它的 Receiver 的类名?这是有歧义的,所以 Kotlin 就干脆不许我们引用既是成员函数又是扩展函数的函数了,一了百了**。
同样,跟普通函数的引用一样,扩展函数的引用也可以被调用,直接调用或者用 invoke() 都可以,不过要记得把 Receiver 也就是接收者或者说调用者填成第一个参数:
~~~kotlin
(String::method1)("rengwuxian", 1)
String::method1.invoke("rengwuxian", 1)
// 以上两句都等价于:
"rengwuxian".method1(1)
~~~
### 把扩展函数的引用赋值给变量
同样的,**扩展函数的引用也可以赋值给变量**:
~~~kotlin
val a: String.(Int) -> Unit = String::method1
~~~
然后**你再拿着这个变量去调用,或者再次传递给别的变量,都是可以的**:
~~~kotlin
"rengwuxian".a(1)
a("rengwuxian", 1)
a.invoke("rengwuxian", 1)
~~~
### 有无 Receiver 的变量的互换
**另外大家可能会发现,当你拿着一个函数的引用去调用的时候,不管是一个普通的成员函数还是扩展函数,你都需要把 Receiver 也就是接收者或者调用者作为第一个参数填进去**。
~~~kotlin
(String::method1)("rengwuxian", 1) // 等价于 "rengwuxian".method1(1)
(Int::toFloat)(1) // 等价于 1.toFloat()
~~~
**为什么?因为你拿到的是函数引用而不是调用者的对象**,所以没办法在左边写上调用者啊,是吧?
所以 Kotlin 要想支持让我们拿着函数的引用去调用,就必须给个途径让我们提供调用者。那提供怎样的途径呢?最终 Kotlin 给我们的方案就是:**在这种调用方式下,增加一个函数参数,让我们把第一个参数的位置填上调用者。这样,我们就可以用函数的引用来调用成员函数和扩展函数了**。但同时,又有一个问题我不知道你们发现没有:
既然有 Receiver 的函数可以以无 Receiver 的方式来调用,那……它可以**赋值给无 Receiver 的函数类型的变量**吗?
~~~kotlin
val b: (String, Int) -> Unit = String::method1 // 这样可以吗?
~~~
答案是,可以的。**在 Kotlin 里,每一个有 Receiver 的函数——其实就是成员函数和扩展函数——它的引用都可以赋值给两种不同的函数类型变量:一种是有 Receiver 的,一种是没有 Receiver 的**:
~~~kotlin
val a: String.(Int) -> Unit = String::method1
val b: (String, Int) -> Unit = String::method1
~~~
这两种写法都是合法的。为什么?因为有用啊,是吧?有什么用我刚讲过,忘了的倒个带。
而且同样的,**这两种类型的变量也可以互相赋值来进行转换**:
~~~kotlin
val a: String.(Int) -> Unit = String::method1
val b: (String, Int) -> Unit = String::method1
val c: String.(Int) -> Unit = b
val d: (String, Int) -> Unit = a
~~~
既然这两种类型的变量可以互相赋值来转换,那不就是说无 Receiver 的函数引用也可以赋值给有 Receiver 的变量?
这样的话,是不是**一个普通的无 Receiver 的函数也可以直接赋值给有 Receiver 的变量**?
~~~kotlin
fun method3(s: String, i: Int) {
}
...
val e: (String, Int) -> Unit = ::method3
val f: String.(Int) -> Unit = ::method3 // 这种写法也行哦
~~~
是的,这样赋值也是可以的。
**通过这些类型的互相转换,你可以把一个本来没有 Receiver 的函数变得可以通过 Receiver 来调用**:
~~~kotlin
fun method3(s: String, i: Int) {
}
...
val f: String.(Int) -> Unit = ::method3
"rengwuxian".method3(1) // 不允许调用,报错
"rengwuxian".f(1) // 可以调用
~~~
这就很爽了哈?
当然了你也可以反向操作,去把一个有 Receiver 的函数变得不能用 Receiver 调用:
~~~kotlin
fun String.method1(i: Int) {
}
...
val b: (String, Int) -> Unit = String::method1
"rengwuxian".method1(1) // 可以调用
"rengwuxian".b(1) // 不允许调用,报错
~~~
这样收窄功能好像没什么用哈?不过我还是要把这个告诉你,因为这样你的知识体系才是完整的。
## 扩展属性
除了扩展函数,**Kotlin 的扩展还包括扩展属性**。它跟扩展函数是一个逻辑,就是**在声明的属性左边写上类名加点,这就是一个扩展属性了**,英文原名叫 Extension Property。
~~~kotlin
val Float.dp
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
...
val RADIUS = 200f.dp
~~~
**它的用法和扩展函数一样,但少了扩展函数在引用上以及 Receiver 上的一些比较绕的问题**,所以很简单,你自己去研究吧。**有些东西写成扩展属性是比扩展函数要更加直观和方便的**,所以虽然它很简单,但研究一下绝对有好处。
## 总结
这次讲的内容挺多的,但其实也很简单,主要就这么几点:扩展函数、扩展函数的引用、有无 Receiver 的函数类型的转换以及扩展属性。
- 前言
- 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