那些年,我們看不懂的那些Kotlin標準函式

歐陽鋒發表於2018-04-26

Kotlin標準庫中提供了一套用於常用操作的函式。最近,在我的Kotlin交流群中有人再次問到了關於這些函式的用法。今天,讓我們花一點時間,一起看一下這些函式的用法。

Ready go >>>

注:這裡所說的標準函式主要來自於標準庫中在檔案Standard.kt中的所有函式。

run#1

@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
複製程式碼

contract部分主要用於編譯器上下文推斷,這裡我們忽略掉這部分程式碼。

觀察原始碼發現,run方法僅僅是執行傳入的block表示式並返回執行結果而已(block是一個lambda表示式)。

因此,如果你僅僅需要執行一個程式碼塊,可以使用該函式

看一個例子:

val x = run {
           println("Hello, world")
           return@run 1
        }
println(x)

// 執行結果
Hello,world
1
複製程式碼

run#2

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
複製程式碼

這個函式跟上面的函式功能是完全一樣的。不同的是,block的receiver是當前呼叫物件,即在block中可以使用當前物件的上下文。

因此,如果你需要在執行的lambda表示式中使用當前物件的上下文的話,可以使用該函式。除此之外,兩者沒有任何差別

看一個例子:

class A {
    fun sayHi(name: String) {
        println("Hello, $name")
    }
}

class B {

}

fun main(args: Array<String>) {
    val a = A()
    val b = a.run {
        // 這裡你可以使用A的上下文
        a.sayHi("Scott Smith")
        return@run B()
    }
    println(b)
}

// 執行結果
Hello,Scott Smith
b@2314
複製程式碼

從例子中,我們可以看到,這個函式還可以用於對資料型別進行轉換。

with

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
複製程式碼

這個函式其實和run函式也是做了一樣的事情。不同的是,這裡可以指定block的接收者。

因此,如果你在執行lambda表示式的時候,希望指定不同的接收者的話,可以使用該方法

class A {
    fun sayHi(name: String) {
        println("Hello, $name")
    }
}


fun main(args: Array<String>) {
    val a = A()
    with(a) {
        // 這裡的接收者是物件a,因此可以呼叫a例項的所有方法
        sayHi("Scott Smith")
    }
}

複製程式碼

apply

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
複製程式碼

可以看到,這個方法是針對泛型引數的擴充套件方法,即所有物件都將擁有該擴充套件方法。相對於run#2方法,apply不僅執行了block,同時還返回了receiver本身。

這在鏈式程式設計中很常用,如果你希望執行lambda表示式的同時而不破壞鏈式程式設計,可以使用該方法

看一個例子:

class A {
    fun sayHi(name: String) {
        println("Hello, $name")
    }
    
    fun other() {
        println("Other function...")
    }
}


fun main(args: Array<String>) {
    val a = A()
    a.apply { 
        println("This is a block")
        sayHi("Scott Smith")
    }.other()
}

// 執行結果
This is a block
Hello, Scott Smith
Other function...
複製程式碼

also

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}
複製程式碼

這個函式跟with又很像,不同的是,block帶有一個當前receiver型別的引數。在block中,你可以使用該引數對當前例項進行操作。

這個函式和with完全可以互相通用,with函式可以直接在當前例項上下文中對其進行操作,而also函式要通過block引數獲取當前類例項。因為用法完全一致,這裡就不舉例了

let

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
複製程式碼

如果你使用過RxJava,可能會感到似曾相識,這其實就是RxJava的map函式。這個函式也是針對泛型引數的擴充套件函式,所有類都將擁有這個擴充套件函式。

如果你希望對當前資料型別進行一定的轉換,可以使用該方法。該方法的block中同樣可以使用當前receiver的上下文

看一個例子:

class Triangle {}

class Rectangle {}

fun main(args: Array<String>) {
    val tr = Triangle()
    val rect = tr.let { it ->
        println("It is $it")
        return@let Rectangle()
    }
    println(rect)
}

// 執行結果
It is Triangle@78308db1
Rectangle@27c170f0
複製程式碼

從例子中可以看到,我們成功地將三角形轉換成了矩形,這就是let函式的作用。

takeIf

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}
複製程式碼

這個函式也是針對泛型引數的擴充套件函式,所有類都將擁有這個擴充套件。這個函式使用了一個預言函式作為引數,主要用於判斷當前物件是否符合條件。 這個條件函式由你指定。如果條件符合,將返回當前物件。否則返回空值。

因此,如果你希望篩選集合中某個資料是否符合要求,可以使用這個函式

看一個例子:

fun main(args: Array<String>) {
    val arr = listOf(1, 2, 3)
    arr.forEach {
        println("$it % 2 == 0 => ${it.takeIf { it % 2 == 0 }}")
    }
}

// 執行結果
1 % 2 == 0 => null
2 % 2 == 0 => 2
3 % 2 == 0 => null
複製程式碼

takeUnless

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}
複製程式碼

這個函式剛好與takeIf篩選邏輯恰好相反。即:如果符合條件返回null,不符合條件返回物件本身。

看一個例子:

fun main(args: Array<String>) {
    val arr = listOf(1, 2, 3)
    arr.forEach {
        println("$it % 2 == 0 => ${it.takeUnless { it % 2 == 0 }}")
    }
}

// 執行結果
1 % 2 == 0 => 1
2 % 2 == 0 => null
3 % 2 == 0 => 3
複製程式碼

看到了嗎?這裡的執行結果和takeIf恰好相反。

repeat

@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0 until times) {
        action(index)
    }
}
複製程式碼

這個函式意思很明顯,就是將一個動作重複指定的次數。動作對應一個lambda表示式,表示式中持有一個參數列示當前正在執行的次數索引。

看一個例子:

fun main(args: Array<String>) {
    repeat(3) {
        println("Just repeat, index: $it")
    }
}

Just repeat, index: 0
Just repeat, index: 1
Just repeat, index: 2
複製程式碼

簡單總結

最後,我們用一個表格簡單總結一下這些函式的用法:

函式 用途 特點 形式
run#1 執行block,並返回執行結果 block中無法獲取接收者上下文 全域性函式
run#2 執行block,並返回執行結果 block中可以獲取接收者上下文 擴充套件函式
with 指定接收者,通過接收者執行block block中可以獲取接收者的上下文,可以對接收者資料型別做一定轉換 全域性函式
apply 執行block,並返回接收者例項本身 block中可以獲取接收者的上下文,可用於鏈式程式設計 擴充套件
also 執行block,並返回接收者例項本身 block中有一個引數代表接收者例項,可用於鏈式程式設計 擴充套件
let 執行block,並返回執行結果 block中有一個引數代表接收者例項,可以對接收者資料型別做一定轉換 擴充套件
takeIf 根據條件predicate判斷當前例項是否符合要求 如果符合要求,返回當前例項本身;否則返回null 擴充套件函式
takeUnless 根據條件predicate判斷當前例項是否不符合要求 如果不符合要求,返回當前例項本身;否則返回null 擴充套件

搞定Receiver

理解上面這幾個函式,最重要的一點是要理解Receiver。遺憾的是,Kotlin官方文件中並沒有針對Receiver的詳細講解。關於這部分的講解,請掃描下方二維碼關注歐陽鋒工作室,回覆搞定Receiver檢視文章。

歐陽鋒工作室

歡迎加入Kotlin交流群

關於Kotlin,如果你有任何問題,歡迎加入我的Kotlin交流群: 329673958。當前群交流活躍,問題解答速度很快,期待你的加入。

相關文章