Property Accessors trong Kotlin
1. Giới thiệu
Trong Kotlin, properties tự động có getter và setter. Bạn có thể tùy chỉnh chúng để thêm logic validation, computation, hoặc side effects.
2. Default Accessors
class Person {
val name: String = "Alice" // Chỉ có getter (read-only)
var age: Int = 0 // Có cả getter và setter
}
// Tương đương với:
class PersonJavaStyle {
private val _name: String = "Alice"
private var _age: Int = 0
fun getName(): String = _name
fun getAge(): Int = _age
fun setAge(value: Int) { _age = value }
}3. Custom Getter
Computed Property
class Rectangle(val width: Int, val height: Int) {
// Computed property - không lưu trữ giá trị
val area: Int
get() = width * height
val perimeter: Int
get() = 2 * (width + height)
val isSquare: Boolean
get() = width == height
}
fun main() {
val rect = Rectangle(5, 10)
println(rect.area) // 50 (tính mỗi lần gọi)
println(rect.perimeter) // 30
println(rect.isSquare) // false
}Getter với logic phức tạp
class User(val firstName: String, val lastName: String) {
val fullName: String
get() = "$firstName $lastName"
val initials: String
get() = "${firstName.first()}${lastName.first()}"
val displayName: String
get() {
return if (firstName.isNotBlank()) {
fullName
} else {
"Anonymous"
}
}
}
fun main() {
val user = User("John", "Doe")
println(user.fullName) // John Doe
println(user.initials) // JD
println(user.displayName) // John Doe
}4. Custom Setter
Setter với validation
class Person {
var name: String = ""
set(value) {
field = value.trim().replaceFirstChar { it.uppercase() }
}
var age: Int = 0
set(value) {
if (value < 0) {
throw IllegalArgumentException("Age cannot be negative")
}
field = value
}
}
fun main() {
val person = Person()
person.name = " alice "
println(person.name) // Alice
person.age = 25 // OK
// person.age = -5 // Throws IllegalArgumentException
}Setter với logging
class Configuration {
var theme: String = "light"
set(value) {
println("Theme changing from '$field' to '$value'")
field = value
}
var fontSize: Int = 14
set(value) {
if (value != field) {
println("Font size: $field -> $value")
field = value
}
}
}
fun main() {
val config = Configuration()
config.theme = "dark" // Theme changing from 'light' to 'dark'
config.fontSize = 16 // Font size: 14 -> 16
config.fontSize = 16 // (không in gì vì giá trị không đổi)
}5. Backing Field (field)
field là từ khóa đặc biệt chỉ dùng được trong accessors, đại diện cho giá trị thực của property:
class Counter {
var count: Int = 0
get() = field
set(value) {
if (value >= 0) {
field = value
}
}
// Property không có backing field (computed)
val isPositive: Boolean
get() = count > 0 // Không dùng 'field', không có backing field
}Lưu ý: Nếu getter không tham chiếu
fieldvà setter không gán chofield, property sẽ không có backing field.
6. Backing Property Pattern
Khi cần kiểm soát nhiều hơn:
class User {
// Private backing property
private var _emails = mutableListOf<String>()
// Public read-only view
val emails: List<String>
get() = _emails.toList() // Return immutable copy
fun addEmail(email: String) {
_emails.add(email)
}
}
fun main() {
val user = User()
user.addEmail("alice@example.com")
user.addEmail("alice@work.com")
println(user.emails) // [alice@example.com, alice@work.com]
// user.emails.add("x") // Error: List is read-only
}Trong ViewModel (Android)
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
}7. lateinit Properties
Cho phép khởi tạo property sau constructor:
class UserProfile {
lateinit var name: String
lateinit var email: String
fun isInitialized(): Boolean {
return ::name.isInitialized && ::email.isInitialized
}
fun setup(name: String, email: String) {
this.name = name
this.email = email
}
}
fun main() {
val profile = UserProfile()
println(profile.isInitialized()) // false
profile.setup("Alice", "alice@example.com")
println(profile.isInitialized()) // true
println(profile.name) // Alice
}Quy tắc lateinit:
- Chỉ dùng với
var(không phảival) - Không dùng với primitive types (Int, Boolean, etc.)
- Không có custom getter/setter
- Phải khởi tạo trước khi sử dụng
class Example {
// lateinit var count: Int // ERROR: primitive type
lateinit var name: String // OK
// Kiểm tra đã khởi tạo chưa
fun printName() {
if (::name.isInitialized) {
println(name)
} else {
println("Not initialized")
}
}
}8. Accessor Visibility Modifiers
Có thể set visibility riêng cho setter:
class Counter {
var count: Int = 0
private set // Getter public, setter private
fun increment() {
count++
}
}
class User {
var id: String = ""
internal set // Setter chỉ accessible trong module
var nickname: String = ""
protected set // Setter chỉ accessible trong subclasses
}
fun main() {
val counter = Counter()
println(counter.count) // OK: getter is public
// counter.count = 10 // ERROR: setter is private
counter.increment() // OK
}9. Properties trong Interface
interface Named {
val name: String // Abstract property
val greetingMessage: String
get() = "Hello, $name!" // Property với default getter
}
class Person(override val name: String) : Named
class Robot : Named {
override val name: String
get() = "Robot-${hashCode()}"
}
fun main() {
val person = Person("Alice")
println(person.name) // Alice
println(person.greetingMessage) // Hello, Alice!
val robot = Robot()
println(robot.greetingMessage) // Hello, Robot-12345678!
}10. Best Practices
✅ Do
class User {
// Computed property cho derived values
val displayName: String
get() = "$firstName $lastName"
// Setter với validation
var email: String = ""
set(value) {
require(value.contains("@")) { "Invalid email" }
field = value
}
// Private set cho controlled mutation
var loginCount: Int = 0
private set
}❌ Don’t
class BadExample {
// Avoid: Heavy computation trong getter
val processedData: List<Data>
get() {
return fetchFromNetwork() // Called every access!
}
// Avoid: Side effects trong getter
var counter: Int = 0
get() {
field++ // Modifying trong getter!
return field
}
}Khi nào dùng computed vs stored property
class Rectangle(val width: Int, val height: Int) {
// ✅ Computed: cheap, derived từ other properties
val area: Int
get() = width * height
// ✅ Stored: expensive computation, cache kết quả
val expensiveValue: String by lazy {
// One-time expensive computation
computeExpensiveValue()
}
}📝 Tóm tắt
| Feature | Description |
|---|---|
| Custom getter | get() = expression |
| Custom setter | set(value) { field = value } |
field | Backing field trong accessors |
lateinit | Khởi tạo sau, chỉ cho var non-primitive |
private set | Setter private, getter public |
| Backing property | _propName pattern |
Best practices:
- Dùng computed properties cho derived values
- Dùng setter validation để enforce invariants
- Dùng
private setcho controlled mutation - Avoid side effects trong getters
- Dùng
lateinitkhi initialization timing không xác định
Last updated on