NOTE: this will generally pertain to C# but applies to any programming language which uses similar exception handling (for example Java). This article covers a few misconceptions about exceptions and is not intended as a complete reference. In depth study of this important programming aspect is highly recommended.
Overview
The exceptions a method throws are as
important as its parameters or return value. They are part of your
contract with the user, even if you are not required to declare them
as part of your method definitions (as you are in Java). Too often
methods are completely written before any thought is given to the exceptions it should throw. This unfortunate behavior leads to a less than ideal interface for the user.
Many developers believe, and in many
cases are taught, exceptions exist to handle rare or infrequent
conditions encountered during execution. The truth is as developers
we have no control over the circumstances our code is used in beyond
the compiler enforcing the existence of our parameters. As such we
can never say what the frequency of any use case may be. Thus we can
never say if any given case is rare or common.
Usage
The correct use of exceptions is to
convey our inability to keep our contract with the user as defined by
our method signature. Let us consider the following method signature
from the Int32 type:
public static int Parse(string s)
This method takes a string
representation of an integer and returns its integer representation.
Viewing the documentation reveals it throws the following exceptions:
ArgumentNullException – thrown when s
is null.
FormatException – thrown when s
cannot be converted to an int.
OverflowException – thrown when the
representation of s is beyond the capacity of an Int32 to represent.
Each of these exceptions represents a
condition which prevents the method from honoring its contract,
returning an integer representation of the string parameter, to the user. There is a different exception for
each semantically meaningful mode of failure. That is to say each
exception represents a different reason the conversion failed giving
the caller the ability to make a meaningful decision about how to
proceed. The OverflowException can represent failure due to either a
value above or below what can be represented by an Int32. As the user
would not take different actions based on whether the
value is too big or too small we have just one exception to represent
both instead of an OverflowHighException and an OverflowLowException.
Example
Using these ideas let us design our own
method. Let us suppose we have a type which represents a temperature
probe. We might start by writing a class method with the signature:
public float GetTemperature()
[Note: normally we would use a property here but for demonstration purposes we will make this a method. Also, interestingly enough, the CLR doesn't know about properties so C# converts them into methods during compilation]
What exceptions should this method
throw? Anything which violates our contract with the user should be
represented by an exception. In this case any condition which would
prevent us from returning a temperature will need an exception. We
could start with the following:
TemperatureProbeNotAvailableException – thrown if our connection to the probe is lost.
TemperatureOffScaleException – thrown if the
probe provides a value beyond what its specification states it can
accurately measure.
Previously when considering the Parse
method we said there was no need to have multiple exceptions to
represent the high and low possibilities for the OverflowException.
Here we also have a single exception to represent an out of range
condition. Considering further is there any action our users might
wish to take based on whether the temperature probe is off scale high
vs low? In this case there is.
If the probe were attached to a heating or cooling system knowing which direction the temperature
is off scale would be very meaningful information. For a more awesome
example let us say this was a very high temperature probe in the
exhaust of a gas turbine engine. Many such probes do not operate
correctly until they reach a minimum temperature well above normal
ambient. An off scale low condition might be normal while the engine
was offline or idle but an off scale high might indicate the engine was running
beyond specification and should be throttled down.
Since the direction of the off scale condition is
potentially meaningful to the user we should provide exceptions for
these conditions.
public class TemperatureOffScaleHighException
: TemperatureOffScaleException
public class TemperatureOffScaleLowException :
TemperatureOffScaleException
Though the off scale direction of the
temperature probe could be useful to
the user this might not always be the case. By extending the high
and low exceptions from the TemperatureOffScaleException we give the
user a choice If they do not care about the direction of the off
scale condition they can choose to catch the base
TemperatureOffScaleException. Otherwise if they have code to handle
the high and low conditions differently they may catch these derived
exceptions in separate catch blocks and deal with them as they see
fit.
public static bool TryParse(string s, out int result)
This method does not throw a FormatException. Instead if will return a boolean indicating if the conversion was successful and if so it will provide the converted value via the result out parameter. By using a boolean instead of an exception this call avoids the cost associated with handling an exception.
Does this method keep its contract with its user? Yes it does.
Whereas the Parse method implies by its signature it will return a converted value TryParse implies it will attempt a conversion. This distinction in naming is important in creating an API your users will be able to easily understand.
A simple look through the FCL will demonstrate the occurrence of TryX is very limited. While the TryX methods seem to match the X method in capacity and excel it in speed it fails in code simplicity. The code necessary to support the TryX method is more cumbersome than the a simple X method.
Aren't Exceptions Time Expensive?
Depending on the environment the throwing of exceptions can be time expensive, especially if they repeatedly do so in tight loops. The previously mentioned Int32 type addresses this with the TryParse method.
This method does not throw a FormatException. Instead if will return a boolean indicating if the conversion was successful and if so it will provide the converted value via the result out parameter. By using a boolean instead of an exception this call avoids the cost associated with handling an exception.
Does this method keep its contract with its user? Yes it does.
Whereas the Parse method implies by its signature it will return a converted value TryParse implies it will attempt a conversion. This distinction in naming is important in creating an API your users will be able to easily understand.
A simple look through the FCL will demonstrate the occurrence of TryX is very limited. While the TryX methods seem to match the X method in capacity and excel it in speed it fails in code simplicity. The code necessary to support the TryX method is more cumbersome than the a simple X method.
Conclusion
There are various opinions about the role of exceptions and how they are used. What I would like to convey here is exceptions are an important yet overlooked aspect of development in environments which support them. A more in depth study than what is presented here is recommended though simply considering them at the outset of development instead of as a postscript will help in API design.
No comments:
Post a Comment