Effective Kotlin Item 52: 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 JvmInline
annotation.
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
inline
modifier 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.
To optimize for memory usage.
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):
Regarding indicating the amount of time, we can also use the Duration
class from the standard library, which is an inline value class, and that offers DSL-like extension properties:
Protect us from value misuse
It is a popular practice in bigger projects to use a wrapper around primitive types to protect us from misusing them. For instance, let's say that you write an application for a university in which each student is identified by a unique ID but is also associated with a class id. Both might be represented as raw strings, but it would be easy to make a mistake and use the class id instead of the student id. To avoid this, we should define wrappers over different kinds of ids, and we make these inline value classes to avoid performance overhead:
Optimize for memory usage
As we learned in Item 47: Avoid unnecessary object creation, using primitive types instead of wrapped types is an optimization. However, operating on primitives can be harder. To have your cake and eat it too, we can use inline value classes to wrap primitives and operate on them as if they were objects:
Inline value classes and interfaces
Inline value classes can implement interfaces. We could use this in the example presented above to avoid transforming from one type 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.
Typealias
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 Millis
and 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.
Summary
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 Int
or String
), especially a type that might have different units of measure, consider wrapping it with inline value classes.