會寫「18.dp」只是個入門——Kotlin 的擴展函數和擴展屬性(Extension Functions / Properties)

視頻先行

要看視頻的能夠直接去 嗶哩嗶哩 或者 YouTube 觀看。不方便看視頻的,下面文章搞起。web

開始

Kotlin 有個特別好用的功能叫擴展,你能夠給已有的類去額外添加函數和屬性,並且既不須要改源碼也不須要寫子類。這就是今天這個視頻的主題。另外不少人雖然會用擴展,但只會最基本的使用,好比就只用來寫個叫 dp  的擴展屬性來把 dp 值轉成像素值:面試

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)app

Math.pow(2, 10); // 2 的 10 次方
複製代碼

pow 這個詞你可能不認識,其實它不是個完整的詞,而是 power 的縮寫,power 就是乘方的意思,哎中國人學程序常常還須要學英文好煩。這個 pow(a, n)  方法是 Math  類的一個靜態方法,這類方法咱們用得比較多的是 max()  和 min()編輯器

Math.max(1, 2); // 2
Math.min(1, 2); // 1 複製代碼

比較兩個數的大小,用靜態方法很符合直覺;可是冪運算的話,靜態方法就不如成員方法來得更直觀了:ide

2.pow(10); // 要是 Java 裏能這樣寫就行了
複製代碼

但咱們只能選擇靜態方法。爲何?很簡單,由於 Integer、Float、Double 這幾個類沒提供這個方法,因此咱們只能用 Math 類的靜態方法。函數

Kotlin 的擴展函數 Float.pow()

在 Kotlin 裏,咱們用的不是 Java 的 Integer、Float、Double,而是另外幾個名字相同或相像的 Kotlin 本身新創造的類。這幾個類一樣沒有提供 pow()  這個函數,但好的是,咱們依然能夠用看起來像是成員函數的方式來作冪運算。學習

2f.pow(10) // Kotlin 能夠這麼寫
複製代碼

爲何?由於 Float.pow(n: Int)  是 Kotlin 給 Float  這個類增長的一個擴展函數:ui

// kotlin.util.MathJVM.kt
public actual inline fun Float.pow(n: Int): Float  = nativeMath.pow(this.toDouble(), n.toDouble()).toFloat() 複製代碼

在聲明一個函數的時候在函數名的左邊寫個類名再加個點,你就能對這個類的對象調用這個函數了。這種函數就叫擴展函數,Extension Functions。就好像你鑽到這個類的源碼裏,改了它的代碼,給它增長了一個新的函數同樣。雖然事實上不是,但用起來基本同樣。具體區別我等會兒說。this

這種用法給咱們的開發帶來了極大的便利,咱們能夠用它來作不少事。

舉個例子?

  • 好比 pow() 吧?
  • 再好比,AndroidX 裏有個東西叫 ViewModel 對吧?——這個我之後有空的話也講一下,不少人對 ViewModel 有很大誤解,居然覺得這是用來寫 MVVM 架構的——AndroidX 的 KTX 庫裏有一個對於 ComponentActivity 類的擴展函數叫 viewModels(): image.png只要引用了對應的 KTX 庫,在 Activity 裏你能夠直接就調用這個函數來很方便地初始化 ViewModel:
class MainActivity : AppCompatActivity() {
  val model: MyViewModel by viewModels()   ...  } 複製代碼

而不須要重寫 Activity 類。

  • 相似的用法能夠有不少不少,限制你的是你的想象力。因此其實對於擴展函數,你更須要注意的是謹慎和剋制:須要用了再用,而不要由於它很酷很方便就能用則用。由於這些方便的東西若是太多,就會變成對你和同事的打擾。

擴展函數的寫法

擴展函數寫在哪均可以,但寫的位置不一樣,做用域就也不一樣。所謂做用域就是說你能在哪些地方調用到它。 最簡單的寫法就是把它寫成 Top Level 也就是頂層的,讓它不屬於任何類,這樣你就能在任何類裏使用它。這也和成員函數的做用域很像——哪裏能用到這個類,哪裏就能用到類裏的這個函數:

package com.rengwuxian
 fun String.method1(i: Int) {  ... }  ...  "rengwuxian".method1(1) 複製代碼

有一點要注意了:這個函數屬於誰?屬於函數名左邊的類嗎?並非的,它是個 Top-level Function,它誰也不屬於,或者說它只屬於它所在的 package。 那它爲何能夠被這個類的對象調用呢?——由於它在函數名的左邊呀!在 Kotlin 裏,當你給聲明的函數名左邊加上一個類名的時候,表示你要給這個函數限定一個 Receiver——直譯的話叫接收者,其實也就是哪一個類的對象能夠調用這個函數。雖說你是個 Top-level Function,不屬於任何類——確切地說是,不是任何一個類的成員函數——但我要限制只有經過某個類的對象才能調用你。這就是擴展函數的本質。 那這……和成員函數有什麼區別嗎?這種奇怪又繞腦子的知識有什麼用嗎?聽我繼續講。

成員擴展函數

除了寫成 Top Level 的,擴展函數也能夠寫在某個類裏:

class Example {
  fun String.method2(i: Int) {  ...  }  } 複製代碼

而後你就能夠在這個類裏調用這個函數,但必須使用那個前綴類的對象來調用它:

class Example {
  fun String.method2(i: Int) {  ...  }   ...   "rengwuxian".method2(1) // 能夠調用  } 複製代碼

看起來……有點奇怪了。這個函數這麼寫,它究竟是屬於誰的呀?屬於外部的類仍是左邊前綴的類? 屬於誰?這個「屬於誰」其實有點模糊的,我須要問再明確點:它是誰的成員函數?固然是外部的類的成員函數了,由於它寫在它裏面嘛,對吧?那函數名左邊的是什麼?剛纔我剛說過,它是這個函數的 Receiver,對吧?也就是誰能夠去調用它。 因此它既是外部類的成員函數,又是前綴類的擴展函數。 這種既是成員函數、又是擴展函數的函數,它們的用法跟 Top Level 的擴展函數同樣,只是因爲它同時仍是成員函數,因此只能在它所屬的類裏面被調用,到了外面就不能用了:

class Example {
  fun String.method2(i: Int) {  ...  }   ...   "rengwuxian".method2(1) // 能夠調用  }  "rengwuxian".method2(1) // 類的外部不能調用 複製代碼

這個……也好理解吧?你爲何要把擴展函數寫在類的裏面?不就是爲了讓它不要被外界看見形成污染嗎,是吧?

指向擴展函數的引用

在以前 Lambda 那一期視頻裏,我說過函數是可使用雙冒號被指向的對吧:

Int::toFloat
複製代碼

我當時也講了,其實指向的並非函數自己,而是和函數等價的一個對象,這也是爲何你能夠對這個引用調用 invoke(),卻不能對函數自己調用:

(Int::toFloat)(1) // 等價於 1.toFloat()
Int::toFloat.invoke(1) // 等價於 1.toFloat() 1.toFloat.invoke() // 報錯 複製代碼

可是爲了簡單起見,咱們一般能夠把這個「指向和函數等價的對象的引用」稱做是「指向這個函數的引用」,這個問題不大。那麼咱們基於這個叫法繼續說。 普通函數能夠被指向,擴展函數一樣也是能夠被指向的:

fun String.method1(i: Int) {
 }  ...  String::method1 複製代碼

不過若是這個擴展函數不是 Top-Level 的,也就是說若是它是某個類的成員函數,它就不能被引用了:

class Extensions {
  fun String.method1(i: Int) {  ...  }   ...   String::method1 // 報錯 } 複製代碼

爲何?你想啊,一個成員函數怎麼引用:類名加雙冒號加函數名對吧?擴展函數呢?也是類名加雙冒號加函數名對吧?只不過此次是 Receiver 的類名。那成員擴展函數呢?還用類名加雙冒號加函數名唄?可是……用誰的類名?是這個函數所屬的類名,仍是它的 Receiver 的類名?這是有歧義的,因此 Kotlin 就乾脆不準咱們引用既是成員函數又是擴展函數的函數了,一了百了。 一樣,跟普通函數的引用同樣,擴展函數的引用也能夠被調用,直接調用或者用 invoke() 均可以,不過要記得把 Receiver 也就是接收者或者說調用者填成第一個參數:

(String::method1)("rengwuxian", 1)
String::method1.invoke("rengwuxian", 1)  // 以上兩句都等價於: "rengwuxian".method1(1) 複製代碼

把擴展函數的引用賦值給變量

一樣的,擴展函數的引用也能夠賦值給變量:

val a: String.(Int) -> Unit = String::method1
複製代碼

而後你再拿着這個變量去調用,或者再次傳遞給別的變量,都是能夠的:

"rengwuxian".a(1)
a("rengwuxian", 1) a.invoke("rengwuxian", 1) 複製代碼

有無 Receiver 的變量的互換

另外你們可能會發現,當你拿着一個函數的引用去調用的時候,不論是一個普通的成員函數仍是擴展函數,你都須要把 Receiver 也就是接收者或者調用者做爲第一個參數填進去。

(String::method1)("rengwuxian", 1)  // 等價於 "rengwuxian".method1(1)
(Int::toFloat)(1) // 等價於 1.toFloat() 複製代碼

爲何?由於你拿到的是函數引用而不是調用者的對象,因此沒辦法在左邊寫上調用者啊,是吧? 因此 Kotlin 要想支持讓咱們拿着函數的引用去調用,就必須給個途徑讓咱們提供調用者。那提供怎樣的途徑呢?最終 Kotlin 給咱們的方案就是:在這種調用方式下,增長一個函數參數,讓咱們把第一個參數的位置填上調用者。這樣,咱們就能夠用函數的引用來調用成員函數和擴展函數了。但同時,又有一個問題我不知道大家發現沒有: 既然有 Receiver 的函數能夠以無 Receiver 的方式來調用,那……它能夠賦值給無 Receiver 的函數類型的變量嗎?

val b: (String, Int) -> Unit = String::method1 // 這樣能夠嗎?
複製代碼

答案是,能夠的。在 Kotlin 裏,每個有 Receiver 的函數——其實就是成員函數和擴展函數——它的引用均可以賦值給兩種不一樣的函數類型變量:一種是有 Receiver 的,一種是沒有 Receiver 的:

val a: String.(Int) -> Unit = String::method1
val b: (String, Int) -> Unit = String::method1 複製代碼

這兩種寫法都是合法的。爲何?由於有用啊,是吧?有什麼用我剛講過,忘了的倒個帶。

蔡依林:「終於看開……」

並且一樣的,這兩種類型的變量也能夠互相賦值來進行轉換:

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 的變量?

fun method3(s: String, i: Int) {
 }  ...  val e: (String, Int) -> Unit = ::method3 val f: String.(Int) -> Unit = ::method3 // 這種寫法也行哦 複製代碼

哇塞,沒有報錯! 是的,這樣賦值也是能夠的。 經過這些類型的互相轉換,你能夠把一個原本沒有 Receiver 的函數變得能夠經過 Receiver 來調用:

fun method3(s: String, i: Int) {
 }  ...  val f: String.(Int) -> Unit = ::method3 "rengwuxian".method3(1) // 不容許調用,報錯 "rengwuxian".f(1) // 能夠調用 複製代碼

這就很爽了哈? 固然了你也能夠反向操做,去把一個有 Receiver 的函數變得不能用 Receiver 調用:

fun String.method1(i: Int) {
 }  ...  val b: (String, Int) -> Unit = String::method1 "rengwuxian".method1(1) // 能夠調用 "rengwuxian".b(1) // 不容許調用,報錯 複製代碼

這樣收窄功能好像沒什麼用哈?不過我仍是要把這個告訴你,由於這樣你的知識體系纔是完整的。

說到完整啊,每一個作 Android 的人都應該把本身的支撐體系擴充一下,讓本身的技能樹變完整,你才能百毒不侵,工做和麪試都不怕。那要怎麼完整呢?最好的方式就是來學習一下個人 Android 高級進階系列化課程。掃描屏幕上的二維碼,加個人助教,更多課程相關的信息以及我更多的知識輸出渠道找 TA 瞭解。 image.png

擴展屬性

除了擴展函數,Kotlin 的擴展還包括擴展屬性。它跟擴展函數是一個邏輯,就是在聲明的屬性左邊寫上類名加點,這就是一個擴展屬性了,英文原名叫 Extension Property。

val Float.dp
 get() = TypedValue.applyDimension(  TypedValue.COMPLEX_UNIT_DIP,  this,  Resources.getSystem().displayMetrics  )  ...  val RADIUS = 200f.dp 複製代碼

它的用法和擴展函數同樣,但少了擴展函數在引用上以及 Receiver 上的一些比較繞的問題,因此很簡單,你本身去研究吧。有些東西寫成擴展屬性是比擴展函數要更加直觀和方便的,因此雖然它很簡單,但研究一下絕對有好處。

總結

此次講的內容挺多的,但其實也很簡單,主要就這麼幾點:擴展函數、擴展函數的引用、有無 Receiver 的函數類型的轉換以及擴展屬性。記不住的把視頻多刷幾遍,不要怕,我在個人課程裏也常常跟個人學員說:你把每節課多刷幾遍,別嫌費時間,又不是電視劇,知識密度這麼大的課程你多看幾遍只賺不虧。

好了今天的視頻就到這裏,若是你喜歡個人內容,歡迎點贊留言收藏分享。掃碼關注我,不錯過個人任何新內容。我是扔物線,我不和你比高低,我只助你成長。咱們下期見。 image.png

本文使用 mdnice 排版

相關文章
相關標籤/搜索