07. 数值类和字符串详解

数值类

和 Java 一样,Kotlin 中的所有数字类型都是有符号的(signed),也就是说,它们既可以表示正数,也可以表示负数。 除了是否支持小数外,数字类型还有个区别是在内存中所占的位数(直接的结果就是它们所能支持的最大值和最小值)。

整数是不带小数位的数字,在 Kotlin 中常用 Int 类型表示。带小数位的数字要以 Float 或 Double 类型表示。

这里有必要提一下 Short 和 Byte 这两个类型。千言万语汇成一句话,你几乎不会用它们来表示常见的数。它们主要用于和 Java 遗留代码互操作这样的场景。例如,从文件读取数据流 或处理图像时(1 个颜色像素常以 3 字节表示,对应 RGB 三色),你可能就需要和 Byte 打交道。 而需要和不支持 32 位指令的 CPU 原生代码交互时,你很可能会看到 Short 的身影。不管怎么说,大多数情况下,表示整数都用 Int,如果需要更大的数,那么就用 Long。

数字的字面常数

在 Java 中表示 long 类型整数时可以在数字后面加小写英文字母 l,但由于可读性不好,容易被误认为是阿拉伯数字 1,所以在 Kotlin 中部不允许这样表示。

在使用整数变量赋值,还可以使用二进制和十六进制表示,但不支持八进制,它们的表示方式分别如下:

  • 二进制数:以 0b 或 0B 为前缀,注意 0 是阿拉伯数字,不要误认为是英文字母 o。
  • 十六进制数:以 0x 或 0X 为前缀,注意 0 是阿拉伯数字。

可以使用下划线使数字常量更易读:

1
2
3
4
5
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

显式数字转换

在 Kotlin 中,没有隐式的数字扩大转换。例如,带有 Double 参数的函数只能在 Double 值上调用,而不能在 Float、 Int 或其他数值上调用。

1
2
3
4
5
6
7
8
9
10
11
fun main() {
fun printDouble(d: Double) { print(d) }

val i = 1
val d = 1.0
val f = 1.0f

printDouble(d)
// printDouble(i) // Error: Type mismatch
// printDouble(f) // Error: Type mismatch
}

kotlin 提供了可以把所有数字类型(包括字符串)转换为包括数在内的其他类型值的显式数字转换。

  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
  • toFloat(): Float
  • toDouble(): Double

在许多情况下,不需要显式转换,因为类型是从上下文推断出来的,并且对于适当的转换,算术操作是重载的,例如:

1
val l = 1L + 3 // Long + Int => Long

尝试转换格式错误的字符串会抛出异常。例如,调用 toInt 函数转换字符串 “5.91” 就会抛出异常,因为字符串的 .91 部分无法转成 Int 值。

考虑到这个问题,Kotlin 提供了 toDoubleOrNull 和 toIntOrNull 这样的安全转换函数。 如果数值不能正确转换,与其触发异常不如干脆返回 null 值。另外,调用 toIntOrNull 函数时, 可以同时使用空合并操作符。例如,你可以提供一个默认值:

1
val gold: Int = "5.91".toIntOrNull() ?: 0

Double 类型格式化

要格式化的值作为值参传给 format 函数。Kotlin 使用的格式化字符串和 Java、C/C++、Ruby 等其他语言中的标准格式化字符串是一 样的。想了解更多格式化字符串形式和规则,参见 Java API 文档:https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html

1
2
3
4
5
6
7
8
9
10
val d = 1.234567
// 正常输出
println("$d")

val s = "%.2f".format(d)
// 格式化输出
println(s)

// 也可以写在一起
println("s = ${"%.2f".format(d)}")

无符号整数类型

除了整数类型之外,Kotlin 还为无符号整数提供了以下类型:

  • UByte: 一个无符号的8位整数,范围从 0 到 255
  • UShort: 一个无符号的16位整数,范围从 0 到 65535
  • UInt: 一个无符号的32位整数,范围从 0 到 2 ^ 32-1
  • ULong: 一个无符号64位整数,范围从 0 到 2 ^ 64-1
1
2
3
4
5
6
val b: UByte = 1u  // UByte, expected type provided
val s: UShort = 1u // UShort, expected type provided
val l: ULong = 1u // ULong, expected type provided

val a1 = 42u // UInt: no expected type provided, constant fits in UInt
val a2 = 0xFFFF_FFFF_FFFFu // ULong: no expected type provided, constant doesn't fit in UInt

数值操作

整数除法

整数数字之间的除总是返回一个整数数字。任何小数部分都被丢弃。

1
2
3
val x = 5 / 2
//println(x == 2.5) // ERROR: Operator '==' cannot be applied to 'Int' and 'Double'
println(x == 2)

对于任意两种整数类型之间的除法都是如此:

1
2
val x = 5L / 2
println(x == 2L)

若要返回浮点类型,请显式地至少将其中一个参数转换为浮点类型:

1
2
val x = 5 / 2.toDouble()
println(x == 2.5)

位运算

Kotlin 提供了一系列函数用于二进制数的运算,这种运算又称为位运算。如果熟悉 Java 语言, 那你应该见过它们。

注:这里用了中缀的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 整数转二进制
println(Integer.toBinaryString(42))

// 按位左移-有符号的左移位
println(1 shl 2)
// 按位右移-有符号的右移位
println(16 shr 3)

// 无符号右移
println(16 ushr 3)

// 按位异或预算
println(3 xor 5)
// 按位或运算
println(10 or 42)
// 按位与运算
println(10 and 42)
// 取反
println(42.inv())

字符串

字符串在 Kotlin 是由类型字符串表示的。通常,字符串值是双引号(“)中的字符序列。

字符串是不可变的。初始化字符串后,不能更改其值或为其赋予新值。所有转换字符串的操作都将其结果返回到一个新的 String 对象中,而原始字符串保持不变:

1
2
3
val str = "abcd"
println(str.uppercase()) // Create and print a new String object
println(str) // The original string remains the same

若要连接字符串,请使用 + 运算符。只要表达式中的第一个元素是字符串,就可以将字符串与其他类型的值连接起来:

1
2
val s = "abc" + 1
println(s + "def")

截取

substring

substring 函数支持 IntRange 类型(表示一个整数范围的类型)的参数。这个 IntRange。

示例:

1
2
val indexOfApostrophe = TAVERN_NAME.indexOf('\'')
val tavernMaster = TAVERN_NAME.substring(0 until indexOfApostrophe)

参数表示的范围决定要截取哪些位置的字符。这里,范围是从第一个字符直到撇号之前的字符 (until 创建的范围不包括上限值)。

split

split 函数返回的是 List 集合数据。使用给定的分隔符,该函数会返回一系列子字符串。

1
2
3
4
val data = menuData.split(',')
val type = data[0]
val name = data[1]
val price = data[2]

而 List 集合又支持一种叫解构(destructuring)的语法特性(这个 Kotlin 特性允许你在一个表达式里给多个变量赋值)。所以,如下所示, 我们可以用解构语法来替换一条条的赋值语句。

1
2
val menuData = ...
val (type, name, price) = menuData.split(',')

字符串操作

replace

replace 函数。顾名思义,此函数能按给定规则替换字符串中的 字符。replace 函数支持用正则表达式(稍后会展开讨论)确定要操作哪些字符,然后调用你定 义的匿名函数来确定如何替换匹配字符。

使用的 replace 函数需要两个值参。第一个是正则表达式(regex),用来决定要替换哪些字符。正则表达式可以针对你要搜索的字符定义通用的搜索模式。第二个值参是匿名函数,用来 确定该如何替换正则表达式搜索到的字符。

1
2
3
phrase.replace(Regex("[aeiou]")) {
...
}

字符串比较

使用结构相等操作符 == 来判断 name 变量值和 “Dragon’s Breath” 是否结 构相等。之前,你已见过用 == 判断数值相等的例子。用于字符串时,它会检查两个字符串中的字 符是否都匹配,顺序是否一致。

判断两个变量相等还有另一种方式:引用相等。这种方式会检查两个变量是否引用着同一个 类实例,也就是说两个变量是否指向内存堆上的同一对象。做引用相等判断时使用 === 符号。

引用相等比较很少用于比较字符串。一般来说,相比关心两个字符串是不是同一实例,我们更关心它们是不是相等字符,以及顺序是否一致(或者说两个不同的类实例是否结构相同)。

如果熟悉 Java 语言,那么你就知道,就比较字符串来说,== 操作符的行为方式和 Kotlin 不 一样,Java 用 == 做引用比较。要在 Java 中做结构相等比较,要用 equals 函数。

字符串遍历

String 类型还有一些其他函数,可以一次一个地遍历字符串,就像 indexOf 和 split 函数 一样。例如,使用 forEach 函数,你可以打印出小客栈饮料名字的每个字符,一次一个字符。

示例:

1
"Dragon's Breath".forEach { println("$it") }

许多这类函数也可以用于 List 类型。之后我们将见到大多数可以遍历集合的函数也适用于 String 类型。在很多方面,Kotlin 中 String 类型的行为就像字符集合。

raw 字符串

原始字符串可以包含换行符和任意文本。它由三重引号(“”")分隔,不包含转义,可以包含换行符和任何其他字符:

1
2
3
4
val text = """
for (c in "foo")
print(c)
"""

要从原始字符串中删除前导空格,请使用 trimMargin() 函数:

1
2
3
4
5
6
val text = """
|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin()

默认情况下,管道符号 | 用作边距前缀,但是您可以选择另一个字符并将其作为参数传递,如 trimMargin(">")

字符串模板

字符串模板允许您将变量引用和表达式包含到字符串中。当请求字符串的值时(例如,通过 println) ,所有引用和表达式都被实际值替换。

1
2
3
4
5
6
val greeting = "Kotliner"

println("Hello $greeting") // 1
println("Hello ${greeting.uppercase()}") // 2
val price = """${'$'}"""
println(price) // 3
  1. 打印具有变量引用的字符串。字符串中的引用以 $开头。
  2. 用表达式打印一个字符串。表达式以 $开头,用大括号括起来。
  3. 输出美元符。

参考