Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.65 MB, 448 trang )
Consider the following Ruby program. It adds two numbers passed to it on the
command line and prints the sum:
x = ARGV[0].to_f
y = ARGV[1].to_f
sum = x + y
puts sum
#
#
#
#
Convert first argument to a number
Convert second argument to a number
Add the arguments
Print the sum
This is a simple program that consists primarily of variable assignment and method
invocations. What makes it particularly simple is its purely sequential execution. The
four lines of code are executed one after the other without branching or repetition. It
is a rare program that can be this simple. This chapter introduces Ruby’s control structures, which alter the sequential execution, or flow-of-control, of a program. We cover:
•
•
•
•
•
•
•
Conditionals
Loops
Iterators and blocks
Flow-altering statements like return and break
Exceptions
The special-case BEGIN and END statements
The esoteric control structures known as fibers and continuations
5.1 Conditionals
The most common control structure, in any programming language, is the
conditional. This is a way of telling the computer to conditionally execute some code:
to execute it only if some condition is satisfied. The condition is an expression—if it
evaluates to any value other than false or nil, then the condition is satisfied.
Ruby has a rich vocabulary for expressing conditionals. The syntax choices are
described in the subsections that follow. When writing Ruby code, you can choose the
one that seems most elegant for the task at hand.
5.1.1 if
The most straightforward of the conditionals is if. In its simplest form, it looks like this:
if expression
code
end
The code between if and end is executed if (and only if) the expression evaluates to
something other than false or nil. The code must be separated from the expression
118 | Chapter 5: Statements and Control Structures
with a newline or semicolon or the keyword then.* Here are two ways to write the same
simple conditional:
# If x
if x <
x +=
end
if x <
is less than 10, increment it
10
# newline separator
1
10 then x += 1 end
# then separator
You can also use then as the separator token, and follow it with a newline. Doing so
makes your code robust; it will work even if the newline is subsequently removed:
if x < 10 then
x += 1
end
Programmers who are used to C, or languages whose syntax is derived from C, should
note two important things about Ruby’s if statement:
• Parentheses are not required (and typically not used) around the conditional
expression. The newline, semicolon, or then keyword serves to delimit the expression instead.
• The end keyword is required, even when the code to be conditionally executed
consists of a single statement. The modifier form of if, described below, provides
a way to write simple conditionals without the end keyword.
5.1.1.1 else
An if statement may include an else clause to specify code to be executed if the
condition is not true:
if expression
code
else
code
end
The code between the if and else is executed if expression evaluates to anything other
than false or nil. Otherwise (if expression is false or nil), the code between the
else and end is executed. As in the simple form of if, the expression must be separated
from the code that follows it by a newline, a semicolon, or the keyword then. The
else and end keywords fully delimit the second chunk of code, and no newlines or
additional delimiters are required.
Here is an example of a conditional that includes an else clause:
if data
data << x
else
# If the array exists
#
then append a value to it.
# Otherwise...
* Ruby 1.8 also allows a colon, but this syntax is no longer legal in 1.9.
5.1 Conditionals | 119
data = [x]
end
#
create a new array that holds the value.
# This is the end of the conditional.
5.1.1.2 elsif
If you want to test more than one condition within a conditional, you can add one or
more elsif clauses between an if and an else. elsif is a shortened form of “else if.”
Note that there is only one e in elsif. A conditional using elsif looks like this:
if expression1
code1
elsif expression2
code2
.
.
.
elsif expressionN
codeN
else
code
end
If expression1 evaluates to anything other than false or nil, then code1 is executed.
Otherwise, expression2 is evaluated. If it is anything other than false or nil, then
code2 is executed. This process continues until an expression evaluates to something
other than false or nil, or until all elsif clauses have been tested. If the expression
associated with the last elsif clause is false or nil, and the elsif clause is followed by
an else clause, then the code between else and end is executed. If no else clause is
present, then no code is executed at all.
elsif is like if: the expression must be separated from the code by a newline, a semicolon, or a then keyword. Here is an example of a multiway conditional using elsif:
if x == 1
name = "one"
elsif x == 2
name = "two"
elsif x == 3 then name = "three"
elsif x == 4; name = "four"
else
name = "many"
end
5.1.1.3 Return value
In most languages, the if conditional is a statement. In Ruby, however, everything is
an expression, even the control structures that are commonly called statements. The
return value of an if “statement” (i.e., the value that results from evaluating an if
expression) is the value of the last expression in the code that was executed, or nil if
no block of code was executed.
120 | Chapter 5: Statements and Control Structures
The fact that if statements return a value means that, for example, the multiway
conditional shown previously can be elegantly rewritten as follows:
name = if
elsif
elsif
elsif
else
end
x
x
x
x
==
==
==
==
1
2
3
4
then
then
then
then
"one"
"two"
"three"
"four"
"many"
5.1.2 if As a Modifier
When if is used in its normal statement form, Ruby’s grammar requires that it be
terminated with the end keyword. For simple, single-line conditionals, this is somewhat
awkward. This is just a parsing problem, and the solution is to use the if keyword itself
as the delimiter that separates the code to be executed from the conditional
expression. Instead of writing:
if expression then code end
we can simply write:
code if expression
When used in this form, if is known as a statement (or expression) modifier. If you’re
a Perl programmer, you may be accustomed to this syntax. If not, please note that the
code to execute comes first, and the expression follows. For example:
puts message if message
# Output message, if it is defined
This syntax places more emphasis on the code to be executed, and less emphasis on
the condition under which it will be executed. Using this syntax can make your code
more readable when the condition is a trivial one or when the condition is almost always
true.
Even though the condition is written last, it is evaluated first. If it evaluates to anything
other than false or nil, then the code is evaluated, and its value is used as the return
value of the modified expression. Otherwise, the code is not executed, and the return
value of the modified expression is nil. Obviously, this syntax does not allow any kind
of else clause.
To use if as a modifier, it must follow the modified statement or expression immediately, with no intervening line break. Inserting a newline into the previous example
turns it into an unmodified method invocation followed by an incomplete if statement:
puts message
if message
# Unconditional
# Incomplete!
The if modifier has very low precedence and binds more loosely than the assignment
operator. Be sure you know just what expression you are modifying when you use it.
For example, the following two lines of code are different:
5.1 Conditionals | 121
y = x.invert if x.respond_to? :invert
y = (x.invert if x.respond_to? :invert)
In the first line, the modifier applies to the assignment expression. If x does not have a
method named invert, then nothing happens at all, and the value of y is not modified.
In the second line, the if modifier applies only to the method call. If x does not have
an invert method, then the modified expression evaluates to nil, and this is the value
that is assigned to y.
An if modifier binds to the single nearest expression. If you want to modify more than
one expression, you can use parentheses or a begin statement for grouping. But this
approach is problematic because readers don’t know that the code is part of a conditional until they reach the bottom. Also, using an if modifier in this way gives up the
conciseness that is the primary benefit of this syntax. When more than one line of code
is involved, you should typically use a traditional if statement rather than an if modifier. Compare the following three side-by-side alternatives:
if expression
line1
line2
end
begin
line1
line2
end if expression
(
line1
line2
) end if expression
Note that an expression modified with an if clause is itself an expression that can be
modified. It is therefore possible to attach multiple if modifiers to an expression:
# Output message if message exists and the output method is defined
puts message if message if defined? puts
Repeating an if modifier like this is hard to read, however, and it makes more sense to
combine the two conditions into a single expression:
puts message if message and defined? puts
5.1.3 unless
unless, as a statement or a modifier, is the opposite of if: it executes code only if an
associated expression evaluates to false or nil. Its syntax is just like if, except that
elsif clauses are not allowed:
# single-way unless statement
unless condition
code
end
# two-way unless statement
unless condition
code
else
code
end
# unless modifier
code unless condition
122 | Chapter 5: Statements and Control Structures
The unless statement, like the if statement, requires that the condition and the code
are separated by a newline, a semicolon, or the then keyword. Also like if, unless
statements are expressions and return the value of the code they execute, or nil if they
execute nothing:
# Call the to_s method on object o, unless o is nil
s = unless o.nil?
# newline separator
o.to_s
end
s = unless o.nil? then o.to_s end
# then separator
For single-line conditionals like this, the modifier form of unless is usually clearer:
s = o.to_s unless o.nil?
Ruby has no equivalent of the elsif clause for an unless conditional. You can still write
a multiway unless statement, however, if you’re willing to be a little more verbose:
unless x == 0
puts "x is not 0"
else
unless y == 0
puts "y is not 0"
else
unless z == 0
puts "z is not 0"
else
puts "all are 0"
end
end
end
5.1.4 case
The case statement is a multiway conditional. There are two forms of this statement.
The simple (and infrequently used) form is nothing more than an alternative syntax for
if/elsif/else. These two side-by-side expressions are equivalent:
name = case
when
when
when
when
else
end
x == 1
x == 2
x == 3
x == 4
"many"
then
then
then
then
"one"
"two"
"three"
"four"
name = if
x == 1
elsif x == 2
elsif x == 3
elsif x == 4
else "many"
end
then
then
then
then
"one"
"two"
"three"
"four"
As you can see from this code, the case statement returns a value, just as the if statement
does. As with the if statement, the then keyword following the when clauses can be
replaced with a newline or semicolon:*
* Ruby 1.8 also allows a colon in place of then, as it does for the if statement. But this syntax is no longer
allowed in Ruby 1.9.
5.1 Conditionals | 123
case
when x == 1
"one"
when x == 2
"two"
when x == 3
"three"
end
The case statement tests each of its when expressions in the order they are written until
it finds one that evaluates to true. If it finds one, it evaluates the statements that come
between that when and the following when, else, or end. The last expression evaluated
becomes the return value of the case statement. Once a when clause that evaluates to
true has been found, no other when clauses are considered.
The else clause of a case statement is optional, but if it appears, it must come at the
end of the statement, after all when clauses. If no when clause is true, and there is an
else clause, then the code between else and end is executed. The value of the last
expression evaluated in this code becomes the value of the case statement. If no when
clause is true and there is no else clause, then no code is executed and the value of the
case statement is nil.
A when clause within a case statement may have more than one (comma-separated)
expression associated with it. If any one of these expressions evaluates to true, then
the code associated with that when is executed. In this simple form of the case statement,
the commas aren’t particularly useful and act just like the || operator:
case
when x == 1, y == 0 then "x is one or y is zero" # Obscure syntax
when x == 2 || y == 1 then "x is two or y is one" # Easier to understand
end
All the case examples we’ve seen so far demonstrate the simpler, less common form of
the statement. case is really more powerful than this. Notice that in most of the examples, the left side of each when clause expression is the same. In the common form of
case, we factor this repeated lefthand expression of the when clause and associate it with
the case itself:
name = case x
when 1
"one"
when 2 then "two"
when 3; "three"
else "many"
end
# Just the value to compare to x
# Then keyword instead of newline
# Semicolon instead of newline
# Optional else clause at end
In this form of the case statement, the expression associated with the case is evaluated
once, and then it’s compared to the values obtained by evaluating the when expression.
The comparisons are performed in the order in which the when clauses are written, and
the code associated with the first matching when is executed. If no match is found, the
code associated with the else clause (if there is one) is executed. The return value of
124 | Chapter 5: Statements and Control Structures
this form of the case statement is the same as the return value of the simpler form: the
value of the last expression evaluated, or nil if no when or else matches.
The important thing to understand about the case statement is how the values of the
when clauses are compared to the expression that follows the case keyword. This comparison is done using the === operator. This operator is invoked on the value of the
when expression and is passed the value of the case expression. Therefore, the case
statement above is equivalent to the following (except that x is only evaluated once in
the code above):
name = case
when
when
when
else
end
1 === x then "one"
2 === x then "two"
3 === x then "three"
"many"
=== is the case equality operator. For many classes, such as the Fixnum class used earlier,
the === operator behaves just the same as ==. But certain classes define this operator in
interesting ways. The Class class defines === so that it tests whether the righthand
operand is an instance of the class named by the lefthand operand. Range defines this
operator to test whether the value on the right falls within the range on the left.
Regexp defines it so that it tests whether the text on the right matches the pattern on
the left. In Ruby 1.9,Symbol defines === so that it tests for symbol or string equality.
With these definitions of case equality, we are able to write interesting case statements
like the following:
# Take different actions depending on the class of x
puts case x
when String then "string"
when Numeric then "number"
when TrueClass, FalseClass then "boolean"
else "other"
end
# Compute 2006 U.S. income tax using case and Range objects
tax = case income
when 0..7550
income * 0.1
when 7550..30650
755 + (income-7550)*0.15
when 30650..74200
4220 + (income-30655)*0.25
when 74200..154800
15107.5 + (income-74201)*0.28
when 154800..336550
37675.5 + (income-154800)*0.33
else
97653 + (income-336550)*0.35
end
# Get user's input and process it, ignoring comments and exiting
5.1 Conditionals | 125
# when the user enters the word "quit"
while line=gets.chomp do # Loop, asking the user for input each time
case line
when /^\s*#/
# If input looks like a comment...
next
#
skip to the next line.
when /^quit$/i
# If input is "quit" (case insensitive)...
break
#
exit the loop.
else
# Otherwise...
puts line.reverse
#
reverse the user's input and print it.
end
end
A when clause can have more than one expression associated with it. Multiple expressions are separated by commas, and the === operator is invoked on each one. That is,
it is possible to trigger the same block of code with more than one value:
def hasValue?(x)
case x
when nil, [], "", 0
false
else
true
end
end
#
#
#
#
#
#
Define a method named hasValue?
Multiway conditional based on value of x
if nil===x || []===x || ""===x || 0===x then
method return value is false
Otherwise
method return value is true
case versus switch
Java programmers and others accustomed to C-derived language syntax are familiar
with a multiway conditional switch statement, which is similar to Ruby’s case statement. There are, however, a number of important differences:
• In Java and related languages, the name of the statement is switch and its clauses
are labeled with case and default. Ruby uses case as the name of the statement,
and when and else for the clauses.
• The switch statement of other languages simply transfers control to the start of the
appropriate case. From there, control continues and can “fall through” to other
cases, until it reaches the end of the switch statement or encounters a break or
return statement. This fall-through behavior allows multiple case clauses to refer
to the same block of code. In Ruby, this same purpose is served by allowing multiple comma-separated expressions to be associated with each when clause. Ruby’s
case statement never allows fall-through.
• In Java and most compiled languages with C-like syntax, the expressions associated with each case label must be compile-time constants rather than arbitrary
runtime expressions. This often allows the compiler to implement the switch
statement using a very fast lookup table. There is no such restriction on Ruby’s
case statement, and its performance is equivalent to using an if statement with
repeated elsif clauses.
126 | Chapter 5: Statements and Control Structures
5.1.5 The ?: Operator
The conditional operator ?:, described earlier in §4.6.10, behaves much like an if
statement, with ? replacing then and : replacing else. It provides a succinct way to
express conditionals:
def how_many_messages(n) # Handle singular/plural
"You have " + n.to_s + (n==1 ? " message." : " messages.")
end
5.2 Loops
This section documents Ruby’s simple looping statements: while, until, and for. Ruby
also includes the ability to define custom looping constructs known as iterators. Iterators (see §5.3) are probably more commonly used than Ruby’s built-in looping
statements; they are documented later in this chapter.
5.2.1 while and until
Ruby’s basic looping statements are while and until. They execute a chunk of code
while a certain condition is true, or until the condition becomes true. For example:
x = 10
while x >= 0 do
puts x
x = x - 1
end
# Initialize a loop counter variable
# Loop while x is greater than or equal to 0
#
Print out the value of x
#
Subtract 1 from x
# The loop ends here
# Count back up to 10 using an until loop
x = 0
# Start at 0 (instead of -1)
until x > 10 do
# Loop until x is greater than 10
puts x
x = x + 1
end
# Loop ends here
The loop condition is the Boolean expression that appears between the while or until
and do keywords. The loop body is the Ruby code that appears between the do and the
end keyword. The while loop evaluates its condition. If the value is anything other than
false or nil, it executes its body, and then loops to evaluate its condition again. In this
way, the body is executed repeatedly, zero or more times, while the condition remains
true (or, more strictly, non-false and non-nil).
The until loop is the reverse. The condition is tested and the body is executed if the
condition evaluates to false or nil. This means that the body is executed zero or more
times while the condition is false or nil. Note that any until loop can be converted to
a while simply by negating the condition. Most programmers are familiar with while
loops, but many have not used until loops before. For this reason, you may want to
use while loops except when until truly improves the clarity of your code.
5.2 Loops | 127
The do keyword in a while or until loop is like the then keyword in an if statement: it
may be omitted altogether as long as a newline (or semicolon) appears between the
loop condition and the loop body.*
5.2.2 while and until As Modifiers
If the body of a loop is a single Ruby expression, you can express that loop in a particularly compact form by using while or until as a modifier after the expression. For
example:
x = 0
puts x = x + 1 while x < 10
# Initialize loop variable
# Output and increment in a single expression
This modifier syntax uses the while keyword itself to separate the loop body from the
loop condition, and avoids the need for the do (or newline) and end keywords. Contrast
this code with the more traditional while loop written on a single line:
x = 0
while x < 10 do puts x = x + 1 end
until can be used as a modifier just as while can be:
a = [1,2,3]
puts a.pop until a.empty?
# Initialize an array
# Pop elements from array until empty
Note that when while and until are used as modifiers, they must appear on the same
line as the loop body that they modify. If there is a newline between the loop body and
the while or until keyword, the Ruby interpreter will treat the loop body as an
unmodified expression and the while or until as the beginning of a regular loop.
When while and until are used as modifiers for a single Ruby expression, the loop
condition is tested first, even though it is written after the loop body. The loop body is
executed zero or more times, just as if it were formatted as a regular while or until loop.
There is a special-case exception to this rule. When the expression being evaluated is
a compound expression delimited by begin and end keywords, then the body is executed
first before the condition is tested:
x = 10
begin
puts x
x = x - 1
end until x == 0
# Initialize loop variable
# Start a compound expression: executed at least once
#
output x
#
decrement x
# End compound expression and modify it with a loop
This results in a construct much like the do/while loop of C, C++, and Java. Despite
its similarity to the do/while loop of other languages, this special-case behavior of loop
modifiers with the begin statement is counterintuitive and its use is discouraged.
Future releases of Ruby may forbid the use of while and until modifiers with begin/end.
* In Ruby 1.8, a colon may be used in place of the do keyword. This is no longer allowed in Ruby 1.9.
128 | Chapter 5: Statements and Control Structures