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

Kotlin 知識梳理系列文章

Kotlin 知識梳理(1) - Kotlin 基礎
Kotlin 知識梳理(2) - 函數的定義與調用
Kotlin 知識梳理(3) - 類、對象和接口
Kotlin 知識梳理(4) - 數據類、類委托 及 object 關鍵字
Kotlin 知識梳理(5) - lambda 表達式和成員引用
Kotlin 知識梳理(6) - Kotlin 的可空性
Kotlin 知識梳理(7) - Kotlin 的類型系統
Kotlin 知識梳理(8) - 運算符重載及其他約定
Kotlin 知識梳理(9) - 委托屬性
Kotlin 知識梳理(10) - 高階函數:Lambda 作為形參或返回值
Kotlin 知識梳理(11) - 內聯函數
Kotlin 知識梳理(12) - 泛型類型參數


一、本文概要

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

二、Lambda 表達式和成員引用

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

2.1 Lambda 簡介:作為函數參數的代碼塊

Lambda的應用場景有:

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

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

button.setOnClickListener { /* 點擊后執行的動作 */}

2.2 Lambda 和集合

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


運行結果為:

這上面的例子用到了集合上的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表達式存儲在一個變量中,把這個變量當做普通函數對待(即通過相應的實參調用它):

2.3.2 直接調用 Lambda 表達式

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


運行結果為:

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函數以外的方法來把一個 元素轉換成字符串,下面顯示如何 只打印出人的名字


運行結果為:

2.3.5 lambda 表達式包含更多語句

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


運行結果為:

2.4 在作用域中訪問變量

當在函數內聲明一個匿名內部類時,能夠在這個匿名內部類引用這個函數的參數和局部變量,也可以用lambda做同樣的事情,如果在函數內部使用lambda,也可以訪問這個函數的參數,還有在lambda之前定義的局部變量。

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


運行結果為:

2.4.1 在 lambda 中改變局部變量

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


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,它會調用相應的函數:


運行結果為:

2.5.3 存儲或者延期執行創建類實例的動作

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

2.5.4 引用擴展函數

我們還可以以同樣的方式引用擴展函數,這里我們定義一個擴展函數isAdult方法,選出年齡大于21的人。


運行結果為:

參考文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容