Inline Classes (Value Classes) trong Kotlin
1. Giới thiệu
Inline classes (từ Kotlin 1.5+ gọi là value classes) cho phép tạo type-safe wrappers mà không có overhead runtime. Compiler sẽ inline giá trị bên trong, loại bỏ wrapper object.
2. Cú pháp cơ bản
@JvmInline
value class Password(val value: String)
@JvmInline
value class Username(val value: String)
fun authenticate(username: Username, password: Password) {
println("Authenticating ${username.value}...")
}
fun main() {
val username = Username("alice")
val password = Password("secret123")
authenticate(username, password) // OK
// authenticate(password, username) // Compile error!
}Tại sao dùng Value Classes?
// ❌ Không có type safety
fun sendEmail(to: String, subject: String, body: String) { }
// Dễ nhầm thứ tự!
sendEmail("Hello", "alice@example.com", "Message") // Bug!
// ✅ Với value classes
@JvmInline value class Email(val value: String)
@JvmInline value class Subject(val value: String)
@JvmInline value class Body(val value: String)
fun sendEmail(to: Email, subject: Subject, body: Body) { }
// Compiler catches errors
sendEmail(Email("alice@example.com"), Subject("Hello"), Body("Message"))3. Các quy tắc
@JvmInline
value class UserId(val id: Long) // ✅ OK
// ❌ Phải có exactly 1 property trong primary constructor
// value class Invalid(val a: Int, val b: Int)
// ❌ Không thể có init blocks với side effects
// value class Invalid(val value: Int) {
// init { println("Created") } // Not allowed
// }
// ✅ Có thể có secondary properties (computed)
@JvmInline
value class Name(val value: String) {
val length: Int get() = value.length
val uppercase: String get() = value.uppercase()
}4. Methods và Properties
@JvmInline
value class Percentage(val value: Double) {
// Computed properties
val asDecimal: Double get() = value / 100
// Functions
fun format(): String = "$value%"
// Validation trong init
init {
require(value in 0.0..100.0) { "Percentage must be between 0 and 100" }
}
}
fun main() {
val discount = Percentage(15.0)
println(discount.format()) // 15.0%
println(discount.asDecimal) // 0.15
// Percentage(150.0) // Throws IllegalArgumentException
}5. Implementing Interfaces
interface Printable {
fun print()
}
@JvmInline
value class Message(val text: String) : Printable {
override fun print() {
println("Message: $text")
}
}
fun main() {
val msg = Message("Hello")
msg.print() // Message: Hello
val printable: Printable = msg
printable.print() // Message: Hello
}Note: Khi dùng interface, boxing có thể xảy ra.
6. Use Cases thực tế
Domain Primitives
@JvmInline
value class CustomerId(val value: Long) {
init {
require(value > 0) { "Customer ID must be positive" }
}
}
@JvmInline
value class OrderId(val value: Long)
@JvmInline
value class Money(val cents: Long) {
val dollars: Double get() = cents / 100.0
operator fun plus(other: Money) = Money(cents + other.cents)
operator fun minus(other: Money) = Money(cents - other.cents)
operator fun times(multiplier: Int) = Money(cents * multiplier)
fun format(): String = "$${"%.2f".format(dollars)}"
}
fun processOrder(customerId: CustomerId, orderId: OrderId, amount: Money) {
println("Processing order $orderId for customer $customerId: ${amount.format()}")
}
fun main() {
val customerId = CustomerId(12345)
val orderId = OrderId(67890)
val amount = Money(9999) // $99.99
processOrder(customerId, orderId, amount)
// processOrder(orderId, customerId, amount) // Compile error!
val total = amount + Money(500) // $99.99 + $5.00
println(total.format()) // $104.99
}Type-safe IDs
@JvmInline
value class UserId(val value: String)
@JvmInline
value class PostId(val value: String)
@JvmInline
value class CommentId(val value: String)
class PostRepository {
fun getPost(id: PostId): Post? = TODO()
fun getComments(postId: PostId): List<Comment> = TODO()
}
class UserRepository {
fun getUser(id: UserId): User? = TODO()
}
// Compiler prevents mixing up IDs
fun loadPostWithAuthor(postId: PostId, userId: UserId) {
val repo = PostRepository()
// repo.getPost(userId) // Compile error! Type mismatch
val post = repo.getPost(postId) // OK
}Units of Measurement
@JvmInline
value class Meters(val value: Double) {
fun toKilometers() = Kilometers(value / 1000)
fun toFeet() = Feet(value * 3.28084)
}
@JvmInline
value class Kilometers(val value: Double) {
fun toMeters() = Meters(value * 1000)
}
@JvmInline
value class Feet(val value: Double) {
fun toMeters() = Meters(value / 3.28084)
}
// Extension functions for nice syntax
val Int.meters get() = Meters(this.toDouble())
val Int.km get() = Kilometers(this.toDouble())
val Double.meters get() = Meters(this)
fun main() {
val distance = 5.km
println("${distance.value} km = ${distance.toMeters().value} meters")
val height = 100.meters
println("${height.value} m = ${height.toFeet().value} feet")
}Duration (before Kotlin Duration)
@JvmInline
value class Milliseconds(val value: Long) {
val seconds: Long get() = value / 1000
val minutes: Long get() = value / 60_000
operator fun plus(other: Milliseconds) = Milliseconds(value + other.value)
operator fun compareTo(other: Milliseconds) = value.compareTo(other.value)
}
val Int.ms get() = Milliseconds(this.toLong())
val Int.seconds get() = Milliseconds(this * 1000L)
val Int.minutes get() = Milliseconds(this * 60_000L)
fun main() {
val timeout = 30.seconds
val delay = 500.ms
println("${timeout.value} ms") // 30000 ms
println("${(timeout + delay).seconds} seconds") // 30 seconds
}7. Boxing và Performance
@JvmInline
value class Id(val value: Long)
fun directUse(id: Id) {
// No boxing - id được inline thành Long
println(id.value)
}
fun nullableUse(id: Id?) {
// Boxing occurs - cần object để represent null
println(id?.value)
}
fun genericUse(item: Any) {
// Boxing occurs - cần object cho Any
println(item)
}
fun main() {
val id = Id(123)
directUse(id) // No boxing
nullableUse(id) // Boxing
genericUse(id) // Boxing
}8. So sánh với Type Alias
// Type alias - chỉ là tên khác, KHÔNG type-safe
typealias UserId = Long
typealias OrderId = Long
fun processWithAlias(userId: UserId, orderId: OrderId) {}
// Value class - type-safe, có runtime cost minimal
@JvmInline value class SafeUserId(val value: Long)
@JvmInline value class SafeOrderId(val value: Long)
fun processWithValue(userId: SafeUserId, orderId: SafeOrderId) {}
fun main() {
val userId: UserId = 1L
val orderId: OrderId = 2L
// ❌ Type alias không ngăn lỗi
processWithAlias(orderId, userId) // Compiles! Bug!
val safeUserId = SafeUserId(1L)
val safeOrderId = SafeOrderId(2L)
// ✅ Value class catches errors
// processWithValue(safeOrderId, safeUserId) // Compile error!
processWithValue(safeUserId, safeOrderId) // OK
}📝 Tóm tắt
| Feature | Description |
|---|---|
| Syntax | @JvmInline value class Name(val value: Type) |
| Properties | Only 1 primary property, can have computed properties |
| Methods | Can have methods và implement interfaces |
| Performance | Inlined at runtime (no boxing in most cases) |
| Type safety | Prevents mixing up similar types |
Khi nào dùng value classes:
- Wrapper primitives với meaning khác nhau (IDs, units)
- Domain primitives với validation
- Type-safe API design
- Khi cần type safety mà không muốn overhead
Khi KHÔNG nên dùng:
- Khi cần multiple properties
- Khi cần inheritance
- Khi thường xuyên dùng với generics/nullables (boxing)
Last updated on