amiga-e/amigae33a/E_v3.3a/Docs/BeginnersGuide/Exceptions.guide

406 lines
16 KiB
Plaintext

@database beginner.guide
@Master beginner
@Width 75
This is the AmigaGuide® file beginner.guide, produced by Makeinfo-1.55 from
the input file beginner.
@NODE "main" "Exception Handling"
@Next "Memory.guide/main"
@Prev "Modules.guide/main"
@Toc "Contents.guide/main"
Exception Handling
******************
Often your program has to check the results of functions and do
different things if errors have occurred. For instance, if you try to
open a window (using @{b }OpenW@{ub }), you may get a @{b }NIL@{ub } pointer returned which
shows that the window could not be opened for some reason. In this case
you normally can't continue with the program, so you must tidy up and
terminate. Tidying up can sometimes involve closing windows, screens and
libraries, so sometimes your error cases can make your program cluttered
and messy. This is where exceptions come in--an @{fg shine }exception@{fg text } is simply an
error case, and @{fg shine }exception handling@{fg text } is dealing with error cases. The
exception handling in E neatly separates error specific code from the real
code of your program.
@{" Procedures with Exception Handlers " Link "Procedures with Exception Handlers" }
@{" Raising an Exception " Link "Raising an Exception" }
@{" Automatic Exceptions " Link "Automatic Exceptions" }
@{" Raise within an Exception Handler " Link "Raise within an Exception Handler" }
@ENDNODE
@NODE "Procedures with Exception Handlers" "Procedures with Exception Handlers"
@Next "Raising an Exception"
@Toc "main"
Procedures with Exception Handlers
==================================
A procedure with an exception handler looks like this:
PROC fred(params...) HANDLE
/* Main, real code */
EXCEPT
/* Error handling code */
ENDPROC
This is very similar to a normal procedure, apart from the @{b }HANDLE@{ub } and
@{b }EXCEPT@{ub } keywords. The @{b }HANDLE@{ub } keyword means the procedure is going to have
an exception handler, and the @{b }EXCEPT@{ub } keyword marks the end of the normal
code and the start of the exception handling code. The procedure works
just as normal, executing the code in the part before the @{b }EXCEPT@{ub }, but when
an error happens you can pass control to the exception handler (i.e., the
code after the @{b }EXCEPT@{ub } is executed).
@ENDNODE
@NODE "Raising an Exception" "Raising an Exception"
@Next "Automatic Exceptions"
@Prev "Procedures with Exception Handlers"
@Toc "main"
Raising an Exception
====================
When an error occurs (and you want to handle it), you @{fg shine }raise@{fg text } an
exception using either the @{b }Raise@{ub } or @{b }Throw@{ub } function. You call @{b }Raise@{ub } with a
number which identifies the kind of error that occurred. The code in the
exception handler is responsible for decoding the number and then doing
the appropriate thing. @{b }Throw@{ub } is very similar to @{b }Raise@{ub }, and the following
description of @{b }Raise@{ub } also applies to @{b }Throw@{ub }. The difference is that @{b }Throw@{ub }
takes a second argument which can be used to pass extra information to a
handler (usually a string). The terms `raising' and `throwing' an
exception can be used interchangeably.
When @{b }Raise@{ub } is called it immediately stops the execution of the current
procedure code and passes control to the exception handler of most recent
procedure which has a handler (which may be the current procedure). This
is a bit complicated, but you can stick to raising exceptions and handling
them in the same procedure, as in the next example:
CONST BIG_AMOUNT = 100000
ENUM ERR_MEM=1
PROC main() HANDLE
DEF block
block:=New(BIG_AMOUNT)
IF block=NIL THEN Raise(ERR_MEM)
WriteF('Got enough memory\\n')
EXCEPT
IF exception=ERR_MEM
WriteF('Not enough memory\\n')
ELSE
WriteF('Unknown exception\\n')
ENDIF
ENDPROC
This uses an exception handler to print a message saying there wasn't
enough memory if the call to @{b }New@{ub } returns @{b }NIL@{ub }. The parameter to @{b }Raise@{ub } is
stored in the special variable @{b }exception@{ub } in the exception handler part of
the code, so if @{b }Raise@{ub } is called with a number other than @{b }ERR_MEM@{ub } a message
saying "Unknown exception" will be printed.
Try running this program with a really large @{b }BIG_AMOUNT@{ub } constant, so
that the @{b }New@{ub } can't allocate the memory. Notice that the "Got enough
memory" is not printed if @{b }Raise@{ub } is called. That's because the execution
of the normal procedure code stops when @{b }Raise@{ub } is called, and control
passes to the appropriate exception handler. When the end of the
exception handler is reached the procedure is finished, and in this case
the program terminates because the procedure was the @{b }main@{ub } procedure.
If @{b }Throw@{ub } is used instead of @{b }Raise@{ub } then, in the handler, the special
variable @{b }exceptioninfo@{ub } will contain the value of the second parameter.
This can be used in conjunction with @{b }exception@{ub } to provide the handler with
more information about the error. Here's the above example re-written to
use @{b }Throw@{ub }:
CONST BIG_AMOUNT = 100000
ENUM ERR_MEM=1
PROC main() HANDLE
DEF block
block:=New(BIG_AMOUNT)
IF block=NIL THEN Throw(ERR_MEM, 'Not enough memory\\n')
WriteF('Got enough memory\\n')
EXCEPT
IF exception=ERR_MEM
WriteF(exceptioninfo)
ELSE
WriteF('Unknown exception\\n')
ENDIF
ENDPROC
An enumeration (using @{b }ENUM@{ub }) is a good way of getting different
constants for various exceptions. It's always a good idea to use
constants for the parameter to @{b }Raise@{ub } and in the exception handler, because
it makes everything a lot more readable: @{b }Raise(ERR_MEM)@{ub } is much clearer
than @{b }Raise(1)@{ub }. The enumeration starts at one because zero is a special
exception: it usually means that no error occurred. This is useful when
the handler does the same cleaning up that would normally be done when the
program terminates successfully. For this reason there is a special form
of @{b }EXCEPT@{ub } which automatically raises a zero exception when the code in the
procedure successfully terminates. This is @{b }EXCEPT DO@{ub }, with the @{b }DO@{ub }
suggesting to the reader that the exception handler is called even if no
error occurs. Also, the argument to the @{b }Raise@{ub } function defaults to zero
if it is omitted (see @{"Default Arguments" Link "Procedures.guide/Default Arguments" }).
So, what happens if you call @{b }Raise@{ub } in a procedure without an exception
handler? Well, this is where the real power of the handling mechanism
comes to light. In this case, control passes to the exception handler of
the most @{fg shine }recent@{fg text } procedure with a handler. If none are found then the
program terminates. `Recent' means one of the procedures involved in
calling your procedure. So, if the procedure @{b }fred@{ub } calls @{b }barney@{ub }, then when
@{b }barney@{ub } is being executed @{b }fred@{ub } is a recent procedure. Because the @{b }main@{ub }
procedure is where the program starts it is a recent procedure for every
other procedure in the program. This means, in practice:
@{b }*@{ub } If you define @{b }fred@{ub } to be a procedure with an exception handler then
any procedures called by @{b }fred@{ub } will have their exceptions handled by
the handler in @{b }fred@{ub } if they don't have their own handler.
@{b }*@{ub } If you define @{b }main@{ub } to be a procedure with an exception handler then
any exceptions that are raised will always be dealt with by some
exception handling code (i.e., the handler of @{b }main@{ub } or some other
procedure).
Here's a more complicated example:
ENUM FRED=1, BARNEY
PROC main()
WriteF('Hello from main\\n')
fred()
barney()
WriteF('Goodbye from main\\n')
ENDPROC
PROC fred() HANDLE
WriteF(' Hello from fred\\n')
Raise(FRED)
WriteF(' Goodbye from fred\\n')
EXCEPT
WriteF(' Handler fred: \\d\\n', exception)
ENDPROC
PROC barney()
WriteF(' Hello from barney\\n')
Raise(BARNEY)
WriteF(' Goodbye from barney\\n')
ENDPROC
When you run this program you get the following output:
Hello from main
Hello from fred
Handler fred: 1
Hello from barney
This is because the @{b }fred@{ub } procedure is terminated by the @{b }Raise(FRED)@{ub } call,
and the whole program is terminated by the @{b }Raise(BARNEY)@{ub } call (since
@{b }barney@{ub } and @{b }main@{ub } do not have handlers).
Now try this:
ENUM FRED=1, BARNEY
PROC main()
WriteF('Hello from main\\n')
fred()
WriteF('Goodbye from main\\n')
ENDPROC
PROC fred() HANDLE
WriteF(' Hello from fred\\n')
barney()
Raise(FRED)
WriteF(' Goodbye from fred\\n')
EXCEPT
WriteF(' Handler fred: \\d\\n', exception)
ENDPROC
PROC barney()
WriteF(' Hello from barney\\n')
Raise(BARNEY)
WriteF(' Goodbye from barney\\n')
ENDPROC
When you run this you get the following output:
Hello from main
Hello from fred
Hello from barney
Handler fred: 2
Goodbye from main
Now the @{b }fred@{ub } procedure calls @{b }barney@{ub }, so @{b }main@{ub } and @{b }fred@{ub } are recent
procedures when @{b }Raise(BARNEY)@{ub } is executed, and therefore the @{b }fred@{ub }
exception handler is called. When this handler finishes the call to @{b }fred@{ub }
in @{b }main@{ub } is finished, so the @{b }main@{ub } procedure is completed and we see the
`Goodbye' message. In the previous program the @{b }Raise(BARNEY)@{ub } call did not
get handled and the whole program terminated at that point.
@ENDNODE
@NODE "Automatic Exceptions" "Automatic Exceptions"
@Next "Raise within an Exception Handler"
@Prev "Raising an Exception"
@Toc "main"
Automatic Exceptions
====================
In the previous section we saw an example of raising an exception when
a call to @{b }New@{ub } returned @{b }NIL@{ub }. We can re-write this example to use
@{fg shine }automatic@{fg text } exception raising:
CONST BIG_AMOUNT = 100000
ENUM ERR_MEM=1
RAISE ERR_MEM IF New()=NIL
PROC main() HANDLE
DEF block
block:=New(BIG_AMOUNT)
WriteF('Got enough memory\\n')
EXCEPT
IF exception=ERR_MEM
WriteF('Not enough memory\\n')
ELSE
WriteF('Unknown exception\\n')
ENDIF
ENDPROC
The only difference is the removal of the @{b }IF@{ub } which checked the value of
@{b }block@{ub }, and the addition of a @{b }RAISE@{ub } part. This @{b }RAISE@{ub } part means that
whenever the @{b }New@{ub } function is called in the program, the exception @{b }ERR_MEM@{ub }
will be raised if it returns @{b }NIL@{ub } (i.e., the exception @{b }ERR_MEM@{ub } is
automatically raised). This unclutters the program by removing a lot of
error checking @{b }IF@{ub } statements.
The precise form of the @{b }RAISE@{ub } part is:
RAISE @{fg shine }exception@{fg text } IF @{fg shine }function@{fg text }() @{fg shine }compare@{fg text } @{fg shine }value@{fg text } ,
@{fg shine }exception2@{fg text } IF @{fg shine }function2@{fg text }() @{fg shine }compare2@{fg text } @{fg shine }value2@{fg text } ,
...
@{fg shine }exceptionN@{fg text } IF @{fg shine }functionN@{fg text }() @{fg shine }compareN@{fg text } @{fg shine }valueN@{fg text }
The @{fg shine }exception@{fg text } is a constant (or number) which represents the exception
to be raised, @{fg shine }function@{fg text } is the E built-in or system function to be
automatically checked, @{fg shine }value@{fg text } is the return value to be checked against,
and @{fg shine }compare@{fg text } is the method of checking (i.e., @{b }=@{ub }, @{b }<>@{ub }, @{b }<@{ub }, @{b }<=@{ub }, @{b }>@{ub } or @{b }>=@{ub }).
This mechanism only exists for built-in or library functions because they
would otherwise have no way of raising exceptions. The procedures you
define yourself can, of course, use @{b }Raise@{ub } to raise exceptions in a much
more flexible way.
@ENDNODE
@NODE "Raise within an Exception Handler" "Raise within an Exception Handler"
@Prev "Automatic Exceptions"
@Toc "main"
@{b }Raise@{ub } within an Exception Handler
=================================
If you call @{b }Raise@{ub } within an exception handler then control passes to
the next most recent handler. In this way you can write procedures which
have handlers that perform local tidying up. By using @{b }Raise@{ub } at the end of
the handler code you can invoke the next layer of tidying up.
As an example we'll use the Amiga system functions @{b }AllocMem@{ub } and @{b }FreeMem@{ub }
which are like the built-in function @{b }New@{ub } and @{b }Dispose@{ub }, but the memory
allocated by @{b }AllocMem@{ub } @{i }must@{ui } be deallocated (using @{b }FreeMem@{ub }) when it's
finished with, before the end of the program.
CONST SMALL=100, BIG=123456789
ENUM ERR_MEM=1
RAISE ERR_MEM IF AllocMem()=NIL
PROC main()
allocate()
ENDPROC
PROC allocate() HANDLE
DEF mem=NIL
mem:=AllocMem(SMALL, 0)
morealloc()
FreeMem(mem, SMALL)
EXCEPT
IF mem THEN FreeMem(mem, SMALL)
WriteF('Handler: deallocating "allocate" local memory\\n')
ENDPROC
PROC morealloc() HANDLE
DEF more=NIL, andmore=NIL
more:=AllocMem(SMALL, 0)
andmore:=AllocMem(BIG, 0)
WriteF('Allocated all the memory!\\n')
FreeMem(andmore, BIG)
FreeMem(more, SMALL)
EXCEPT
IF andmore THEN FreeMem(andmore, BIG)
IF more THEN FreeMem(more, SMALL)
WriteF('Handler: deallocating "morealloc" local memory\\n')
Raise(ERR_MEM)
ENDPROC
The calls to @{b }AllocMem@{ub } are automatically checked, and if @{b }NIL@{ub } is returned
the exception @{b }ERR_MEM@{ub } is raised. The handler in the @{b }allocate@{ub } procedure
checks to see if it needs to free the memory pointed to by @{b }mem@{ub }, and the
handler in the @{b }morealloc@{ub } checks @{b }andmore@{ub } and @{b }more@{ub }. At the end of the
@{b }morealloc@{ub } handler is the call @{b }Raise(ERR_MEM)@{ub }. This passes control to the
exception handler of the @{b }allocate@{ub } procedure, since @{b }allocate@{ub } called
@{b }morealloc@{ub }.
There's a couple of subtle points to notice about this example.
Firstly, the memory variables are all initialised to @{b }NIL@{ub }. This is because
the automatic exception raising on @{b }AllocMem@{ub } will result in the variables
not being assigned if the call returns @{b }NIL@{ub } (i.e., the exception is raised
before the assignment takes place), and the handler needs them to be @{b }NIL@{ub }
if @{b }AllocMem@{ub } fails. Of course, if @{b }AllocMem@{ub } does not return @{b }NIL@{ub } the
assignments work as normal.
Secondly, the @{b }IF@{ub } statements in the handlers check the memory pointer
variables do not contain @{b }NIL@{ub } by using their values as truth values. Since
@{b }NIL@{ub } is actually zero, a non-@{b }NIL@{ub } pointer will be non-zero, i.e., true in
the @{b }IF@{ub } check. This shorthand is often used, and so you should be aware of
it.
It is quite common that an exception handler will want to raise the
same exception after it has done its processing. The function @{b }ReThrow@{ub }
(which has no arguments) can be used for this purpose. It will re-raise
the exception, but only if the exception is not zero (since this special
value means that no error occurred). If the exception is zero then this
function has no effect. In fact, the following code fragments (within a
handler) are equivalent:
ReThrow()
IF exception THEN Throw(exception, exceptioninfo)
There are two examples, in Part Three, of how to use an exception
handler to make a program more readable: one deals with using data files
(see @{"String Handling and I-O" Link "Examples.guide/String Handling and I-O" }) and the other deals with opening screens and
windows (see @{"Screens" Link "Examples.guide/Screens" }).
@ENDNODE