Kotlin官方参考整理——03.类和对象2

3.8 数据类

我们经常创建一些只保存数据的类。在Kotlin中,这叫做数据类并标记为data:

data class User(val name: String, val age: Int)

数据类必须满足以下要求:

  • 主构造函数需要至少有一个参数;
  • 主构造函数的所有参数需要标记为val或var;
  • 数据类不能是抽象、开放、密封或者内部的;

数据类的特点是,编译器会自动根据其主构造函数中声明的属性生成如下方法:

  • equals()、hashCode()
  • toString(),格式是 “User(name=John, age=42)”
  • copy()
  • componentN(),用于支持解构声明

3.8.1 复制

在很多情况下,我们需要复制一个对象并改变它的一些属性,而其余部分保持不变。copy()函数就是为此而生成的。对于上文的User类,会生成类似下面这样的copy():

//函数参数有默认值
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

可见,在copy()函数中创建了一个新的User对象,如果copy函数传入了某个属性的值,则新User对象的该属性就使用传入的值,如果copy函数没有传入某个属性的值,则新User对象的该属性就使用函数参数的默认值,即原有User对象的该属性值:

val jack = User("Jack", 1)
val olderJack = jack.copy(age = 2)  //olderJack和Jack的name属性相同,age属性不同

3.8.2 解构声明

有时把一个对象解构成多个变量会很方便,例如:

val (name, age) = person

println(name)
println(age)

这种语法称为解构声明,一个解构声明一次创建多个变量。解构声明创建的变量和一般的变量没有什么区别,你可以独立地使用它们。

对一个对象使用解构声明的前提是对象所属的类中声明了所需的componentN函数(称为对象可被解构或类可被解构),因为实际上解构声明val (name, age) = person会被编译器编译成如下代码:

val name = person.component1()
val age = person.component2()

在for循环中使用解构

如果集合中的元素可被解构,则可以:

for ((a, b) in collection) {
    ...
}

例如:

for ((key, value) in map) {
    //使用key、value做些事情
}

通过in map获得的是map中的元素entrySet,而entrySet是可被解构的(Entryset类中声明了所需的componentN函数)。

从函数中返回两个变量

在Kotlin中一个简洁的实现⽅式是声明一个数据类并返回其实例:

data class Result(val result: Int, val status: Status)

fun function(……): Result {
    //各种计算...
    return Result(result, status)
}

//系统会自动为数据类生成componentN函数,因此数据类必然是可被解构的
val (result, status) = function(……)

在lambda表达式中使用解构

代码如下所示。系统在调用lambda表达式时,传入的参数是一个entry。第一行代码保持了entry的原样,而第二行代码对entry进行了解构:

map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }

以下划线代表不关心的变量(自kotlin1.1起)

如果在解构声明中你不需要某个变量,不想费心去给它取一个名字,那么可以用下划线来代表它:

val (_ , status) = getResult()

3.9 密封类

密封类的子类是一个有限的集合。通过sealed关键字来声明一个密封类。只能在声明密封类的文件中声明密封类的子类(Kotlin1.1之前要求更为严格,要求必须在密封类内部声明密封类的子类)。

package cn.szx.kotlindemo

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr() //这是一个对象声明

//因为Expr是密封类,因此expr只能是Const对象、 Sum对象、或者NotANumber,没有其他的可能了。
fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    //不再需要else子句,因为我们已经覆盖了所有的情况
}

3.10 泛型

3.10.1 基本使用

泛型类

同Java一样,Kotlin中的类也可以有类型参数(即泛型):

class Box<T>(t: T) {
    var value = t
}

创建对象时需要提供类型参数:

val box: Box<Int> = Box<Int>(1)

如果类型参数可以从构造函数的参数或者其他途径推断出来,则允许省略类型参数:

val box = Box(1)    //1具有类型Int,所以编译器知道我们说的是Box<Int>

泛型函数

函数也可以有类型参数(即泛型)。类型参数要放在函数名之前:

fun <T> singletonList(item: T): List<T> {
    ...
}

fun <T> T.basicToString() : String { //扩展函数
    ...
}

调用时要在函数名之后指定类型参数:

val l = singletonList<Int>(1)

3.10.2 实现协变与逆变(了解)

在阅读以下内容或官方参考的相关部分之前,请务必先阅读《协变与逆变.md》

Joshua Bloch称那些你只能从中读取的对象为生产者,称那些你只能写入的对象为消费者。

声明处型变

假设有这样一个泛型类Source<T>

class Source<T> {
    ...
}

为保证安全,Source<T>是不可型变的(即不支持协变也不支持逆变),Source<Object>不是Source<String>的父类,Source<String>也不是Source<Object>的父类。

我们可以在泛型声明之前加上out关键字,来告诉系统,Source<T>只支持读取T,不支持写入T。那么此时,Source<T>就变成了可协变的:

class Source<out T> {
    ...
}

fun demo(strs: Source<String>) {
    val objects: Source<Object> = strs //合法,因为可协变,因此Source<Object>是Source<String>的父类
}

同理,我们也可以在泛型声明之前加上in关键字,来告诉系统,Source<T>只支持写入T,不支持读取T。那么此时,Source<T>就变成了可逆变的:

class Source<in T> {
    ...
}

fun demo(objs: Source<Object>) {
    val strs: Source<String> = objs //合法,因为可逆变,因此Source<String>是Source<Object>的父类
}

使用处型变:类型投影

将类型参数T声明为out非常方便,但是有些类实际上不能限制为只能读取T不能写入T,一个很好的例子是Array:

class Array<T>(val size: Int) {
    fun get(index: Int): T {...}
    fun set(index: Int, value: T) {...}
}

因为有读、写T的方法,该类在T上既不能是协变的也不能是逆变的,这造成了一些不灵活性。考虑下述函数:

//将from中的元素复制到to中
fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices) to[i] = from[i]
}

让我们在实践中使用它:

val ints: Array<Int> = arrayOf(1, 2, 3)
val anys = Array<Any>(3)
copy(ints, anys) //编译报错,函数的第一个参数声明为Array<Any>,而实际传入的是Array<Int>,但Array<Int>并不是Array<Any>的子类(不可协变)

这是很不方便的。为了让copy的参数from能够接受一个Array<Int>,可以使用out来声明copy函数的参数from:

fun copy(from: Array<out Any>, to: Array<Any>) {
    ...
}

这样做其实是告诉编译器,在函数中只会从from中读取元素,不会向其中写入元素,那么使参数from协变就是安全的,因此from就可以接受任意的Array<Any的子类>了,包括Array<Int>,这就是使用处协变。

同理,因为在函数中只会向参数to中写入元素,而不会从中读取元素,因此我们可以使用关键字in来声明参数to,这样参数to就可以接受任意的Array<Any的父类>了:

fun copy(from: Array<out Any>, to: Array<in Any>) {
    ...
}

使用处型变也称为类型投影,以from为例,这里的from是一个受限制的数组(只能读、不能写),就像是一个影子,它只具有原始数组的一部分功能(只能读、不能写)。

3.11 嵌套类、内部类、匿名内部类

package cn.szx.kotlindemo

class AAA {
    private val m: Int = 1

    //嵌套类,不持有外部类对象的引用,不能访问外部类的成员
    class BBB {
        fun foo() = 2
    }

    //被inner修饰的嵌套类就是内部类,持有外部类对象的引用,可以访问外部类的成员
    inner class CCC {
        fun foo() = m
    }
}

//BBB是嵌套类,不持有AAA对象的引用,因此直接通过AAA的类名就能访问
val x = AAA.BBB().foo()
//CCC是内部类,持有AAA对象的引用,因此只能先创建AAA的对象(即AAA()),再通过AAA的对象访问
val y = AAA().CCC().foo()

Kotlin中没有static关键字,因此没有静态内部类的概念。但是很容易看出,Kotlin中的嵌套类类似于Java中的静态内部类,而内部类类似于Java中的非静态内部类。

this关键字也可以与标签配合使用,这在多层嵌套的类中很有用:

class A {                       //隐式标签@A
    inner class B {             //隐式标签@B

        //为Int类扩展了一个foo()函数,详见“扩展函数”部分
        fun Int.foo() {         //隐式标签@foo
            val a = this@A      //指A的对象
            val b = this@B      //指B的对象
            val c = this        //指foo()的接收者,即Int对象
            val c1 = this@foo   //指foo()的接收者,即Int对象

            //带接收者的匿名函数
            val funLit = fun String.() {
                val d = this    //指funLit的接收者
            }
            //lambda表达式
            val funLit2 = { s: String ->
                val d1 = this   //这个lambda表达式没有接收者,因此这里的this指foo()的接收者,也就是一个Int对象
            }
        }
    }
}

匿名内部类的写法见“3.12.1 对象表达式”。

3.12 对象表达式和对象声明

对象表达式和对象声明都使用object关键字。

3.12.1 对象表达式

有时候我们需要创建一个对某个类做了轻微改动的类的对象,而不想为此去专门声明一个新的类。Java中使用匿名内部类来处理这种情况,而在Kotlin中我们可以使用对象表达式。

对象表达式的基本格式(父类是可选的):

object[:父类]{
    ...
}

比如给一个View设置点击监听,我们可以这样写:

val tv = findViewById(R.id.tv)

tv.setOnClickListener(object : OnClickListener {
    override fun onClick(v: View?) {
        Log.e("Log", "onClick")
    }
})

//实际上,这里的代码使用lambda表达式可以进一步简化,详见lambda表达式相关章节

需要注意的是,如果父类是类而不是接口,则必须传递适当的构造函数参数给它。多个父类以逗号分隔:

/*
 * A、B是类,C是接口
 */

open class A(x: Int) {
    open val y: Int = x
}

open class B {
    ...
}

interface C {
    ...
}

/*
 * 使用对象表达式来创建对象
 */

val ac1:A = object : A(1), C {
}
val ac2:C = object : A(1), C {
}
val bc1:B = object : B(), C {
}
val bc2:C = object : B(), C {
}

就像Java中的匿名内部类那样,对象表达式中的代码可以访问来自包含它的作用域的变量。与Java不同的是,这不仅限于final变量(被final修饰的变量在Java中意为符号常量,而在kotlin中意为不可被覆写):

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override
        fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
}

没有父类的对象表达式一般用不到,并且其中有一些坑,详见官方参考:

private val obj = object {
    val x: String = "x"
}

3.12.2 对象声明

对象声明的作用也是创建一个对象,但对象声明不是表达式,不能用在等号的右边。

对象声明的格式(父类是可选的):

object 对象名[:父类]{
    ...
}

例如:

//有父类
object aaa : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {

    }

    override fun mouseEntered(e: MouseEvent) {

    }
}

//也可以没有父类
object bbb{
    val b = 1

    fun functionB() = 2
}

通过对象名访问对象的成员:

fun test(){
    aaa.mouseClicked(event1)
    aaa.mouseEntered(event2)

    print(bbb.b)
    print(bbb.functionB())
}

在类内使用对象声明创建的对象不同于一般的类成员,它可以直接通过所在类的类名来访问。但是在这样的对象内不能访问其所在类中的成员。

package cn.szx.kotlindemo

class MyClass {
    val a = 1

    //通过对象声明创建了名为obj1的对象    
    object obj1{
        val b = 2
        //val c = a//编译报错,不能访问MyClass中的成员
    }
}



fun test() {
    print(MyClass().a)              //通过MyClass对象来访问MyClass的成员a
    print(MyClass.obj1.toString())  //通过MyClass类名来访问对象obj1
    print(MyClass.obj1.b)           //通过MyClass类名来访问对象obj1
}

对象声明可以位于顶层位置、类内、嵌套类内,不能位于函数内、内部类内。

伴生对象

在类内使用对象声明创建对象时,可以加上companion关键字,即创建伴生对象。伴生对象的特点是,访问伴生对象的成员时可以省略伴生对象的对象名:

package cn.szx.kotlindemo

class MyClass {
    val a = 1

    companion object obj1{
        val b =2
    }
}

fun test() {
    print(MyClass().a)
    print(MyClass.obj1.toString())
    print(MyClass.obj1.b)           //访问伴生对象的成员
    print(MyClass.b)                //伴生对象的特性:访问伴生对象的成员时,可以省略伴生对象的对象名
}

创建伴生对象时,伴生对象的对象名也可以省略,这时伴生对象的名字为“Companion”:

class MyClass {
    val a = 1

    companion object{
        val b =2
    }
}

对象声明的初始化时机(即创建对象的时机)

以下来自官方参考,了解即可:

  • 对象声明:当第一次被访问时
  • 伴生对象:当其相应的类被加载时

利用对象声明实现单例模式

参考官方参考

//File1.kt
package cn.szx.kotlindemo

object DataProviderManager {
    fun getData() = 1
    ...
}
//File2.kt
package cn.szx.kotlindemo

fun test() {
    val data = DataProviderManager.getData()
}

3.13 代理(Delegation)

注意:中文参考中将代理(Delegation)翻译成委托,其实代理和委托描述是相同的关系。例如,用户调用a的doSomething()方法,而a在自己的doSomething()方法中调用b的doSomething()方法。对于用户而言,a是b的代理(类似于一个中介);而对于a自己而言,它把doSomething这项工作委托给了b(因为真正doSomething的是b)。

Kotlin中要实现代理非常简单:

interface Base {
    fun doSomething()
}

//Impl是Base的实现类
class Impl(val x: Int) : Base {
    override fun doSomething() {
        print(x)
    }
}

//代理类,代理了b的工作
//注意这段“: Base by b”,意思是:通过b(即by b)来实现Base接口,
//系统会自动为Delegation生成doSomething方法,并在doSomething方法中调用b的doSomething
class Delegation(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = Impl(10)
    //调用Delegation的doSomething方法,实际调用的b的doSomething方法
    Delegation(b).doSomething() //输出 10
}

3.14 委托属性

3.14.1 示例

Koltin支持委托属性。

class Example {
    //通过Delegate()来实现String
    //或者说,将自己的操作(读、写)委托给Delegate()
    var p: String by Delegate()
}

class Delegate {
    //当对p进行读操作时会调用此方法
    //这里thisRef就是Example对象,而property就是属性p
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    //当对p进行写操作时会调用此方法
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name} in $thisRef.'")
    }
}

//使用
fun test() {
    val e = Example()

    println(e.p)    //会输出:Example@33a17727, thank you for delegating ‘p’ to me!
    e.p = "NEW"     //会输出:NEW has been assigned to ‘p’ in Example@33a17727.
}

3.14.2 委托属性的用途

通过委托属性,可以将属性的声明和实现分离到不同的文件中

有一些常见的属性类型,虽然我们可以在每次需要的时候⼿动实现它们,但是如果能有人把它们实现好并放入一个库中,以后大家要使用这些属性类型的时候可以直接从库中引用,这显然是更好的选择。常见的属性类型:

  • 延迟属性(lazy properties): 其值只在首次访问时计算。
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知。
  • 把多个属性储存在一个映射(map)中,而不是每个都存储在单独的字段中。

其实,Kotlin标准库已经为实现上面几种属性类型提供了工厂方法,通过这些工厂方法可以很方便的生成所需的被委托对象(即by之后的对象)。

3.14.3 实现延迟属性

lazy()是接受一个lambda表达式并返回一个Lazy <T>实例的函数,返回的实例可以作为实现延迟属性的被委托对象:第一次读取属性值时会执行lamdba表达式并记录结果,后续再读取属性值则只是返回记录的结果。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

输出:
computed!
Hello
Hello

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

3.14.4 实现可观察属性

Delegates.observable()接受两个参数:初始值和lambada表达式。每次属性被赋值后就会执行lambda表达式,lambda表达式有三个参数:属性、旧值和新值:

class User {
    //first为初始值,lambda表达式为当属性被赋值后要执行的代码
    var name: String by Delegates.observable("first") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "second"
    user.name = "third"
}

输出:
first -> second
second -> third

如果你希望能够截获一个赋值并“否决”它,那么可以使⽤vetoable()取代observable()。在对属性的赋值生效之前会执行传递给vetoable的lambda表达式,你将有机会否决掉这次赋值。

3.14.5 把属性储存在映射(map)中

一个常见的用例是在一个map⾥中存储属性的值,这时候你可以直接使用一个map对象来作为被委托对象。

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

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

这也适用于var属性,如果把只读的Map换成MutableMap的话:

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

3.14.6 属性委托的要求&委托属性的实现原理

详见官方参考(了解)

另,从Kotlin1.1开始,局部变量也能像属性那样进行委托了。

发布了46 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/al4fun/article/details/73928649
03.
今日推荐