Kotlin Reference(二)-基础部分
标签(空格分隔): 翻译 kotlin
译者:陈小默
版权声明:禁止商用,转载请注明出处
数据类型(Basic Types)
在Kotlin中,万物皆对象,在此定义上我们可以访问任何变量的函数和属性。某些内建类型使用时看起来就像是普通的类一样。在这一节,我们将回去介绍这些类型:numbers, characters, booleans and arrays.
Numbers
Kotlin对于数字的处理十分接近java,但又不完全相同。例如,Kotlin不会隐式的在类型转换时扩大数字的长度,并且在某些情况下数字的字面表示方式的方式也有些不同。
Type | Bit width |
---|---|
Double | 64 |
Float | 32 |
Long | 64 |
Int | 32 |
Short | 16 |
Byte | 8 |
注:在Kotlin中字符不再表示为数字
常量书写
书写整型常量有如下几种方式
十进制书写: 123
– 使用L
书写十进制长整型: 123L十六进制书写: 0x0F
- 二进制书写: 0b00001011
注: 不支持八进制书写方式.
Kotlin还支持传统的浮点型标记
– Doubles by default: 123.5
, 123.5e10
– Floats are tagged by f
or F
: 123.5f
表现形式
在Java平台上,数字是以物理形式存储在JVM当中的原始数据类型,除非我们需要一个允许为空的数字引用或者相关的类型。而后者是一种装箱类型。
注:数字装箱过程并不能保证数据一致:
val a: Int = 10000
print(a === a) // Prints 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA === anotherBoxedA) // !!!Prints 'false'!!!
另一方面,他们保存的值是相等的:
val a: Int = 10000
print(a == a) // Prints 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA) // Prints 'true'
显式类型转换
由于不同的表示方式,较小的类型不会被看作是大类型的子类型。如果是的话,我们会遇到如下所示的问题:
// 假想代码,实际无法编译:
val a: Int? = 1 // 装箱类型 (java.lang.Integer)
val b: Long? = a // 隐士的数据转换产生一个Long的装箱类型 (java.lang.Long)
print(a == b) // 令人意外的是,这里打印了‘false’因为Long的equals()方法会检查对方是否也是Long类型
这样一来,在发生类型转换的地方不仅是同一性,甚至连数据相等都无法保持了。
因此,小的数据类型无法被隐式的提升为较大的类型。这意味着我们不能明确指定一个Byte类型的数据赋值给Int变量
val b: Byte = 1 // 正确,字面值会被静态检查
val i: Int = b // 错误
我们可以显式扩大数字的宽度
val i: Int = b.toInt() // 正确,显式扩大数字宽度
所有的数字类型都支持一下转换方式
- toByte(): Byte
- toShort(): Short
- toInt(): Int
- toLong(): Long
- toFloat(): Float
- toDouble(): Double
- toChar(): Char
隐式转换的缺乏并没有引起太大的注意,因为这些类型可以通过上下文被推断出来,并且这里的数学运算符为了适应类型的转换而进行了重载。例如:
val l = 1L + 3 // Long + Int => Long
运算符
Kotlin支持一套标准的数学运算符,这些运算被定义成了相关类的成员方法(但是编译器将这些函数优化为了对应的运算指令)
对于位运算符,没有用特别的字符去表示,而仅仅是用以函数名中缀的方式表示,例如
val x = (1 shl 2) and 0x000FF000
以下是位运算的完整列表 (仅适用于Int
和Long
类型):
shl(bits)
– 带符号左移 (Java’s<<
)shr(bits)
– 带符号右移 (Java’s>>
)ushr(bits)
– 无符号右移 (Java’s>>>
)and(bits)
– 按位与 andor(bits)
– 按位或 orxor(bits)
– 按位异或 xorinv()
– 按位取反 inversion
Character
字符使用 Char
类型表达. 字符不能直接当作数值使用
fun check(c: Char) {
if (c == 1) { // 错误:类型不一致
// ...
}
}
字符的字面值(literal)使用单引号表达: '1'
. 特殊字符使用反斜线转义表达. Kotlin 支持的转义字符包括: \t
, \b
, \n
, \r
, \'
, \"
, \\
以及 \$
. 其他任何字符, 都可以使用 Unicode 转义表达方式: '\uFF00
’
我们可以显式的将字符转换为Int
型数字
fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() // 显式的转换为数字
}
与数字一样,当需要一个可以为null的引用时,字符会被装箱。装箱操作同样不会保留同一性
Booleans
Boolean 类型用来表示布尔值, 有两个可能的值: true
和 false
.
当需要一个可为 null
的布尔值引用时, 布尔值也会被装箱(box).
布尔值的内建运算符包括
||
– 短路或运算&&
– 短路与运算!
- 非运算
Arrays
Kotlin 中的数组通过 Array
类表达, 这个类拥有 get
和 set
函数(这些函数通过运算符重载转换为 []
运算符), 此外还有 size
属性, 以及其他一些有用的成员函数:
class Array<T> private constructor() {
val size: Int
fun get(index: Int): T
fun set(index: Int, value: T): Unit
fun iterator(): Iterator<T>
// ...
}
要创建一个数组, 我们可以使用库函数 arrayOf()
, 并向这个函数传递一些参数来指定数组元素的值, 所以 arrayOf(1, 2, 3)
将创建一个数组, 其中的元素为 [1, 2, 3]. 或者, 也可以使用库函数 arrayOfNulls()
来创建一个指定长度的数组, 其中的元素全部为 null 值.
另一种方案是使用一个工厂函数, 第一个参数为数组大小, 第二个参数是另一个函数, 这个函数接受数组元素下标作为自己的输入参数, 然后返回这个下标对应的数组元素的初始值:
// Creates an Array<String> with values ["0", "1", "4", "9", "16"]
val asc = Array(5, { i -> (i * i).toString() })
我们在前面提到过, []
运算符可以用来调用数组的成员函数 get()
和 set()
.
注意: 与 Java 不同, Kotlin 中数组的类型是不可变的. 所以 Kotlin 不允许将一个 Array<String>
赋值给一个 Array<Any>
, 否则可能会导致运行时错误(但你可以使用 Array<out Any>
, 参见 类型投射).
Kotlin 中也有专门的类来表达基本数据类型的数组: ByteArray
, ShortArray
, IntArray
等等, 这些数组可以避免数值对象装箱带来的性能损耗. 这些类与 Array 类之间不存在继承关系, 但它们的方法和属性是一致的. 各个基本数据类型的数组类都有对应的工厂函数:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]
Strings
字符串由 String 类型表示. 字符串的内容是不可变的. 字符串中的元素是字符, 可以通过下标操作符来访问: s[i]. 可以使用 for 循环来遍历字符串:
for (c in str) {
println(c)
}
字符串的字面值
Kotlin 中存在两种字符串字面值: 一种称为转义字符串(escaped string), 其中可以包含转义字符, 另一种成为原生字符串(raw string), 其内容可以包含换行符和任意文本. 转义字符串(escaped string) 与 Java 的字符串非常类似:
val s = "Hello, world!\n"
转义字符使用通常的反斜线方式表示. 关于 Kotlin 支持的转义字符, 请参见上文的 Character
小节.
原生字符串(raw string)由三重引号表示("""
), 其内容不转义, 可以包含换行符和任意字符:
val text = """
for (c in "foo")
print(c)
"""
你可以使用 trimMargin() 函数来删除字符串的前导空白
val text = """
|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin()
默认情况下, 会使用 | 作为前导空白的标记前缀, 但你可以通过参数指定使用其它字符, 比如 trimMargin(">")
.
字符串模板
字符串内可以包含模板表达式, 也就是说, 可以包含一小段代码, 这段代码会被执行, 其计算结果将被拼接为字符串内容的一部分. 模板表达式以 $ 符号开始, $ 符号之后可以是一个简单的变量名:
val i = 10
val s = "i = $i" // evaluates to "i = 10"
$ 符号之后也可以是任意的表达式, 由大括号括起:
val s = "abc"
val str = "$s.length is ${s.length}" // evaluates to "abc.length is 3"
原生字符串(raw string)和转义字符串(escaped string)内都支持模板. 由于原生字符串无法使用反斜线转义表达方式, 如果你想在字符串内表示 $ 字符本身, 可以使用以下语法:
val price = """
${'$'}9.99
"""
包(Package)
一个包的声明应该位于源文件的开始:
package foo.bar
fun baz() {}
class Goo {}
// ...
所有的一切内容(包括类和方法)都被包含在所声明的包之内。所以,在上面的示例中,方法 baz()
的全路径名为 foo.bar.baz
,Goo
类的全路径名为 foo.bar.Goo
如果没有明确的指明包信息,则这些文件的全部内容将属于一个没有名字的默认包
Imports
除了默认导入的包之外,任何文件都可以有自己的import指令。
我们可以使用具体的名称导入一个单独的文件
import foo.Bar // Bar is now accessible without qualification
也可以导入某个范围内(包、类、对象 等等)所有可以访问的内容
import foo.* // everything in 'foo' becomes accessible
如果发生名称冲突,我们可以使用一个as
关键字重命名来区分冲突实体
import foo.Bar // Bar is accessible
import bar.Bar as bBar // bBar stands for 'bar.Bar'
import关键字并不仅限于导入类;同样的,你也可以导入其他声明
- 顶级(top-level)函数和属性
- 类中声明的方法和属性
- 枚举常量
不同于Java的是,Kotlin没有单独的“import static” 语法;所有的声明引入都使用同一的import
关键字
顶级声明的可见范围
如果一个顶级声明被标记为private,那么他将只能在被声明的文件中访问,即成为私有
控制流程
If 表达式
在Kotlin中,if
是一个表达式,也就是说,他有返回值。因此Kotlin中取消了三元表达式(条件 ? then : else), 因为 if
能起到更好的作用。
//传统的使用方式
var max = a
if (a < b)
max = b
// 使用else
var max: Int
if (a > b)
max = a
else
max = b
// 使用if表达式
val max = if (a > b) a else b
if 语句分支可以是代码块,并且代码块的最后一个表达式就是返回值
val max = if (a > b) {
print("Choose a")
a
}
else {
print("Choose b")
b
}
如果你使用if
作为表达式而不是条件语句(例如,最为函数返回值或者给变量赋值),则这个表达式要求必须包含一个else
分支
When 表达式
when
表达式替换了类C语言的switch-case
表达式。一个最简单的例子看起来就像这样
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意代码块
print("x is neither 1 nor 2")
}
}
when
表达式会顺序匹配所有分支知道某一个分支条件满足。when
既可以作为表达式也可以作为流程控制语句。如果它是作为一个表达式,则满足条件的分支的值就成为了整个表达式的值。如果它作为流程控制语句,则其分支的返回值将会被忽略。(就像if
表达式那样,每个分支都可以是代码块,并且代码块最后一个表达式的值将成为返回值)
如果其他分支条件全都不满足的话就会调用else
分支。如果when
被用作表达式,则要求必须存在else
分支,除非编译器能证明分支的条件满足了全部的可能性。
如果多个条件具有相同的处理方式,则分支条件之间可以用逗号分隔
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
我们可以使用任意表达式(不仅仅是常量)作为分支条件
when (x) {
parseInt(s) -> print("s encodes x")
else -> print("s does not encode x")
}
我们也可以使用in
或者!in
判断条件在或者不在某一个区间和集合内
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
另外还可以使用is或者!is判断值是不是属于一个具体的类型。注:由于Kotlin的智能转型,你可以直接访问该类型的方法和属性而不用进行额外的检查。
val hasPrefix = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
我们也可以用它来替代if-else if链式表达式。如果没有声明参数,则所有分支条件都是单纯的boolean表达式,并且当分支条件为true时执行分支。
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
For 循环
for循环能够迭代一切能产生迭代器的数据。语法如下所示
for (item in collection)
print(item)
函数体可以是代码块
for (item: Int in ints) {
// ...
}
就像前面提到的,for
表达式可以迭代任何能产生迭代器的数据,也就是说参数需要提供一个iterator()
方法,并且这个iterator()
方法返回的数据包括一个能够产生数据的next()
方法和一个返回Boolean
类型的判断是否包含下一个的hasNext()
方法:
上述三个方法被称作运算符
数组的循环过程被编译为一个基于索引的循环,这个循环并不会产生一个迭代器(iterator)对象
如果你想要使用索引去迭代一个数组或者是链表,你可以使用下面这种方式
for (i in array.indices)
print(array[i])
注:这种“区间迭代”是一种在编译时不会产生额外对象的最佳实现方式
或者,你可以使用函数库中的withIndex()
方法
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
While 循环
while
和 do..while
和其他语言一样
while (x > 0) {
x--
}
do {
val y = retrieveData()
} while (y != null) // y is visible here!
返回和跳转
Kotlin提供三中跳转操作符
- return. 默认跳出最近的方法或者匿名方法
- break. 跳出最近的一层循环
- continue. 结束最近一层的循环操作开始执行下一次循环
Break 和 Continue 标签
Kotlin中任何表达式都可以使用label
去标记。标签的格式是 标识符后跟随一个@
符号,例如abc@
,fooBar@
都是可用的标签。使用标签表达式的时候,我们只需要将标签放在表达式的前面即可。
loop@ for (i in 1..100) {
// ...
}
现在,我们就可以使用带标签的break
和continue
了
loop@ for (i in 1..100) {
for (j in 1..100) {
if (...)
break@loop
}
}
使用标签的break将会跳出被标记的循环。而continue将会执行被标记循环的下一次循环。
使用标签的Return
在Kotlin中可以通过字面方法(function literals), 本地方法(local functions)和 对象表达式(object expression)进行函数嵌套。 使用标签的return
允许我们从外层方法返回。最重要的方法是从lambda表达式中返回。回忆一下我们写过的代码
fun foo() {
ints.forEach {
if (it == 0) return
print(it)
}
}
return表达式会从最近的方法放回,也就是foo方法(注意这种非局部返回仅对内联函数的lambda表达式有效) 如果我们想要从lambda表达式中返回,则需要在lambda表达式上使用标签
fun foo() {
ints.forEach lit@ {
if (it == 0) return@lit
print(it)
}
}
现在,这个方法仅从lambda表达式中返回了。通常情况下更方便的是使用隐式的标签:例如和被传递的方法名同名的标签。
fun foo() {
ints.forEach {
if (it == 0) return@forEach
print(it)
}
}
或者,我们也可以使用匿名方法去替换一个lambda表达式。这个匿名方法的return语句会从这个匿名方法内返回
fun foo() {
ints.forEach(fun(value: Int) {
if (value == 0) return
print(value)
})
}
当函数有返回值时。解析器会给标签更高的优先级,比如说
return@a 1
的含义是“将1返回到@a标签指定的方法” 而不是 “返回一个标签表达式 (@a 1)”.