Kotlin契約(Contract)

Contract是Kotlin1.3的東西,比較新,目前仍是處於實現性階段(Experimental),即API在穩定版以前可能會發生變更。因爲是實現性API,使用時須要額外添加註解,下面代碼中會具體講到。
android

配置環境

在項目的gradle文件中程序員

buildscript {
    dependencies {
        //kotlin_version確保在1.3或以上
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
複製代碼

因爲契約處於實驗性
能夠經過添加如下編譯器選項(可選),這樣就不用在使用契約時到處添加註解了
在模塊的gradle文件中函數

android {
    kotlinOptions {
        freeCompilerArgs += [
                "-Xuse-experimental=kotlin.contracts.ExperimentalContracts"
        ]
    }
}
複製代碼

爲什麼要使用契約

先看一下下面這段簡單的代碼post

fun runFun(action: () -> Unit) {
    action()
}

fun getValue(): Int {
    var ret: Int
    runFun {
        ret = 15;
    }
    return ret
}
複製代碼

getValue中調用一次runFun,運行時效果至關於把ret = 15調用了一次,注意是運行時,在編譯時編譯器並不知道runFun調用時傳入的action有無被調用,於是編譯時報錯Variable 'ret' must be initialized
gradle

再看一個相似的例子ui

fun printLength(s: String?) {
    if (s != null) {
        Log.d("TAG", "${s.length}")
    }
}
複製代碼

當字符串不爲null時則將長度打印出來。
this

It works fine.
spa

但若是程序中對字符串有不少這種判斷,應該就會想到這個判斷寫成一個函數,減小代碼冗餘。因而就可能寫成下面的版本code

fun printLength(s: String?) {
    if (s.notNull()) {
        Log.d("TAG", "${s.length}")
    }
}

fun String?.notNull(): Boolean {
    return this != null
}
複製代碼

這個版本對可空字符串的檢查封裝成了拓展函數形式,一眼望上去,聰明的編譯器應該會在s.length的地方,有一個smart cast,將String?自動轉換成String以使得length能正確被調用,但事實倒是:編譯器報錯 Only safe (?.) or non-null asserted(!!.) calls are allowed on a nullable reciever of type String?,編譯器並無作上述類型轉換,Why?
cdn

不難解釋,通常函數的調用都是在運行時知道結果的,上述的notNullrunFun天然也是如此,函數調用的結果沒法做爲調用處編譯時的上下文,即函數內部在編譯時在調用處是不可見的,所以編譯器沒法經過這個上下文做出smart cast的行爲

所以不要太難爲編譯器,咱們應該給編譯器一點提示,契約正式出場!

使用契約

runFun 的契約版本

//有了上面模塊gradle配置,註解可省略,若是兩個都沒有,編譯器報錯
@ExperimentalContracts
fun runFun(action: () -> Unit) {
    contract {
        callsInPlace(action, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
    }
    action()
}
複製代碼

先來解釋一下這段代碼含義
咱們在runFun的開頭加入了contract函數

//contract源碼
public inline fun contract(builder: ContractBuilder.() -> Unit) { }
複製代碼

其接受帶一個無參無返回值的函數,並且這個函數還有一個值接收者ContractBuilder用於提供callsInPlacereturns等函數的調用
其中上面的callsInPlace兩個參數,第一個是任意函數類型,第二個參數表示傳入的函數會被調用的次數,好比例子中的InvocationKind.EXACTLY_ONCE代表函數在運行時會被執行一次。說到這裏,大概能夠猜到,contract面向編譯器的,給編譯器看的,就是爲了向編譯器代表調用contract函數的這個函數(好比上面的runFun)是作什麼的,getValue中調用契約版的runFun函數,編譯器就能知道,傳入的action函數會被調用一次,即變量ret將會在運行時會被初始化成15。

(除了InvocationKind.EXACTLY_ONCE外還有AT_LEAST_ONCE等常量,具體含義查閱文檔)

notNull 的契約版本

@ExperimentalContracts
fun String?.notNull(): Boolean {
    contract {
        returns(true) implies (this@notNull != null)
    }
    return this != null
}
複製代碼

contract 代碼代表當implies後的值成立,函數將會返回returns函數中的內容,注意這裏implies是一箇中綴運算符
因此notNull函數中的contract告訴了編譯器,當字符串不爲null時函數在運行時將會返回true

契約能讓編譯器smart cast的能力進一步發揮出來,這也說明了你能夠"欺騙"編譯器,好比在剛纔的notNull函數中,將returns中的true改爲false(本身體會),並且再次說明契約在開發環境中爲實驗性API,這代表它即便能在kotlin標準庫中的函數好比letcheckNotNull正常發揮做用,可是在你使用的時候,可能會有一些編譯時的bug,並且未來API的使用可能會發生變更,因此請謹慎使用

分析契約 參考連接

Effect.kt ,裏面定義了幾個直接和間接繼承於Effect的接口,代碼量很少,具體含義所有都寫了出來

//用來表示一個函數被調用的效果
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調用聲明必須是函數體內第一條語句
  • 編譯器無條件地信任契約,這意味着程序員負責編寫正確合理的契約,不要欺騙編譯器它會傷心的

總結

契約的做用就是把函數行爲(好比例子中的null-check,和對action的調用)告知給編譯器,使得開發者能夠把這些行爲封裝到函數中,同時還能發揮編譯器的智能推導效果

相關文章
相關標籤/搜索