Kotlin入門系列:第四章 Lambda編程

1 Lambda表達式和成員引用

1.1 Lamda簡介:作爲函數參數的代碼塊

// java
button.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View view) {}
});

// kotlin與lamda的結合
button.setOnClickListener {...}

1.2 Lambda和集合

查找集合的最大值:maxBy

>>>val people = listOf(Person("Alice", 29), Person("Bob" 31))
>>>println(people.maxBy { it.age }) // it.age是lambda返回值,可以是任意表達式
Person(name=Bob, age=31)

maxBy 函數可以在任何集合上調用,且只需要一個實參:一個函數,指定比較哪個值來找到最大元素。

1.3 Lambda表達式的語法

// kotlin的lambda始終在花括號中,->左邊的是參數,->右邊的是表達式
// 注意實參並沒有用括號括起來
{ x: Int, y: Int -> x + y }

// 最原始的lambda表達式
>>>people.maxBy({ p: Person -> p.age })

// 如果lambda表達式是函數調用的最後一個實參,可以把表達式放到外面
>>>people.maxBy() { p: Person -> p.age }

// 當lambda是函數唯一的實參時,還可以去掉調用代碼中的空括號對
// 當你有多個實參時,既可以把lambda留在括號內來強調它是一個實參,也可以把它放在括號的外面,兩種選擇都是可行的
// 如果你想傳遞兩個或更多的lambda,不能把超過一個的lambda放到外面
>>>people.maxBy { p: Person -> p.age } 

// 如果lambda參數的類型可以被推導出來,你就不需要顯式地指定它
>>>people.maxBy { p -> p.age }

// 如果當前上下文期望的是隻有一個參數的lambda且這個參數的類型可以被推斷出來,就會生成it名稱
// 僅在實參名稱沒有顯式地指定時這個默認的名稱纔會生成
// 在嵌套lambda的情況下,最好顯式地聲明每個lambda的參數。否則,很難搞清楚it引用的到底是哪個值
// it.age是lambda返回值,可以是任意表達式
>>>people.maxBy { it.age }

// 存儲在臨時變量,需要指定具體的類型爲Person,否則lambda無法推斷具體類型
>>>val getAge = { p: Person -> p.age } 
>>>people.maxBy(getAge)

// lambda語句塊
>>>val sum = { x: Int, y: Int -> 
				println("Computing the sum of $x and $y...") 
				x + y
			 }
>>>println(sum(1, 2))			 

1.4 在作用域中訪問變量

和java的lambda不一樣的是,kotlin允許在lambda內部訪問非final變量甚至修改它們。從lambda內訪問外部變量,稱爲這些變量被lambda捕捉。

fun printlnMessagesWithPrefix(messages: Collection<String>, prefix: String) {
	// forEach函數循環集合messages
	messages.forEach {
		println("$prefix $it")
	}
}

fun printProblemCounts(responses: Collection<String>) {
	var clientErrors = 0
	var serverErrors = 0
	responses.forEach {
		if (it.startsWith("4")) {
			clientErrors++ // 修改lambda外部的非final變量
		} else if (it.startWidth("5")) {
			serverErrors++ // 修改lambda外部的非final變量
		}
	}
}

注意,默認情況下,局部變量的生命期被限制在聲明這個變量的函數中。但是如果它被lambda捕捉了,使用這個變量的代碼可以被存儲並稍後再執行。其原理如下:

  • 當你捕捉final變量時,它的值和使用這個值的lambda代碼一起存儲

  • 而對非final變量來說,它的值被封裝在一個特殊的包裝器中,這樣你就可以改變這個值,而對這個包裝器的引用會和lambda代碼一起存儲

捕獲可變變量:實現細節
java只允許你捕捉final變量,當你想捕捉可變變量時,可以使用兩種技巧:
1、聲明一個單元素的數組,其中存儲可變值
2、創建一個包裝類的實例,其中存儲要改變的值的引用

// 模擬包裝類存儲要改變的可變變量
class Ref<T>(var value: T) // 模擬捕捉可變變量的類
>>>val counter = Ref(0)
>>>val inc = { counter.value++ } // 形式上是不變量被捕捉了,但是存儲在字段中的實際值是可以修改的

// 實際使用代碼時
var counter = 0
val inc = { counter++ }

第一段代碼展示的就是第二段代碼的原理。
任何時候你捕捉了一個final變量(val),它的值被拷貝下來,這和java一樣。
當你捕捉了一個可變變量(var),它的值被作爲Ref類的一個實例被存儲下來。Ref變量是final的能輕易被捕捉,然而實際值被存儲在其字段中,並且可以在lambda內修改

如果lambda被用作事件處理器或者用在其他異步執行的情況,對局部變量的修改只會在lambda執行的時候發生:

// 始終返回0
// 這個函數正確的實現需要把點擊次數存儲在函數外依然可以訪問的地方——例如類的屬性,而不是存儲在函數的局部變量中
fun tryToCountButtonClicks(button: Button): Int {
	var clicks = 0
	button.OnClick { clicks++ }
	return clicks
}

1.5 成員引用

kotlin和java8一樣,如果把函數轉換成一個值,你就可以傳遞它,使用 :: 運算符來轉換:

val getAge = Person::age
等價於
val getAge = { p: Person -> p.age }

這種表達式稱爲成員引用,它提供了簡明語法,來創建一個調用單個方法或者訪問單個屬性的函數值。雙冒號把類名稱與你要引用的成員(一個方法或者一個屬性)名稱隔開。

注意,不管你引用的是函數還是屬性,都不要在成員引用的名稱後面加括號。

fun salutes() = println("Salute!")
>>>run(::salutes) // 引用頂層函數

成員引用 ::salute 被當作實參傳遞給函數run,它會調用相應的函數。

如果lambda要委託給一個接收多個參數的函數,提供成員引用代替它將會非常方便:

// lambda委託給sendEmail函數
val action = { person: Person, message: String -> sendEmail(person, message) }
// 用成員引用代替
val nextAction = ::sendEmail

構造方法引用存儲或者延期執行創建類實例的動作。構造方法引用的形式是在雙冒號指定類名稱:

data class Person(val name: String, val age: Int)

>>>val createPerson = ::Person // 創建Person實例的動作被保存成了值
>>>val p = createPerson("Alice", 29)
>>>println(p)

還可以用同樣的方式引用擴展函數:

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult

2 集合的函數式API

2.1 基礎:filter和map

filter 函數遍歷集合並選出應用給定lambda後會返回true的那些元素:

>>>val list = listOf(1, 2, 3, 4)
>>>println(list.filter { it % 2 == 0 }) // 篩選打印出偶數

map 函數對集合中的每一個元素應用給定的函數並把結果收集到一個新集合:

>>>>val list = listOf(1, 2, 3, 4)
// 結果是一個新集合,包含的元素個數不變,但是每個元素根據給定的判斷式做了變換
>>>pritln(list.map { it * it })

輸出:
[1, 4, 9, 6]

lambda雖然簡單簡潔,但也要注意執行的lambda的原理避免多餘的計算:

// filter傳遞每一個元素判斷後,filter的元素又會去執行people.maxBy()遍歷,people.maxBy()放在裏面是多餘的計算
people.filter { it.age == people.maxBy(Person::age)?.age } 

// 先查找出最大年齡在去執行filter減少計算
val maxAge = people.maxBy(Person::age)?.age
people.filter { it.age == maxAge }

對map應用過濾和變換函數:

>>>val numbers = mapOf(0 to "zero", 1 to "one")
>>>println(numbers.mapValues { it.value.toUpperCase() })

輸出:
{0=ZERO, 1=ONE}

鍵和值分別由各自的函數來處理。filterKeysmapKeys 過濾和變換map的鍵,而另外的 filterValuesmapValues 過濾和變換對應值。

2.2 all、any、count和find:對集合應用判斷式

檢查集合中的所有元素是否都複合某個條件(或者它的變種,是否存在符合的元素)。它們是通過 allany 函數表達的。count 函數檢查有多少個元素滿足判斷式,而 find 函數返回第一個符合條件的元素。

val canBeInClub27 = { p: Person -> p.age <= 27 }

如果你對是否所有元素都滿足判斷式感興趣,應該使用 all 函數:

>>>val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>>println(people.all(canBeInClub27))

輸出結果:
false

如果你需要檢查集合中是否至少存在一個匹配的元素,那就用 any

>>>println(people.any(canBeInClub27))

輸出結果:
true

注意,!all 加上某個條件,可以用 any 加上這個條件的取反來替換,應該儘量用確定的判斷來表達:

>>>val list = listOf(1, 2, 3)
>>>prinltn(!list.all { it == 3 })
>>>println(list.ayn { it != 3 }) // 應該使用這種方式

如果你想知道有多少個元素滿足了判斷式,使用 count

>>>val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>>println(people.count(canBeInClub27))

輸出結果:
1

count 方法容易被遺忘,然後通過過濾集合之後再取大小來實現它:

// 這種情況下,filter會創建一箇中間集合並用來存儲所有滿足判斷式的元素
// 而count只是跟蹤匹配元素的數量,不關心元素本身,所以更高效
>>>println(people.filter(canBeInClub27).size)

要找到一個滿足判斷式的元素,使用 find 函數:

>>>val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>>println(people.find(canBeInClub27))

輸出結果:
Person(name=Alice, age=27)

如果有多個匹配的元素就返回其中第一個元素;或者返回null,如果沒有一個元素能滿足判斷式。find 還有一個同義方法 firstOrNull

2.3 groupBy:把列表轉換成分組的map

groupBy 函數能把所有元素按照不同的特徵劃分成不同的分組。操作的結果是一個map:

>>>val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31))
>>>println(people.groupBy { it.age })

輸出結果:
{29=[Person(name=Bob, age=29)],
31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}

每一個分組都是存儲在一個列表中,上面的例子結果類型就是 Map<Int, List<Person>>。可以使用像 mapKeysmapValues 這樣的函數對這個map做進一步的修改。

2.4 flatMap和flatten:處理嵌套集合中的元素

class Book(val title: String, val authors: List<String>)

books.flatMap { it.authors }.toSet()

flatMap 函數做了兩件事情:首先根據作爲實參給定的函數對集合中的每個元素做變換(或者說映射),然後把多個列表合併(或者說平鋪)成一個列表。

>>>val strings = listOf("abc", "def")
>>>println(strings.flatMap { it.toList() })

輸出結果:
[a, b, c, d, e, f]

在這裏插入圖片描述

>>>val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")), 
					  Book("Mort", listOf("Terry Pratchett")),
					  Book("Good Omens", listOf("Terry Pratchett", "Neil Gaiman")))
>>>println(books.flatMap { it.authors }.toSet())

輸出結果:
[Jasper Fforde, Terry Pratchett, Neil Gaiman]

當你卡殼在元素集合的集合不得不合併成一個的時候,你可能會想起 flatMap 來。注意,如果你不需要做任何變換,只是需要平鋪一個集合,可以使用 flatten 函數:listOfLists.flatten()

3 惰性集合操作:序列

people.map(Person::name).filter { it.startWith("A") }

kotlin標準庫參考文檔有說明,flitermap 都會返回一個列表。這意味着上面例子中的鏈式調用會創建兩個列表:一個保存 filter 函數的結果,另一個保存 map 函數的結果。如果數量龐大時調用將會非常低效。

kotlin惰性集合操作的入口就是 Sequence 接口。這個接口表示的就是一個可以逐個列舉元素的元素序列。Sequence 只提供了一個方法 iterator,用來從序列中獲取值。

Sequence 接口的強大之處在於其操作的實現方式。序列中的元素求值時惰性的。因此,可以使用序列更高效地對集合元素執行鏈式操作,而不需要創建額外的集合來保存過程中產生的中間結果。

擴展函數 asSequence 把任意集合轉換成序列,調用 toList 來做反向的轉換。序列轉換集合的場景是,如果你只需要迭代序列中的元素,可以直接使用序列。如果你要用其他的API方法,比如用下標訪問元素,那麼你需要把序列轉換成列表。

3.1 執行序列操作:中間和末端操作

序列操作分爲兩類:中間的和末端的。一次中間操作返回的是另一個序列,這個新序列知道如何變換原始序列中的元素。而一次末端操作返回的是一個結果,這個結果可能是集合、元素、數字,或者其他從初始集合的變換序列中獲取的任意對象。

// map、filter都是中間操作,toList()是末端操作
sequence.map { ... }.filter { ... }.toList()

>>>listOf(1, 2, 3, 4).asSequence()
			.map { print("map($it)  "); it * it }
			.filter { print("filter($it)  "; it % 2 == 0 }
			.toList() // 沒有末端操作,map和filter被延期了,只有調用toList()末端操作獲取結果時纔會調用map和filter

輸出結果:
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)

值得注意的是計算執行順序。

  • 普通集合調用 mapfilter:在每個元素上調用 map 函數,然後在結果序列的每個元素上再調用 filter 函數(即先對集合的所有元素都執行了 map 後,再將map後的集合迭代給 filter )。

  • 序列集合調用 mapfilter:所有操作是按順序應用在每一個元素上:處理完第一個元素(先 mapfilter),然後完成第二個元素的處理,以此類推。

在這裏插入圖片描述

3.2 創建序列

generateSequence 函數:給定序列中的前一個元素,這個函數會計算出下一個元素:

>>>val naturalNumbers = genrateSequence(0) { it + 1 }
>>>val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>>println(numbersTo100.sum()) // 調用sum時纔開始執行

輸出結果:
5050

4 使用java函數式接口

4.1 把lambda當作參數傳遞給java方法

void postponeComputation(int delay, Runnable computation);

在kotlin中,可以調用它並把一個lambda作爲實參傳給它。編譯器會自動把它轉換成一個Runnable實例:

postponeComputation(1000) { println(42) }

注意,當我們說“一個Runnable實例”時,指的是“一個實現了Runnable接口的匿名類的實例”。

通過顯式地創建一個實現了Runnable的匿名對象也能達到同樣的效果:

postponeComputation(1000, object: Runnable {
	override fun run() {
		println(42)
	}
})

但是這裏有一點不一樣。當你顯式地聲明對象時,每次調用都會創建一個新的實例。使用lambda的情況不同:如果lambda沒有訪問任何來自定義它的函數的變量,相應的匿名類實例可以在多次調用之間重用:

// 使用lambda每次調用該函數,只會重用一個Runnable,不會創建新的實例
postponeComputation(1000) { println(42) } 
等價於
val runnable = Runnable { println(42) }
fun handleComputation() {
	postponeComputation(1000, runnable)
}

如果lambda從包圍它的作用域中捕捉了變量,每次調用就不再可能重用同一個實例了。這種情況下,每次調用時編譯器都要創建一個新的對象,其中存儲着被捕捉的變量的值。

fun handleComputation(id: String) {
	postponeComputation(1000) { println(id) } // lambda捕捉了id,每次調用都會創建Runnable
}

lambda的實現細節:

自kotlin1.0起,每個lambda表達式都會被編譯成一個匿名類,除非它是一個內聯lambda。
後續版本計劃支持生成java8字節碼。一旦實現,編譯器就可以避免爲每一個lambda表達式都生成一個獨立的.class文件。
如果lambda捕捉了變量,每個被捕捉的變量會在匿名類中有對應的字段,而且每次調用都會創建一個這個匿名類的新實例。
否則,一個單例就會被創建。類的名稱由lambda聲明所在的函數名字稱加上後綴衍生出來:

class HandleComputation$1(val id: String): Runnable {
	override fun run() {
		println(id)
	}
}
fun handleComputation(id: String) {
	postponeComputation(1000, HandleComputation$1(id)) // 底層創建的是一個特殊類的實例,而不是一個lambda
}

4.2 SAM構造方法:顯式地把lambda轉換成函數式接口

SAM構造方法是編譯器生成的函數,讓你執行從lambda到函數式接口實例的顯式轉換。可以在編譯器不會自動應用轉換的上下文中使用它。例如,如果有一個方法返回的是一個函數式接口的實例,不能直接返回一個lambda,要用SAM構造方法把它包裝起來。

fun createAllDoneRunnable(): Runnable {
	return Runnable { println("All done!") }
}
>>>createAllDoneRunnable().run()

SAM構造方法的名稱和底層函數式接口的名稱一樣。SAM構造方法只接收一個參數——一個被用作函數式接口單抽象方法體的lambda——並返回實現了這個接口的類的一個實例。

除了返回值外,SAM構造方法還可以用在需要把從lambda生成的函數式接口實例存儲在一個變量中的情況。

val listener = OnClickListener { view ->
	val text = when(view.id) {
		R.id.button1 -> "First button"
		R.id.button2 -> "Second button"
		else -> "Unknown button"
	}
	toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)

lambda和添加/移除監聽器:

注意lambda內部沒有匿名對象那樣的 `this`:
沒有辦法引用到lambda轉換成的匿名類實例。
從編譯器的角度來看,lambda是一個代碼塊,不是一個對象,而且也不能把它當成對象引用。lambda中的 `this` 引用指向的是包圍它的類。

儘管方法調用中的SAM轉換一般都自動生成,但是當把lambda作爲參數傳給一個重載方法時,也有編譯器不能選擇正確的重載情況。這時,使用顯式的SAM構造方法是解決編譯器錯誤的好方法。

5 帶接收者的lambda:with與apply

5.1 with函數

// 多次的使用一個臨時變量result,可以用with解決
fun alphabet(): String {
	val result = StringBuilder()
	for (letter in 'A'..'Z') {
		result.append(letter)
	}
	result.append("\nNow I know the alphabet!")
	return result.toString()
}

// this可有可無
// with(stringBuilder, { ... })也是可以,但是可讀性比較差
fun alphabet(): String {
	val stringBuilder = StringBuilder()
	return with(stringBuilder) {
		for (letter in 'A'..'Z') {
			this.append(letter)
		}
		this.append("\nNow I know the alphabet!")
		return toString()
	}
}
或
fun alphabet(): String = with(StringBuilder()) {
	for (letter in 'A'..'Z') {
		append(letter)
	}
	append("\nNow I know the alphabet!")
	return toString()
}

with 結構看起來像是一種特殊的語法結構,但它實際上是一個接收兩個參數的函數:這個例子中兩個參數分別是 stringBuilder 和一個lambda。

with 函數把它的第一個參數轉換成作爲第二個參數傳給它的lambda的接收者。可以顯式地通過 this 引用來訪問這個接收者。或者,按照慣例,可以省略 this 引用,不用任何限定符直接訪問這個值的方法和屬性。

方法名稱衝突:

如果你當作參數傳給with的對象已經有這樣的方法,該方法的名稱和你正在使用with的類中的方法一樣,怎麼辦?
這種情況下,可以給this引用加上顯式標籤來表明你要調用的是哪個方法。
假設函數alphabet是類OuterClass的一個方法,如果你想引用的是定義在外部類的toString方法而不是StringBuilder,可以如下:

this@OuterClass.toString()

with 返回的值是執行lambda代碼的結果,該結果就是lambda中的左後一個表達式的值。有時候你想返回的是接收者對象(StringBuilder),而不是執行lambda的結果,可以使用 apply 函數。

5.2 apply函數

apply 函數幾乎和 with 函數一摸一樣,唯一的區別是 apply 始終會返回作爲實參傳遞給它的對象(即接收者對象)。

// apply函數返回了StringBuilder對象
fun aplhabet() = StringBuilder().apply {
	for (letter in 'A'..'Z') {
		append(letter)
	}
	append("\nNow I know the alphabet!")
}.toString()

6 內聯函數

kotlin在集合API中大量使用了Lambda,這確實使得我們在對集合進行操作的時候優雅了許多。但是這種方式的代價就是,在kotlin中使用Lambda表達式會帶來一些額外的開銷。

kotlin中的內聯函數之所以被設計出來,主要是爲了優化kotlin支持Lambda表達式之後所帶來的開銷。然而,在java中我們卻似乎不需要特別關注這個問題,因爲在java 7之後,JVM引入了一種叫做 invokedynamic 技術自動幫助我們優化Lambda。

6.1 優化Lambda開銷

kotlin中每聲明一個Lambda表達式,就會在字節碼中產生一個匿名類。該匿名類包含一個 invoke(),作爲Lambda的調用方法,每次調用的時候還會創建一個新對象。Lambda雖然簡潔,但是額外增加的開銷也不少。

6.1.1 invokedynamic

在Java 7之後通過 invokedynamic 技術實現了在運行期才產生相應的翻譯代碼。在invokedynamic被首次調用的時候,就會觸發產生一個匿名類來替換中間碼invokedynamic,後續的調用會直接採用這個匿名類的代碼。這樣的好處是:

  • 由於具體的轉換實現是在運行時產生的,在字節碼中能看到的只有一個固定的invokedynamic,所以需要靜態生成的類的個數及字節碼大小都顯著減少

  • 與kotlin編譯時寫死在字節碼中的策略不同,利用invokedynamic可以把實際的翻譯策略隱藏在jdk庫的實現,這極大提高了靈活性,在確保向後兼容性的同時,後期可以繼續對翻譯策略不斷優化升級

  • JVM天然支持了針對該方式的Lambda表達式的翻譯和優化,這也意味着開發者在書寫Lambda表達式的同時,可以完全不用關心這個問題,極大提升了開發的體驗

6.1.2 inline:內聯函數

kotlin在一開始就需要兼容Android最主流的java 6,這導致它無法通過invokedynamic來解決Android平臺的Lambda開銷問題。

通過關鍵字 inline 修飾方法成爲內聯函數,內聯函數在編譯期會被嵌入每一個被調用的地方,以減少額外生成的匿名類數以及函數執行的時間開銷。

fun main(args: Array<String>) {
	foo {
		println("dive into kotlin...")
	}
}

fun foo(block: () -> Unit) {
	println("before block")
	block()
	println)"end block")
}

將上面的代碼反編譯爲java:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
   this.foo((Function0)null.INSTANCE);
}

public final void foo(@NotNull Function0 block) {
   Intrinsics.checkParameterIsNotNull(block, "block");
   String var2 = "before block";
   boolean var3 = false;
   System.out.println(var2);
   block.invoke();
   var2 = "end block";
   var3 = false;
   System.out.println(var2);
}

調用 foo() 就會產生一個 Function0 類型的 block 類,通過 invoke() 來執行,這會增加額外的生成類和調用開銷。現在添加 inline 修飾符:

fun main(args: Array<String>) {
    foo {
        println("dive into kotlin...")
    }
}

inline fun foo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

再反編譯爲java:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
   int $i$f$foo = false;
   String var4 = "before block";
   // block函數體開始粘貼
   boolean var5 = false;
   System.out.println(var4);
   int var6 = false;
   String var7 = "dive into kotlin...";
   boolean var8 = false;
   System.out.println(var7);
   // block函數結束粘貼
   var4 = "end block";
   var5 = false;
   System.out.println(var4);
}

public final void foo(@NotNull Function0 block) {
   int $i$f$foo = 0;
   Intrinsics.checkParameterIsNotNull(block, "block");
   String var3 = "before block";
   boolean var4 = false;
   System.out.println(var3);
   block.invoke();
   var3 = "end block";
   var4 = false;
   System.out.println(var3);
}

通過內聯函數 foo 函數體代碼及調用的Lambda代碼都粘貼到了相應調用的位置。這樣就徹底消除額外調用,從而節約了開銷。

內聯函數典型的應用場景是kotlin的集合類,可以看下操作符的源碼:

inline fun <T, R> Array<out T>.map(transform: (T) -> R): List<R>

inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T>

但內聯函數不是萬能的,以下情況應避免使用內聯函數:

  • JVM對普通的函數已經能夠根據實際情況智能地判斷是否進行內聯化,所以普通函數不需要添加 inline,否則只會讓字節碼變得更加複雜

  • 儘量避免對具有大量函數體的函數進行內聯,這樣會導致過多的字節碼數量

  • 一旦一個函數被定義爲內聯函數,便不能獲取閉包類的私有成員,除非你把它們聲明爲 internal

6.2 noinline:避免參數被內聯

在現實中情況比較複雜,有一種可能是函數需要接收多個參數,但我們只想對其中部分Lambda內聯,其他的則不內聯。

通過 noinline 關鍵字,我們可以把它加在不想要內聯的參數開頭,該參數便不會具有內聯的效果。

fun main(args: Array<String>) {
    foo ({
        println("I am inlined...")
    }, {
        println("I'm not inlined...")
    })
}

inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {
    println("before block")
    block1()
    block2()
    println("end block")
}

反編譯爲java:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
   Function0 block2$iv = (Function0)null.INSTANCE;
   int $i$f$foo = false;
   String var5 = "before block";
   boolean var6 = false;
   System.out.println(var5);
   int var7 = false;
   // block1被內聯了
   String var8 = "I am inlined...";
   boolean var9 = false;
   System.out.println(var8);
   // block2原樣每被內聯
   block2$iv.invoke();
   var5 = "end block";
   var6 = false;
   System.out.println(var5);
}

public final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {
   int $i$f$foo = 0;
   Intrinsics.checkParameterIsNotNull(block1, "block1");
   Intrinsics.checkParameterIsNotNull(block2, "block2");
   String var4 = "before block";
   boolean var5 = false;
   System.out.println(var4);
   block1.invoke();
   block2.invoke();
   var4 = "end block";
   var5 = false;
   System.out.println(var4);
}

6.3 非局部返回

kotlin中的內聯函數除了優化Lambda之外,還帶來了其他方面的特效,典型的就是非局部返回和具體化參數類型。

fun main(args: Array<String>) {
	foo()
}
fun localReturn() {
	return
}
fun foo() {
	println("before local return")
	localReturn()
	println("after local return")
	return
}

輸出:
before local return
after local return

localReturn() 執行後,其函數體中的 return 只會在該函數的局部生效,所以 localReturn() 之後的 println() 依舊生效。

我們再把函數換成Lambda:

fun main(args: Array<String>) {
	foo {
		return
	}
}
fun foo(returning: () -> Unit) {
	println("before local return")
	returning()
	println("after local return")
	return
}

輸出:
Error:(2, 11) Kotlin: 'return' is not allowed here

編譯報錯了,在kotlin中,正常情況下Lambda表達式不允許存在 return 關鍵字。

那我們換成內聯函數:

fun main(args: Array<String>) {
	foo {
		return
	}
}
inline fun foo(returning: () -> Unit) {
	println("before local return")
	returning()
	println("after local return")
	return
}

輸出:
before local return

編譯通過了,而且 return 後直接讓 foo() 退出了執行。其實細想以下也很簡單,內聯函數是將編譯是是將代碼粘貼到過去的,相當於 return 是直接暴露在 main 函數中。這個也就是非局部返回。

還有一種等效的寫法:

fun main(args: Array<String>) {
	foo {
		return@foo // 非局部返回
	}
}
fun foo(returning: () -> Unit) {
	println("before local return")
	returning()
	println("after local return")
	return
}

非局部返回在循環控制中特別有用,比如kotlin的 forEach,它接收的就是一個Lambda參數,由於它是一個內聯函數,所以可以直接在它調用的Lambda中執行 return 退出上一層程序:

fun hasZeros(list: List<Int>): Boolean {
	list.forEach {
		if (it == 0) return true
	}
	return false
}

6.4 crossinline

值得注意的是,非局部返回雖然在某些場合下非常有用,但可能也存在危險。因爲有時候,我們內聯函數所接收的Lambda參數常常來自於上下文其他地方。爲了避免帶有 return 的Lambda參數產生破壞,可以使用 crossinline 關鍵字修飾該參數,從而杜絕此類問題的發生:

fun main(args: Array<String>) {
	foo { return }
}
inline fun foo(crossinline returning: () -> Unit {
	println("before local return")
	returning()
	println("after local return")
	return
}

輸出:
Error(2, 11) Kotlin: 'return' is not allowed here
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章