Kotlin 語言學習(4) - 數據類、類委託 及 object 關鍵字
Kotlin 語言學習(5) - lambda 表達式和成員引用
一、本文概要
二、聲明高階函數
按照定義,高階函數就是 以另一個函數作爲參數或者返回值的函數,在Kotlin
中,函數可以用lambda
或者函數引用來表示。例如,標準庫中的filter
函數將一個判斷式函數作爲參數,因此它就是一個高階函數。
list.filter { x > 0 }
2.1 函數類型
爲了聲明一個以lambda
作爲實參的函數,你需要知道如何聲明 對應形參的類型。下面我們先看一個簡單的例子,把lambda
表達式保存在局部變量當中:
val sum = { x : Int, y : Int -> x + y }
val action = { println(42) }
在上面的例子中,我們省去了類型的聲明。但是編譯器可以推導出sum
和action
這兩個 變量具有函數類型,這些變量的顯示聲明爲:
//有兩個 Int 型參數和 Int 型返回值的函數
val sum : (Int, Int) -> Int = {x, y -> x + y}
//沒有參數和返回值的函數
val action : () -> Unit = { println(42) }
聲明函數類型,需要 將函數參數類型放在括號中,緊接着是一個箭頭和函數的返回類型:
(Int, String) -> Unit
Unit
類型用於表示函數不返回任何有用的值,在聲明一個普通的函數時,Unit
類型的返回值是可以忽略的,但是一個 函數類型聲明總是需要一個顯示的返回類型,所以在這種場景下Unit
是不能省略的。
在{x, y -> x + y}
中,因爲它們的類型已經在函數類型的變量聲明部分指定了,不需要在lambda
當中重複聲明。
就像其它方法一樣,函數類型的返回值也可以標記爲可空類型:
var canReturnNull : (Int, Int) -> Int? = { null }
也可以定義一個 函數類型的可空變量,爲了明確表示 變量本身可空,而不是函數類型的返回類型可空,你需要 將整個函數類型的定義包含在括號內並在括號後添加一個問號:
var funOrNull : ((Int, Int) -> Int)? = null
函數類型的參數名
可以爲函數類型聲明中的參數指定名字:
//函數類型的參數現在有了名字...
fun performRequest(url : String, callback : (code : Int, content : String) -> Unit) {
//....
}
調用方法爲:
>> val url = "http://kotl.in"
//可以使用 API 中提供的參數名字作爲 lambda 參數的名字....
>> performRequest(url) { code, content -> / *...* / }
>> performRequest(url) { code, page -> / *...* / }
2.2 調用作爲參數的函數
下面我們討論如何實現一個高階函數,這個例子會盡量簡單並且使用之前的lambda sum
同樣的聲明,這個函數實現對於兩個整數的任意操作,然後打印出結果:
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
fun main(args: Array<String>) {
twoAndThree { a, b -> a + b }
twoAndThree { a, b -> a * b }
}
運行結果爲:
>> The result is 5
>> The result is 6
調用作爲參數的函數operation
和調用普通函數的語法是一樣:把括號放在函數名後,並把參數放在括號內。下面,讓我們實現一個標準的庫函數:filter
函數。它會過濾掉字符串中不屬於a..z
範圍內的字母。
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
for (index in 0 until length) {
val element = get(index)
if (predicate(element)) sb.append(element)
}
return sb.toString()
}
filter
函數以一個判斷式作爲參數,判斷式的類型是一個函數,以字符串作爲參數並返回boolean
類型的值。
fun main(args: Array<String>) {
println("ab1c".filter { it in 'a'..'z' })
}
運行結果:
>> abc
2.3 在 Java 中使用函數
背後的原理是:
- 函數類型被聲明爲普通的接口:一個函數類型的變量是
FunctionN
接口的一個實現。Kotlin
標準庫定義了一系列的接口:Function0<R>
表示沒有參數的函數,Function1<P1, R>
表示一個參數的函數。 - 一個函數類型的變量就是實現了對應的
Function
接口的實現類的實例,每個接口定義了一個invoke
方法,實現類的invoke
方法包含了lambda
函數體,調用這個方法就會執行函數。
在Java
中可以很簡單地調用使用了函數類型的Kotlin
函數,Java 8
的lambda
會被自動轉換爲函數類型的值:
//Kotlin 聲明
fun processTheAnswer(f : (Int) -> Int) {
println(f(42))
}
//Java
processTheAnswer(number -> number + 1)
在舊版的Java
中,可以傳遞一個實現了函數接口中的invoke
方法的匿名內部類的實例:
>> processTheAnswer(
new Function1<Integer, Integer>() {
@override
public Integer invoke(Integer number) {
System.out.println(number);
return number + 1;
}
}
)
在Java
中可以很容易地使用Kotlin
標準庫中以lambda
作爲參數的擴展函數,但是必須要 顯示地傳遞一個接收者作爲第一個參數:
List<String> strings = new ArrayList();
strings.add("42");
CollectionsKt.forEach(strings, s -> {
System.out.println(s);
retrun Unit.INSTANCE;
});
在Java
中,函數或者lambda
可以返回Unit
。但因爲在Kotlin
中Unit
類型是有一個值的,所以需要顯示地返回它。一個返回void
的lambda
不能作爲返回Unit
的函數類型的實參,就像之前的例子中的(String) -> Unit
。
2.4 函數類型的參數默認值和 null 值
2.4.1 函數類型的參數默認值
以joinToString
函數爲例,我們除了可以定義前綴、後綴和分隔符以外,還可以通過最後一個 函數類型的參數 指定如何將集合當中的每個元素轉換成爲String
,這是一個泛型函數:它有一個類型參數T
表示集合中的元素的類型,Lambda transform
將接收這個類型的參數,下面我們來看一下如何爲它指定一個lambda
作爲默認值:
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
//爲函數類型的參數提供默認值。
transform: (T) -> String = { it.toString() }
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
//調用傳入的函數。
result.append(transform(element))
}
result.append(postfix)
return result.toString()
}
fun main(args: Array<String>) {
val letters = listOf("Alpha", "Beta")
println(letters.joinToString())
println(letters.joinToString { it.toLowerCase() })
println(letters.joinToString(separator = "! ", postfix = "! ",
transform = { it.toUpperCase() }))
}
運行結果爲:
>> Alpha, Beta
>> alpha, beta
>> ALPHA! BETA!
2.4.2 聲明一個參數可爲空的函數類型
當聲明一個參數爲可空的函數類型時,不能直接調用作爲參數傳遞進來的函數:Kotlin
會因爲檢測到潛在的空指針而導致編譯失敗,在這種情況下有兩種處理方式:
- 顯示地檢查
null:
顯示地檢查null
是一種比較容易理解的方法。
fun foo(callback : (() _ Unit)?) {
if (callback != null) {
callback()
}
}
- 通過安全調用語法調用:除此之外,因爲函數類型是一個包含
invoke
方法的接口的具體實現,作爲一個普通方法,invoke
可以通過安全調用語法調用。
callback?.invoke() ?: /* 默認實現 */
2.5 返回函數的函數
從函數中返回另一個函數適用於下面的場景:程序中的一段邏輯可能會因爲程序的狀態或者其他條件而產生變化,比如說下面的例子,運輸費用的計算依賴於選擇的運輸方式:
//聲明一個枚舉類型。
enum class Delivery { STANDARD, EXPIRED }
class Order(val itemCount : Int)
//返回的函數類型爲:形參爲 Order 類,返回類型爲 Double。
fun getShippingCalculator(delivery : Delivery) : (Order) -> Double {
if (delivery == Delivery.EXPIRED) {
return { order -> 6 + 2.1 * order.itemCount }
}
return { order -> 1.2 * order.itemCount }
}
fun main(args: Array<String>) {
val calculator = getShippingCalculator(Delivery.EXPIRED)
println("cost ${calculator(Order(3))}")
}
在上面的例子中,getShippingCalculator
返回了一個函數,這個函數以Order
作爲參數並返回一個Double
類型的值,要返回一個函數,需要寫一個return
表達式,跟上一個lambda
、一個成員引用,或者其他的函數類型的表達式。
下面,我們來看一個過濾器的例子:
data class Person(val firstName : String, val phoneNumber : String?)
class ContactListFilter {
var prefix : String = ""
var onlyWithPhoneNumber : Boolean = false
fun getPredicate() : (Person) -> Boolean {
val startWithPrefix = { p : Person ->
p.firstName.startsWith(prefix)
}
if (!onlyWithPhoneNumber) {
return startWithPrefix
}
return { startWithPrefix(it) && it.phoneNumber != null }
}
}
fun main(args: Array<String>) {
val contacts = listOf(Person("Dmitry", "123-4567"),
Person("Svelana", null))
val contactListFilters = ContactListFilter()
contactListFilters.prefix = "S"
contactListFilters.onlyWithPhoneNumber = false
println(contacts.filter(contactListFilters.getPredicate()))
}
運行結果爲:
>> [Person(firstName=Svelana, phoneNumber=null)]
2.6 通過 lambda 去除重複代碼
我們來看一個分析網站的例子,SiteVisit
類用來保存每次訪問的路徑、持續時間和用戶的操作系統。
data class SiteVisit(
val path: String,
val duration: Double,
val os: OS
)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
val log2 = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
接下來,我們通過擴展函數的方式,定義一個方法用於統計 符合特定條件 的操作系統用戶的平均使用時長。
fun List<SiteVisit>.averageDuration(predicate : (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
運行下面的代碼:
fun main(args: Array<String>) {
println(log2.averageDuration {it.os in setOf(OS.WINDOWS, OS.ANDROID) })
}
對於一些廣爲人知的設計模式可以使用函數類型和lambda
表達式進行簡化,比如策略模式。沒有lambda
表達式的情況下,你需要聲明一個接口,併爲每一種可能的策略提供實現類。使用函數類型,可以用一個通用的函數類型來描述策略,然後傳遞不同的lambda
表達式作爲不同的策略。