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à:
. Biểu thức sau val/var <property name>: <Type> by <expression>
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()
và 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 var
s).
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ụ.
Nội dung chính
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 ReadOnlyProperty
và ReadWriteProperty
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ử getValue
và setValue
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!