跳至内容

字符串

在之前的课程中,我们已经了解了大多数程序的主要构建块之一:字符串。让我们回顾一下基本属性。

一个 字符串 是一个 Unicode 字符序列,以 UTF-8 编码。字符串是 不可变的:如果您对字符串应用修改,实际上您将获得一个包含修改内容的新字符串。原始字符串保持不变。

字符串通常以双引号字符 (") 括起来的字面量形式编写。

插值

字符串插值是组合字符串的一种便捷方法:字符串字面量中的 #{...} 将花括号之间的表达式的值插入到字符串中的这个位置。

name = "Crystal"
puts "Hello #{name}"

插值中的表达式应保持简短,要么是一个变量,要么是一个简单的函数调用。更复杂的表达式会降低代码可读性。

表达式的值不需要是字符串。任何类型都可以,并且通过调用 #to_s 方法将其转换为字符串表示形式。此方法为任何对象定义。让我们尝试使用一个数字

name = 6
puts "Hello #{name}!"

注意

插值的另一种方法是连接。您可以使用 "Hello " + name + "!" 而不是 "Hello #{name}!"。但这更笨拙,并且在非字符串类型中存在一些问题。插值通常比连接更可取。

转义

某些字符无法直接在字符串字面量中编写。例如双引号:如果在字符串中使用,编译器会将其解释为结束分隔符。

解决这个问题的方法是转义:如果双引号前面有反斜杠 (\),则将其解释为转义序列,这两个字符一起编码一个双引号字符。

puts "I say: \"Hello World!\""

还有其他转义序列:例如不可打印字符,如换行符 (\n) 或制表符 (\t)。如果您想编写一个文字反斜杠,转义序列是一个双反斜杠 (\\)。空字符(代码点 0)是 Crystal 字符串中的一个普通字符。在某些编程语言中,此字符表示字符串的结尾。但在 Crystal 中,它只由其 #size 属性确定。

puts "I say: \"Hello \\\n\tWorld!\""

提示

您可以在 字符串字面量参考 中找到有关可用转义序列的更多信息。

替代分隔符

某些字符串字面量可能包含很多双引号 - 例如,想想带有名词参数值的 HTML 标签。如果必须使用反斜杠转义每一个,将会很麻烦。替代字面量分隔符是一个方便的替代方法。%(...) 等同于 "...",只是分隔符由圆括号 (()) 表示,而不是双引号。

puts %(I say: "Hello World!")

转义序列和插值仍然以相同的方式工作。

提示

您可以在 字符串字面量参考 中找到有关替代分隔符的更多信息。

Unicode

Unicode 是一个国际标准,用于在许多不同的书写系统中表示文本。除了英语和许多其他语言使用的拉丁字母字母之外,它还包括许多其他字符集。不仅仅是普通文本,Unicode 标准还包括表情符号和图标。

以下示例使用 Unicode 字符 U+1F310 (带经线球体) 来称呼世界

puts "Hello 🌐"

处理 Unicode 符号有时可能有点棘手。某些字符可能不受您的编辑器字体支持,某些字符甚至不可打印。作为替代方法,Unicode 字符可以表示为转义序列。反斜杠后跟字母 u 表示 Unicode 代码点。代码点值以十六进制数字形式编写,并用花括号括起来。如果代码点正好有四位数字,则可以省略花括号。

puts "Hello \u{1F310}"

转换

假设您想更改字符串中的某些内容。也许想大喊消息并将其全部改为大写?String#upcase 方法将所有小写字符转换为其大写等效项。相反的是 String#downcase。还有几个类似的方法,让我们可以用不同的风格表达我们的信息

message = "Hello World! Greetings from Crystal."

puts "normal: #{message}"
puts "upcased: #{message.upcase}"
puts "downcased: #{message.downcase}"
puts "camelcased: #{message.camelcase}"
puts "capitalized: #{message.capitalize}"
puts "reversed: #{message.reverse}"
puts "titleized: #{message.titleize}"
puts "underscored: #{message.underscore}"

#camelcase#underscore 方法不会更改此特定字符串,但请尝试使用输入 "snake_cased""CamelCased"

信息

让我们更详细地看一下字符串,以及我们可以了解它的信息。首先,字符串有一个长度,即它包含的字符数。此值以 String#size 形式提供。

message = "Hello World! Greetings from Crystal."

p! message.size

要确定字符串是否为空,您可以检查大小是否为零,或者直接使用简写 String#empty?

empty_string = ""

p! empty_string.size == 0,
  empty_string.empty?

String#blank? 方法如果字符串为空或仅包含空格字符,则返回 true。一个相关的方法是 String#presence,如果字符串为空,则返回 nil,否则返回字符串本身。

blank_string = ""

p! blank_string.blank?,
  blank_string.presence

相等和比较

您可以使用相等运算符 (==) 测试两个字符串是否相等,并使用比较运算符 (<=>) 比较它们。两者都严格逐个字符地比较字符串。请记住,<=> 返回一个整数,表示两个操作数之间的关系,而 == 如果比较结果为 0(即两个值比较相等),则返回 true

但是,还有一个 #compare 方法可以进行不区分大小写的比较。

message = "Hello World!"

p! message == "Hello World",
  message == "Hello Crystal",
  message == "hello world",
  message.compare("hello world", case_insensitive: false),
  message.compare("hello world", case_insensitive: true)

部分组件

有时,知道字符串是否完全匹配另一个字符串并不重要,您只是想知道一个字符串是否包含另一个字符串。例如,让我们使用 #includes? 方法检查消息是否与 Crystal 相关。

message = "Hello World!"

p! message.includes?("Crystal"),
  message.includes?("World")

有时,字符串的开头或结尾特别重要。这就是 #starts_with?#ends_with? 方法发挥作用的地方。

message = "Hello World!"

p! message.starts_with?("Hello"),
  message.starts_with?("Bye"),
  message.ends_with?("!"),
  message.ends_with?("?")

索引子字符串

我们可以通过 #index 方法获取子字符串位置的更详细的信息。它返回子字符串第一次出现的第一个字符的索引。结果 0starts_with? 的含义相同。

p! "Crystal is awesome".index("Crystal"),
  "Crystal is awesome".index("s"),
  "Crystal is awesome".index("aw")

该方法有一个可选的 offset 参数,它可以用来从字符串的开头以外的位置开始搜索。当子字符串可能出现多次时,这很有用。

message = "Crystal is awesome"

p! message.index("s"),
  message.index("s", offset: 4),
  message.index("s", offset: 10)

#rindex 方法的工作原理相同,但它从字符串的末尾开始搜索。

message = "Crystal is awesome"

p! message.rindex("s"),
  message.rindex("s", 13),
  message.rindex("s", 8)

如果未找到子字符串,则结果为一个名为 nil 的特殊值。它表示“无值”。当子字符串没有索引时,这是有意义的。

查看 #index 的返回类型,我们可以看到它返回 Int32Nil

a = "Crystal is awesome".index("aw")
p! a, typeof(a)
b = "Crystal is awesome".index("meh")
p! b, typeof(b)

提示

我们将在下一课中更深入地讲解 nil

提取子字符串

子字符串是字符串的一部分。如果要提取字符串的部分,有几种方法可以做到。

索引访问器 #[] 允许通过字符索引和大小引用子字符串。字符索引从 0 开始,到长度(即 #size 的值)减一结束。第一个参数指定子字符串中第一个字符的索引,第二个参数指定子字符串的长度。message[6, 5] 提取一个长度为 5 个字符的子字符串,从索引 6 开始。

message = "Hello World!"

p! message[6, 5]

假设我们已经确定字符串以 Hello 开头,以 ! 结尾,并且想要提取中间的部分。如果消息是 Hello Crystal,我们将无法获取整个 Crystal 这个词,因为它超过了 5 个字符。

一个解决方案是从整个字符串的长度减去开头和结尾的长度来计算子字符串的长度。

message = "Hello World!"

p! message[6, message.size - 6 - 1]

有一个更简单的方法:索引访问器可以与 Range 字符索引一起使用。范围文字由一个开始值和一个结束值组成,由两个点 (..) 连接。第一个值表示子字符串的开始索引,如前所述,但第二个值是结束索引(而不是长度)。现在我们不需要在计算中重复开始索引,因为结束索引只是大小减二(一个用于结束索引,一个用于排除最后一个字符)。

它甚至可以更简单:负索引值会自动关联到字符串的末尾,因此我们不需要显式地从字符串大小计算结束索引。

message = "Hello World!"

p! message[6..(message.size - 2)],
  message[6..-2]

替换

以非常类似的方式,我们可以修改一个字符串。让我们确保我们正确地向 Crystal 打招呼,而不是其他任何东西。我们不访问子字符串,而是调用 #sub。第一个参数仍然是一个范围,用于指示被第二个参数的值替换的位置。

message = "Hello World!"

p! message.sub(6..-2, "Crystal")

#sub 方法非常灵活,可以用不同的方式使用。我们也可以将一个搜索字符串作为第一个参数传递,它将用第二个参数的值替换该子字符串。

message = "Hello World!"

p! message.sub("World", "Crystal")

#sub 只替换搜索字符串的第一个实例。它的“哥哥”#gsub 应用于所有实例。

message = "Hello World! How are you, World?"

p! message.sub("World", "Crystal"),
  message.gsub("World", "Crystal")

提示

您可以在 字符串文字参考String API 文档 中找到更详细的信息。