JetBrains開發者日見聞(二)之Kotlin1.3的新特性(Contract契約與協程篇)

簡述: 上接上篇文章,今天咱們來說點Kotlin 1.3版本中比較時髦的東西,那麼,今天就開始第二篇,看過一些大佬寫關於Kotlin 1.3版本新特性的文章,基本上都是翻譯了Kotlin Blog的官網博客。今天我不打算這麼講,既然今天的主題是時髦那就講點有意思的東西。就像JetBrains開發者日上佈道師Hali在講Kotlin1.3新特性的時候徹底就不用PPT的,拿起代碼就是幹。一塊兒來看下今天提綱:java

1、Coroutine協程轉正,Kotlin/Native 1.0Beta

一、Coroutine協程的基本介紹

協程涉及內容不少,不是一句話就能講清楚的,鑑於文章篇幅問題,因此這裏不會去展開,後續會有專門系列文章去深刻探討。這裏只是作個基本介紹。git

  • 協程轉正

Kotlin 1.3版本最重要的莫過於協程轉正,協程實際上在官方的庫中早就有應用,可是因爲API不穩定一直在Experimental中,若是你以前就玩過協程的話,對比正式版本Coroutine API上仍是有那麼一點變化的。協程實際上就是輕量級的線程,一個線程中能夠執行多個協程。程序員

  • 協程做用

協程給開發者最大的感受就是很好地解決了回調地獄的問題,在代碼層面將異步的任務寫成同步的調用形式。並且不會阻塞當前的線程。 對於Android開發者而言,爲了比較好地解決多請求回調的問題,咱們天然就想到了RxJava。RxJava確實能夠在必定程度下緩解回調地獄問題,可是仍是沒法擺脫使用回調的事實。那麼協程將帶你用一個全新的方式來解決這個問題.在Android開發中Kotlin的Coroutine協程是基本能夠替代RxJava,而且這次大會上來自Google中國技術團隊負責人就演講關於Corouine在Android中的使用,而且還說不久Retrofit庫將支持協程中的suspend函數。此外Jake Wharton 大神寫了一個Retrofit支持協程Coroutine的adapter庫 retrofit2-kotlin-coroutines-adapter. 對於使用Kotlin開發Android的新項目來講,建議你不妨嘗試使用Corouine來替代RxJava來用,也許你會以爲是真的爽啊。不信的話能夠來看個協程在Android中應用的例子(僞代碼):github

suspend fun updateData(showId: Long) = coroutineScope {
    val localCacheDeferred = async {//啓動一個異步任務處理本地耗時IO
        localDataStore.getShow(showId)
    }
    val remoteDataDeferred1 = async {//啓動一個異步任務網絡請求1
        remoteSource1.getShow(showId)
    }
    val remoteDataDeferred2 = async {//啓動一個異步任務網絡請求2
        remoteSource2.getShow(showId)
    }

    val localResult = localCacheDeferred.await()//調用await函數會掛起等待,直到執行任務結果返回,根本不須要出傳入一個callback等待結果回來回調。
    val remoteResult1 = remoteDataDeferred1.await()
    val remoteResult2 = remoteDataDeferred2.await()
    val mergedResult = handleMerged(localResult, remoteResult1, remoteResult2)
    localDataStore.saveShow(mergedResult)
}
複製代碼

能夠看到以上涉及到了三個異步任務,然而只須要使用await函數掛起等待結果返回便可,根本就不須要callback回調,能夠看到整個代碼看起來都是同步的,實際上包含三個異步任務。能夠看到協程使用起來仍是很爽的。ajax

  • 三、解決回調地獄其餘例子

不只僅是Kotlin有這樣的應用例子 ,若是你對JavaScript有所瞭解,JavaScript也是這樣的,老版的JS執行異步任務通常用的是Promise或者ajax方式,一個異步執行後面總能帶一個callback, 一旦涉及稍微複雜一點頁面場景就出現了callback嵌套(回調地獄) 若是對ES6的語法新特性熟悉的話. 在Es6語法新增了async、await函數,也是將異步任務寫成同步的調用,解決了JS中回調地獄的問題。編程

  • 四、其餘語言中的協程

協程不只僅是Kotlin這門語言纔有,相反在它出來以前老早就有了,諸如Python中的協程還有window編程的纖程,實際上和協程差很少。記得前段時間看到國外一篇文章說Java內部也在研究一個相似協程的輕量級線程框架項目(Loom),具體仍然還在試驗階段。想一想Oracle這尿性估計獲得Java 14以後吧,感興趣的能夠去具體瞭解下。網絡

二、Kotlin/Native 1.0Beta

關於Kotlin/Native這裏就不詳細介紹了,感興趣去參考個人上篇文章JetBrains開發者日見聞(一)之Kotlin/Native 嚐鮮篇,裏面有不少Kotlin/Native的詳細介紹。app

2、有意思的Contract 契約(Experimental)

看到標題有趣的contract契約,確實挺有趣也是很是實用的一個語法點。接下來一塊兒看下Kotlin 1.3版本帶來一個好玩的語法特性Contract契約,可是cotract仍是在Experimental, API後期可能有變更,建議能夠提早玩玩,可是先不要引入到生產環境中.框架

一、爲什麼須要Contract契約

咱們都知道Kotlin中有個很是nice的功能就是類型智能推導(官方稱爲smart cast), 不知道小夥伴們在使用Kotlin開發的過程當中有沒有遇到過這樣的場景,會發現有時候智能推導可以正確識別出來,有時候卻失敗了。 不知道你們有沒有去深刻研究過這個問題啊,那麼這裏將會給出一個例子復現那種場景,一塊兒來看下:異步

  • 案例一
//在Java中定義一個生成token的函數的類,而且這個函數有可能返回null
package com.mikyou.news;

import org.jetbrains.annotations.Nullable;

public class TokenGenerator {
    public @Nullable String generateToken(String type) {
        return type.isEmpty() ? null : "2s4dfhj8aeddfduvcqopdflfgjfgfgj";
    }
}

複製代碼
//在Kotlin中去調用這個函數並生成可空類型String接收
import com.mikyou.news.TokenGenerator

fun main(args: Array<String>) {
    val token: String? = TokenGenerator().generateToken("kotlin")
    if (token != null && token.isNotBlank()) {//這裏作判空處理
        println("token length is ${token.length}")//這裏token.length編譯正常而且就進行了smart cast
    }
}
複製代碼

將鼠標移動到token.length中token上編譯器就會告知你已經被smart cast處理過了

  • 案例二 這時候的你忽然想把判空處理檢查放在一個checkTokenIsValid函數處理,在代碼職責和可讀性那塊徹底合情合理。可是若是你這麼作了,神奇的事就悄然發生了。
fun main(args: Array<String>) {
    val token: String? = TokenGenerator().generateToken("kotlin")
    if (checkTokenIsValid(token)) {//這裏判空處理交於函數來處理,根據函數返回值作判斷
        println("token length is ${token.length}")//編譯異常: 報token是個可空類型,須要作判空處理。這時候是否是就很鬱悶了
    }
}

fun checkTokenIsValid(token: String?): Boolean{
    return token != null && token.isNotBlank()
}
複製代碼

你會發現token.length那報錯了提示你進行判空處理,以下圖所示,是否是一臉懵逼,實際上你在函數中已經作了判空處理啊。可是編譯器說在它所知做用域內token就是個可空類型,因此就必須判空。這時候你就會以爲了哪裏智能?

遇到上述的場景,相信不少人是否是都是使用 !! 來解決的啊。

//使用!!來解決問題
fun main(args: Array<String>) {
    val token: String? = TokenGenerator().generateToken("kotlin")
    if (checkTokenIsValid(token)) {//這裏判空處理交於函數來處理,根據函數返回值作判斷
        println("token length is ${token!!.length}")//編譯正常: 使用!!強制告訴編譯器這裏不爲null
    }
}

fun checkTokenIsValid(token: String?): Boolean{
    return token != null && token.isNotBlank()
}
複製代碼
  • 案例三: 使用官方內置判斷函數isNullOrBlank函數處理

使用Kotlin內置的函數去處理,你又會更加懵逼了...

fun main(args: Array<String>) {
    val token: String? = TokenGenerator().generateToken("kotlin")
    if (!token.isNullOrBlank()) {//這裏判空處理交於函數來處理,根據函數返回值作判斷
        println("token length is ${token.length}")//編譯正常: 使用isNullOrBlank取反操做,這裏智能推導正常識別
    }
}
複製代碼

看完這三個案例是否是一臉懵逼啊,一樣都是定義成一個函數爲啥咱們本身的函數不能被識別智能推導,而Kotlin內置就能夠呢,內置那個函數裏面到底有什麼黑魔法。因而咱們彷佛發現了問題的本質,就是對比一下isNullOrBlank函數實現有什麼不一樣不就明白了嗎?打開isNullOrBlank函數源碼:

@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {//這裏彷佛不一樣,多了個contract
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()//這裏基本都是正常的判空
}
複製代碼

經過查看isNullOrBlank函數源碼,彷佛我發現了一個新的東西contract,沒錯這就是咱們今天要分析的主角Contract 契約

  • 案例四: 利用自定義contract契約讓checkTokenIsValid函數具備被編譯器智能推導識別的黑魔法
@ExperimentalContracts //因爲Contract契約API仍是Experimental,因此須要使用ExperimentalContracts註解聲明
fun main(args: Array<String>) {
    val token: String? = TokenGenerator().generateToken("kotlin")
    if (checkTokenIsValid(token)) {//這裏判空處理交於函數來處理,根據函數返回值作判斷
        println("token length is ${token.length}")//編譯正常: 使用自定義契約實現,這裏智能推導正常識別
    }
}

@ExperimentalContracts //因爲Contract契約API仍是Experimental,因此須要使用ExperimentalContracts註解聲明
fun checkTokenIsValid(token: String?): Boolean{
    contract {
        returns(true) implies (token != null)
    }
    return token != null && token.isNotBlank()
}
複製代碼

經過以上幾個例子對比,是否是發現契約很神奇,貌似它有和編譯器溝通說話方式,告訴它這裏是smart cast,不要再提示我判空處理了。如今就揭開Contract神祕面紗,請接着往下看。

二、Contract契約基本介紹

  • 基本定義

Kotlin中的Contract契約是一種向編譯器通知函數行爲的方法。 就像上面所說那樣,貌似它能告訴編譯器此時它行爲是什麼。

  • 基本格式
//僞代碼
fun someThing(){
    contract{
       ...//get some effect
    }
}
複製代碼

上述代碼意思是: 調用函數someThing併產生某種效果

是否是一臉懵逼,好抽象啊,不慌來一個實例解釋一下:

@ExperimentalContracts //因爲Contract契約API仍是Experimental,因此須要使用ExperimentalContracts註解聲明
fun checkTokenIsValid(token: String?): Boolean{
    contract {
        returns(true) implies (token != null)
    }
    return token != null && token.isNotBlank()
}
//這裏契約的意思是: 調用checkTokenIsValid函數,會產生這樣的效果: 若是返回值是true, 那就意味着token != null. 把這個契約行爲告知到給編譯器,編譯器就知道了下次碰到這種情形,你的token就是非空的,天然就smart cast了。注意: 編譯器下次才能識別,因此當你改了契約後,你會發現smart cast不會立刻生效,而是刪除後從新調用纔可生效。
複製代碼

三、Kotlin源碼中Contract契約的應用

儘管在Contract契約目前仍是處於Experimental狀態,可是在Kotlin以前的版本標準庫就已經大量使用Contract契約。包括上述例子所看到的isNullOrBlank函數就用到了契約,你能夠找到1.2版本Kotlin源碼中就能輕鬆找到。那麼這裏就舉幾個常見的例子。

  • CharSequence類擴展函數isNullOrBlank()、isNullOrEmpty()
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrBlank(): Boolean {

    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()
}
複製代碼

契約解釋: 這裏契約表示告訴編譯器:調用isNullOrBlank()擴展函數產生效果是若是該函數的返回值是false,那麼就意味着當前CharSequence實例不爲空。因此咱們能夠發現一個細節當你調用isNullOrBlank()只有在取反的時候,smart cast纔會生效,不信能夠本身試試。

  • requireNotNull函數(這個函數相信你們都用過吧)
@kotlin.internal.InlineOnly
public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T {
    contract {
        returns() implies (value != null)
    }

    if (value == null) {
        val message = lazyMessage()
        throw IllegalArgumentException(message.toString())
    } else {
        return value
    }
}
複製代碼

契約解釋: 這裏能夠看到和上面有點不同,不帶參數的returns()函數,這表示告訴編譯器:調用requireNotNull函數後產生效果是若是該函數正常返回,沒有異常拋出,那麼就意味着value不爲空

  • 常見標準庫函數run,also,with,apply,let(這些函數你們再熟悉不過吧,每一個裏面都用到contract契約)
//以apply函數舉例,其餘函數同理
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
複製代碼

契約解釋: 看到這個契約是否是感受一臉懵逼,再也不是returns函數了,而是callsInPlace函數,還帶傳入一個InvocationKind.EXACTLY_ONCE參數又是什麼呢? 該契約表示告訴編譯器:調用apply函數後產生效果是指定block lamba表達式參數在適當的位置被調用。適當位置就是block lambda表達式只能在本身函數(這裏就是指外層apply函數)被調用期間被調用,當apply函數被調用結束後,block表達式不能被執行,而且指定了InvocationKind.EXACTLY_ONCE表示block lambda表達式只能被調用一次,此外這個外層函數還必須是個inline內聯函數。

四、Contract契約背後原理(Contract源碼分析)

看到上述Contract契約在源碼中普遍的應用,而且看到上個例子分別表明三種不一樣類型的契約。此時的你是否是對Contract的源碼頓時產生了濃厚的興趣呢?下面咱們將去簡要剖析下Contract契約的源碼。一說到分析源碼不少人會腦殼疼,那是你可能沒找到很好分析源碼方法。

下面給出我的分析源碼的一個習慣(一直沿用至今,效果感受仍是不錯的):

源碼分析的規則: 須要分析一個源碼問題,首先肯定問題域,而後再列舉出問題域中全部參與的角色或者名詞概念,每一個角色或名詞所起的做用,角色與角色之間的關係,他們是如何通訊的,如何創建聯繫的。

那麼,咱們就用這個原則一步步揭開Contract神祕面紗,讓你對Contract的API有個更深全面的瞭解。

  • 第一步,肯定問題域(也就是你須要研究東西)

梳理和理解Contract契約背後原理,以及它的工做流程

  • 第二步,肯定問題域中參與角色(也就是Contract中那些API類),先給出一個contracts package中全部類和接口

  • 第三步,理清它們各自職責。
//用來表示一個函數被調用的效果
public interface Effect 
//繼承Effect接口,用來表示在觀察函數調用後另外一個效果以後,某些條件的效果爲true。
public interface ConditionalEffect : Effect 

//繼承Effect接口,用來表示一個函數調用後的結果(這個通常就是最爲普通的Effect)
public interface SimpleEffect : Effect {
    public infix fun implies(booleanExpression: Boolean): ConditionalEffect //infix代表了implies函數是一箇中綴函數,那麼它調用起來就像是中綴表達式同樣
}
//繼承SimpleEffect接口,用來表示當一個函數正常返回給定的返回值
public interface Returns : SimpleEffect
//繼承SimpleEffect接口,用來表示當一個函數正常返回非空的返回值
public interface ReturnsNotNull : SimpleEffect
//繼承Effect接口,用來表示調用函數式參數(lambda表達式參數)的效果,而且函數式參數(lambda表達式參數)只能在本身函數被調用期間被調用,當本身函數被調用結束後,函數式參數(lambda表達式參數)不能被執行.
public interface CallsInPlace : Effect
複製代碼

而後重點來看下ContractBuilder.kt文件,這個其實是Contract契約一個DSL Builder以及暴露給最外面一個contract函數.

//ContractBuilder接口聚合了不一樣的Effect返回對應接口對象的函數
public interface ContractBuilder {
    @ContractsDsl public fun returns(): Returns

    @ContractsDsl public fun returns(value: Any?): Returns
    
    @ContractsDsl public fun returnsNotNull(): ReturnsNotNull

    @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}

//用於枚舉callsInPlace函數lambda表達式參數被調用次數狀況
public enum class InvocationKind {
    //最多隻能被調用一次(不能調用或只能被調用1次)
    @ContractsDsl AT_MOST_ONCE,
    //至少被調用一次(只能調用1次或屢次)
    @ContractsDsl AT_LEAST_ONCE,
    //僅僅只能調用一次
    @ContractsDsl EXACTLY_ONCE,
    //不能肯定被調用多少次
    @ContractsDsl UNKNOWN
}
複製代碼
  • 第四步,理清Effect之間關係。

  • 第五步,分析一個例子模擬contract工做流程
fun checkTokenIsValid(token: String?): Boolean{
    contract {//首先這裏實際上就是調用那個contract函數傳入一個帶ContractBuilder類型返回值的Lambad表達式。
        returns(true) implies (token != null)
        //而後這裏開始定義契約規則,lambda表達式體內就是ContractBuilder,因此這裏的returns(value)函數實際上至關於this.returns(true);
        //再接着分析implies函數這是一箇中綴調用能夠看到寫起來像中綴表達式,實際上至關於returns(value)函數返回一個Returns接口對象,Returns接口是繼承了SimpleEffect接口(帶有implies中綴函數)的,因此直接用Returns接口對象中綴調用implies函數
    }
    return token != null && token.isNotBlank()
}
複製代碼

其實分析完後發現契約描述的實際上就是函數的行爲,包括函數返回值、函數中lambda表達式形參在函數內部執行規則。把這些行爲約束告知給編譯器,能夠節省編譯器智能分析的時間,至關於開發者幫助編譯器更快更高效作一些智能推導事情。

五、自定義Contract契約

實際上自定義在上述例子中早就給出來,因爲在Kotlin1.3版本Contract還處於實驗階段,因此不能直接使用。看了源碼中各類契約使用例子,相信自定義一個契約應該很簡單了。

//這裏給出一個instanceOf類型smart cast例子
data class Message(val msg: String)
 
@ExperimentalContracts //加上Experimental註解
fun handleMessage(message: Any?) {
    if (isInstanceOf(message)) {
        println(message.msg) 
    }
}
 
@ExperimentalContracts
fun isInstanceOf(message: Any?): Boolean {
    contract { 
        returns(true) implies (message is Message)
    }
    return message is Message
}

複製代碼

其實說了那麼多,你們有沒有體會到一點東西,契約實際上就是開發者和編譯器之間定義一個協議規則,開發者經過契約去向編譯器傳遞一些特殊效果Effect。並且這些效果都是針對函數調用的行爲來肯定的。因此從另外一方面也說明了開發者必須足夠了解業務場景才能使用契約,由於這至關於編譯器把一些操做信任於開發者來處理,開發者使用空間靈活度越高,那麼危險性也越大,切記不能濫用。

六、Contract契約使用限制

雖然Kotlin契約看起來很棒,但目前的語法目前還不穩定,而且將來API可能會有改變。不過有了上述一系列分析,就算未來變換了,你也能很快理解。

  • 一、咱們只能在頂層函數體內使用Contract契約,即咱們不能在成員和類函數上使用它們。
  • 二、Contract調用聲明必須是函數體內第一條語句
  • 三、就像我上面說得那樣,編譯器無條件地信任契約;這意味着程序員負責編寫正確合理的契約。

儘管Contract還處於實驗階段,可是咱們也看到了在好久以前版本中stalib標準庫源碼中就大量使用了契約,因此預測在後續版本中API改動也不會很大,因此這時候深刻分析仍是值得的。

3、結語

因爲Contract契約深刻分析一下,佔用文章篇幅過大。因此其餘新特性相關介紹和分析挪到了下篇文章,歡迎持續關注~~~。

Kotlin系列文章,歡迎查看:

原創系列:

翻譯系列:

實戰系列:

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索