Chào mừng bạn đến với Cafedev, nơi chúng ta cùng nhau khám phá những điều mới mẻ về công nghệ! Trong chuyên mục hôm nay, chúng ta sẽ đàm phán về “Kotlin với Thuộc tính được Ủy quyền.” Kotlin không chỉ là một ngôn ngữ lập trình mạnh mẽ, mà còn mang đến mô hình lập trình sáng tạo với khả năng ủy quyền thuộc tính. Điều này giúp chúng ta tối ưu hóa mã nguồn và tái sử dụng mã một cách linh hoạt. Hãy cùng nhau khám phá hơn về sức mạnh của Kotlin và cách nó làm thay đổi cách chúng ta viết mã!”

Với một số loại thuộc tính phổ biến, mặc dù bạn có thể triển khai chúng thủ công mỗi khi cần, nhưng nó hữu ích hơn khi triển khai chúng một lần, thêm chúng vào thư viện và sau đó tái sử dụng chúng. Ví dụ:
* Thuộc tính Lazy: giá trị chỉ được tính toán khi truy cập lần đầu tiên.
* Thuộc tính Observable: người nghe được thông báo về các thay đổi của thuộc tính này.
* Lưu trữ thuộc tính trong một map thay vì một trường riêng lẻ cho mỗi thuộc tính.
Để xử lý những trường hợp như vậy (và các trường hợp khác), Kotlin hỗ trợ thuộc tính được ủy quyền:

class Example {
    var p: String by Delegate()
}

Cú pháp là: val/var <property name>: <Type> by <expression>. Biểu thức sau by là một ủy quyền, vì get() (và set()) tương ứng với thuộc tính sẽ được ủy quyền cho các phương thức getValue()setValue() của nó. Thủ tục ủy quyền không cần phải triển khai một giao diện, nhưng chúng phải cung cấp một hàm getValue() (và setValue() cho vars).
Ví dụ:

import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

Khi bạn đọc từ p, nó được ủy quyền cho một thể hiện của Delegate, hàm getValue() từ Delegate được gọi. Tham số đầu tiên của nó là đối tượng bạn đọc p từ, và tham số thứ hai giữ mô tả của p chính nó (ví dụ, bạn có thể lấy tên của nó).

val e = Example()
println(e.p)

Điều này in ra:
Example@33a17727, cảm ơn bạn đã ủy quyền 'p' cho tôi!
Tương tự, khi bạn gán cho p, hàm setValue() được gọi. Hai tham số đầu tiên là giống nhau, và thứ ba giữ giá trị đang được gán:

e.p = “NEW”

Điều này in ra:
NEW đã được gán cho 'p' trong Example@33a17727.
Thông số của yêu cầu đối tượng được ủy quyền có thể được tìm thấy bên dưới.
Bạn có thể khai báo một thuộc tính được ủy quyền trong một hàm hoặc khối mã; nó không nhất thiết phải là một thành viên của một lớp. Dưới đây bạn có thể tìm thấy một ví dụ.

1. Các ủy quyền cơ bản

Thư viện tiêu chuẩn của Kotlin cung cấp các phương thức tạo đối với một số loại ủy quyền hữu ích.

1.1 Thuộc tính Lazy

lazy() là một hàm nhận một lambda và trả về một thể hiện của Lazy, có thể làm ủy quyền để triển khai một thuộc tính lười biếng. Cuộc gọi đầu tiên của get() thực thi lambda được truyền vào lazy() và ghi nhớ kết quả. Các cuộc gọi sau của get() đơn giản trả lại kết quả đã ghi nhớ.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

Theo mặc định, việc đánh giá của thuộc tính lười biếng là đồng bộ hóa: giá trị chỉ được tính toán trong một luồng, nhưng tất cả các luồng sẽ thấy cùng một giá trị. Nếu đồng bộ hóa của ủy quyền khởi tạo không cần thiết để cho phép nhiều luồng thực hiện nó đồng thời, hãy chuyển LazyThreadSafetyMode.PUBLICATION làm tham số cho lazy().
Nếu bạn chắc chắn rằng việc khởi tạo luôn xảy ra trong cùng một luồng như luồng bạn sử dụng thuộc tính, bạn có thể sử dụng LazyThreadSafetyMode.NONE. Nó không gây ra bất kỳ cam kết về an toàn đối với luồng và chi phí liên quan nào.

1.2 Thuộc tính Observable

Delegates.observable() nhận hai đối số: giá trị ban đầu và một trình xử lý cho các sửa đổi.
Trình xử lý được gọi mỗi khi bạn gán giá trị cho thuộc tính (sau khi gán đã được thực hiện). Nó có ba tham số: thuộc tính đang được gán, giá trị cũ và giá trị mới:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}

Nếu bạn muốn chặn và veto các phép gán, hãy sử dụng vetoable() thay vì observable(). Trình xử lý được chuyển đến vetoable sẽ được gọi trước khi gán một giá trị mới cho thuộc tính.

2. Ủy quyền cho một thuộc tính khác

Một thuộc tính có thể ủy quyền getter và setter của nó cho một thuộc tính khác. Chủ ủy quyền này có sẵn cho cả thuộc tính cấp cao và thuộc tính của lớp (thành viên và mở rộng). Thuộc tính được ủy quyền có thể là:
* Một thuộc tính cấp cao
* Một thành viên hoặc một thuộc tính mở rộng của cùng một lớp
* Một thành viên hoặc một thuộc tính mở rộng của một lớp khác
Để ủy quyền một thuộc tính cho một thuộc tính khác, hãy sử dụng bộ chọn :: trong tên ủy quyền, ví dụ, this::delegate hoặc MyClass::delegate.

var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

Điều này có thể hữu ích, ví dụ, khi bạn muốn đổi tên một thuộc tính một cách tương thích ngược: giới thiệu một thuộc tính mới, chú thích thuộc tính cũ với chú thích @Deprecated, và ủy quyền triển khai của nó.

class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}
fun main() {
   val myClass = MyClass()
   // Notification: 'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

3. Lưu trữ thuộc tính trong một map

Một trường hợp sử dụng phổ biến là lưu trữ các giá trị của thuộc tính trong một map. Điều này thường xuyên xuất hiện trong ứng dụng cho các công việc như phân tích cú pháp JSON hoặc thực hiện các công việc động khác. Trong trường hợp này, bạn có thể sử dụng chính thể hiện map làm ủy quyền cho một thuộc tính được ủy quyền.

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

Trong ví dụ này, hàm tạo nhận một map:

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

Thuộc tính được ủy quyền lấy giá trị từ map này thông qua các khóa chuỗi, được liên kết với tên của các thuộc tính:

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

Điều này cũng hoạt động cho thuộc tính var nếu bạn sử dụng một MutableMap thay vì một Map chỉ đọc:

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

4. Thuộc tính được ủy quyền cục bộ

Bạn có thể khai báo biến cục bộ như là thuộc tính được ủy quyền. Ví dụ, bạn có thể tạo một biến cục bộ làm lười biếng:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

Biến memoizedFoo sẽ được tính toán chỉ khi truy cập lần đầu tiên. Nếu someCondition không thành công, biến sẽ không được tính toán.

5. Yêu cầu ủy quyền thuộc tính

Đối với thuộc tính chỉ đọc (val), một ủy quyền phải cung cấp một hàm toán tử getValue() với các tham số sau:
* thisRef phải là cùng loại hoặc là một loại siêu loại của chủ sở hữu thuộc tính (đối với thuộc tính mở rộng, nó phải là loại đang được mở rộng).
* property phải là kiểu KProperty<*> hoặc là siêu loại của nó.
getValue() phải trả về cùng loại như thuộc tính (hoặc là loại con của nó).

class Resource

class Owner {
    val valResource: Resource by ResourceDelegate()
}

class ResourceDelegate {
    operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
        return Resource()
    }
}

Đối với thuộc tính thay đổi được (var), một ủy quyền cũng phải cung cấp một hàm toán tử setValue() với các tham số sau:
* thisRef phải là cùng loại hoặc là một loại siêu loại của chủ sở hữu thuộc tính (đối với thuộc tính mở rộng, nó phải là loại đang được mở rộng).
* property phải là kiểu KProperty<*> hoặc là siêu loại của nó.
* value phải là cùng loại như thuộc tính (hoặc là loại siêu loại của nó).

class Resource

class Owner {
    var varResource: Resource by ResourceDelegate()
}

class ResourceDelegate(private var resource: Resource = Resource()) {
    operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
        return resource
    }
    operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
        if (value is Resource) {
            resource = value
        }
    }
}

Hàm getValue() và/hoặc setValue() có thể được cung cấp làm hàm thành viên của lớp ủy quyền hoặc làm hàm mở rộng. Hàm sau là thuận tiện khi bạn cần ủy quyền thuộc tính cho một đối tượng không gốc có sẵn các hàm này. Cả hai hàm đều cần được đánh dấu bằng từ khóa operator.
Bạn có thể tạo ủy quyền như là đối tượng ẩn danh mà không cần tạo các lớp mới, bằng cách sử dụng các giao diện ReadOnlyPropertyReadWriteProperty từ thư viện tiêu chuẩn của Kotlin. Chúng cung cấp các phương thức yêu cầu: getValue() được khai báo trong ReadOnlyProperty; ReadWriteProperty mở rộng nó và thêm setValue(). Điều này có nghĩa là bạn có thể truyền một ReadWriteProperty mỗi khi một ReadOnlyProperty được mong đợi.

fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> =
    object : ReadWriteProperty<Any?, Resource> {
        var curValue = resource
        override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
            curValue = value
        }
    }

val readOnlyResource: Resource by resourceDelegate()  // ReadWriteProperty as val
var readWriteResource: Resource by resourceDelegate()

6. Quy tắc dịch cho thuộc tính được ủy quyền

Trình biên dịch Kotlin tạo ra các thuộc tính phụ trợ cho một số loại thuộc tính được ủy quyền và sau đó ủy quyền cho chúng.

Vì mục đích tối ưu hóa, trình biên dịch không tạo ra các thuộc tính phụ trợ trong một số trường hợp. Tìm hiểu về tối ưu hóa trên ví dụ của ủy quyền cho một thuộc tính khác.
Ví dụ, đối với thuộc tính prop, nó tạo ra thuộc tính ẩn prop$delegate, và mã của các truy cập chỉ đơn giản là ủy quyền cho thuộc tính bổ sung này:

class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Trình biên dịch Kotlin cung cấp tất cả thông tin cần thiết về prop trong các đối số: đối số đầu tiên this tham chiếu đến một thể hiện của lớp ngoại C, và this::prop là một đối tượng phản ánh của kiểu KProperty mô tả prop chính nó.</code<></em<>

6.1 Các trường tối ưu hóa cho thuộc tính được ủy quyền

Trường $delegate sẽ bị bỏ qua nếu một ủy quyền là:
* Một thuộc tính được tham chiếu:

class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}

* Một đối tượng có tên:

object NamedObject {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
}

val s: String by NamedObject

* Một thuộc tính val cuối cùng với một trường hỗ trợ và một hàm getter mặc định trong cùng một module:

val impl: ReadOnlyProperty<Any?, String> = ...

class A {
    val s: String by impl
}

* Một biểu thức hằng số, mục enum, this, null. Ví dụ về this:

class A {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) ...

    val s by this
}

6.2 Quy tắc dịch khi ủy quyền cho một thuộc tính khác

Khi ủy quyền cho một thuộc tính khác, trình biên dịch Kotlin tạo ra truy cập trực tiếp đến thuộc tính được tham chiếu. Điều này có nghĩa là trình biên dịch không tạo ra trường prop$delegate. Tối ưu hóa này giúp tiết kiệm bộ nhớ.
Xem mã sau đây, ví dụ:

class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}

Các trình truy cập thuộc tính của biến prop gọi trực tiếp biến impl, bỏ qua các toán tử getValuesetValue của thuộc tính được ủy quyền, và do đó không cần đến đối tượng tham chiếu KProperty.
Đối với mã trên, trình biên dịch tạo ra mã sau đây:

class C<Type> {
    private var impl: Type = ...

    var prop: Type
        get() = impl
        set(value) {
            impl = value
        }

    fun getProp$delegate(): Type = impl // This method is needed only for reflection
}

7. Cung cấp một ủy quyền

Bằng cách định nghĩa toán tử provideDelegate, bạn có thể mở rộng logic để tạo ra đối tượng mà thuộc tính được ủy quyền đến. Nếu đối tượng được sử dụng bên phải của by định nghĩa provideDelegate như là một hàm thành viên hoặc mở rộng, hàm đó sẽ được gọi để tạo ra thể hiện ủy quyền thuộc tính.
Một trong các trường hợp sử dụng có thể của provideDelegate là kiểm tra tính nhất quán của thuộc tính khi khởi tạo.
Ví dụ, để kiểm tra tên thuộc tính trước khi ràng buộc, bạn có thể viết như sau:

class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
    override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // create delegate
        return ResourceDelegate()
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

class MyUI {
    fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

Các tham số của provideDelegate giống như của getValue:
* thisRef phải là cùng loại hoặc là một loại siêu loại của chủ sở hữu thuộc tính (đối với thuộc tính mở rộng, nó phải là loại đang được mở rộng);
* property phải là kiểu KProperty<*> hoặc là siêu loại của nó.
Phương thức provideDelegate được gọi cho mỗi thuộc tính trong quá trình tạo ra thể hiện của MyUI, và nó thực hiện kiểm tra cần thiết ngay lập tức.
Mà không có khả năng chặn sự ràng buộc giữa thuộc tính và ủy quyền của nó, để đạt được cùng một chức năng, bạn phải truyền tên thuộc tính một cách rõ ràng, điều này không tiện lợi:

// Checking the property name without "provideDelegate" functionality
class MyUI {
    val image by bindResource(ResourceID.image_id, "image")
    val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
): ReadOnlyProperty<MyUI, T> {
    checkProperty(this, propertyName)
    // create delegate
}

Trong mã được tạo ra, phương thức provideDelegate được gọi để khởi tạo thuộc tính phụ trợ prop$delegate. So sánh mã được tạo ra cho khai báo thuộc tính val prop: Type by MyDelegate() với mã được tạo ra ở trên (khi phương thức provideDelegate không có mặt):

class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler
// when the 'provideDelegate' function is available:
class C {
    // calling "provideDelegate" to create the additional "delegate" property
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Lưu ý rằng phương thức provideDelegate chỉ ảnh hưởng đến việc tạo ra thuộc tính phụ trợ và không ảnh hưởng đến mã được tạo ra cho getter hoặc setter.
Với giao diện PropertyDelegateProvider từ thư viện tiêu chuẩn, bạn có thể tạo ra các nhà cung cấp ủy quyền mà không cần tạo ra các lớp mới.

val provider = PropertyDelegateProvider { thisRef: Any?, property ->
    ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}
val delegate: Int by provider

Cảm ơn bạn đã dành thời gian để đọc về “Kotlin với Thuộc tính được Ủy quyền” trên Cafedev. Hy vọng rằng thông tin mà chúng tôi chia sẻ đã mang lại cho bạn cái nhìn mới về sức mạnh của Kotlin trong việc quản lý thuộc tính. Đừng quên tiếp tục theo dõi Cafedev để cập nhật những xu hướng công nghệ mới nhất và chia sẻ kiến thức cùng cộng đồng. Hãy lan tỏa sự đam mê lập trình và khám phá thế giới mã nguồn mở cùng Cafedev!

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!