3、特殊类型

数据类型

对于那些只需要保存数据的类型,我们常常需要为其重写toStringequals等函数,针对于这种情况下,Kotlin为我们提供了专门的数据类,数据类不仅能像普通类一样使用,并且自带我们需要的额外成员函数,比如打印到输出、比较实例、复制实例等。

//在class前面添加data关键字表示为一个数据类
data class User(val name: String, val age: Int)

数据类声明后,编译器会根据主构造函数中声明的所有属性自动为其生成以下函数:

fun main() {
    var s1 = Student("张三", 18)
    println(s1) // Student(name=张三, age=18)
    var s2 = Student("张三", 18)
    println(s1 == s2) // true
    var (name, age) = s1
    println("name:$name,age:$age") // name:张三,age:18
}

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

当然,为了确保生成代码的一致性和有效性,数据类必须满足以下要求:

注意:编译器会且只会根据主构造函数中定义的属性生成对应函数,如果有些时候我们不希望某些属性被添加到自动生成的函数中,我们需要手动将其移出主构造函数

data class Person(val name: String) {
    var age: Int = 0   //age属性不会被处理
}

枚举类型

定义

//在类前面添加enum表示这是一个枚举类型
enum class LightState {
    GREEN, YELLOW, RED   //直接在枚举类内部写上所有枚举的名称,一般全部用大写字母命名
}

类成员

同样的,枚举类也可以具有成员

//同样可以定义成员变量,但是不能命名为name,因为name拿来返回枚举名称了
enum class LightState(val color: String) {
    GREEN("绿灯"), YELLOW("黄灯"), RED("红灯");  //枚举在定义时也必须填写参数,如果后面还要编写成员函数之类的其他内容,还需在末尾添加分号结束
  
  	fun isGreen() = this == LightState.GREEN  //定义一个函数也是没问题的
}

使用

fun main() {
    val state: LightState = LightState.RED  //直接得到红灯
  	println(state.name)   //自带name属性,也就是我们编写的枚举名称,这里是RED
}

枚举类型可以用于when表达式进行判断,因为它的状态是有限的

val state: LightState = LightState.RED
val message: String = when(state) {
    LightState.RED -> "禁止通行"
    LightState.YELLOW -> "减速通行"
    LightState.GREEN -> "正常通行"
}
println(message)   //结果为: 禁止通行

匿名类

有些时候,可能我们并不需要那种通过class关键字定义的对象,而是以匿名的形式创建一个临时使用的对象,在使用完之后就不再需要了,这种情况完全没有必要定义一个完整的类型,我们可以使用匿名类的形式去编写。

// 使用object关键字声明一个匿名类并创建其对象,可以直接使用变量接收得到的对象
val obj = object {   
  	val name: String = ""
    override fun toString(): String = "我是一个匿名类"   //匿名类默认继承于Any,可以直接重写其toString
}
println(obj)

可以看到,匿名类除了没名字之外,也可以定义成员,只不过这种匿名类不能定义任何构造函数,因为它是直接创建的,这种写法我们也可以叫做对象表达式

匿名类不仅可以直接定义,也可以作为某个类的子类定义,或是某个接口的实现

interface Person {
    fun chat()
}

fun main() {
    val obj: Person = object : Person {   //直接实现Person接口
        override fun chat() = println("实现的接口 chat")
    }
    obj.chat()  //当做Person的实现类使用
}

函数式接口

特别的,对于只存在一个抽象函数的接口称为函数式接口单一抽象方法(SAM)接口,函数式接口可以有N个非抽象成员,但是只能有一个抽象成员。对于函数式接口,可以使用我们前面介绍的Lambda表达式来使代码更简洁

fun interface KRunnable {  //在接口声明前面添加fun关键字
    fun invoke()
}


fun main() {
    val runnable = KRunnable {   //支持使用Lambda替换
        println("我是函数invoke的实现")
    }
    runnable.invoke()
}

单例对象

object关键字除了用于声明匿名类型,也可以用于声明单例类。单例类是什么意思呢?就像它的名字一样,在整个程序中只能存在一个对象,也就是单个实例,不可以创建其他的对象,始终使用的只能是那一个对象。

//声明的一个单例类
object Singleton {   
    private var name = "你干嘛"
    override fun toString() = "我叫$name"
}

fun main() {
  	//通过类名直接得到此单例类的对象
    val singleton = Singleton  
  	//不可以通过构造函数的形式创建对象
    println(singleton)
}
object Singleton {
    fun test() = println("原神,启动!")
}

fun main() {
    Singleton.test()   //单例定义的函数直接使用类名即可调用
}

伴生对象

现在我们希望一个类既支持单例类那样的直接调用,又支持像一个普通class那样使用,这时该怎么办呢

使用companion关键字在内部编写一个伴生对象

class Student(val name: String, val age: Int) {
  	//使用companion关键字在内部编写一个伴生对象,它同样是单例的
    companion object Tools {
      	//伴生对象定义的函数可以直接通过外部类名调用
        fun create(name: String, age: Int) = Student(name, age)
    }
}

fun main() {
  	//现在Student不仅具有对象的函数,还可以通过类名直接调用其伴生对象通过的函数
    val student = Student.create("小明", 18)
  	println(student.toString())
}

密封类型

有些时候,我们可能会编写一些类给其他人使用,但是我们不希望他们随意继承使用我们提供的类,我们只希望在我们提供的框架内部自己进行使用,这时我们就可以将类或接口设定为密封的。

package com.test

sealed class A   //声明密封类很简单,直接添加sealed关键字即可
sealed class B: A()   //密封类同一个模块或包中可以随意继承,并且子类也可以是密封的