Last Updated:

Inline classes in Kotlin

Inline classes in Kotlin

Zero-cost* abstractions in Kotlin is an article with a detailed explanation of Kotlin's new experimental language construct called inline classes.

 

One of the key features of the Kotlin language is null safety, which ensures that the programmer cannot mistakenly call the methods of an object that has a null value, or pass this object as an argument to other methods. Null safety significantly increases the reliability of the code, but does not protect against other programmer errors.

Let's say you have a database of cats and dogs that you identify by ID. Also you have a method getDogById(dogId: Long)that returns information about the dog with a specific ID. Obviously, if you pass the cat ID to the method as a dog ID, it will be an error that will lead to an uncertain result. But neither the development environment nor the compiler will tell you about it.

Back in the days of Java, programmers came up with a method to work around this problem using so-called wrapper classes. You just create a class. DogId with a single field (Dog ID) and use it wherever you used to use the Long type as an ID. The compiler and development environment will do the rest for you: they just won't let you pass DogId as an argument to the function that expects CatId, is a bug.

But there is one problem with the wrapper classes. Creating objects is not the cheapest operation. If you breed them for every sneeze, you will soon notice an increased consumption of RAM and cpu resources.

And this is where inline classes come into play. At its core, an inline class is a single-parameter wrapper class that, when compiled, expands into that parameter to avoid overhead. For example:

This code is written using a wrapper to avoid the error described above. However, when you compile the object DogId will be replaced by Long, so no additional overhead costs will be required.

The compiler imposes the following restrictions on inline classes:

 
  • No more than one parameter
  • No shadow fields.
  • no initialization blocks;
  • no inheritance.

However, inline classes can:

  • implement the interface;
  • have properties and functions.

It is also worth bearing in mind that inline classes will not always be deployed to their parameter. The main rule here is that an inline class object will not be expanded if it is used as an argument to a function that expects a different type.

For example, functions for working with collections (listOf(),setOf(), and the like) typically take as input a parameter of type Object or Any, so that the object of the inline class passed to them will not be expanded. The equals() function also takes the Any type as an argument, so the following two examples work the same way, but the second one will result in additional overhead:

val doggo1 = DogId(1L)

val doggo2 = DogId(2L)

// Both objects will be expanded

doggo1 == doggo2


// doggo1 will expand, but doggo2 won't

doggo1.equals(doggo2)

The object will not be expanded and if the object of the inline class passes to a function whose argument is of nullable type:

val doggo = DogId(1L)

fun pet(doggoId: DogId?) {}

// The object will not be expanded
pet(doggo)

It is also interesting that the compiler supports overriding functions that accept an inline class object and its unshelled counterpart. That is, the following code will be compiled successfully: