一文講解Kotlin中的contract到底有什么用
前言
我們?cè)陂_(kāi)發(fā)中肯定會(huì)經(jīng)常用Kotlin提供的一些通用拓展函數(shù),當(dāng)我們進(jìn)去看源碼的時(shí)候會(huì)發(fā)現(xiàn)許多函數(shù)里面有contract {}包裹的代碼塊,那么這些代碼塊到底有什么作用呢??
測(cè)試
接下來(lái)用以下兩個(gè)我們常用的拓展函數(shù)作為例子
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我相信大家在開(kāi)發(fā)中是經(jīng)常見(jiàn)到的。
不知道那些代碼有什么作用,那么我們就把那幾行代碼去掉,然后看看函數(shù)使用起來(lái)有什么區(qū)別。
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{}代碼塊后的兩個(gè)函數(shù) 調(diào)用看看
fun test() {
var str1: String = ""
var str2: String = ""
runWithoutContract {
str1 = "jayce"
}
run {
str2 = "jayce"
}
println(str1) //jayce
println(str2) //jayce
}經(jīng)過(guò)測(cè)試發(fā)現(xiàn),看起來(lái)好像沒(méi)什么問(wèn)題,run代碼塊都能都正常執(zhí)行,做了賦值的操作。
那么如果是這樣呢
將str的初始值去掉,在run代碼塊里面進(jìn)行初始化操作
@Test
fun test() {
var str1: String
var str2: String
runWithoutContract {
str1 = "jayce"
}
run {
str2 = "jayce"
}
println(str1) //編譯不通過(guò) (Variable 'str1' must be initialized)
println(str2) //編譯通過(guò)
}??????
我們不是在runWithoutContract做了初始化賦值的操作了嗎?怎么IDE還報(bào)錯(cuò),難道是IDE出了什么問(wèn)題?好 有問(wèn)題就重啟,我去,重啟還沒(méi)解決。。。。好重裝。不不不?。e急 會(huì)不會(huì)Contract代碼塊就是干這個(gè)用的?是不是它悄悄的跟IDE說(shuō)了什么話 以至于它能正常編譯通過(guò)?
好 這個(gè)問(wèn)題先放一放 我們?cè)倏纯礇](méi)contract版本的isNullOrEmpty對(duì)比有contract的有什么區(qū)別
fun test() {
val str: String? = "jayce"
if (!str.isNullOrEmpty()) {
println(str) //jayce
}
if (!str.isNullOrEmptyWithoutContract()) {
println(str) //jayce
}
}發(fā)現(xiàn)好像還是沒(méi)什么問(wèn)題。相信大家根據(jù)上面遇到的問(wèn)題可以猜測(cè),這其中肯定也有坑。
比如這種情況
fun test() {
val str: String? = "jayce"
if (!str.isNullOrEmpty()) {
println(str.length) // 編譯通過(guò)
}
if (!str.isNullOrEmptyWithoutContract()) {
println(str.length) // 編譯不通過(guò)(Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?)
}
}根據(jù)錯(cuò)誤提示可以看出,在
isNullOrEmptyWithoutContract判斷為flase之后的代碼塊,str這個(gè)字段還是被IDE認(rèn)為是一個(gè)可空類型,必須要進(jìn)行空檢查才能通過(guò)。然而在isNullOrEmpty返回flase之后的代碼塊,IDE認(rèn)為str其實(shí)已經(jīng)是非空了,所以使用前就不需要進(jìn)行空檢查。
查看 contract 函數(shù)
public inline fun contract(builder: ContractBuilder.() -> Unit) { }點(diǎn)進(jìn)去源碼,我們可以看到contract是一個(gè)內(nèi)聯(lián)函數(shù),接收一個(gè)函數(shù)類型的參數(shù),該函數(shù)是ContractBuilder的一個(gè)拓展函數(shù)(也就是說(shuō)在這個(gè)函數(shù)體里面擁有ContractBuilder的上下文)
看看ContractBuilder給我們提供了哪些函數(shù)(主要就是依靠這些函數(shù)來(lái)約定我們自己寫(xiě)的lambda函數(shù))
public interface ContractBuilder {
//描述函數(shù)正常返回,沒(méi)有拋出任何異常的情況。
@ContractsDsl public fun returns(): Returns
//描述函數(shù)以value返回的情況,value可以取值為 true|false|null。
@ContractsDsl public fun returns(value: Any?): Returns
//描述函數(shù)以非null值返回的情況。
@ContractsDsl public fun returnsNotNull(): ReturnsNotNull
//描述lambda會(huì)在該函數(shù)調(diào)用的次數(shù),次數(shù)用kind指定
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}
復(fù)制代碼returns
其中 returns() returns(value) returnsNotNull() 都會(huì)返回一個(gè)繼承于SimpleEffect的Returns 接下來(lái)看看SimpleEffect
public interface SimpleEffect : Effect {
//接收一個(gè)Boolean值的表達(dá)式 改函數(shù)用來(lái)表示當(dāng)SimpleEffect成立之后 保證Boolean值的表達(dá)式返回值為true
//表達(dá)式可以傳判空代碼塊(`== null`, `!= null`)判斷實(shí)例語(yǔ)句 (`is`, `!is`)。
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}
可以看到SimpleEffect里面有一個(gè)中綴函數(shù)implies 。可以使用ContractBuilder的函數(shù)指定某種返回的情況 然后用implies來(lái)聲明傳入的表達(dá)式為true。
看到這里 那么我們應(yīng)該就知道 isNullOrEmpty() 加的contract是什么意思了
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
//返回值為false的情況 returns(false)
//意味著 implies
//調(diào)用該函數(shù)的對(duì)象不為空 (this@isNullOrEmpty != null)
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.length == 0
}因?yàn)閕sNullOrEmpty里面加了contract代碼塊,告訴IDE說(shuō):返回值為false的情況意味著調(diào)用該函數(shù)的對(duì)象不為空。所以我們就可以直接在判斷語(yǔ)句后直接使用非空的對(duì)象了。
有些同學(xué)可能還是不理解,這里再舉一個(gè)沒(méi)什么用的例子(運(yùn)行肯定會(huì)crash哈。。。)
@ExperimentalContracts //因?yàn)樵撎匦赃€在試驗(yàn)當(dāng)中 所以需要加上這個(gè)注解
fun CharSequence?.isNotNull(): Boolean {
contract {
//返回值為true returns(true)
//意味著implies
//調(diào)用該函數(shù)的對(duì)象是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可是沒(méi)有這個(gè)函數(shù)的,因?yàn)槲覀冇胏ontract讓他強(qiáng)制轉(zhuǎn)換成StringBuilder了 所以才有了這個(gè)函數(shù)
}
}是的 這樣IDE居然沒(méi)有報(bào)錯(cuò),因?yàn)榻?jīng)過(guò)我們contract的聲明,只要這個(gè)函數(shù)返回true,調(diào)用函數(shù)的對(duì)象就是一個(gè)StringBuilder。
callsInPlace
//描述lambda會(huì)在該函數(shù)調(diào)用的次數(shù),次數(shù)用kind指定 @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
可以知道callsInPlace是用來(lái)指定lambda函數(shù)調(diào)用次數(shù)的
kind有四種取值
- InvocationKind.AT_MOST_ONCE:最多調(diào)用一次
- InvocationKind.AT_LEAST_ONCE:最少調(diào)用一次
- InvocationKind.EXACTLY_ONCE:調(diào)用一次
- InvocationKind.UNKNOWN:未知,不指定的默認(rèn)值
我們?cè)倏椿厝ブ皉un函數(shù)里面的contract聲明了什么
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
//block這個(gè)函數(shù),剛好調(diào)用一次
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
復(fù)制代碼看到這里 應(yīng)該就知道為什么我們自己寫(xiě)的runWithoutContract會(huì)報(bào)錯(cuò)(Variable 'str1' must be initialized),而系統(tǒng)的run卻不會(huì)報(bào)錯(cuò)了,因?yàn)閞un聲明了lambda會(huì)調(diào)用一次,所以就一定會(huì)對(duì)str2做初始化操作,然而runWithoutContract卻沒(méi)有聲明,所以IDE就會(huì)報(bào)錯(cuò)(因?yàn)橛锌赡懿粫?huì)調(diào)用,所以就不會(huì)做初始化操作了)。
總結(jié)
- Kotlin提供了一些自動(dòng)轉(zhuǎn)換的功能,例如平時(shí)判空和判斷是否為某個(gè)實(shí)例的時(shí)候,Kotlin都會(huì)為我們自動(dòng)轉(zhuǎn)換。但是如果這個(gè)判斷被提取到其他函數(shù)的時(shí)候,這個(gè)轉(zhuǎn)換會(huì)失效。所以提供了contract給我們?cè)诤瘮?shù)體添加聲明,編譯器會(huì)遵守我們的約定。
- 當(dāng)使用一個(gè)高階函數(shù)的時(shí)候,可以使用callsInPlace指定該函數(shù)會(huì)被調(diào)用的次。例如在函數(shù)體里面做初始化,如果申明為EXACTLY_ONCE的時(shí)候,IDE就不會(huì)報(bào)錯(cuò),因?yàn)榫幾g器會(huì)遵守我們的約定。
到此這篇關(guān)于Kotlin中contract的文章就介紹到這了,更多相關(guān)Kotlin中contract用處內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 使用 Path 實(shí)現(xiàn)搜索動(dòng)態(tài)加載動(dòng)畫(huà)效果
這篇文章主要介紹了Android 使用 Path 實(shí)現(xiàn)搜索動(dòng)態(tài)加載動(dòng)畫(huà)效果,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-08-08
淺談Android單元測(cè)試的作用以及簡(jiǎn)單示例
本篇文章主要介紹了淺談Android單元測(cè)試的作用以及簡(jiǎn)單示例,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
Android ScrollView滑動(dòng)實(shí)現(xiàn)仿QQ空間標(biāo)題欄漸變
這篇文章主要為大家詳細(xì)介紹了Android ScrollView滑動(dòng)實(shí)現(xiàn)仿QQ空間標(biāo)題欄漸變,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08
Android RecyclerView網(wǎng)格布局示例解析
這篇文章主要介紹了Android RecyclerView網(wǎng)格布局示例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12
Android在不使用數(shù)據(jù)庫(kù)的情況下存儲(chǔ)數(shù)據(jù)的方法
這篇文章主要介紹了Android在不使用數(shù)據(jù)庫(kù)的情況下存儲(chǔ)數(shù)據(jù)的方法,涉及Android存儲(chǔ)數(shù)據(jù)的相關(guān)技巧,需要的朋友可以參考下2015-04-04
Android-ViewModel和LiveData使用詳解
這篇文章主要介紹了Android-ViewModel和LiveData使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03

