Kotlin学习(六):对象和委托

Kotlin学习(六):对象和委托

对象

对象表达式

Java 中,有 个匿名类的概念,也就是在创建类时,无须指定类的名字。匿名类用于方法的参数类型。基本理念是方法参数需要接收一个类或接口的实例,而这个实例只是在该方法中临时用一下,并没有必要单独定义一个类,或单独创建一个对象变量。 因此,就在传入方法参数值的同时创建了类的实例。下面的代码演示了 Java 中匿名类的使用。

public class MyClass {
    
    
    public String name;

    public MyClass(String name) {
    
    
        this.name = name;
    }

    public void verify() {
    
    
        System.out.println("verify");
    }
}

public class Test {
    
    
    public static void process(MyClass myClass) {
    
    
        myClass.verify();
    }

    public static void main(String[] args) {
    
    
        // 参数类型是 MyClass , 这里创建了一个匿名的类,该类是 MyClass 的子类,并创建了该类的实例
        process(new MyClass("Mike") {
    
    
            @Override
            public void verify() {
    
    
                System.out.println(name + " verify ");  // 输出: Mike verify 
            }
        });
    }
}

Kotlin 中,也有类似的功能,但不是匿名类,而是对象。要想创建一个对象,需要是用 object 关键字,该对象要继承的类需要与 object 之间用 ( : ) 分隔,如 **MyClass ( “Mike” ) ** 。

open class MyClass(name: String) {
    
    
    open var name = name
    
    open fun verify() {
    
    
        println("verify")
    }
    
}

fun process(obj: MyClass) {
    
    
    obj.verify()
    // 判断 obj 是否是 MyInterface 的实例,如果是,则调用 closeData 方法
    if (obj is MyInterface) {
    
    
        obj.closeData()
    }
}

fun main(args: Array<String>) {
    
    
    //process 参数值是一个对象,该对象是 MyClass 匿名子类的实例,并在对象中重写 verify 函数
    process(object : MyClass("Mike") {
    
    
        override fun verify() {
    
    
            println("$name verify") // 输出: Mike verify 
        }
    })
}

对象和类一样,只能有一个父类,但可以实现多个接口。在下面的代码中,对象不仅继承了 MyClass 类,还实现了 MyInterface 接口,该接口的成员函数有一个默认实现。

open class MyClass(name: String) {
    
    
    open var name = name

    open fun verify() {
    
    
        println("verify")
    }
}

fun process(obj: MyClass) {
    
    
    obj.verify()
    // 判断 obj 是否是 MyInterface 的实例,如果是,则调用 closeData 方法
    if (obj is MyInterface) {
    
    
        obj.closeData()
    }
}

interface MyInterface {
    
    
    // 默认实现了 closeData 函数
    fun closeData() {
    
    
        println("closeData")
    }
}

fun main(args: Array<String>) {
    
    
    //对象和类一样,只能有一个父类,但可以实现多个接口。在下面的代码中,对象不仅继承了MyClass 类,还实现了MyInterface 接口,该接口的成员函数有一个默认实现。
    process(object : MyClass("Bill"), MyInterface {
    
    
        override fun verify() {
    
    
            println("$name verify ")
        }
    })
}

// 输出: 
// Mike verify
// closeData

如果只想建立一个对象,不想从任何类继承,也不实现任何接口,可以按下面的方式建立对象。

//如果只想建立一个对象,不想从任何类继承,也不实现任何接口,可以按下面的方式建立对象。
fun foo() {
    
    
    // 建立一个对象,该对象没有任何父类型
    var adHo = object {
    
    
        var x: Int = 0
        var y: Int = 0
    }
    println("x = ${
      
      adHo.x} , y = ${
      
      adHo.y}") // 输出:x = 0 , y = 0
}

声明匿名对象

匿名对象只能用在本地 ( 函数 ) 或 private 声明中。如果将匿名对象用于 public 函数的返回值,或 public 属性的类型,那么 Kotlin 编译器会将这些函数或属性的返回类型重新定义为匿名对象的父类型,如果匿名对象没有实现任何接口,也没有从任何类继承,那么父类型就是 Any 。因此,添加在匿名对象中的任何成员都将无法访问。

class MyClass(name: String) {
    
    
    
    // private 函数,返回类型是匿名对象本身,可以访问x
    private fun foo() = object {
    
    
        var x: String = "x"
    }

    //public 函数,由于匿名对象没有任何父类型,因此函数的返回类型是 Any
    fun publicFoo() = object {
    
    
        var x: String = "x"
    }

    fun bar() {
    
    
        // 可以访问
        var x1 = foo().x
        // 编译错误,因为 publicFoo public 方法,返回类型是 Any
       // var x2=publicFoo().x
    }
}

访问封闭作用域内的变量

Java 中,匿名对象访问封闭作用域内的变量,需要用 final 声明该变量,这也就意味着在匿名对象中无法修改封闭作用域内变量的值。在 Java 8 中,如果只是使用封闭作用域内的变量,该变量并需要使用 final ,但一旦修改变量的值,就必须使用 final 来声明变量。其实,在 Java 8 中,规则并没有改变,只是在使用变量时,封闭作用域内的变量是一个隐式的 final 变量,知道该变量被修改的那一刻,才要求使用 final 声明变量。

public class Test {
    
    
    public static void process(MyClass myClass) {
    
    
        myClass.verify();
    }

    public static void main(String[] args) {
    
    
        int n = 20; // 该变量对象当前封闭作用域内的匿名方法是 final 的
        // 参数类型是 MyClass , 这里创建了一个匿名的类,该类是 MyClass 的子类,并创建了该类的实例
        process(new MyClass("Mike") {
    
    
            @Override
            public void verify() {
    
    
                // 编译错误,n必须使用final,但使用final后,仍然会有编译错误,因为final变量的值不可以修改
                n = 30;
                if (n == 20) {
    
    
                    System.out.println("success");
                } else {
    
    
                    System.out.println("failed");
                }
            }
        });
    }
}

在这段代码中, process 函数参数类型是 MyClass 。调用 process 方法时使用匿名对象,井在匿名对象的实现函数 test 中访问了 main 函数作用域中的 变量。在这种情况下, test 函数中只能读取 的值(无论 是否使用 final 声明)。

如果这段代码用 Kotlin 实现,在匿名对象中就可以任意访问变量 n 了,包括修改 n 的值。

open class MyClass {
    
    
    open fun test() {
    
    

    }
}

fun process(obj: MyClass) {
    
    
    obj.test()
}

fun main(args: Array<String>) {
    
    
    var n = 20
    process(object : MyClass() {
    
    
        override fun test() {
    
    
            if (n == 20) {
    
    
                println("success")
                n = 30
            } else {
    
    
                println("failed")
            }
        }
    })
}

伴生对象

Kotlin 中并没有静态类成员的概念,但并不等于不能实现类似于静态类成员的功能。伴生对象**( Companion Objects ) **就是 Kotlin 用来解决这个问题的语法糖。

如果在 Kotlin 类中定义对象,那么就称这个对象为该类的伴生对象。伴生对象要使用companion 关键字声明。

class MyClass {
    
    
    companion object Factory {
    
    
        fun create(): MyClass = MyClass()
    }
}

该伴生对象的成员可通过只使用类名作为限定符来调用:

扫描二维码关注公众号,回复: 13379811 查看本文章
val instance = MyClass.create()

可以省略伴生对象的名称,在这种情况下将使用名称 Companion

class MyClass {
    
    
    companion object {
    
     }
}

val x = MyClass.Companion

其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象 (无论是否具名)的引用:

class MyClass1 {
    
    
    companion object Named {
    
     }
}

val x = MyClass1

class MyClass2 {
    
    
    companion object {
    
     }
}

val y = MyClass2

请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且,例如还可以实现接口:

interface Factory<T> {
    
    
    fun create(): T
}

class MyClass {
    
    
    companion object : Factory<MyClass> {
    
    
        override fun create(): MyClass = MyClass()
    }
}

val f: Factory<MyClass> = MyClass

当然,在 JVM 平台,如果使用 @JvmStatic 注解,你可以将伴生对象的成员生成为真正的静态方法和字段。

对象表达式和对象声明之间的语义差异

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

委托

委托 ( Delegate ) 其实是一种非常好的代码重用方式,有点类似 AOP ( 面向方面编程 ) ,也就是将在多个地方出现的代码放到同一个地方,以便被多个类和属性重用。

类的委托

委托模式己被实践证明为类继承模式之外的另一种很好的替代方案,Kotlin 直接支持委托模式, 因此用户不必再为了实现委托模式而手动编写那些 “无聊” 的重复代码。例如, Derived类可以继承 Base 接口,并将 Base 接口所有的 public 方法委托给一个指定的对象。

interface Base {
    
    
    fun print()
}

class BaseImpl(val x: Int) : Base {
    
    
    override fun print() {
    
     print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    
    
    val b = BaseImpl(10)
    Derived(b).print() //打印结果为:10
}

从这段代码可以看出,Derived 类使用 by 关键字将 Base 类的 print 函数 托给了一个对象。该对象需要通过 Derived 类的主构造器传入,而且该对象的类必须实现 Base 接口 。在本例中,该对象是 Baselmpl 类的实例。如果 Derived 类不进行委托,就需要再实现一遍 print函数。

属性委托

在实际应用中,有很多类属性的 gettersetter 函数的代码相同或类似,当然,从技术上来说,在每一个类中编写相同的代码是可行的,但这样就会造成代码的大量冗余,而且维护困难。为了解决这个问题,Kotlin 允许属性委托,也就是将属性的 gettersetter 函数的代码放到一个委托类中,如果在类中声明属性,只需要指定属性委托类。这样,相同的代码,就可以在同一个地方,大大减少代码的冗余,也让代码更容易维护。下面先看一个不使用属性委托的例子。

class MyClass1 {
    
    
    var name: String = ""
        get() :String {
    
    
            println("MyClass1.get 已经被调用")
            return field
        }
        set(value) {
    
    
            println("MyClass1.set 已经被调用")
            field = value
        }
}

class MyClass2 {
    
    
    var name: String = ""
        get() :String {
    
    
            println("MyClass2.get 已经被调用")
            return field
        }
        set(value) {
    
    
            println("MyClass2.set 已经被调用")
            field = value
        }
}

fun main(args: Array<String>) {
    
    
    var c1 = MyClass1()
    var c2 = MyClass2()
    c1.name = "Bill"
    c2.name = "Mike"
    println(c1.name)
    println(c2.name)
}
// 输出以下内容:
// MyClass1.set 已经被调用
// MyClass2.set 已经被调用
// MyClass1.get 已经被调用
// Bill
// MyClass2.get 已经被调用
// Mike

在这段代码中,有两个类 MyClass1MyClass2, 这两个类都有一个 name 属性,该属性的 gettersetter 函数的代码非常相似,因此产生了代码元余。 下面就用委托类解决这个问题。

所谓委托类,就是一个包含 getValuesetValue 函数的类。 这两个函数用 operator 声明。在使用委托类时, 需要用 by 关键字, 创建委托类实例的代码放在 by 后面,代码如下:

class Delegate {
    
    
    // 保存属性值的成员变量
    var name: String = ""

    // 调用委托属性的 getter 函数,会调用委托类的getValue函数
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
        // 获取thisRef指定的类名
        val className = thisRef.toString().substringBefore("@")
        println("${
      
      className}.get 已经被调用")
        return name
    }

    // 调用委托属性的 setter 函数,会调用委托类的 setValue 函数
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    
    
        // 获取thisRef指定的类名
        val className = thisRef.toString().substringBefore("@")
        println("${
      
      className}.set 已经被调用")
        name = value
    }
}

class MyClass1 {
    
    
    // 将 name 属性委托给 Delegate
    var name :String by Delegate()
}

class MyClass2 {
    
    
    // 将 name 属性委托给 Delegate
    var name: String by Delegate()
}

fun main(args: Array<String>) {
    
    
    var c1 = MyClass1()
    var c2 = MyClass2()
    c1.name = "Bill"
    c2.name = "Mike"
    println(c1.name)
    println(c2.name)
}

现在执行这段代码,与前面未使用委托类时输出的内容完全一样。现在即使有更多的拥有 name 属性的类,只要 name 属性的 gettersetter 函数的实现类似,都可以将 name 属性委托 Delegate 类。

委托类的初始化函数

如果委托类有主构造器,也可以向主构造器传入一个初始化函数。这时,可以定义一个委托函数的返回值是委托类,并在委托时指定初始化函数。下面的代码是基于上一小节代码的改进。在这段代码中 ,为委托类加了一个主构造器,并传入了一个初始化函数。初始化函数返回 String 类型的值,该值会在委托类中的 getValuesetValue 函数中调用.

class Delegate<T>(initializer: () -> T) {
    
    
    // 保存属性值的成员变量
    var name: String = ""

    var className = initializer()

    // 调用委托属性的 getter 函数,会调用委托类的getValue函数
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    
    
        println("${
      
      className}.get 已经被调用")
        return name
    }

    // 调用委托属性的 setter 函数,会调用委托类的 setValue 函数
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    
    
        println("${
      
      className}.set 已经被调用")
        name = value
    }

}

/**
 *  delegate 是委托泛型函数,可以适应任何 Kotlin 类型,该函数的返回值类型是泛型委托类
 *  函数体只有一行代码,就是传入初始化函数的泛型委托类的实例( Delegate (initializer) }
 *
 * @param T
 * @param initializer
 * @return
 */
fun <T> delegate(initializer: () -> T): Delegate<T> = Delegate(initializer)

class MyClass1 {
    
    
    // 将 name 属性委托给 Delegate
    var name: String by delegate {
    
    
        println("MyClass1.name初始化函数调用")
        "<MyClass1>"
    }
}

class MyClass2 {
    
    
    // 将 name 属性委托给 Delegate
    var name: String by delegate {
    
    
        println("MyClass2.name初始化函数调用")
        "<MyClass2>"
    }
}

fun main(args: Array<String>) {
    
    
    var c1 = MyClass1()
    var c2 = MyClass2()
    c1.name = "Bill"
    c2.name = "Mike"
    println(c1.name)
    println(c2.name)
}
// 输出结果:
// MyClass1.name初始化函数调用
// MyClass2.name初始化函数调用
// <MyClass1>.set 已经被调用
// <MyClass2>.set 已经被调用
// <MyClass1>.get 已经被调用
// Bill
// <MyClass2>.get 已经被调用
//Mike

属性委托要求

这里我们总结了委托对象的要求。

对于一个只读属性(即 val 声明的),委托必须提供一个操作符函数 getValue(),该函数具有以下参数:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。
  • property —— 必须是类型 KProperty<*> 或其超类型。

getValue() 必须返回与属性相同的类型(或其子类型)。

class Resource

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

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

对于一个可变属性(即 var 声明的),委托必须额外提供一个操作符函数 setValue(), 该函数具有以下参数:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是其超类型。
  • property —— 必须是类型 KProperty<*> 或其超类型。
  • value — 必须与属性类型相同(或者是其超类型)。
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
        }
    }
}

getValue() 或/与 setValue() 函数可以通过委托类的成员函数提供或者由扩展函数提供。 当你需要委托属性到原本未提供的这些函数的对象时后者会更便利。 两函数都需要用 operator 关键字来进行标记。

委托类可以实现 ReadOnlyPropertyReadWriteProperty 接口中的一个,前者只包含 getValue 函数,后者包含了 getValuesetValue 函数。这些接口被声明在 Kotlin 标准库中的kotlin.properties 包内 。这两个接口 定义代码如下:

public fun interface ReadOnlyProperty<in T, out V> {
    
    
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
    
    
    public override operator fun getValue(thisRef: T, property: KProperty<*>): V
    public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

标准委托

Kotlin 标准库中提供了一些委托函数,可以实现几种很有用的委托。

惰性装载

lazy 是一个函数,接受一个 Lambda 表达式作为参数(初始化函数,与前面实现的 delegate委托函数类似,返回一个 Lazy<T> 类型的实例,这个实例可以作为一个委托,实现惰性加载属性 ( lazy property ) :第一次调用 get() 时,将会执行从 lazy 函数传入的 Lambda 表达式,然后会记住这次执行的结果,以后所有对 get() 的调用都只会简单地返回以前记住的结果。

val lazyValue: String by lazy {
    
    
      // 该属性初始化函数的执行部分,只有第一次访问该属性时才会调用
      println("computed")
      "HELLO"
  }
fun main(args: Array<String>) {
    
    
  var testDelegate = TestDelegate()
  println(testDelegate.lazyValue)
  println(testDelegate.lazyValue)
}
// 输出结果:
// computed
// HELLO
// HELLO

默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用 LazyThreadSafetyMode.NONE 模式:它不会有任何线程安全的保证以及相关的开销。

可观察属性 Observable

所谓可观察属性就是当属性变化时可以拦截其变化。实现观察属性值变化的委托函数是Delegates.observable 。该函数接受两个参数,第1个是初始化值,第2个是属性值变化事件的响应器 ( handler)。每次我们向属性赋值时,响应器都会被调用(在属性赋值处理完成之后)。响应器函数有3个参数,被赋值的属性 (prop)、赋值前的旧属性值 (old),以及赋值后的新属性值 (new)

var name: String by Delegates.observable("Jack") {
    
     // "Jack"是name属性的初始值
          prop, old, new -> // 响应器函数的三个参数
      println("old = [${
      
      old}], new = [${
      
      new}]")
}
fun main(args: Array<String>) {
    
    
  var testDelegate = TestDelegate()
  println(testDelegate.lazyValue)
  println(testDelegate.lazyValue)
  testDelegate.name = "john"
  testDelegate.name = "Bill"
}
// 输出结果:
// old = [Jack], new = [john]
// old = [john], new = [Bill]

阻止属性的赋值操作

如果你希望能够拦截属性的赋值操作,并且还能够“否决”赋值操作,那么不要使用 observable 函数,而应该使用 vetoable 函数。传递给 vetoable 函数的事件响应器会返回一个布尔类型的值 如果返回 true 表示 许给属性赋值,如果返回 false ,就会否决属性的赋值(仍然保留原来的值)。

var age: Int by Delegates.vetoable(20) {
    
      // 20 是age属性的初始值
          prop, old, new ->
      println(" old = [${
      
      old}], new = [${
      
      new}]")
      var result = true
      if (new == 30) {
    
    
          result = false
          println("年龄不能为$new")
      }
      result
}
fun main(args: Array<String>) {
    
    
  var testDelegate = TestDelegate()
  testDelegate.age=35
  println("age = ${
      
      testDelegate.age}")
  testDelegate.age=30
  println("age = ${
      
      testDelegate.age}")
}
// 输出结果:
// old = [20], new = [35]
// age = 35
// old = [35], new = [30]
// 年龄不能为30
// age = 35

Map 委托

有一种常见的使用场景是将 Map 中的 key-value 映射到对象的属性中,这通常在解析 JSON数据时用到。下面的代码中有 User 类,该类有 nameage 两个属性。通过主构造器传入 一个 Map 对象,并将 Map 中的 key 的值映射到同名的属性中 。所使用的方法就是每一个需要映射的属性使用 by 指定 Map 作为自己的委托。

class User(var map: Map<String, Any>) {
    
    
    val name: String by map  // 将map作为name属性的委托
    val age: Int by map      // 将map作为age 属性的委托
}

fun main(args: Array<String>) {
    
    
    var map = mapOf(
        "name" to "Mike",
        "age" to 25
    )
    var user = User(map) // 将map中的 key-value 直接映射到User类的属性上
    println(" name = ${
      
      user.name} , age = ${
      
      user.age}")
}
// 输出: name = Mike , age = 25

我们可以看到,在 User 类中使用了 val 声明 nameage 属性,这就意味着这两个属性值是不可修改的,即使将 val 改成 var 也不行,因为 Map 只有 getValue 方法,而没有 setValue 方法, 所以 Map 只能通过 User 对象读取 nameage 属性值,而不能通过 User 对象设置 nameage 属性值。

如果要让 User 类的 nameage 属性映射到可读写的委托,需要将这两个属性委托给 MutableMap

class MutableUser(var map: MutableMap<String, Any>) {
    
    
    var name: String by map  // 将map作为name属性的委托
    var age: Int by map      // 将map作为age 属性的委托
}

fun main(args: Array<String>) {
    
    
    var map = mutableMapOf<String, Any>(
        "name" to "Jack",
        "age" to 30
    )
    var user = MutableUser(map)
    println("初始化值: name = ${
      
      user.name} , age = ${
      
      user.age}")
    user.name = "Bill"  // 修改user的值
    println("修改后: map = $map ")
    map["age"] = 18    // 修改map的值
    println("修改后值: name = ${
      
      user.name} , age = ${
      
      user.age}")
}
// 输出:
// 初始化值: name = Jack , age = 30
// 修改后: map = {name=Bill, age=30} 
// 修改后值: name = Bill , age = 18

局部委托属性

你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:

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

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

memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算。

小结

对象和委托是 Kotlin 中两个比较大的语法糖。对象相当于 Java 中的匿名对象,伴生对象也可以代替 Java 中的静态类成员使用。而委托的主要作用就是实现代码重用 ,有点类似 AOP(面向方面编程)。

猜你喜欢

转载自blog.csdn.net/u010812857/article/details/121520435