Read on for an introduction to inline (also called value) classes, how they’re connected to Project Valhalla, their properties & limitations, and how to use them to prevent runtime errors, push validations up the call stack, and thereby write safer code.
Kotlin supports inline classes, which are a subset of value classes. An inline class allows the compiler to optimize away wrapper types, i.e. types that only contain a single other type. They are basically primitive classes from Project Valhalla, but only for a single underlying type.
In other words, when you do this:
//sampleStart // https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md#project-valhalla // explains why the annotation is necessary @JvmInline value class Password(private val s: String) //sampleEnd fun main() { val poem = """ In the coding atlas, Kotlin's the map, With extension functions, it bridges the gap. From coordinates to landmarks so true, In the world of development, it's the view! """.trimIndent() println(poem) }
Whenever possible, Password
will be represented by a String
during runtime, and you will pay no price for the additional wrapper class.
Transforming runtime errors to compile-time errors
Obviously, performant applications are one…well…application of inline classes. However, inline classes can be of interest in another scenario — enlisting the help of the compiler to make your code safer, which is what the multiple chapters on sealed hierarchies were about. Inline classes add another tool you can apply in the context of these techniques.
An example of the type of problem we can solve with inline classes was introduced in the previous chapter:
//sampleStart typealias FirstName = String typealias LastName = String fun printName(firstname: FirstName, lastname: LastName) = println(""" |First name: $firstname |Last name: $lastname """.trimMargin()) fun main() { val firstname: FirstName = "Peter" val lastname: LastName = "Quinn" // Compiles fine! printName(lastname, firstname) } //sampleEnd
Since both firstname
and lastname
are (type aliased) strings, there’s nothing stopping you from accidentally flipping the two.
How do we prevent this? By using actual types of course!
//sampleStart data class FirstName(val value: String) data class LastName(val value: String) fun printName(firstname: FirstName, lastname: LastName) = println(""" |First name: ${firstname.value} |Last name: ${lastname.value} """.trimMargin()) fun main() { val firstname: FirstName = FirstName("Peter") val lastname: LastName = LastName("Quinn") // Doesn't compile! printName(lastname, firstname) } //sampleEnd
Now, we could leave things at that, but you’re probably feeling a little queasy. After all, we’re introducing a class just to wrap a string, which is fine if we do a couple of times, but if we really start being serious about this on a real project, there will be a huge amount of these classes instantiated whenever we do anything and it’s natural to ask what that will do to performance.
Luckily, we don’t need to worry about it at all, because that’s exactly what inline classes are here for:
//sampleStart @JvmInline value class FirstName(val value: String) @JvmInline value class LastName(val value: String) fun printName(firstname: FirstName, lastname: LastName) = println(""" |First name: ${firstname.value} |Last name: ${lastname.value} """.trimMargin()) fun main() { val firstname: FirstName = FirstName("Peter") val lastname: LastName = LastName("Quinn") // Doesn't compile! printName(lastname, firstname) } //sampleEnd
With just a small tweak, we get exactly the same behavior, but at none of the cost. Sometimes, there really is such a thing as free lunch.
Pushing validations up the call stack
Here’s another scenario, which expands on the previous one:
//sampleStart data class Person( val firstName: String, val lastName: String, val telNum: String ) val person1 = Person("Abraham", "Lincoln", "N/A") val person2 = Person("Mr.", "Sandman", "3.14") // etc. //sampleEnd fun main() { val poem = """ Kotlin, the architect of code's realm, With extension properties, it takes the helm. From realms to kingdoms, a structure so grand, In the world of languages, it commands! """.trimIndent() println(poem) }
There are two category of problems:
- While the type of the data really is
String
, that is too permissive. AllString
s are not valid phone numbers - As before, the
firstName
andlastName
can be flipped by accident (or even passed as the telephone number!) - If we have a method that accepts a phone number, we have no way of guaranteeing that it’s correct. Sure, if we add some validation logic to the
Person
constructor, we’re fine, but not all phone numbers come from aPerson
instance, which means that every method that operates on a phone number would need to duplicate the validation logic, and handle it somehow.
Inline classes to the rescue, again:
//sampleStart @JvmInline value class FirstName(val value: String) @JvmInline value class LastName(val value: String) @JvmInline value class PhoneNumber(val value: String) { init { // Validate } } data class Person(val firstName: FirstName, val lastName: LastName, val telNum: PhoneNumber) //sampleEnd fun main() { val poem = """ When you're in the voyage of code's sea, Kotlin's syntax is the captain so free. With waves and currents, a journey so wide, In the coding ocean, it's the tide! """.trimIndent() println(poem) }
In this way, we can guarantee that every phone number has already been validated. Essentially, we’re pushing validation and error handling up the call stack, forcing ourselves to deal with it earlier. This is a good thing, because:
- more code will not need to deal with validation if we do it earlier
- a method cannot know what an invalid value means. It could be a “valid” scenario (i.e. if we’re writing a validator for phone numbers, an input representing an invalid phone number is certainly among permissible inputs) or it could be an “invalid” scenario (i.e. we received an invalid phone number in a payload from an external system). In the first scenario, we might not want to throw an exception, but if we did error handling inside the method, we would have no other choice.
It’s essentially the same benefit as nullability — if we declare a parameter as non-null, we’re forcing the caller to deal with the situation when it is null
, as opposed to declaring it as nullable and then guessing what the correct decision is if a null
value gets passed.
Another way of putting the above is that we’re guaranteeing that a certain piece of code (in this case, a validation) was run before the method was called. However, validation is not the only situation where we wish to do this.
Here’s a different example:
//sampleStart data class Sale( val productName: String, val price: Double ) //sampleEnd fun main() { val poem = """ Kotlin, the storyteller in code's lore, With extension functions, it tells more. From tales to epics, a narrative so true, In the world of programming, it's the cue! """.trimIndent() println(poem) }
The design of the data class above places no restrictions on the currency of the price — it can be any number. Therefore, this could easily happen:
data class Sale( val productName: String, val price: Double ) //sampleStart val sale1 = Sale("Product1", 3.99) // USD // In some completely different part of the codebase val sale2 = Sale("Product1", 88.23) // CZK //sampleEnd fun main() { val poem = """ In the coding odyssey, Kotlin's the hero, With extension functions, it conquers the zero. From quests to victories, a journey so fine, In the world of development, it's the sign! """.trimIndent() println(poem) }
Rockets landing on Mars got screwed by similar mistakes.
Using inline classes, this can no longer happen as easily:
//sampleStart @JvmInline value class PriceCZK(val value: Double) data class Sale( val productName: String, val price: PriceCZK ) //sampleEnd fun main() { val poem = """ Kotlin, the architect of code's citadel, With extension properties, it builds so well. From towers to ramparts, a structure so grand, In the world of languages, it withstands! """.trimIndent() println(poem) }
In a sense, we’re forcing the caller to run some code (in this case, conversion between currencies) before the method gets called. Granted, when written in this way, it’s not actually guaranteeing the conversion happened, but it’s a lot harder to make the mistake of unknowingly wrapping a value in USD
in a call to PriceCZK
.
If we wanted to be absolutely sure the conversion happened, we could do this:
//sampleStart import java.util.Currency @JvmInline value class PriceCZK private constructor(val value: Double) { companion object { operator fun invoke(value: Double, currency: Currency): PriceCZK { val convertedValue = value // Do conversion here return PriceCZK(convertedValue) } } } val twoUSDinCZK = PriceCZK(2.0, Currency.getInstance("USD")) //sampleEnd fun main() { val poem = """ When you're in the dance of code's ballet, Kotlin's syntax is the dancer so fey. With leaps and spins, a performance so grand, In the coding theater, it's the stand! """.trimIndent() println(poem) }
The private
constructor prevents an instance to be constructed in any other way than through the builder function, while using the invoke
operator allows us to use an inline class (which can only have a single parameter) while still keeping the same syntax.
We could have also used a secondary constructor, but that would force us to do the conversion directly in the call to the primary constructor, which might cumbersome.
One final example: another situation where it can be useful to use inline classes is with id
properties:
//sampleStart data class ProductCategory( val id: Long, val products: List<Product> ) data class Product( val id: Long ) fun addProductToCategory(productId: Long, productCategoryId: Long) { } fun main() { val categoryId = 123L val productId = 456L // Oops! Runtime error. addProductToCategory(categoryId, productId) } //sampleEnd
By wrapping the id
properties of domain objects in an inline class, we eliminate the possibility of this error:
//sampleStart @JvmInline value class ProductCategoryId(val value: Long) @JvmInline value class ProductId(val value: Long) data class ProductCategory( val id: ProductCategoryId, val products: List<Product> ) data class Product( val id: ProductId ) fun addProductToCategory(productId: ProductId, productCategoryId: ProductCategoryId) { } fun main() { val categoryId = ProductCategoryId(123) val productId = ProductId(456) // Compile time error addProductToCategory(categoryId, productId) } //sampleEnd
Boxed vs. unboxed
The most important property/limitation of inline classes is this: instances of inline classes are represented as the underlying type (i.e. unboxed) only if they are statically used as their actual type (and not a super-type, template, etc.). Otherwise, they are boxed, and we lose the benefit of using them in the first place:
//sampleStart interface I @JvmInline value class Foo(val i: Int) : I fun asInline(f: Foo) {} fun <T> asGeneric(x: T) {} fun asInterface(i: I) {} fun asNullable(i: Foo?) {} fun <T> id(x: T): T = x fun main() { val f = Foo(42) asInline(f) // unboxed: used as Foo itself asGeneric(f) // boxed: used as generic type T -> same as if it were not an inline class asInterface(f) // boxed: used as type I -> same as if it were not an inline class asNullable(f) // boxed: used as Foo?, which is different from Foo -> same as if it were not an inline class // below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id') // In the end, 'c' contains unboxed representation (just '42'), as 'f' val c = id(f) } //sampleEnd
It is really important to understand this in order to use inline classes properly. For example, returning to the example in the article about using sealed classes to model illegal states, we could be tempted to do this:
//sampleStart sealed interface ValidationResult @JvmInline value class Valid(val result: Int) : ValidationResult @JvmInline value class Invalid(val message: String) : ValidationResult fun execute() = sendResponse( format( validate( calculateValue() ) ) ) //sampleEnd
However, in this specific scenario, validate()
returns a ValidationResult
, which means that the return value will always be boxed, and we gain nothing form using an inline class (and additionally lose the benefits of data
classes). Therefore, it makes absolutely no sense to use inline classes in this scenario.
Properties & other limitations
Inline classes:
- are declared by the
value
keyword, - can only have a single non-synthetic property, which must be initialized in the primary constructor,
- may define other simple synthetic properties (no backing field, no delegates,
lateinit
etc.), - may define methods
- may only inherit from interfaces
- cannot participate in class hierarchies (cannot be
open
and cannot extend other classes)
You can find a lost more information in the docs. In particular, if you ever call code dealing with inline classes from Java, familiarize yourself with mangling and calling from Java code.
Considerations
Let’s recap what the main benefits of using inline classes are, from the perspective of maintainability (i.e. code safety):
- Prevent mistakes caused by accidentally interchanging incompatible values of the same type (e.g.
firstName
andlastName
, which are bothString
s) - Push code up the call stack, i.e. requiring the caller to run a certain piece of code before a method is called
The downside is that whenever we need to access the underlying value, we need to add an extra .value
.
Therefore, it is important to consider which of these are likely to happen more often, and what the actual net benefit to your codebase will be. Just blindly using inline classes instead of primitive types all the time will lead to little actual benefit, and much more clutter — a gazillion types, and the necessity to use an extra .value
everywhere. However, when used properly, and especially in the context of forcing some code to be run before a method is called, inline classes are extremely beneficial.