Kotlin 學(xué)習(xí)筆記

Kotlin_2021

閱讀文檔和《Kotlin in Action》做的一些筆記丰介。

代碼規(guī)范

源碼組織

目錄結(jié)構(gòu)

在純 Kotlin 的項目中穴翩,推薦的目錄結(jié)構(gòu)是省略根包名颂暇。比如包名是 com.example.kotlin概疆,那么所有的代碼都應(yīng)該在這個根目錄之下棘捣,比如 org.example.kotlin.network.socket 中的文件就應(yīng)該放在 network/socket 子目錄下。

文件命名

如果文件中只包含一個類(包括頂層聲明)汗茄,則它的文件名應(yīng)該和類名保持一致盔沫。如果文件包含多個類、頂層聲明等淤翔,則應(yīng)該選擇最能描述這些類作用的命名翰绊,盡量選擇清晰易懂的名稱,如果做不到就應(yīng)該使用多個文件分別保存旁壮。對于命名風(fēng)格應(yīng)該選擇大駝峰命名法监嗜,并且應(yīng)該避免使用一些無意義的后綴,比如 util 等抡谐。

文件組織

如果多個類相互關(guān)系密切裁奇,并且描述的是同一個功能,那么推薦將它們放在同一個文件麦撵,只要最終的文件不是太長就可以刽肠。尤其是當(dāng)我們需要為類定義一些擴展方法的時候,如果擴展方法只和當(dāng)前類有關(guān)免胃,那么就應(yīng)該把這些擴展方法放到類一起音五,而不是單獨創(chuàng)建一個文件用于保存擴展方法。

類的內(nèi)容排布

類的各部分排列應(yīng)該按照以下順序:

  1. 屬性聲明和初始化代碼塊
  2. 從構(gòu)造器
  3. 方法聲明
  4. 伴生對象

初次之外羔沙,不要將方法根據(jù)首字母順序排序躺涝,或者根據(jù)可見性排序,也不要將普通方法和擴展方法分開來扼雏,而是應(yīng)該將相關(guān)的方法放在一起坚嗜,根據(jù)功能依次排列夯膀,這樣閱讀你的代碼的人才能方便地從上到下閱讀你的代碼,而不是頻繁地來回跳轉(zhuǎn)尋找相關(guān)代碼苍蔬。推薦將關(guān)鍵方法放到上面诱建,然后是較基礎(chǔ)和底層的方法。

對于嵌套類來說碟绑,推薦將它們放到使用到這些類的地方俺猿。如果嵌套類只是被外界使用,則可以將它們放到類的底部蜈敢,位于伴生對象之后辜荠。

實現(xiàn)接口時的排布

保持類中實現(xiàn)的各個方法的順序和接口中方法定義順序一致,如果有私有方法的話抓狭,則放在實現(xiàn)的方法附近伯病。

重載方法的排布

永遠要把重載方法放在一起,方便閱讀者看到一個方法全部的重載方法否过。

命名規(guī)則

包名和類名的命名規(guī)則:

  • 包名由小寫單詞和 . 構(gòu)成午笛,且不能使用下劃線,多個單詞推薦使用 . 分割或者直接拼接苗桂。
  • 類名使用 UpperCamelCase药磺,大駝峰命名。

方法名

方法名煤伟、屬性名癌佩、局部變量都應(yīng)該使用小寫字母開頭,并且使用駝峰命名法便锨,且不能使用下劃線围辙。除了工廠方法,可以使用與類名相同的方法放案。

fun Foo(): Foo { return FooImpl() }

測試方法的命名

只有在測試方法中才可以使用反引號姚建,但是注意安卓運行環(huán)境下并不支持,不過可以使用下劃線吱殉。

class MyTestCase {
    @Test fun `ensure everything works`() { /*...*/ }

    @Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}

屬性名

常量掸冤,被標(biāo)記為 const val 的屬性,以及沒有自定義 getter友雳、數(shù)據(jù)不可變的 val 屬性應(yīng)該使用大寫字符+下劃線分割:

const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"

頂層變量或者類的成員屬性如果攜帶可變數(shù)據(jù)稿湿,則應(yīng)該使用小駝峰命名法:

val mutableCollection: MutableSet<String> = HashSet()

如果表示單例對象,則命名應(yīng)該和對象聲明保持一致:

val PersonComparator: Comparator<Person> = /*...*/

枚舉類的命名應(yīng)該和 Java 中的命名保持一致:

enum class Color { RED, GREEN, LIGHT_BLUE }

幕后屬性的命名

幕后字段

一個類中通常包含屬性(Property)和字段(Field)押赊,外界不可直接訪問字段缎罢,一般是通過屬性提供對字段的訪問,有時屬性中還包括對字段的計算和轉(zhuǎn)換,然后再返回策精。Kotlin 中的屬性分為只讀的(通過 val 聲明)和可變的(通過 var 聲明),除此之外崇棠,我們還可以為屬性提供自定義的訪問器(getters & setters)咽袜。

Kotlin 中的字段無法顯式被聲明(我們只能創(chuàng)建屬性),但是枕稀,當(dāng)屬性需要用到字段的時候询刹,Kotlin 會默認(rèn)它自動生成幕后字段(backing field),這個幕后字段可以在訪問器通過 field 關(guān)鍵字進行調(diào)用萎坷。

var counter = 0 // the initializer assigns the backing field directly
    set(value) {
        if (value >= 0)
            field = value
            // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
            // counter = value
    }

只有在使用了至少一個默認(rèn)的訪問器或者自定義訪問器中引用了 field 才會生成幕后字段凹联,下面這個例子中就沒有幕后字段:

// 沒有初始化器而且自定義訪問器中沒有調(diào)用 field,此時就不會有幕后字段哆档。
val isEmpty: Boolean // 只要有初始化器蔽挠,則必定會生成幕后字段;沒有幕后字段就不能被初始化瓜浸。
    get() = this.size == 0
幕后屬性

如果你不喜歡這種隱式的幕后字段澳淑,則可以使用幕后屬性,也就是通過在類的內(nèi)部維護一個可讀可寫的屬性插佛,然后對外界提供一個只讀的屬性:

/**
 * 外部只能通過 table 來訪問杠巡,但實際上訪問的是 _table
 * 類似于 Java 中通過設(shè)置 Getter 和 Setter 來控制對屬性的訪問
 * */
private var _table: Map<String, Int>? = null
val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap()
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

在命名幕后字段時,私有屬性應(yīng)該使用下劃線作為開頭雇寇。

如何選擇名稱

類名應(yīng)該以名詞為結(jié)尾氢拥,描述該類的作用或功能贞远。類名應(yīng)該避免使用一些無意義的單詞牍蜂,比如前面提到過的 Util,還有 Manager/Wrapper 等等鸳惯。

方法名應(yīng)該是動詞或者動詞短語识腿,描述方法的功能或執(zhí)行哪些操作出革,并且應(yīng)該表明是否會修改對象或者返回一個新的對象,比如 sort 和 sorted渡讼。

對于縮寫詞骂束,應(yīng)該盡量少用,除非是特別常見的成箫,比如 IO/TV 等等展箱,或者是在當(dāng)前項目的文檔中注明過的常見縮寫。如果是兩個字母以上的縮寫蹬昌,應(yīng)該當(dāng)做普通的單詞使用混驰,比如 HttpConnection/XmlParser 等等。

格式化

打開 IDE (IDEA/Android Studio) 的 Preferences > Editor > Code Style > Kotlin,使用默認(rèn)設(shè)置栖榨,全部看一遍就夠了昆汹。

縮進

使用四個空格作為縮進,不要使用 Tab婴栽。

如果類的簽名很長满粗,主構(gòu)造函數(shù)中的參數(shù)很多,實現(xiàn)的接口很多時愚争,都應(yīng)該使用換行:

class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name),
    KotlinMaker,
    SomeOtherInterface,
    AndAnotherOne {
    
    // 這種情況下類的第一行應(yīng)該使用空白行增加可讀性
    fun foo() {
      // ...
    }
}

方法

同理映皆,如果方法簽名很長,應(yīng)該使用換行:

fun longMethodName(
    argument: ArgumentType = defaultValue,
    argument2: AnotherArgumentType,
): ReturnType {

    // body
}

表達體

如果方法體只有一行代碼轰枝,或者只有返回值捅彻,則應(yīng)該使用表達體:

// = 后面就是表達體(expression bodies)
fun foo() = value.size()

// 當(dāng)表達體很長時,使用換行并添加 4 個空格
fun f(x: String, y: String, z: String) =
    veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)

屬性

如果是比較簡單的屬性鞍陨,應(yīng)該放在同一行:

val isEmpty: Boolean get() = size == 0

如果 getters/setters 比較復(fù)雜步淹,則應(yīng)該換行并使用縮進:

val foo: String
    get() {
        // body
    }

如果初始化器比較長,同樣應(yīng)該換行并使用縮進:

private val defaultCharset: Charset? =
    EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

控制流程語句

如果 if 或 when 中包含多個語句湾戳,必須使用花括號贤旷,并且每個條件或者語句都要使用縮進,與第一個語句條件對齊:

if (!component.isSyncing &&
    !hasAnyKotlinRuntimeInScope(module) ||
    (someOtherCondition && andSomeMore)
) {
    return createKotlinNotConfiguredPanel(module)
}

when 語句中砾脑,只有在分支包含多個表達式時才使用花括號:

private fun parsePropertyValue(propName: String, token: Token) {
    when (token) {
        condition1 -> shortCondition()
        is Token.ValueToken ->
            callback.visitValue(propName, token.value)
      
        Token.LBRACE -> {
          // long body
        }
    }
}

方法調(diào)用

如果方法的參數(shù)較多幼驶,使用換行,并將多個相近的參數(shù)放在同一行:

drawSquare(
    x = 10, y = 10,
    width = 100, height = 100,
    fill = true
)

鏈?zhǔn)秸{(diào)用

val anchor = owner
    ?.firstChild!!
    .siblings(forward = true)
    .dropWhile { it is PsiComment || it is PsiWhiteSpace }

Lambdas

如果方法只接收一個 lambda 表達式韧衣,方法體應(yīng)該放在括號外面并省略括號:

list.filter { it > 10 }

如果給 lambda 指定了標(biāo)簽盅藻,標(biāo)簽和花括號之間的不能有空格:

fun foo() {
    ints.forEach lit@{
        // ...
    }
}

如果 lambda 中指定了參數(shù)名,則應(yīng)該使用換行:

appendCommaSeparated(properties) { prop ->
    val propertyValue = prop.get(obj)
    // ...
}

如果 lambda 中的參數(shù)列表很長畅铭,則應(yīng)該使用換行并將 -> 單獨放在一行:

foo {
   context: Context,
   environment: Env
   ->
   context.configureEnv(environment)
}

拖尾逗號

當(dāng)參數(shù)或者值的列表很長時氏淑,應(yīng)該使用拖尾逗號 (Trailing commas):

@ApplicableFor([
    "serializer",
    "balancer",
    "database",
    "inMemoryCache", // trailing comma
])
class Person(
    val firstName: String,
    val lastName: String,
    val age: Int, // trailing comma
) {

    val colors = listOf(
        "red",
        "green",
        "blue", // trailing comma
    )
}

文檔注釋

當(dāng)文檔注釋較長時,應(yīng)該使用換行:

/**
 * This is summary, what this class does, blablabla...
 * 
 * More detailed description, explain how this works,
 * how to use it, and maybe add some sample codes.
 */
class AwesomeClass { /* ... */ }

文檔注釋中硕噩,盡量不要使用 @param 或者 @return假残,而是盡可能地使用敘述性的文字炉擅,這樣可以增加可讀性辉懒。只有在參數(shù)特別多、解釋性文字特別長時谍失,才使用它們眶俩。

/**
 * Returns the absolute value of the given [number].
 */
fun abs(number: Int) { /*...*/ }

習(xí)慣用法

不可變性

傾向于使用不可變的數(shù)據(jù)。對于局部變量而言快鱼,盡量使用 val 聲明而不是 var颠印,如果初始化之后不會再進行修改的話纲岭。盡量創(chuàng)建不可變的集合,以及使用不可變的集合作為參數(shù)线罕,以避免在使用過程中止潮,集合被意外改變造成的各種錯誤。

// Bad: use of mutable collection type for value which will not be mutated
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }

// Good: immutable collection type used instead
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }

// Bad: arrayListOf() returns ArrayList<T>, which is a mutable collection type
val allowedValues = arrayListOf("a", "b", "c")

// Good: listOf() returns List<T>
val allowedValues = listOf("a", "b", "c")

默認(rèn)參數(shù)

盡量為方法創(chuàng)建默認(rèn)參數(shù)钞楼。

類型別名

如果某個方法簽名或者類型參數(shù)在你的代碼庫中被多次使用沽翔,那就應(yīng)該為它創(chuàng)建類型別名:

typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>

盡量使用 import xxx as xxx 避免命名沖突。

Lambda 參數(shù)

Lambda 中盡量使用 it 而不是顯式指定參數(shù)名稱窿凤,但是如果是在嵌套的 lambda 中,應(yīng)該為每個 lambda 表達式指定參數(shù)跨蟹。

Lambda 返回值

盡量使得 lambda 表達式的返回值只有一個出口雳殊,而不是多處 return,否則你應(yīng)該使用匿名方法代替 lambda 表達式窗轩。

命名參數(shù)

如果方法的參數(shù)很多且有多個相同類型的參數(shù)夯秃,或者包含多個布爾值,則應(yīng)該使用命名參數(shù):

drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)

條件表達式

盡量使用條件表達式的返回值:

// if 
return if (x) foo() else bar()

// when
return when(x) {
    0 -> "zero"
    else -> "nonzero"
}

// try..catch
val readFromInput: Int? = try {
    parseInt(input)
} catch (e: NumberFormatException) {
    null
}

Loops

盡量使用高階函數(shù) (filter, map 等) 而不是循環(huán)痢艺,除了 forEach仓洼。

在決定使用高階函數(shù)還是循環(huán)時,需要考慮使用場景和操作的性能消耗堤舒。

Loops on range

需要關(guān)閉區(qū)間則使用 until 而不是 x..n-1色建。

// bad:
for (i in 0..n - 1) { /*...*/ }
// good:
for (i in 0 until n) { /*...*/ }

String

盡量使用字符串模板而不是字符串拼接。當(dāng)需要換行時舌缤,優(yōu)先使用多行字符串而不是 \n箕戳。

Functions vs properties

對于沒有參數(shù)的方法而言,其作用和自定義 getter 的屬性是類似的国撵。如果滿足下列條件陵吸,則應(yīng)該使用屬性:

  • 不會拋出異常;
  • 結(jié)果的計算是比較輕量級的操作介牙;
  • 只要對象的狀態(tài)沒有發(fā)生變化壮虫,調(diào)用的結(jié)果就不會發(fā)生變化;

擴展方法

請自由使用擴展方法环础,如果一個方法在某個對象上被多次調(diào)用囚似,那就應(yīng)該把它設(shè)為擴展方法。為了減少 API 污染喳整,應(yīng)該注意擴展方法的可見性谆构。

中綴方法 (Infix functions)

只有在兩個對象角色(功能)類似時才使用中綴方法,正確示例:and, to, zip框都,錯誤示例:add搬素。

不要將一個會改變接收對象的方法定義為中綴方法呵晨。

工廠方法

工廠方法名盡量不要定義成和類名一樣,盡量使用能體現(xiàn)其作用的命名熬尺,比如 fromXxx 等等摸屠。

標(biāo)準(zhǔn)庫

Scope functions

Kotlin 標(biāo)準(zhǔn)庫中包含了一些在對象的上下文中執(zhí)行代碼塊的方法,比如 let, run, with, apply, also 等粱哼,在調(diào)用這些方法時季二,會在 lambda 表達式中形成一個臨時的 scope,在這個 scope 中揭措,你可以直接訪問對象的屬性和方法胯舷。

舉個例子:

// 傳統(tǒng)方式調(diào)用,需要創(chuàng)建一個變量绊含,然后通過變量來調(diào)用其方法
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

// 使用 let
Person("Alice", 20, "Amsterdam").let {
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

可以看到使用 scope function 可以使我們的代碼更簡短以及具有可讀性桑嘶。

本質(zhì)上來看,所有這些方法的執(zhí)行效果都是相同的躬充,唯一不同的是對象被調(diào)用的方式以及返回的結(jié)果不同逃顶。所以,我們了解并區(qū)分它們的區(qū)別充甚,并在合適的場景下調(diào)用它們以政。

最簡單的使用原則可以參考以下幾點:

  • 執(zhí)行一段 lambda 表達式:let

  • 在當(dāng)前上下文中,創(chuàng)建一個表達式并使用其結(jié)果作為變量:let

  • 對象構(gòu)建伴找,比如使用 Builder 模式:apply

  • 配置對象屬性并計算結(jié)果:run

  • 執(zhí)行需要含有表達式的語句:run

  • 添加更多效果:also

  • 將多個方法調(diào)用合并到一起:with

可以看到盈蛮,有些功能并不是只有一種方法才能實現(xiàn),以上只是推薦的做法疆瑰。另外眉反,雖然使用 scope functions 可以使你的代碼更靈活,但是如果過度使用比如使用嵌套等也會降低你的代碼的可讀性穆役。另外寸五,也要注意鏈?zhǔn)秸{(diào)用時 context 的變化,比如對象的參數(shù)由 it 變成了 this 或者由 this 變成了 it耿币。

Distinctions

Scope functions 直接最明顯的區(qū)別主要有兩個:如何引用 context 對象以及返回值梳杏。

Context object: this or it

run, with, apply 都通過 this 引用當(dāng)前的 context 對象,因此淹接,我們可以像在對象中一樣訪問它的屬性和方法十性。當(dāng)然,我們也可以省略 this 關(guān)鍵字塑悼,但是劲适,這樣做有時候會造成與外部其它代碼造成名字沖突,所以厢蒜,推薦的做法是加上 this. 來訪問對象的屬性和方法霞势。

相對應(yīng)的烹植,letalso 通過 it 引用當(dāng)前的 context 對象,而且我們需要手動調(diào)用 it 來訪問對象上的屬性和方法愕贡,不過這樣可以使我們的代碼顯得更清晰草雕。

Return value

apply, also 返回的是當(dāng)前的 context 對象本身。因此固以,它們可以使用鏈?zhǔn)秸{(diào)用:

val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
    .apply {
        add(2.71)
        add(3.14)
        add(1.0)
    }
    .also { println("Sorting the list") }
    .sort()

也可以被用在 return 語句中:

fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog("getRandomInt() generated value $it")
    }
}

let, run, with 返回的 lambda 表達式的結(jié)果墩虹。所以,你可以用它們給變量賦值憨琳,也可以使用鏈?zhǔn)秸{(diào)用:

val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
    add("four")
    add("five")
    count { it.endsWith("e") }
}

let

在 let 方法中诫钓,context 對象以參數(shù) it 的形式存在,返回值是 lambda 表達式的結(jié)果篙螟。

使用 let 方法可以幫我們減少創(chuàng)建一些臨時變量尖坤,尤其是不打破鏈?zhǔn)秸{(diào)用的結(jié)構(gòu):

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // and more function calls if needed
}

另外,如果 let 中只包含一個方法調(diào)用闲擦,可以使用方法引用:

numbers.map { it.length }.filter { it > 3 }.let(::println)

第三種用法是為變量創(chuàng)建一個局部 scope 提升代碼的可讀性:

// 需要在第一個數(shù)字上做一些操作
val modifiedFirstItem = numbers.first().let { firstItem ->
    println("The first item of the list is '$firstItem'")
    if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()

with

with 方法是一個非擴展函數(shù),context 對象通過 this 訪問场梆,返回值是 lambda 表達式的結(jié)果墅冷。但是不推薦在 with 方法中返回結(jié)果,最好只做一些操作或油,表示 "with this object, do the following."

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

另一種使用場景是使用對象的屬性和方法計算值:

val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)

run

context 對象通過 this 訪問寞忿,返回值是 lambda 表達式的結(jié)果。

run 方法的效果和 with 的效果一致顶岸,但是調(diào)用方式和 let 一樣腔彰,作為擴展方法。run 適合在既需要對對象進行初始化配置辖佣,也需要對結(jié)果進行計算的情況:

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// 使用 let
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

除此之外霹抛,run 方法還有一個非擴展方法,我們可以用它執(zhí)行一些需要執(zhí)行表達式的語句卷谈,比如變量聲明時:

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

apply

context 對象通過 this 訪問杯拐,返回值是對象本身。

當(dāng)不需要返回值世蔗,且主要對對象的成員和方法進行操作的時候可以使用 apply端逼,最常見的是對象初始化配置:

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}

由于返回的是對象本身,所以我們可以很方便地進行鏈?zhǔn)秸{(diào)用污淋。

also

context 對象通過 it 訪問顶滩,返回值是對象本身。

also 適用于需要對象的引用而不是其屬性和方法的場景下寸爆,以及你不想污染 this 關(guān)鍵字的時候礁鲁,可以將其理解為 "and also do the following with the object."

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

takeIf and takeUnless

除了 scope functions 之外盐欺,標(biāo)準(zhǔn)庫中還提供了 takeIftakeUnless 方法,可以讓我們在使用對象之前對其狀態(tài)進行檢查救氯。

takeIf 只有在對象滿足斷言時才返回對象找田,否則返回 null,takeUnless 則恰恰相反着憨,只有在對象不滿足斷言時才返回對象墩衙,否則返回 null,所以 takeIftakeUnless 是對單個對象的篩選方法甲抖。

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }

evenOrNull?.let {
    println("even: $evenOrNull")
}
oddOrNull?.let {
    println("odd: $oddOrNull")
}

由于返回值可能為空漆改,所以必須使用 ?.。而且可以看到 takeIftakeUnless 非常適合配合 scope functions 使用准谚。

類與對象

泛型

和 Java 中一樣挫剑,Kotlin 中也有類型參數(shù):

class Box<T>(t: T) {
    var value = t
}

如果通過構(gòu)造器創(chuàng)建對象,類型參數(shù)也可以被推斷出來柱衔,所以可以省略:

val box = Box(1)

型變

Java 中的泛型不是型變的樊破,因此會帶來很多問題,所以我們一般需要使用通配符來為泛型確定上下邊界唆铐。Kotlin 中沒有通配符類型哲戚,而是引入了聲明處型變(declaration-site variance)和類型投影(type projections)。

聲明處型變

先看個例子:

public interface List<E> extends Collection<E> { /* ... */ }

List<Number> numberList = new ArrayList<Integer>(); // Incompatible types

類似這樣的聲明在 Java 中是不被允許的艾岂,因為泛型不支持協(xié)變(covariant顺少,即 AB 的父類,同時 List<A> 也是 List<B> 的父類王浴,則稱 List 類是協(xié)變的)脆炎,我們需要使用通配符來告訴編譯器這種聲明是安全的:

List<? extends Number> numbers = new ArrayList<Integer>();

而在 Kotlin 中,我們通過 out 標(biāo)注類型參數(shù)來支持協(xié)變氓辣,并且確保它只是被返回(生產(chǎn))從不被消費:

public interface List<out E> : Collection<E> {
    fun get(index: Int): E // 返回類型叫 out 位置秒裕,生產(chǎn)類型為 T 的元素
    // 參數(shù)類型叫 in 位置,它消費類型為 T 的值钞啸。使用 @UnsafeVariance 是為了避免編譯器報錯
    fun indexOf(element: @UnsafeVariance E): Int
}

fun copyList(list: List<Int>) {
    var source: List<Number> = list // 現(xiàn)在可以被允許了
}

這里有一個原則簇爆,如果一個類 C 的類型參數(shù) T 被聲明為 out 時,它就只能出現(xiàn)在 C 的成員的輸出位置(返回類型)爽撒,但是回報是 C<Base> 可以安全地作為 C<Derived> 的父類入蛆。這樣,我們就可以稱類型參數(shù) T 在 C 上是協(xié)變的硕勿。你可以認(rèn)為 C 是 T 的生產(chǎn)者哨毁,而不是 T 的消費者。

out 修飾符稱為型變注解源武,并且由于它在類型參數(shù)聲明處提供扼褪,所以我們稱之為聲明處型變想幻。Java 中是在使用處通過通配符使得類型型變,Kotlin 與之正好相反话浇。另外脏毯,與之相對的,Kotlin 中還提供了另一個型變注釋 in幔崖,它的作用是使得類型參數(shù)逆變(contravariant食店,如果 AB 的父類,那么 List<A> 就是 List<B> 的子類型)赏寇,只可以被消費而不能被生產(chǎn)吉嫩。逆變類型的一個很好的例子是 Comparable

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 參數(shù) 1.0 的類型 Double,它是 Number 的子類型
    // 因此嗅定,我們可以將 x 賦給類型為 Comparable<Double> 的變量
    val y: Comparable<Double> = x // OK自娩!
}

總結(jié)一下,我們使用 out 關(guān)鍵字把類聲明成是協(xié)變 的渠退,并且要求 T 只能在 out 位置(被生產(chǎn))忙迁,只有這樣才能確保子類型才是安全的:List<Int>List<Number> 的子類型。使用 in 關(guān)鍵字使得類是逆變的并且要求 T 只能在 in 位置(被消費)碎乃。

使用處型變

同樣先看個例子:

fun copy(from: Array<Number>, to: Array<Number>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val nums = Array<Number>(3) { 3.3 }
    // 無法調(diào)用 copy 方法动漾,因為 Array<Int> 不是 Array<Number>!
    // copy(ints, nums) // Type mismatch
}

Array 類需要能被讀和寫荠锭,因此既不能是協(xié)變的也不能是逆變的,這就帶來了一個問題晨川,Array<Number> 不能被轉(zhuǎn)換為 Array<Int>证九,因此,我們需要在使用處 也就是 from 的類型參數(shù)前添加 out 關(guān)鍵字:

fun copy(from: Array<out Number>, to: Array<Number>) { …… }

我們把 from 稱為一個受限制 (projected) 的數(shù)組共虑,只可以調(diào)用返回類型為 T 的方法愧怜,這就叫做 type projection(不知道怎么翻譯按脚,Kotlin 中文網(wǎng)使用類型投影來翻譯票编,我覺得不是太好北启,所以先不翻譯)吞加。

這其實就是 Kotlin 中的使用處型變感憾,對應(yīng)與 Java 中的 Array<? extends Number>搁宾,限制泛型類型的上邊界侧漓。

當(dāng)然罚舱,我們也可以使用 in 關(guān)鍵字培愁,它對應(yīng)于 Java 中的 Array<? super Number>著摔,限制泛型類型的下邊界。不過定续,和 Java 中不同谍咆,我們可以在這樣的限制了下邊界的數(shù)組中禾锤,添加任何父類及子類元素:

val arr: Array<in String> = arrayOf('a', "abc", 123, Origin(), null) // 連 null 也可以添加
Star-projections

如果你對泛型參數(shù)的類型一無所知,但是依舊想要使用它摹察,則可以使用 star-projections恩掷,用 * 表示。我覺得有點類似于 Java 中的捕獲轉(zhuǎn)換供嚎,使用無界通配符 <?> 去捕捉類型黄娘。

  • 如果用 House<*> 去捕獲簽名為 House<out T : Human> 的類,則捕獲到的具體類型為 House<out Human>查坪,這樣你就可以安全地調(diào)用 House 中的成員方法和屬性了寸宏。
  • 如果用 House<*> 去捕獲簽名為 House<in T> 的類,由于類型參數(shù) T 是逆變的偿曙,而且沒有任何有關(guān) T 的類型氮凝,所以捕獲到的類型為 House<in Nothing>,此時往 House 添加任何對象都是不安全的望忆。
  • 如果用 House<*> 去捕獲簽名為 House<T : Human> 的類罩阵,則對于讀取而言捕獲到的是 House<out Human>,對于寫入而言捕獲到的是 House<in Nothing>启摄。

如果類的類型參數(shù)有多個稿壁,則每個類型參數(shù)都可以被單獨 projected。比如類型聲明為 interface Function<in T, out U>歉备,則它的 proection 可以分為以下幾種情況:

  • Function<*, String> 表示 Function<in Nothing, String>傅是;
  • Function<String, *> 表示 Function<String, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>蕾羊。

泛型函數(shù)

Kotlin 中的泛型函數(shù)和 Java 中的泛型方法類似喧笔,類型參數(shù)要放在方法名之前:

// 聲明泛型方法
fun <T> singletonList(item: T): List<T> { /* ... */ }

// 調(diào)用方法
val l = singletonList<Int>(1)

泛型約束

我們可以給泛型參數(shù)限定其可能的類型。

// 使用 : 限定其上界龟再,如果沒有指定书闸,默認(rèn)的上界是 Any?
fun <T : Comparable<T>> sort(list: List<T>) { /* ... */ }

// 如果同一個類型參數(shù)需要使用多個上界,則應(yīng)該使用 where-clause
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    // ...
}

類型擦除

與 Java 中類似利凑,泛型的類型安全檢測僅在編譯期有效浆劲,運行時泛型類的實例不保留其類型實參的任何信息,其類型信息被擦除了哀澈。比如 Foo<Bar>Foo<Baz> 都會被擦除為 Foo<*>牌借。

val foo: Box<String> = Box("foo")
val bar: Box<Number> = Box(1)
println(foo is Box<*>) // true
println(bar is Box<*>) // true
println(foo.javaClass.toGenericString()) // Box<T>
println(bar.javaClass.toGenericString()) // Box<T>

函數(shù)與 Lambda 表達式

Kotlin 中函數(shù)是頭等的,這意味著函數(shù)可以存儲在變量割按、參數(shù)走哺、數(shù)據(jù)結(jié)構(gòu)中,或者從其它高等函數(shù)中被返回”铮可以簡單理解為择示,函數(shù)也可以被當(dāng)做變量使用。

高階函數(shù)

高階函數(shù)是指接收函數(shù)作為參數(shù)或者返回一個函數(shù)的函數(shù)晒旅。最具代表性的是 fold 函數(shù):

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

它接收一個初始值和一個計算函數(shù)栅盲。計算函數(shù)中包含一個累積值和 next 元素值,在方法體中將集合遍歷废恋,將遍歷到的元素通過計算函數(shù)計算谈秫,得到新的累積值,并替換原有的累積值鱼鼓,最終返回結(jié)果拟烫。

我們可以通過以下方式調(diào)用該方法:

items.fold(0, { acc: Int, i: Int -> // 參數(shù)后用 -> 分割
    // 方法主體
    print("acc = $acc, i = $i, ")
    val result = acc + i
    println("result = $result")
    // 最后一個表達式作為返回值     
    result
})

// 參數(shù)類型如果可以被推斷出來則可以省略
items.fold("Elements:") { acc, i -> "$acc $i" }

// 也可以使用方法引用
items.fold(1, Int::times)

函數(shù)類型

Kotlin 中用 (Int) -> String 這樣的形式聲明一個函數(shù)類型的變量,比如:

val onClick: () -> Unit = // ...

函數(shù)類型主要形式如下:

  • 所有函數(shù)類型都有一個圓括號括起來的參數(shù)類型列表以及一個返回類型:(A, B) -> C迄本,表示它接受類型分別為 AB 的兩個參數(shù)硕淑,并返回一個 C 類型的值。參數(shù)類型列表可以為空嘉赎,比如 () -> A置媳;如果沒有返回值則必須注明,比如 (A, B) -> Unit公条。
  • 函數(shù)類型可以有一個額外的接收者類型拇囊,通過 A. 這樣的形式表示。當(dāng)調(diào)用函數(shù)時靶橱,第一個參數(shù)是該接收者寥袭,然后才是函數(shù)參數(shù)。另外关霸,函數(shù)中可以通過 this 關(guān)鍵字引用該對象传黄。帶與不帶接收者的函數(shù)類型可以互換。
  • 掛起函數(shù)是一種特殊的函數(shù)類型谒拴,表示法中包含一個 suspend 修飾符,如 suspend () -> Unit涉波。

聲明函數(shù)類型時英上,函數(shù)的參數(shù)名是可選的。下面看幾個例子:

// 可為空的函數(shù)類型
val nullableFun : ((Int, Int) -> Int)? = // ...

// 函數(shù)的返回值也為函數(shù)類型啤覆,使用 () 括起來就可以了
val returnFun : (Int) -> ((Int) -> Unit) = // ...

我們還可以通過類型別名給函數(shù)起一個別名:

typealias ClickHandler = (View) -> Unit

函數(shù)類型實例化

我們主要可以通過以下幾種方法獲得函數(shù)類型的實例:

  • 使用函數(shù)字面值的代碼塊

    • lambda 表達式:{ a, b -> a + b }
    • 匿名函數(shù):fun(s: String): Int { return s.toIntOrNull() ?: 0 }
  • 使用已聲明的可調(diào)用引用:

    • 頂層苍日、局部、成員相恃、擴展函數(shù):::isOddString::toInt
    • 頂層盈简、成員、擴展屬性:List<Int>::size
    • 構(gòu)造函數(shù):::Regex
  • 使用實現(xiàn)函數(shù)類型接口的自定義類的實例:

    class IntTransformer: (Int) -> Int {
        override operator fun invoke(x: Int): Int = TODO()
    }
    
    val intFunction: (Int) -> Int = IntTransformer() // 實例化
    

如果有足夠信息,編譯器可以推斷出具體的函數(shù)類型:

val a = { i: Int -> i + 1 } // 推斷出的類型是 (Int) -> Int

不過毅臊,函數(shù)類型推斷默認(rèn)推斷出的是沒有接收者的函數(shù)類型:

val a = { i: Int, s: String -> s + i }

上面的例子中著隆,默認(rèn)推斷出的是 (String, Int) -> String 而不是 String.(Int) -> String。如果這不符合你的需要,請顯式指定函數(shù)類型页响。

函數(shù)類型實例調(diào)用

除了直接調(diào)用之外,我們還可以使用 invoke() 對函數(shù)進行調(diào)用:

val stringPlus: (String, String) -> String = String::plus

println(stringPlus("Hello, ", "world!")) 
println(stringPlus.invoke("<-", "->"))

Lambda 表達式

語法

完整語法如下:

// 聲明一個名稱為 sum 的函數(shù)類型青灼,并使用 lambda 表達式初始化
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

Lambda 表達式總是括在花括號中,完整語法形式的參數(shù)聲明放在花括號內(nèi)妓盲,并有可選的類型標(biāo)注杂拨,函數(shù)體跟在一個 -> 符號之后。如果推斷出的該 lambda 的返回類型不是 Unit悯衬,那么該 lambda 主體中的最后一個表達式會被視為返回值:

val result = {
    val s = "hi"
    s.length
}
println(result is Function<Int>) // 返回值是 Int

傳遞末尾的 lambda 表達式

如果函數(shù)的最后一個參數(shù)是函數(shù)類型弹沽,那么作為參數(shù)傳入的 lambda 表達式可以放在圓括號之外:

val product = items.fold(1) { acc, e -> acc * e }

這種語法叫做拖尾 lambda (trailing lambdas),如果該 lambda 表達式是調(diào)用時唯一的參數(shù)筋粗,那么圓括號可以省略:run { println("...") }策橘。

it:單個參數(shù)的隱式名稱

如果 lambda 表達式中只有一個參數(shù),那么我們可以省略它以及 ->娜亿,該參數(shù)會被隱式聲明為 it

ints.filter { it > 0 }

從 lambda 表達式中返回值

我們可以使用標(biāo)簽返回的語法從 lambda 顯式返回一個值丽已。 否則,將隱式返回最后一個表達式的值暇唾。

下劃線用于未使用的變量

如果 lambda 表達式的參數(shù)未使用促脉,那么可以用下劃線取代其名稱:

map.forEach { _, value -> println("$value!") }

lambda 表達式中的解構(gòu)聲明

map.mapValues { (key, value) -> "$value!" }

匿名函數(shù)

Lambda 表達式缺少指定函數(shù)的返回類型的能力。在大多數(shù)情況下策州,這是不必要的瘸味,因為返回值類型可以被推斷出來,但是如果你需要顯式指定返回值類型够挂,就可以使用匿名函數(shù)代替 lambda 表達式旁仿。

匿名函數(shù)與普通函數(shù)的唯一區(qū)別是匿名函數(shù)省略了函數(shù)名稱。其使用方式和 lambda 表達式基本一致:

val add = fun(x: Int, y: Int): Int = x + y

不過孽糖,不同的是枯冈,匿名函數(shù)的參數(shù)必須在括號內(nèi)才能傳遞。

ints.filter(fun(item) = item > 0)

除此之外办悟,還有非局部返回的不同尘奏。非 lambda 表達式中,不帶標(biāo)簽的 return 總是在函數(shù)中直接返回病蛉,而 lambda 表達式是從包含它的函數(shù)返回炫加,所以在 lambda 表達式中如果要正確地 return 需要使用標(biāo)簽:

ints.filter {
    val mold = it % 2
    println(mold)
    return@filter mold == 0
}

閉包

Lambda 表達式或者匿名函數(shù)(以及局部函數(shù)對象表達式) 可以訪問其閉包 瑰煎,即在外部作用域中聲明的變量。與 Java 不同俗孝,Kotlin 中不但允許訪問閉包中的非 final 變量酒甸,還允許直接修改它們:

var sum = 0
ints.filter { it > 0 }.forEach {
    // 修改閉包中的變量
    sum += it
}
print(sum)

其實現(xiàn)原理是,Kotlin 為我們捕捉并保存了可變變量的引用赋铝,然后在我們修改其值的時候改變引用插勤。所以,如果變量被 lambda 表達式捕捉革骨,其聲明周期會和 lambda 的表達式的生命周期一致农尖。

內(nèi)聯(lián)函數(shù)

使用高階函數(shù)會帶來一些運行時的效率損失:每一個函數(shù)都是一個對象,并且會捕獲一個閉包良哲,即在那些函數(shù)體內(nèi)會訪問到的變量卤橄,對于函數(shù)對象和類的內(nèi)存分配和虛擬調(diào)用會引入運行時開銷。舉個例子:

lock(l) { foo() }

在這個方法中臂外,foo() 方法其實只有在被調(diào)用到時才起作用窟扑,其他時候只是作為參數(shù)傳遞,所以我們期望的行為是希望編譯器可以幫我們生成這樣一個方法:

lock.lock()
try {
    body() // 對目標(biāo)方法進行調(diào)用
} finally {
    lock.unlock()
}

為了讓編譯器這么做漏健,我們需要使用 inline 關(guān)鍵字嚎货。

inline fun <T> lock(lock: Lock, body: () -> T): T { /* ... */ }

inline 修飾符影響函數(shù)和傳給它的 lambda 表達式:所有這些都被內(nèi)聯(lián)到調(diào)用的地方。也就是 lambda 表達式成為函數(shù)調(diào)用者定義的一部分蔫浆,而不是保存在匿名類中殖属。

內(nèi)聯(lián)雖然有可能會導(dǎo)致生成的方法數(shù)增加,但是只要內(nèi)聯(lián)的方法體不是太大就可以節(jié)省性能開銷瓦盛。

禁用內(nèi)聯(lián)

如果希望只內(nèi)聯(lián)一部分函數(shù)洗显,我們可以在內(nèi)聯(lián)函數(shù)上使用 noinline 關(guān)鍵字:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { /* ... */ }

非局部返回

在 Kotlin 中,我們只能對具名或匿名函數(shù)使用正常的原环、非限定的 return 來退出挠唆。 但是,如果一個 lambda 表達式是內(nèi)聯(lián)的嘱吗,那么就可以使用非局部返回

fun foo(s: String) {
    s.let {
        return // OK:該 lambda 表達式是內(nèi)聯(lián)的
    }
}

不僅僅是 let玄组,所有的 scope functions 都是內(nèi)聯(lián)的。

一些內(nèi)聯(lián)函數(shù)可能調(diào)用傳給它們的不是直接來自函數(shù)體谒麦、而是來自另一個執(zhí)行上下文的 lambda 表達式參數(shù)俄讹,例如來自局部對象或嵌套函數(shù)。在這種情況下绕德,該 lambda 表達式中也不允許非局部控制流患膛。為了標(biāo)識這種情況,該 lambda 表達式參數(shù)需要用 crossinline 修飾符標(biāo)記:

// 使用 crossinline 修飾耻蛇,不允許局部返回
inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body() // 比如這段代碼執(zhí)行的 context 和當(dāng)前 context 不同
    }
}

具體化的類型參數(shù)

有時候踪蹬,我們需要得到類型參數(shù)的具體信息驹溃,這個時候可以在內(nèi)聯(lián)函數(shù)中使用 reified 關(guān)鍵字:

inline fun <reified T : Number> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

例子中,由于函數(shù)是內(nèi)聯(lián)的延曙,不需要反射,所以 !isas 都可以使用了亡哄。

內(nèi)聯(lián)屬性

inline 修飾符可用于沒有幕后字段的屬性的訪問器枝缔。既可以單獨標(biāo)注某個屬性訪問器,也可以標(biāo)注整個屬性使得兩個訪問器都是內(nèi)聯(lián)的:

val foo: Foo
    inline get() = Foo()

var bar: Bar
    get() = // ...
    inline set(v) { /* ... */ }

inline var bar: Bar
    get() = // ...
    set(v) { /* ... */ }

協(xié)程

異步編程技術(shù)

在學(xué)習(xí)協(xié)程之前蚊惯,讓我們先回顧一下已有的異步編程方案愿卸。

線程

線程可能是目前為止最著名的防止程序造成阻塞的方案。

fun postItem(item: Item) {
    val token = preparePost()
    val post = submitPost(token, item)
    processPost(post)
}

fun preparePost(): Token {
    // makes a request and consequently blocks the main thread
    return token
}

比如上面這段代碼中截型,我們需要在 preparePost() 方法中做網(wǎng)絡(luò)請求獲取數(shù)據(jù)趴荸,我們可以把它放在子線程中來防止 UI 被阻塞,但是這樣做有一些缺陷:

  • 創(chuàng)建線程所需的性能開銷并不低宦焦。線程造成的上下文切換非常昂貴发钝。
  • 線程不是無限制的〔郑可創(chuàng)建的線程數(shù)量受限于當(dāng)前操作系統(tǒng)酝豪,如果是服務(wù)端的應(yīng)用程序,這會造成主要的瓶頸精堕。
  • 線程不一定總是可用孵淘。在一些平臺,比如在 JavaScript 中就不支持線程歹篓。
  • 線程的使用并不簡單瘫证。在多線程編程中,多線程應(yīng)用的調(diào)試和避免出現(xiàn)競爭狀況是常見的問題庄撮。

回調(diào)

另一種思路是使用回調(diào)背捌,也就是將目標(biāo)方法作為參數(shù)傳遞到另一個函數(shù)中,在任務(wù)結(jié)束時再對目標(biāo)函數(shù)進行調(diào)用洞斯。

fun postItem(item: Item) {
    preparePostAsync { token ->
        submitPostAsync(token, item) { post ->
            processPost(post)
        }
    }
}

fun preparePostAsync(callback: (Token) -> Unit) {
    // make request and return immediately
    // arrange callback to be invoked later
}

使用回調(diào)看起來優(yōu)雅了許多载萌,但是依舊存在一些問題:

  • 多層嵌套導(dǎo)致代碼變復(fù)雜。
  • 錯誤處理變得異常困難巡扇。

Futures / Promises 及其它

Future / Promises 背后的思想是當(dāng)我們調(diào)用了異步請求之后扭仁,我們會得到一個 Promise 對象,其中包含了異步請求成功或失敗的結(jié)果厅翔,然后我們可以對它進行處理:

fun postItem(item: Item) {
    preparePostAsync()
        .thenCompose { token ->
            submitPostAsync(token, item)
        }
        .thenAccept { post ->
            processPost(post)
        }
}

fun preparePostAsync(): Promise<Token> {
    // makes request and returns a promise that is completed later
    return promise
}

這種解決方式需要我們改變編程方式乖坠,具體而言:

  • 不同的編程模型。從自上而下的命令式編程到通過鏈?zhǔn)秸{(diào)用的組合式編程刀闷。
  • 需要學(xué)習(xí)如何使用一套全新的 API熊泵。
  • 指定返回值類型仰迁。返回值從原始的真實數(shù)據(jù)到 Promise 對象。
  • 錯誤處理變得異常復(fù)雜顽分。

響應(yīng)式插件

響應(yīng)式插件 (Reactive Extensions, Rx) 最初是在 C# 中被 Erik Meijer 提出的徐许,后來 Netflix 將它移植到了 Java 中創(chuàng)造了 RxJava,于是慢慢受到了越來越多人的青睞卒蘸。其背后的思想是 observable streams雌隅,數(shù)據(jù)以可被觀察的流的形式存在。與 Future 返回具體的對象不同缸沃,Rx 返回的是數(shù)據(jù)流恰起,并且使用觀察者模式。

如果你接受并理解了 Rx 的核心理念趾牧,那么這種編程習(xí)慣的確可以很快被應(yīng)用到其它平臺上检盼,而且其錯誤處理也比前面提到的幾種更好一些。

協(xié)程

Kotlin 處理異步代碼的方式是通過協(xié)程翘单,其思想核心是可掛起的運算:函數(shù)可以將它的執(zhí)行掛起吨枉,并在稍后繼續(xù)執(zhí)行。協(xié)程最大的優(yōu)勢是開發(fā)者可以像寫阻塞式代碼一樣寫非阻塞式代碼(寫出的異步代碼和順序執(zhí)行的代碼一樣):

fun postItem(item: Item) {
    launch {
        val token = preparePost()
        val post = submitPost(token, item)
        processPost(post)
    }
}

suspend fun preparePost(): Token {
    // makes a request and suspends the coroutine
    return suspendCoroutine { /* ... */ }
}

在上面這個例子中哄芜,postItem 中會執(zhí)行一些耗時操作东羹,但是它不會阻塞主線程,preparePost)() 就是一個可掛起的函數(shù)忠烛,它會在執(zhí)行并返回結(jié)果之后属提,再繼續(xù)往下執(zhí)行其它代碼。

相比前面的一些方案美尸,協(xié)程具有以下優(yōu)勢:

  • 方法簽名不需要改變冤议;
  • 代碼結(jié)構(gòu)也不需要改變,我們可以像寫同步代碼一樣編寫異步代碼师坎;
  • 編程模型和 API 保持可用恕酸,比如使用循環(huán)、異常處理等保持一致胯陋;
  • 平臺獨立性蕊温,無論是針對 JVM、JavaScript 或者其它平臺遏乔,代碼始終保持一致义矛,編譯器會為我們適配到各自的平臺。

Kotlin 并不是唯一采用這種異步編程思想的語言盟萨,比如 C# Go 等語言很早就開始使用了凉翻。比較特殊的是,除了 suspend 關(guān)鍵字之外捻激,Kotlin 中協(xié)程功能全都是以庫的形式提供的制轰,我們需要導(dǎo)入 kotlinx.coroutines 包才能使用協(xié)程前计。

協(xié)程基礎(chǔ)

一個協(xié)程是一個可終止運算的實例。從概念上看垃杖,它與線程相似男杈,因為它需要運行一塊與其余代碼塊同時工作的代碼塊。但是调俘,協(xié)程不綁定到任何特定線程伶棒,它可以在一個線程中暫停執(zhí)行,然后在另一個線程中恢復(fù)執(zhí)行脉漏。因此,協(xié)程可以被看作是輕量級的線程袖牙。

fun main() = runBlocking { // runBlocking 用于連接阻塞式代碼和協(xié)程代碼
    val job = GlobalScope.launch { // 啟動一個新協(xié)程并保持對這個 job 的引用
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // 等待直到子協(xié)程執(zhí)行結(jié)束
}

結(jié)構(gòu)化并發(fā)

以上示例中侧巨,我們創(chuàng)建了一個頂層協(xié)程,保持對其的引用并將協(xié)程掛起直至 job 執(zhí)行結(jié)束鞭达。這存在一些問題司忱,比如創(chuàng)建頂層協(xié)程需要消耗更多的資源塑煎,手動保持對協(xié)程的引用容易出錯等等瞬欧。

更好的做法是使用結(jié)構(gòu)化并發(fā)。我們可以在 runBlocking 所在的 CoroutineScope 中直接啟動一個新協(xié)程得糜,這樣就毋需顯式 join 它了叨襟。正因為在同一個作用域中繁扎,所以會等待所有啟動的協(xié)程都執(zhí)行完畢后才會退出:

fun main() = runBlocking { // this: CoroutineScope
    launch { // 在 runBlocking 作用域中啟動一個新協(xié)程
        delay(1000L)
        println("World!")
    }
    print("Hello ")
}

依次介紹下這里涉及到的幾個協(xié)程函數(shù):

  • launch 是一個協(xié)程構(gòu)造器,它會相對其余代碼并發(fā)地啟動一個新的協(xié)程糊闽,并且與它們保持相互獨立梳玫。這也是為什么 "Hello " 會被先打印出來。
  • delay 是一個特殊的掛起函數(shù)右犹,它會將當(dāng)前協(xié)程掛起指定的一段時間提澎。掛起一個協(xié)程不會阻塞當(dāng)前所在的線程,但是會允許其它協(xié)程運行并且使用當(dāng)前線程運行它們的代碼念链。
  • runBlocking 也是一個協(xié)程構(gòu)造器盼忌,它橋接起了外部非協(xié)程代碼和方法體中的協(xié)程代碼。

所謂的結(jié)構(gòu)化并發(fā)指的是新的協(xié)程只能在 CoroutineScope 中才能啟動掂墓,這樣就限定了該協(xié)程的生命周期谦纱。在真實的使用場景中,我們通常會啟動非常多的協(xié)程君编,結(jié)構(gòu)化并發(fā)保證了這些協(xié)程不會被丟失或者泄露服协。外部的協(xié)程只有等待內(nèi)部的子協(xié)程全都執(zhí)行完畢才會退出。另外啦粹,結(jié)構(gòu)化并發(fā)也保持了當(dāng)代碼出錯的時候偿荷,錯誤能夠被正確地拋出而且不會被丟失窘游。

掛起函數(shù)

我們可以將上面打印 "World!" 的部分代碼提取到一個 printWorld() 方法中:

// 掛起函數(shù)
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

可以看到提取該方法時,IDE 為我們自動添加了 suspend 修飾符跳纳,這樣的函數(shù)被稱為掛起函數(shù) (suspending function)忍饰。掛起函數(shù)的作用是可以讓我們在協(xié)程中調(diào)用其它掛起函數(shù),并且暫停執(zhí)行寺庄。

作用域構(gòu)建器

我們還可以使用 coroutineScope 創(chuàng)建自己的協(xié)程作用域艾蓝,它和 runBlocking 一樣會等待代碼主體和子協(xié)程執(zhí)行完畢,唯一的不同之處是 runBlocking 會阻塞當(dāng)前線程斗塘,但 coroutineScope 只是掛起赢织,它會釋放占有的當(dāng)前線程下的資源。正因如此馍盟,coroutineScope 是一個掛起函數(shù)于置,而 runBlocking 只是普通的函數(shù)。

fun main() = runBlocking {
    doWorld()
    println("Done")
}

suspend fun doWorld() = coroutineScope {
    launch {
        delay(2000L)
        println("World 2")
    }
    launch {
        delay(1000L)
        println("World 1")
    }
    println("Hello")
}

// 輸出:
Hello
World 1
World 2
Done

在上面這個例子中贞岭,由于 coroutineScope 不會阻塞當(dāng)前線程八毯,所以當(dāng)它在內(nèi)部啟動兩個協(xié)程之后,這些代碼會被同步執(zhí)行瞄桨。而 runBlocking 是阻塞式運行的话速,所以它會等待 doWorld() 執(zhí)行完畢。所以芯侥,最先被打印的是 "Hello"泊交,然后是 delay 了 1 秒的 "World 1",再然后是 delay 了 2 秒的 "World 2"柱查,最后才是 "Done"活合。

全局協(xié)程像守護線程

看個例子:

GlobalScope.launch {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}
delay(1300L)

這里打印的結(jié)果是:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

原因是在 GlobalScope 中啟動的活動協(xié)程并不會使進程保活物赶,它們就像守護線程白指。

取消與超時

取消

job.cancel()
// or
job.cancelAndJoin()

如果協(xié)程沒有檢查取消狀態(tài),那么僅僅調(diào)用 cancel() 是無法被取消的:

fun cancelIsCooperative() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消 job 并且等待它結(jié)束
    println("main: Now I can quit.")
}

// 輸出
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.

為了使取消其作用酵紫,我們需要對子協(xié)程做以下修改:

// isActive 是 CoroutineScope 的擴展屬性
while (isActive) {
    // ...
}

我們還可以在 finally 代碼塊中釋放資源:

try {
    repeat(1000) { i ->
        println("job: I'm sleeping $i ...")
        delay(500L)
    }
} finally {
    println("job: I'm running finally")
}

對于不可取消的協(xié)程還可以在 finally 中判斷:

try {
    repeat(1000) { i ->
        println("job: I'm sleeping $i ...")
        delay(500L)
    }
} finally {
    // NonCancellable 是一個對象告嘲,且需要結(jié)合 withContext 方法使用
    withContext(NonCancellable) {
        println("job: I'm running finally")
        delay(1000L)
        println("job: And I've just delayed for 1 sec because I'm non-cancellable")
    }
}

超時

由于協(xié)程很有可能會超時,所以協(xié)程庫為我們提供了 withTimeout() 函數(shù):

fun runWithTimeout() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

運行結(jié)果會拋出 TimeoutCancellationException 異常奖地,它是 CancellationException 的子類橄唬,我們之前沒有看到這個異常是因為被取消的協(xié)程中,即使拋出 CancellationExcetption 也被認(rèn)為是正確退出的参歹。

如果不想看到異常仰楚,我們可以使用 withTimeoutOrNull() 方法,該方法中對 TimeoutCancellationException 進行了捕捉:

public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {
    if (timeMillis <= 0L) return null

    var coroutine: TimeoutCoroutine<T?, T?>? = null
    try {
        return suspendCoroutineUninterceptedOrReturn { uCont ->
            val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)
            coroutine = timeoutCoroutine
            setupTimeout<T?, T?>(timeoutCoroutine, block)
        }
    } catch (e: TimeoutCancellationException) {
        if (e.coroutine === coroutine) {
            return null
        }
        throw e
    }
}

withTimeout 中的 timeout 事件是異步的,并且有可能在任何時間點發(fā)生僧界,甚至在內(nèi)部代碼塊返回值被返回之前侨嘀。來看個例子:

var acquired = 0
var released = 0

class Resource {
    init {
        acquired++
    }

    fun close() {
        released++
    }
}

/**
 * 由于 timeout 事件是異步的,所以最終捂襟,acquired 事件觸發(fā)的此時有可能多于 released 事件
 * */
fun runTimeoutWithCloseResource() {
    runBlocking {
        repeat(100_000) {
            launch {
                val resource = withTimeout(60) {
                    delay(50)
                    Resource() // 由于 timeout 事件隨時都有可能被觸發(fā)咬腕,所以這里有可能會被調(diào)用多次
                }
                resource.close()
            }
        }
    }
    println(acquired)
    println(released)
}

為了解決這個問題,我們可以使用 try..finally 語句:

/**
 * 使用 try..finally 保證 Resource 的 acquire 和 release 都是成對的
 * */
fun runTimeoutWithCloseResourceSafely() {
    runBlocking {
        repeat(100_000) {
            launch {
                // 不依賴 withTimeout 的返回值
                var resource: Resource? = null
                try {
                    withTimeout(60) {
                        delay(50)
                        resource = Resource()
                    }
                } finally {
                    resource?.close()
                }
            }
        }
    }
    println(acquired)
    println(released)
}

組合掛起函數(shù)

首先看個例子:

fun main() {
    sequentialInvocation()
}

fun sequentialInvocation() = runBlocking {
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    println("Calculating one...")
    delay(1000L) // 假設(shè)我們在這里做了一些有用的事
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    println("Calculating two...")
    delay(1000L) // 假設(shè)我們在這里也做了一些有用的事
    return 29
}

由于它們都是運行在協(xié)程中的葬荷,所以如果按順序調(diào)用涨共,則它們也會像常規(guī)方法一樣,按順序被執(zhí)行宠漩。

使用 async 并發(fā)

上面的例子中举反,doSomethingUsefulOnedoSomethingUsefulTwo 之間并沒有依賴,為了更快地得到結(jié)果扒吁,我們可以對它們使用并發(fā)火鼻,這需要用到 async 關(guān)鍵字:

fun concurrentAsync() = runBlocking {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

asyncCoroutineScope 的擴展方法,默認(rèn)會在調(diào)用后立即執(zhí)行瘦陈,并且返回一個 Deferred 作為結(jié)果凝危,我們可以在 Deferred 上調(diào)用 await() 獲取結(jié)果值波俄。

惰性啟動的 async

async 可以通過將 start 參數(shù)設(shè)置為 CoroutineStart.LAZY 而變?yōu)槎栊缘某渴拧T谶@個模式下,只有結(jié)果通過 await 獲取的時候協(xié)程才會啟動懦铺,或者在 Jobstart 函數(shù)調(diào)用的時候捉貌。

fun concurrentAsyncLazy() = runBlocking {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        one.start()
        two.start()
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

async 風(fēng)格的函數(shù)

我們可以定義異步風(fēng)格的函數(shù)來異步 地調(diào)用 doSomethingUsefulOnedoSomethingUsefulTwo

fun main() {
    val time = measureTimeMillis {
        // 我們可以在協(xié)程外面啟動異步執(zhí)行
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // 但是等待結(jié)果必須調(diào)用其它的掛起或者阻塞
        // 當(dāng)我們等待結(jié)果的時候,這里我們使用 `runBlocking { …… }` 來阻塞主線程
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

這里我們通過 GlobalScope 對象創(chuàng)建出來的 CoroutineScope 創(chuàng)建出一個 async 協(xié)程冬念,并在其中調(diào)用我們的掛起函數(shù)趁窃。這樣,我們就可以在協(xié)程外調(diào)用該方法了急前,因為該方法不依賴外界是否是 CoroutineScope醒陆。而且這些方法總是異步且并發(fā)被執(zhí)行的(在頂層協(xié)程中)。不過裆针,在獲取運行結(jié)果 (Deferred.await()) 的時候刨摩,我們需要等待掛起函數(shù)執(zhí)行的結(jié)果,這里的例子里我們使用了 runBlocking 阻塞主線程并創(chuàng)建了一個 CoroutineScope 來等待執(zhí)行結(jié)束世吨。

使用 async 的結(jié)構(gòu)化并發(fā)

上面的例子中澡刹,雖然我們可以這么做,但是 Kotlin 中并不推薦這種異步編程的風(fēng)格耘婚“战剑考慮一下,如果程序在 somethingUsefulOneAsync 或者在 one.await() 中發(fā)生錯誤拋出了異常,那么嚷闭,somethingUsefulTwoAsync 依舊會被執(zhí)行攒岛,這明顯破壞了結(jié)構(gòu)化并發(fā)的原則。所以我們可以對代碼做以下修改:

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

我們把 doSomethingUsefulOnedoSomethingUsefulTwo 放到了同一個 CoroutineScope 中凌受,這樣阵子,當(dāng)程序拋出異常的時候,所有在當(dāng)前作用域內(nèi)啟動的協(xié)程都會被取消:

fun runFailedConcurrentSum() = runBlocking {
    try {
        failedConcurrentSum()
    } catch (e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async {
        try {
            delay(Long.MAX_VALUE) // 模擬一個長時間的運算
            42
        } finally {
            // 結(jié)束或者取消時會被打印
            println("First child was cancelled")
        }
    }
    val two = async<Int> {
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}

協(xié)程上下文與調(diào)度器

協(xié)程總是運行在一些以 CoroutineContext 類型為代表的上下文中胜蛉。協(xié)程上下文是各種不同元素的集合挠进,其中主元素是協(xié)程中的 Job

調(diào)度器與線程

協(xié)程上下文包含一個協(xié)程調(diào)度器 CoroutineDispatcher誊册,它確定了相關(guān)的協(xié)程在哪個線程或哪些線程上執(zhí)行领突。協(xié)程調(diào)度器可以將協(xié)程限制在一個特定的線程執(zhí)行,或?qū)⑺峙傻揭粋€線程池案怯,亦或是讓它不受限地運行君旦。

所有的協(xié)程構(gòu)建器諸如 launchasync 接收一個可選的 CoroutineContext 參數(shù),它可以被用來顯式的為一個新協(xié)程或其它上下文元素指定一個調(diào)度器嘲碱。

fun main() = runBlocking<Unit> {
    launch { // 運行在父協(xié)程的上下文中金砍,即 runBlocking 主協(xié)程
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // 不受限的——將工作在主線程中
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // 將會獲取默認(rèn)調(diào)度器
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // 將使它獲得一個新的線程
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }    
}

異步流

掛起函數(shù)可以異步的返回單個值,但是該如何異步返回多個計算好的值呢麦锯?這正是 Kotlin 流(Flow)的用武之地恕稠。

序列與流

如果使用一些消耗 CPU 資源的阻塞代碼計算數(shù)字(每次計算需要 100 毫秒)那么我們可以使用 Sequence 來表示數(shù)字:

private fun printSequenceList() {
    val sequence: Sequence<Int> = sequence { // 序列構(gòu)建器
        for (i in 1..3) {
            Thread.sleep(300) // 假裝我們正在計算
            yield(i) // 產(chǎn)生下一個值
        }
    }

    sequence.forEach { println(it) }
}

我們可以使用掛起函數(shù),在不阻塞的情況下執(zhí)行其工作并將結(jié)果作為列表返回:

private fun printDelayedList() = runBlocking {
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(300)
        }
    }

    val list = mutableListOf<Int>()
    for (i in 1..3) {
        delay(300) // delay 函數(shù)不是阻塞式的
        list.add(i)
    }

    list.forEach { println(it) }
}

使用 List 結(jié)果類型扶欣,意味著我們只能一次返回所有值鹅巍。 為了表示異步計算的值流(stream),我們可以使用 Flow 類型(正如同步計算值會使用 Sequence 類型):

private fun flowList() = flow { // 流構(gòu)建器
    for (i in 1..3) {
        delay(100) // 假裝我們在這里做了一些有用的事情
        println("Emitting $i")
        emit(i) // 發(fā)送下一個值
    }
}

private fun printFlowList() = runBlocking {
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(300)
        }
    }
    flowList().collect { println(it) }
}

使用 flow 具有以下特點:

  • 名為 flow 的構(gòu)建器函數(shù)
  • flow { ... } 構(gòu)建塊中的代碼可以掛起
  • flow 不是 suspend 函數(shù)
  • flow 使用 emit 函數(shù)發(fā)射
  • flow 使用 collect 函數(shù)收集

流的取消

流采用與協(xié)程同樣的協(xié)作取消料祠。

private fun runFlowWithTimeOut() = runBlocking {
    withTimeoutOrNull(800) { // 超時后 flow 不再繼續(xù)執(zhí)行
        flowList().collect { value -> println(value) }
    }
    println("Done")
}

流是冷的

Flow 是一種類似于序列的冷流——只有在被 collect 的時候才會運行骆捧。

流是連續(xù)的

  • 流的每次單獨收集都是按順序執(zhí)行的,除非進行特殊操作的操作符使用多個流髓绽。
  • 收集過程直接在當(dāng)前協(xié)程中運行敛苇,默認(rèn)情況下不啟動新協(xié)程。
  • 從上游到下游每個過渡操作符都會處理每個發(fā)射出的值然后再交給末端操作符顺呕。

流上下文

流的收集總是在調(diào)用協(xié)程的上下文中發(fā)生枫攀,流的該屬性稱為上下文保存 。默認(rèn)情況下塘匣,flow { ... } 構(gòu)建器中的代碼總是運行在相應(yīng)流的收集器提供的上下文中脓豪。

withContext 發(fā)出錯誤

然而,長時間運行的消耗 CPU 的代碼也許需要在 Dispatchers.Default 上下文中執(zhí)行忌卤,并且更新 UI 的代碼也許需要在 Dispatchers.Main 中執(zhí)行扫夜。通常,withContext 用于在 Kotlin 協(xié)程中改變代碼的上下文,但是 flow {...} 構(gòu)建器中的代碼必須遵循上下文一致原則笤闯,并且不允許從其他上下文中發(fā)射(emit)堕阔。

private fun runFlowWithContextDispatchers() = runBlocking {
    flow {
        // 在流構(gòu)建器中更改上下文,會拋出異常:
        // java.lang.IllegalStateException: Flow invariant is violated
        withContext(Dispatchers.Default) {
            for (i in 1..3) {
                delay(300)
                emit(i)
            }
        }
    }.collect { println(it) }
}

流構(gòu)建器

除了使用 flow { ... } 構(gòu)建 Flow 之外颗味,我們還可以使用:

  • flowOf:定義了一個發(fā)射固定值集的流
  • .asFlow():擴展函數(shù)超陆,可以將各種集合與序列轉(zhuǎn)換為流

比如可以將一個整數(shù)區(qū)間轉(zhuǎn)換為流:(1..3).asFlow().collect { value -> println(value) }

流操作符

過渡操作符 map

我們可以像使用集合與序列一樣,使用操作符對流進行轉(zhuǎn)換浦马,比如 filtermap时呀。過渡操作符應(yīng)用于上游流,并返回下游流晶默,而且像流一樣也是冷操作符谨娜。

suspend fun performRequest(request: Int): String {
    delay(1000) // 模仿長時間運行的異步工作
    return "response $request"
}

private fun runFlowWithFilterAndMap() = runBlocking {
    (1..3).asFlow() // 一個請求流
        .filter { request -> request > 1 }
        .map { request -> performRequest(request) }
        .collect { response -> println(response) }
}
轉(zhuǎn)換操作符 transform

我們還可以使用 轉(zhuǎn)換操作符 實現(xiàn)更為復(fù)雜的轉(zhuǎn)換,使用形式 transformXxx

fun runFlowWithTransform() = runBlocking {
    (1..3).asFlow() // 一個請求流
        .transform { request ->
            emit("Making request $request")
            emit(performRequest(request))
        }
        .collect { response -> println(response) }
}
限長操作符 take

我們可以使用限長操作符在流觸及相應(yīng)限制的時候磺陡,將它的執(zhí)行取消趴梢。形式如 takeXxx

fun runFlowWithTake() = runBlocking {
    (1..3).asFlow()
        .take(2)
        .collect { response -> println(response) }
}
末端操作符

末端操作符是在流上用于啟動流收集的掛起函數(shù)collect 是最基礎(chǔ)的末端操作符币他,但是還有另外一些更方便使用的末端操作符:

  • 轉(zhuǎn)化為各種集合坞靶,例如 toListtoSet
  • 獲取第一個 first 值與確保流發(fā)射單個 single 值的操作符
  • 使用 reducefold 將流規(guī)約到單個值
private fun runFlowWithTerminalOperators() = runBlocking {
    (1..5).asFlow()
        .map { it * it } // 數(shù)字 1 至 5 的平方
        // .toList()
        .reduce { a, b -> a + b } // 求和
        .let { println(it) }
}
flowOn 操作符

前面說過不允許直接使用 withContext 修改上下文,所以正確更改流發(fā)射的上下文需要通過 flowOn 操作符:

private fun runFlowWithFlowOn() = runBlocking {
    flowList()
        .flowOn(Dispatchers.Default)
        .collect { println(it) }
}
緩沖操作符

我們可以使用 buffer 操作符來并發(fā)運行上流中發(fā)射元素的代碼以及 collect 中的代碼蝴悉,而不是順序運行它們:

private fun runFlowWithBuffer() = runBlocking {
    val time = measureTimeMillis {
        flowList()
            .buffer() // 緩沖發(fā)射項彰阴,無需等待
            .collect { value ->
                delay(300)
                println(value)
            }
    }
    println("Collected in $time ms")
}
conflate

當(dāng)流只代表部分操作結(jié)果或操作狀態(tài)更新時,可能沒有必要處理每個值辫封,而是只處理最新的值硝枉。當(dāng)收集器處理它們太慢的時候廉丽,我們可以使用 conflate 操作符倦微,用于跳過中間值:

private fun runFlowWithConflate() = runBlocking {
    val time = measureTimeMillis {
        flowList()
            .conflate() // 合并發(fā)射項,不對每個值進行處理
            .collect { value ->
                delay(300)
                println(value)
            }
    }
    println("Collected in $time ms")
}

運行結(jié)果:

Emitting 1
Emitting 2
Emitting 3
1
3
Collected in 797 ms

可以看到正压,雖然第一個數(shù)字仍在處理中欣福,但第二個和第三個數(shù)字已經(jīng)產(chǎn)生,因此第二個被 conflated焦履,只有最新的(第三個)被交付給收集器拓劝。

collectLatest

當(dāng)發(fā)射器和收集器都很慢的時候,合并是加快處理速度的一種方式嘉裤,它通過刪除發(fā)射值來實現(xiàn)。另一種方式是取消緩慢的收集器,并在每次發(fā)射新值的時候再收集:

private fun runFlowWithCollectLatest() = runBlocking {
    val time = measureTimeMillis {
        flowList()
            .collectLatest { value -> // 只收集最新的值
                println("Collecting $value")
                delay(300)
                println("Done with $value")
            }
    }
    println("Collected in $time ms")
}

在這個例子中粗俱,由于 flowList 每個 100ms 發(fā)射一個新值,但是收集的時候會被 delay 300ms,所以只有最后一個值才會被收集。

合并操作符
Zip

與標(biāo)準(zhǔn)庫中的 Sequence.zip 擴展函數(shù)一樣收奔,流擁有一個 zip 操作符用于組合兩個流:

private fun runFlowWithZip() = runBlocking {
    val nums = (1..3).asFlow()
    val strs = flowOf("one", "two", "three")
    nums.zip(strs) { a, b -> "$a -> $b" } // 組合成新的字符串
        .collect { println(it) } // 收集并打印
}
combine

當(dāng)流表示一個變量或操作的最新值時绷蹲,可能需要執(zhí)行計算棒卷,我們可以使用 combine 來對上游流進行重新計算:

/**
 * 各個時間節(jié)點下產(chǎn)生的單個和合并后的事件
 *
 *    Time       : 300   400     600     800     900     1200
 * Single Event  :  1    one      2      two      3      three
 * Combined Event:     1->one  2->one  2->two  3->two  3->three
 * */
private fun runFlowWithCombine() = runBlocking {
    val nums = (1..3).asFlow().onEach { delay(300) } // 發(fā)射數(shù)字 1..3顾孽,間隔 300 毫秒
    val strs = flowOf("one", "two", "three").onEach { delay(400) } // 每 400 毫秒發(fā)射一次字符串
    val startTime = System.currentTimeMillis() // 記錄開始的時間
    nums.combine(strs) { a, b -> "$a->$b" } // 使用“zip”組合單個字符串
        .collect { value -> // 收集并打印
            println("$value at ${System.currentTimeMillis() - startTime} ms from start")
        }
}
展平操作符

流表示異步接收的值的序列,所以很容易遇到這樣的情況:每個值都會觸發(fā)對另一個值序列的請求比规。所有當(dāng)我們對流進行操作的時候會出現(xiàn)包含流的流若厚,這個時候我們就需要對流進行展平 (flatten) 然后再進行其它操作。

flatMapConcat

展平連接主要由 flatMapConcatflattenConcat 操作符實現(xiàn)蜒什。

flatMapMerge

另一種展平模式是并發(fā)收集所有傳入的流测秸,并將它們的值合并到一個單獨的流,以便盡快的發(fā)射值灾常。 它由 flatMapMergeflattenMerge 操作符實現(xiàn)霎冯。他們都接收可選的用于限制并發(fā)收集的流的個數(shù)的 concurrency 參數(shù)(默認(rèn)情況下,它等于 DEFAULT_CONCURRENCY)钞瀑。

flatMapLatest

collectLatest 操作符類似沈撞,在發(fā)出新流后立即取消先前流的收集,這由 flatMapLatest 操作符來實現(xiàn)雕什。

異常操作符

當(dāng)運算符中的發(fā)射器或代碼拋出異常時缠俺,我們可以使用異常操作符對異常進行處理。

使用 try..catch 捕獲異常
private fun runFlowWithTryCatch() = runBlocking {
    try {
        flowList().collect { value ->
            println(value)
            check(value <= 1) { "Collected $value" } // 值 > 1 的時候拋出一個 IllegalStateException
        }
    } catch (e: Throwable) {
        println("Caught $e")
    }
}

但是贷岸,上面這個例子中實際上捕獲了任何在發(fā)射器或者過渡操作符晋修、末端操作符中拋出的異常。

catch 操作符

發(fā)射器可以使用 catch 操作符來保留此異常的透明性并允許封裝它的異常處理凰盔。catch 操作符的代碼塊可以分析異常并根據(jù)捕獲到的異常以不同的方式對其做出反應(yīng)墓卦,它的特點是:

  • 可以使用 throw 重新拋出異常
  • 可以使用 emit 將異常轉(zhuǎn)換為值發(fā)射出去
  • 可以將異常忽略,或用日志打印户敬,或使用一些其他代碼處理它
  • 僅捕獲上游異常
private fun catchFlowException() = runBlocking {
    try {
        flowListWithException()
            .catch { e -> emit("Caught $e") } // 發(fā)射一個異常
            .collect { value ->
                check(value.length < 20) { "Collected $value" } // 僅捕獲上游異常
                println("Collect $value")
            }
    } catch (e: Exception) {
        // 捕獲下游的異常
        println("Collect Exception: $e")
    }
}
聲明式捕獲 onEach

我們可以將 catch 操作符的聲明性與處理所有異常的期望相結(jié)合落剪,將 collect 操作符的代碼塊移動到 onEach 中,并將其放到 catch 操作符之前尿庐。收集該流必須由調(diào)用無參的 collect() 來觸發(fā):

private fun catchFlowExceptionOnEach() = runBlocking {
    flowListWithException()
        .onEach {
            check(it.length > 10) { "Collected $it" }
            println(it)
        }
        .catch { println("Caught $it") }
        .collect()
}
完成操作符

當(dāng)流收集完成時(普通情況或異常情況)忠怖,它可能需要執(zhí)行一個動作。我們可以使用命令式或聲明式在流完成時做一些操作抄瑟。

命令式 finally 塊
private fun flowWithFinally() = runBlocking {
    try {
        flowList().collect { println("Collected $it") }
    } finally {
        println("Done")
    }
}
聲明式處理 onCompletion

我們可以使用 onCompletion 操作符在流完成收集時進行調(diào)用:

private fun flowOnCompletion() = runBlocking {
    flowList()
        .onCompletion { println("Done") }
        .collect { println("Collected $it") }
}

上面的輸出和使用 finally 代碼塊的輸出一致凡泣,除此之外,如果流完成時拋出了異常我們還可以通過 onCompletion 中的可空參數(shù)進行捕捉:

private fun flowOnCompletionWithUpStreamException() = runBlocking {
    flowListWithException()
        .onCompletion { cause -> cause?.let { println("Flow completed but has exceptions: $it") } }
        .catch { println("Exception: $it") }
        .collect { println("Collected $it") }
}

不過皮假,onCompletion 不會對異常進行處理鞋拟,而是交由后面的 catch 操作符進行處理。并且與 catch 操作符不同惹资,收集時拋出的異常在 onCompletion 也會感知到:

private fun flowOnCompletionWithDownStreamException() = runBlocking {
    flowList()
        // onCompletion 能觀察到所有的異常贺纲,包括下游收集時拋出的異常
        .onCompletion { cause -> cause?.let { println("Flow completed but has exceptions: $it") } }
        .catch { println("Exception: $it") }
        .collect {
            check(it > 1) { "Illegal value: $it" }
            println("Collected $it")
        }
}

選擇命令式或者聲明式對異常和流完成進行處理,取決于我們的需求和喜好褪测,兩種方式都是有效的猴誊。

啟動操作符

我們可以使用 launchIn 替換 collect潦刃,在單獨的協(xié)程中啟動流的收集。我們需要通過指定參數(shù) CoroutineScope 用以確定哪一個協(xié)程來啟動流的收集懈叹。

private fun flowWithLaunchIn() = runBlocking {
    flowList()
        .onEach { event -> println("Event: $event") }
        .launchIn(this) // <--- 在單獨的協(xié)程中執(zhí)行流
    println("Done")
}

在實際的應(yīng)用中乖杠,作用域來自于一個壽命有限的實體。在該實體的壽命終止后澄成,相應(yīng)的作用域就會被取消滑黔,即取消相應(yīng)流的收集。這種成對的 onEach { ... }.launchIn(scope) 工作方式就像 addEventListener 一樣环揽。而且略荡,這不需要相應(yīng)的 removeEventListener 函數(shù), 因為取消與結(jié)構(gòu)化并發(fā)可以達成這個目的歉胶。

另外汛兜,launchIn 也會返回一個 Job,可以在不取消整個作用域的情況下僅取消相應(yīng)的流收集或?qū)ζ溥M行 join通今。

可取消的流

使用 flow { ... } 創(chuàng)建的流會對每個發(fā)射值執(zhí)行附加的 ensureActive 檢測以進行取消粥谬,但是大多數(shù)其它流(比如 asFlow())都不會自行執(zhí)行取消檢測,不過我們可以在 onEach 中對添加 currentCoroutineContext().ensureActive() 或者使用 cancellable 操作符:

private fun makeFlowCancellable() = runBlocking {
    (1..5).asFlow().cancellable().collect { value ->
        if (value == 3) cancel()
        println(value)
    }
}

Flow 與 Rx

熟悉 RxJava 的人會覺得 Flow 非常相似辫塌,這是因為 Flow 的設(shè)計靈感正是來源于響應(yīng)式流及其各種實現(xiàn)漏策。雖然略有不同,但從概念上講臼氨,F(xiàn)low 依然是 響應(yīng)式流掺喻。

通道

通道提供了一種在流中傳輸值的方法。

通道基礎(chǔ)

一個 Channel 是一個和 BlockingQueue 非常相似的概念储矩。其中一個不同是它代替了阻塞的 put 操作并提供了掛起的 send感耙,還替代了阻塞的 take 操作并提供了掛起的 receive

private fun headFirstChannel() = runBlocking {
    val channel = Channel<Int>() // 實現(xiàn)了 SendChannel 和 ReceiveChannel
    launch {
        // 這里可能是消耗大量 CPU 運算的異步邏輯持隧,我們將僅僅做 5 次整數(shù)的平方并發(fā)送
        for (x in 1..5) channel.send(x * x)
    }
    // 這里我們打印了 5 次被接收的整數(shù):
    repeat(5) { println(channel.receive()) }
    println("Done!")
}
通道的關(guān)閉與迭代

和隊列不同即硼,一個通道可以通過被關(guān)閉來表明沒有更多的元素將會進入通道。在接收者中可以使用 for 循環(huán)來從通道中接收元素:

private fun closeAndIterateChannel() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (x in 1..5) channel.send(x * x)
        channel.close()
    }
    for (c in channel) println(c)
    println("Done!")
}
構(gòu)建通道生產(chǎn)者

使用生產(chǎn)者-消費者模式對通道進行創(chuàng)建和使用屡拨,需要注意這些接口是實驗性的:

private fun produceAndConsumeChannel() = runBlocking {
    produce {
        for (x in 1..5) send(x * x)
    }.consumeEach { println(it) }
    println("Done!")
}

管道

管道是指在一個協(xié)程中創(chuàng)建擁有無窮多個值的流只酥。

private fun infiniteNumberPipeline() = runBlocking {
    val numbers = produce {
        var x = 1
        while (true) send(x++)
    }
    val squares = produce {
        for (x in numbers) send(x * x)
    }
    repeat(10) {
        print("${squares.receive()}, ")
    }
    println("Done!")

    // 獲取 coroutineContext 取消所有后續(xù)的 job
    coroutineContext.cancelChildren()
}

扇出

多個協(xié)程也許會接收相同的管道,它們之間可以進行分布式的工作呀狼。

private fun panOut() = runBlocking {
    val producer = infiniteDelayedNumbers(1)
    repeat(5) { launchProcessors(it, producer) }
    delay(950) // 等待一會兒
    producer.cancel() // 取消 channel
}

扇入

多個協(xié)程可以發(fā)送到同一個通道裂允。 比如說,讓我們創(chuàng)建一個字符串的通道赠潦,并且在這個通道中以指定的延遲反復(fù)發(fā)送一個字符串:

private fun panIn() = runBlocking {
    val channel = Channel<String>()
    launch { sendString(channel, "Foo", 200) }
    launch { sendString(channel, "Bar", 500) }
    repeat(6) { println(channel.receive()) }
    coroutineContext.cancelChildren()
}

帶緩沖的通道

到目前為止展示的通道都是沒有緩沖區(qū)的叫胖。無緩沖的通道在發(fā)送者和接收者相遇時才傳輸元素,也叫對接她奥。如果發(fā)送先被調(diào)用瓮增,則它將被掛起直到接收被調(diào)用;如果接收先被調(diào)用哩俭,則它將被掛起直到發(fā)送被調(diào)用绷跑。

Channel 構(gòu)造器與 produce 建造器通過一個可選的參數(shù) capacity 來指定緩沖區(qū)的大小。緩沖允許發(fā)送者在被掛起前發(fā)送多個元素凡资, 就像 BlockingQueue 有指定的容量一樣砸捏,當(dāng)緩沖區(qū)被占滿的時候?qū)鹱枞?/p>

private fun bufferedChannels() = runBlocking {
    val channel = Channel<Int>(4)
    val sender = launch {
        repeat(10) {
            println("Sending $it")
            channel.send(it)
        }
    }
    // 不對其進行接收,只是等待
    delay(1000)
    sender.cancel()
}

通道是公平的

發(fā)送和接收操作是公平的 并且尊重調(diào)用它們的多個協(xié)程隙赁。它們遵守先進先出原則垦藏,可以看到第一個協(xié)程調(diào)用 receive 并得到了元素。

private fun channelIsSequential() = runBlocking {
    data class Ball(var hits: Int)

    suspend fun player(name: String, table: Channel<Ball>) {
        for (ball in table) { // 在循環(huán)中接收球
            ball.hits++
            println("$name $ball")
            delay(300) // 等待一段時間
            table.send(ball) // 將球發(fā)送回去
        }
    }

    val table = Channel<Ball>()
    // 先啟動的協(xié)程先接收到事件
    launch { player("ping", table) }
    launch { player("pong", table) }
    table.send(Ball(0))
    delay(2000)
    coroutineContext.cancelChildren()
}

計時器通道

計時器通道是一種特別的會合通道 (ReceiveChannel)伞访,每次經(jīng)過特定的延遲都會從該通道進行消費并產(chǎn)生 Unit掂骏。雖然它看起來似乎沒用,它被用來構(gòu)建分段來創(chuàng)建復(fù)雜的基于時間的 produce 管道和進行窗口化操作以及其它時間相關(guān)的處理厚掷〉茏疲可以在 select 中使用計時器通道來進行“打勾”操作。

使用工廠方法 ticker 來創(chuàng)建這些通道冒黑,使用 ReceiveChannel.cancel 方法關(guān)閉通道田绑。

private fun tickerChannel() = runBlocking {
    val tickerChannel = ticker(delayMillis = 1000, initialDelayMillis = 0)
    var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
    println("Initial element is available immediately: $nextElement")

    nextElement = withTimeoutOrNull(500) { tickerChannel.receive() }
    println("Next element is not available in 500ms: $nextElement")

    nextElement = withTimeoutOrNull(600) { tickerChannel.receive() }
    println("Next element is available in 1100ms: $nextElement")

    println("Consumer pauses for 1500ms")
    delay(1500)

    nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
    println("Next element is available after delay: $nextElement")

    nextElement = withTimeoutOrNull(600) { tickerChannel.receive() }
    println("Next element is available sooner because of the previous delay: $nextElement")

    tickerChannel.cancel()
}

異常處理與監(jiān)督

我們已經(jīng)知道被取消的協(xié)程會在掛起點拋出 CancellationException 并且它會被協(xié)程的機制所忽略。在這里我們會看看在取消過程中拋出異陈盏或同一個協(xié)程的多個子協(xié)程拋出異常時會發(fā)生什么掩驱。

異常的傳播

協(xié)程構(gòu)建器有兩種形式:自動傳播異常(launchactor)或向用戶暴露異常(asyncproduce)。當(dāng)這些構(gòu)建器用于創(chuàng)建一個 協(xié)程時冬竟,即該協(xié)程不是另一個協(xié)程的 協(xié)程昙篙,前者這類構(gòu)建器將異常視為未捕獲異常,類似 Java 的 Thread.uncaughtExceptionHandler诱咏,而后者則依賴用戶來最終消費異常苔可,例如通過 awaitreceive

private fun exceptionPropagation() = runBlocking {
    val job = GlobalScope.launch { // launch 根協(xié)程
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException()
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // async 根協(xié)程
        println("Throwing exception from async")
        throw ArithmeticException() // 沒有打印任何東西袋狞,依賴用戶去調(diào)用等待
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}

CoroutineExceptionHandler

未捕獲異常打印到控制臺的默認(rèn)行為是可自定義的焚辅。 協(xié)程中的 CoroutineExceptionHandler 上下文元素可以被用于這個根協(xié)程通用的 catch 塊,及其所有可能自定義了異常處理的子協(xié)程苟鸯。

它類似于 Thread.uncaughtExceptionHandler 同蜻。你無法從 CoroutineExceptionHandler 的異常中恢復(fù)痰滋。當(dāng)調(diào)用處理者的時候赊豌,協(xié)程已經(jīng)完成并帶有相應(yīng)的異常。通常壹甥,該處理者用于記錄異常砌梆,顯示某種錯誤消息默责,終止和(或)重新啟動應(yīng)用程序贬循。

取消與異常

取消與異常緊密相關(guān)。協(xié)程內(nèi)部使用 CancellationException 來進行取消桃序,這個異常會被所有的處理者忽略杖虾,所以那些可以被 catch 代碼塊捕獲的異常僅僅應(yīng)該被用來作為額外調(diào)試信息的資源。當(dāng)一個協(xié)程使用 Job.cancel 取消的時候媒熊,它會被終止奇适,但是它不會取消它的父協(xié)程。

異常聚合

當(dāng)協(xié)程的多個子協(xié)程因異常而失敗時芦鳍,一般規(guī)則是“取第一個異橙峦”,因此將處理第一個異常柠衅。在第一個異常之后發(fā)生的所有其他異常都作為被抑制的異常綁定至第一個異常皮仁。

監(jiān)督

取消是在協(xié)程的整個層次結(jié)構(gòu)中傳播的雙向關(guān)系。讓我們看一下需要單向取消的情況茄茁。

此類需求的一個良好示例是在其作用域內(nèi)定義作業(yè)的 UI 組件魂贬。如果任何一個 UI 的子作業(yè)執(zhí)行失敗了,它并不總是有必要取消(有效地殺死)整個 UI 組件裙顽, 但是如果 UI 組件被銷毀了(并且它的作業(yè)也被取消了)付燥,由于它的結(jié)果不再被需要了,它有必要使所有的子作業(yè)執(zhí)行失敗愈犹。

另一個例子是服務(wù)進程孵化了一些子作業(yè)并且需要監(jiān)督 它們的執(zhí)行键科,追蹤它們的故障并在這些子作業(yè)執(zhí)行失敗的時候重啟。

監(jiān)督作業(yè)

SupervisorJob 可以用于這些目的漩怎。它類似于常規(guī)的 Job勋颖,唯一的不同是:SupervisorJob 的取消只會向下傳播:

private fun supervisorJobExample() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // 啟動第一個子作業(yè)——這個示例將會忽略它的異常(不要在實踐中這么做!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // 啟動第二個子作業(yè)
        val secondChild = launch {
            firstChild.join()
            // 取消了第一個子作業(yè)且沒有傳播給第二個子作業(yè)
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // 但是取消了監(jiān)督的傳播
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // 等待直到第一個子作業(yè)失敗且執(zhí)行完成
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}
監(jiān)督作用域

對于作用域 的并發(fā)勋锤,可以用 supervisorScope 來替代 coroutineScope 來實現(xiàn)相同的目的饭玲。它只會單向的傳播并且當(dāng)作業(yè)自身執(zhí)行失敗的時候?qū)⑺凶幼鳂I(yè)全部取消。作業(yè)自身也會在所有的子作業(yè)執(zhí)行結(jié)束前等待叁执,就像 coroutineScope 所做的那樣茄厘。

private fun supervisorScopeExample() = runBlocking {
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("The child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("The child is cancelled")
                }
            }
            // 使用 yield 來給我們的子作業(yè)一個機會來執(zhí)行打印
            yield()
            println("Throwing an exception from the scope")
            throw AssertionError()
        }
    } catch (e: AssertionError) {
        println("Caught an assertion error")
    }
}
監(jiān)督協(xié)程中的異常

常規(guī)的作業(yè)和監(jiān)督作業(yè)之間的另一個重要區(qū)別是異常處理。監(jiān)督協(xié)程中的每一個子作業(yè)應(yīng)該通過異常處理機制處理自身的異常谈宛。

private fun coroutineExceptionHandler() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    supervisorScope {
        val child = launch(handler) {
            println("The child throws an exception")
            throw AssertionError()
        }
        println("The scope is completing")
    }
    println("The scope is completed")
}

共享的可變狀態(tài)與并發(fā)

協(xié)程可被多線程調(diào)度器并發(fā)地執(zhí)行次哈,這會造成常見的并發(fā)問題。主要的問題是同步訪問共享的可變狀態(tài)吆录。

線程安全的數(shù)據(jù)結(jié)構(gòu)

一種對線程窑滞、協(xié)程都有效的常規(guī)解決方法,就是使用線程安全(也稱為同步的、 可線性化哀卫、原子)的數(shù)據(jù)結(jié)構(gòu)巨坊,它為需要在共享狀態(tài)上執(zhí)行的相應(yīng)操作提供所有必需的同步處理。比如使用 AtomicInteger 類代替 Int 類聊训。

以細(xì)粒度限制線程

限制線程 是解決共享可變狀態(tài)問題的一種方案:對特定共享狀態(tài)的所有訪問權(quán)都限制在單個線程中抱究。它通常應(yīng)用于 UI 程序中:所有 UI 狀態(tài)都局限于單個事件分發(fā)線程或應(yīng)用主線程中恢氯。這在協(xié)程中很容易實現(xiàn)带斑,通過使用一個單線程上下文。

以粗粒度限制線程

在實踐中勋拟,線程限制是在大段代碼中執(zhí)行的勋磕,比如在單線程上下文中運行每個協(xié)程。

private fun massiveRunExecutorCoarseGrained() = runBlocking {
    withContext(coroutineContext) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")
}

互斥

另外我們還可以使用互斥解決方案:使用永遠不會同時執(zhí)行的關(guān)鍵代碼塊 來保護共享狀態(tài)的所有修改敢靡。

在阻塞的世界中挂滓,你通常會為此目的使用 synchronized 或者 ReentrantLock。 在協(xié)程中的替代品叫做 Mutex啸胧,它具有 lockunlock 方法赶站,可以隔離關(guān)鍵的部分。關(guān)鍵的區(qū)別在于 Mutex.lock() 是一個掛起函數(shù)纺念,它不會阻塞線程贝椿。

還有 withLock 擴展函數(shù),可以方便的替代常用的 mutex.lock(); try { …… } finally { mutex.unlock() } 模式陷谱。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末烙博,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子烟逊,更是在濱河造成了極大的恐慌渣窜,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宪躯,死亡現(xiàn)場離奇詭異乔宿,居然都是意外死亡,警方通過查閱死者的電腦和手機访雪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門详瑞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人冬阳,你說我怎么就攤上這事蛤虐。” “怎么了肝陪?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵驳庭,是天一觀的道長。 經(jīng)常有香客問我,道長饲常,這世上最難降的妖魔是什么蹲堂? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮贝淤,結(jié)果婚禮上柒竞,老公的妹妹穿的比我還像新娘。我一直安慰自己播聪,他們只是感情好朽基,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著离陶,像睡著了一般稼虎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上招刨,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天霎俩,我揣著相機與錄音,去河邊找鬼沉眶。 笑死打却,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谎倔。 我是一名探鬼主播柳击,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼传藏!你這毒婦竟也來了腻暮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤毯侦,失蹤者是張志新(化名)和其女友劉穎哭靖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侈离,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡试幽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了卦碾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铺坞。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖洲胖,靈堂內(nèi)的尸體忽然破棺而出济榨,到底是詐尸還是另有隱情,我是刑警寧澤绿映,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布擒滑,位于F島的核電站腐晾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏丐一。R本人自食惡果不足惜藻糖,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望库车。 院中可真熱鬧巨柒,春花似錦、人聲如沸柠衍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拧略。三九已至芦岂,卻和暖如春瘪弓,著一層夾襖步出監(jiān)牢的瞬間垫蛆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工腺怯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留袱饭,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓呛占,卻偏偏與公主長得像虑乖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子晾虑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容