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
不難解釋,通常函數的調用都是在運行時知道結果的,上述的notNull
和runFun
天然也是如此,函數調用的結果沒法做爲調用處編譯時的上下文,即函數內部在編譯時在調用處是不可見的,所以編譯器沒法經過這個上下文做出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
用於提供callsInPlace
、returns
等函數的調用
其中上面的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標準庫中的函數好比let
,checkNotNull
正常發揮做用,可是在你使用的時候,可能會有一些編譯時的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的調用)告知給編譯器,使得開發者能夠把這些行爲封裝到函數中,同時還能發揮編譯器的智能推導效果