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

  1. 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 providescontractAdd a declaration to our function body, and the compiler will follow our convention.
  2. Can be used when using a higher-order functioncallsInPlaceSpecifies how many times this function will be called. For example, to initialize a function in the body, if declared asEXACTLY_ONCEThe IDE will not report an error because the compiler will follow our convention.