擴展是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