據說……Kotlin 能夠用 Lambda?java
不錯不錯,Java 8 也有 Lambda,挺好用的。app
據說……Kotlin 的 Lambda 還能當函數參數?ide
啊挺好挺好,我也來寫一個!函數
哎,報錯了?我改!工具
哎?post
我……再改?開發工具
我……再……改?ui
啊!!!!!!!!!!!!this
這裏是視頻版本:【碼上開學】Kotlin 的高階函數、匿名函數和 Lambda 表達式 spa
看了視頻就不用看後面的文字了(但若是喜歡,點個贊再溜啊)。
你們好,我是扔物線朱凱。Kotlin 很方便,但有時候也讓人頭疼,並且越方便的地方越讓人頭疼,好比 Lambda 表達式。不少人由於 Lambda 而被 Kotlin 吸引,但不少人也由於 Lambda 而被 Kotlin 嚇跑。其實大多數已經用了好久 Kotlin 的人,對 Lambda 也只會簡單使用而已,甚至至關一部分人不靠開發工具的自動補全功能,根本就徹底不會寫 Lambda。今天我就來跟你們嘮一嘮 Lambda。不過,要講 Lambda,咱們得先從 Kotlin 的高階函數——Higher-Order Function 提及。
在 Java 裏,若是你有一個 a 方法須要調用另外一個 b 方法,你在裏面調用就能夠;
int a() {
return b(1);
}
a();
複製代碼
而若是你想在 a 調用時動態設置 b 方法的參數,你就得把參數傳給 a,再從 a 的內部把參數傳給 b:
int a(int param) {
return b(param);
}
a(1); // 內部調用 b(1)
a(2); // 內部調用 b(2)
複製代碼
這均可以作到,不過……若是我想動態設置的不是方法參數,而是方法自己呢?好比我在 a 的內部有一處對別的方法的調用,這個方法多是 b,多是 c,不必定是誰,我只知道,我在這裏有一個調用,它的參數類型是 int ,返回值類型也是 int ,而具體在 a 執行的時候內部調用哪一個方法,我但願能夠動態設置:
int a(??? method) {
return method(1);
}
a(method1);
a(method2);
複製代碼
或者說,我想把方法做爲參數傳到另外一個方法裏,這個……能夠作到嗎?
不行,也行。在 Java 裏是不容許把方法做爲參數傳遞的,可是咱們有一個歷史悠久的變通方案:接口。咱們能夠經過接口的方式來把方法包裝起來:
public interface Wrapper {
int method(int param);
}
複製代碼
而後把這個接口的類型做爲外部方法的參數類型:
int a(Wrapper wrapper) {
return wrapper.method(1);
}
複製代碼
在調用外部方法時,傳遞接口的對象來做爲參數:
a(wrapper1);
a(wrapper2);
複製代碼
若是到這裏你以爲聽暈了,我換個寫法你再感覺一下:
咱們在用戶發生點擊行爲的時候會觸發點擊事件:
// 注:這是簡化後的代碼,不是 View.java 類的源碼
public class View {
OnClickListener mOnClickListener;
...
public void onTouchEvent(MotionEvent e) {
...
mOnClickListener.onClick(this);
...
}
}
複製代碼
所謂的點擊事件,最核心的內容就是調用內部的一個 OnClickListener 的 onClick() 方法:
public interface OnClickListener {
void onClick(View v);
}
複製代碼
而所謂的這個 OnClickListener 其實只是一個殼,它的核心全在內部那個 onClick() 方法。換句話說,咱們傳過來一個 OnClickListener:
OnClickListener listener1 = new OnClickListener() {
@Override
void onClick(View v) {
doSomething();
}
};
view.setOnClickListener(listener1);
複製代碼
本質上實際上是傳過來一個能夠在稍後被調用的方法(onClick())。只不過由於 Java 不容許傳遞方法,因此咱們才把它包進了一個對象裏來進行傳遞。
而在 Kotlin 裏面,函數的參數也能夠是函數類型的:
fun a(funParam: Fun): String {
return funParam(1);
}
複製代碼
當一個函數含有函數類型的參數的時候——這句話有點繞啊——若是你調用它,你就能夠——固然你也必須——傳入一個函數類型的對象給它;
fun b(param: Int): String {
return param.toString()
}
a(b)
複製代碼
不過在具體的寫法上沒有個人示例這麼粗暴。
首先我寫的這個 Fun 做爲函數類型實際上是錯的,Kotlin 裏並無這麼一種類型來標記這個變量是個「函數類型」。由於函數類型不是一「個」類型,而是一「類」類型,由於函數類型能夠有各類各樣不一樣的參數和返回值的類型的搭配,這些搭配屬於不一樣的函數類型。例如,無參數無返回值(() -> Unit)和單 Int 型參數返回 String (Int -> String)是兩種不一樣的類型,這個很好理解,就好像 Int 和 String 是兩個不一樣的類型。因此不能只用 Fun 這個詞來表示「這個參數是個函數類型」,就好像不能用 Class 這個詞來表示「這個參數是某個類」,由於你須要指定,具體是哪一種函數類型,或者說這個函數類型的參數,它的參數類型是什麼、返回值類型是什麼,而不能籠統地一句說「它是函數類型」就完了。
因此對於函數類型的參數,你要指明它有幾個參數、參數的類型是什麼以及返回值類型是什麼,那麼寫下來就大概是這個樣子:
fun a(funParam: (Int) -> String): String {
return funParam(1)
}
複製代碼
看着有點可怕。可是隻有這樣寫,調用的人才知道應該傳一個怎樣的函數類型的參數給你。
一樣的,函數類型不僅能夠做爲函數的參數類型,還能夠做爲函數的返回值類型:
fun c(param: Int): (Int) -> Unit {
...
}
複製代碼
這種「參數或者返回值爲函數類型的函數」,在 Kotlin 中就被稱爲「高階函數」——Higher-Order Functions。
這個所謂的「高階」,總給人一種神祕感:階是什麼?哪裏高了?其實沒有那麼複雜,高階函數這個概念源自數學中的高階函數。在數學裏,若是一個函數使用函數做爲它的參數或者結果,它就被稱做是一個「高階函數」。好比求導就是一個典型的例子:你對 f(x) = x 這個函數求導,結果是 1;對 f(x) = x² 這個函數求導,結果是 2x。很明顯,求導函數的參數和結果都是函數,其中 f(x) 的導數是 1 這其實也是一個函數,只不過是一個結果恆爲 1 的函數,因此——啊講岔了,總之, Kotlin 裏,這種參數有函數類型或者返回值是函數類型的函數,都叫作高階函數,這只是個對這一類函數的稱呼,沒有任何特殊性,Kotlin 的高階函數沒有任何特殊功能,這是我想說的。
另外,除了做爲函數的參數和返回值類型,你把它賦值給一個變量也是能夠的。
不過對於一個聲明好的函數,無論是你要把它做爲參數傳遞給函數,仍是要把它賦值給變量,都得在函數名的左邊加上雙冒號才行:
a(::b)
val d = ::b
複製代碼
這……是爲何呢?
若是你上網搜,你會看到這個雙冒號的寫法叫作函數引用 Function Reference,這是 Kotlin 官方的說法。可是這又表示什麼意思?表示它指向上面的函數?那既然都是一個東西,爲何不直接寫函數名,而要加兩個冒號呢?
由於加了兩個冒號,這個函數才變成了一個對象。
什麼意思?
Kotlin 裏「函數能夠做爲參數」這件事的本質,是函數在 Kotlin 裏能夠做爲對象存在——由於只有對象才能被做爲參數傳遞啊。賦值也是同樣道理,只有對象才能被賦值給變量啊。但 Kotlin 的函數自己的性質又決定了它沒辦法被當作一個對象。那怎麼辦呢?Kotlin 的選擇是,那就建立一個和函數具備相同功能的對象。怎麼建立?使用雙冒號。
在 Kotlin 裏,一個函數名的左邊加上雙冒號,它就不表示這個函數自己了,而表示一個對象,或者說一個指向對象的引用,但,這個對象可不是函數自己,而是一個和這個函數具備相同功能的對象。
怎麼個相同法呢?你能夠怎麼用函數,就能怎麼用這個加了雙冒號的對象:
b(1) // 調用函數
d(1) // 用對象 a 後面加上括號來實現 b() 的等價操做
(::b)(1) // 用對象 :b 後面加上括號來實現 b() 的等價操做
複製代碼
但我再說一遍,這個雙冒號的這個東西,它不是一個函數,而是一個對象,一個函數類型的對象。
對象是不能加個括號來調用的,對吧?可是函數類型的對象能夠。爲何?由於這實際上是個假的調用,它是 Kotlin 的語法糖,實際上你對一個函數類型的對象加括號、加參數,它真正調用的是這個對象的 invoke() 函數:
d(1) // 實際上會調用 d.invoke(1)
(::b)(1) // 實際上會調用 (::b).invoke(1)
複製代碼
因此你能夠對一個函數類型的對象調用 invoke(),但不能對一個函數這麼作:
b.invoke(1) // 報錯
複製代碼
爲何?由於只有函數類型的對象有這個自帶的 invoke() 能夠用,而函數,不是函數類型的對象。那它是什麼類型的?它什麼類型也不是。函數不是對象,它也沒有類型,函數就是函數,它和對象是兩個維度的東西。
包括雙冒號加上函數名的這個寫法,它是一個指向對象的引用,但並非指向函數自己,而是指向一個咱們在代碼裏看不見的對象。這個對象複製了原函數的功能,但它並非原函數。
這個……是底層的邏輯,但我知道這個有什麼用呢?
這個知識能幫你解開 Kotlin 的高階函數以及接下來我立刻要講的匿名函數、Lambda 相關的大部分迷惑。
好比我在代碼裏有這麼幾行:
fun b(param: Int): String {
return param.toString()
}
val d = ::b
複製代碼
那我若是想把 d 賦值給一個新的變量 e:
val e = d
複製代碼
我等號右邊的 d,應該加雙冒號仍是不加呢?
不用試,也不用搜,想想:這是個賦值操做對吧?賦值操做的右邊是個對象對吧?d 是對象嗎?固然是了,b 不是對象是由於它來自函數名,但 d 已是個對象了,因此直接寫就好了。
咱們繼續講。
要傳一個函數類型的參數,或者把一個函數類型的對象賦值給變量,除了用雙冒號來拿現成的函數使用,你還能夠直接把這個函數挪過來寫:
a(fun b(param: Int): String {
return param.toString()
});
val d = fun b(param: Int): String {
return param.toString()
}
複製代碼
另外,這種寫法的話,函數的名字其實就沒用了,因此你能夠把它省掉:
a(fun(param: Int): String {
return param.toString()
});
val d = fun(param: Int): String {
return param.toString()
}
複製代碼
這種寫法叫作匿名函數。爲何叫匿名函數?很簡單,由於它沒有名字唄,對吧。等號左邊的不是函數的名字啊,它是變量的名字。這個變量的類型是一種函數類型,具體到咱們的示例代碼來講是一種只有一個參數、參數類型是 Int、而且返回值類型爲 String 的函數類型。
另外呢,其實剛纔那種左邊右邊都有名字的寫法,Kotlin 是不容許的。右邊的函數既然要名字也沒有用,Kotlin 乾脆就不准它有名字了。
因此,若是你在 Java 裏設計一個回調的時候是這麼設計的:
public interface OnClickListener {
void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
this.listener = listener;
}
複製代碼
使用的時候是這麼用的:
view.setOnClickListener(new OnClickListener() {
@Override
void onClick(View v) {
switchToNextPage();
}
});
複製代碼
到了 Kotlin 裏就能夠改爲這麼寫了:
fun setOnClickListener(onClick: (View) -> Unit) {
this.onClick = onClick
}
view.setOnClickListener(fun(v: View): Unit) {
switchToNextPage()
})
複製代碼
簡單一點哈?另外大多數(幾乎全部)狀況下,匿名函數還能更簡化一點,寫成 Lambda 表達式的形式:
view.setOnClickListener({ v: View ->
switchToNextPage()
})
複製代碼
終於講到 Lambda 了。
若是 Lambda 是函數的最後一個參數,你能夠把 Lambda 寫在括號的外面:
view.setOnClickListener() { v: View ->
switchToNextPage()
}
複製代碼
而若是 Lambda 是函數惟一的參數,你還能夠直接把括號去了:
view.setOnClickListener { v: View ->
switchToNextPage()
}
複製代碼
另外,若是這個 Lambda 是單參數的,它的這個參數也省略掉不寫:
view.setOnClickListener {
switchToNextPage()
}
複製代碼
哎,不錯,單參數的時候只要不用這個參數就能夠直接不寫了。
其實就算用,也能夠不寫,由於 Kotlin 的 Lambda 對於省略的惟一參數有默認的名字:it:
view.setOnClickListener {
switchToNextPage()
it.setVisibility(GONE)
}
複製代碼
有點爽哈?不過咱們先停下想想:這個 Lambda 這也不寫那也不寫的……它不迷茫嗎?它是怎麼知道本身的參數類型和返回值類型的?
靠上下文的推斷。我調用的函數在聲明的地方有明確的參數信息吧?
fun setOnClickListener(onClick: (View) -> Unit) {
this.onClick = onClick
}
複製代碼
這裏面把這個參數的參數類型和返回值寫得清清楚楚吧?因此 Lambda 纔不用寫的。
因此,當你要把一個匿名函數賦值給變量而不是做爲函數參數傳遞的時候:
val b = fun(param: Int): String {
return param.toString()
}
複製代碼
若是也簡寫成 Lambda 的形式:
val b = { param: Int ->
return param.toString()
}
複製代碼
就不能省略掉 Lambda 的參數類型了:
val b = {
return it.toString() // it 報錯
}
複製代碼
爲何?由於它沒法從上下文中推斷出這個參數的類型啊!
若是你出於場景的需求或者我的偏好,就是想在這裏省掉參數類型,那你須要給左邊的變量指明類型:
val b: (Int) -> String = {
return it.toString() // it 能夠被推斷出是 Int 類型
}
複製代碼
另外 Lambda 的返回值不是用 return 來返回,而是直接取最後一行代碼的值:
val b: (Int) -> String = {
it.toString() // it 能夠被推斷出是 Int 類型
}
複製代碼
這個必定注意,Lambda 的返回值別寫 return,若是你寫了,它會把這個做爲它外層的函數的返回值來直接結束外層函數。固然若是你就是想這麼作那沒問題啊,但若是你是隻是想返回 Lambda,這麼寫就出錯了。
另外由於 Lambda 是個代碼塊,它總能根據最後一行代碼來推斷出返回值類型,因此它的返回值類型確實能夠不寫。實際上,Kotlin 的 Lambda 也是寫不了返回值類型的,語法上就不支持。
如今我再停一下,咱們想一想:匿名函數和 Lambda……它們究竟是什麼?
咱們先看匿名函數。它能夠做爲參數傳遞,也能夠賦值給變量,對吧?
可是咱們剛纔也說過了函數是不能做爲參數傳遞,也不能賦值給變量的,對吧?
那爲何匿名函數就這麼特殊呢?
由於 Kotlin 的匿名函數不——是——函——數。它是個對象。匿名函數雖然名字裏有「函數」兩個字,包括英文的原名也是 Anonymous Function,但它其實不是函數,而是一個對象,一個函數類型的對象。它和雙冒號加函數名是一類東西,和函數不是。
因此,你才能夠直接把它當作函數的參數來傳遞以及賦值給變量:
a(fun (param: Int): String {
return param.toString()
});
val a = fun (param: Int): String {
return param.toString()
}
複製代碼
同理,Lambda 其實也是一個函數類型的對象而已。你能怎麼使用雙冒號加函數名,就能怎麼使用匿名函數,以及怎麼使用 Lambda 表達式。
這,就是 Kotlin 的匿名函數和 Lambda 表達式的本質,它們都是函數類型的對象。Kotlin 的 Lambda 跟 Java 8 的 Lambda 是不同的,Java 8 的 Lambda 只是一種便捷寫法,本質上並無功能上的突破,而 Kotlin 的 Lambda 是實實在在的對象。
在你知道了在 Kotlin 裏「函數並不能傳遞,傳遞的是對象」和「匿名函數和 Lambda 表達式其實都是對象」這些本質以後,你之後去寫 Kotlin 的高階函數會很是輕鬆很是舒暢。
Kotlin 官方文檔裏對於雙冒號加函數名的寫法叫 Function Reference 函數引用,故意引導你們認爲這個引用是指向原函數的,這是爲了簡化事情的邏輯,讓你們更好上手 Kotlin;但這種邏輯是有毒的,一旦你信了它,你對於匿名函數和 Lambda 就怎麼也搞不清楚了。
你們若是喜歡個人視頻,也能夠了解一下個人 Android 高級進階系列化課程,掃碼加小助理丟丟讓她給你發試聽課:
白嫖黨記得點贊轉發,也是對個人支持。
再說一下 Java 的 Lambda。對於 Kotlin 的 Lambda,有不少從 Java 過來的人表示「好用好用但不會寫」。這是一件頗有意思的事情:你都不會寫,那你是怎麼會用的呢?Java 從 8 開始引入了對 Lambda 的支持,對於單抽象方法的接口——簡稱 SAM 接口,Single Abstract Method 接口——對於這類接口,Java 8 容許你用 Lambda 表達式來建立匿名類對象,但它本質上仍是在建立一個匿名類對象,只是一種簡化寫法而已,因此 Java 的 Lambda 只靠代碼自動補全就基本上能寫了。而 Kotlin 裏的 Lambda 和 Java 本質上就是不一樣的,由於 Kotlin 的 Lambda 是實實在在的函數類型的對象,功能更強,寫法更多更靈活,因此不少人從 Java 過來就有點搞不明白了。
另外呢,Kotlin 是不支持使用 Lambda 的方式來簡寫匿名類對象的,由於咱們有函數類型的參數嘛,因此這種單函數接口的寫法就直接不必了。那你還支持它幹嗎?
不過當和 Java 交互的時候,Kotlin 是支持這種用法的:當你的函數參數是 Java 的單抽象方法的接口的時候,你依然可使用 Lambda 來寫參數。但這其實也不是 Kotlin 增長了功能,而是對於來自 Java 的單抽象方法的接口,Kotlin 會爲它們額外建立一個把參數替換爲函數類型的橋接方法,讓你能夠間接地建立 Java 的匿名類對象。
這就是爲何,你會發現當你在 Kotlin 裏調用 View.java 這個類的 setOnClickListener() 的時候,能夠傳 Lambda 給它來建立 OnClickListener 對象,但你照着一樣的寫法寫一個 Kotlin 的接口,你卻不能傳 Lambda。由於 Kotlin 指望咱們直接使用函數類型的參數,而不是用接口這種折中方案。
好,這就是 Kotlin 的高階函數、匿名函數和 Lambda。簡單總結一下:
在 Kotlin 裏,有一類 Java 中不存在的類型,叫作「函數類型」,這一類類型的對象在能夠當函數來用的同時,還能做爲函數的參數、函數的返回值以及賦值給變量;
建立一個函數類型的對象有三種方式:雙冒號加函數名、匿名函數和 Lambda;
必定要記住:雙冒號加函數名、匿名函數和 Lambda 本質上都是函數類型的對象。在 Kotlin 裏,匿名函數不是函數,Lambda 也不是什麼玄學的所謂「它只是個代碼塊,無法歸類」,Kotlin 的 Lambda 能夠歸類,它屬於函數類型的對象。
固然了這裏面的各類細節還有不少,這個你能夠本身學去,我就無論你了。下期內容是 Kotlin 的擴展屬性和擴展函數,關注我,不錯過個人任何新內容。你們拜拜~
【碼上開學】Kotlin 協程的掛起好神奇好難懂?今天我把它的皮給扒了