原文章出處:[會寫「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():

只要引用了對應的 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