Team LiB
Previous Section Next Section

6) Step Over All New Code in the Debugger As Soon As You Finish Writing It

An early reviewer of this book suggested I include a chapter on preventing bugs before they happen. That's hard to do without insulting people's intelligence. Preventative advice tends to either fall into the "Be really careful when writing code and try to plan ahead" category (which is true but not too helpful) or just becomes a laundry list of language-specific tricks. For instance in C and C++, writing if (5 == x) is safer than if (x == 5) because the first statement eliminates the possibility of accidentally typing a single equal sign instead of double equal signs—if (x = 5)—which is a very common and serious bug in C/C++. But making that mistake with if (5 = x) just plain won't compile, so you're saved from the bug.

VB .NET and C# also have their own set of dangerous idioms you can avoid. But I'm afraid to write a chapter about preventing bugs because people tend to miss the forest for the trees and take away the wrong lessons. Nevertheless, I will say that stepping through all your new code in a debugger as soon as you've finished writing it is one of the single most powerful things you can do to prevent bugs from getting into your product.

Basically, I'm talking about using the debugger for tracing purposes to make sure the code really is running the paths you expected. Unlike regular debugging, you're not looking for anything specific here; you're just watching the code execution to make sure there are no obvious problems. You don't need to do a deep study, just a quick run-through. Theoretically, a well-designed set of test cases ought to eliminate the need for this, but in actual practice, this technique tends to find bugs that are easily overlooked in most test cases.

Why do I think stepping over new code in the debugger is so vital? Ask yourself if you've ever done any of the following:

Let's examine each of these problems and see how stepping through code in a debugger can prevent them.

Forgetting to Fill In the Details of a Function

It's good development practice to work in stages and focus on one problem at a time. Say you're writing a word processing program and you're trying to wrap your brain around the very difficult task of displaying the page with all the correct fonts, graphics, italics, bolds, and colors in exactly the right size and position. You realize you need to know the size of each piece of text, but you're in the middle of an inspired train of thought about page layout and you don't want to put that aside to think about font sizes right now, so you write the ComputeFontSize function as shown here to serve as a placeholder until you can come back to it later:


public int ComputeFontSize() {
    //Obviously need to fill this in later, but for now
    //just return a hard-coded default font size.
    return 12;
}

Hey, you obviously can't ship that in a finished product, but it's enough to start testing some other code. Since you were concentrating on writing some other function, it's understandable if you wanted to just do something quick and dirty here so you don't lose your train of thought. Use this hard-coded function to finish writing your other functions, and then come back and rewrite this function correctly later. So far you haven't done anything wrong.

But 6 hours later, you forget the ComputeFontSize function isn't finished and you add your code to the source tree thinking you're done when you're really not. Eventually someone finds the fonts can't be resized, but by then it's 2 weeks later. The details are no longer fresh in your mind, so you have to waste time relearning the code. But if you had just looked for problems like this by stepping through all your new code in the debugger before declaring yourself finished, you would have spotted the problem while the code details were still at the forefront of your brain. After finishing your code, do a quick walk-through in the debugger to make sure that all executing paths are doing what you think they're doing.

Discovering Your Error Handling Code Was Hiding a Serious Problem

Suppose you finally get around to filling in your ComputeFontSize function and you implement it as follows:


public int ComputeFontSize() {
    //Support backwards compatibility with version 1.0
    //fonts (which can't be resized); but most customers
    //will use our new fonts, which do support sizes.
    if (IsThisNewFormatFont()) {
     ... //Compute font size for the new style fonts
    }
    else {//a legacy, 1.0 style font.
      //Rare. Since these fonts could not
      //be re-sized, return the default font size.
      return DEFAULT_FONT_SIZE;
    }
}

You finish writing the function and you run a test of your page layout. The page looks fine! But a few days later, your testing department tells you the exact same bug still exists. The fonts still can't be resized—not the legacy format, nor the new format. You step over the code and then you eventually discover a bug in the IsThisNewFormatFont function that always incorrectly returns false, which means that your code assumes every font is a 1.0 legacy format font. Since the code actually does produce results that look reasonable (at least on casual inspection), you might not notice the bug until it's too late.

Now in this particular case, the problem would have been detected in any half-decent test case, so hopefully you would have noticed the problem yourself anyway. But this is a trivial example. Developers pride themselves on writing robust code that can handle all errors, which means that sometimes we have a bug that forces the code down the error path when it shouldn't, but we don't even notice because the error handling code covers the problem up. It may take very substantial testing to discover this sort of bug. But if you simply step over your new code in the debugger before adding your code to the source control, you would have instantly seen this problem.

Changing Code As a Test but Accidentally Checking That Code In Anyway

Have you ever written code like the following snippet as a quick test for debugging purposes, and you didn't want that code to be checked in to your project, but it somehow ended up in your shipping product anyway?

    public int ComputeFontSize()
    {
       ...
       //While debugging, I added this line to track down a bug.
       MessageBox.Show("ComputeFontSize is " + fontSize.ToString());
       ...
    }

Once you've found and fixed the bug, you probably want to remove that MessageBox. Unfortunately, if you were hunting for a nasty bug that required changes to multiple files, you might forget about some of these changes and accidentally add them to the code base. Luckily, though, that's another thing that walking over your new code in the debugger can help you spot. You would have already noticed this error if you did a line-by-line code review, but unfortunately, most code reviews aren't line-by-line. Most are merely casual comparisons to look for very obvious problems, and one or two bad lines might slip by if surrounded by lots of other new code. But simply stepping across the code in a debugger forces you to examine your new code in a line-by-line fashion, and you'll notice things you wouldn't otherwise see.

The opposite effect comes into play, too. Maybe while stepping through the debugger, you notice debugging code that should have been added but wasn't. Perhaps you were too rushed to write a log message for when a method call failed. Or maybe you notice the code you wrote lacks any comments describing what it's doing. Better add those comments now—in 2 weeks, you won't remember why you wrote that cryptic check to break out of the loop on the 37th iteration. Stepping through the code in a debugger will help you see the code from a perspective you didn't have when writing it, and you'll suddenly notice when the code isn't as clean as it should be.

Missing a Chance for an Optimization

After writing your code, you run a test and it takes about 10 seconds. That's on the slow side, but you figure you can live with it since this section of code isn't really time-critical; and besides, most of the delay is probably in a network connection that you can't control, anyway.

But what might happen if you walked over the new code in a debugger before declaring the code done? Using a code profiler would be great if you're serious about optimizing, but you already decided this code isn't time-critical, so there's no need to do any formal profiling here. Instead, you can just do a quick once-over with the debugger. Step across each function to see if all of them respond instantly, or if there's one or two lines that seem to hang. In the worst case, you'll confirm your suspicion that the time delays are in the network, but in the best case you might notice a multi-second hang on some function you weren't expecting. Who knows, looking at that function might help you notice an easy performance improvement. For example, say you wrote the following code:

string s1 =...;
for (int i = 0; i < NUM_TRIES; ++i)
{
      if (TimeConsumingFunctionThatAlwaysReturnsTheSameValue(s1))
      ...
}

Maybe when you started writing that code, you didn't yet realize the TimeConsumingFunctionThatAlwaysReturnsTheSameValue function could be declared outside the loop, saving much computation time. You will see this the second time you look at this code, though. If you step over your new code in the debugger immediately after writing it, you tend to notice things like this because the code is still fresh in your mind. But if you wait a week, then there's too much old code to hunt through.

"But hold on!" you cry. "You said this code wasn't time-critical, so why are we bothering to optimize?" It's true that the rule is to only optimize where it matters: Find the speed-critical areas (usually the nested loops) and focus your optimization effort there. After all, a 1 percent gain on each iteration of a loop that executes a hundred million times will add up, but a 10 percent gain on code that executes only once may not make a noticeable difference. So if this code is not time-critical, why should you bother to optimize? But that's just the point—you're not spending the time to do formal optimization here. You're not going to do anything that's risky or time consuming.

You're just stepping over new code in a debugger (which you need to do for other reasons anyway). But if, while doing that, you see an easy optimization that shaves off a few seconds, why not take it? Even if the change isn't in a nested loop, a performance boost is still a performance boost. Although it's true that the optimization bang-for-the-buck comes from the time-critical nested loops, that doesn't justify being sloppy in the non–time-critical code. Inefficient coding there will still add up to make a sluggish program, so do at least casual inspections of the non–speed-critical code to make sure it looks reasonable.

Basically, it's not worth doing full-fledged optimization of non–speed-critical code, but since you're already walking over the code in the debugger anyway, it requires no additional effort to casually verify the code isn't doing anything grossly inefficient.

The Bottom Line

Bottom line: Stepping over your new code through the debugger gives you one last chance to look over what you've written before committing your changes. This isn't a replacement for fully testing your code; it's simply an additional check you should run. When writing code, you try to think holistically and see the big picture. But when stepping over that code in the debugger, you see the code one line at a time, and that's an entirely different perspective. You'll notice all kinds of things you wouldn't notice otherwise. Before adding any new code to the source control system, always step over it in the debugger to make sure everything really is working the way you think it's working.


Team LiB
Previous Section Next Section