类型系统#
类型系统作为编程语言的基础之一,经常被忽略, 随着 typescript 的风靡,类型系统的重要性也逐渐被更多开发者意识到。
毫无疑问,类型系统方面, kotlin 相对于 typescript 也只是个弟弟,但相对于 java 仍然有一些改进。
在介绍 kotlin 类型系统之前,有必要先介绍一下类型系统本身。
类型系统的重要性#
类型系统在编译期为数据提供保障,决定了数据如何解释。
例如我们有下面的类型:
// 只读数组接口
interface ReadonlyStrArray {
fun get(idx: Int): String
}
// 读写数组接口
interface ReadWriteStrArray: ReadonlyStrArray {
fun set(idx: Int, value: String)
}
这是两个普通的数组接口,一个是只读,一个是读写。然后我们可以写出这样的函数:
fun getStrArray(): ReadonlyStrArray {
val arr: ReadWriteStrArray = ...
return arr
}
val strArr = getStrArray()
strArr.set(...) // ERROR
作为开发者,我们知道 getStrArray 返回的是 可读写 的数组,它底层数据与 ReadWriteStrArray 一模一样。 但是由于类型系统的存在,我们无法调用这个对象的 set 接口!
正是因为类型系统给我们提供了这种约束,划分数据能力边界,使得组织大型软件更加简单。
关于类型#
类型是什么? 在编程语言理论中,类型是集合。
以一些常见的类型举例: Boolean, Int, String. 他们的集合形式是:
Boolean = {true, false}
Int = {-2^31, ..., -1, 0, 1, 2, ..., 2^31-1}
String = {"a", "b", ..., "aa", "ab", ...}
任何类型都可以看作一个集合
那...函数是什么? 当我们写下这样一个函数的时候:
fun numberToString(num: Int) {
return num.toString()
}
从集合角度考虑,它其实是从一个集合到另一个集合的映射: Int => String
。
那再看看另一个简单的函数:
fun doubleNumber(num: Int) {
return number*2
}
它将一个整数集合映射为另一个偶数集合。
所以,理想情况下,是不是也应该存在一种偶数类型: Even = {0, 2, -2, 4, -4, ...}
.
如此一来,这个函数就可以表达为 Int => Even
。
对于熟悉传统编程类型系统的人来说,可能会对这种说法感觉很怪异, 实际上,对于一个完备的类型系统,这是合理的,因为类型系统本身能够做到图灵完备 (虽然实践中略有预感,但是第一次看到这个结论被明确表达出来时我感到很震惊)。 但是图灵完备的类型系统,可能会导致编译期间类型推导陷入死循环,所以才有了众多残缺的类型系统。
这个理论的数学分支是<范畴论>, 后面不再深入介绍。这里重点介绍 kotlin 类型系统相对于 java 的改进。
回到类型本身,接下来要介绍几个奇怪的类型。
几个奇怪的类型#
问题 1: 从集合的角度来讲,Void 如何表达?
答案: Void = {singleValue}
, void 是一个只拥有一个元素的集合(Boolean 稍微比它强一些,有两个)。
里面的 singleValue 可以是任何唯一值, 这乍一看可能有点反直觉,可能很多人(包括我)会下意识认为 Void 应该对应一个空集合。
问题 2: 为什么 Void 集合里面有一个值?
它的解释过程是这样的。
先说一个普通函数,例如 Int => String
, 任何一个 Int 都会映射到一个 String,这个函数本身存在的意义就是让我们得到那个 String;
而对于 Void 函数: Int => Void
, 因为 Void 集合本身只有一个值,所以即使这个函数不执行,
我们也知道输入的 Int 会映射到的目标值(因为 Void 集合本身只有一个值)。这种返回值的意义不是很大,
实践中这种函数的意义都是通过 副作用 体现,例如 deleteFile(file: String): Void
.
问题 3: 既然 Void 不能是空集合,那空集合对应了什么类型?
这就对了! 在数学上 Int => ∅
是无意义的! 因为目标是一个空集合,压根没法映射。
如果我们写出了这样一个函数,意味着这个函数压根没法返回!
可是,真的有这样的函数吗? ... 还真有! 至少有两种情况: 死循环、强行退出。
fun deadLoop(): "∅" {
while (true) {
...
}
}
fun notImplementFunc(): "∅" {
throw NotImplementError()
}
fun errorExit(): "∅" {
exit(1)
}
why#
所以这些花里胡哨的东西有什么用呢?
Unit#
在 kotlin 中, Void
更名为 Unit
, 从语义上表达了它是一个单一值的集合。
并且由于 Unit 是按照标准模型实现,这允许我们得到一个全局的 Unit 实例单例,这在泛型中尤其有用。 例如 Http 返回值, 通常会定义一个泛型结构将数据存储在泛型字段 data 中:
class WebResult<T>(val code: Int, val msg: String, val data: T)
但是,如果有一个接口不需要返回任何数据怎么办呢? 我们可以实例化 WebResult<Unit>(0, "ok", Unit)
,
而在 java 中, 我们可以声明 WebResult<Void>
, 但没有办法实例化 Void, 只能用 null 将就一下.
Nothing#
kotlin 中空集类型为 Nothing
, 对应 typescript 的 never
.
它的应用场景是这样的. 下面这个例子是一个做加减乘除的函数:
fun binCalc(left: Int, right: Int, op: String): Int {
return when (op) {
"+" -> left+right
"-" -> left-right
"*" -> left*right
"/" -> left/right
else -> throw IllegalArgumentError()
}
}
一般来说调用方是绝不会传入非法值,所以上面的 else 分支只是一种保护性措施。
对于这种非法状态,如果我们想将它封装为一个更可读、可复用的函数怎么办呢? 例如:
fun YOU_SHOULD_NOT_BE_HERE(): ??? {
throw IllegalArgumentError("you should not be here, maybe it's a bug")
}
fun binCalc(left: Int, right: Int, op: String): Int {
return when (op) {
...
else -> YOU_SHOULD_NOT_BE_HERE()
}
}
会发现,在 java 中压根实现不了这样的函数! 无法通过类型检查,而在 kotlin 中却可以, 这都得益于 Nothing:
fun YOU_SHOULD_NOT_BE_HERE(): Nothing {
throw IllegalArgumentError("you should not be here, maybe it's a bug")
}
kotlin 的 TODO
功能就借助了 Nothing 实现
非空#
咱们可以先思考一下 java 中的类型,以 Integer
为例, 其实它并不是一个纯粹的整数集合,
而是 Integer = {null} | {0, 1, -1, 2, -2, ...}
, 这导致了 java 类型的不纯粹,
要命的是,java 没有办法表达纯粹的 Integer 类型,
这种隐含的不纯粹也导致 java 在函数式编程上举步维艰。
不过,java 有自己的解决方案,通过引入 @NotNull
,以及 Optional
.
这两种方案都有各自的问题, @NotNull
只是一种标准,并未提供语言级别实现,
而且实现方式也是运行期检查,已经脱离了类型系统和编译期的范畴。
Optional
的问题在于它发挥作用需要团队自发的编程约定,而且由于 Optional 本质上是一种全新的类型,
Optional<Int>
与 Int
之间并不具备直接替换性(不具备父子关系,不适用里氏代换法则)。
又由于泛型擦除, 我们并不知道 Optional<T>
中的 T 是什么, 会导致诸如 T => Optional<T>
的函数返回多层 Optional 嵌套: Optonal<Optonal<Int>>
. 虽然用户知道 Optional 无论嵌套多少层都是等价的,
但语言编译检查无法理解。
而 kotlin 提供了纯粹的类型: Int = {0, 1, -1, 2, -2, ...}
,
如果需要类似 java 的可空类型,可以加上问号: Int? = {null} | {0, 1, -1, 2, -2, ...}
。
在语言级别支持了非空类型以及 Optional,且它们之间具备继承关系。
对于 null 的其他支持#
?.
操作符
obj?.member
等价于obj != null? obj.member : null
, 例如:val num = text?.toInt()?.inc()
!!
操作符
对于第三方返回的可空值,如
val text: String?
, 如果我们业务逻辑上知道它必不为空, 可以通过!!
断言转换为非空值,例如"hello " + text!!
val num = text!!.toInt().inc()
.
?:
操作符
一般用于函数入口处的保护判断,后面可接 return/throw, 例如数据库没有查到会返回 null 的情况:
fun getUser(id: Int?): User { val user = userRepository.findById(id)?: throw NotFoundError() val user = userRepository.findById(id)?: return AnoymousUser() }也可以在为 null 的时候取默认值:
val num: Int = text?.toInt() ?: 0
总结#
总体上 kotlin 在类型系统上解决了一些痛点,不过改进比较有限。
如果想体验另一些 更好/更强大的类型系统 ,强烈推荐了解一下 typescript, 如果条件放宽一些,不要求 更好 也可以尝试了解一下 C++ template。