by Andrew Shapira
View Source
Introduction
Two ubiquitous coding strategies are to use assertions and to generate debugging
output. The .NET System.Diagnostics.Debug and System.Diagnostics.trace classes are designed to help
programmers do these things. Unfortunately, the organization of these classes makes
using them inappropriate in many cases. This article describes the reasoning behind
this conclusion and gives a solution that does not have the problems of the Debug and trace classes.
C# code is included. The solution involves four entities: a class that is used for
assertions and only assertions, a preprocessor token that controls whether the class
executes assertions, and a similar class and preprocessor token for development
output. We also give a general introduction to using assertions.
Outline
An outline of this article is as follows. The next two sections respectively introduce
assertions and development output. These introductions are followed by a discussion
of some goals that should be met by classes that assist with assertions and development
output. The next section discusses the .NET Debug
and trace classes and shows how these classes do
not meet the goals. The remainder of the article presents two standalone classes
that can be used to meet the goals.
Introduction to Assertions
Assertions are a traditional software engineering tool that can be used with almost
all programming languages. In his excellent book, “Large-Scale C++ Software Design”
[2], John Lakos discusses using assertions in C and C++ (the text has been adapted
slightly for this article):
The Standard C library provides a macro called assert
(see assert.h) for guaranteeing that a given expression
evaluates to a non-zero (true) value; otherwise an error message is printed and
program execution is terminated. Assertions are convenient to use and are a powerful
implementation-level documentation tool for developers. Assert statements are like
active comments -- they not only make assumptions clear and precise, but if these
assumptions are violated, they actually do something about it.
The use of assert statements can be an effective way to catch program logic errors
at runtime, and yet they are easily filtered out of production code. Once development
is complete, the runtime cost of these redundant tests for coding errors can be
eliminated simply by defining the preprocessor symbol NDEBUG
during compilation. Be sure, however, to remember that code placed in the assert
itself will be omitted in the production version.
An assertion is best used to test a condition only when all of the following hold:
- the condition should never be false if the code is correct,
- the condition is not so trivial so as to obviously be always true, and
- the condition is in some sense internal to a body of software.
Assertions should almost never be used to detect situations that arise during software's
normal operation. For example, usually assertions should not be used to check for
errors in a user's input. It may, however, make sense to use assertions to verify
that a caller has already checked a user's input.
An “assertion failure” is said to occur when an assertion detects that its condition
is false and takes appropriate action, such as throwing an exception. Since this
is exactly what an assertion is supposed to do, the term “assertion failure” is
something of a misnomer. Nevertheless, the term is standard, and is useful because
it provides a name for an important situation.
Let's look at an example of using assertions. This example deals with a small C#
program; the true value of assertions may not become apparent until one has used
assertions with larger programs. With this in mind, then, consider the following
small example. (In this example we assume that the Assert.Test
method is defined elsewhere and performs a function similar to that of C's assert macro.)
class Shift
{
/*
This method returns a circular
left shift of x's right 3 bits.
If x is not between 0 and 7 inclusive,
undefined behavior
may result, and this undefined
behavior may change over
time and may depend on what algorithm
this method uses.
*/
static int shift3(int x) {
Assert.Test((x >= 0) &&
(x <= 7));
return ((x >> 2) & 1) | ((x << 1) & 6);
}
static void Main() {
while (true) {
string s = Console.ReadLine();
if ((s ==
null) || (s.Length == 0))
// no more input
{ break ; }
char c = s[0];
#if inappropriate
Assert.Test((c
>= '0') && (c <= '7'));
int x = c;
Console.WriteLine("The
result is {0}", shift3(x));
#else
if (((c >= '0') && (c <= '7'))) {
int x = c;
Console.WriteLine("The
result is {0}", shift3(x));
} else {
Console.WriteLine(
"Please enter a number between 0 and 7.");
}
#endif
}
}
}
The assertion in Main is inappropriate, because
users may enter numbers outside of the range 0 to 7, and the program's normal function
includes detecting such entries and responding appropriately. The code inside the
#else region checks the input appropriately. This
example illustrates a general test that weeds out some inappropriate assertions:
ask whether the program would function correctly with a given assertion removed,
and if the answer is “no,” then the assertion is probably inappropriate.
Next, let's consider the assertion in the shift3
method. This assertion is appropriate because shift3
explicitly assumes that its argument x is between
0 and 7. The documentation before shift3 implies
that if the program is correct, then the calling method will ensure that the argument
to shift3 is between 0 and 7, as does the code in
Main's #else region.
Were it shift3's responsibility to check its argument
and return an error code for invalid values, then the assertion in
shift3 would not be appropriate. Notice that the appropriateness of this
assertion depends not only on the code but also on the documentation, i.e., on policies
regarding the responsibilities of code.
The Shift class contains a serious bug. When we
run the program, we find that the assertion in shift3
throws an exception. How can we find out what's going on? Well, the assertion did
its job by throwing an exception, so we know immediately that the contract between
shift3 and its caller has been broken. We can proceed
by determining why the contract was broken, i.e., why shift3
received an improper value of x. The problem is
that x in Main should
be assigned the value c - '0', not
c. After changing the assignment and recompiling we find that the program
works.
The assertion in this example performs two valuable functions. First, it concisely
summarizes the contract that shift3 has with its
callers. The assertion makes it easy for a reader to quickly understand details
of this contract. Second, if the contract is broken, the breaking of the contract
is detected immediately. It is almost always easier to figure out what is wrong
when a problem is exposed immediately, before program execution has reached a later
point that may be only tenuously related to the source of the problem. This service
that assertions can provide -- immediate detection of errors -- is called “feedback
at the point of failure.”
More information about assertions can be found in web search engines and in software
engineering textbooks.
Introduction to Development Output
We define development output to simply be program output that is intended
to be generated only during the development phase of software production. Such output
is often used for determining what is going on in a program, especially during debugging.
For example, while debugging the code in the previous section, we might want to
print some output. We could do this by adding Console.WriteLine
calls, as follows. (The code differs slightly from that in the previous section.)
static void Main() {
while (true) {
string
s = Console.ReadLine();
if
((s == null) || (s.Length == 0))
{ break ; }
int x = s[0];
Console.WriteLine("s={0}", s);
Console.WriteLine("x={0}", x);
if
(((x >= 0) && (x <= 7))) {
Console.WriteLine("The
result is {0}", shift3(x));
} else
{
Console.WriteLine(
"Please enter a number between 0 and 7.");
}
}
}
After running the program and seeing the development output, we may realize that
the value of x is not being computed correctly from
the string s. This may help us understand that we
need to subtract the character constant '0' from
s[0] when computing x.
After fixing the bug and inspecting the development output in the fixed version
of the program, we would likely remove the Console.WriteLine
statements that produce development output.
Using the Console.WriteLine method like this to
produce development output is not too bad. In fact, in a small program, sometimes
this is the best way. This technique does have some drawbacks, though, and these
drawbacks become important in large projects. First, it can be difficult to distinguish
between temporary and permanent Console.WriteLine
calls. Second, temporary calls like the ones in the example can come to reside in
a program for a long time, possibly permanently, and we do not want these calls
to produce output in released software. What we would like is a way for this output
to appear during the development process, but not with released versions of software.
Goals for Providers of Assertion and Development Output Services
Let's look at the capabilities that we do and do not want from software that provides
assertion and development output utility services. In particular we will look at
what types of software builds should have assertions execute, and what types of
builds shouldn't. We will also look at the same topic for development output.
Usually, we want assertions on (executing) during development. It's also useful
to be able to turn assertions off during development, e.g., when code is temporarily
structured in a way that causes assertions to fail.
In release builds, we usually want assertions off. But for some release builds it
makes sense to have assertions on, especially when developers have a close relationship
to the environment in which the released product is being used, or when developers
run with assertions on all the time, as many developers do.
We also want to be able to turn development output on and off during development
builds. Executables built in release builds should not produce development output.
Our goals, then, are as follows: we want a choice about having assertions on or
off for development builds, a choice about having assertions on or off for release
builds, and a choice about having development output on or off for development builds.
We want development output to be always be off for release builds.
Deficiencies of the .NET Debug and
trace Classes
Two obvious candidates for achieving our goals are the Debug
and trace classes in .NET's
System.Diagnostics namespace. Let's examine these classes to see if they
help us meet our goals.
First, let's look at using the Debug class. We will
call using the Debug class “Policy 1”; variants
are called “Policy 1A”, “Policy 1B”, etc.
Policy 1A. Use Debug for both assertions
and development output.
Policy 1A fails to meet our goals because it requires that if assertions are on
in a given release build, then development output will also be on in the same build.
This is because, as controlled by the DEBUG preprocessor
token, either all the members of the Debug class
are on, or none are.
Policy 1B. Use Debug for assertions, and
mandate not using the members of Debug that involve
development output.
One flaw with Policy 1B is that if we want assertions on in a release build, we
have to define DEBUG for the release build, which
is confusing at best. Also, this policy is difficult to maintain. Someone may simply
forget to avoid using development output aspects of Debug.
Or, when someone new to a project encounters code that uses
Debug.Assert, she may start using the non-assert members of
Debug because she may be used to this from other projects, or because
when she sees Debug.Assert in the code, it may seem
natural to use other parts of Debug.
Policy 1C. Use Debug for development output,
and do not use the members of Debug that involve
assertions.
Policy 1C's problems are essentially the same as Policy 1B's.
If one gives sufficient weight to the flaws described above, as we do, then one
can conclude that the Debug class should be used
for neither assertions nor development output.
Now let's look at using .NET's trace class.
Using the trace class has exactly the same problems
as using the Debug class.
There is another problem with using the trace class.
Grimes[1] has observed that “Visual Studio.NET defines trACE
[the preprocessor token] for C# projects created with the project wizards.” This
creates an expectation among Visual Studio.NET users that trACE
will be defined for release builds. Respecting this expectation would mean that
- if trACE controls development output, then release
builds would contain development output, and
- if trACE controls assertions, then we could not
turn assertions off for release builds.
Both of these consequences violate the design goals in the previous section.
As with the Debug class, we conclude that the trace class should be used for neither assertions
nor for development output.
The problems we have discussed with the .NET base class library's
Debug and trace classes stem from the
dependencies they introduce between assertions and development output. It is probably
better to separate assert functionality from development output functionality. For
example, the Debug and trace
classes might better have been designed to have no assert functionality, and assert
functionality could have been implemented in a separate class that is used for only
assertions.
Our Solution
Since .NET's base class library does not provide a separate class that is used only
for assertions, we developed our own system for assertions and development output.
This system is very simple to understand and use. It comprises four entities:
- the Assert class,
- the Nib class,
- the ASSERT preprocessor token, and
- the NIB preprocessor token.
The Assert class is for assertions, and only assertions.
The Nib class is for development output, and only
development output.
Listing 1 shows the Assert class. The
Assert class's public methods execute only when the
ASSERT preprocessor token is defined. This is the only thing controlled
by the ASSERT token.
Listing 2 shows the Nib class. The
Nib class's public methods execute only when the NIB
preprocessor token is defined, and this is the only thing controlled by the NIB token.
Below is an example where the Shift class has been
written to use the Assert and
Nib classes. (For the comments, see the previous examples.)
class Shift
{
static
int shift3(int x) {
Assert.Test((x >= 0) &&
(x <= 7));
return ((x >> 2) & 1) | ((x << 1) & 6);
}
static void Main() {
while (true) {
string s = Console.ReadLine();
if ((s ==
null) || (s.Length == 0))
{ break ; }
char c = s[0];
if (((c >= '0') && (c <= '7'))) {
int x = c;
Nib.WriteLine("s={0}",
s);
Nib.WriteLine("c={0}",
c);
Nib.WriteLine("x={0}",
x);
Console.WriteLine("The
result is {0}", shift3(x));
} else {
Console.WriteLine(
"Please enter a number between 0 and 7.");
}
}
}
}
The use here of the Assert and
Nib classes should be self-explanatory.
It's helpful to have a software engineering term that means, “something that controls
development output.” In this regard, the term “nib” possesses the advantage of not
having other meanings commonly associated with programming. Another nice feature
is that that “nib” is only a few characters long. A bonus is that the meaning of
the English word “nib” is related to the Nib class's function -- the nib of a pen
is the part that applies ink to paper.
Conclusion
In this article, we have introduced assertions and development output, discussed
goals for a provider of assertion and development output services, and reviewed
problems with using the .NET Debug and
trace classes for assertions and development output. We then presented
a simple and easily understood system for using assertions and development output.
This system uses two classes and two preprocessor tokens. We have found that this
system is convenient and effective in practice.
References
[1] Richard Grimes, Developing Applications with Visual Studio.NET. Addison-Wesley,
2002, ISBN 0-201-70852-3.
[2] John Lakos, Large-Scale C++ Software Design. Addison-Wesley, 1996, ISBN
0-201-63362-0.
Listings
Listing 1: The Assert Class.
using System;
class Assert
{
// Probably, FailedException
instances should be created
// only from within the Assert class.
public class FailedException
: ApplicationException {
public FailedException(string s) : base(s) {}
}
[System.Diagnostics.Conditional("ASSERT")]
public static
void Test(bool condition)
{
if (condition) {
return; }
throw new FailedException("Assertion failed.");
}
[System.Diagnostics.Conditional("ASSERT")]
public static
void Test(bool condition, string message)
{
if (condition) {
return; }
throw new FailedException("Assertion '" + message + "' failed.");
}
}
Listing 2:
The Nib Class.
// (This version of the Nib class always writes
to System.Console. Later
// versions might add functionality similar to System.Debug.Listeners.)
using System;
class Nib
{
[System.Diagnostics.Conditional("NIB")]
public static
void Write(object obj)
{
Console.Write(obj.ToString());
Console.Out.Flush();
}
[System.Diagnostics.Conditional("NIB")]
public static
void W(object obj) //
short name
{
Console.Write(obj.ToString());
Console.Out.Flush();
}
[System.Diagnostics.Conditional("NIB")]
public static
void Write(string s, params object[] args)
{
Console.Write(s, args);
Console.Out.Flush();
}
[System.Diagnostics.Conditional("NIB")]
public static
void W(string s, params object[] args)
// short name
{
Console.Write(s, args);
Console.Out.Flush();
}
[System.Diagnostics.Conditional("NIB")]
public static
void WriteLine(string s, params object[] args)
{
Console.WriteLine(s, args);
Console.Out.Flush();
}
[System.Diagnostics.Conditional("NIB")]
public static
void WL(string s, params object[] args) // short name
{
Console.WriteLine(s, args);
Console.Out.Flush();
}
}
Revisions
4/17/04 - Original
About the Author
Andrew Shapira has been writing programs since
1976, after he was introduced to the PLATO computer system. He has worked in theoretical
and applied computer science, computer engineering, large software projects, mathematics,
and online games. Andrew received the PhD in computer engineering in 1997 from Rensselaer
Polytechnic Institute. He lives in the Seattle area.
Link
View Source