End

Kotlin 朱涛 思维-4 空安全思维 平台类型 非空断言

本文地址


目录

Kotlin 空安全思维

NPE 问题解决方案

Java 的解决方案

Java 中规避 NPE 有以下几种方案:

  • 判空:这是防御式编程的一种体现,应用范围也很广泛
  • 注解:借助 @Nullable@NotNull 之类的注解,通过 IDE 的提示帮我们规避 NPE
  • 封装数据:例如 1.8 中引入的 Optional,这种手段的核心思路就是封装数据,不再直接使用 null

这三种方案都无法令人满意。

Kotlin 的解决方案

Kotlin 的类型系统与 Java 有很大的不同。在 Kotlin 当中,同样是字符串类型,却有三种表示方法。

  • String:不可为空的字符串
  • String?:可能为空的字符串
  • String!:不知道是不是可能为空的字符串 -- 平台类型

Kotlin 通过让开发者必须明确规定每一个变量类型是否可能为空,来帮我们规避 NPE 问题。

Kotlin 解决方案的局限性

在不与 Kotlin 以外的环境进行交互时,Kotlin 编译器已经可以帮我们彻底消灭 NPE 了。

但在与 Java 等其他语言环境打交道时,问题就变得复杂了。

  • 语言角度上看:Kotlin 不仅会和 Java 交互,还可以与其他语言交互,如果其他语言没有可空的类型系统,就一定要警惕起来
  • 环境角度上看:Kotlin 可以与其他外界环境交互,这些外界的环境中的数据,往往也是没有可空类型系统的,这时候也要警惕起来

Kotlin 中的平台类型

在 Kotlin 中访问 Java 代码时,把 Java 中的所有未知是否可为空的类型,都看做是平台类型。平台类型用 ! 来表示,比如String!

Java 代码

class TestJava {
    @Nullable
    public static String get1(@Nullable String s) {
        return s;
    }

    @NotNull
    public static String get2(@NotNull String s) {
        return s;
    }

    public static String get3(String s) {
        return s;
    }
}

Kotlin 代码

get3() 方法没有任何 可空/非空 注解,它在 Kotlin 中会被认为是平台类型 String!

平台类型既能被当作可空类型,也能被当作不可空类型

fun main() {
    val s1: String? = TestJava.get1(null) // 可传 null
    val s2: String = TestJava.get2("bqt")
    val s3: String = TestJava.get2(null)  // 编译报错,不可传 null

    val s4: String? = TestJava.get3(null) // 既能被当作【可空类型】
    val s5: String = TestJava.get3("bqt") // 也能被当作【不可空类型】
}

Kotlin 中的非空断言

  • Kotlin 空安全调用语法:?.
  • Kotlin 非空安全的调用语法:!!.,这样的语法也叫做非空断言

如果使用非空断言强行调用,可能会产生空指针异常。

非空断言的引入场景

非空断言代码主要在以下两种情况下会被引入:

  • 使用 IDE 的 Convert Java File To Kotlin File 功能时,工具会自动生成带有非空断言的代码
  • 某些场景下,Smart Cast 会失效,导致我们主动使用了非空断言的代码

IDE 代码转换引入非空断言

public class JavaConvertExample {
    private String name = null;
    void init() {
        name = "";
    }
    void test() {
        if (name != null) {
            System.out.println(name.length());
        }
    }
}

上面 Java 代码通过 IDE 转换成 Kotlin 代码后为:

class JavaConvertExample {
    private var name: String? = null
    fun init() {
        name = ""
    }

    fun test() {
        if (name != null) {
            println(name!!.length) // 非空断言
        }
    }
}

Kotlin 支持 Smart Cast

Kotlin 支持 Smart Cast,如果已经在 if 中判断了局部变量不等于空,就可以在 if 语句中转换成非空类型了,例如:

fun main() {
    val tag: String? = listOf<String>().firstOrNull() // tag 是局部变量,使用 var 还是 val 修饰都可以
    if (tag != null) {
        println(tag.length)                           // 判断非空后,就可以被转换成【非空类型】了
    }
}

Smart Cast 失效的原因

但是,上面 IDE 自动转换的 Kotlin 代码中,如果将转换出来的非空断言语法删除掉,IDE 就报错了:

private var name: String? = null

fun test() {
    if (name != null) {
        println(name.length) // 编译报错
    }
}

Smart cast to 'String' is impossible 不可能发生的, because 'name' is a mutable property that could have been changed by this time

这是因为,前面的 tag 是局部变量,而下面的 name 是全局变量。这就导致,即使 if 中 name 已经判空了,后一行代码运行时,name 也可能已经被改变了(比如多线程场景)。所以此时没办法使用 Smart Cast

如何避免使用非空断言

借助 lateinit、懒加载,可以做到灵活初始化的同时,还能消灭可空类型。

① 改为函数传参的形式

fun test(name: String?) {    // 改为函数参数
    if (name != null) {
        println(name.length) // 函数参数肯定支持 Smart Cast
    }
}

函数的参数是不可变的,因此,以函数参数的形式传进来后,就可以用于 Smart Cast 了。

② 改为使用 val

private val name: String? = null // 将可变变量 var 改为不可变变量 val

fun test() {
    if (name != null) {
        println(name.length)     // val 肯定支持 Smart Cast
    }
}

③ 借助局部变量

private var name: String? = null

fun test() {
    var _name = name          // 定义一个局部变量,使用 var 还是 val 修饰都可以
    if (_name != null) {
        println(_name.length) // 局部变量肯定支持 Smart Cast
    }
}

④ 借助标准函数 let

private var name: String? = null

fun test() {
    name?.let { println(it.length) } // 使用标准函数 let
}

这种方式和第三种方式,从本质上来讲是相似的,都是定义了一个局部变量。

⑤ 延迟初始化 lateinit

private lateinit var name: String // 【延迟初始化】【不可空类型】的变量

fun test() = if (::name.isInitialized) println(name.length) else println("未初始化")

fun main() {
    test()         //【未初始化】
    name = "白乾涛" // 初始化
    test()         //【3】
}

这种方案,其实就是将判空问题变成了,判断是否已初始化的问题。

⑥ 懒加载委托 by lazy

 private val name: String by lazy { init() } // 【不可变】的【非空】属性

 private fun init() = "Tom"

 fun test() {
     println(name.length)
 }

借助这种方式,可以尽可能地延迟初始化,同时也消灭了可变性、可空性。

明确泛型的可空性

一个案例

fun <T> f1(data: T) {                             // 注意,泛型 T 是可为空的类型
    val set: java.util.TreeSet<T> = sortedSetOf() // 完全是 Java 中的 TreeSet
    set.add(data)                                 // TreeSet 内部无法存储 null
}

fun main() {
    f1("bqt") // 泛型实参自动推导为 String
    f1(null)  // 编译通过,运行时报 NullPointerException
}

上面代码中的泛型参数 T,是可为空的类型,所以可以将 null 作为参数传进去的。然而,由于 TreeSet 内部无法存储 null,所以在 set.add(data) 这里会抛出 NPE。

泛型的可空问题

实际上,泛型 <T> 等价于 <T: Any?>,因此,泛型的 T 可以接收 null 作为实参。

fun <T> f1(data: T) {}       // 两者等价
fun <T: Any?> f2(data: T) {} // 两者等价

为泛型增加上界 Any

为了解决泛型的可空问题,可以为泛型 T 增加上界 Any,这样当传入 null 时,编译器就会报错。

fun <T: Any> f1(data: T) {} // 增加泛型的边界限制【Any】

小结

Kotlin 的空安全思维,主要有四大准则:

  • 警惕 Kotlin 与外界的交互
  • 绝不使用非空断言 !!.
  • 尽可能使用非空类型
  • 明确泛型的可空性

2016-08-29

posted @ 2016-08-29 11:33  白乾涛  阅读(6835)  评论(0编辑  收藏  举报