Kotlin之‘by‘关键字的威力与应用

一、回忆一下JAVA的动态代理

Java 中的动态代理是一种非常强大的机制,它允许你在运行时创建代理对象,用于代表其他对象执行一些操作。通常用于实现AOP(面向切面编程)和处理横切关注点,例如日志记录、性能监控、事务管理等。Java 的动态代理主要使用了 java.lang.reflect 包中的 Proxy 类和 InvocationHandler 接口。

下面是一个简单的 Java 动态代理示例:

interface Calculator {
    int operator(int a, int b);

    void print(int result);
}

class CalculatorProxy implements InvocationHandler {
    private Calculator target;
    public CalculatorProxy(Calculator target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

class AddCalculator implements Calculator {

    @Override
    public int operator(int a, int b) {
        return a + b;
    }

    @Override
    public void print(int result) {
        System.out.println("a + b = " + result);
    }
}

class MultiplyCalculator implements Calculator {
    @Override
    public int operator(int a, int b) {
        return a * b;
    }

    @Override
    public void print(int result) {
        System.out.println("a * b = " + result);
    }
}

class Test{
    public static void main(String[] args) {
        // add
        Calculator addCalculator = new AddCalculator();
        Calculator addProxy = (Calculator) Proxy.newProxyInstance(Calculator.class.getClassLoader(), new Class[]{Calculator.class}, new CalculatorProxy(addCalculator));
        int add = addProxy.operator(2, 4);
        addProxy.print(add);

        // multiply
        Calculator multiplyCalculator = new MultiplyCalculator();
        Calculator multiplyProxy = (Calculator) Proxy.newProxyInstance(Calculator.class.getClassLoader(), new Class[]{Calculator.class}, new CalculatorProxy(multiplyCalculator));
        int multiply = multiplyProxy.operator(2, 4);
        multiplyProxy.print(multiply);
    }
}

这是一个典型的 Java 动态代理示例,用于对不同的计算器实现进行代理。在这个示例中,我们有以下几个关键组成部分:

  • 定义一个接口 Calculator,定义了两个方法 operatorprint
  • 有两个实现了 Calculator 接口的类,AddCalculatorMultiplyCalculator,分别实现了 operatorprint 方法。
  • 然后创建了一个 CalculatorProxy 类,它实现了 InvocationHandler 接口,该类的实例将用于代理 Calculator 接口的实现类。
  • main 方法中,你首先创建了真实的 Calculator 实例,然后使用 Proxy.newProxyInstance 方法创建了代理对象,将它绑定到 Calculator 接口上。
  • 通过代理对象调用 operator 方法,实际上会触发 CalculatorProxy 中的 invoke 方法,该方法中通过反射调用了真实对象的 operator 方法。
  • 同样,通过代理对象调用 print 方法也会触发 CalculatorProxy 中的 invoke 方法,该方法通过反射调用了真实对象的 print 方法。

这个示例演示了动态代理的基本原理,使我们可以在运行时动态地创建代理对象并在其中添加自定义逻辑。

在 Kotlin 中,代理模式的实现更加简洁,不需要涉及反射。那么接下来我们对比一下。

二、Kotlin 没有反射的动态代理

1、by 接口代理,提高代码清晰度和可读性

当使用 Kotlin 语言时,可以使用 Kotlin 标准库中的 kotlin.reflect 包提供的函数来实现动态代理,而不需要像 Java 那样使用反射。

首先,确保你的项目依赖了 Kotlin 标准库。接下来,这是一个 Kotlin 版本的示例,演示如何使用 Kotlin 的动态代理:

interface Calculator {
    fun operator(a: Int, b: Int): Int
    fun print(result: Int)
}

class CalculatorProxy(calculator: Calculator) : Calculator by calculator

class AddCalculator : Calculator {
    override fun operator(a: Int, b: Int): Int = a + b

    override fun print(result: Int) = println("a + b = $result")
}

class MultiplyCalculator : Calculator {
    override fun operator(a: Int, b: Int): Int = a * b

    override fun print(result: Int) = println("a * b = $result")
}


fun main() {
    var addProxy = CalculatorProxy(AddCalculator())
    addProxy.print(addProxy.operator(2, 4))

    var multiplyProxy = CalculatorProxy(MultiplyCalculator())
    multiplyProxy.print(multiplyProxy.operator(2, 4))
}

这段 Kotlin 代码展示了如何使用 by 关键字来实现接口代理,从而消除冗余的代码。我们具体来分析一下:

  • 首先,我们定义了一个 Calculator 接口,其中包含两个方法:operator 用于执行数学运算,print 用于打印结果。
  • 然后,我们创建了两个类 AddCalculatorMultiplyCalculator,它们分别实现了 Calculator 接口。这两个类分别执行加法和乘法运算,并在 print 方法中打印结果。
  • 接着,我们创建了一个 CalculatorProxy 类,它实现了 Calculator 接口,但并没有实际实现接口中的方法。相反,它通过 by 关键字将接口的实现委托给传入的 calculator 对象,这就是代理的关键。这意味着 CalculatorProxy 对象将调用 calculator 对象的方法来执行运算和打印。
  • 最后,在 main 函数中,我们创建了一个 AddCalculator 对象和一个 MultiplyCalculator 对象,并将它们分别传递给 CalculatorProxy 对象 addProxymultiplyProxy。然后,我们使用这些代理对象来执行数学运算并打印结果。

使用 by 关键字,我们可以在不实际实现接口方法的情况下创建代理对象,从而减少了冗余的代码,使代码更清晰易懂。这是 Kotlin 中非常有用的特性,特别是在实现接口时。

by 关键字在 Kotlin 中用于委托,它的主要作用是将接口的实现委托给另一个对象。在这里,我们将着重分析这个语法:

class CalculatorProxy(calculator: Calculator) : Calculator by calculator

by calculator:这是委托语法的关键部分。通过使用 by 关键字,我们告诉编译器将 Calculator 接口的实现委托给 calculator 参数所引用的对象。换句话说,所有 Calculator 接口中定义的方法将由 calculator 对象来实际执行。

这种语法非常有用,因为它允许我们创建代理对象,将接口的实现委托给其他对象,从而避免了在代理类中手动实现接口的所有方法。这简化了代码并提高了可维护性,特别是当我们需要实现一些接口但只关心其中的一部分方法时。

2、在 Kotlin 中,你可以在代理类中重写接口方法以覆盖委托对象的行为

当然我们也可以在 CalculatorProxy 中重写 operatorprint 方法,就会调用 CalculatorProxy 中的方法,而不是委托给 calculator 对象。

class CalculatorProxy(calculator: Calculator) : Calculator by calculator {
    override fun operator(a: Int, b: Int): Int {
        TODO("Not yet implemented")
    }

    override fun print(result: Int) {
        TODO("Not yet implemented")
    }
}

这是因为你在 CalculatorProxy 类中提供了对这些方法的自定义实现,覆盖了接口中的默认实现。

所以,如果你想让 CalculatorProxy 仍然委托给 calculator 对象,你应该避免重写这些方法,或者在实现中通过调用 calculator 对象来实现委托。如果你需要自定义行为,可以在调用 calculator 对象之前或之后添加自己的逻辑。

3、使用 by 关键字委托对象必须返回具体对象或表达式

另外,在 Kotlin 中,by 后面必须要返回一个具体的对象,也可以是表达式。比如:

class CalculatorProxy(calculator: Calculator) : Calculator by (if (BuildConfig.DEBUG) DebugCalculator()  else calculator)

在示例中,使用了 by 关键字委托给了一个具体的对象。委托对象在这种情况下是一个条件表达式,根据 BuildConfig.DEBUG 的值,它要么返回 DebugCalculator() 对象,要么返回 calculator 对象,在运行时返回一个具体的对象。我们根据调试模式的不同,它会将属性的存储和访问委托给不同的对象。这是一种有用的模式,用于在不同的环境下切换属性的实现。

4、Kotlin 中的代理模式仅支持接口类型

最后,Kotlin 中的代理模式仅支持接口类型。这是因为 Kotlin 的代理模式是通过接口委托实现的,它允许一个类实现一个接口并将接口的具体实现委托给另一个类。这种方式只能用于代理接口的方法调用,而不能用于代理普通类的方法。

在 Kotlin 中,代理通过 by 关键字来实现,只能委托给接口类型的对象。如果你想代理一个普通类的方法,通常需要使用其他模式,如装饰器模式。

在这里插入图片描述

三、属性委托

属性委托是 Kotlin 中一个强大的特性,它允许你将属性的 getter 和 setter 方法委托给其他对象,以实现复杂的行为和逻辑。属性委托可以用于简化代码、提高可维护性,并在某些情况下引入懒加载等优化。

Kotlin 标准库提供了一些内置的属性委托,例如 lazyobservablevetoable,同时你也可以创建自定义的属性委托。

以下是属性委托的一些重要概念和用法:

  1. 内置属性委托
    • lazy:用于实现懒加载,只有在首次访问属性时才会计算并初始化它。
    • observable:用于监视属性的变化,并在变化时执行自定义逻辑。
    • vetoable:类似于 observable,但允许你阻止属性的修改。
  2. 自定义属性委托:你可以创建自定义的属性委托,通过实现 getValuesetValue 方法来控制属性的行为。

接下来我们逐个解析一下。

1、lazy 属性

(1)使用 lazy 属性委托实现延迟初始化

使用 lazy 属性委托实现延迟初始化。这是一种非常有用的模式,特别是当你希望在属性被访问时才进行初始化时。以下是 lazy 的基本语法和执行顺序:

基本语法:

val propertyName: T by lazy {
    // 在这里执行初始化操作
    // 返回值将作为属性的值
}
  • propertyName:需要委托的属性的名称。
  • lazy 闭包:这个闭包会在首次访问属性时被调用,执行初始化操作。它返回的值将作为属性的值。

执行顺序:

  • 当首次访问属性时,lazy 闭包会被调用,执行初始化操作。
  • 闭包中的初始化操作只会在首次访问属性时执行一次,之后的访问直接返回已计算的值。

下面是一个使用 lazy 属性委托的示例:

val lazyValue: String by lazy {
    println("使用 lazy 属性委托实现延迟初始化")
    "Hello, Lazy!"
}

fun main() {
    println(lazyValue) // 第一次访问属性时计算并初始化,输出 "Computed!" 和 "Hello, Lazy!"
    println(lazyValue) // 第二次访问属性时直接返回已计算的值,输出 "Hello, Lazy!"
}

lazyValue 是一个延迟初始化的属性,它的值只有在首次访问时才会计算和初始化。这可以有效地节省计算资源,特别是当属性的初始化成本较高时。

main 函数中,首先访问了 lazyValue 属性,这导致了延迟初始化代码块的执行,输出了 “使用 lazy 属性委托实现延迟初始化” 和 “Hello, Lazy!”。然后,再次访问了相同的属性,但这次它直接返回了已计算的值,而不再执行初始化代码块。

使用 lazy 属性委托实现延迟初始化
Hello, Lazy!
Hello, Lazy!

这种模式非常有用,因为它允许你在需要时进行初始化,而不是在对象创建时,从而提高了性能和资源利用率。

(2)lazy 处理多线程初始化的问题

在 Kotlin 中,lazy 函数提供了多个重载,其中一个允许你选择 LazyThreadSafetyMode 以控制懒加载的线程安全性。LazyThreadSafetyMode 有三个选项:

  1. SYNCHRONIZED:这是最安全的选项,确保在多线程环境下只有一个线程初始化对象。它使用同步锁来实现线程安全。
  2. PUBLICATION:这个选项适用于在多个线程之间共享不可变数据。它不使用锁,因此初始化可能会在多个线程中同时执行,但最终只有一个线程的结果会被返回。
  3. NONE:这个选项不提供线程安全性保障,适用于单线程环境或者自己处理线程安全性。初始化可能会在多个线程中同时执行,不做任何同步。

lazy 函数为 Kotlin 标准包的内置函数:未指定模式则默认为 LazyThreadSafetyMode.SYNCHRONIZEDlazy 函数定义如下所示:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

下面是一个示例,演示如何使用 LazyThreadSafetyMode 控制懒加载的线程安全性:

val lazyValue: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    println("使用 lazy 属性委托实现延迟初始化")
    "Hello, Lazy!"
}

在上面的示例中,SYNCHRONIZED 模式确保只有一个线程可以执行初始化代码,从而提供了最高级别的线程安全性。你还可以选择其他模式,根据你的需求来平衡性能和线程安全性。

我们再看一个例子:线程安全的懒汉同步块式单例。线程安全的懒汉式单例模式可以确保只有在需要时才创建实例,并且多个线程同时请求实例时不会导致多次创建。

Java代码:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Kotlin代码:

class Singleton private constructor() {
    companion object {
        // 等同于 by lazy(LazyThreadSafetyMode.SYNCHRONIZED)
        val instance: Singleton by lazy {
            Singleton()
        }
    }
}

在这个代码中,我们使用了 by lazy 或者 LazyThreadSafetyMode.SYNCHRONIZED 参数来创建懒汉式单例。

  • by lazy 会确保在首次访问 instance 属性时执行初始化操作,而且只有一个线程能够初始化实例。
  • LazyThreadSafetyMode.SYNCHRONIZED 参数确保了线程安全性,即使在多线程环境下,只有一个线程能够执行初始化代码块。

因此,这个代码确保了在多线程环境中也能够正确创建单例实例,不会出现多次创建或竞态条件的问题。这是一种常见的线程安全的懒汉式单例实现方式。

总结,lazy 函数提供了多种方式来控制懒加载的线程安全性,使你能够根据应用程序的要求进行选择。

(3)Lazy 的工作原理是什么?

下面我们来看一下 Lazy 的工作原理,以及它的源码分析。

首先,Lazy 属性的创建通常使用 lazy { ... } 表达式,其中 { ... } 是一个 lambda 表达式,用于指定属性的初始化逻辑。例如:

val lazyValue: String by lazy {
    println("初始化操作") 
    "Hello, Lazy!"
}

在上述代码中,lazyValue 属性将在第一次访问时执行 lambda 表达式中的初始化操作,并缓存结果,之后的访问直接返回缓存的值。

Lazy 的核心是通过 LazyImpl 类来实现的。以下是 LazyImpl 类的简化版本,以便更好地理解它的工作原理:

// 简化版本伪代码
class XxxLazyImpl<out T>(initializer: () -> T) : Lazy<T> {
    private var initializer: (() -> T)? = initializer
    @Volatile
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            // 执行初始化操作
            val typedValue = initializer!!()
            _value = typedValue
            initializer = null
            return typedValue
        }
}

下面是对这段代码的详细分析:

  1. XxxLazyImpl 是一个泛型类,使用了一个泛型参数 T,表示属性的类型。
  2. initializer 是一个 lambda 表达式,用于指定属性的初始化逻辑。
  3. @Volatile private var _value: Any? 定义了一个 _value 变量,使用 @Volatile 标识,表示该变量是可见的并且可以在多线程环境下正确访问。_value 用于存储属性的值,初始值为 UNINITIALIZED_VALUE,表示属性尚未初始化。
  4. value 属性是 Lazy 接口要求实现的属性,用于获取属性的值。

下面是 value 属性的工作原理:

  • 首先,它检查 _value 是否等于 UNINITIALIZED_VALUE。如果不等于,说明属性已经初始化过了,直接将 _value 强制类型转换为 T 类型并返回。
  • 如果 _value 等于 UNINITIALIZED_VALUE,表示属性尚未初始化,那么它执行 initializer!!() 来执行属性的初始化逻辑,并将结果赋值给 _value
  • 接着,将 initializer 设置为 null,表示属性已经初始化,避免重复初始化。
  • 最后,它通过强制类型转换将 _value 强制转换为 T 类型,并返回该值。

这样,XxxLazyImpl 实现了属性的延迟初始化。当首次访问 value 属性时,会执行初始化操作,后续的访问将直接返回缓存的值,实现了属性的延迟初始化。

需要注意的是,这个简化的实现没有考虑线程安全性,因此在多线程环境下可能存在问题。实际的 Kotlin 标准库中的 Lazy 实现会考虑线程安全性,并提供不同的线程安全模式供选择。

2、Delegates

(1)observable 属性委托监视属性的变化

observable 属性委托允许你监视属性的变化,每当属性被修改时都会执行自定义的逻辑。以下是 observable 的基本语法和执行顺序:

基本语法:

var propertyName: T by Delegates.observable(initialValue) { property, oldValue, newValue ->
    // 在这里执行属性变化时的操作
    // 可以通知监听器,执行特定的操作等
}
  • propertyName:需要委托的属性的名称。
  • initialValue:属性的初始值。
  • observable 闭包:这个闭包会在属性值发生变化时被调用,它接受三个参数:
    • property:属性的元数据,通常在 lambda 中不需要使用。
    • oldValue:当前属性的旧值。
    • newValue:新设置的属性值。

执行顺序:

  • 当首次访问属性时,属性的值会被初始化为 initialValue
  • 当属性的值即将被修改时,observable 闭包会被调用,你可以在这个闭包中执行特定的操作,例如通知监听器。
  • observable 闭包中,你可以访问属性的旧值(oldValue)和新值(newValue),并在属性变化时执行相应的操作。
  • observable 委托属性的修改不会被拦截,而是允许属性的变化。它主要用于在属性变化时通知其他部分代码,例如更新用户界面。

下面是一个示例,演示了如何使用 observable 属性委托:

var name: String by Delegates.observable("outa") { _, oldName, newName ->
    println("Name changed from $oldName to $newName")
}

fun main() {
    name = "danke"
    name = "Katrina"
}

在上面的示例中,我们定义了一个 name 属性使用 observable 属性委托。observable 接受两个参数:

  • 初始值(在这里是 “outa”)。
  • 一个 lambda 表达式,用于定义属性变化时的逻辑。这个 lambda 接收三个参数:
    • property:属性对象本身(在这里是 name)。
    • oldValue:修改前的旧值。
    • newValue:修改后的新值。

name 属性被修改时,observable 会执行 lambda 表达式内的逻辑,输出属性的变化情况。

main 函数中,我们修改了 name 属性的值,这导致了监视逻辑的执行。输出将是:

Name changed from outa to danke
Name changed from danke to Katrina

这样,就可以使用 observable 属性委托来监视属性的变化,以执行自定义的逻辑,这在许多应用中非常有用,例如在属性修改时更新 UI 或记录日志。

observable 属性委托和 Android Architecture Components 中的 ViewModel 有一些相似之处,但它们是用于不同的目的和场景的。

总结来说,observable 属性委托是一种通用的 Kotlin 特性,用于监视属性的变化,但在 Android 中需要额外的代码生成工具和手动管理属性变化的通知。LiveData 是专门用于 Android 的生命周期感知、自动通知和线程安全的数据观察机制,它更适合用于处理界面数据的变化和通知 UI 更新。通常情况下,建议在 Android 应用中使用 LiveData 来处理数据通知,而在普通 Kotlin 类中使用 observable 属性委托来监视属性的变化。

(2)vetoable 类似于 observable,但允许你阻止属性的修改

vetoable 是 Kotlin 标准库中的一个属性委托,它用于在属性值即将发生变化时执行自定义的验证或拦截操作。如果验证或拦截操作返回 false,则属性的变化会被取消,新的值不会被接受。以下是 vetoable 的基本语法和执行顺序:

基本语法:

var propertyName: T by vetoable(initialValue) { property, oldValue, newValue ->
    // 在这里执行验证或拦截操作
    // 如果返回 true,新值将被接受;如果返回 false,新值将被取消
}
  • propertyName:需要委托的属性的名称。
  • initialValue:属性的初始值。
  • vetoable 闭包:这个闭包会在属性值即将发生变化时被调用,它接受三个参数:
    • property:属性的元数据,通常在 lambda 中不需要使用。
    • oldValue:当前属性的旧值。
    • newValue:即将设置的新值。

执行顺序:

  • 当首次访问属性时,属性的值会被初始化为 initialValue
  • 当属性的值即将被修改时,vetoable 闭包会被调用,你可以在这个闭包中执行自定义的验证或拦截操作。
  • 如果 vetoable 闭包返回 true,新值将被接受,属性值被修改。
  • 如果 vetoable 闭包返回 false,新值将被取消,属性保持原来的值。

下面是一个示例,演示了如何使用 vetoable 属性委托来验证年龄是否在有效范围内:

var age: Int by Delegates.vetoable(0) {_, _, newAge ->
    if (newAge >= 0 && newAge <= 120) {
        true // 验证通过,接受新值
    } else {
        false // 验证失败,取消新值
    }
}

fun main() {
    println(age) // 输出 0,初始值
    age = 130
    println(age) // 输出 0,新值被取消
    age = 18
    println(age) // 输出 18,新值被接受
}

在上述示例中,age 属性使用 vetoable 委托,它会验证新值是否在有效范围内(0 到 120 岁)。如果新值不在范围内,验证失败,新值被取消;否则,验证通过,新值被接受。

vetoable 通常用于属性的验证,以确保属性值满足特定条件,如果不满足条件,可以取消属性的变化。例如,你可以使用 vetoable 来验证用户的输入是否有效,以防止无效的输入值被设置到属性中。

(3)notNull 确保属性在被访问之前已经被初始化

在 Kotlin 中,Delegates.notNull() 是一种属性委托,用于确保属性在被访问之前已经被初始化。这可以用于避免属性在未初始化的情况下引发空指针异常。

Delegates.notNull() 的基本语法如下:

var propertyName: Type by Delegates.notNull()
  • propertyName 是属性的名称。
  • Type 是属性的类型。

使用 Delegates.notNull() 的属性必须是可变属性(var)。

var name : String by Delegates.notNull<String>()

fun main() {
    println(name) // IllegalStateException: Property name should be initialized before get.
    name = "danke"
    println(name)
}

在上述示例中,name 属性使用 Delegates.notNull() 委托,但在访问该属性之前,必须初始化它。这样可以确保属性在被访问之前已经被赋值,避免了空指针异常。

Delegates.notNull() 适用于那些必须在使用前初始化的属性,它是一种有效的方式来确保属性不会在未初始化的情况下被访问。但请注意,它不是 Kotlin 推荐的最佳实践,通常情况下,更好的方式是在构造函数中初始化属性或者使用默认值来避免空指针异常。 Delegates.notNull() 应该被谨慎使用,因为它可能使代码更加复杂。

3、自定义属性委托

自定义属性委托是一种强大的 Kotlin 特性,允许你自定义属性的访问和修改行为。通过创建自定义属性委托,你可以在属性访问和修改时执行自定义的逻辑。

实现 ReadOnlyPropertyReadWriteProperty 接口的方法,具体取决于属性是只读还是可读写。

下面是如何创建和使用自定义属性委托的示例:

// 创建一个自定义属性委托
class MyDelegate : ReadWriteProperty<Any?, String> {
    private var storedValue: String = ""

    // 获取属性值的逻辑
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return storedValue
    }

    // 设置属性值的逻辑
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        // 在设置属性值之前,可以执行自定义的逻辑
        println("Setting value: $value")
        storedValue = value
    }
}

var delegate by MyDelegate()

fun main() {
    // 访问属性会触发自定义的 getValue 逻辑
    println(delegate) // 输出 ""

    // 修改属性会触发自定义的 setValue 逻辑
    delegate = "danke" // 输出 "Setting value: danke"

    // 再次访问属性会触发自定义的 getValue 逻辑
    println(delegate) // 输出 "danke"

}

在上述示例中,我们创建了一个名为 MyDelegate 的自定义属性委托,它实现了 ReadWriteProperty 接口,需要提供 getValuesetValue 方法。

  • getValue 方法用于获取属性值,你可以在其中定义自己的逻辑。
  • setValue 方法用于设置属性值,你也可以在其中执行自定义的逻辑。

然后,我们使用 MyDelegate 委托 delegate 属性。当访问和修改 delegate 属性时,会触发自定义的 getValuesetValue 逻辑。

这种方式允许你在属性的访问和修改过程中执行自定义操作,例如执行验证、日志记录或其他业务逻辑。自定义属性委托是 Kotlin 中非常强大的特性,可以帮助你编写更具可维护性和可重用性的代码。

4、代理其他属性

在 Kotlin 中,双冒号 :: 是一种非常强大和灵活的语法,用于引用属性、函数或构造函数。它可以用于不同的上下文中,包括属性和方法的代理。双冒号 :: 引用函数,在之前的文章中已经介绍过了,我们现在来看一下用双冒号 :: 来引用代理。

下面是使用双冒号 :: 语法来代理属性的示例:

data class Person(var name: String)

fun main() {
    val person = Person("danke")
    var name: String by person::name
    println("name: $name")
    person.name = "蛋壳"
    println("update name: $name")
}

在上述示例中,我们定义了一个 Person 数据类,其中包含一个可变属性 name。然后,再创建了一个名为 personPerson 对象,并使用属性委托将 name 属性委托给了 person::name,这意味着 name 属性的读取和写入操作会被委托给 person 对象的 name 属性。

我们打印一下输出:

name: danke
update name: 蛋壳

总结,属性委托允许你将属性的读取和写入操作委托给其他对象,这可以用于实现各种有趣的行为,例如在访问属性时执行自定义逻辑或者在属性值变化时触发其他操作。

5、代理Map

代理 Map 是 Kotlin 中一个非常强大的特性,它允许你通过委托属性将 Map 中的键值对映射到类的属性上。这在处理配置、动态数据、解析 JSON 等场景中非常有用。让我们看一个例子:

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

fun main() {
    val map: MutableMap<String, Any?> = mutableMapOf(
        "name" to "danke",
        "age" to 18
    )

    val person = Person(map)
    println("Name: ${person.name}")
    println("Age: ${person.age}")

    // 修改映射中的值,会自动反映到属性上
    map["name"] = "蛋壳"
    map["age"] = 20

    println("Name: ${person.name}")
    println("Age: ${person.age}")
}

在上面的示例中,我们定义了一个 Person 类,该类接受一个 MutableMap 作为构造函数参数。然后,我们在 Person 类中定义了两个属性 nameage,这些属性使用了 Map 作为委托,即通过 by map 实现。这意味着 Person 对象的 nameage属性的值将从传入的 Map 中获取。

main 函数中,我们首先创建了一个 map,其中包含了初始的键值对,然后创建了一个 person 对象,并将 map 传递给它。当我们修改了 map 中的值,这些变化也会自动反映到 person 对象的属性上。

这个示例展示了如何使用 Map 作为属性委托,将 Map 中的键值对映射到对象的属性上,从而使数据的访问变得非常方便。这在处理动态数据或配置时非常实用。

四、总结

最后我们总结一下 Kotlin 中的代理特点:

  1. 接口代理: Kotlin 提供了一种机制,使用 by 关键字可以将接口的实现委托给另一个对象。这有助于提高代码的清晰度和可读性。

  2. by 关键字: 在代理模式中,by 关键字用于指定代理对象,它必须返回一个具体的对象或表达式。

  3. 属性委托基础: Kotlin 中的属性委托是一种用于延迟初始化、监视属性变化等目的的机制。它通过 by 关键字将属性的实际操作委托给另一个对象。

  4. 代理模式仅支持接口类型: 在 Kotlin 中,代理模式主要用于接口类型。

  5. Lazy 委托: lazy 属性委托用于延迟初始化属性,只有在首次访问属性时才会执行初始化操作。它的初始化操作可以是一个闭包。

  6. 线程安全性: 在多线程环境下,需要考虑属性委托的线程安全性。Kotlin 提供了不同的线程安全模式,如 LazyThreadSafetyMode,以满足不同需求。

  7. Delegates 相关内容:

    • Observable 委托: observable 属性委托允许监视属性的变化,当属性的值发生变化时,可以触发额外的操作。
    • Vetoable 委托: vetoable 属性委托类似于 observable,但允许在属性修改前取消修改操作,可以用于属性值的校验和过滤。
    • NotNull 委托: notNull 属性委托确保属性在被访问之前已经被初始化,避免了空指针异常。
  8. 自定义属性委托: Kotlin 允许开发者自定义属性委托,以满足特定需求。这可以通过实现 ReadWritePropertyReadOnlyProperty 接口来实现。

  9. 代理其他属性: Kotlin 支持使用 by 关键字将一个属性的操作代理给另一个属性,这在代码重用和可读性方面非常有用。

  10. 代理 Map: Kotlin 内置支持将属性的操作代理给 Map 对象,这在动态操作属性和解析 JSON 数据时非常有用。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值