5.4.6 字符串

原文链接: https://kotlinlang.org/docs/strings.html

5.4.6 字符串

String 类型表示一个字符序列。你可以用它表示文本值,例如单词、句子、消息或结构化文本。

String 类型是不可变的。创建 String 对象之后,它的内容在其整个生命周期内保持不变。任何看起来是在修改字符串的操作,实际上都会创建一个新字符串。

声明字符串

要声明 String 字面量,请把值放在双引号("")中。你可以显式指定 String 类型,也可以让 Kotlin 根据值推断:

1
2
val name: String = "Kotlin"
val message = "Hello, world!" // Kotlin infers String

双引号字符串字面量支持 \n 或 \t 等转义序列:

1
2
val message = "Hello,\nworld!"
val quote = "Kotlin says, \"Hi\"."

多行字符串

要存储由多行组成、或包含你不想转义的引号的文本,请使用以三个引号(""" """)括起来的多行字符串:

1
2
3
4
5
6
val text = """
Hello,
Kotlin
"""

val quote = """Kotlin says, "Hi"."""

注意: 多行字符串不支持转义序列。Kotlin 会把这些字符当作普通文本。

多行字符串会按源码中的书写方式保留换行和缩进。当你希望运行期值与文件中的文本布局一致时,这种行为很有用。

在下面的示例中,每行前面的空格都是结果字符串的一部分:

1
2
3
4
val text = """
    Hello,
    Kotlin
"""

要去掉共同的起始缩进,请使用 trimIndent() 函数。它会找出非空行的最小公共缩进并将其移除:

1
2
3
4
5
6
7
8
fun main() {
    val text = """
        Hello,
        Kotlin
    """.trimIndent()

    println(text)
}

要更明确地控制缩进移除,请使用 trimMargin() 函数。它会移除每行中边界前缀及其之前的所有内容:

1
2
3
4
5
6
7
8
fun main() {
    val text = """
        |Hello,
        |Kotlin
    """.trimMargin()

    println(text)
}

默认情况下,trimMargin() 函数使用竖线符号(|)作为边界前缀,但你也可以传入其他字符作为参数,例如:trimMargin(">")。

注意: 当你用 trimIndent() 或 trimMargin() 之类的函数处理字符串时,结果字符串只使用换行符(\n)作为分隔符,与平台无关。

字符串模板

字符串模板让你可以把变量和表达式直接嵌入 String 字面量中。这个过程称为插值。你可以在普通字符串和多行字符串中使用字符串模板。

要把变量插入字符串,请使用 $ 符号:

1
2
3
4
5
fun main() {
    val name = "Kotlin"
    println("Hello, $name")
    // Hello, Kotlin
}

要把表达式插入字符串,或把变量直接放在其他文本旁边,请使用 ${}:

1
2
3
4
5
6
7
8
9
fun main() {
    val text = "abc"
    println("The length of $text is ${text.length}")
    // The length of abc is 3

    val language = "Kotlin"
    println("${language}Lang")
    // KotlinLang
}

提示: 你也可以用 + 运算符拼接字符串。不过字符串模板通常更易读,也更符合 Kotlin 习惯。

模板表达式也可以包含无需转义的双引号字符串:

1
2
3
4
5
6
7
// Double-quoted string
val test = "${"test".uppercase()}"

// Multiline string
val result = """
Result: ${"OK".lowercase()}
"""

字符串模板中的可空值

如果被插值的表达式或变量的求值结果是 null,Kotlin 编译器会把文本 null 插入结果字符串。要把 null 替换为其他值,请使用 Elvis 运算符(?:):

1
2
3
4
5
6
7
8
9
fun main(){
    val text: String? = null

    println("Hello, $text")
    // Hello, null

    println("Hello, ${text ?: "Kotlin"}")
    // Hello, Kotlin
}

多美元字符串插值

在普通字符串模板中,单个美元符号($)就会开始插值。如果你需要在字符串中包含字面的美元符号,请使用多美元字符串插值。

多美元字符串插值允许你指定需要多少个连续的美元符号才会触发插值。少于该数量的美元符号会被当作普通字符。

例如,当你在字符串字面量之前使用 $$ 时,只有连续两个美元符号才会开始插值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val KClass<*>.jsonSchema : String
    get() = $$"""
    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://example.com/product.schema.json",
      "$dynamicAnchor": "meta",
      "title": "$${simpleName ?: qualifiedName ?: "unknown"}",
      "type": "object"
    }
    """

提示: 如果你使用的是单美元字符串插值,多美元字符串插值不会影响你的代码。你可以继续使用单个 $,并在需要时使用多个美元符号。

基本字符串操作

Kotlin 提供了多种处理字符串的操作。本节介绍其中最常用的一些。

提示: 所有可用函数的更多内容请参阅 API 参考。

获取字符串长度

要获取字符串中的字符数量,请使用 length 属性:

1
2
3
4
5
fun main (){
    val language = "Kotlin"
    println(language.length)
    // 6
}

访问字符

你可以用索引运算符([])访问字符串中的单个字符:

1
2
3
4
5
6
7
8
fun main (){
    val language = "Kotlin"

    println(language[0])
    // K
    println(language[5])
    // n
}

提示: 字符串索引从零开始。如果你尝试访问超出有效范围的索引,Kotlin 会抛出异常。

你也可以遍历字符串中的字符:

1
2
3
4
5
fun main(){
    for (char in "Kotlin") {
      println(char)
    }
}

提取字符串的一部分

要提取字符串的一部分,请使用以下函数之一:

  • substring():返回一个包含原文本所选部分的新字符串。
  • subSequence():返回一个包含原文本所选部分的 CharSequence。

例如:

1
2
3
4
5
6
7
8
9
fun main() {
    val text = "Kotlin"
    println(text.substring(1))
    // otlin
    println(text.substring(1, 5))
    // otli
    println(text.subSequence(1, 5))
    // otli
}

由于 String 类型是不可变的,这些函数不会修改原字符串。

比较字符串

你可以用 == 运算符检查两个字符串的内容是否相同:

1
2
3
4
5
6
7
fun main(){
    println("kotlin" == "kotlin")
    // true

    println("kotlin" == "Kotlin")
    // false
}

你也可以用 compareTo() 函数按字典序(逐字符)比较字符串。它会扫描两个字符串,直到找到第一对不同的字符,并返回:

  • 字符串相等时返回 0。
  • 接收者小于实参时返回小于 0 的值。
  • 接收者大于实参时返回大于 0 的值。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    println("abc".compareTo("abd") < 0)
    // true

    println("abc".compareTo("ABC") > 0)
    // true

    // Pass true to ignore case differences
    println("abc".compareTo("ABC", ignoreCase = true) == 0)
    // true
}

处理字符串内容

如果你想改变字符串的内容,请用 .trim()、.replace()、.uppercase() 和 .lowercase() 之类的函数创建它的修改副本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun main() {
    val text = "  Hello, Kotlin  "

    println(text.trim())
    // Hello, Kotlin

    println(text.replace("Kotlin", "world"))
    //   Hello, world

    println(text.uppercase())
    //   HELLO, KOTLIN

    println(text.lowercase())
    //   hello, kotlin
}

你也可以用 contains()、startsWith() 和 endsWith() 函数检查字符串内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun main() {
    val domain = "kotlinlang.org"

    // Checks if the string contains "."
    println(domain.contains("."))
    // true

    // Checks if the string starts with "kotlin"
    println(domain.startsWith("kotlin"))
    // true

    // Checks if the string ends with ".org"
    println(domain.endsWith(".org"))
    // true
}

拆分字符串

你可以用 split() 函数按分隔符把字符串切分成多个部分:

1
2
3
4
5
fun main() {
    val numbers = "one, two, three"
    println(numbers.split(", "))
    // [one, two, three]
}

如果你想按行把字符串拆开,请使用 lines() 函数:

1
2
3
4
5
fun main() {
    val numbers = "one\ntwo\nthree"
    println(numbers.lines())
    // [one, two, three]
}

构建与格式化字符串

Kotlin 中的大多数格式化任务,请使用字符串模板。

当你用 + 运算符拼接字符串时,Kotlin 每次操作都会创建一个新的 String 对象。不过这种方式在循环中、或需要拼接很多片段时可能并不划算。为避免这类问题,你可以使用 buildString() 函数或 StringBuilder。它们把所有片段收集到一个可变缓冲区中,最后只产出一个字符串。

当决定要追加什么内容的逻辑比较复杂时,请使用 buildString() 函数。例如,当你有多个条件、各自贡献不同的片段时。使用 buildString() 时你不需要直接操作缓冲区。该函数会在内部创建一个 StringBuilder,运行你的代码块,并返回结果字符串。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fun main() {

    val hasErrors = true
    val hasWarnings = true
    val isComplete = false

    // buildString creates an empty buffer
    val status = buildString {
        // Appends "Errors found" to the buffer
        if (hasErrors) append("Errors found")
        if (hasWarnings) {
            // The buffer is not empty, appends "; "
            if (isNotEmpty()) append("; ")
            // Appends "Warnings found"
            append("Warnings found")
        }
        // isComplete = false, nothing to append
        if (isComplete) {
            if (isNotEmpty()) append("; ")
            append("Completed")
        }
        // The buffer is not empty, skips the fallback
        if (isEmpty()) append("OK")
    }

    println(status)
    // Errors found; Warnings found
}

当你需要把缓冲区作为显式值使用时,请使用 StringBuilder。例如,修改已有文本:

1
2
3
4
5
6
7
8
fun main() {
    val text = "Hello, Kotlin"
    val builder = StringBuilder(text)

    builder.replace(7, 13, "world")
    println(builder.toString())
    // Hello, world
}

在 JVM 上,你还可以用 String.format() 函数格式化字符串:

1
val text = String.format("Hello, %s", "Kotlin")

只有在 JVM 上确实需要格式化器风格的占位符时,才使用 String.format() 函数。 关于格式占位符的更多内容,请参阅 Java Class Formatter 文档。

字符串转换

你经常会用字符串来表示其他类型的值,例如数字、Boolean 值或来自输入的标识符。Kotlin 提供了把值转换为字符串的函数,以及把字符串解析为其他类型的函数。

要返回某个值的字符串表示,请使用 toString() 函数:

1
2
val number = 10
val text = number.toString()

在字符串模板和字符串拼接中,Kotlin 会自动把值转换为字符串。

要把字符串转换为其他类型,请使用相应的解析函数:

如果字符串格式有效,这些函数会返回所请求类型的值。如果输入可能无效,请使用带 OrNull 的变体。这些函数会返回 null 而不是抛出异常,因此对用户输入或你无法完全掌控的数据是安全的:

1
2
3
4
5
6
7
val toInt = "10".toInt() // 10

// 1000000000000 exceeds maximum value of Int
val toIntInvalid = "1000000000000".toIntOrNull()

val toBoolean = "true".toBooleanStrict() // true
val toBooleanInvalid = "yes".toBooleanStrictOrNull() // null