Kotlin属性委托入门
2022-07-133.5k 阅读
Kotlin属性委托基础概念
在Kotlin中,属性委托是一种强大的特性,它允许我们将属性的存储和逻辑委托给其他对象。简单来说,当我们定义一个属性时,我们可以不直接在类中实现它的getter和setter方法,而是把这些操作交给另一个对象来处理。
先来看一个简单的示例:
class Example {
var p: String by Delegate()
}
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.")
}
}
在上述代码中,Example
类中的属性p
通过by
关键字将其访问和赋值操作委托给了Delegate
类的实例。当我们获取p
的值时,会调用Delegate
类的getValue
方法;当我们设置p
的值时,会调用Delegate
类的setValue
方法。
委托属性的原理剖析
- 属性访问的本质
- Kotlin中的属性访问本质上是通过生成的getter和setter方法来实现的。对于一个普通属性
var name: String
,编译器会生成getName()
和setName(String value)
方法(对于Java风格的命名规范,在Kotlin中实际使用name
和name = value
这样的语法糖来调用这些方法)。 - 当使用属性委托时,
by
关键字后面的对象需要提供特定的操作符函数,即getValue
和setValue
(对于可变属性)。这些函数负责实际的属性值获取和设置逻辑。
- Kotlin中的属性访问本质上是通过生成的getter和setter方法来实现的。对于一个普通属性
getValue
函数getValue
函数的定义格式为operator fun getValue(thisRef: Any?, property: KProperty<*>): T
,其中thisRef
是委托属性所在类的实例(如果属性是val
类型且在对象字面量中使用,thisRef
可能为null
),property
是描述委托属性的KProperty
对象,通过它可以获取属性的名称等信息,返回值T
就是属性的类型。- 例如,在前面的示例中,
getValue
函数根据传入的thisRef
和property
生成一个描述性的字符串,作为属性p
的值返回。
setValue
函数setValue
函数用于可变属性的赋值操作,其定义格式为operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
,参数thisRef
和property
与getValue
中的含义相同,value
就是要赋给属性的值。- 在示例中,
setValue
函数只是打印出赋值的相关信息,实际应用中可以实现更复杂的逻辑,如数据验证、持久化等。
标准库中的属性委托
lazy
委托lazy
委托用于实现延迟初始化。当属性第一次被访问时,才会执行初始化代码。这在属性初始化代价较高且可能在某些情况下根本不会被使用时非常有用。- 示例代码如下:
class MyClass {
val lazyValue: String by lazy {
println("Initializing lazyValue")
"Value after initialization"
}
}
fun main() {
val myObject = MyClass()
println("Before accessing lazyValue")
println(myObject.lazyValue)
println("After first access lazyValue")
println(myObject.lazyValue)
}
- 在上述代码中,
lazyValue
属性使用lazy
委托。第一次访问myObject.lazyValue
时,会执行lazy
块中的代码,打印“Initializing lazyValue”,并返回初始化的值。后续再次访问lazyValue
时,不会再次执行初始化代码,直接返回已初始化的值。 lazy
函数接受一个LazyThreadSafetyMode
参数,用于指定线程安全模式。默认是LazyThreadSafetyMode.SYNCHRONIZED
,即线程安全的懒加载,在多线程环境下,只有一个线程能初始化属性。如果确定是单线程环境,可以使用LazyThreadSafetyMode.NONE
来提高性能,此时不会进行同步操作。
observable
委托observable
委托用于监听属性值的变化。当属性值发生改变时,会通知注册的监听器。- 示例代码如下:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
property, oldValue, newValue ->
println("Property ${property.name} changed from $oldValue to $newValue")
}
}
fun main() {
val user = User()
user.name = "John"
user.name = "Jane"
}
- 在上述代码中,
User
类的name
属性使用observable
委托。初始值为<no name>
,当name
的值发生变化时,会执行监听器代码块,打印出属性名称、旧值和新值。 observable
委托的第一个参数是属性的初始值,第二个参数是监听器。监听器是一个函数,接受三个参数:描述属性的KProperty
对象、旧值和新值。
vetoable
委托vetoable
委托与observable
类似,但它可以在属性值即将改变时进行否决。如果否决,属性值不会改变。- 示例代码如下:
import kotlin.properties.Delegates
class Settings {
var volume: Int by Delegates.vetoable(0) {
property, oldValue, newValue ->
if (newValue in 0..100) {
true
} else {
false
}
}
}
fun main() {
val settings = Settings()
settings.volume = 50
println(settings.volume)
settings.volume = 150
println(settings.volume)
}
- 在上述代码中,
Settings
类的volume
属性使用vetoable
委托。初始值为0,当尝试设置volume
的值时,会执行否决函数。如果新值在0到100之间,返回true
,允许赋值;否则返回false
,不允许赋值。所以第一次设置volume
为50成功,第二次设置为150失败,volume
的值仍为50。
自定义属性委托
- 基本步骤
- 要自定义属性委托,首先需要创建一个类,该类要提供
getValue
和setValue
(对于可变属性)操作符函数。 - 例如,我们创建一个用于实现可重置属性的委托:
- 要自定义属性委托,首先需要创建一个类,该类要提供
class ResettableProperty<T>(initialValue: T) {
private var value = initialValue
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
value = newValue
}
fun reset() {
value = initialValue
}
}
class MyData {
var data: String by ResettableProperty("default value")
}
fun main() {
val myData = MyData()
println(myData.data)
myData.data = "new value"
println(myData.data)
myData.data.reset()
println(myData.data)
}
- 在上述代码中,
ResettableProperty
类是自定义的属性委托类。它包含一个存储属性值的value
变量,getValue
和setValue
方法用于属性的访问和赋值,reset
方法用于将属性值重置为初始值。MyData
类中的data
属性使用ResettableProperty
委托,通过调用reset
方法可以将data
属性值重置。
- 泛型的应用
- 在自定义属性委托中,经常会使用泛型来提高委托的通用性。如上面的
ResettableProperty
类,通过泛型<T>
可以支持不同类型的属性委托,而不需要为每种类型都创建一个单独的委托类。 - 再看一个更复杂的泛型委托示例,实现一个带有缓存功能的属性委托:
- 在自定义属性委托中,经常会使用泛型来提高委托的通用性。如上面的
class CachingProperty<T>(initializer: () -> T) {
private var value: T? = null
private val initializer: () -> T = initializer
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value?: initializer().also { value = it }
}
}
class MyCalculation {
val result: Int by CachingProperty {
println("Calculating result")
42
}
}
fun main() {
val myCalculation = MyCalculation()
println(myCalculation.result)
println(myCalculation.result)
}
- 在上述代码中,
CachingProperty
类是一个泛型属性委托类。它接受一个初始化函数initializer
,在getValue
方法中,首先检查value
是否已缓存,如果已缓存则直接返回;否则调用初始化函数计算值,缓存并返回。MyCalculation
类中的result
属性使用CachingProperty
委托,第一次访问result
时会执行初始化计算并缓存结果,后续访问直接返回缓存的值,不会再次执行计算。
属性委托在Android开发中的应用
- 视图绑定
- 在Android开发中,视图绑定是一种常见的需求。以前,我们通常使用
findViewById
来获取视图对象,这种方式代码冗长且容易出错。使用属性委托可以简化这个过程。 - 假设我们有一个简单的布局文件
activity_main.xml
:
- 在Android开发中,视图绑定是一种常见的需求。以前,我们通常使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, Kotlin!" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click me" />
</LinearLayout>
- 我们可以使用属性委托来绑定视图:
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlin.properties.Delegates
class MainActivity : AppCompatActivity() {
private val textView: TextView by lazy { findViewById<TextView>(R.id.text_view) }
private val button: Button by lazy { findViewById<Button>(R.id.button) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
textView.text = "Button clicked!"
}
}
}
- 在上述代码中,
textView
和button
属性使用lazy
委托。这样在第一次访问这些属性时,才会调用findViewById
方法获取视图对象,提高了初始化性能。同时,代码更加简洁,也减少了空指针异常的风险,因为lazy
委托保证了属性在访问时一定是初始化好的。
- 数据绑定与MVVM架构
- 在MVVM架构中,属性委托可以用于实现数据绑定。例如,我们可以创建一个视图模型(ViewModel)类,使用
observable
委托来监听数据的变化,并将变化通知给视图。 - 首先定义一个简单的视图模型类:
- 在MVVM架构中,属性委托可以用于实现数据绑定。例如,我们可以创建一个视图模型(ViewModel)类,使用
import androidx.lifecycle.ViewModel
import kotlin.properties.Delegates
class MyViewModel : ViewModel() {
var message: String by Delegates.observable("Initial message") {
property, oldValue, newValue ->
// 这里可以通知视图数据变化
println("Message changed from $oldValue to $newValue")
}
}
- 在Activity中使用这个视图模型:
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
val textView: TextView = findViewById(R.id.text_view)
viewModel.message = "New message"
textView.text = viewModel.message
}
}
- 在上述代码中,
MyViewModel
类的message
属性使用observable
委托。当message
的值发生变化时,会执行监听器代码块,我们可以在这个监听器中通知视图更新。在MainActivity
中,获取视图模型实例并设置message
的值,同时将message
的值显示在TextView
上。通过这种方式,实现了数据与视图的绑定,符合MVVM架构的理念。
属性委托与依赖注入
- 依赖注入的概念
- 依赖注入(Dependency Injection,简称DI)是一种软件设计模式,它允许将对象所依赖的其他对象通过外部传入,而不是在对象内部创建。这样可以提高代码的可测试性、可维护性和可扩展性。
- 属性委托实现依赖注入
- 我们可以使用属性委托来实现简单的依赖注入。例如,假设有一个服务接口
UserService
和它的实现类UserServiceImpl
:
- 我们可以使用属性委托来实现简单的依赖注入。例如,假设有一个服务接口
interface UserService {
fun getUserName(): String
}
class UserServiceImpl : UserService {
override fun getUserName(): String {
return "John Doe"
}
}
- 然后我们有一个使用
UserService
的类UserViewModel
,可以通过属性委托来注入UserService
:
class UserViewModel {
var userService: UserService by ServiceLocator()
fun displayUserName() {
println("User name: ${userService.getUserName()}")
}
}
class ServiceLocator {
private val services: MutableMap<Class<*>, Any> = mutableMapOf()
init {
services[UserService::class.java] = UserServiceImpl()
}
operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T {
return services[property.returnType.classifier as Class<T>] as T
}
}
- 在上述代码中,
ServiceLocator
类是一个简单的服务定位器,它使用getValue
操作符函数来实现属性委托。UserViewModel
类的userService
属性通过ServiceLocator
委托来获取UserService
的实例。在ServiceLocator
的初始化块中,我们将UserServiceImpl
的实例注册到服务映射中。当UserViewModel
访问userService
属性时,ServiceLocator
会从服务映射中返回对应的UserService
实例,从而实现了依赖注入。
属性委托的高级应用场景
- 多平台开发中的属性委托
- 在多平台开发(如Kotlin Multi - Platform)中,属性委托可以用于处理不同平台特定的属性逻辑。例如,在Android平台上,我们可能需要属性与Android系统的某些功能(如资源管理)进行交互;在iOS平台上,可能需要与iOS的特定框架进行交互。
- 假设我们有一个跨平台的类
AppSettings
,它有一个theme
属性,在Android上可能需要从资源文件中加载主题,在iOS上可能需要从特定的配置文件中加载主题。我们可以通过属性委托来实现:
// 通用的AppSettings类
expect class AppSettings {
var theme: String
}
// Android平台实现
actual class AppSettings actual constructor() {
private var _theme: String = "default_theme"
actual var theme: String by object {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
// 从Android资源文件中加载主题逻辑
return _theme
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
// 设置主题到Android资源文件逻辑
_theme = value
}
}
}
// iOS平台实现(伪代码示例,实际需与iOS框架交互)
actual class AppSettings actual constructor() {
private var _theme: String = "default_theme"
actual var theme: String by object {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
// 从iOS配置文件中加载主题逻辑
return _theme
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
// 设置主题到iOS配置文件逻辑
_theme = value
}
}
}
- 在上述代码中,
AppSettings
类在不同平台上通过属性委托实现了theme
属性的不同逻辑。expect
和actual
关键字是Kotlin Multi - Platform中的关键字,用于声明和实现跨平台的类型和成员。
- 动态属性解析
- 属性委托还可以用于实现动态属性解析。例如,在一个动态配置系统中,属性的名称和值可能在运行时才确定。我们可以通过属性委托来实现这种动态解析。
- 示例代码如下:
class DynamicProperties {
private val propertyMap: MutableMap<String, Any> = mutableMapOf()
operator fun getValue(thisRef: Any?, property: KProperty<*>): Any? {
return propertyMap[property.name]
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Any) {
propertyMap[property.name] = value
}
}
class MyDynamicObject {
var dynamicProperty: Any? by DynamicProperties()
}
fun main() {
val myObject = MyDynamicObject()
myObject.dynamicProperty = "Initial value"
println(myObject.dynamicProperty)
myObject.dynamicProperty = 42
println(myObject.dynamicProperty)
}
- 在上述代码中,
DynamicProperties
类通过getValue
和setValue
方法实现了动态属性的获取和设置。MyDynamicObject
类的dynamicProperty
属性使用DynamicProperties
委托。这样,dynamicProperty
的实际类型和值可以在运行时动态变化,实现了动态属性解析的功能。
通过以上对Kotlin属性委托的详细介绍,从基础概念到深入本质,再到各种应用场景和高级应用,相信读者对Kotlin属性委托有了全面且深入的理解,可以在实际项目中灵活运用这一强大特性。