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