閱讀文檔和《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)該按照以下順序:
- 屬性聲明和初始化代碼塊
- 從構(gòu)造器
- 方法聲明
- 伴生對象
初次之外羔沙,不要將方法根據(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)的烹植,let
和 also
通過 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)庫中還提供了 takeIf
和 takeUnless
方法,可以讓我們在使用對象之前對其狀態(tài)進行檢查救氯。
takeIf
只有在對象滿足斷言時才返回對象找田,否則返回 null,takeUnless
則恰恰相反着憨,只有在對象不滿足斷言時才返回對象墩衙,否則返回 null,所以 takeIf
和 takeUnless
是對單個對象的篩選方法甲抖。
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")
}
由于返回值可能為空漆改,所以必須使用 ?.
。而且可以看到 takeIf
和 takeUnless
非常適合配合 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顺少,即 A
是 B
的父類,同時 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食店,如果 A
是 B
的父類,那么 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
迄本,表示它接受類型分別為A
與B
的兩個參數(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 }
- lambda 表達式:
-
使用已聲明的可調(diào)用引用:
- 頂層苍日、局部、成員相恃、擴展函數(shù):
::isOdd
、String::toInt
- 頂層盈简、成員、擴展屬性:
List<Int>::size
- 構(gòu)造函數(shù):
::Regex
- 頂層苍日、局部、成員相恃、擴展函數(shù):
-
使用實現(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)的延曙,不需要反射,所以 !is
和 as
都可以使用了亡哄。
內(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ā)
上面的例子中举反,doSomethingUsefulOne
和 doSomethingUsefulTwo
之間并沒有依賴,為了更快地得到結(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")
}
async
是 CoroutineScope
的擴展方法,默認(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é)程才會啟動懦铺,或者在 Job
的 start 函數(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)用 doSomethingUsefulOne
和 doSomethingUsefulTwo
:
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()
}
我們把 doSomethingUsefulOne
和 doSomethingUsefulTwo
放到了同一個 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)建器諸如 launch 和 async 接收一個可選的 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)換浦马,比如 filter
和 map
时呀。過渡操作符應(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)化為各種集合坞靶,例如
toList
與toSet
- 獲取第一個
first
值與確保流發(fā)射單個single
值的操作符 - 使用
reduce
與fold
將流規(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
展平連接主要由 flatMapConcat 與 flattenConcat 操作符實現(xiàn)蜒什。
flatMapMerge
另一種展平模式是并發(fā)收集所有傳入的流测秸,并將它們的值合并到一個單獨的流,以便盡快的發(fā)射值灾常。 它由 flatMapMerge 與 flattenMerge 操作符實現(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)建器有兩種形式:自動傳播異常(launch 與 actor)或向用戶暴露異常(async 與 produce)。當(dāng)這些構(gòu)建器用于創(chuàng)建一個根 協(xié)程時冬竟,即該協(xié)程不是另一個協(xié)程的子 協(xié)程昙篙,前者這類構(gòu)建器將異常視為未捕獲異常,類似 Java 的 Thread.uncaughtExceptionHandler
诱咏,而后者則依賴用戶來最終消費異常苔可,例如通過 await 或 receive。
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啸胧,它具有 lock 和 unlock 方法赶站,可以隔離關(guān)鍵的部分。關(guān)鍵的區(qū)別在于 Mutex.lock()
是一個掛起函數(shù)纺念,它不會阻塞線程贝椿。
還有 withLock 擴展函數(shù),可以方便的替代常用的 mutex.lock(); try { …… } finally { mutex.unlock() }
模式陷谱。