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

1、可空类型

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

fun strLenSafe(s: String?) = ..
复制代码

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

Type?  =  Type  or  null
可空类型的变量能够存储null引用
复制代码

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

一个可空类型的值限制app

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

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

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

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

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

// 定义
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
复制代码
  • 连接多个安全调用

2、Elvis运算符:“?.”

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

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

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

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

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

  • 使用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
复制代码

3、安全转换:“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,并且这些操做所有在同一个表达式中。

4、非空断言:“!!”

非空断言是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。

5、“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)

6、延迟初始化的属性

  • 使用非空断言访问可空属性
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

7、可空类性的扩展

可空类型定义扩展函数是一种更强大的处理***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的实参将会是可空的:

10、类型参数的可控性

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
复制代码

11、可空性和Java

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

Java -> Kotlin

@Nullable + Type = Type?

@NotNull + Type = Type

相关文章
相关标签/搜索