Kotlin 知識梳理(5) lambda 表示式和成員引用

澤毛發表於2017-12-21

一、本文概要

本文是對<<Kotlin in Action>>的學習筆記,如果需要執行相應的程式碼可以訪問線上環境 try.kotlinlang.org,這部分的思維導圖為:

Kotlin 知識梳理(5)   lambda 表示式和成員引用

二、Lambda 表示式和成員引用

Lambda表示式,本質上是可以 傳遞給函式的一小段程式碼,可以輕鬆地把通用的程式碼結構抽取成庫函式,Kotlin標準庫就大量地使用了它們。

2.1 Lambda 簡介:作為函式引數的程式碼塊

Lambda的應用場景有:

  • 當一個事件發生的時候執行這個事件處理器
  • 把這個操作應用到這個資料結構中所有的元素上

Java中,可以用匿名內部類來實現,但是它的語法很囉嗦,下面我們演示用Lambda來實現點選監聽:

button.setOnClickListener { /* 點選後執行的動作 */}
複製程式碼

2.2 Lambda 和集合

我們對集合執行的大部分任務都遵循幾個通用的模式,所以實現這幾個模式的程式碼應該放在一個庫裡,下面我們演示一個例子:將Person資料類放到一個集合當中,並從中選出年齡最大的一個人。

Kotlin 知識梳理(5)   lambda 表示式和成員引用
執行結果為:
Kotlin 知識梳理(5)   lambda 表示式和成員引用
這上面的例子用到了集合上的maxBy函式,它只需要一個實參:一個函式,指定比較哪個值來找到最大的元素,花括號中的程式碼{ it.age }就是實現了這個邏輯的lambda,它接收一個集合中的元素作為實參(使用it引用它)並且返回用來比較的值,在上面的例子中:

  • 集合元素是Person物件
  • 用來比較的值是儲存在其age屬性中的值

如果lambda剛好是 函式或者屬性的委託,可以用 成員引用 替換。

people.maxBy(Person :: age)
複製程式碼

2.3 Lambda 表示式語法

一個Lambda表示式把一小段行為進行編碼,你能把它 當做值到處傳遞,它可以被 獨立地宣告並儲存到一個變數中,但是最常見的還是直接宣告它並傳遞給函式,下面是一個Lambda表示式的語法,->前為 引數,後為 函式體,始終用 花括號包圍

{x : Int, y : Int -> x + y}
複製程式碼

2.3.1 將 Lambda 表示式儲存在變數中

可以將Lambda表示式儲存在一個變數中,把這個變數當做普通函式對待(即通過相應的實參呼叫它):

Kotlin 知識梳理(5)   lambda 表示式和成員引用

2.3.2 直接呼叫 Lambda 表示式

如果需要把一小段程式碼封閉在一個程式碼塊中,可以使用庫函式run,這種呼叫和內建語言結構一樣高效且不會帶來額外執行時開銷:

Kotlin 知識梳理(5)   lambda 表示式和成員引用
執行結果為:
Kotlin 知識梳理(5)   lambda 表示式和成員引用

2.3.3 Lambda 表示式的簡化過程

現在,讓我們回到最開始尋找集合中年齡最大的人的例子,它原本的呼叫方法如下,maxBy函式接收一個lambda表示式{ p : Person -> p.age }作為引數:

people.maxBy({ p : Person -> p.age })
複製程式碼

上面這段程式碼的解釋為:花括號中的程式碼片段是lambda表示式,把它作為實參傳給函式,這個lambda接收一個型別為Person的引數並返回它的年齡。下面,我們一起來看一下如何簡化這個表示式:

  • 第一步:Kotlin有一個語法規定,如果lambda表示式是函式呼叫的 最後一個實參,它可以 放到括號的外邊,因此上面的例子簡化為:
//第一步:將 lambda 表示式放到括號的外邊。
people.maxBy() { p : Person -> p.age }
複製程式碼
  • 第二步:當lambda是函式 唯一的實參,還可以 去掉呼叫程式碼中的空括號對
//第二步:去掉空括號對。
people.maxBy { p : Person -> p.age }
複製程式碼
  • 第三步:和區域性變數一樣,如果lambda引數的型別可以被推倒出來,你就不需要顯示地指定它,以maxBy函式為例,其 引數型別始終和集合的元素型別相同,因此編譯器知道你是對Person物件的集合呼叫maxBy函式,可以簡化為:
//第三步:省略 lambda 引數型別。
people.maxBy { p -> p.age }
複製程式碼

但是如果我們用變數儲存lambda,那麼就沒有可以推斷出引數型別的上下文,所以你必須顯示地指定引數型別:

//無法推斷出引數的型別,必須顯示地指定引數的型別。
val getAge = { p : Person : p.age }
people.maxBy (getAge)
複製程式碼
  • 第四步:如果當前上下文期望的是 只有一個引數的 lambda ,並且這個引數的型別可以推斷出來,那麼可以使用預設引數名稱it代替命名引數:
//第四步:使用預設引數名稱。
people.maxBy { it.age }
複製程式碼

2.3.4 又見 joinToString 函式

Kotlin 知識梳理(2) - 函式的定義與呼叫 中,我們通過joinToString介紹了命名引數和預設引數值的用法,實際上在標準庫中也有定義這個函式,不同之處在於它可以接收一個附加的函式引數,這個函式可以用toString函式以外的方法來把一個 元素轉換成字串,下面顯示如何 只列印出人的名字

Kotlin 知識梳理(5)   lambda 表示式和成員引用
執行結果為:
Kotlin 知識梳理(5)   lambda 表示式和成員引用

2.3.5 lambda 表示式包含更多語句

lambda表示式可以包含更多的語句,最後一個表示式就是lambda的結果:

Kotlin 知識梳理(5)   lambda 表示式和成員引用
執行結果為:
Kotlin 知識梳理(5)   lambda 表示式和成員引用

2.4 在作用域中訪問變數

當在函式內宣告一個匿名內部類時,能夠在這個匿名內部類引用這個函式的引數和區域性變數,也可以用lambda做同樣的事情,如果在函式內部使用lambda,也可以訪問這個函式的引數,還有在lambda之前定義的區域性變數。

下面我們用標準庫函式forEach來展示這種行為,它是最基本的集合操作函式之一:它所做的全部事情就是在集合中的每一個元素之上都呼叫給定的lambda

Kotlin 知識梳理(5)   lambda 表示式和成員引用
執行結果為:
Kotlin 知識梳理(5)   lambda 表示式和成員引用

2.4.1 在 lambda 中改變區域性變數

Kotlin中不會僅限於訪問final變數,在lambda內部也可以修改這些變數,下面的程式碼中對給定的相應狀態碼set分別進行計數。

Kotlin 知識梳理(5)   lambda 表示式和成員引用
Kotlin中,它允許在lambda內部訪問非final變數甚至修改它們。從lambda內訪問外部變數,我們稱這些 變數被 lambda 捕捉,就像上面例子中的clientErrorsserverErrors

預設情況下,區域性變數的生命週期被限制在宣告這個區域性變數的函式當中,但是如果它被lambda捕捉了,使用這個變數的程式碼可以被儲存並稍後執行,原理為:

  • 當捕捉final變數時,它的值和使用這個值的lambda程式碼一起儲存。
  • 對非final變數,它的值被封裝在一個包裝器中,這樣你就可以改變這個值,而對這個包裝器的引用會和lambda程式碼一起儲存。

2.4.2 捕捉可變變數

Java只允許捕捉final變數,而當你想捕捉可變變數的時候,可以使用兩種技巧:

  • 宣告一個單元素的陣列,其中儲存可變值
  • 建立一個包裝類的例項,其中儲存要改變的值的引用

這樣,當捕捉了一個可變變數var的時候,它的值被作為Ref類的一個例項被儲存下來,Ref變數是final的能輕易被捕捉,然後實際值儲存在其欄位中,並且可以在lambda內被修改。

2.5 成員引用

2.5.1 基本概念

在上面的例子中,我們演示了 如何讓你把程式碼塊作為引數傳遞給函式,但是如果要當做引數傳遞的程式碼已經被定義成了函式,這時候就需要 把函式轉換成一個值,這種方式稱為 成員引用

val getAge = Person :: age
複製程式碼

它提供了簡明的語法,來建立一個 呼叫單個方法或者訪問單個屬性的函式值,雙冒號把 類名稱你要引用的成員(一個方法或者屬性)名稱 隔開。

成員引用和呼叫該函式的lambda具有一樣的型別,所以可以互換使用:

people.maxBy(Person : age)
複製程式碼

2.5.2 引用頂層函式

除此之外,還可以引用頂層函式,這裡我們省略了類名稱,直接以:開頭,成員引用::salute被當作實參傳遞給庫函式run,它會呼叫相應的函式:

Kotlin 知識梳理(5)   lambda 表示式和成員引用
執行結果為:
Kotlin 知識梳理(5)   lambda 表示式和成員引用

2.5.3 儲存或者延期執行建立類例項的動作

我們還可以使用 構造方法引用 儲存或者延期執行建立類例項的動作,構造方法引用的形式是 在雙冒號後指定類的名稱

Kotlin 知識梳理(5)   lambda 表示式和成員引用

2.5.4 引用擴充套件函式

我們還可以以同樣的方式引用擴充套件函式,這裡我們定義一個擴充套件函式isAdult方法,選出年齡大於21的人。
Kotlin 知識梳理(5)   lambda 表示式和成員引用
執行結果為:
Kotlin 知識梳理(5)   lambda 表示式和成員引用

更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章