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 )
Figure 5-2. The return statement in a block
return is useful if you want to return from a method prematurely, or if you want to
return more than one value. For example:
# Return two copies of x, if x is not nil
def double(x)
return nil if x == nil
# Return prematurely
return x, x.dup
# Return multiple values
end
When first learning about Ruby blocks, it is natural to think of them as some kind of
nested function or mini-method. And if you think of them this way, you might expect
return simply to cause the block to return to the iterator that yielded to it. But blocks
are not methods, and the return keyword does not work this way. In fact, return is
remarkably consistent; it always causes the enclosing method to return, regardless of
how deeply nested within blocks it is.*
Note that the enclosing method is not the same thing as the invoking method. When
the return statement is used in a block, it does not just cause the block to return. And
it does not just cause the iterator that invokes the block to return. return always causes
the enclosing method to return. The enclosing method, also called the lexically enclosing method, is the method that the block appears inside of when you look at the source
code. Figure 5-2 illustrates the behavior of the return statement in a block.
The following code defines a method that uses return to return from inside a block:
# Return the index of the first occurrence of target within array or nil
# Note that this code just duplicates the Array.index method
def find(array, target)
array.each_with_index do |element,index|
* We’ll see an exception when we consider lambdas in §6.5.5.1. A lambda is a kind of a function created from
a block, and the behavior of return within a lambda is different from its behavior in an ordinary block.
5.5 Altering Control Flow | 147
return index if (element == target) # return from find
end
nil # If we didn't find the element, return nil
end
The return statement in this code does not just cause the block to return to the iterator
that invoked it. And it does not just cause the each_with_index iterator to return. It
causes the find method to return a value to its caller.
5.5.2 break
When used within a loop, the break statement transfers control out of the loop to the
first expression following the loop. Readers who know C, Java, or a similar language
will already be familiar with the use of break in a loop:
while(line = gets.chop)
break if line == "quit"
puts eval(line)
end
puts "Good bye"
# A loop starts here
# If this break statement is executed...
# ...then control is transferred here
When used in a block, break transfers control out of the block, out of the iterator that
invoked the block, and to the first expression following the invocation of the iterator.
For example:
f.each do |line|
break if line == "quit\n"
puts eval(line)
end
puts "Good bye"
# Iterate over the lines in file f
# If this break statement is executed...
# ...then control is transferred here
As you can see, using break inside a block is lexically the same as using it inside a loop.
If you consider the call stack, however, break in a block is more complicated because
it forces the iterator method that the block is associated with to return. Figure 5-3
illustrates this.
Note that unlike return, break never causes the lexically enclosing method to return.
break can only appear within a lexically enclosing loop or within a block. Using it in
any other context causes a LocalJumpError.
5.5.2.1 break with a value
Recall that all syntactic constructs in Ruby are expressions, and all can have a value.
The break statement can specify a value for the loop or iterator it is breaking out of.
The break keyword may be followed by an expression or a comma-separated list of
expressions. If break is used with no expression, then the value of the loop expression,
or the return value of the iterator method, is nil. If break is used with a single expression,
then the value of that expression becomes the value of the loop expression or the return
value of the iterator. And if break is used with multiple expressions, then the values of
148 | Chapter 5: Statements and Control Structures
Figure 5-3. The break statement in a block
those expressions are placed into an array, and that array becomes the value of the loop
expression or the return value of the iterator.
By contrast, a while loop that terminates normally with no break always has a value of
nil. The return value of an iterator that terminates normally is defined by the iterator
method. Many iterators, such as times and each, simply return the object on which they
were invoked.
5.5.3 next
The next statement causes a loop or iterator to end the current iteration and begin the
next. C and Java programmers know this control structure by the name continue. Here
is next in a loop:
while(line = gets.chop)
# A loop starts here
next if line[0,1] == "#" # If this line is a comment, go on to the next
puts eval(line)
# Control goes here when the next statement is executed
end
When next is used within a block, it causes the block to exit immediately, returning
control to the iterator method, which may then begin a new iteration by invoking the
block again:
f.each do |line|
# Iterate over the lines in file f
next if line[0,1] == "#"
# If this line is a comment, go to the next
puts eval(line)
# Control goes here when the next statement is executed
end
Using next in a block is lexically the same as using it in a while, until, or for/in loop.
When you consider the calling sequence, however, the block case is more complicated,
as Figure 5-4 illustrates.
5.5 Altering Control Flow | 149
Figure 5-4. The next statement in a block
next, break, and return
It is instructive to contrast Figure 5-4 with Figures 5-2 and 5-3. The next statement
causes a block to return to the iterator method that invoked it. The break statement
causes the block to return to its iterator and the iterator to return to the enclosing
method. And the return statement causes the block to return to the iterator, the iterator
to return to the enclosing method, and the enclosing method to return to its caller.
next may only be used within a loop or a block; it raises a LocalJumpError when used
in any other context.
5.5.3.1 next and block value
Like the return and break keywords, next may be used alone, or it may be followed by
an expression or a comma-separated list of expressions. When next is used in a loop,
any values following next are ignored. In a block, however, the expression or expressions become the “return value” of the yield statement that invoked the block. If
next is not followed by an expression, then the value of the yield is nil. If next is
followed by one expression, then the value of that expression becomes the value of the
yield. And if next is followed by a list of expressions, then the value of the yield is an
array of the value of those expressions.
In our earlier discussion of the return statement, we were careful to explain that blocks
are not functions, and that the return statement does not make a block return to the
iterator that invoked it. As you can see, this is exactly what the next statement does.
Here is code where you might use it in this way:
squareroots = data.collect do |x|
next 0 if x < 0 # Return 0 for negative values
150 | Chapter 5: Statements and Control Structures
Math.sqrt(x)
end
Normally, the value of a yield expression is the value of the last expression in the block.
As with the return statement, it is not often necessary to explicitly use next to specify
a value. This code could also have been written like this, for example:
squareroots = data.collect do |x|
if (x < 0) then 0 else Math.sqrt(x) end
end
5.5.4 redo
The redo statement restarts the current iteration of a loop or iterator. This is not the
same thing as next. next transfers control to the end of a loop or block so that the next
iteration can begin, whereas redo transfers control back to the top of the loop or block
so that the iteration can start over. If you come to Ruby from C-like languages, then
redo is probably a new control structure for you.
redo transfers control to the first expression in the body of the loop or in a block. It
does not retest the loop condition, and it does not fetch the next element from an
iterator. The following while loop would normally terminate after three iterations, but
a redo statement makes it iterate four times:
i = 0
while(i < 3)
# Prints "0123" instead of "012"
# Control returns here when redo is executed
print i
i += 1
redo if i == 3
end
redo is not a commonly used statement, and many examples, like this one, are con-
trived. One use, however, is to recover from input errors when prompting a user for
input. The following code uses redo within a block for this purpose:
puts "Please enter the first word you think of"
words = %w(apple banana cherry)
# shorthand for ["apple", "banana", "cherry"]
response = words.collect do |word|
# Control returns here when redo is executed
print word + "> "
# Prompt the user
response = gets.chop
# Get a response
if response.size == 0
# If user entered nothing
word.upcase!
# Emphasize the prompt with uppercase
redo
# And skip to the top of the block
end
response
# Return the response
end
5.5 Altering Control Flow | 151
5.5.5 retry
The retry statement is normally used in a rescue clause to reexecute a block of code
that raised an exception. This is described in §5.6.3.5. In Ruby 1.8, however, retry has
another use: it restarts an iterator-based iteration (or any method invocation) from the
beginning. This use of the retry statement is extremely rare, and it has been removed
from the language in Ruby 1.9. It should, therefore, be considered a deprecated
language feature and should not be used in new code.
In a block, the retry statement does not just redo the current invocation of the block;
it causes the block and the iterator method to exit and then reevaluates the iterator
expression to restart the iteration. Consider the following code:
n = 10
n.times do |x|
print x
if x == 9
n -= 1
retry
end
end
#
#
#
#
#
Iterate n times from 0 to n–1
Print iteration number
If we've reached 9
Decrement n (we won't reach 9 the next time!)
Restart the iteration
The code uses retry to restart the iterator, but it is careful to avoid an infinite loop. On
the first invocation, it prints the numbers 0123456789 and then restarts. On the second
invocation, it prints the numbers 012345678 and does not restart.
The magic of the retry statement is that it does not retry the iterator in exactly the same
way each time. It completely reevaluates the iterator expression, which means that the
arguments to the iterator (and even the object on which it is invoked) may be different
each time the iterator is retried. If you are not used to highly dynamic languages like
Ruby, this reevaluation may seem counterintuitive to you.
The retry statement is not restricted to use in blocks; it always just reevaluates the
nearest containing method invocation. This means that it can be used (prior to Ruby
1.9) to write iterators like the following that works like a while loop:
# This method behaves like a while loop: if x is non-nil and non-false,
# invoke the block and then retry to restart the loop and test the
# condition again. This method is slightly different than a true while loop:
# you can use C-style curly braces to delimit the loop body. And
# variables used only within the body of the loop remain local to the block.
def repeat_while(x)
if x
# If the condition was not nil or false
yield # Run the body of the loop
retry # Retry and re-evaluate loop condition
end
end
152 | Chapter 5: Statements and Control Structures
5.5.6 throw and catch
throw and catch are Kernel methods that define a control structure that can be thought
of as a multilevel break. throw doesn’t just break out of the current loop or block but
can actually transfer out any number of levels, causing the block defined with a catch
to exit. The catch need not even be in the same method as the throw. It can be in the
calling method, or somewhere even further up the call stack.
Languages like Java and JavaScript allow loops to be named or labeled with an arbitrary
prefix. When this is done, a control structure known as a “labeled break” causes the
named loop to exit. Ruby’s catch method defines a labeled block of code, and Ruby’s
throw method causes that block to exit. But throw and catch are much more general
than a labeled break. For one, it can be used with any kind of statement and is not
restricted to loops. More profoundly, a throw can propagate up the call stack to cause
a block in an invoking method to exit.
If you are familiar with languages like Java and JavaScript, then you probably recognize
throw and catch as the keywords those languages use for raising and handling exceptions. Ruby does exceptions differently, using raise and rescue, which we’ll learn about
later in this chapter. But the parallel to exceptions is intentional. Calling throw is very
much like raising an exception. And the way a throw propagates out through the lexical
scope and then up the call stack is very much the same as the way an exception propagates out and up. (We’ll see much more about exception propagation later in the
chapter.) Despite the similarity to exceptions, it is best to consider throw and catch as
a general-purpose (if perhaps infrequently used) control structure rather than an exception mechanism. If you want to signal an error or exceptional condition, use
raise instead of throw.
The following code demonstrates how throw and catch can be used to “break out” of
nested loops:
for matrix in data do
# Process a deeply nested
catch :missing_data do
# Label this statement so
for row in matrix do
for value in row do
throw :missing_data unless value # Break out of two
# Otherwise, do some actual data processing here.
end
end
end
# We end up here after the nested loops finish processing
# We also get here if :missing_data is thrown.
end
data structure.
we can break out.
loops at once.
each matrix.
Note that the catch method takes a symbol argument and a block. It executes the block
and returns when the block exits or when the specified symbol is thrown. throw also
expects a symbol as its argument and causes the corresponding catch invocation to
return. If no catch call matches the symbol passed to throw, then a NameError exception
5.5 Altering Control Flow | 153
is raised. Both catch and throw can be invoked with string arguments instead of symbols.
These are converted internally to symbols.
One of the features of throw and catch is that they work even when the throw and
catch are in different methods. We could refactor this code to put the innermost loop
into a separate method, and the control flow would still work correctly.
If throw is never called, a catch invocation returns the value of the last expression in its
block. If throw is called, then the return value of the corresponding catch is, by default,
nil. You can, however, specify an arbitrary return value for catch by passing a second
argument to throw. The return value of catch can help you distinguish normal completion of the block from abnormal completion with throw, and this allows you to write
code that does any special processing necessary to respond to the throw.
throw and catch are not commonly used in practice. If you find yourself using catch
and throw within the same method, consider refactoring the catch into a separate
method definition and replacing the throw with a return.
5.6 Exceptions and Exception Handling
An exception is an object that represents some kind of exceptional condition; it indicates
that something has gone wrong. This could be a programming error—attempting to
divide by zero, attempting to invoke a method on an object that does not define the
method, or passing an invalid argument to a method. Or it could be the result from
some kind of external condition—making a network request when the network is
down, or trying to create an object when the system is out of memory.
When one of these errors or conditions occurs, an exception is raised (or thrown). By
default, Ruby programs terminate when an exception occurs. But it is possible to declare exception handlers. An exception handler is a block of code that is executed if an
exception occurs during the execution of some other block of code. In this sense,
exceptions are a kind of control statement. Raising an exception transfers the flow-ofcontrol to exception handling code. This is like using the break statement to exit from
a loop. As we’ll see, though, exceptions are quite different from the break statement;
they may transfer control out of many enclosing blocks and even up the call stack in
order to reach the exception handler.
Ruby uses the Kernel method raise to raise exceptions, and uses a rescue clause to
handle exceptions. Exceptions raised by raise are instances of the Exception class or
one of its many subclasses. The throw and catch methods described earlier in this chapter are not intended to signal and handle exceptions, but a symbol thrown by throw
propagates in the same way that an exception raised by raise does. Exception objects,
exception propagation, the raise method, and the rescue clause are described in detail
in the subsections that follow.
154 | Chapter 5: Statements and Control Structures
5.6.1 Exception Classes and Exception Objects
Exception objects are instances of the Exception class or one of its subclasses. Numerous subclasses exist. These subclasses do not typically define new methods or new
behavior, but they allow exceptions to be categorized by type. The class hierarchy is
illustrated in Figure 5-5.
Object
+--Exception
+--NoMemoryError
+--ScriptError
|
+--LoadError
|
+--NotImplementedError
|
+--SyntaxError
+--SecurityError
# Was
+--SignalException
|
+--Interrupt
+--SystemExit
+--SystemStackError
# Was
+--StandardError
+--ArgumentError
+--FiberError
# New
+--IOError
|
+--EOFError
+--IndexError
|
+--KeyError
# New
|
+--StopIteration # New
+--LocalJumpError
+--NameError
|
+--NoMethodError
+--RangeError
|
+--FloatDomainError
+--RegexpError
+--RuntimeError
+--SystemCallError
+--ThreadError
+--TypeError
+--ZeroDivisionError
a StandardError in 1.8
a StandardError in 1.8
in 1.9
in 1.9
in 1.9
Figure 5-5. The Ruby Exception Class Hierarchy
You don’t need to be familiar with each of these exception subclasses. Their names tell
you what they are used for. It is important to note that most of these subclasses extend
a class known as StandardError. These are the “normal” exceptions that typical Ruby
programs try to handle. The other exceptions represent lower-level, more serious, or
less recoverable conditions, and normal Ruby programs do not typically attempt to
handle them.
If you use ri to find documentation for these exception classes, you’ll find that most of
them are undocumented. This is in part because most of them add no new methods to
5.6 Exceptions and Exception Handling | 155