控制流¶
原始类型¶
Nil¶
最简单的类型是 Nil
。它只有一个值:nil
,表示没有实际值。
还记得 上一课的String#index
吗?如果字符串中不存在子字符串,它将返回 nil
。它没有索引,因此索引位置不存在。
p! "Crystal is awesome".index("aw"),
"Crystal is awesome".index("xxxx")
Bool¶
Bool
类型只有两个可能的值:true
和 false
,它们分别代表逻辑和布尔代数的真值。
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
您可以尝试切换 a
和 b
的值,以查看不同输入值下运算符的行为。
真值¶
布尔代数并不局限于布尔类型。所有值都具有隐式的真值:nil
、false
和空指针(为了完整性,我们将在后面介绍)是假值。任何其他值(包括 0
)都是真值。
让我们用其他值替换上面示例中的 true
和 false
,例如 "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
AND
和 OR
运算符返回与运算符真值匹配的第一个操作数的值。
p! "foo" && nil,
"foo" && false,
false || "foo",
"bar" || "foo"
NOT
、XOR
和等价运算符始终返回 Bool
值(true
或 false
)。
控制流¶
控制程序的流程意味着根据条件采取不同的路径。到目前为止,本教程中的每个程序都是一系列顺序的表达式。现在这将发生改变。
条件语句¶
条件子句将一段代码置于一个门后,只有在满足条件时才会打开该门。
最基本的形式是关键字 if
后跟一个用作条件的表达式。当表达式的返回值是真值时,条件就满足了。所有后续表达式都是分支的一部分,直到它以关键字 end
结束。
按照惯例,我们将嵌套的分支缩进两个空格。
以下示例仅在消息满足以 Hello
开头的条件时才打印消息。
message = "Hello World"
if message.starts_with?("Hello")
puts "Hello to you, too!"
end
注意
从技术上讲,这个程序仍然按照预定义的顺序运行。固定消息始终匹配并使条件为真值。但假设我们没有在源代码中定义消息的值。它也可能来自用户输入,例如聊天客户端。
如果消息的值不是以 Hello
开头的,条件分支将跳过,程序将不会打印任何内容。
条件表达式可以更复杂。通过 布尔代数,我们可以构造一个接受 Hello
或 Hi
的条件。
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
。我们可以在同一个条件语句中添加针对不同条件的分支。它就像一个带有另一个集成 if
的 else
。因此关键字是 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
分支仅在之前没有满足任何条件时才会执行。尽管如此,它始终可以省略。
请注意,不同的分支是互斥的,条件从上到下进行评估。在上面的例子中,这并不重要,因为两个条件不可能同时为真(消息不能同时以Hello
和Bye
开头)。但是,我们可以添加一个不排他的替代条件来演示这一点
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
while
和end
之间的代码执行了 10 次。它打印当前计数器值并将其增加 1。在第 10 次迭代之后,counter
的值为10
,因此counter < 10
失败,循环中断。
另一种方法是用关键字until
替换while
,它期望完全相反的真值。until x
等效于while !x
。
counter = 0
until counter >= 10
counter += 1
puts "Counter: #{counter}"
end
无限循环¶
在使用循环时,重要的是要注意循环条件在某个时候为假。否则,它将永远持续下去,或者直到您从外部停止程序(例如,Ctrl+C、kill
、拔掉电源或世界末日来临)。
在这个例子中,不递增计数器就相当于编写
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"