Safety First, with Kotlin’s Inline Classes
Avoiding Costly Mistakes in Kotlin Codebases
In software development, even a minor misstep can lead to serious issues. One common scenario is misplacing variables of the same type. This happens when two variables have the same data type but represent different concepts—like coordinates (x: Double, y: Double), measurements (length: Long, height: Long, depth: Long), or entity IDs shared across multiple domains (customerId: UUID, orderId: UUID). In such cases, the Kotlin compiler can’t detect a mistake when you accidentally swap these variables.
Imagine a function fetching an entity from a database: if you accidentally pass the ID of entity A while fetching entity B, it can lead to unexpected 404 errors or worse, retrieving an entity of the wrong type, which could break your code at runtime.
Let’s explore how Kotlin’s relatively new feature, inline classes, helps avoid these issues.
Why Traditional Type Classes Aren’t Enough
You might be thinking, “Why not create a separate type class to handle this?” Indeed, defining distinct classes like Latitude and Longitude instead of using raw Double types can introduce type safety. However, creating a new class for each distinct variable type in real-world applications can be impractical.
Not only does it add complexity, but it also comes with performance costs—wrapping primitive values into classes consumes memory and CPU resources.
Example:
Here’s an example from a retail system:
data class Order(
? ? val id: UUID,
? ? val created: Instant,
? ? val updated: Instant,
? ? val customerId: UUID,
? ? val status: OrderStatus,
? ? val branchId: UUID,
? ? val subTotal: BigDecimal,
? ? val total: BigDecimal,
? ? val discount: BigDecimal,
? ? val tax: BigDecimal,
? ? val billingAddress: Address,
) { ... }
// Hypothetical update function (with an error):
fun updateOrder(params: UpdateOrderParams) {
? ? val order = orderService.fetchOrder(params.id)
? ? order.setDiscount(params.total) // It should be params.discount!
? ? ...
}
In this scenario, if we mistakenly pass the "total" instead of the "discount", the system might start giving away products for free!
This kind of subtle error can be hard to detect, especially in large codebases with multiple developers.
Kotlin’s Inline Classes: A Solution
Kotlin’s inline classes, or value classes, provide a solution by allowing you to enforce type safety without sacrificing performance.
Inline classes are essentially value-based classes that wrap a single primitive value. Unlike regular classes, the Kotlin compiler “unboxes” them, meaning it uses the underlying type directly at runtime, which eliminates the overhead of additional wrapper objects.
领英推荐
Implementing Inline Classes for Type Safety
Here’s how you can start using inline classes in Kotlin:
@JvmInline
value class OrderTotal(val value: BigDecimal)
Note: You can omit the?@JvmInline?annotation if you're not using Kotlin in the JVM.
Example, with type-safety:
For the previous example, and only considering the money-related fields:
@JvmInline
value class OrderSubTotal(val value: BigDecimal)
@JvmInline
value class OrderTotal(val value: BigDecimal)
@JvmInline
value class OrderDiscount(val value: BigDecimal)
@JvmInline
value class OrderTax(val value: BigDecimal)
data class Order(
? ? val id: UUID,
? ? val created: Instant,
? ? val updated: Instant,
? ? val customerId: UUID,
? ? val status: OrderStatus,
? ? val branchId: UUID,
? ? val subTotal: OrderSubTotal,
? ? val total: OrderTotal,
? ? val discount: OrderDiscount,
? ? val tax: OrderTax,
? ? val billingAddress: Address,
) { ... }
With this implementation, if you try to assign a total to a discount field, the IDE will catch it and display an error:
Type mismatch.
Required: OrderDiscount
Found: OrderTotal
This helps prevent logical errors at compile time, making the code safer and reducing debugging time.
Conclusion
Kotlin’s inline classes are a game-changer for type safety. They allow you to model your domain more accurately without the performance hit of regular classes. Inline classes not only make the code more robust, but they also ensure that variable misplacements are caught during compilation, not at runtime.
For large teams or complex codebases, adopting inline classes can significantly reduce the potential for bugs, save debugging time, and lead to cleaner, more maintainable code.