Exceptions Constructors and Overloading
From Real Software Documentation
We take a bit of a breather from complex algorithms. We’re going to look at one major subject (Exceptions), and along the way, we’ll pick up a couple of simple extras (Constructors and Overloading).
Handling errors and unusual situations with the control mechanisms we have seen (loops, if-thens, and the like) can make otherwise simple code much more complex, because you have to litter your code with tests for every possible error or unusual condition. This can make such code much harder to develop and maintain.
Exceptions provide an elegant answer to this problem, by providing a controlled way of jumping out of the regular flow of control.
Here is an example. Assume this is the normal flow of control in some part of a program (Method A calls Method B calls… Method D returns to Method C…):
Now, what do we do when…
This problem might be an error in your code, or it might be something out of your control (perhaps we were about to open a file, but it has been deleted since we checked that it existed). Let’s just concoct an example. Here, an error occurs when we try to access an element of an index that doesn’t exist. In Real Studio, this raises an exception:
When an exception is raised, Real Studio follows a very different kind of flow of control to its regular line-by-line execution. It looks for the nearest matching Exception Handler. This can be at the end of the current method…
or in the first method containing such a handler, starting from the bottom of the current call chain, and looking up:
Exceptions are raised automatically when something goes wrong:
…or you can raise an exception yourself:
You can even invent your own exceptions, which are just subclasses of the class RuntimeException. You can add your own methods and properties to them, just as you can with any class, and then raise them when you want to deal with an unusual circumstance:
(The argument to the New command here is part of a constructor, which we’ll learn about in a moment).
It’s important to realize that Exceptions are objects, having a class, with all that entails:
- They can be part of a class hierarchy
- Handlers can catch exception superclasses or subclasses;
- You can add methods and properties to them;
- They can carry error messages or other information; and
- They can carry out an action.
An exception handler can either catch all exceptions:
Or it can receive the exception object itself as an argument:
And it can also catch the exception only if it is of a particular type:
You can have multiple exception handlers at the end of a method. This example provides specific handlers for two particular exceptions, then a general handler that will catch any other type of exception:
When there are multiple handlers like this, the handler matching the exception that is closest to the top of the code will be called.
The above exception handlers occur at the end of the methods in which they occur. You can also build a smaller exception handling block in just a part of a method, using the Try – Catch – Finally – End Try construct, which looks like this:
This is the equivalent of the first example above, because it catches all exceptions that occur between Try and Catch. You can also get the exception itself in a variable:
And you can catch only exceptions of a particular type:
Another option you have with this kind of block is to provide code that must be executed after the Try block, whether or not an exception is raised. This uses a Finally section:
You should always think about whether there is anything that the Try-Catch block must make sure is true after it has finished (for example, it must close any files it opened), and put that into a Finally block.
Note: that unlike with the Exception block, you can have nested Try-Catch blocks (meaning, having Try-Catch blocks inside Try-Catch blocks).
Default Exception Handling
If an exception is raised but no matching handler is encountered, Real Studio provides a default handling:
- In the IDE, Real Studio will highlight the line at which the exception was raised, and will display a message about the type of exception that was raised (you’ve doubtless seen this many times already):
- If you compile your program and run it separately from the development environment (which is usually your goal), when an exception is raised, Real Studio displays an error message and quits:
You want to avoid having Real Studio display a message like this if you possibly can, so learning to handle Exceptions yourself is a very important. Real Studio provides a special extra way to handle exceptions: an exception that is not handled anywhere else will trigger an UnhandledException event in App. You should always provide such a handler in an application you are providing to someone else, so that you can provide a more elegant way of dealing with unhandled exceptions than Real Studio’s rather scary automatic dialog.
That’s an overview of Exceptions; now we’ll examine them in action.
- Open the included project, Exceptional Eliza.
We’re going to make the Eliza file parsing routine handle various errors in the file. By using Exceptions, we will be able to leave the “regular” parsing routines almost entirely intact, while adding some very different behavior in these exceptional circumstances.
Parsing is pulling apart a string to get structured information out of it. Humans do it when we comprehend an English sentence, and computers do it with files.
- Examine the File Parse Error class.
Notice that it is intended to be an abstract superclass (it doesn’t do anything itself; when you call its Announce method, this just gets handed on to the subclass as an event).
- Examine the NaughtyWordError class. Particularly make note of the NaughtyWordError method.
This is a special method called a constructor.
Constructors and Destructors
You write a constructor for a custom class by creating a new method for the class and naming it “Constructor”. The drop-down list for the Method name field suggests this name and the names of all other methods that can be overridden. Any such method will run automatically when an object of that type is created.
Note that a constructor can have arguments, which you provide as arguments to the New command (as we did above).
You can also have a destructor, which runs when an object is no longer referenced by any variable (and that won’t continue to exist for any other reason — a window is an example of something that will continue to exist even if nothing is referring to it). Such an object can never be used again (there is no way for your code to refer to it), so Real Studio reclaims the space it occupies. Before it does, it calls the object’s destructor if one exists, so you can use that to “clean up after” your object, if necessary.
A destructor is a method with no parameters and no return type, named Destructor. Notice that technically speaking, constructors and destructors are not necessary. You can write any kind of program without writing them. But it is a good idea to use a constructor in any class where it is appropriate, because it helps to avoid many errors.
- Examine the InvalidLineError class.
Particularly make note of the two constructors. Having two or more methods or functions with the same name is called overloading the method.
We can have more than one method or function with the same name, if at least one of their arguments is of a different type, or if they have a different number of arguments. Note that Real Studio will only be able to choose one version of the method for any particular call, since the arguments will only match one version.
Overloading is particularly common for constructors, but you should feel free to use overloading wherever that seems reasonable (we will see a number of examples of overloading in this and coming lessons). Note that other than in the case of constructors, overloading is merely a convenience. It would make no huge difference to have methods with different names. But you should use overloading whenever you have different ways of requesting what amounts to the same operation.
Limits on Overloading
The current Real Studio compiler has two limits on overloading you should be aware of:
- A class and its subclass are not considered different types when distinguishing between versions of an overloaded method. So if Thing is a superclass to SubThing, the following method definitions will not be distinguished:
- Real Studio searches for a match for a method call up through the class hierarchy; if a method is defined in a class, it will not see a different version of that method defined in a superclass.
The solution is to override the superclass’s method in the subclass and to then direct a call back to the superclass’s method explicitly. We will cover how to call a method in a superclass in a coming lesson.
Take a quick look at StringList. This is all stuff we’ve seen before.
Examine the module BadThings. A feature of modules we’ve not seen before is that they can have constants, which are like variables whose values can’t change.
It is a good idea to use a named constant like this for any fixed value your program relies on, because it gives you a single place to change that value. In this case, it is storing a list of naughty words you can’t have in the responses file.
Now examine the changes to the StringResponder class.
Note that the code to parse a line has been moved to a separate method, ReadElizaLine. Double-click on the name of the method in the left pane of the code editor, and notice that the method is declared to be Protected.
Some methods will form part of the public interface of a class. These are the operations that define the Abstract Data Type that the class implements (remember: a type is defined by what it does), and are meant to be called from other classes. Other methods in a class will be part of the class’s internal operations, and are not meant to be called from outside of the class. ReadElizaLine is a good example of such a method.
Making the method Private indicates that a method is not part of the public interface for a class. When a method is Private, the compiler makes it impossible to call the method from outside the class. Also, subclasses of the class do not inherit the method.
Another alternative is the Protected method. Like Private methods, Protected methods cannot be accessed from outside the class, but subclasses of the current class inherit Protected methods.
Back to Exceptions
Note how the Raise commands generate the object right in the command, and how the arguments are passed to the constructors for the class. Note that New is now obviously behaving as a function (which it was all along…)
Observe the locations of the Raise commands and the method handlers (use the Search area in the main toolbar to find these calls). Note that we have a handler for both the base class FileParseError (in LoadFromFile) and the subclass Naughty-WordError in ReadElizaLine.
Open the Responses.txt file, and see if you can find the two places where the exceptions will be raised.
- Run the project, put breakpoints everywhere an exception is raised, and step through what happens in each case.
Notice that the exception for the NaughtyWordError handler (in the ReadElizaLine method) causes the project to skip the line, but to continue parsing the file. On the other hand, the breakpoint for the FileParseError (in LoadFromFile) aborts the parsing process. Try commenting out the Quit command in the FileParseError handler to make this clearer.
- Comment out the NaughtyWordError handler, and observe the behavior when the project is run.
- Leaving the NaughtyWordError handler commented out, move the FileParseError handler into the ReadElizaLine method, but remove its Quit command.
Now see what happens when the errors occur.
If you comment out the Quit commands from all of the exception handlers and run the program, it will display a couple of alerts and then run. But try entering a phrase that doesn’t merit one of the canned responses (so it should get a random response; a good example would be “Hello”). The program will quit with an OutOfBoundsException. You can fix this by fixing the Responses.txt file, but you should also make the program handle this better. Add a suitable OutofBoundsException handler.
Another interesting exercise would be to have the Exceptions record themselves (say in an ExceptionLog class), and then report themselves at the end of the parsing process, before the project can continue. In case it isn’t obvious, the best place to record the generation of an object will be in its constructor…