We are sure to use some of the general extension functions provided by Kotlin frequently in development. When we look at the source code, we will find that many functions have code blocks wrapped in contract {}, so what are these code blocks?
test
The following two commonly used extension functions are used as examples
public inline fun <T, R> T.run(block: T. () - >R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
public inline funCharSequence? .isNullOrEmpty(a): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty! =null)}return this= =null || this.length == 0
}
Copy the code
Run and isNullOrEmpty I’m sure you see them all the time in development.
We don’t know what that code does, so let’s get rid of those lines and see how the function works.
public inline fun <T, R> T.runWithoutContract(block: T. () - >R): R {
return block()
}
public inline funCharSequence? .isNullOrEmptyWithoutContract(a): Boolean {
return this= =null || this.length == 0
}
Copy the code
Here are the two function calls after removing the Contract {} code block
fun test(a) {
var str1: String = ""
var str2: String = ""
runWithoutContract {
str1 = "jayce"
}
run {
str2 = "jayce"
}
println(str1) //jayce
println(str2) //jayce
}
Copy the code
After testing, it looks like there’s nothing wrong with the run block, and the assignment is done.
So what if this is the case
Remove the initial value of STR and initialize it in the run block
@Test
fun test(a) {
var str1: String
var str2: String
runWithoutContract {
str1 = "jayce"
}
run {
str2 = "jayce"
}
println(str1) // Compile failed (Variable 'str1' must be initialized)
println(str2) // The compiler passes
}
Copy the code
??????
Didn’t we do an initial assignment in the runWithoutContract? How does the IDE still report errors? Is there something wrong with the IDE? If there’s a problem, restart it. Damn it. Easy to reload. No no no!! Don’t worry, is that what Contract blocks are for? Does it whisper something to the IDE so that it will compile properly?
Okay, so let’s put that aside and see what’s the difference between isNullOrEmpty without a contract and isNullOrEmpty with a contract
fun test(a) {
val str: String? = "jayce"
if(! str.isNullOrEmpty()) { println(str)//jayce
}
if(! str.isNullOrEmptyWithoutContract()) { println(str)//jayce}}Copy the code
It seems there’s nothing wrong with it. I believe you can guess according to the problems encountered above, which must also have pits.
Like this case
fun test(a) {
val str: String? = "jayce"
if(! str.isNullOrEmpty()) { println(str.length)// The compiler passes
}
if(! str.isNullOrEmptyWithoutContract()) { println(str.length)// Failed to compile (Only safe (? .). or non-null asserted (!! .). Calls are allowed on a nullable receiver of type String?}}Copy the code
According to the error message, you can see that block of code after isNullOrEmptyWithoutContract judgment for the flase, STR this field is still considered by the IDE is a nullable type, check to pass has to be empty. However, when isNullOrEmpty returns a block of code after flase, the IDE assumes that STR is already non-null, so no null-checking is required before use.
Check the contract function
public inline fun contract(builder: ContractBuilder. () - >Unit){}Copy the code
Contract is an inline function that takes arguments of a function type, which is an extension of ContractBuilder.
Take a look at the functions ContractBuilder provides (which we rely on to contract our own lambda functions)
public interface ContractBuilder {
// Describes the case where the function returns normally without throwing any exceptions.
@ContractsDsl public fun returns(a): Returns
/ / description function to the value returned by the situation, the value can be value to true | | false null.
@ContractsDsl public fun returns(value: Any?).: Returns
// describes the case when a function returns a non-null value.
@ContractsDsl public fun returnsNotNull(a): ReturnsNotNull
// Describes the number of times lambda will be called in the function, using kind
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}
Copy the code
returns
Returns () returns(value) returnsNotNull() returns an inherited return from SimpleEffect
public interface SimpleEffect : Effect {
// The function that accepts a Boolean expression is used to ensure that the Boolean expression returns true after SimpleEffect is set
// Expressions can pass empty code blocks (' == null ', '! = null ') to determine the instance statement (' is', '! Is `).
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}
Copy the code
We can see that the infix function implies in SimpleEffect. You can use the ContractBuilder function to specify some kind of return case and then imply to declare that the passed expression is true.
“Contract” isNullOrEmpty(), okay
public inline funCharSequence? .isNullOrEmpty(a): Boolean {
contract {
// Returns (false)
// implies
// The object on which this function is called is not empty (this@isNullOrEmpty! = null)
returns(false) implies (this@isNullOrEmpty! =null)}return this= =null || this.length == 0
}
Copy the code
Because isNullOrEmpty has a contract block in it, it tells the IDE that a case where the return value is false means that the object calling this function is not null. So we can use non-empty objects directly after the statement.
For those of you who still don’t understand, here’s another example that doesn’t work.
@ExperimentalContracts // This annotation is needed because the features are still being tested
funCharSequence? .isNotNull(a): Boolean {
contract {
// Returns true.
// implies
// Call StringBuilder (this@isNotNull is StringBuilder)
returns(true) implies (this@isNotNull is StringBuilder)
}
return this! =null
}
fun test(a) {
val str: String? = "jayce"
if (str.isNotNull()) {
str.append("")//String does not have this function, because we use contract to cast it to StringBuilder}}Copy the code
The IDE does not return an error, because our contract declares that as long as this function returns true, the object calling the function is a StringBuilder.
callsInPlace
// Describes the number of times lambda will be called in the function, using kind
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
Copy the code
You can see that callsInPlace is used to specify the number of lambda function calls
There are four values of kind
- Invocationkind. AT_MOST_ONCE: can be invoked at most once
- Invocationkind. AT_LEAST_ONCE: invocationKind. AT_LEAST_ONCE: at least once
- Invocationkind. EXACTLY_ONCE: Call once
- Invocationkind. UNKNOWN: UNKNOWN, default value not specified
So let’s go back to what was the contract declaration in the run function
public inline fun <T, R> T.run(block: T. () - >R): R {
contract {
//block is called just once
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
Copy the code
RunWithoutContract: initialized (Variable ‘str1’ must be initialized); run: initialized (Variable ‘str1’ must be initialized); So str2 must be initialized, but runWithoutContract is not declared, so the IDE will report an error (it may not be called, so the initialization will not be done).
conclusion
- Kotlin provides some automatic conversion functions, such as short call and the detection of an instance, Kotlin will automatically convert for us. However, if this judgment is extracted from other functions, the conversion will fail. So it provides
contract
Add a declaration to our function body, and the compiler will follow our convention. - Can be used when using a higher-order function
callsInPlace
Specifies how many times this function will be called. For example, to initialize a function in the body, if declared asEXACTLY_ONCE
The IDE will not report an error because the compiler will follow our convention.