Object, are you null? Or how to make a contract with the compiler in Kotlin

So, further in the article:

  • Why the hell can you call methods on null objects?

  • How to negotiate with the compiler?

  • How are extension functions different from native class methods?

Let's get started. I'll say right away that I'm not a mega-advanced Kotlin developer. Just yesterday I wrote an admiring article about my discoveries from the language, and today I'm already getting under the hood.

When I started writing in Kotlin (after Java, of course), one of the things that caught my attention was the function isNullOrEmpty() which can be very cleverly called on various objects of the standard Kotlin library. Damn, I thought, really, how come I didn't think of that before! Who needs it? obj == null when there is obj.isNull().

I sat down and wrote the following:

public class MyClass{
  public boolean isNull(){
    return this == null;
  }
}

I understand that the listing is crazy, the type of the returned argument is not only specified explicitly, but this rogue also stands before (!) the name of the function, so, boilermakers, take this:

 class MyClass(){
   fun isNull() = this == null 
 }

Well, I think that's it, I've improved readability. I launch it:

fun main() {
  val myClass:MyClass? = null
  println(myClass!!.isNull())
}

And I get:

Exception in thread "main" java.lang.NullPointerException

the poor guy called a method on a null object and got an NPE

the poor guy called a method on a null object and got an NPE

In fact, doubts crept in even when calling the method isNull(): the compiler required to install !! before calling the method:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type MyClass?

The solution turned out to be simple and elegant, and perhaps it has already occurred to you, but it is waiting for you at the end of the article along with a bonus focus. In the meantime, let's climb into the library class Strings.kt:

We find this miracle there:

@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
  contract {
    returns(false) implies (this@isNullOrEmpty != null)
  }
  return this == null || this.length == 0
}

There's a lot of interesting stuff here, so let's go through it in order:

  1. Annotation @kotlin.internal.InlineOnly in combination with the keyword inline in the signature, does the following:

    The code that calls our function and the code of this inline function itself are combined and substituted directly into the call site. An article on this subject.

    Moreover, the java method corresponding to this inline function becomes private, so there will be no other way to use it except for direct embedding at the call sites. Learned from here.

    Attention question: why is this here?

    Inline methods should actually accept lambdas. Without them, this keyword simply doesn't make sense.

    Or does it matter? I found this question on stackoverflowand smart people explained that this was done in order to:

    1.1) Reduce the number of methods in artifacts (this is important for Android) – I emphasize again, this reduction will not occur at the expense of inlinebut due to the combination inline+ @InlineOnly

    1.2) Maintain reified type parameters. I advise you to follow the link and read, but in short, the key word reified only available from inline methods and allows you to work with generics like regular classes (they are not erased during compilation)

    In our case, we'll just take it for granted. Well, if the developers wanted to reduce the number of methods after compilation, that's their right.

  2. Further in the signature we see CharSequence?.isNullOrEmpty() – Ahaaa, it's an extension function! Gotcha.

    So, what's so special about the extension function? It's essentially a regular decorator. Calls to this function don't go to the decorated class, which is why we can do the trick with this == null. Essentially, we don't check whether the real this is null. We check whether the decorated object is null.

  3. And then the contract:

    contract {
      returns(false) implies (this@isNullOrEmpty != null)
    }

    Now it will be interesting.
    Let me first try to simply translate this line into Russian:

    So, compiler! If the function returns false, then keep in mind that this object is not null and not empty.

    Now reference:

    Contracts with the compiler are in experimental API.

    Any places where contracts are used must be marked with an annotation. @kotlin.contracts.ExperimentalContracts or @OptIn(kotlin.contracts.ExperimentalContracts::class)

    Any use of parts of the program annotated @ExperimentalContractsmust be agreed upon either by annotating this use with an annotation OptInFor example @OptIn(ExperimentalContracts::class)or using a compiler argument -opt-in=kotlin.contracts.ExperimentalContracts

    Hmm, some kind of experimental feature. Let's figure out what it does using a small example (the examples are made similar to from herealmost the only normal material on contracts in Kotlin):

    So, let's make a pet class:

    class Pet(val name: String = "default pet name")

    Now let's make a separate function that will accept a NUMBER. And then print the PET NAME from this argument to the console. How about that, not bad?

    fun printPetName(arg:Int?){
      println(arg.name)
    }

    The compiler won't appreciate such a joke and will say:

    Unresolved reference: name (and he will do the right thing)

    And I want to! I want to deceive the compiler. My soul asks to make a pet out of the number.

    Kotlin reluctantly allows me to do this, I just have to write:

    private fun runContract(arg: Any?){
      contract {
        returns() implies (arg is Pet)
      }
    }

    Let me explain: in this method I put a contract declaration that says that after this function completes ( return() – will return nothing), the compiler will begin to believe that the argument passed to this method is an object of the class Pet.

    Add a method with a contract to the pet name print method:

    fun printPetName(arg:Int?){
      runContract(arg)
      println(arg.name)
    }

    As was said earlier, literally everything that can be done needs to be covered with screaming annotations, like I'm using the experimental Kotlin API!!

    So the final code looks like this:

    import kotlin.contracts.ExperimentalContracts
    import kotlin.contracts.contract
    
    class Pet(val name: String = "default pet name")
    
    @OptIn(ExperimentalContracts::class)
    fun main() {
      printPetName(10)
    }
    
    @ExperimentalContracts 
    fun printPetName(arg:Int?){
      runContract(arg)
      println(arg.name)
    }  
    
    @ExperimentalContracts 
    fun runContract(arg: Any?){
      contract {
        returns() implies (arg is Pet)
      }
    }
    )

    )

    And now, with a clear conscience, we can fall with runtime:

    Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class Pet

    It's crazy, but fun.

But something has carried us away. Let's return to the original question: how can we organize the method isNull() for a self-written class?

We've established that the original class methods can't be called from a null object (and never could). But we can decorate our class with an extension function. Let's do that:

class MyClass(){}
fun MyClass?.isNull() = this == null

Everything turned out to be very simple, as you can see.

And now the promised bonus trick.

I literally just recently tried it out: I was seriously telling a fellow Kotlin developer about a new Kotlin feature in the area of ​​NPE processing, which looks like this:

No more NPEs!

No more NPEs!

To pull off this trick, we, of course, create an extension function in a separate file and call it:

fun MyClass?.printAuthоrUsername(){
  if(this!=null) println("youngmyn")
  else println("Method calls are not allowed on a nullable receiver of type MyClass?")
}

And so that the extension function is called from the original code, and not the original one (in Kotlin, by the way, this is hard recorded“if a class has a native function and an extension function is defined that applies to the same class, has the same name, and is applicable to the given arguments, then native always wins”) – I replaced the Latin letter 'o' with its Cyrillic brother and left only syntax highlighting in the IDE.

fun main() {
  val myClass : MyClass? = null
  myClass.printAuthоrUsername()
}

class MyClass(){
  fun printAuthorUsername() = println("youngmyn")
}

It's a childish trick, but I almost managed to convince an adult of its truthfulness. bearded Geek Kotlinist.

I'll take my leave here. Respect to everyone who has honestly read to this point. I hope it turned out readable.

Sources:

Inline functions docs

Inline under the hood [хабр]

Discussion about @InlineOnly

Why use inline without lambdas?

Reified type parameters docs

Briefly about extension functions in the social security format

ContractBuilder Sources

The principle of operation and limitations of contracts in Kotlin (with examples)

extensions docs

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *