跳至内容

控制流

原始类型

Nil

最简单的类型是 Nil。它只有一个值:nil,表示没有实际值。

还记得 上一课的String#index 吗?如果字符串中不存在子字符串,它将返回 nil。它没有索引,因此索引位置不存在。

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

Bool

Bool 类型只有两个可能的值:truefalse,它们分别代表逻辑和布尔代数的真值。

p! true, false

布尔值 对于管理程序中的控制流特别有用。

布尔代数

以下示例显示了使用布尔值实现 布尔代数 的运算符。

a = true
b = false

p! a && b, # conjunction (AND)
  a || b,  # disjunction (OR)
  !a,      # negation (NOT)
  a != b,  # inequivalence (XOR)
  a == b   # equivalence

您可以尝试切换 ab 的值,以查看不同输入值下运算符的行为。

真值

布尔代数并不局限于布尔类型。所有值都具有隐式的真值:nilfalse 和空指针(为了完整性,我们将在后面介绍)是假值。任何其他值(包括 0)都是真值

让我们用其他值替换上面示例中的 truefalse,例如 "foo"nil

a = "foo"
b = nil

p! a && b, # conjunction (AND)
  a || b,  # disjunction (OR)
  !a,      # negation (NOT)
  a != b,  # inequivalence (XOR)
  a == b   # equivalence

ANDOR 运算符返回与运算符真值匹配的第一个操作数的值。

p! "foo" && nil,
  "foo" && false,
  false || "foo",
  "bar" || "foo"

NOTXOR 和等价运算符始终返回 Bool 值(truefalse)。

控制流

控制程序的流程意味着根据条件采取不同的路径。到目前为止,本教程中的每个程序都是一系列顺序的表达式。现在这将发生改变。

条件语句

条件子句将一段代码置于一个门后,只有在满足条件时才会打开该门。

最基本的形式是关键字 if 后跟一个用作条件的表达式。当表达式的返回值是真值时,条件就满足了。所有后续表达式都是分支的一部分,直到它以关键字 end 结束。

按照惯例,我们将嵌套的分支缩进两个空格。

以下示例仅在消息满足以 Hello 开头的条件时才打印消息。

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
end

注意

从技术上讲,这个程序仍然按照预定义的顺序运行。固定消息始终匹配并使条件为真值。但假设我们没有在源代码中定义消息的值。它也可能来自用户输入,例如聊天客户端。

如果消息的值不是以 Hello 开头的,条件分支将跳过,程序将不会打印任何内容。

条件表达式可以更复杂。通过 布尔代数,我们可以构造一个接受 HelloHi 的条件。

message = "Hello World"

if message.starts_with?("Hello") || message.starts_with?("Hi")
  puts "Hey there!"
end

让我们反转一下条件:仅当消息Hello 开头时才打印它。这只是对上一个示例的细微改动:我们可以使用否定运算符 (!) 将条件转换为相反的表达式。

message = "Hello World"

if !message.starts_with?("Hello")
  puts "I didn't understand that."
end

另一种选择是将 if 替换为关键字 unless,它期待相反的真值。unless x 等同于 if !x

message = "Hello World"

unless message.starts_with?("Hello")
  puts "I didn't understand that."
end

让我们看一个使用 String#index 查找子字符串并突出显示其位置的示例。请记住,如果它找不到子字符串,它将返回 nil?在这种情况下,我们无法突出显示任何内容。因此我们需要一个 if 子句,其条件检查索引是否为 nil.nil? 方法非常适合这种情况。

str = "Crystal is awesome"
index = str.index("aw")

if !index.nil?
  puts str
  puts "#{" " * index}^^"
end

编译器强制您处理 nil 的情况。尝试删除条件或将条件更改为 true:会出现一个类型错误,说明您无法在该表达式中使用 Nil 值。使用适当的条件,编译器就知道 index 在分支内部不可能为 nil,并且可以使用它作为数字输入。

提示

if !index.nil? 的简短形式是 if index,它在很大程度上是等效的。只有在您想区分假值是 nil 还是 false 时,两者才会有所不同,因为前一个条件匹配 false,而后一个不匹配。

Else

让我们完善我们的程序,并在两种情况下做出反应,无论消息是否满足条件。

我们可以将它们作为两个具有否定条件的独立条件语句。

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
end

if !message.starts_with?("Hello")
  puts "I didn't understand that."
end

这有效,但有两个缺点:条件表达式 message.starts_with?("Hello") 评估了两次,效率低下。稍后,如果我们在一个地方更改了条件(可能还允许 Hi),我们可能会忘记在另一个地方也进行更改。

条件语句可以有多个分支。备用分支由关键字 else 表示。如果条件不满足,它将执行。

message = "Hello World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
else
  puts "I didn't understand that."
end

更多分支

我们的程序只对 Hello 做出反应,但我们希望有更多互动。让我们添加一个分支来响应 Bye。我们可以在同一个条件语句中添加针对不同条件的分支。它就像一个带有另一个集成 ifelse。因此关键字是 elsif

message = "Bye World"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
elsif message.starts_with?("Bye")
  puts "See you later!"
else
  puts "I didn't understand that."
end

else 分支仅在之前没有满足任何条件时才会执行。尽管如此,它始终可以省略。

请注意,不同的分支是互斥的,条件从上到下进行评估。在上面的例子中,这并不重要,因为两个条件不可能同时为真(消息不能同时以HelloBye开头)。但是,我们可以添加一个不排他的替代条件来演示这一点

message = "Hello Crystal"

if message.starts_with?("Hello")
  puts "Hello to you, too!"
elsif message.includes?("Crystal")
  puts "Shine bright like a crystal."
end

if message.includes?("Crystal")
  puts "Shine bright like a crystal."
elsif message.starts_with?("Hello")
  puts "Hello to you, too!"
end

两个子句都有具有相同条件但顺序不同的分支,它们的行为也不同。第一个匹配的条件选择哪个分支执行。

循环

本节介绍代码重复执行的基础知识。

基本功能是while子句。它的结构与if子句非常相似:关键字while指定开头,后面跟着用作循环条件的表达式。所有后续表达式都是循环的一部分,直到结束关键字end。只要条件的返回值为,循环就会继续重复自身。

让我们尝试一个简单的程序,从 1 计数到 10

counter = 0

while counter < 10
  counter += 1

  puts "Counter: #{counter}"
end

whileend之间的代码执行了 10 次。它打印当前计数器值并将其增加 1。在第 10 次迭代之后,counter的值为10,因此counter < 10失败,循环中断。

另一种方法是用关键字until替换while,它期望完全相反的真值。until x等效于while !x

counter = 0

until counter >= 10
  counter += 1

  puts "Counter: #{counter}"
end

提示

您可以在语言规范中找到有关这些表达式的更多详细信息:whileuntil

无限循环

在使用循环时,重要的是要注意循环条件在某个时候为。否则,它将永远持续下去,或者直到您从外部停止程序(例如,Ctrl+Ckill、拔掉电源或世界末日来临)。

在这个例子中,不递增计数器就相当于编写

while true
  puts "Counter: #{counter}"
end

或者如果条件是counter > 0,它将匹配所有值:它们只从1开始增加。这在技术上不是无限循环,因为它将在计数器达到 32 位整数的最大值时出现数学错误。但从概念上讲,这类似于无限循环。这样的逻辑错误很容易被忽略,因此在编写循环条件时以及注意满足该断点时,务必注意。对于索引变量(例如我们示例中的counter),一个好的做法是在循环开始时对其进行递增。这样可以减少忘记更新它们的可能性。

提示

幸运的是,语言中有很多功能可以减轻手动编写循环的负担,还可以确保有效的断开条件。以下课程将介绍其中一些功能。

在某些情况下,意图确实是创建一个无限循环。例如,一个服务器总是重复等待连接,或者一个命令处理器等待用户输入。然后,它应该很明显,而不是隐藏在一个复杂的、永远不会失败的循环条件中。最简单的方法是while true。条件true始终为真,因此循环无限制地重复。

while true
  puts "Hi, what's your name? (hit Enter when done)"

  # `gets` returns input from the console
  name = gets

  puts "Nice to meet you, #{name}."
  puts "Now, let's repeat."
end

注意

这个例子不是一个交互式游乐场,因为游乐场无法处理非自终止程序和处理用户输入。它只会超时并打印错误。但是,您可以使用本地编译器编译并运行此代码。

要停止程序,请按Ctrl+C。这会向进程发送一个信号,要求它退出。

跳过和中断

在某些条件下,跳过一些迭代或完全停止迭代可能很有用。

关键字next在循环体中跳过到下一个迭代,忽略当前迭代中剩余的任何表达式。如果循环条件不满足,循环将结束,并且主体将不再执行。

counter = 0

while counter < 10
  counter += 1

  if counter % 3 == 0
    next
  end

  puts "Counter: #{counter}"
end

这个例子本来可以很容易地不使用next来编写,方法是将puts表达式放在条件中。当方法体中有更多要跳过的表达式时,next的价值就显而易见了。

循环条件可能难以计算,例如,因为它们需要多个步骤或依赖于需要确定的输入。在这种情况下,在循环条件中编写所有逻辑并不实用。关键字break可以在循环体中的任何地方使用,并且可以作为一种额外的选择,从循环中跳出,而不管其循环条件如何。控制流立即在循环结束之后继续进行。

counter = 0

while true
  counter += 1

  puts "Counter: #{counter}"

  if counter >= 10
    break
  end
end

puts "done"