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

6  Exceptions and Exception Handling

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 )


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



those defined by the base Exception class. The important thing to know about a given

exception class is when it can be raised. This is typically documented by the methods

that raise the exception rather than by the exception class itself.



5.6.1.1 The methods of exception objects

The Exception class defines two methods that return details about the exception. The

message method returns a string that may provide human-readable details about what

went wrong. If a Ruby program exits with an unhandled exception, this message will

typically be displayed to the end user, but the primary purpose of this message is to aid

a programmer in diagnosing the problem.

The other important method of exception objects is backtrace. This method returns

an array of strings that represents the call stack at the point that the exception was

raised. Each element of the array is a string of the form:

filename : linenumber in methodname



The first element of the array specifies the position at which the exception was raised;

the second element specifies the position at which the method that raised the exception

was called; the third element specifies the position at which that method was called;

and so on. (The Kernel method caller returns a stack trace in this same format; you

can try it out in irb.) Exception objects are typically created by the raise method. When

this is done, the raise method sets the stack trace of the exception appropriately. If you

create your own exception object, you can set the stack trace to whatever you want

with the set_backtrace method.



5.6.1.2 Creating exception objects

Exception objects are typically created by the raise method, as we’ll see below. However, you can create your own objects with the normal new method, or with another

class method named exception. Both accept a single optional string argument. If specified, the string becomes the value of the message method.



5.6.1.3 Defining new exception classes

If you are defining a module of Ruby code, it is often appropriate to define your own

subclass of StandardError for exceptions that are specific to your module. This may be

a trivial, one-line subclass:

class MyError < StandardError; end



5.6.2 Raising Exceptions with raise

The Kernel method raise raises an exception. fail is a synonym that is sometimes used

when the expectation is that the exception will cause the program to exit. There are

several ways to invoke raise:



156 | Chapter 5: Statements and Control Structures



• If raise is called with no arguments, it creates a new RuntimeError object (with no

message) and raises it. Or, if raise is used with no arguments inside a rescue clause,

it simply re-raises the exception that was being handled.

• If raise is called with a single Exception object as its argument, it raises that

exception. Despite its simplicity, this is not actually a common way to use raise.

• If raise is called with a single string argument, it creates a new RuntimeError

exception object, with the specified string as its message, and raises that exception.

This is a very common way to use raise.

• If the first argument to raise is an object that has an exception method, then

raise invokes that method and raises the Exception object that it returns. The

Exception class defines an exception method, so you can specify the class object

for any kind of exception as the first argument to raise.

raise accepts a string as its optional second argument. If a string is specified, it is

passed to the exception method of the first argument. This string is intended for

use as the exception message.

raise also accepts an optional third argument. An array of strings may be specified

here, and they will be used as the backtrace for the exception object. If this third

argument is not specified, raise sets the backtrace of the exception itself (using the

Kernel method caller).

The following code defines a simple method that raises an exception if invoked with a

parameter whose value is invalid:

def factorial(n)

raise "bad argument" if n < 1

return 1 if n == 1

n * factorial(n-1)

end



#

#

#

#



Define a factorial method with argument n

Raise an exception for bad n

factorial(1) is 1

Compute other factorials recursively



This method invokes raise with a single string argument. These are some equivalent

ways to raise the same exception:

raise RuntimeError, "bad argument" if n < 1

raise RuntimeError.new("bad argument") if n < 1

raise RuntimeError.exception("bad argument") if n < 1



In this example, an exception of class ArgumentError is probably more appropriate than

RuntimeError:

raise ArgumentError if n < 1



And a more detailed error message would be helpful:

raise ArgumentError, "Expected argument >= 1. Got #{n}" if n < 1



The intent of the exception we’re raising here is to point out a problem with the invocation of the factorial method, not with the code inside the method. The exception

raised by the code here will have a backtrace whose first element identifies where

raise was called. The second element of the array will actually identify the code that



5.6 Exceptions and Exception Handling | 157



called factorial with the bad argument. If we want to point directly to the problem

code, we can provide a custom stack trace as the third argument to raise with the

Kernel method caller:

if n < 1

raise ArgumentError, "Expected argument >= 1. Got #{n}", caller

end



Note that the factorial method checks whether its argument is in the correct range,

but it does not check whether it is of the right type. We might add more careful errorchecking by adding the following as the first line of the method:

raise TypeError, "Integer argument expected" if not n.is_a? Integer



On the other hand, notice what happens if we pass a string argument to the

factorial method as it is written above. Ruby compares the argument n to the integer

1 with the < operator. If the argument is a string, the comparison makes no sense, and

it fails by raising a TypeError. If the argument is an instance of some class that does not

define the < operator, then we get a NoMethodError instead.

The point here is that exceptions can occur even if we do not call raise in our own

code. It is important, therefore, to know how to handle exceptions, even if we never

raise them ourselves. Handling exceptions is covered in the next section.



5.6.3 Handling Exceptions with rescue

raise is a Kernel method. A rescue clause, by contrast, is a fundamental part of the

Ruby language. rescue is not a statement in its own right, but rather a clause that can

be attached to other Ruby statements. Most commonly, a rescue clause is attached to

a begin statement. The begin statement exists simply to delimit the block of code within

which exceptions are to be handled. A begin statement with a rescue clause looks like

this:

begin

# Any number of Ruby statements go here.

# Usually, they are executed without exceptions and

# execution continues after the end statement.

rescue

# This is the rescue clause; exception-handling code goes here.

# If an exception is raised by the code above, or propagates up

# from one of the methods called above, then execution jumps here.

end



5.6.3.1 Naming the exception object

In a rescue clause, the global variable $! refers to the Exception object that is being

handled. The exclamation mark is a mnemonic: an exception is kind of like an

exclamation. If your program includes the line:

require 'English'



158 | Chapter 5: Statements and Control Structures



then you can use the global variable $ERROR_INFO instead.

A better alternative to $! or $ERROR_INFO is to specify a variable name for the exception

object in the rescue clause itself:

rescue => ex



The statements of this rescue clause can now use the variable ex to refer to the

Exception object that describes the exception. For example:

begin

x = factorial(-1)

rescue => ex

puts "#{ex.class}: #{ex.message}"

end



#

#

#

#

#



Handle exceptions in this block

Note illegal argument

Store exception in variable ex

Handle exception by printing message

End the begin/rescue block



Note that a rescue clause does not define a new variable scope, and a variable named

in the rescue clause is visible even after the end of the rescue clause. If you use a variable

in a rescue clause, then an exception object may be visible after the rescue is complete,

even when $! is no longer set.



5.6.3.2 Handling exceptions by type

The rescue clauses shown here handle any exception that is a StandardError (or subclass) and ignore any Exception object that is not a StandardError. If you want to handle

nonstandard exceptions outside the StandardError hierarchy, or if you want to handle

only specific types of exceptions, you must include one or more exception classes in

the rescue clause. Here’s how you would write a rescue clause that would handle any

kind of exception:

rescue Exception



Here’s how you would write a rescue clause to handle an ArgumentError and assign the

exception object to the variable e:

rescue ArgumentError => e



Recall that the factorial method we defined earlier can raise ArgumentError or

TypeError. Here’s how we would write a rescue clause to handle exceptions of either

of these types and assign the exception object to the variable error:

rescue ArgumentError, TypeError => error



Here, finally, we see the syntax of the rescue clause at its most general. The rescue

keyword is followed by zero or more comma-separated expressions, each of which must

evaluate to a class object that represents the Exception class or a subclass. These

expressions are optionally followed by => and a variable name.

Now suppose we want to handle both ArgumentError and TypeError, but we want to

handle these two exceptions in different ways. We might use a case statement to run

different code based on the class of the exception object. It is more elegant, however,

to simply use multiple rescue clauses. A begin statement can have zero or more of them:



5.6 Exceptions and Exception Handling | 159



begin

x = factorial(1)

rescue ArgumentError => ex

puts "Try again with a value >= 1"

rescue TypeError => ex

puts "Try again with an integer"

end



Note that the Ruby interpreter attempts to match exceptions to rescue clauses in the

order they are written. Therefore, you should list your most specific exception subclasses first and follow these with more general types. If you want to handle EOFError

differently than IOError, for example, be sure to put the rescue clause for EOFError first

or the IOError code will handle it. If you want a “catch-all” rescue clause that handles

any exception not handled by previous clauses, use rescue Exception as the last

rescue clause.



5.6.3.3 Propagation of exceptions

Now that we’ve introduced rescue clauses, we can explain in more detail the propagation of exceptions. When an exception is raised, control is immediately transferred

outward and upward until a suitable rescue clause is found to handle the exception.

When the raise method executes, the Ruby interpreter looks to see whether the containing block has a rescue clause associated with it. If not (or if the rescue clause is not

declared to handle that kind of exception), then the interpreter looks at the containing

block of the containing block. If there is no suitable rescue clause anywhere in the

method that called raise, then the method itself exits.

When a method exits because of an exception, it is not the same thing as a normal

return. The method does not have a return value, and the exception object continues

propagating from the site of the method invocation. The exception propagates outward

through the enclosing blocks, looking for a rescue clause declared to handle it. And if

no such clause is found, then this method returns to its caller. This continues up the

call stack. If no exception handler is ever located, then the Ruby interpreter prints the

exception message and backtrace and exits. For a concrete example, consider the

following code:

def explode

# This method raises a RuntimeError 10% of the time

raise "bam!" if rand(10) == 0

end

def risky

begin

10.times do

explode

end

rescue TypeError

puts $!

end

"hello"

end



#

#

#

#

#

#



This block

contains another block

that might raise an exception.

No rescue clause here, so propagate out.

This rescue clause cannot handle a RuntimeError..

so skip it and propagate out.



# This is the normal return value, if no exception occurs.

# No rescue clause here, so propagate up to caller.



160 | Chapter 5: Statements and Control Structures



def defuse

begin

puts risky

rescue RuntimeError => e

puts e.message

end

end



#

#

#

#



The following code may fail with an exception.

Try to invoke and print the return value.

If we get an exception

print the error message instead.



defuse



An exception is raised in the method explode. That method has no rescue clause, so

the exception propagates out to its caller, a method named risky. risky has a rescue

clause, but it is only declared to handle TypeError exceptions, not RuntimeError

exceptions. The exception propagates out through the lexical blocks of risky and then

propagates up to the caller, a method named defuse. defuse has a rescue clause for

RuntimeError exceptions, so control is transferred to this rescue clause and the

exception stops propagating.

Note that this code includes the use of an iterator (the Integer.times method) with an

associated block. For simplicity, we said that the exception simply propagated outward

through this lexical block. The truth is that blocks behave more like method invocations

for the purposes of exception propagation. The exception propagates from the block

up to the iterator that invoked the block. Predefined looping iterators like

Integer.times do no exception handling of their own, so the exception propagates up

the call stack from the times iterator to the risky method that invoked it.



5.6.3.4 Exceptions during exception handling

If an exception occurs during the execution of a rescue clause, the exception that was

originally being handled is discarded, and the new exception propagates from the point

at which it was raised. Note that this new exception cannot be handled by rescue clauses

that follow the one in which it occurred.



5.6.3.5 retry in a rescue clause

When the retry statement is used within a rescue clause, it reruns the block of code to

which the rescue is attached. When an exception is caused by a transient failure, such

as an overloaded server, it might make sense to handle the exception by simply trying

again. Many other exceptions, however, reflect programming errors (TypeError,

ZeroDivisionError) or nontransient failures (EOFError or NoMemoryError). retry is not

a suitable handling technique for these exceptions.

Here is a simple example that uses retry in an attempt to wait for a network failure to

be resolved. It tries to read the contents of a URL, and retries upon failure. It never tries

more than four times in all, and it uses “exponential backoff” to increase the wait time

between attempts:



5.6 Exceptions and Exception Handling | 161



require 'open-uri'

tries = 0

# How many times have we tried to read the URL

begin

# This is where a retry begins

tries += 1

# Try to print out the contents of a URL

open('http://www.example.com/') {|f| puts f.readlines }

rescue OpenURI::HTTPError => e # If we get an HTTP error

puts e.message

# Print the error message

if (tries < 4)

# If we haven't tried 4 times yet...

sleep(2**tries)

# Wait for 2, 4, or 8 seconds

retry

# And then try again!

end

end



5.6.4 The else Clause

A begin statement may include an else clause after its rescue clauses. You might guess

that the else clause is a catch-all rescue: that it handles any exception that does not

match a previous rescue clause. This is not what else is for. The else clause is an

alternative to the rescue clauses; it is used if none of the rescue clauses are needed. That

is, the code in an else clause is executed if the code in the body of the begin statement

runs to completion without exceptions.

Putting code in an else clause is a lot like simply tacking it on to the end of the begin

clause. The only difference is that when you use an else clause, any exceptions raised

by that clause are not handled by the rescue statements.

The use of an else clause is not particularly common in Ruby, but they can be stylistically useful to emphasize the difference between normal completion of a block of code

and exceptional completion of a block of code.

Note that it does not make sense to use an else clause without one or more rescue

clauses. The Ruby interpreter allows it but issues a warning. No rescue clause may

appear after an else clause.

Finally, note that the code in an else clause is only executed if the code in the begin

clause runs to completion and “falls off” the end. If an exception occurs, then the

else clause will obviously not be executed. But break, return, next, and similar statements in the begin clause may also prevent the execution of the else clause.



5.6.5 The ensure Clause

A begin statement may have one final clause. The optional ensure clause, if it appears,

must come after all rescue and else clauses. It may also be used by itself without any

rescue or else clauses.

The ensure clause contains code that always runs, no matter what happens with the

code following begin:



162 | Chapter 5: Statements and Control Structures



• If that code runs to completion, then control jumps to the else clause—if there is

one—and then to the ensure clause.

• If the code executes a return statement, then the execution skips the else clause

and jumps directly to the ensure clause before returning.

• If the code following begin raises an exception, then control jumps to the appropriate rescue clause, and then to the ensure clause.

• If there is no rescue clause, or if no rescue clause can handle the exception, then

control jumps directly to the ensure clause. The code in the ensure clause is executed before the exception propagates out to containing blocks or up the call stack.

The purpose of the ensure clause is to ensure that housekeeping details such as closing

files, disconnecting database connections, and committing or aborting transactions get

taken care of. It is a powerful control structure, and you should use it whenever you

allocate a resource (such as a file handle or database connection) to ensure that proper

deallocation or cleanup occurs.

Note that ensure clauses complicate the propagation of exceptions. In our earlier explanation, we omitted any discussion of ensure clauses. When an exception propagates,

it does not simply jump magically from the point where it is raised to the point where

it is handled. There really is a propagation process. The Ruby interpreter searches out

through containing blocks and up through the call stack. At each begin statement, it

looks for a rescue clause that can handle the exception. And it looks for associated

ensure clauses, and executes all of them that it passes through.

An ensure clause can cancel the propagation of an exception by initiating some other

transfer of control. If an ensure clause raises a new exception, then that new exception

propagates in place of the original. If an ensure clause includes a return statement, then

exception propagation stops, and the containing method returns. Control statements

such as break and next have similar effects: exception propagation is abandoned, and

the specified control transfer takes place.

An ensure clause also complicates the idea of a method return value. Although

ensure clauses are usually used to ensure that code will run even if an exception occurs,

they also work to ensure that code will be run before a method returns. If the body of

a begin statement includes a return statement, the code in the ensure clause will be run

before the method can actually return to its caller. Furthermore, if an ensure clause

contains a return statement of its own, it will change the return value of the method.

The following code, for example, returns the value 2:

begin

return 1

ensure

return 2

end



# Skip to the ensure clause before returning to caller

# Replace the return value with this new value



Note that an ensure clause does not alter the return value of a method unless it explicitly

uses a return statement. The following method, for example, returns 1, not 2:



5.6 Exceptions and Exception Handling | 163



def test

begin return 1 ensure 2 end

end



If a begin statement does not propagate an exception, then the value of the statement

is the value of the last expression evaluated in the begin, rescue, or else clauses. The

code in the ensure clause is guaranteed to run, but it does not affect the value of the

begin statement.



5.6.6 rescue with Method, Class, and Module Definitions

Throughout this discussion of exception handling, we have described the rescue,

else, and ensure keywords as clauses of a begin statement. In fact, they can also be used

as clauses of the def statement (defines a method), the class statement (defines a class),

and the module statement (defines a module). Method definitions are covered in Chapter 6; class and module definitions are covered in Chapter 7.

The following code is a sketch of a method definition with rescue, else, and ensure

clauses:

def method_name(x)

# The body of the method goes here.

# Usually, the method body runs to completion without exceptions

# and returns to its caller normally.

rescue

# Exception-handling code goes here.

# If an exception is raised within the body of the method, or if

# one of the methods it calls raises an exception, then control

# jumps to this block.

else

# If no exceptions occur in the body of the method

# then the code in this clause is executed.

ensure

# The code in this clause is executed no matter what happens in the

# body of the method. It is run if the method runs to completion, if

# it throws an exception, or if it executes a return statement.

end



5.6.7 rescue As a Statement Modifier

In addition to its use as a clause, rescue can also be used as a statement modifier. Any

statement can be followed by the keyword rescue and another statement. If the first

statement raises an exception, the second statement is executed instead. For example:

# Compute factorial of x, or use 0 if the method raises an exception

y = factorial(x) rescue 0



This is equivalent to:

y = begin

factorial(x)

rescue



164 | Chapter 5: Statements and Control Structures



0

end



The advantage of the statement modifier syntax is that the begin and end keywords are

not required. When used in this way, rescue must be used alone, with no exception

class names and no variable name. A rescue modifier handles any StandardError

exception but does not handle exceptions of other types. Unlike if and while modifiers,

the rescue modifier has higher precedence (see Table 4-2 in the previous chapter) than

assignment operators. This means that it applies only to the righthand side of an assignment (like the example above) rather than to the assignment expression as a whole.



5.7 BEGIN and END

BEGIN and ENDare reserved words in Ruby that declare code to be executed at the very

beginning and very end of a Ruby program. (Note that BEGIN and END in capital letters

are completely different from begin and end in lowercase.) If there is more than one

BEGIN statement in a program, they are executed in the order in which the interpreter

encounters them. If there is more than one END statement, they are executed in the



reverse of the order in which they are encountered—that is, the first one is executed

last. These statements are not commonly used in Ruby. They are inherited from Perl,

which in turn inherited them from the awk text-processing language.

BEGIN and END must be followed by an open curly brace, any amount of Ruby code, and

a close curly brace. The curly braces are required; do and end are not allowed here. For



example:

BEGIN {

# Global initialization code goes here

}

END {

# Global shutdown code goes here

}



The BEGIN and END statements are different from each other in subtle ways. BEGIN statements are executed before anything else, including any surrounding code. This means

that they define a local variable scope that is completely separate from the surrounding

code. It only really makes sense to put BEGIN statements in top-level code; a BEGIN within

a conditional or loop will be executed without regard for the conditions that surround

it. Consider this code:

if (false)

BEGIN {

puts "if";

a = 4;

}

else

BEGIN { puts "else" }

end



# This will be printed

# This variable only defined here

# Also printed



5.7 BEGIN and END | 165



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

×