类委托和委托属性

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。

类委托

类委托的核心思想在于将一个类的具体实现委托给另外一个类是去完成。Kotlin中委托使用的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上接受委托的辅助对象,就可以了。

1
2
3
4
5
6
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
    fun helloWorld() = println("Hello World")
    override fun isEmpty(): Boolean {
        return helperSet.isEmpty()
    }
}

上面就创建了MySet类就是通过委托对象helperSet实现了接口Set里的所有方法, 我们只需要根据自己的需要,重写helperSet里的方法就可以了,剩余的其它方法又helperSet自身实现。

使用类委托的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
}

覆写类委托里的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
interface Base {
    fun printMessage()
    fun printMessageLine()
}

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

class Derived(b: Base) : Base by b {
    override fun printMessage() { print("abc") }
}

fun main() {
    val b = BaseImpl(10)
    Derived(b).printMessage() // abc
    Derived(b).printMessageLine() // 10
}

属性是不会被覆写的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Base {
    val message: String
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override val message = "BaseImpl: x = $x"
    override fun print() { println(message) }
}

class Derived(b: Base) : Base by b {
    // This property is not accessed from b's implementation of `print`
    override val message = "Message of Derived"
}

fun main() {
    val b = BaseImpl(10)
    val derived = Derived(b)
    // 这里输出的仍然是代理类的message变量
    derived.print() //  BaseImpl: x = 10
	  // 自身的属性只能通过自己的对象来方法
    println(derived.message) // Message of Derived
}

委托属性

委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。委托属性的语法结构如下:

1
2
3
class MyClass {
	var p by Delegate()
}

这里使用by关键字连接了左边的p属性和右边的Delegate实例,代表着将p属性的具体实现委托给了Delegate类去完成。到调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。

因此,我们还必须对Delegate类进行实现才可以,具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Delegate {
    private var propValue: Any? = null
    operator fun getValue(myClass: MyClass, property: KProperty<*>): Any? {
        return propValue
    }

    operator fun setValue(myClass: MyClass, property: KProperty<*>, value: Any?) {
        propValue = value
    }
}

Delegate类中,我们必须实现getValue()setValue()这两个方法,并且都要使用operator关键字进行声明。

getValue()方法接收两个参数: 第一个是用于声明该Delegate类的委托功能可以在什么类中使用,这里写成MyClass表示仅可以在MyClass类中使用; 第二个参数KProperty<*>是Kotlin的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不上,但是必须在方法参数上声明。

setValue()的方法也是类似,只不过它要接收3个参数。前两个参数和getValue()方法中是一样的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法返回值的类型保持一致。

不过还存在一种不需要在Delegate类中实现setValue()的方法,那就是MyClass类中的p属性使用的是关键字val声明的。如果p属性是使用val声明的,那么就意味着p属性是无法在初始之后重新被赋值的,因此也就没有逼下台实现setValue()方法,只需要实现getValue()方法即可。

标准库中的委托属性

Lazy

我们先看看lazy函数的作用

lazy() is a function that takes a lambda and returns an instance of Lazy which can serve as a delegate for implementing a lazy property: the first call to get() executes the lambda passed to lazy() and remembers the result, subsequent calls to get() simply return the remembered result.

具体效果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

// 输出结果为
// computed!
// Hello
// Hello

从输出结果我们可以看到,实际上lazy函数的Lambda表达式只有在第一次获取值的时候运行了一次,第二次调用lazyValue的时候用的是它的缓存值。

从代码可以看到: 懒加载背后的原理就是属性委托, 在lazy函数中创建并返回了一个Delegate对象,当我们调用属性lazyValue的时候,其实就是调用Delegate对象的getValue()方法,然后getValue()中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码就可以执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。

让我们自己实现一个简单版本的lazy函数,新建一个Later.kt文件,创建一个Later类,并实现getValue()方法

1
2
3
4
5
6
7
8
9
class Later<T>(val block: () -> T) {
    private var value: Any? = null
    operator fun getValue(any: Any?, prop: KProperty<*>): T {
        if (value == null) {
            value = block()
        }
        return value as T
    }
}

getValue()方法第一个参数指定为Any?,表示我们希望Later的委托功能在所有类中都可以用。然后使用了一个变量value变量对值进行缓存,如果value为空就调用构造函数中传入的函数类型参数去获取值,否则就直接返回。

由于懒加载技术是不会对属性赋值的,因此这里我们就不用实现setValue方法了。

然后让我们再定义一个顶层函数,让它的调用方式更接近lazy函数

1
fun <T> later(block: () -> T) = Later(block)

现在我们就可以使用自己定义的later函数来代替lazy函数了。

1
2
3
4
5
6
7
8
9
val lazyValue: String by later {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

注意,虽然我们自己实现了懒加载函数later,但是它只是一个简单的版本,在一些诸如同步,空值处理等方面并没有实现得很严谨。因此在正式项目中,推荐使用Kotlin内置的lazy函数才是最好的。

观察属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first" //<no name> -> first
    user.name = "second" //first -> second
}

观察属性会在每次给对象赋值时(赋值之后),都执行一次观察里的方法。

同理,如果想在每次赋值之前被调用,可以使用vetoable来实现

属性委托同样可以用语委托给类自身的另给一个属性,使用::来引用另一个属性。例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}
fun main() {
   val myClass = MyClass()
   // Notification: 'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

oldName是被抛弃的方法,为了向前兼容,把它委托给newName属性。

使用属性委托的特定,我们还可以实现存储属性的效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

fun main() {
    val user = User(mapOf(
        "name" to "John Doe",
        "age"  to 25
    ))
    println(user.name) // Prints "John Doe"
    println(user.age)  // Prints 25
}

创建User对象的时候通过map来构建User,然后就可以通过属性直接访问了.

类型别名

可以给已经存在的类型设置一个别名,方便在编写代码时调用,多用于泛型或集合类型。

1
2
3
4
typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>

也可以给函数类型设置别名

1
2
3
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate<T> = (T) -> Boolean

内部类和嵌套类也可以设置别名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class A {
    inner class Inner
}
class B {
    inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner

空指针检查

Kotlin利用编译时判空检查的机制几乎(并不是百分百)杜绝了空指针异常,也就是说,Kotlin将空指针检查提前到了编译时期。

可空类型声明

Kotlin中默认所有的参数和变量都不能为空,要定义可空类型,就要在类名后面加上?

1
val a: String

?.操作符

?.操作符表示当对象不为空时正常调用相应的方法,否则什么也不做。

如:

1
2
3
if (a ! = null) {
	a.doSomething()
}

可以简化为

1
a?.doSomething()

?:操作符

?:操作符的左右两边都接受一个表达式,如果左边表示表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。

如:

1
2
3
4
5
6
val c = if ( a != null) {
	a
}
else {
	b
}

可以简化为

1
val c = a ?: b

?.?:结合使用

上面两个操作符可以结合使用,如:

1
2
3
4
5
6
fun getTextLength(text: String?): Int {
	if (text != null) {
		return text.length
	}
	return 0
}

简化为

1
fun getTextLength(text: String?) = text?.length ?: 0

!!非空断言工具

如下面的代码,虽然我们在main函数中对content做了非空判断的检查,但是下面的代码仍然无法编译过。因为printUpperCase函数并不知道外部已经对content进行了非空检查。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val content: String? = "Hello"
fun main() {
    if (content != null) {
        printUpperCase()
    }
}

fun printUpperCase() {
    val upperCase = content.toUpperCase()
    println(upperCase)
}

要让上面的代码编译过,可以使用非可断言工具!!,写法是在对象后面加上!!

1
2
3
4
fun printUpperCase() {
    val upperCase = content!!.toUpperCase()
    println(upperCase)
}

相等比较

kotlin中比较两个变量是否相等有两种比较方式:

  1. ==比较两个变量的值是否相等,即两个遍历的equals()返回值是否相等,等价于

    1
    
    a?.equals(b) ?: (b === null)
    
  2. ===比较两个变量是否引用同一个对象

方法引用::

Kotlin中使用::来引用方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Book(val name: String)

fun main() {
    // 让getBook引用Book的构造函数
    val getBook = ::Book
    println(getBook("Dive into Kotlin").name)

    val bookNames = listOf(
        Book("Dive into Java"),
        Book("Dive into Kotlin")
    ).map(
        // 引用获取name的方法
        Book::name
    )
    println(bookNames)
}

延迟初始化lateinit

Kotlin 要求在实例构建过程中初始化每个成员属性。有时,类的使用方式使构造函数没有足够的信息来初始化所有属性(例如,在生成构建器类或使用基于属性的依赖注入时)。为了不必使这些属性可为空,可以使用后期初始化的属性:

1
lateinit var name: String

Kotlin 将允许声明该属性而无需初始化它,并且可以在构造后的某个时候(直接或通过函数)设置属性值。类本身及其用户都有责任注意在设置属性值之前不要读取该属性,并且 Kotlin 允许编写读取name的代码,就像它是一个普通的,不可为空的属性一样。但是,编译器无法强制正确使用,因此,如果在设置属性之前先读取该属性,将在运行时抛出 UninitializedPropertyAccessException异常。

在声明了lateinit 属性的类中,可以检查它是否已初始化::name.isInitialized

1
if (::name.isInitialized) println(name)

lateinit只能与var一起使用,而不能与val一起使用。

异常抛出与捕获

ko tlin中所有的异常类都继承自Throwable类,每个异常都有一个message属性,一个stack trace,以及一个可选的cause

1
2
3
4
5
6
7
fun divideOrZero(numerator: Int, denominator: Int): Int {
    try {
        return numerator / denominator
    } catch (e: ArithmeticException) {
        return 0
    }
}

与 Python 不同,try/catch 是一个表达式:try 代码块(如果成功)或所选的 catch 代码块的最后一个表达式将成为结果值(finally 不会影响结果),因此可以将上面的函数体重构为:

1
2
3
4
5
6
7
fun divideOrZero(numerator: Int, denominator: Int): Int {
    return try {
        numerator / denominator
    } catch (e: ArithmeticException) {
        0
    }
}

基本异常类是Throwable(但是扩展其子类Exception更为常见),并且有大量内置的异常类。如果找不到满足需求的异常类,则可以通过从现有异常类继承来创建自己的异常类。

请注意,除了与Java 代码进行交互时,在Kotlin中不建议使用异常。与其在自己的代码中引发异常,不如考虑使用特殊的返回类型,例如 Arrow 库中的 Option 或 Either