本文由 Prefert 发表在 ScalaCool 团队博客。html
不管在静态语言仍是动态语言中,「类型系统」都起到了相当重要的做用。编程
在计算机科学中,类型系统用于定义如何将编程语言中的数值和表达式归类为许多不一样的类型,如何操做这些类型,这些类型如何互相做用。安全
类型能够确认一个值或者一组值具备特定的意义和目的(虽然某些类型,如抽象类型和函数类型,在程序运行中,可能不表示为值)。闭包
类型系统在各类语言之间存在比较大的差别。最主要的差别存在于编译时期的语法,以及运行时期的操做实现方式。咱们能够简单理解为两个部分:jvm
可是他们的目的都是一致的:编程语言
1. 安全。有了类型系统之后就能够实现类型安全,这时候程序就变成了一个严格的数学证实过程,编译器能够机械地验证程序某种程度的正确性,从而杜绝不少错误的发生。好比:Scala、Java。可是 JavaScript 等动态语言/弱类型语言就要借助其余插件(如 ESLint)来提示语法等错误。ide
2. 抽象能力。在安全的前提下,一个强大的类型系统的标准是抽象能力,能将程序中的不少东西归入安全的类型系统中进行抽象,这在安全性的前提下又不损耗灵活性,甚至性能也能很优化。动态语言的抽象能力能够很强,但安全性和性能就不行了。泛型、高阶函数(闭包)、类型类、Monad
、Lifetime
(Rust) 属于这一块。函数
3. 工程能力。一个强类型的编程语言比动态类型的语言更适合大规模软件的构建,哪怕不存在性能问题,可是一样取决于前两点。性能
Hint: 想深刻了解类型系统的朋友能够参考 《Type Systems》和 《Types and Programming》学习
Kotlin 做为一门静态类型编程语言,一样拥有着强大的类型系统。
你可能会对类型后面的 ?
产生疑问,那咱们就先来看看 Kotlin 中的可空类型。
Int?
Boolean?
及其余许多编程语言中最多见的陷阱之一是访问空引用的成员,致使空引用异常。在 Java 中,这被称做 NullPointerException
或简称 NPE
。
Kotlin 的类型系统旨在从咱们的代码中消除 NullPointerException
。
NPE 发生的缘由多是
throw NullPointerException();
!!
操做符(要求抛出 NullPointerException
)与 Java 不一样,Kotlin 区分非空(non-null)和可空(nullable)类型。到目前为止,咱们看到的类型都是非空类型,Kotlin 不容许 null 做为这些类型的值。访问非空类型的变量将永远不会抛出空指针异常。
因为 null
只能被存储在 Java 的引用类型的变量中,因此在 Kotlin 中基本数据的可空版本都会使用该类型的包装形式。
一样的,若是你用基本数据类型做为泛型类的类型参数,Kotlin 一样会使用该类型的包装形式。
咱们能够在任何类型后面加上?
,好比Int?
,实际上等同于Int? = Int or null
,经过合理的使用,咱们可以简化不少判空代码。而且咱们可以有效规避 NullPointerException
致使的崩溃。
接下去让咱们看看,非空的原理到底怎么样的。
对于如下一段 Kotlin 代码:
fun testNullable1(x: String, y: String?): Int {
return x.length
}
fun testNullable2(x: String, y: String?): Int? {
return y?.length
}
fun testNullable3(x: String, y: String?): Int? {
return y!!.length
}
复制代码
咱们利用 Idea 反编译后,产生的 Java 代码以下:
public final class NullableTypesKt {
public static final int testNullable1(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x"); // 若是为 null, 抛出异常
return x.length();
}
@Nullable
public static final Integer testNullable2(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x");
return y != null?Integer.valueOf(y.length()):null;
}
@Nullable
public static final Integer testNullable3(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x");
if(y == null) {
Intrinsics.throwNpe();
}
return Integer.valueOf(y.length());
}
}
复制代码
能够看到,在不可空变量调用函数以前,都使用 kotlin.jvm.internal.Intrinsics
类里面的 checkParameterIsNotNull
方法检查是否为 null
,若是是 null
则抛出异常:
public static void checkParameterIsNotNull(Object value, String paramName) {
if (value == null) {
throwParameterIsNullException(paramName);
}
}
复制代码
基于可空类型,Kotlin 才拥有不少促使安全的运算符。
?.
—— 安全调用?.
容许咱们把一次 null
检查和一次方法的调用合并成一个操做,好比:
str?.toUpperCase()
等同于 if (str != null) str.toUpperCase() else null
固然,?.
一样能够处理属性:
class User(val nickname: String, val master: User?)
fun masterInfo(user: User): String? = user.master?.nickname
// test
val ceo = User("boss", null)
val employee = User("employee-1", ceo)
println(masterInfo(employee)) // boss
println(masterInfo(ceo)) // null
复制代码
?:
—— Elvis 运算符刚开始我也不知道为何称之为「Elvis 」运算符——直到我看到了这张图...
若是你不喜欢这个名字,咱们也能够叫它——「null 合并运算符」。若是你学习过 Scala,这相似于 getOrElse
:
fun getOrElse(str: String?) {
val result: String = str ?: "" // 等价于 str == null ? "" : str
}
复制代码
另外还有as?
(安全转换)、!!
(非空断言)、let
、lateinit
(延迟初始化属性)等此处就不详细介绍。
Int
, Boolean
及其余咱们都知道,Java 将 基本数据类型 和 引用类型 作了区分:
在 Kotlin 中,并不区分基本数据类型和包装类型 —— 你使用的永远是同一个类型。
Kotlin 中咱们必须使用 显示转换 来对数字进行转换,例:
fun main(args: Array<String>) {
val z = 13
println(z.toLong() in list(9L, 5L, 2L))
}
复制代码
若是以为这种方式不够简便,你也能够尝试使用 Kotlin 中的字面量:
L
表示 Long
: 123L
F
表示 Float
: .123f
、1e3f
0x
/ 0X
表示十六进制:0xadcL
当你使用字面量去初始化一个类型已知的变量,或是把字面量做为实参传给函数时 ,会发生隐式转换,而且算数运算符会被重载。 例:
fun long(l: Long) = println(1)
fun main(args: Array<String>) {
val b: Byte = 1 // Int -> Byte
val l = b + 1L // 重载 plus 运算符
foo(234)
}
复制代码
Any
, Any?
和 Object
做为 Java 类层级结构的顶层相似,Any
类型是 Kotlin 中 全部非空类型(ex: String
, Int
) 的顶级类型——超类。
与 Java 不一样的是: Kotlin 不区分「原始类型」(primitive type)和其它的类型。它们都是同一类型层级结构的一部分。
若是定义了一个没有指定父类型的类型,则该类型将是 Any
的直接子类型:
class Fruit(val weight: Double)
复制代码
若是你为定义的类型指定了父类型,则该父类型将是新类型的直接父类型,可是新类型的最终祖先为 Any
。
abstract class Fruit(val weight: Double)
class Banana(weight: Double, val size: Double): Fruit(weight)
class Peach(weight: Double, val color: String): Fruit(weight)
复制代码
若是你的类型实现了多个接口,那么它将具备多个直接的父类型,而 Any
一样是最终的祖先。
interface ICanGoInASalad
interface ICanBeSunDried
class Tomato(weight: Double): Fruit(weight), ICanGoInASalad, ICanBeSunDried
复制代码
Kotlin 的 Type Checker 强制执行父子关系。
例如: 你能够将子类型值存储到父类型变量中:
var f: Fruit = Banana(weight = 0.1)
f = Peach(weight = 0.15)
复制代码
可是你不能将父类型值存储到子类型变量中:
val b = Banana(weight=0.1)
val f: Fruit = b
val b2: Banana = f
// Error: Type mismatch: inferred type is Fruit but Banana was expected
复制代码
正好也符合咱们的平常理解:“香蕉是水果,水果不是香蕉。”
另外,Kotlin 把 Java 方法参数和返回类型中用到的 Object
类型看做 Any
(更确切地是当作「平台类型」)。当 Kotlin 函数函数中使用 Any
时,它会被编译成 Java 字节码中的 Object
。
Hint: 平台类型本质上就是 Kotlin 不知道可控性信息的类型 —— 全部 Java 引用类型在 Kotlin 中都表现为平台类型。
上面提到:在 Kotlin 中, Any
是全部 非空类型 的超类。
你可能会有疑问: null
类型的父类是什么呢?
Kotlin 是一种表达式导向的语言,全部流程控制语句都是表达式。它没有 Java 和 C 中的 void
函数,函数老是会返回一个值。有时候函数并无计算任何东西 —— 这被咱们称做他们的反作用(side effect),这时将会返回 Unit
——具备单一值的类型。
大多数状况下,你不须要明确指定 Unit
做为返回类型或从函数返回 Unit
。若是编写的函数具备块代码体,而且不指定返回类型,则编译器会将其视为返回 Unit
类型,不然编译器会使用推断的类型。
fun example() {
println("block body and no explicit return type, so returns Unit")
}
val u: Unit = example()
复制代码
Unit
并没什么特别之处。就像任何其余类型同样,它是 Any
的子类型,而 Unit?
是 Any?
的子类型。
然而 Unit?
类型倒是一个奇怪的特殊例子,这是 Kotlin 的类型系统一致性的结果。Unit?
类型只有两个值:Unit
单例和 null
。我暂时还没发现使用 Unit?
类型的地方,可是在类型系统中没有特殊的 void 这一事实,使得处理各类函数泛型变得更加容易。
在 Kotlin 类型层级结构的最底层是 Nothing
类型。
顾名思义,Nothing
是没有实例的类型。Nothing
类型的表达式不会产生任何值。
注意 Unit
和 Nothing
之间的区别,对 Unit
类型的表达式求值将返回 Unit
的单例,而对 Nothing
类型的表达式求值则永远都不会返回。
这意味着任何类型为 Nothing
的表达式以后的全部代码都是没法获得执行的(unreachable code),编译器和 IDE 会向你发出警告。
什么样的表达式类型为 Nothing
呢?流程控制中与跳转相关的表达式。
例如 throw
关键字会中断表达式的计算,并从函数中抛出异常。所以 throw
就是 Nothing
类型的表达式。
经过将 Nothing
做为全部类型的子类型,类型系统容许程序中的任何表达求值失败。例如: JVM 在计算表达式时内存不足,或者是有人拔掉了计算机的电源插头。这也意味着咱们能够从任何表达式中抛出异常。
fun formatCell(value: Double): String =
if (value.isNaN())
throw IllegalArgumentException("$value is not a number")
else
value.toString()
复制代码
你可能会惊奇地发现,return
语句的类型也为 Nothing
。return
是一个流程控制语句,它当即从函数中返回一个值,打断其所在表达式的求值。
fun formatCellRounded(value: Double): String =
val rounded: Long = if (value.isNaN()) return "#ERROR" else Math.round(value)
rounded.toString()
复制代码
进入无限循环或杀死当前进程的函数返回类型也为 Nothing。例如 Kotlin 标准库将 exitProcess
函数声明为:
fun exitProcess(status: Int): Nothing
复制代码
若是你编写返回 Nothing
的自定义函数,编译器一样能检查出调用函数后没法获得执行的代码,就像使用语言自己的流程控制语句同样。
inline fun forever(action: ()->Unit): Nothing {
while(true) action()
}
fun example() {
forever {
println("doing...")
}
println("done") // Warning: Unreachable code
}
复制代码
与空安全同样,不可达代码分析是类型系统的一个特性。无需像 Java 同样在编译器和 IDE 中使用一些手段进行特殊处理。
Nothing
像任何其余类型同样,若是容许其为空则能够获得对应的类型 Nothing?
。Nothing?
只能包含一个值:null
。事实上 Nothing?
就是 null
的类型。
Nothing?
是全部可空类型的最终子类型,因此咱们可使用 null 做为任何可空类型的值。
若是你仍是对 Kotlin 类型系统不够清晰,下面这张图可能会对你有所帮助:
做为「Better Java」,Kotlin 的类型系统更加简洁,同时为了提升代码的安全性、可靠性,引入了一些新的特性(ex. Nullable Types 和 Immutable Collection)。
咱们将在下一篇详细介绍 Kotlin 中的集合。
参考: