Item 47 Consider Using Inline Classes

Item 47: Consider using inline classes

Not only functions can be inlined, but also objects holding a single value can be replaced with this value. Such possibility was introduced as experimental in Kotlin 1.3, and to make it possible, we need to place the inline modifier before a class with a single primary constructor property:

  1. inline class Name(private val value: String) {
  2. // ...
  3. }

Such a class will be replaced with the value it holds whenever possible:

  1. // Code
  2. val name: Name = Name("Marcin")
  3. // During compilation replaced with code similar to:
  4. val name: String = "Marcin"

Methods from such a class will be evaluated as static methods:

  1. inline class Name(private val value: String) {
  2. // ...
  3. fun greet() {
  4. print("Hello, I am $value")
  5. }
  6. }
  7. // Code
  8. val name: Name = Name("Marcin")
  9. name.greet()
  10. // During compilation replaced with code similar to:
  11. val name: String = "Marcin"
  12. Name.`greet-impl`(name)

We can use inline classes to make a wrapper around some type (like String in the above example) with no performance overhead (Item 45: Avoid unnecessary object creation). Two especially popular uses of inline classes are:

  • To indicate a unit of measure
  • To use types to protect user from misuse

Let’s discuss them separately.

Indicate unit of measure

Imagine that you need to use a method to set up timer:

  1. interface Timer {
  2. fun callAfter(time: Int, callback: ()->Unit)
  3. }

What is this time? Might be time in milliseconds, seconds, minutes… it is not clear at this point and it is easy to make a mistake. A serious mistake. One famous example of such a mistake is Mars Climate Orbiter that crashed into Mars atmosphere. The reason behind that was that the software used to control it was developed by an external company and it produced outputs in different units of measure than the ones expected by NASA. It produced results in pound-force seconds (lbf·s), while NASA expected newton-seconds (N·s). The total cost of the mission was 327.6 million USD and it was a complete failure. As you can see, confusion of measurement units can be really expensive.

One common way for developers to suggest a unit of measure is by including it in the parameter name:

  1. interface Timer {
  2. fun callAfter(timeMillis: Int, callback: ()->Unit)
  3. }

It is better but still leaves some space for mistakes. The property name is often not visible when a function is used. Another problem is that indicating the type this way is harder when the type is returned. In the example below, time is returned from decideAboutTime and its unit of measure is not indicated at all. It might return time in minutes and time setting would not work correctly then.

  1. interface User {
  2. fun decideAboutTime(): Int
  3. fun wakeUp()
  4. }
  5. interface Timer {
  6. fun callAfter(timeMillis: Int, callback: ()->Unit)
  7. }
  8. fun setUpUserWakeUpUser(user: User, timer: Timer) {
  9. val time: Int = user.decideAboutTime()
  10. timer.callAfter(time) {
  11. user.wakeUp()
  12. }
  13. }

We might introduce the unit of measure of the returned value in the function name, for instance by naming it decideAboutTimeMillis, but such a solution is rather rare as it makes a function longer every time we use it, and it states this low-level information even when we don’t need to know about it.

A better way to solve this problem is to introduce stricter types that will protect us from misusing more generic types, and to make them efficient we can use inline classes:

  1. inline class Minutes(val minutes: Int) {
  2. fun toMillis(): Millis = Millis(minutes * 60 * 1000)
  3. // ...
  4. }
  5. inline class Millis(val milliseconds: Int) {
  6. // ...
  7. }
  8. interface User {
  9. fun decideAboutTime(): Minutes
  10. fun wakeUp()
  11. }
  12. interface Timer {
  13. fun callAfter(timeMillis: Millis, callback: ()->Unit)
  14. }
  15. fun setUpUserWakeUpUser(user: User, timer: Timer) {
  16. val time: Minutes = user.decideAboutTime()
  17. timer.callAfter(time) { // ERROR: Type mismatch
  18. user.wakeUp()
  19. }
  20. }

This would force us to use the correct type:

  1. fun setUpUserWakeUpUser(user: User, timer: Timer) {
  2. val time = user.decideAboutTime()
  3. timer.callAfter(time.toMillis()) {
  4. user.wakeUp()
  5. }
  6. }

It is especially useful for metric units, as in frontend, we often use a variety of units like pixels, millimeters, dp, etc. To support object creation, we can define DSL-like extension properties (and you can make them inline functions):

  1. inline val Int.min
  2. get() = Minutes(this)
  3. inline val Int.ms
  4. get() = Millis(this)
  5. val timeMin: Minutes = 10.min

Protect us from type misuse

In SQL databases, we often identify elements by their IDs, which are all just numbers. Therefore, let’s say that you have a student grade in a system. It will probably need to reference the id of a student, teacher, school etc.:

  1. @Entity(tableName = "grades")
  2. class Grades(
  3. @ColumnInfo(name = "studentId")
  4. val studentId: Int,
  5. @ColumnInfo(name = "teacherId")
  6. val teacherId: Int,
  7. @ColumnInfo(name = "schoolId")
  8. val schoolId: Int,
  9. // ...
  10. )

The problem is that it is really easy to later misuse all those ids, and the typing system does not protect us because they are all of type Int. The solution is to wrap all those integers into separate inline classes:

  1. inline class StudentId(val studentId: Int)
  2. inline class TeacherId(val teacherId: Int)
  3. inline class SchoolId(val studentId: Int)
  4. @Entity(tableName = "grades")
  5. class Grades(
  6. @ColumnInfo(name = "studentId")
  7. val studentId: StudentId,
  8. @ColumnInfo(name = "teacherId")
  9. val teacherId: TeacherId,
  10. @ColumnInfo(name = "schoolId")
  11. val schoolId: SchoolId,
  12. // ...
  13. )

Now those id uses will be safe, and at the same time, the database will be generated correctly because during compilation, all those types will be replaced with Int anyway. This way, inline classes allow us to introduce types where they were not allowed before, and thanks to that, we have safer code with no performance overhead.

Inline classes and interfaces

Inline classes just like other classes can implement interfaces. Those interfaces could let us properly pass time in any unit of measure we want.

  1. interface TimeUnit {
  2. val millis: Long
  3. }
  4. inline class Minutes(val minutes: Long): TimeUnit {
  5. override val millis: Long get() = minutes * 60 * 1000
  6. // ...
  7. }
  8. inline class Millis(val milliseconds: Long): TimeUnit {
  9. override val millis: Long get() = milliseconds
  10. }
  11. fun setUpTimer(time: TimeUnit) {
  12. val millis = time.millis
  13. //...
  14. }
  15. setUpTimer(Minutes(123))
  16. setUpTimer(Millis(456789))

The catch is that when an object is used through an interface, it cannot be inlined. Therefore in the above example there is no advantage to using inline classes, since wrapped objects need to be created to let us present a type through this interface. When we present inline classes through an interface, such classes are not inlined.

Typealias

Kotlin typealias lets us create another name for a type:

  1. typealias NewName = Int
  2. val n: NewName = 10

Naming types is a useful capability used especially when we deal with long and repeatable types. For instance, it is popular practice to name repeatable function types:

  1. typealias ClickListener =
  2. (view: View, event: Event) -> Unit
  3. class View {
  4. fun addClickListener(listener: ClickListener) {}
  5. fun removeClickListener(listener: ClickListener) {}
  6. //...
  7. }

What needs to be understood though is that typealiases do not protect us in any way from type misuse. They are just adding a new name for a type. If we would name Int as both Millis and Seconds, we would make an illusion that the typing system protects us while it does not:

  1. typealias Seconds = Int
  2. typealias Millis = Int
  3. fun getTime(): Millis = 10
  4. fun setUpTimer(time: Seconds) {}
  5. fun main() {
  6. val seconds: Seconds = 10
  7. val millis: Millis = seconds // No compiler error
  8. setUpTimer(getTime())
  9. }

In the above example it would be easier to find what is wrong without type aliases. This is why they should not be used this way. To indicate a unit of measure, use a parameter name or classes. A name is cheaper, but classes give better safety. When we use inline classes, we take the best from both options - it is both cheap and safe.

Summary

Inline classes let us wrap a type without performance overhead. Thanks to that, we improve safety by making our typing system protect us from value misuse. If you use a type with unclear meaning, especially a type that might have different units of measure, consider wrapping it with inline classes.

results matching ""

No results matching ""