Contracts: Condition on "this" in extension function


#1

I’m trying to create an extension function like this:

val MyClass?.myAttribute: String?
    get() {
        if(this == null){
           return null
        }else{
           return this.performSomeOperation()
        }
    }

The problem with this is the following:

  • I want this method to be safe when called on a null object (MyClass? is intentional)
  • The method performSomeOperation() actually never returns null.
  • Hence, the extension value myAttribute will only return null if this is null

In other words:

val str1: String = myNonNullObject.myAttribute 
val str2: String? = null.myAttribute

Alas, the Kotlin compiler can’t know this semantic and will therefore complain that str1 should actually be of type String?.

Can I enhance the get function with a contract that says something like:

contract {
    receiverNotNull() implies (result != null)
}

The code above is just a dummy to outline what I’m looking for, I never used contracts so far.


#2

You don’t need contracts for that. Just overload the function/property like this:

val MyClass.myAttribute: String get() = this.performSomeOperation()

val MyClass?.myAttribute: String? get() = when(this) {
    null -> null
    else -> myAttribute // the other property is taken, thanks to smart-cast
}

The compiler will choose the most specific property, and know its type’s nullability. That is, your example will be free of compiler errors or warnings.


#3

Right, that works too. Though I don’t see why this works: isn’t the JVM “erasure type” of both MyClass?.myAttribute and MyClass.myAttribute exactly the same? Usually the kotlin compiler complains when this happens.


#4

There are no generics involved, so there is also no Erasure. It works for the same reason why this works:

fun foo(n: String) = n.toInt()
fun foo(n: Int) = n

Basic overloading of functions. Note that overloaded functions are statically bound at compile-time. So even if there were erasue, it would not matter, since erasure happens at runtime.

Update:

I see what you mean now. The JVM does not distinguish the types String? and String. So we will end-up with the same function signature twice. I don’t know how it works, but I am sure the answer is simple and can be found by decompiling the bytecode.

I know that, unlike Java, the JVM actually allows overloading methods that are different only in return type. But in our case even the return type is the same at runtime, so it must be something else.


#5

You can use annotation to prevent signature clash:

val MyClass.attribute: String get() = performSomeOperation()
val MyClass?.attribute: String? @JvmName("getAttribute0") get() = this?.performSomeOperation()

#6

Oh now that’s nice! Neat trick :slight_smile: