Kotlin知识归纳(六) —— 类型系统

1,698 阅读9分钟

前序

      Kotlin引入可空性的新特性,旨在消除来自代码空引用的危险。将运行时的NPE转变成编译器的错误。

可空类型与非空类型

      在Kotlin类型系统中,分为可空类型和非空类型。当你允许一个变量为null时,需要显示在类型后面加上一个问号,将其非空类型转换为可空类型。

      常见的类型都是非空类型,不能存储null引用,只有在类型后面添加个问号转换为可空类型后,变量才可存储null引用。

val str:String? = null

      对于一个可空类型的值,不能直接调用该类型的方法,也不能把他赋值给非空类型,更不能把它传递给接受非空类型参数的函数。可空类型看似和非空类型并没有什么交互性,但其实并不是,只是需要对可空类型进行一个判空后,才能正常交互:

val str:String? = “”
if (str != null)
    str.length

      一旦对可空类型的对象进行判空,编译器就会对判空的作用域内把该对象当作非空对待。

Kotlin的可空性与Java的Optional

      Java8中引入的特殊包装类型Optional来解决null引用问题。但这种方法使代码更加冗长,并且额外的包装类还影响运行时的性能,因此并没有被广泛使用起来。

      但在Kotlin中,可空和非空的对象在运行时没有什么区别,可空类型并不是非空类型的包装类。所有的检查都是编译器完成,这使得Kotlin的可空类型并不会在运行时带来额外的开销。

安全调用运算符:?.

      Kotlin标准库中有一个高效的安全调度运算符:?. 。它将null检查和调用合并成一个操作。当你使用?.调用一个可空类型对象的方法时,若值不为空,则方法会被正常执行;若值为null,则方法调用不发生,并整个表达式返回null。

安全调用除了可以调用方法,还可以用来访问属性。

Elvis运算符:?:

      Elvis运算符?:用来提供替代null的默认值。Elvis运算符接收两个表达式,如果左侧表达式非空,则返回其左侧表达式。当左侧表达式为空,则返回右侧表达式。

Elvis运算符经常与安全调度运算符一起使用:

val str:String? = null
println(str?.length ?: 0)

Elvis运算符也可以配合return 和 throw一起使用,当运算符左边为null时,能提前返回函数或抛出异常。

val str:String? = null
//为空抛一次
val length = str?.length ?: throw IllegalArgumentException()
println(length)
str?.let {
    println(length)
} ?: return

//等价于
if(str == null)
    //函数类型为空时直接打断函数继续执行
    return
    
//str不为null,则继续执行。
println(length)

也可以配合run函数配合使用,替代if-lese:

str?.let { 
    //str不为空的逻辑
} ?: run { 
    //str为空时逻辑
}

非空断言:!!

      Kotlin为NPE爱好者提供非空断言运算符 !! (双感叹号),可以把任何对象转换成非空类型,从而调用该对象方法,但可能造成抛出NPE。

val str:String? = null
//抛NPE
println(str!!.length)

      所以只有确保该可空类型对象不为空时,才使用非空断言。当使用非空断言而且发生异常时,异常栈只表明异常发生在哪一行,并不会指明哪个表达式,所以最好避免同一行中使用非空断言。

安全转换:as?

      和常规的Java转换一样,当被转换的值不是你视图转换的类型时,会抛出ClassCastException异常。一般解决方案是在使用在转换前使用is检查来确定该值是否符合转换类型。但Kotlin提供更简洁的运算符——安全转换运算符:as?

//定义父类和子类
open class Animal{
    fun getName(){
    }
}
class Dog:Animal(){
    fun getDogName(){
    }
}

fun main(args:Array<String>){
    val animal:Animal = Dog()
    val dog = animal as? Dog ?: return
    dog.getDogName()
}

安全转换运算符尝试将值转换成给定的类型,否则返回null:

let函数

      let函数将调用它的对象变成lambda表达式的参数。配合安全调度运算符可以把调用let函数的可空对象,转变成非空类型。然后在let函数中调用一系列对该可空类型的操作。

fun main(args:Array<String>){
    val str:String? = null
    str?.let {
        daqi(it)
    }
}

fun daqi(str:String){

}

      当需要检查多个值是否为null时,不建议使用嵌套的let调用来处理,建议使用一个if语句对这些值进行一次性检查。

可空类型的扩展

      对可空类型的进行扩展的好处是,允许接收者为null时调用扩展函数,并在扩展函数中处理null,而不用确保变量不为null后再调用该对象的方法。因为当实例为null时,成员方法永远不会被执行。

      Kotlin标准库中的CharSequence存在两个扩展函数:isNullOrEmpty和isNullOrBlank,可以由String?类型的接收者调用。

      对可空类型定义扩展函数时,意味着函数体中的this可能为空,需要做对应的空处理。

fun String?.daqi(){
    if (this == null){
        println("this is null")
    }
}

fun main(args:Array<String>){
    val str:String? = null
    由于接收的是可空类型,不需要使用?.
    str.daqi()
}

延迟初始化

      Kotlin中,属性声明为非空类型时,必须在构造函数中初始化。但属性可以在一个特殊的方法中,通过依赖注入来初始化。这时不能在构造函数中为属性提供一个非空初始化器,但你仍想将该类型声明为非空类型,避免空检查。可以使用lateinit关键字修饰该变量,请将该变量使用var修饰,因为val必须会编译成必须在构造方法中初始化的final字段。

class daqi{
    private lateinit var name:String
    
    fun onCreate(){
        name = "daqi"
    }
}

可空性与Java

      Kotlin会根据Java中的可空性注解,来对来自Java的类型分为可空类型和非空类型。如,@Nullable注解的对象,会被Kotlin当作可空类型的对象。@Notnull注解的对象,会被Kotlin当作非空类型的对象。

      当可空性注解不存在时,Java类型会被转换为Kotlin的平台类型。平台类型本质上是Kotlin不知道其可空信息,既可以把它当作可空类型,也可以把它当作非空类型。如果选择非空类型,编译器会在赋值时触发一个断言,防止Kotlin的非空变量保存空值。这意味着需要开发者负责正确处理来自Java的值。

      Kotlin定义的函数中,编译器会生成对每个非空类型的参数的检查,如果使用不正确的参数调用,会立即抛出异常。(这种检查在函数调用的时候就被执行了,而不是等到该异常参数被使用时才执行。)

基本数据类型

      Java区分基本数据类型和引用类型,基本数据类型具有高效存储和传递的性质。当你需要在泛型类中存储一些基本数据类型时,需要以基本数据类型的包装类型进行存储。因为JVM不支持用基本数据类型作为类型参数。

      Kotlin并不区分基本类型和包装类型。对于变量、属性和返回类型,Kotlin的基本数据类型会被编译成Java的基础数据类型。只有对于泛型类时,才会被编译器成对应的Java基本类型包装类。

基本数据类型

      当使用Java声明的基本数据类型变量时,该类型会变成非空类型,而不是平台类型。因为Java的基本数据类型不能存储null值。

      Kotlin中可空的基本数据类型会被编译成对应的包装类型,因为Java的基本数据类型不能存储null值。

数字转换

      kotlin不会自动把数字从一种类型转换成另一种取值范围更大的类型。Kotlin为每种基本数据类型(Boolean除外)都定义了转换到其他基本数据类型的函数。

      Kotlin要求转换必须显式的,因为在Java中,比较装箱值时,不仅检查他们存储的值,还会比较装箱类型。

//此处比较会返回false
new Integer(42).equals(new Long(42))

      Kotlin标准库为字符串也提供了转换基本数据类型的扩展函数。如果对字符串解析失败,则抛出NumberFormatException()方法。

根类型

      Any类型是所有Kotlin非空类型的超类。但Any不能持有null值,当需要持有任何值的变量包括null值,必须使用Any?

      Any只包含toString、equals和hashCode。所有Kotlin的这些方法都是从Any中继承来得。但Any不能使用使用其他Object的方法(如:wait和notify)

类型参数的可空性

      Kotlin中所以泛型类和泛型函数的类型参数默认都是可空的,因为默认上界是Any?

      如果需要类型参数非空,则必须为其指定一个非空的上界:

fun <T:Any> daqi(t:T){
    
}

参考资料:

android Kotlin系列:

Kotlin知识归纳(一) —— 基础语法

Kotlin知识归纳(二) —— 让函数更好调用

Kotlin知识归纳(三) —— 顶层成员与扩展

Kotlin知识归纳(四) —— 接口和类

Kotlin知识归纳(五) —— Lambda

Kotlin知识归纳(六) —— 类型系统

Kotlin知识归纳(七) —— 集合

Kotlin知识归纳(八) —— 序列

Kotlin知识归纳(九) —— 约定

Kotlin知识归纳(十) —— 委托

Kotlin知识归纳(十一) —— 高阶函数

Kotlin知识归纳(十二) —— 泛型

Kotlin知识归纳(十三) —— 注解

Kotlin知识归纳(十四) —— 反射