Effective Kotlin Item 49: Consider using inline value classes
This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.
Not only functions can be inlined: objects holding a single value can also be replaced with this value. To do this, we need to define a class with a single read-only primary constructor property, a modifier
value, and a
Value classes were introduced in Kotlin 1.5 due to Java’s plans to introduce value classes. Before that (but since Kotlin 1.3), we could use an
inlinemodifier to achieve a similar result.
Such a class will be replaced with the value it holds whenever possible:
Methods from such a class will be evaluated as static methods:
We can use inline value classes to make a wrapper around some type (like
String in the above example) with no performance overhead (Item 47: Avoid unnecessary object creation). Two especially popular uses of inline value classes are:
To indicate a unit of measure.
To use types to protect users from value misuse.
Let's discuss these separately.
Indicate unit of measure
Imagine that you need to use a method to set up a timer:
What is this
time? It might be a time in milliseconds, seconds, or minutes; it is not clear at this point, so it is easy to make a mistake. A serious mistake. One famous example of such a mistake is the Mars Climate Orbiter, which plowed into the Martian atmosphere. The reason for this was that the software used to control it was developed by an external company, and it produced outputs in different measurement units than those 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 measurement unit is by including it in the parameter name:
This is better, but it still leaves some space for mistakes. For example, the property name is often not visible when a function is used. Another problem is that indicating the type in this way is harder when the type is returned. In the example below, the time is returned from
decideAboutTime but its measurement unit is not indicated at all. It might return the time in minutes, thus we will not set the time correctly.
We might introduce the measurement unit of the returned value in the function name, for instance by naming it
decideAboutTimeMillis; however, this solution is not considered very good as it makes this function provide low-level information even when we don’t need it. Moreover, it does not necessarily solve the problem as a developer still needs to ensure that the measurement units match.
A better way to solve this problem is to introduce stricter types that will protect us from misusing types, and to make them efficient we can use inline value classes:
This would force us to use the correct type:
This is especially useful for metric units. For instance, on the frontend we often use a variety of units like pixels, millimeters, dp, etc. To support object creation, we can define DSL-like extension properties (you can make them inline as well):
Protect us from value misuse
In SQL databases, we often identify elements by their IDs, which are all just numbers. For instance, let’s say that you have a student’s grade in a system, which will probably need to reference the id of a student, teacher, school, etc:
The problem is that it is really easy to later misuse all these ids, and the type system does not protect us because they are all of type
Int. The solution is to wrap all these integers into separate inline value classes:
Now those id uses will be safe and the database will be generated correctly because all these types will be replaced with
Int anyway during compilation. This way, inline value classes allow us to introduce types where they were not allowed before; thanks to this, we have safer code with no performance overhead.
Inline value classes and interfaces
Inline value classes can implement interfaces. We could use this in the example presented above to avoid casting from one class to another.
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 value classes since wrapped objects need to be created to let us present a type through this interface. When we present inline value classes through an interface, such classes are not inlined.
Another situation in which a type will not be inlined is when it is nullable and the value class holds a primitive as a parameter. In the example below, when
Millis is used as a parameter type, it will be replaced with
Long. However, if
Millis? is used, it cannot be replaced because
Long cannot be
null. But if
Millis held a non-primitive type, like
String, then its type nullability wouldn't influence inlining.
Kotlin’s typealias lets us create another name for a type:
Naming types is a useful capability that is used especially when we deal with long and repeatable types. For instance, it is a popular practice to name repeatable function types:
What needs to be understood though is that type aliases do not protect us in any way from type misuse. They just add a new name for a type. If we named
Int as both
Seconds, we would create the illusion that the type system protects us, but it does not:
In the above example, it would be easier to find what is wrong without using 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 value classes, we take the best from both options: they are both cheap and safe.
Inline value classes let us wrap a type without a performance overhead. Therefore, we improve safety by making our type system protect us from value misuse. If you use a type whose meaning is unclear (like
String), especially a type that might have different units of measure, consider wrapping it with inline value classes.