MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

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方法。

委托属性的原理剖析

  1. 属性访问的本质
    • Kotlin中的属性访问本质上是通过生成的getter和setter方法来实现的。对于一个普通属性var name: String,编译器会生成getName()setName(String value)方法(对于Java风格的命名规范,在Kotlin中实际使用namename = value这样的语法糖来调用这些方法)。
    • 当使用属性委托时,by关键字后面的对象需要提供特定的操作符函数,即getValuesetValue(对于可变属性)。这些函数负责实际的属性值获取和设置逻辑。
  2. getValue函数
    • getValue函数的定义格式为operator fun getValue(thisRef: Any?, property: KProperty<*>): T,其中thisRef是委托属性所在类的实例(如果属性是val类型且在对象字面量中使用,thisRef可能为null),property是描述委托属性的KProperty对象,通过它可以获取属性的名称等信息,返回值T就是属性的类型。
    • 例如,在前面的示例中,getValue函数根据传入的thisRefproperty生成一个描述性的字符串,作为属性p的值返回。
  3. setValue函数
    • setValue函数用于可变属性的赋值操作,其定义格式为operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T),参数thisRefpropertygetValue中的含义相同,value就是要赋给属性的值。
    • 在示例中,setValue函数只是打印出赋值的相关信息,实际应用中可以实现更复杂的逻辑,如数据验证、持久化等。

标准库中的属性委托

  1. 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来提高性能,此时不会进行同步操作。
  1. 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对象、旧值和新值。
  1. 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。

自定义属性委托

  1. 基本步骤
    • 要自定义属性委托,首先需要创建一个类,该类要提供getValuesetValue(对于可变属性)操作符函数。
    • 例如,我们创建一个用于实现可重置属性的委托:
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变量,getValuesetValue方法用于属性的访问和赋值,reset方法用于将属性值重置为初始值。MyData类中的data属性使用ResettableProperty委托,通过调用reset方法可以将data属性值重置。
  1. 泛型的应用
    • 在自定义属性委托中,经常会使用泛型来提高委托的通用性。如上面的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开发中的应用

  1. 视图绑定
    • 在Android开发中,视图绑定是一种常见的需求。以前,我们通常使用findViewById来获取视图对象,这种方式代码冗长且容易出错。使用属性委托可以简化这个过程。
    • 假设我们有一个简单的布局文件activity_main.xml
<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!"
        }
    }
}
  • 在上述代码中,textViewbutton属性使用lazy委托。这样在第一次访问这些属性时,才会调用findViewById方法获取视图对象,提高了初始化性能。同时,代码更加简洁,也减少了空指针异常的风险,因为lazy委托保证了属性在访问时一定是初始化好的。
  1. 数据绑定与MVVM架构
    • 在MVVM架构中,属性委托可以用于实现数据绑定。例如,我们可以创建一个视图模型(ViewModel)类,使用observable委托来监听数据的变化,并将变化通知给视图。
    • 首先定义一个简单的视图模型类:
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架构的理念。

属性委托与依赖注入

  1. 依赖注入的概念
    • 依赖注入(Dependency Injection,简称DI)是一种软件设计模式,它允许将对象所依赖的其他对象通过外部传入,而不是在对象内部创建。这样可以提高代码的可测试性、可维护性和可扩展性。
  2. 属性委托实现依赖注入
    • 我们可以使用属性委托来实现简单的依赖注入。例如,假设有一个服务接口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实例,从而实现了依赖注入。

属性委托的高级应用场景

  1. 多平台开发中的属性委托
    • 在多平台开发(如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属性的不同逻辑。expectactual关键字是Kotlin Multi - Platform中的关键字,用于声明和实现跨平台的类型和成员。
  1. 动态属性解析
    • 属性委托还可以用于实现动态属性解析。例如,在一个动态配置系统中,属性的名称和值可能在运行时才确定。我们可以通过属性委托来实现这种动态解析。
    • 示例代码如下:
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类通过getValuesetValue方法实现了动态属性的获取和设置。MyDynamicObject类的dynamicProperty属性使用DynamicProperties委托。这样,dynamicProperty的实际类型和值可以在运行时动态变化,实现了动态属性解析的功能。

通过以上对Kotlin属性委托的详细介绍,从基础概念到深入本质,再到各种应用场景和高级应用,相信读者对Kotlin属性委托有了全面且深入的理解,可以在实际项目中灵活运用这一强大特性。