Kotlin读书笔记之面向对象

141 阅读7分钟

一. 对象与类

1.1. 对象和单例

Kotlin直接提供了对单例的支持,消除了实现模式的负担以及出错的风险。

1.1.1. 带有对象表达式的匿名对象

kotlin的对象表达式类似于js对象和c#中的匿名类型;kotlin中对象表达式是后跟一块{}的关键字object。

fun draw() {
    val temp = object {
        val x = 10
        val y = 20
        val radius = 50
    }
    println("x: ${temp.x}; y:${temp.y}; radius:${temp.radius}")
}

匿名对象使用限制:

  • 匿名对象的内部类型不能作为函数或方法的返回类型
  • 匿名对象的内部类型不能用作函数或方法的参数类型
  • 如果它们作为属性存储在类中,它们将被视作Any类型,它们的任何属性或者方法都将无法直接访问

匿名对象可以作为接口的实现者,这里和Java类型,匿名内部类通常动态的实现接口。

fun createRunnable(): Runnable {
    val runnable = object: Runnable {
        override fun run() {
            println("hello world")
        }
    }
    return runnable
}

// 简写,如果匿名内部类实现单个抽象方法接口,可以直接实现可不需要置顶方法名
fun createRunnable(): Runnable = Runnable { println("hello world") }

匿名内部类实现多个接口,就必须指定实例在返回时应该表示的类型,如下:

fun createRunnable(): Runnable = object: Runnable, AutoCloseable {
    override fun run() {
        TODO("Not yet implemented")
    }
    override fun close() {
        TODO("Not yet implemented")
    }
}

1.1.2. 带有对象声明的单例

如果object关键字和块{}之间放置一个名称,那么kotlin将认为定义是语句或声明,而不是表达式。

使用一个对象表达式来创建一个匿名内部类的实例,使用一个对象声明来创建一个单例(一个只有单例实例的类)。

Unit是kotlin中一个单例,也可以通过object关键字创建一个单例

object Util {
    // 获取核心线程数 
    fun numberOfProcessors() = Runtime.getRuntime().availableProcessors()
}

// 调用
Util.numberOfProcessors()

单例并不局限于拥有方法,也可以有属性(val/var)。对象声明可以实现接口,也可以从已有的类扩展,就像对象表达式;另外单例具有基类或接口,那么该单例实例可以赋给引用或者传递给基类的参数。

object Sun: Runnable {
    const val radiusInKm = 696000
    val coreTemperatureInC = 15000000
    var a = true
    override fun run() {
        println("spin......")
    }
}

fun moveIt(runnable: Runnable) {
    runnable.run()
}

fun main() {
    println(Sun.radiusInKm) // 696000
    println(Sun.a) // true
    Sun.a = false
    println(Sun.a) // false
    moveIt(Sun) // spin......
}

1.1.3. 顶级函数于单例

单例使用还可以运用于在包中函数组合和导出。例如一些函数之间关系比其他函数更为密切,,可以将其放入单例中;此外还有一些函数存在依赖关系,最好也放在一个单例中。

1.2. 创建类

kotlin将很多模版放到了Jvm中,减少Java中很多模板代码。

类最简单的语法 -- class关键字后面跟着类名:

class Person

这种类没有提供任何属性、状态或行为,所以这种类不会做任何事情。

kotlin的只读属性相当于是Java中final描述的属性,只支持读取无法修改。

// year是只读属性
class Person(val year: Int, var name: String)

kotlin创建对象不需要Java使用new的

val person = Person(10, "张三")

1.2.1. 构造函数

kotlin的构造函数分为:主构造函数和次级构造函数。主构造函数可以有一个,次级构造函数可以有一个或者多个。

主构造函数用于初始化类,在类标题中声明,例子:

class Person(val name: String, val age: Int)
// 当constructor关键字没有注解和可见性修饰符作用于它时,constructor关键字可以省略。
class Person constructor(val name: String, val age: Int)

次级构造函数可以创建一个或者多个,并且使用construction关键字创建次级构造函数。例子:

class Person constructor(val name: String) {
    var age: Int = 0
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
    constructor(sex: Boolean) : this("Tom", if (sex) 10 else 20)
    override fun toString() = "$name => $age"
}

fun main() {
    val person1 = Person("Tom", 9)
    val person2 = Person(true)
    println(person1) // Tom => 9
    println(person2) // Tom => 10
}

1.2.2. 控制对属性的修改

kotlin在Jvm中会自动添加属性的getter和setter方法,如果需要自动set和get方法可以如下这样:

class Person(val year: Int, name: String) {
    var name = name
        set(value) {
            if (value.isBlank()) {
                throw RuntimeException("not empty, please")
            }
            field = value
        }
        get() {
            return "名字: $field"
        }
}

fun main() {
    val person = Person(10, "张三")
    println(person.name)
}

1.2.3. 访问修饰符

kotlin中类的属性和方法默认是publish,其他访问修饰符还有:private、protected和internal;public和private与Java中的一致,protected是允许派生类的方法访问该属性,internal允许同一个模块的任何代码访问属性或者方法,值得注意的是internal修饰符没有直接的字节码表示,它有kotin编译器使用一些命名约定来处理,而不会带来任何运行时开销。

下面看一下如何将类属性的setter/getter方法设置为private

class Car(val name: String, age: Int) {
    var age = age
        private set
}

fun main() {
    val car = Car("BM", 10)
    car.age = 10 // Cannot assign to 'age': the setter is private in 'Car'
}

1.2.4. 初始化代码

主构造函数声明是第一行的一部分。参数和属性在构造函数的参数列表中定义;不通过构造函数参数传递的属性也可以在类中定义;如果初始化对象的代码比只设置值复杂,就需要用到init块。

kotlin中一个类可以有零个或多个init块,这些块作为主构造函数的一部分来执行。init块的执行顺序是自上而下的。例子:

class Person() {
    private var age: Int = 10
    constructor(name: String, age: Int):this() {
        println("constructor")
        this.age = age
    }
    init { println("Person init 2,gender:${age}") }
    init { println("Person init 1") }
}



fun main() {
    val person = Person("Tom", 9)
    println(person)
}

// 输出
Person init 2,gender:10
Person init 1
constructor
com.example.one.Person@4f023edb

Kotlin中的init代码块就相当于Java中的普通代码块,在创建对象的时候代码块会先执行。注意是每次创建都会执行一遍。

1.3. 伴生对象和类成员

伴生对象是在类中定义的单例,它们是类的单例伙伴,伴生对象可以实现接口,也可以从基类扩展,因此在代码重用方面也很有用。

注意:伴生对象并不是我们所想象的Java中的静态对象那样。

1.3.1. 类级别成员

下面演示下如何在类上使用伴生对象添加一个属性和方法:

class MachineOperator(val name: String) {
    
    fun checkin() = checkedIn++
    fun checkout() = checkedIn--
    
    companion object {
        var checkedIn = 0
        fun minimumBreak() = "15 minutes every 2 hours"
    }
}

fun main() {
    MachineOperator("Hello").checkin()
    println(MachineOperator.minimumBreak()) // 15 minutes every 2 hours
    println(MachineOperator.checkedIn) // 1
}

注意:在伴生对象中放置可变属性可能会导致多线程环境中线程安全问题

1.3.2. 访问同伴

有时候需要一个对伴生对象的引用,而不是它的成员。例如在希望将该单例实例传递给需要接口实现的函数或方法。

val companion = MachineOperator.Companion

或者也可以给伴生对象添加一个有意义的名称再去获取

class MachineOperator(val name: String) {

    fun checkin() = checkedIn++
    fun checkout() = checkedIn--

    companion object MachineOperatorFactory {
        var checkedIn = 0
        fun minimumBreak() = "15 minutes every 2 hours"
    }
}

val companion = MachineOperator.MachineOperatorFactory

1.3.3. Companion作为Factory

类的伴生对象可以充当工厂,可以提供一个私用的构造方法,具体如下:

class MachineOperator private constructor(val name: String) {

    fun checkin() = checkedIn++
    fun checkout() = checkedIn--
    
    companion object {
        var checkedIn = 0
        fun create(name: String): MachineOperator {
            val operator = MachineOperator(name)
            operator.checkin()
            return operator
        }
    }
}

fun main() {
    val operator = MachineOperator.create("哈哈哈")
}

这里还有一个注意点,伴生对象的init方法和对象中init执行顺序是怎么样?例子:

class Person() {
    private var age: Int = 10
    constructor(name: String, age: Int):this() {
        println("constructor")
        this.age = age
    }
    init { println("Person init 2,gender:${age}") }
    init { println("Person init 1") }
    companion object {
        val instance by lazy { Person() }
      	// val instance = Person()
        init { println("companion init 1") }
        init { println("companion init 2") }
    }
}

fun main() {
    val person = Person.instance
    println(person)
}

// 输出
companion init 1
companion init 2
Person init 2,gender:10
Person init 1
com.example.one.Person@eed1f14

在使用lazy和不使用lazy初始化顺序是不一致的。这里只需要记住,拥有伴生对象是优先执行伴生对象的代码的,lazy是惰性加载的(后面会介绍)只有在使用的时候才加载。

1.4. 泛型类

泛型可以很好的抽象一个事物模型,在kotlin中创建一个泛型类很简单,下面看一个例子:

class PriorityPair<T: Comparable<T>>(member1: T, member2: T) {
    val first: T
    val second: T

    init {
        if (member1 >= member2) {
            first = member1
            second = member2
        } else {
            first = member2
            second = member1
        }
    }

    override fun toString() = "$first $second"
}

fun main() {
    val priorityPair = PriorityPair(2, 1)
    val priorityPair1 = PriorityPair('A', 'B')
    println(priorityPair) // 2 1
    println(priorityPair1) // B A
}

1.5. 数据类

kotlin的数据类是专用类,主要用于承载数据而不是行为。

主构造函数需要使用val或者var定义至少一个属性,这里不允许Non-val或var参数,如果需要可以在body{}中向类添加其他的属性或方法。

kotlin将自动创建equals/hashCode/toString方法,此外它还提供了一个copy方法来复制实例,同时为select属性提供更新的值。

kotlin还创建了以单词component开头的特殊方法,用来访问通过主构造函数定义的属性。

// 定义数据类
data class Example(val id: Int, val name: String, val completed: Boolean)
// 使用
val example = Example(1, "Tom", true)
println(example.toString())
val example1 = example.copy(id = 2)
println(example.toString())

数据类重写相关方法

data class Example(val id: Int, val name: String, val completed: Boolean) {
    override fun toString(): String {
        return "id:$id, name:$name, completed: $completed"
    }
}

属性解构

// 给数据类赋值    
val example = Example(1, "Tom", true)
// 普通获取属性
val id = example.id
val name = example.name
val completed = example.completed
// 类用componentN方法根据构造方法参数顺序进行解构,参数名称可以重新定义,不需要的参数可以使用下划线进行忽略
val (ids, _, completed) = example
println("id: $ids, completed: $completed")

注意:对应数据类而言构造函数参数顺序和解构顺序高度依赖,所以增减属性对于解构顺序都有影响

推荐使用数据类的场景:

  • 在给数据建模,而不是行为
  • 希望生成equals/hashCode/toString/copy
  • 主构造函数至少接受一个属性是有意义的,数据类不允许使用无参数的构造函数
  • 主构造函数只接受属性是有意义的
  • 希望使用解构工具轻松的从对象中提取数据

二. 类层次结构和继承

2.1. 接口

kotlin的接口和Java的接口类似,但是语义语法上有很大的不同:

  • kotlin的接口也可以有静态方法,但是只能通过在接口中编写的伴生对象实现
  • kotlin的接口中可以有方法
  • kotlin的接口可以有属性

下面看一下具体的例子:

interface Remote {
    fun up()
    fun down()
    // 默认方法
    fun doubleUp() {
        up()
        up()
    }
}

接下来我们创建一个类去实现接口

class TV {
    var volume = 0
}

class TVRemote(val tv: TV): Remote {
    override fun up() { tv.volume++ }
    override fun down() { tv.volume-- }
}

fun main() {
    val tvRemote = TVRemote(TV())
    tvRemote.up()
    println(tvRemote.tv.volume) // 1
    tvRemote.doubleUp()
    println(tvRemote.tv.volume) // 3
}

最后看一下接口中的静态方法:

interface Remote {
    fun up()
    fun down()
    fun doubleUp() {
        up()
        up()
    }
    companion object {
        fun combine(first: Remote, second: Remote): Remote = object: Remote {
            override fun up() {
                first.up()
                second.up()
            }
            override fun down() {
                first.down()
                second.down()
            }
        }
    }
}

fun main() {
    val fists = TVRemote(TV())
    val second = TVRemote(TV())
    val remote = Remote.combine(fists, second)
    remote.doubleUp()
    println("${fists.tv.volume} ${second.tv.volume}") // 2 2
}

最看看一下接口中的属性

interface People {
    val email: String
    val nickName: String
        get() = email.substringBefore("@")
    fun send(): String
}

class Superman(override val email: String, override val nickName: String) : People {
    override fun send() = "send email: $nickName -> $email"
}

class SuperWoman(override val email: String): People {
    override val nickName: String
        get() = email.substringBefore("@") + "->"

    override fun send() = "send email: $nickName -> $email"
}

2.2. 抽象类

kotlin的抽象类和Java一样也是通过关键字abstract标注,抽象方法必须在抽象类中标注为abstract

abstract class Musician(val name: String, val activeForm: Int) {
    abstract fun instrumentType(): String
}

class Cellist(name: String, activeForm: Int): Musician(name, activeForm) {
    override fun instrumentType() = "Hello world"
}

fun main() {
    val cellist = Cellist("Tom", 2022)
    println(cellist.instrumentType())
}

接口和抽象类的区别:

  • 在接口中定义的属性没有幕后字段,它们必须依赖抽象方法从实现类中得到属性,并传递给基类
  • 可以实现多个接口,但最多可以从一个类(抽象和非抽象)扩展

2.3. 嵌套类和内部类

将上面的那个Remote接口相关的实现用内部类实现

interface Remote {
    fun up()
    fun down()
    fun doubleUp() {
        up()
        up()
    }
}

class Tv {
    private var volume = 0

    val remote: Remote
        get() = TvRemote()

    override fun toString() = "Volume: $volume"

    inner class TvRemote: Remote {
        override fun up() { volume++ }
        override fun down() { volume-- }
        // this@Tv可以将内部指向TvRemote指向Tv
        // override fun toString() = "Remote: ${this@Tv.toString()}"
        override fun toString() = "Remote: ${super@Tv.toString()}"
    }
}

fun main() {
    val remote = Tv().remote
    remote.doubleUp()
    println(remote) // Remote: Volume: 2
    remote.doubleUp()
    println(remote) // Remote: Volume: 4
}

内部类使用关键字inner标识就可以,这里需要主要两个语法:

  • this@Tv:如果内部类中的属性或方法隐藏了外部类中的对应成员,那么就可以使用this@Tv从内部类的方法访问外部类的成员
  • super@Tv:如果内部类的方法想访问Any,也就是绕过类访问它的基类,但是像这种方法破坏了多态性和方法重写。

上面的内部类可以用匿名内部类实现:

class Tv {
    private var volume = 0

    val remote: Remote
        get() = object: Remote {
            override fun up() { volume++ }
            override fun down() { volume-- }
            override fun toString() = "Remote: ${this@Tv}"
        }

    override fun toString() = "Volume: $volume"
}

2.4. 继承

kotlin中类默认是final,它不支持继承,只能标记为open,只有开放类的开放方法可以在派生类中重写,并且需要标记为override,重写方法可以标记为final override,以防字类进行进一步重写该方法。

注意:基类的val属性可以用派生类的val/var重写,但是基类的var属性只能使用派生类中的var重写。这个限制是因为val只有一个getter,可以通过重写var的派生类中添加一个setter。

先看一个例子:

// year是不能被任何派生类中重写
// color的open是允许派生类重写
open class Vehicle(val year: Int, open var color: String) {
    // km被open标识是可以被重写
    open val km = 0
    // 	toString和repaint也不允许被重写,因为没有被open标识
    final override fun toString() = "year: $year, color: $color, km: $km"
    fun repaint(newColor: String) { color = newColor }
}

接着实现一个派生类

class Car(year: Int, color: String): Vehicle(year, color) {
    override var km: Int = 0
        set(value) {
            if (value < 1) {
                throw RuntimeException("can not set negative value")
            }
            field = value
        }
    override var color: String
        get() = super.color
        set(value) {
            if (value.isEmpty()) {
                throw RuntimeException("color required")
            }
            super.color = value
        }
    fun drive(distance: Int) { km += distance }
}

这里的kotlin不区分implements和extends,只有继承。

2.5. 密封类

sealed类是一种同时拥有枚举类 enum 和 普通类 class 特性的类,叫做密封类。

密封类在Java层面是一个抽象类,派生类继承这个抽象个类,密封类的构造函数是私有的,不能被外部外放;这样就通过继承这个抽象类,达到限制类型的做法。这和Java中使用接口来限定参数类型的做法类似。下面看例子:

sealed class Result
class Success(val code: Int): Result()
class Exception(val code: Int, val message: String): Result()
fun handleResult(result: Result) = when(result) {
    is Success -> "success"
    is Exception -> "exception"
}

fun main() {
    val result = handleResult(Success(200))
    println(result)
}

密封类的派生类可以有任意数量的实例。一个特殊的情况是枚举,它将每个子类的实例数量限制为一个。

2.6. 枚举

kotlin的枚举和Java的枚举类似,先看一个例子:

enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
sealed class Card(val suit: Suit)
class Ace(suit: Suit): Card(suit)
class Queen(suit: Suit): Card(suit)
class Pip(suit: Suit, val number: Int): Card(suit)
class King(suit: Suit): Card(suit) {
    override fun toString() = "king if $suit"
}
fun process(card: Card) = when (card) {
    is Ace -> "${card.javaClass.name} of ${card.suit}"
    is King, is Queen -> "$card"
    is Pip -> "${card.number} of ${card.suit}"
}
fun main() {
    println(process(Ace(Suit.CLUBS)))  // com.example.one.Ace of CLUBS
    println(process(Queen(Suit.SPADES))) // com.example.one.Queen@85ede7b
    println(process(Pip(Suit.DIAMONDS, 10)) // 10 of DIAMONDS
    )
}

接着看一下枚举的一些常用操作:

enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
// 获取某一个
var suit = Suit.valueOf("CLUBS")
// 遍历, name:实例的名称 ordinal:索引
for (suit in Suit.values()) { println("${suit.name} -> ${suit.ordinal}") }

枚举类也可以有属性和方法:

enum class Suit(val symbols: Char) {
    CLUBS('\u2663'),
    DIAMONDS('\u2666'),
    HEARTS('\u2665') {
        override fun display() = "${super.display()} $symbols"
    },
    SPADES('\u2660');
    
    open fun display() = "$symbols $name"
}

三. 通过委托进行扩展

kotlin同时支持继承和委托,如何选择:

  • 如果你想用一个类的对象来替代另一个类的对象,请使用继承
  • 如果你想让一个类的对象只使用另一个类的对象,请使用委托

先看一个委托的实例:

interface Worker {
    fun work()
    fun takeVacation()
}
class JavaProgrammer: Worker {
    override fun work() = println("...write Java...")
    override fun takeVacation() = println("...code at the beach...")
}
class PhpProgrammer: Worker {
    override fun work() = println("...write PHP...")
    override fun takeVacation() = println("...code at the beach...")
}
class Manager(val worker: Worker) {
    fun work() = worker.work()
    fun takeVacation() = worker.takeVacation()
}
fun main() {
    val manager = Manager(JavaProgrammer())
    manager.work()
}

看着比较啰嗦,但是kotlin提供了支持。

3.1. 使用by来进行委托

上面例子是手工实现委托,将方法调用从Manager路由到Worker委托。在kotlin可以让编译器为我们生成路由代码,简化我们的使用。

class Manager(): Worker by JavaProgrammer()
fun main() {
    val manager = Manager()
    manager.work() // ...write Java...
}

例子中可以看到Manager没有实现任何方法,只是通过By关键字,编译器在字节码层面实现了属于Worker的方法。

这是最简单的委托使用

3.2. 委托给一个参数

在3.1的例子中属于最简单的委托例子,但是看一下就会发现,委托不具备扩展性,只能使用JavaProgrammer,另外Manager的实例不具备访问委托的权限。那么怎么改进呢?可以Wroker委托给参数,如下:

class Manager(val staff: Worker): Worker by staff {
    fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
}
fun main() {
    val manager = Manager(JavaProgrammer())
    manager.meeting() // organizing meeting with JavaProgrammer
}

此时还有一个疑问,如果Manager的方法和Worker中的冲突,怎么解决?重写当前冲突的方法

class Manager(val staff: Worker): Worker by staff {
    fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
    override fun takeVacation() = println("of course")
}

fun main() {
    val manager = Manager(JavaProgrammer())
    manager.takeVacation() // of course
}

接着还有一个问题就是委托多个接口怎么处理?例子:

interface Assistant {
    fun doChores()
    fun fileTimeSheet() = println("No escape from that")
}
class DepartmentAssistant: Assistant {
    override fun doChores() = println("routing stuff")
}
// 多个接口委托
class Manager(val worker: Worker, val assistant: Assistant): 
		Worker by worker, Assistant by assistant {
    fun meeting() = println("organizing meeting with ${worker.javaClass.simpleName}")
    override fun takeVacation() = println("of course")
    override fun fileTimeSheet() {
        println("manually forwarding this...")
        assistant.fileTimeSheet()
    }
}

3.3. 委托的一些注意点

kotlin的委托是编译器帮我们处理一部分字节码,但是并不能将委托等价于继承。还是借用开始那个例子:

interface Worker {
    fun work()
    fun takeVacation()
}
class JavaProgrammer: Worker {
    override fun work() = println("...write Java...")
    override fun takeVacation() = println("...code at the beach...")
}
class PhpProgrammer: Worker {
    override fun work() = println("...write PHP...")
    override fun takeVacation() = println("...code at the beach...")
}
class Manager(val worker: Worker): Worker by worker

fun main() {
//  val manager: JavaProgrammer = Manager(PhpProgrammer()) // ERROR 类型不匹配
    val manager: Worker = Manager(JavaProgrammer())  // 这样是可以的
    manager.work() // ...write Java...
}

这里也可以发现委托的一个副作用,Manager可以被当作Worker来对待。

还有一个需要的注意的,这里Manager的参数是val,但是如果将val变为var时,会出现什么现象。例子:

class Manager(var worker: Worker): Worker by worker

fun main() {
    val manager = Manager(JavaProgrammer())
    manager.work() // ...write Java...
    manager.worker = PhpProgrammer()
    manager.work() // ...write Java...
}

出现例子中的现象其实也不奇怪,在想下上面Java的委托实现,可以推测出,kotlin在by关键下,也会保存一个幕后字段作为JavaProgrammer的实现引用,接着将this.worker = worker赋值引用,manager.worker = PhpProgrammer()只是改变了worker参数引用的指向,但是幕后字段的引用并没有改变,修改不生效。

并且在manager.worker = PhpProgrammer()中,涉及将JavaProgrammer改为PhpProgrammer,但是JavaProgrammer并没有垃圾回收,因为委托持有它。因此委托的声明周期和对象的声明周期相同。

3.4. 委托变量和属性

Kotlin的委托还可以使用在get/set对象属性的访问权限以及局部变量的访问权限。

get/set会委托给被委托对象setValue/getValue方法,因此被委托类需要提供setValue/getValue这两个方法。如果是val 属性,只需提供getValue。如果是var 属性,则setValue/getValue都需要提供。

3.4.1. 委托变量

局部变量也可以声明委托,例子:

class PoliteString(var content: String) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) = content.replace("stupid", "s******")
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { content = value }
}

fun main() {
    var comment: String by PoliteString("Some nice message")
    println(comment)  // Some nice message
    comment = "This is stupid"
    println(comment)  // This is s******
}

3.4.2. 委托属性

属性委托和变量委托其实差不多,例子:

import kotlin.reflect.KProperty

class Delegate {
    private var content = "==> "
    operator fun getValue(thisRef: Any?, property: KProperty<*>) = content
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { content = "<== $value" }
}

class NewString {
    var prop: String by Delegate()
}

fun main() {
    val newString = NewString()
    println(newString.prop)    // ==> 
    newString.prop = "hello world"
    println(newString.prop)    // <== hello world
}

thisRef —— 必须与属性所有者类型相同或者是它的超类型

property —— 必须是类型 KProperty<*> 或其超类型

value —— 必须和属性同类型或者是它的超类型

从上面的例子可以看到属性委托需要实现getter/setter方法,kotlin标准库中声明了2个含所需 operator方法的 ReadOnlyProperty / ReadWriteProperty 接口。

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

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

那么看一下上面的例子该如何重写:

class Delegate: ReadWriteProperty<Any, String> {
    private var content = "==> "
    override operator fun getValue(thisRef: Any, property: KProperty<*>) = content
    override operator fun setValue(thisRef: Any, property: KProperty<*>, value: String) { content = "<== $value" }
}

这一下就简单很多了。

3.5. Lazy(惰性)委托

惰性委托可以在使用的才会执行,这个特性和短路求值一样。例子:

fun getTemperature(city: String): Double {
    println("fetch from webservice for $city")
    return 30.0
}

fun main() {
    val showTemperature = false
    val city = "Boulder"
    if (showTemperature && getTemperature(city) > 20) { // 短路求值
        println("Warm")
    } else {
        println("Nothing to report")  // 只输出这一句
    }
}

接着重构下上面的代码:

fun main() {
    val showTemperature = false
    val city = "Boulder"
    val temperature = getTemperature(city)
    if (showTemperature && temperature > 20) {
        println("Warm")
    } else {
        println("Nothing to report")
    }
}
// fetch from webservice for Boulder
// Nothing to report

这样的输出并不是我们希望的,那么如何优化呢?可以惰性委托:

fun main() {
    val showTemperature = false
    val city = "Boulder"
    val temperature by lazy { getTemperature(city) }
    if (showTemperature && temperature > 20) {
        println("Warm")
    } else {
        println("Nothing to report")
    }
}

这里的通过by关键字,将变量temperature变成了一个委托属性。lazy函数接受一个lambda表达式作为参数,该表达式将执行计算,但仅按需执行,而不是急于或立即执行。lambda表达式中的计算将在请求变量的值时进行计算。

一旦对lambda表达式求值,委托将记住结果,以后对该值的请求将接受报错,不会重新计算lambda表达式。

默认情况下,lazy函数同步lambda表达式的执行,因此最多只有一个线程执行它。

3.6. Observable委托

observable委托来监视对象中局部变量或属性的变化。它可以用于监视和调用。例子:

import kotlin.properties.Delegates.observable

fun main() {
    var count by observable(0) {property, oldValue, newValue ->
        println("Property: $property, old: $oldValue, new: $newValue")
    }
    println("count: $count") // count: 0
    count++ // Property: property count (Kotlin reflection is not available), old: 0, new: 1
    println("count: $count") // count: 1
    count-- // Property: property count (Kotlin reflection is not available), old: 1, new: 0
    println("count: $count") // count: 0
}

这里提示Kotlin reflection is not available,是因为缺少依赖

implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10")

这里还需要了解Vetoable函数,这个和observable不同,使用vetoable注册的处理程序返回布尔结果。返回值true表示接受修改,false表示拒绝,如果拒绝修改将被放弃。例子:

var count by vetoable(10) {_, oldValue, newValue -> newValue > oldValue}
println(count) // 10
count++
println(count) // 11
count--
println(count) // 11

本文来源:《Kotlin编程实战:第二部分总结》