📦 Inline/Value Classes
1. Giới thiệu
Phần tiêu đề “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
Phần tiêu đề “2. Cú pháp cơ bản”@JvmInlinevalue class Password(val value: String)
@JvmInlinevalue 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?
Phần tiêu đề “Tại sao dùng Value Classes?”// ❌ Không có type safetyfun sendEmail(to: String, subject: String, body: String) { }
// Dễ nhầm thứ tự!
// ✅ 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 errors3. Các quy tắc
Phần tiêu đề “3. Các quy tắc”@JvmInlinevalue 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)@JvmInlinevalue class Name(val value: String) { val length: Int get() = value.length val uppercase: String get() = value.uppercase()}4. Methods và Properties
Phần tiêu đề “4. Methods và Properties”@JvmInlinevalue 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
Phần tiêu đề “5. Implementing Interfaces”interface Printable { fun print()}
@JvmInlinevalue 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ế
Phần tiêu đề “6. Use Cases thực tế”Domain Primitives
Phần tiêu đề “Domain Primitives”@JvmInlinevalue class CustomerId(val value: Long) { init { require(value > 0) { "Customer ID must be positive" } }}
@JvmInlinevalue class OrderId(val value: Long)
@JvmInlinevalue 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
Phần tiêu đề “Type-safe IDs”@JvmInlinevalue class UserId(val value: String)
@JvmInlinevalue class PostId(val value: String)
@JvmInlinevalue 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 IDsfun loadPostWithAuthor(postId: PostId, userId: UserId) { val repo = PostRepository() // repo.getPost(userId) // Compile error! Type mismatch val post = repo.getPost(postId) // OK}Units of Measurement
Phần tiêu đề “Units of Measurement”@JvmInlinevalue class Meters(val value: Double) { fun toKilometers() = Kilometers(value / 1000) fun toFeet() = Feet(value * 3.28084)}
@JvmInlinevalue class Kilometers(val value: Double) { fun toMeters() = Meters(value * 1000)}
@JvmInlinevalue class Feet(val value: Double) { fun toMeters() = Meters(value / 3.28084)}
// Extension functions for nice syntaxval 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)
Phần tiêu đề “Duration (before Kotlin Duration)”@JvmInlinevalue 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
Phần tiêu đề “7. Boxing và Performance”@JvmInlinevalue 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
Phần tiêu đề “8. So sánh với Type Alias”// Type alias - chỉ là tên khác, KHÔNG type-safetypealias UserId = Longtypealias 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
Phần tiêu đề “📝 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)