Effective Kotlin Item 49: Consider using inline value classes
Not only functions can be inlined, but also objects holding a single value can be replaced with this value. For that, we need to define a class with a single primary constructor read-only property, modifier
value and annotation
Value classes were introduced in Kotlin 1.5 as an introduction to much bigger feature than just class inlining. Before that (but since Kotlin 1.3) we could use
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 user from value misuse
Let's discuss them separately.
Indicate unit of measure
Imagine that you need to use a method to set up timer:
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:
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.
We might introduce the unit of measure of the returned value in the function name, for instance by naming it
decideAboutTimeMillis. Such a solution is not considered very good, as it makes this function state a low-level information even when we don’t need to know about it. Not to mention, it does not necesserly solve the problem - a developer still needs to control that the units of measure 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:
It is especially useful for metric units. For instance 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 (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 grade in a system. It 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 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 value classes:
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 value 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 value classes and interfaces
Inline value classes can implement interfaces. We could use that in the example presented before, 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 when a type will not be inlined is when it is nullable, and the value class holds primitive as a parameter. In the example below, when
Millis is used as a parameter type, it will be replaced with
Long. Although if
Millis? is used, it cannot be replaced, because
Long cannot be
null. Although if
Millis would hold non-primitive type, like
String, then its type nullability wouldn't influence inlining.
Kotlin typealias lets us create another name for a type:
Naming types is a useful capability 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 typealiases do not protect us in any way from type misuse. They are just adding a new name for a type. If we named
Int as both
Seconds, we would make an illusion that the typing system protects us while it does not:
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 value classes, we take the best from both options - it is both cheap and safe.
Inline value 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 (like
String), especially a type that might have different units of measure, consider wrapping it with inline value classes.