Kotlin中的contract到底有什麼用詳解
前言
我們在開發中肯定會經常用Kotlin提供的一些通用拓展函數,當我們進去看源碼的時候會發現許多函數裡面有contract {}包裹的代碼塊,那麼這些代碼塊到底有什麼作用呢??
測試
接下來用以下兩個我們常用的拓展函數作為例子
public inline fun <T, R> T.run(block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() } public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 }
run和isNullOrEmpty我相信大傢在開發中是經常見到的。
不知道那些代碼有什麼作用,那麼我們就把那幾行代碼去掉,然後看看函數使用起來有什麼區別。
public inline fun <T, R> T.runWithoutContract(block: T.() -> R): R { return block() } public inline fun CharSequence?.isNullOrEmptyWithoutContract(): Boolean { return this == null || this.length == 0 }
上面是去掉瞭contract{}代碼塊後的兩個函數 調用看看
fun test() { var str1: String = "" var str2: String = "" runWithoutContract { str1 = "jayce" } run { str2 = "jayce" } println(str1) //jayce println(str2) //jayce }
經過測試發現,看起來好像沒什麼問題,run代碼塊都能都正常執行,做瞭賦值的操作。
那麼如果是這樣呢
將str的初始值去掉,在run代碼塊裡面進行初始化操作
@Test fun test() { var str1: String var str2: String runWithoutContract { str1 = "jayce" } run { str2 = "jayce" } println(str1) //編譯不通過 (Variable 'str1' must be initialized) println(str2) //編譯通過 }
??????
我們不是在runWithoutContract做瞭初始化賦值的操作瞭嗎?怎麼IDE還報錯,難道是IDE出瞭什麼問題?好 有問題就重啟,我去,重啟還沒解決。。。。好重裝。不不不!!別急 會不會Contract代碼塊就是幹這個用的?是不是它悄悄的跟IDE說瞭什麼話 以至於它能正常編譯通過?
好 這個問題先放一放 我們再看看沒contract版本的isNullOrEmpty對比有contract的有什麼區別
fun test() { val str: String? = "jayce" if (!str.isNullOrEmpty()) { println(str) //jayce } if (!str.isNullOrEmptyWithoutContract()) { println(str) //jayce } }
發現好像還是沒什麼問題。相信大傢根據上面遇到的問題可以猜測,這其中肯定也有坑。
比如這種情況
fun test() { val str: String? = "jayce" if (!str.isNullOrEmpty()) { println(str.length) // 編譯通過 } if (!str.isNullOrEmptyWithoutContract()) { println(str.length) // 編譯不通過(Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?) } }
根據錯誤提示可以看出,在isNullOrEmptyWithoutContract判斷為flase之後的代碼塊,str這個字段還是被IDE認為是一個可空類型,必須要進行空檢查才能通過。然而在isNullOrEmpty返回flase之後的代碼塊,IDE認為str其實已經是非空瞭,所以使用前就不需要進行空檢查。
查看 contract 函數
public inline fun contract(builder: ContractBuilder.() -> Unit) { }
點進去源碼,我們可以看到contract是一個內聯函數,接收一個函數類型的參數,該函數是ContractBuilder的一個拓展函數(也就是說在這個函數體裡面擁有ContractBuilder的上下文)
看看ContractBuilder給我們提供瞭哪些函數(主要就是依靠這些函數來約定我們自己寫的lambda函數)
public interface ContractBuilder { //描述函數正常返回,沒有拋出任何異常的情況。 @ContractsDsl public fun returns(): Returns //描述函數以value返回的情況,value可以取值為 true|false|null。 @ContractsDsl public fun returns(value: Any?): Returns //描述函數以非null值返回的情況。 @ContractsDsl public fun returnsNotNull(): ReturnsNotNull //描述lambda會在該函數調用的次數,次數用kind指定 @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace }
returns
其中 returns() returns(value) returnsNotNull() 都會返回一個繼承於SimpleEffect的Returns 接下來看看SimpleEffect
public interface SimpleEffect : Effect { //接收一個Boolean值的表達式 改函數用來表示當SimpleEffect成立之後 保證Boolean值的表達式返回值為true //表達式可以傳判空代碼塊(`== null`, `!= null`)判斷實例語句 (`is`, `!is`)。 public infix fun implies(booleanExpression: Boolean): ConditionalEffect }
可以看到SimpleEffect裡面有一個中綴函數implies 。可以使用ContractBuilder的函數指定某種返回的情況 然後用implies來聲明傳入的表達式為true。
看到這裡 那麼我們應該就知道 isNullOrEmpty() 加的contract是什麼意思瞭
public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { //返回值為false的情況 returns(false) //意味著 implies //調用該函數的對象不為空 (this@isNullOrEmpty != null) returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 }
因為isNullOrEmpty裡面加瞭contract代碼塊,告訴IDE說:返回值為false的情況意味著調用該函數的對象不為空。所以我們就可以直接在判斷語句後直接使用非空的對象瞭。
有些同學可能還是不理解,這裡再舉一個沒什麼用的例子(運行肯定會crash哈。。。)
@ExperimentalContracts //因為該特性還在試驗當中 所以需要加上這個註解 fun CharSequence?.isNotNull(): Boolean { contract { //返回值為true returns(true) //意味著implies //調用該函數的對象是StringBuilder (this@isNotNull is StringBuilder) returns(true) implies (this@isNotNull is StringBuilder) } return this != null } fun test() { val str: String? = "jayce" if (str.isNotNull()) { str.append("")//String可是沒有這個函數的,因為我們用contract讓他強制轉換成StringBuilder瞭 所以才有瞭這個函數 } }
是的 這樣IDE居然沒有報錯,因為經過我們contract的聲明,隻要這個函數返回true,調用函數的對象就是一個StringBuilder。
callsInPlace
//描述lambda會在該函數調用的次數,次數用kind指定 @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
可以知道callsInPlace是用來指定lambda函數調用次數的
kind有四種取值
- InvocationKind.AT_MOST_ONCE:最多調用一次
- InvocationKind.AT_LEAST_ONCE:最少調用一次
- InvocationKind.EXACTLY_ONCE:調用一次
- InvocationKind.UNKNOWN:未知,不指定的默認值
我們再看回去之前run函數裡面的contract聲明瞭什麼
public inline fun <T, R> T.run(block: T.() -> R): R { contract { //block這個函數,剛好調用一次 callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }
看到這裡 應該就知道為什麼我們自己寫的runWithoutContract會報錯(Variable 'str1' must be initialized),而系統的run卻不會報錯瞭,因為run聲明瞭lambda會調用一次,所以就一定會對str2做初始化操作,然而runWithoutContract卻沒有聲明,所以IDE就會報錯(因為有可能不會調用,所以就不會做初始化操作瞭)。
總結
Kotlin提供瞭一些自動轉換的功能,例如平時判空和判斷是否為某個實例的時候,Kotlin都會為我們自動轉換。但是如果這個判斷被提取到其他函數的時候,這個轉換會失效。所以提供瞭contract給我們在函數體添加聲明,編譯器會遵守我們的約定。
當使用一個高階函數的時候,可以使用callsInPlace指定該函數會被調用的次。例如在函數體裡面做初始化,如果申明為EXACTLY_ONCE的時候,IDE就不會報錯,因為編譯器會遵守我們的約定。
到此這篇關於Kotlin中的contract到底有什麼用的文章就介紹到這瞭,更多相關Kotlin contract用處內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Kotlin線程的橋接與切換使用介紹
- Kotlin協程的啟動方式介紹
- java對象對比之comparable和comparator的區別
- Java算法練習題,每天進步一點點(1)
- 簡單易懂的java8新特性之lambda表達式知識總結