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

本文地址


目录

Kotlin 空安全思维

Java 的空安全思维

Java 中规避 NPE 的几个方案:

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

Java 解决 NPE 的三种思路都无法令人满意。

其实,我们需要的是一种简洁,且能为每一种类型都标明可空性的方式。这样一来,我们自然而然就能想到一个更好的方案,那就是:从类型系统下手。

Kotlin 的空安全思维

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

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

Kotlin 这样的类型系统,让开发者必须明确规定每一个变量类型是否可能为空,通过这样的方式,Kotlin 编译器就能帮我们规避 NPE 了。

在不与 Kotlin 以外的环境进行交互的情况下,仅仅只是纯 Kotlin 开发当中,Kotlin 编译器已经可以帮我们消灭 NPE 了。不过,与 Java 等其他语言环境打交道时,问题就比较复杂了。

Kotlin 中的平台类型

Java 中不存在可空类型这个概念,因此,在 Kotlin 中,我们把 Java 中的所有未知可空性的类型,都看做是平台类型。平台类型用 ! 来表示,比如String!

class Test {
	@Nullable
	public static String getNullableString(@Nullable String s) {
		return s + "Nullable"; // 使用 @Nullable 修饰,代表参数和返回值可能为空
	}

	@NotNull
	public static String getNotNullString(@NotNull String s) {
		return s + "NotNull"; // 使用 @NotNull 修饰,代表参数和返回值不可能为空
	}

	public static String getMsg(String s) {
		return s + "Kotlin";  // 直接返回了一个字符串,但是没有用可空注解标注
	}
}

由于 Java 中的 getMsg() 方法没有任何可空注解,因此,它在 Kotlin 中会被认为是平台类型 String!

对于平台类型,Kotlin 会认为,它 既能被当作可空类型(例如 String?),也可以被当作不可空类型(例如 String)。

以上方法在 Kotlin 调用的时候,就出现以下几种情况:

fun main() {
    val s1: String? = Test.getNullableString(null) // 可传 null,返回值为可空类型
    val s2: String = Test.getNotNullString("Hey,") // 不可传 null,返回值为不可空类型

    val s3: String? = Test.getMsg(null) // 返回值可以为【可空类型】,也可以为【不可空类型】
    val s4: String = Test.getMsg("bqt") // 返回值可以为【可空类型】,也可以为【不可空类型】
}

Kotlin 空安全的第一条准则:警惕 Kotlin 以外的数据类型

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

Kotlin 中的非空断言

Kotlin 空安全的第二条准则:绝不使用非空断言

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

下面代码中,如果使用非空断言,强行调用可为空的 String? 类型的成员,就会产生空指针异常。

fun testNPE(msg: String?) {
    println(msg?.length)  // 空安全调用语法,打印 null
    println(msg!!.length) // 非空断言,非空安全的调用语法,报 NPE
}

fun main() {
    testNPE(null)
}

在 Kotlin 代码中,我们应坚持 绝不使用非空断言非空断言代码主要在两种情况下会被引入:

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

IDE 的代码转换功能

当我们借助 IDE 的 Convert Java File To Kotlin File 时,这个工具可能会自动帮我们生成带有非空断言的代码。

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 代码以后,test() 方法中出现了非空断言。如果我们在转换完代码以后,没有 review,尽可能将非空断言带到生产环境中。

Smart Cast 失效原因

Kotlin 是支持 Smart Cast 的,如果我们已经在 if 中判断了 name 不等于空,那么,就可以被转换成非空类型了,例如:

import kotlin.random.Random

fun getName() = if (Random.nextBoolean()) null else "bqt"

fun main() {
    val name: String? = getName() // name 是局部变量,至于是 var 还是 val 都可以
    if (name != null) {
        val tem: String = name    // 判断非空后,就可以被转换成【非空类型】了
        println(name.length)
    }
}

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

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

我们将代码简化一下:

import kotlin.random.Random

var city: String? = if (Random.nextBoolean()) null else "sz" // city 是全局变量(或成员属性)
fun main() {
    if (city != null) {
        println(city.length) // 编译报错
    }
}

可以发现,两者的核心区别是,上面的 name 是局部变量,而下面的 city 是可变的全局变量。这就导致,即使前一行代码中 city 已经判空了,后一行代码运行时,city 也可能已经被改变了(比如多线程)。所以此时没办法使用 Smart Cast

如何避免使用非空断言

Kotlin 空安全的第三条准则:尽可能使用非空类型

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

① 改为函数传参的形式

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

函数的参数是不可变的,因此,当我们将外部的成员变量或者全局变量,以函数参数的形式传进来后,就可以用于 Smart Cast 了。

② 改为使用不可变变量

class JavaConvertExample {
    private val name: String? = null // 将可变变量 var 改为不可变变量 val
    fun test() {
        if (name != null) {
            println(name.length)     // 不可变变量支持 Smart Cast
        }
    }
}

③ 借助临时的不可变变量

class JavaConvertExample {
    private var name: String? = null
    fun test() {
        val _name = name          // 定义一个临时的【不可变变量】
        if (_name != null) {
            println(_name.length) // 使用临时变量的【不可变变量】
        }
    }
}

④ 借助标准函数 let

class JavaConvertExample {
    private var name: String? = null
    fun test() {
        name?.let { println(it.length) } // 使用标准函数 let
    }
}

这种方式和第三种方式,从本质上来讲是相似的,但是使用 let 的实现更加优雅。

⑤ 借助 lateinit 关键字

class JavaConvertExample {
    private lateinit var name: String // 【稍后初始化】【不可空类型】的变量
    fun init() {
        name = "Tom"
    }
    fun test() {
        if (::name.isInitialized) println(name.length) else println("not init")
    }
}

fun main() {
    val example = JavaConvertExample()
    example.test() // not init
    example.init()
    example.test() // 3
}

这种思路其实是完全抛弃可空性的。由于它的类型是不可为空的,因此我们初始化的时候,必须传入一个非空的值,这就能保证:只要 name 初始化了,它的值就一定不为空。在这种情况下,我们就将判空问题变成了一个判断是否初始化的问题。

⑥ 使用 by lazy 委托

class JavaConvertExample {
    private val name: String by lazy { init() } // 【不可变】的【非空】属性
    private fun init() = "Tom"
    fun test() {
        println(name.length)
    }
}

我们将 name 这个变量声明为了不可变的非空属性,并且,借助 Kotlin 的懒加载委托来完成初始化。借助这种方式,我们可以尽可能地延迟初始化,同时,也消灭了可变性、可空性。

明确泛型的可空性

Kotlin 空安全的第四条准则:明确泛型可空性

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

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

上面的代码中,我们定义的泛型参数 T,是可为空的类型。所以,我们可以将 null 作为参数传进去的,并且编译器也不会报错。紧接着,由于 TreeSet 内部无法存储 null,所以我们的代码在 set.add(data) 这里,会产生空指针异常。

实际上,我们的 T 是等价于 <T: Any?> 的,这也就意味着,泛型的 T 是可以接收 null 作为实参的。

fun <T> saveSomething(data: T) {}
// 等价于
fun <T: Any?> saveSomething(data: T) {}

所以,正确的写法是,为泛型 T 增加上界 Any,这样当我们尝试传入 null 的时候,编译器就会报错,让这个问题在编译期就能暴露出来。

fun <T: Any> saveSomething(data: T) { // 增加泛型的边界限制【Any】,不增加时,默认为【Any?】
    val set = sortedSetOf<T>()
    set.add(data)
}

小结

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

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

2016-08-29

原文地址:https://www.cnblogs.com/baiqiantao/p/5817373.html