Errors happen all the time; they’re a fact of life:
Despite the best efforts of Microsoft Word, an army of highly skilled reviewers and editors, and even your authors, it would be surprising if there wasn’t a typographical error in a book of this length.
Although they are relatively few and far between, there are bugs in the .NET Framework—hence the need for occasional service packs.
You might type your credit card number for an online transaction and accidentally transpose two digits; or forget to type in the expiration date.
Like it or not, we’re going to have to face up to the fact that there are going to be errors of all kinds to deal with in our software too. In this chapter, we’ll look at various types of errors, the tools that C# and the .NET Framework give us to deal with them, and some strategies for applying those tools.
First, we need to recognize that all errors are not made the same. We’ve classified a few of the more common ones in Table 6-1.
Table 6-1. A far-from-exhaustive list of some common errors
Although bugs are probably the most obvious type of error, we won’t actually be dealing with them directly in this chapter. We will, however, look at how our error-handling techniques can make it easier (or harder!) to find the bugs that are often the cause of the other, better defined issues.
Let’s get started with an example we can use to look at error-handling techniques. We’re going to branch out into the world of robotics for this one, and build a turtle-controlling application. The real-world turtle is a rectangular piece of board on which are mounted two motors that can drive two wheels. The wheels are located in the middle of the left and right edges of the board, and there are nondriven castor wheels at the front and back to give it a bit of stability. We can drive the two motors independently: we can move forward, move backward, or stop. And by moving the wheels in different directions, or moving one wheel at time, we can steer it about a bit like a tank.
Let’s create a class to model our turtle (see Example 6-1).
Example 6-1. The Turtle class
class Turtle { // The width of the platform public double PlatformWidth { get; set; } // The height of the platform public double PlatformHeight { get; set; } // The speed at which the motors drive the wheels, // in meters per second. For ease, we assume that takes account // of the distance traveled by the tires in contact // with the ground, and any slipping public double MotorSpeed { get; set; } // The state of the left motor public MotorState LeftMotorState { get; set; } // The state of the right motor public MotorState RightMotorState { get; set; } // The current position of the turtle public Point CurrentPosition { get; private set; } // The current orientation of the turtle public double CurrentOrientation { get; private set; } } // The current state of a motor enum MotorState { Stopped, Running, Reversed }
In addition to the motor control, we can define the size of the platform and the speed at which the motors rotate the wheels. We also have a couple of properties that tell us where the turtle is right now, relative to its point of origin, and the direction in which it is currently pointing.
To make our turtle simulator actually do something, we can add a method which makes time pass. This looks at the state of the different motors and applies an appropriate algorithm to calculate the new position of the turtle. Example 6-2 shows our first, somewhat naive, go at it.
Example 6-2. Simulating turtle motion
// Run the turtle for the specified duration public void RunFor(double duration) { if (LeftMotorState == MotorState.Stopped && RightMotorState == MotorState.Stopped) { // If we are at a full stop, nothing will happen return; } // The motors are both running in the same direction // then we just drive if ((LeftMotorState == MotorState.Running && RightMotorState == MotorState.Running) || (LeftMotorState == MotorState.Reversed && RightMotorState == MotorState.Reversed)) { Drive(duration); return; } // The motors are running in opposite directions, // so we don't move, we just rotate about the // center of the rig if ((LeftMotorState == MotorState.Running && RightMotorState == MotorState.Reversed) || (LeftMotorState == MotorState.Reversed && RightMotorState == MotorState.Running)) { Rotate(duration); return; } }
If both wheels are pointing in the same direction (forward or reverse), we drive (or reverse) in the direction we are pointing. If they are driving in opposite directions, we rotate about our center. If both are stopped, we will remain stationary.
Example 6-3 shows the
implementations of Drive
and Rotate
. They use a little bit of trigonometry to
get the job done.
Example 6-3. Simulating rotation and movement
private void Rotate(double duration) { // This is the total circumference of turning circle double circum = Math.PI * PlatformWidth; // This is the total distance traveled double d = duration * MotorSpeed; if (LeftMotorState == MotorState.Reversed) { // And we're going backwards if the motors are reversed d *= -1.0; } // So we've driven it this proportion of the way round double proportionOfWholeCircle = d / circum; // Once round is 360 degrees (or 2pi radians), so we have traveled // this far: CurrentOrientation = CurrentOrientation + (Math.PI * 2.0 * proportionOfWholeCircle); } private void Drive(double duration) { // This is the total distance traveled double d = duration * MotorSpeed; if (LeftMotorState == MotorState.Reversed) { // And we're going backwards if the motors are reversed d *= -1.0; } // Bit of trigonometry for the change in the x,y coordinates double deltaX = d * Math.Sin(CurrentOrientation); double deltaY = d * Math.Cos(CurrentOrientation); // And update the position CurrentPosition = new Point(CurrentPosition.X + deltaX, CurrentPosition.Y + deltaY); }
Let’s write a quick test program to see whether the code we’ve written actually does what we expect (see Example 6-4).
Example 6-4. Testing the turtle
static void Main(string[] args) { // Here's our turtle Turtle arthurTheTurtle = new Turtle {PlatformWidth = 10.0, PlatformHeight = 10.0, MotorSpeed = 5.0}; ShowPosition(arthurTheTurtle); // We want to proceed forwards arthurTheTurtle.LeftMotorState = MotorState.Running; arthurTheTurtle.RightMotorState = MotorState.Running; // For two seconds arthurTheTurtle.RunFor(2.0); ShowPosition(arthurTheTurtle); // Now, let's rotate clockwise for a bit arthurTheTurtle.RightMotorState = MotorState.Reversed; // PI / 2 seconds should do the trick arthurTheTurtle.RunFor(Math.PI / 2.0); ShowPosition(arthurTheTurtle); // And let's go into reverse arthurTheTurtle.RightMotorState = MotorState.Reversed; arthurTheTurtle.LeftMotorState = MotorState.Reversed; // And run for 5 seconds arthurTheTurtle.RunFor(5); ShowPosition(arthurTheTurtle); // Then rotate back the other way arthurTheTurtle.RightMotorState = MotorState.Running; // And run for PI/4 seconds to give us 45 degrees arthurTheTurtle.RunFor(Math.PI / 4.0); ShowPosition(arthurTheTurtle); // And finally drive backwards for a bit arthurTheTurtle.RightMotorState = MotorState.Reversed; arthurTheTurtle.LeftMotorState = MotorState.Reversed; arthurTheTurtle.RunFor(Math.Cos(Math.PI / 4.0)); ShowPosition(arthurTheTurtle); Console.ReadKey(); } private static void ShowPosition(Turtle arthurTheTurtle) { Console.WriteLine( "Arthur is at ({0}) and is pointing at angle {1:0.00} radians.", arthurTheTurtle.CurrentPosition, arthurTheTurtle.CurrentOrientation); }
We chose the times for which to run quite carefully so that we end up going through relatively readable distances and angles. (Hey, someone could design a more usable facade over this API!) If we compile and run, we see the following output:
Arthur is at (0,0) and is pointing at angle 0.00 radians. Arthur is at (0,10) and is pointing at angle 0.00 radians. Arthur is at (0,10) and is pointing at angle 1.57 radians. Arthur is at (-25,10) and is pointing at angle 1.57 radians. Arthur is at (-25,10) and is pointing at angle 0.79 radians. Arthur is at (-27.5,7.5) and is pointing at angle 0.79 radians.
OK, that seems fine for basic operation. But what happens if we change the width of the platform to zero?
Turtle arthurTheTurtle =
new Turtle { PlatformWidth = 0.0
, PlatformHeight = 10.0, MotorSpeed = 5.0 };
Not only does that not make much sense, but the output is not very useful either; clearly we have divide-by-zero problems:
Arthur is at (0,0) and is pointing at angle 0.00 radians. Arthur is at (0,10) and is pointing at angle 0.00 radians. Arthur is at (0,10) and is pointing at angleInfinity
radians. Arthur is at (NaN
,NaN
) and is pointing at angleInfinity
radians. Arthur is at (NaN
,NaN
) and is pointing at angleNaN
radians. Arthur is at (NaN
,NaN
) and is pointing at angleNaN
radians.
Clearly, our real-world turtle could go badly wrong if we told it to
rotate through an infinite angle. At the very least, we’d get bored waiting
for it to finish. We should prevent the user from running it if the PlatformWidth
is less than or equal to zero.
Previously, we used the following code:
// Run the turtle for the specified duration public void RunFor(double duration) { if (PlatformWidth <= 0.0) { // What to do here? } // ... }
That detects the problem, but what should we do if our particular turtle is not set up correctly? Previously, we silently ignored the problem, and returned as though everything was just fine. Is that really what we want to do?
For this application it might be perfectly safe, but what if another
developer uses our turtle with a paintbrush strapped to its back, to paint
the lines on a tennis court? The developer added a few extra moves at the
beginning of his sequence, and he didn’t notice that he had inadvertently
done so before he initialized the PlatformWidth
. We could have a squiggly paint
disaster on our hands!
Choosing when and how to fail is one of the big debates in software development. There is a lot of consensus about what we do, but things are much less clear-cut when it comes to failures.
You have a number of choices:
Try to plow on regardless.
Try to make sense of what has happened and work around it.
Return an error of some kind to your caller, and hope the caller knows what to do with it.
Stop.
At the moment, we’re using option 1: try to plow on regardless; and you can see that this might or might not be dangerous. The difficulty is that we can be sure it is safe only if we know why our client is calling us. Given that we can’t possibly have knowledge of the continuum of all possible clients (and their clients, and their clients’ clients), plugging on regardless is, in general, not safe. We might be exposing ourselves to all sorts of security problems and data integrity issues of which we cannot be aware at this time.
What about option 2? Well, that is really an extension of the contract: we’re saying that particular types of data outside the range we previously defined are valid, it is just that we’ll special-case them to other values. This is quite common with range properties, where we clamp values outside the range to the minimum and maximum permitted values. Example 6-5 shows how we could implement that.
Example 6-5. Range checking
class Turtle { // The width of the platform must be between 1.0 and 10.0 inclusive // Values outside this range will be coerced into the range. private double platformWidth; public double PlatformWidth { get { return platformWidth; } set { platformWidth = value; EnsurePlatformSize(); } } // The height of the platform must be between 1.0 and 10.0 inclusive // Values outside this range will be coerced into the range. private double platformHeight; public double PlatformHeight { get { return platformHeight; } set { platformHeight = value; EnsurePlatformSize(); } } // The new constructor initializes the platform size appropriately public Turtle() { EnsurePlatformSize(); } // This method enforces the newly documented constraint // we added to the contract private void EnsurePlatformSize() { if (PlatformWidth < 1.0) { PlatformWidth = 1.0; } if (PlatformWidth > 10.0) { PlatformWidth = 10.0; } if (PlatformHeight < 1.0) { PlatformHeight = 1.0; } if (PlatformHeight > 10.0) { PlatformHeight = 10.0; } } // ... }
Here we documented a constraint in our contract, and enforced that constraint first at construction, and then whenever clients attempt to modify the value.
We chose to enforce that constraint at the point when the value can
be changed because that makes the effect of the constraint directly
visible. If users set an out-of-bounds value and read it back they can
immediately see the effect of the constraint on the property. That’s not
the only choice, of course. We could have done it just before we used
it—but if we changed the implementation, or added features, we might have
to add lots of calls to EnsurePlatformSize
, and you can be certain that
we’d forget one somewhere.
When we run the application again, we see the following output:
Arthur is at (0,0) and is pointing at angle 0.00 radians. Arthur is at (0,10) and is pointing at angle 0.00 radians. Arthur is at (0,10) and is pointing at angle 15.71 radians. Arthur is at (-1.53075794227797E-14,35) and is pointing at angle 15.71 radians. Arthur is at (-1.53075794227797E-14,35) and is pointing at angle 7.85 radians. Arthur is at (-3.53553390593275,35) and is pointing at angle 7.85 radians.
Although this is a very useful technique, and it has clearly
banished those less-than-useful NaN
s,
we have to consider: is this the right solution for this particular
problem? Let’s think about our tennis-court-painting robot again. Would we
really want it to paint the court as though it were a 1-meter-wide robot,
just because we forgot to initialize it? Looking at the distances traveled
and the angles through which it has turned, the answer is clearly
no!
Constraints such as this are useful in lots of cases. We might want to ensure that some UI element not extend off the screen, or grow too big or small, for example. But equally, an online banking application that doesn’t permit transactions less than $10 shouldn’t just clamp the amount the user entered from $1 to $10 and carry on happily!
So let’s backtrack a little and look at another option: returning a value that signifies an error.