Kotlin基础知识(十一)——Kotlin的类型系统:可空性

1,250 阅读8分钟

一、可空类型

Kotlin和Java的类型系统之间第一条也可能是最重要的一条区别是:Kotlin对可空类型的显式的支持。这意味着:这是一种指出你的程序中哪些变量和属性允许为null的方式。如果一个变量可以为null,对变量的方法的调用就是不安全的,因为这样会导致NullPointerException

fun strLenSafe(s: String?) = ..

上述代码中***?可以加在任何类型的后面来表示这个类型的变量可以存储null引用:String?、Int?、MyCustomType?***

  Type?  =  Type  or  null
可空类型的变量可以存储null引用

重申一下,没有问号的类型表示这种类型的变量不能存储null引用。这说明所有常见类型默认都是非空的,,除非显式地把它标记为可空。

一个可空类型的值限制

  • 1.不能再调用它的方法
  • 2.不能把它赋值给非空类型的变量
  • 3.不能把可空类型的值传给拥有非空类型参数的函数

二、安全调用运算符:“?.”

Kotlin的弹药库中最有效的一种工具就是安全调用运算符?.,它允许一次null检查和一次方法调用合并成员一个操作。

图1 安全调用运算符只能调用非空值的方法

注意:这次调用的结果类型也是可空的

// 定义
fun printAllCaps(s: String?) {
    // allCaps可能是null
    val appCaps: String? = s?.toUpperCase()
    println(appCaps)
}

// 测试
>>> printAllCaps("abc")
ABC
>>> printAllCaps("null")
null

安全调用不光可以调用方法,也能用来访问属性。

  • 使用安全调用处理可空属性
// 定义
class Employee(val name: String, val manager: Employee?)
fun managerName(employee: Employee): String? = employee.manager?.name

// 测试
>>> val ceo = Employee("Da Boss", null)
>>> val developer = Employee("Bob Smith", ceo)
>>> println(managerName(developer))
Da Boss
>>> println(managerName(ceo))
null
  • 链接多个安全调用

二、Elvis运算符:“?.”

Kotlin有方便的运算符来提供代替null的默认值。它被称作***Elvis运算符***(或者null合并运算符)。

// 定义
fun foo(s: String?) {
    val t: String = s ?: ""
}

Elvis运算符接受两个运算数,第一个运算数不为null,运算结果就是第一个运算数;否则,运算结果就是第二个运算数。

图2 Elvis运算符用其他值代替null

Elvis运算符经常和安全调用运算符一起使用,用一个值代替对null对象调用方法时返回的null

  • 使用Elvis运算符处理null值
// 定义
class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun printShippingLabel(person: Person) {
    val address = person.company?.address
            ?: throw IllegalArgumentException("No address")
    with(address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

// 测试
>>> val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
>>> val jetbrains = Company("JetBrains", address)
>>> val person = Person("Dmitry", jetbrains)

>>> printShippingLabel(person)
Elsestr. 47
80687 Munich, Germany

>>> printShippingLabel(Person("Alexey", null))
java.lang.IllegalArgumentException: No address

三、安全转换:“as?”

***as?***运算符把值转换成指定的类型,如果值不是合适的类型就返回null,如下图:

图3 安全转换运算符"as?"

  • 使用安全转换实现equals
class Person1(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person1 ?: return false

        return otherPerson.firstName == firstName &&
                otherPerson.lastName == lastName
    }

    override fun hashCode(): Int =
            firstName.hashCode() * 37 + lastName.hashCode()
}

>>> val p1 = Person1("Dmitry", "Jemerov")
>>> val p2 = Person1("Dmitry", "Jemerov")
>>> println(p1 == p2)
true
>>> println(p1.equals(42))
false

使用这种模式,可以非常容易地检查实参是否是适当的类型,转换它,并在它的类型不正确时返回false,而且这些操作全部在同一个表达式中。

四、非空断言:“!!”

非空断言是Kotlin提供的最简单直率的处理可空类型值的工具。它使用双感叹号表示,可以把任何值转换成非空类型。如果对null值做非空断言,则会抛出异常。

fun ignoreNulls(s: String?) {
    // 异常指向这一行
    val sNotNull: String = s!!
    println(sNotNull.length)
}

>>> ignoreNulls(null)
Exception in thread "main" kotlin.KotlinNullPointerException

注意:使用***!!并且它的结果是异常时,异常调用栈的跟踪信息只表明异常发生在哪一行代码*,而不会表明异常发生在哪一个表达式。为了让跟踪信息更清晰精确地表示哪个值为***null*,最好避免在同一行中使用多个!!断言**:

// 不要写这样的代码!
person.company!!.address!!.country

上面这一行代码中发生了异常,不能分辨出到底company的值为null,还是address的值为null。

五、“let”函数

let函数允许对表达式求值,检查求值结果是否为null**,并把结果保存为一个变量。所有这些动作都在同一个简洁的表达式中。

可空参数最常见的一种用法应该就是被传递给一个接受非空参数的函数。

  • 示例:
fun sendEmailTo(email: String) { /* ... */ }

>>> val  email: String? = ...
>>> sendEmailTo(email)
ERROR: Type mismatch: inferred type is String? but String was expected

必须显示地检查这个值不为null:

if (email != null) sendEmailTo(email)

另外一种处理方式:使用***let函数,并通过安全调用来调用它。let函数做的所有事情就是把一个调用它的对象变成lambda表达式的参数*。

foo?.let { ... it ... } 中
若:
foo != null   ->  在lambda内部it是非空的
foo == null  ->  什么都不会发生

*let*函数只在email的值非空时才被调用,所以在lambda中把email当作非空的实参使用。

email?.let { email -> sendEmailTo(email) }

使用自动生成的名字it这种简明语法周,上面的代码就更短了:email?.let { sendEmailTo(it)

六、延迟初始化的属性

  • 使用非空断言访问可空属性
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    // 声明一个可空类型的属性并初始化为null
    private var myService: MyService? = null

    // 在setUp方法中提供真正的初始化器
    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        // 必须注意可空性:要么用!!,要么用?.
        Assert.assertEquals("foo",
            myService!!.performAction())
    }
}

上述代码中可以把myService属性声明成可以延迟初始化的,使用***lateinit***修饰符来完成这样的声明。

  • 使用延迟初始化属性
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    // 声明一个可空类型的属性并初始化为null
    private lateinit var myService: MyService? = null

    // 在setUp方法中提供真正的初始化器
    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        // 不需要null检查直接访问属性
        Assert.assertEquals("foo",
            myService.performAction())
    }
}

注意:延迟初始化的属性都是***var***,因为需要在构造方法外修改它的值,而val属性会被编译成必须在构造方法中初始化的final字段。 如果在属性被初始化之前就访问了它,会得到这个异常“lateinit property myService has not been initialized”。

***lateinit***属性常见的一种用法是依赖注入。在这种情况下,lateinit属性的值是被依赖注入框架从外部设置的。为了保证和各种Java(依赖注入)框架的兼容性,Kotlin会自动生成一个lateinit属性具有相同可见性的字段。如果属性的可见性是public,生成字段的可见性也是public

七、可空类性的扩展

可空类型定义扩展函数是一种更强大的处理***null值的方式。可以允许接收者为null的(扩展函数)调用,并在该函数中处理null,而不是在确保变量不为null***之后再调用它的方法。只有扩展函数才能做到这一点,普通成员方法的调用时通过对象实例来分发的。

// 定义
fun verifyUserInput(input: String?) {
    // 这里不需要安全调用
    if(input.isNullOrBlank()) {
        println("Please fill in the required fields");
    }
}

// 测试
>>> verifyUserInput(" ");
Please fill in the required fields
// 这个接受者调用isNullOrBlank并不会导致任何异常
>>> verifyUserInput(null)
Please fill in the required fields

上述测试方法中的***isNullOrBlank***的讲解:

可空类型的值
|----| |-可空类型的扩展- |
input.isNullOrBlank()
    |-|
不需要安全调用

函数***isNullOrBlank***显式地检查了null,这种情况下返回true,然后调用isBlank,它只能在非空String上调用:

// 可空字符串的扩展
fun String?.isNullOrBlank(): Boolean = 
    // 第二个“this”使用了智能转换
    this == null || this.isBlank()

当一个可空类型(以?)定义扩展函数时,这意味着可以对可空的值调用这个函数;并且函数体中的**this可能为null,所以必须显示地检查**。而Java中,this永远是非空的。

注意let函数也能被可空的接受者调用,但它并不检查值是否为null。如果在一个可空类型直接上调用***let***,而没有使用安全调用运算符,lambda的实参将会是可空的:

十、类型参数的可控性

Kotlin中所有泛型类和泛型函数的类型参数默认都是可空的。

  • 处理可空的类型参数
fun <T> printHashCode(t: T) {
    // 因为“t”可能为null,故必须使用安全调用
    println(t?.hashCode())
}

// “T”被推导成“Any?”
>>> printHashCode(null)
null

在printHashCode调用中,类型参数***T推导出的类型是可空类型Any?***。因此,尽管没有用问号结尾,实参t依然允许持有null。

  • 为类型参数声明非空上界
// 现在“T”就不是可空的
fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}

// 这段代码是无法编译的:
// 不能传递null,因为期望的是非空值
>>> printHashCode(null)
Error:Type parameter bound for T in fun <T : Any> printHashCode(t: T): Unit
 is not satisfied: inferred type Nothing? is not a subtype of Any
>>> printHashCode(42)
42

十一、可空性和Java

针对可空,Java和Kotlin的一一对应关系:

Java -> Kotlin

@Nullable + Type = Type?

@NotNull + Type = Type