Debug.Assert
While all of the methods we will discuss in this article are
important, none will change the way you program as much as Debug.Assert. The
purpose of Assert is to allow you to put in checks for assumed conditions,
things which you take for granted at runtime for speed or because you think
other pieces of code have handled it for you. Consider the following simple
piece of code in Listing 4.
Listing 4
private string AddNumericStrings(string first, string second)
{
return (int.Parse(first) + int.Parse(second)).ToString();
}
In this method we are taking two strings, converting them to
integers, summing them and returning the results. Something like this may
appear in your code today, and because it is a private helper, you do not worry
about checking the parameters because you assume that the public interfaces
have done so already. That is, until you start getting exceptions in testing.
This method is a prime example of the most simple of
unstated expectations. You as the developer expect that you will get strings
like "9" and "5," not "Fred" and
"George." You do not want to waste cycles in production checking
every time that the developers calling your code have not made a mistake.
Enter Debug.Assert which allows you to check these conditions, but only in
Debug mode compiles. Let us modify the example above with some basic asserts.
Listing 5
private string AddNumericStrings(string first, string second)
{
Debug.Assert(first != null, "The first parameter cannot be null");
Debug.Assert(second != null, "The second parameter cannot be null");
return (int.Parse(first) + int.Parse(second)).ToString();
}
Now, at least you have ensured you are not going to get a
NullReferenceException from this method. But what happens if a wayward developer
makes that mistake and does call this method with something like
AddNumericStrings(null,null)?
Listing 6 - Debug.Assert Alert Box
Instantly, our wayward developer gets a dialog box with
clear messages from the original developer and a full stack trace. And
furthermore, if he clicks Retry, he will be taken directly to the offending
line of code. This is because you, the original developer, have taken the time
to explicitly declare your expectations.
Write and WriteLine
Every developer is familiar with Console.Write and
Console.WriteLine. These very handy methods output to the Console without and
with a new line, respectively, whatever string you may hand them. Debug.Write
and Debug.WriteLine write instead to whomever you have setup to listen. You see,
Debug has a property called Listeners which details what sources would like to
be notified when Write or WriteLine are called.
There are two ways to add items to the Listeners collection,
and they are not mutually exclusive. The first and most obvious is to simply
write some code. For instance, if you would like all the Debug.Write and
Debug.WriteLine statements output to the Console, you could use the following
code.
Listing 7 - Add a Console Listener via Code
class Program
{
static void Main(string[]args)
{
Debug.Listeners.Add(new ConsoleTraceListener());
Debug.WriteLine("Testing 1 2 3...");
}
}
As you might guess, there are many different trace listeners,
including TextWriterTraceListener, which can take any System.IO.Stream and as
such could also write to a log file. But as I said, there is another way of
controlling what is listening. The other option is to setup a listener inside
the App.Config or Web.Config file of your application.
Listing 8 - Add a ConsoleTraceListener via App.Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.diagnostics>
<trace>
<listeners>
<add name="MyConsoleListener"
type="System.Diagnostics.ConsoleTraceListener" />
</listeners>
</trace>
</system.diagnostics>
</configuration>
Now there is a great deal more to Listeners than we will
cover here today; they come in many shapes and sizes and have untapped power we
will not discuss. But this primer will be enough to guide you towards them and
let you begin to see output from the commands we are discussing.
WriteIf and WriteLineIf
On the surface these two methods will seem little different
from their brothers we just discussed, but in reality they require quite a bit
more care in use to be used properly. In function these methods write a
message out to the listeners, but only if a Boolean condition that is provided
is true. This may seem to make both Example A and Example B in Listing 9 the
same piece of code, but reality they are very different.
Listing 9
class Program
{
static void Main(string[]args)
{
// Example A
Debug.WriteLineIf(Convert.ToBoolean(Console.ReadLine()),
"Complex Condition Met");
// Example B
if (Convert.ToBoolean(Console.ReadLine()))
Debug.WriteLine("Complex Condition Met");
}
}
Example A and Example B will behave identically when
compiled in Debug mode, but when compiled in Release mode the difference in
Reflector is striking.
Listing 10 - Comparison between Debug and Release
mode compiles
As you can see in the second image of Listing 10, the
condition of the If statement of Example B has remained in Release mode. This
tells us two things, first the need for WriteIf and WriteLineIf is to ensure
that even the conditional logic is removed in Release mode, secondly that we
should encapsulate all the logic needed for a WriteIf or WriteLineIf into the
statement and not create local variables to control them unless those variables
are needed for other purposes in the code.
Indent and Unindent
Finally, let us discuss how to keep all of these excellent
new messages from becoming a jumbled mess when you need to review the output.
Indent and Unindent are provided by System.Diagnostics.Debug to help keep
things organized. The object will track how many times each method has been
called and then add the appropriate indentation to calls to any of the Write
methods we have discussed. Consider the following example.
Listing 11 - Indent and Unindent Demo
class Program
{
static void Main(string[]args)
{
Debug.WriteLine("Starting Main");
Debug.Indent();
for (int index = 0; index < 10; index++)
{
if ((index % 2) == 0)
Debug.Indent();
if ((index % 3) == 0)
Debug.Unindent();
Debug.WriteLine(string.Format("Loop {0} at Indent Level {1}", index,
Debug.IndentLevel));
}
Debug.IndentLevel = 0;
Debug.WriteLine("Ending Main");
}
}
This code is simply meant to demonstrate how Indent and
Unindent work and how they relate to the IndentLevel property of Debug. If we
run the above code, Listing 12 is the result.
Listing 12 - Output
Starting Main
Loop 0 at Indent Level 1
Loop 1 at Indent Level 1
Loop 2 at Indent Level 2
Loop 3 at Indent Level 1
Loop 4 at Indent Level 2
Loop 5 at Indent Level 2
Loop 6 at Indent Level 2
Loop 7 at Indent Level 2
Loop 8 at Indent Level 3
Loop 9 at Indent Level 2
Ending Main
As you can see this can provide a cleaner output to your
logs and allow you to create a visual hierarchy within your logs. I personally
recommend using Indent and Unindent just before and after any looping structure
such as for, foreach, while, etc. This will create a clear indication of
repetitive tasks in the output.