1. Trang chủ >
  2. Công Nghệ Thông Tin >
  3. Kỹ thuật lập trình >

5  Altering Control Flow

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



Xem Thêm
Tải bản đầy đủ (.pdf) (448 trang)

×