Debugging Tips

   

Knowing the Visual Studio .NET debugger inside and out makes it easier to debug programs. But that's only part of what you need to debug Visual Studio .NET programs. You need an approach to debugging that helps you get to the problem. You can spend hours chasing through a maze trying to find a bug, but if you follow the suggestions presented next , you should better be able to avoid costly dead ends.

You can sometimes fix a bug and then find another bug related to the first or to the way you fixed it. When I fix a bug, I ask myself three questions to ensure that I've thought carefully about its significance. You can use these questions to improve productivity and quality every time you think you've found and fixed a bug.

The key idea behind these questions is that every bug is a symptom of an underlying process. You have to treat the symptoms, but if all you do is treat symptoms, you'll continue to see more symptoms forever. The underlying process that caused your bug probably is nonrandom and can be controlled after you identify what happened and what caused it to happen.

Before you ask the three questions, you need to overcome your natural resistance to looking carefully at the bug. Look at the code and explain what went wrong. Start with the observable facts and work backward, asking "Why?" repeatedly, until you can describe the pattern that underlies the bug. Often, you should do this with a colleague, because explaining what you think happened forces you to confront your assumptions about what the program is up to. Here's an example of such scrutiny:

It blew up because subscript j was out of range.

Why?

j was 10 but the top array subscript is only 9.

Why?

j is a string length, and the array origin is 0; so the last character in a string of length 1 is index 0.

Look for additional surprises in the situation at the time the bug was found. Check key program variables at the point of failure to see whether you can explain their values. For example:

Why is the name null?

Why was it trying to output an error message, anyway?

Keep notes of what you did and what happened. You need to know what is really going on, and this means keeping measurements and history.

The Three-Section Technique Section

When these steps are out of the way, you are ready to ask the first of the three questions.

Is This Mistake Somewhere Else Also?

Look for other places in the code where the same pattern applies. Vary the pattern systematically to look for similar bugs . Ask questions such as these:

Where else do I use a length as a subscript?

Do all my arrays have the same origin?

What would happen for a zero-length string?

Try to describe the local rule that should be true in this section of the code but that the bug disobeyed; your search for this will help you see other potential bugs. This is the general thought pattern:

The starting offset plus the length, minus 1, is the ending subscript unless the length is zero.

It's most productive to fix several bugs for every one you find (because when you find one, you usually find others). Trying to describe the bugs in general terms also raises your level of understanding about what the program is doing and helps you avoid introducing more bugs as you program.

What Next Bug Is Hidden Behind This One?

After you figure out how to fix the bug, you need to imagine what will happen after you fix it. The statement after the failing one might have a bug in it too, but the program never got that far before; or some other code might be entered for the first time as a result of your fix. Take a look at these untried statements and look for bugs in them. Think about this:

Would this next statement work?

While you're thinking about control flow is a good time to ask whether there are other unreached parts of the program. Ask yourself:

Are there combinations of features I've never tested ?

It doesn't take much work to design a program so that you can check off as you execute its various parts; it's often surprising how much of a program doesn't work at all after the builder says it's been tested. The thought process continues:

Can I make all the error messages come out in test?

Beware of making a change in one place that causes a bug somewhere else. A local change to some variable might violate the assumptions made further on in the execution. For example, you might consider this: If I just subtract 1 from j, the move statement later will try to move -1 characters when the string length is 0.

If you've made a lot of changes to the program already, consider carefully whether adding another local fix is the right thing to do, or whether it's time to redesign and reimplement.

What Should I Do to Prevent Bugs Like This?

Ask how you can change your ways to make this kind of bug impossible by definition. By changing methods or tools, it's often possible to completely eliminate a whole class of failures instead of shooting the bugs down one by one.

Start by asking when the bug was introduced: When in the development life cycle could the bug have been prevented? You might think: The design is okay; I introduced this bug in coding.

Examine the reason for the bug in detail. Think about the process that was going on at the moment the bug was introduced, and ask how it could be changed to prevent the bug. For example: Separate data types for offset and length would have caught this error at compilation time. Then, each text item could be output with a macro that hides the subscripting calculation. Then I can figure this out just once.

Don't be satisfied with glib answers. Suppose your explanation for a bug is "I just forgot ." How can the process be changed so that you don't need to remember? The language can be changed so that the detail you omitted is completely hidden, or your omission is detected and causes a compiler to display a diagnostic message. You could use a language preprocessor for this problem domain, or a clever editing tool that fills in defaults, checks for errors, or provides hints and rapid documentation. The bug might be a symptom of communication problems in the programming team, or of conflicting design assumptions that need discussion.

Consider the way the bug was found, and ask how it could have been found earlier. How could testing be made more air-tight? Could tests be generated automatically? Could inline checking code be added that would trap errors all the time? You might think these kinds of thoughts:

I should try a zero length string in my unit tests.

I could enable subscript checking and catch this sooner.

Systematic methods and automated tools for compilation, building, and testing are always worth creating. They pay for themselves quickly by eliminating long debugging and fact-finding sessions.

Applications of the Three-Question Technique

Make a habit of asking the three questions every time you find a bug. You don't even have to wait for a bug to use the three questions.

During design and implementation review, each comment you get can be treated with the three questions. Review comments are the result of an underlying communication process that you can improve. If you feel that the reader's comment on a specification is wrong, for example, you might ask what kept your document from being understood , and how you can communicate better with the reviewer.

Three Approaches to Debugging

I have three approaches to debugging. I use each of them at different times. Deciding which method to use is more black art than logical reasoning ”at least for me. I have an intuition about it that serves me well. I'm also ready at any moment during a debugging session to change my approach.

The Wishful -Thinking Approach

The wishful-thinking approach is the easiest approach to debugging. If it works, it's the fastest way to arrive at a solution. It's also the least effective. But it's where most of us start when we first try to debug applications. I still use it if I feel that the solution is something trivial ” especially if I think it's something more along the lines of a typographical error in the code that I'll spot pretty quickly.

Here's how you do it. Run the debugger. Set a breakpoint in the code ahead of where you suspect the problem is located. Then step into methods or step over them. Step over methods you're sure are okay, and step into methods you're not sure about.

As you do this, your mind can't help doing a mental simulation of the code execution while the debugger is in operation. When I use this method of debugging, 90% of the time I find the problem when I'm getting close to the code with the error. It's almost as if the debugger is nothing more than a crutch to force my mind to carefully simulate the code's execution.

Note

Use the wishful-thinking approach to debugging when your program is relatively small or when you think the problem is fairly trivial. This approach saves you time if the bug is easy to find, but it is a big waste for bugs that are hard to find.


The Split-the-Difference Approach

Have you ever played the game in which someone thinks of a number, and you have a certain number of guesses to determine the number? Say you're guessing a number from 1 to 100. The wisest approach is to guess 50. Then if it's above 50, you guess 75. If it's below 75 you guess halfway between 50 and 75 ”63. And the game goes on like this. Basically, you're splitting the difference by guessing a number that's midway in the range into which the target number falls . At least that's how the logicians suggest playing that game. You might have a bolder strategy.

This split-the-difference method is similar to the numbers guessing game. You start off with the given information. You know that everything is fine at a certain point, but you're sure it's not fine at this other point. Okay, you have a range of source code in which you know the problem occurs.

Start off by splitting the difference. Set a breakpoint halfway into the code. When the debugger stops at your breakpoint, examine all the variables. Is everything okay? If there are no detectable problems, chances are that the error occurred later. If the problem has surfaced, it was in the first half of the code.

Now that you know in which half of the code the problem occurred, clear the breakpoint you've set, and set a breakpoint in the middle of that code. Then restart the program. You proceed with this process, narrowing the area in which you think the error occurs. Finally, when you get a manageable piece of code, step through it and find the exact line at which the error occurs.

Note

Use the split-the-difference approach to debugging if you feel that the error will be somewhat difficult to find. It's also recommended if your program is of a medium to large size .


The Assume-Nothing Approach

The assume-nothing approach to debugging is my last resort. It's the most rigorous approach to debugging, and it takes the most time. You still have to identify the block of code in which the error occurs before you start. If this is the entire program, so be it ”that's the block of code you'll have to work with.

Here's how it works. Imagine that you have a method. Somewhere in that method, a variable is becoming negative when it should always be positive. Start by commenting out all the code and recompiling. To comment out source code, just place two / characters at the beginning of each line of code. Two methods follow. The first method is normal; the second one has all the lines of source code commented out:

 private void MyMethod()  {      Bitmap newBitmap = new Bitmap( 300, 200, PixelFormat.Format24bppRgb );      Graphics g = Graphics.FromImage( newBitmap );      SolidBrush blueBrush = new SolidBrush( Color.Blue );      SolidBrush redBrush = new SolidBrush( Color.Red );      SolidBrush greenBrush = new SolidBrush( Color.Green );      SolidBrush blackBrush = new SolidBrush( Color.Black );      g.FillRectangle( blueBrush, 0, 0, 150, 100 );      g.FillRectangle( redBrush, 150, 0, 150, 100 );      g.FillRectangle( greenBrush, 0, 100, 150, 100 );      g.FillRectangle( blackBrush, 150, 100, 150, 100 );      newBitmap.Save( Response.OutputStream, ImageFormat.Jpeg );  }  private void MyMethod()  {  //    Bitmap newBitmap = new Bitmap( 300, 200, PixelFormat.Format24bppRgb );  //    Graphics g = Graphics.FromImage( newBitmap );  //    SolidBrush blueBrush = new SolidBrush( Color.Blue );  //    SolidBrush redBrush = new SolidBrush( Color.Red );  //    SolidBrush greenBrush = new SolidBrush( Color.Green );  //    SolidBrush blackBrush = new SolidBrush( Color.Black );  //    g.FillRectangle( blueBrush, 0, 0, 150, 100 );  //    g.FillRectangle( redBrush, 150, 0, 150, 100 );  //    g.FillRectangle( greenBrush, 0, 100, 150, 100 );  //    g.FillRectangle( blackBrush, 150, 100, 150, 100 );  //    newBitmap.Save( Response.OutputStream, ImageFormat.Jpeg );  } 

With all the source code in your method commented out, you know for certain that nothing should go wrong. Rebuild the program and run the debugger. Examine your program variables and make sure they're OK. If the variables remain in acceptable states, you're correct in assuming that the problem is in your method.

Now, add back lines of code to the method by uncommenting them. Try adding one line of code at a time. Sometimes you can't and you have to add two or three lines. Each time you uncomment lines of code, rebuild the application and run the debugger. When you finally add the code that makes the variable become negative, you've found the problem.

This example is actually pretty trivial compared to some situations I've encountered . Still, though, it shows you how to implement the assume-nothing approach to debugging.

Note

Use the assume-nothing approach to debugging as a last resort. It takes the most time but is the most systematic when you need to find an error that's difficult to uncover.


Good Design Practices to Reduce the Need for Debugging

You now know how to use the Visual Studio.NET debugger. These skills will help you get the most out of the debugger. But there's more to debugging than knowing which key to press and which window to watch. You need to develop a systematic approach to debugging programs. A random or haphazard technique wastes time and only sometimes produces useful results.

This section mentions several important topics related to good design:

  • Design your program carefully

  • Organize your program well

  • Make each class and method easy to understand

  • Read your source code and simulate it mentally

Design Your Program Carefully

It might sound strange to talk about program design in a chapter about debugging. But if you think about it, it's not strange at all. A properly designed program prevents bugs and makes the debugging process easier when the need arises.

If you're a software developer in a company that has several developers, somewhere along the way someone will reuse some of your code or inherit your project. That person has no chance of understanding your program, especially when it comes to debugging, if it's not properly designed.

Organize Your Program Well

Principle number one: Organize your program into easily understood chunks . I once took over a project from a developer who was long gone from the company. He used a method named redrawScreen() . This seems easy enough to understand. My guess at first glance was that this method would redraw all information on the screen. I couldn't have been more wrong. Although the first couple of lines redrew the screen, the rest of the huge method did stuff such as checking a user 's database connection, calculating variables that had nothing to do with the screen redraw , and checking the state of some hardware peripherals.

This method should never have included anything except the screen redraw code because it was called redrawScreen() . The extra couple of minutes it would have taken the programmer to create separate methods for the unrelated code would have been well worth it. Not only would he have had an easier time debugging the program, but I would have had an easier time understanding it, as well.

I'm not just whining for an easier job. When a program is easier to debug, it saves time. And this translates into a monetary value for the employers of software developers. Anything a developer can do to save time in the long run is good. I know what the pressure of a deadline is like. But don't use sloppy techniques, even if something is due tomorrow.

Take a look at a simple example. Consider the following code:

 int m_nVolts, m_nAmps = 2, m_nOhms = 200;  int m_nSpeed, m_nDistance = 120, m_nTime = 2;  double m_dKinetic, m_dMass = 5.0, m_dVelocity = 3.0;  public void mainProgram()  {    // Do stuff here.    calculateStuff();    //Do stuff here.  }  public void calculateStuff()  {    // Calculate volts.    m_nVolts  =  m_nOhms * m_nAmps;    // Calculate speed.    m_nSpeed  =  m_nDistance / m_nTime;    // Calculate kinetic energy.    m_dKinetic  =      ( m_dMass * m_dVelocity * m_dVelocity ) / 2;  } 

The main program code calls a single method, which then makes three different calculations. This is a simple example, so it might appear okay on the surface. But consider how it would look if the three calculations were complicated, maybe a hundred lines of code each. And what if this program wasn't the trivial example that it is, but instead a program of ten thousand lines of code?

Your program all of a sudden develops a problem. It's incorrectly calculating kinetic energy. To debug this, you have to step into the calculateStuff() method. Now, instead of strictly debugging the Kinetic energy calculations, you're mixed up with two other calculations.

The situation gets even more mixed up when you declare local variables, maybe just some temporary counter and scratch variables, and use them for all three calculations. There's a principle of object-oriented programming called encapsulation . It's one of the cornerstones of object-oriented programming. In a strict sense, encapsulation means separating the implementation details from the abstraction. This is what you should do ”separate the three calculations so that they are by themselves. This technique makes them easier to understand and far easier to debug.

The following change to the preceding poorly designed example follows :

 int m_nVolts, m_nAmps = 2, m_nOhms = 200;  int m_nSpeed, m_nDistance = 120, m_nTime = 2;  double m_dKinetic, m_dMass = 5.0, m_dVelocity = 3.0;  public void mainProgram()  {    // Do stuff here.    calculateVolts();    calculateSpeed();    calculateKinetic();    //Do stuff here.  }  public void calculateVolts()  {    // Calculate volts.    m_nVolts  =  m_nOhms * m_nAmps;  }  public void calculate Speed()  {    // Calculate speed.    m_nSpeed  =  m_nDistance / m_nTime;  }  public void calculateKinetic()  {    // Calculate kinetic energy.    m_dKinetic  =      ( m_dMass * m_dVelocity * m_dVelocity ) / 2;  } 
Make Each Class and Method Easy to Understand

Sure, you're the programmer ”you're going to understand. But how about the poor guy six months from now who gets assigned to make a modification to your program? Do you want him to have to come to you for explanations ? Or, how about yourself six months from now? If you're anything like me, in six months you've worked on so many other things that you've forgotten many of the details regarding how the project works.

Here's how you can avoid the problem: make every class, every method, and every variable easy to understand. Just how are you going to do that? Here's a list of suggestions:

  • Name methods according to exactly what they do. A method that performs screen updates should be named updateScreen() . If it does another thing such as make a beep sound, rename the method to updateScreenAndBeep() .

  • Name variables according to exactly what they're for. A variable that holds the count of a number of times the user has clicked the left mouse button should be named leftMouseButtonClicks . If it's an integer (which it should be), it should be named nLeftMouseButtonClicks . If it's a member variable, it should be named m_nLeftMouseButtonClicks .

  • Add source-code comments whenever it's necessary to understand the code. I don't mean only if it's necessary for you to understand the code right after you've written it. You should add comments if you think that anyone who looks at your code would have trouble understanding it. Furthermore, I've had many instances in which I was working on a difficult method that I just couldn't get correct. I've found that if I started adding source-code comments as I went, my thinking process cleared up and the method was much easier to write. I sometimes find it useful to add the source code comments first and then write the code ”this is like writing an outline in comments and then filling in the outline in code.

  • Indent your source code so that it's easy to see how the logic flows. For instance, after an if statement, all source code should be indented an additional tab until the end of the contents of the if clause.

Read Your Source Code and Simulate It Mentally

Before you run the debugger, run your mental debugger. It's far better for you to discover the problem by following the program's execution. You can simulate what the program does in your mind and follow the code just as if you were running the debugger.

Why would this approach be better than running the debugger? Because you gain more value by mentally simulating the execution of the program. You can better understand exactly how the code works when you follow the code through in your mind.

The next thing you must do is watch the variables. Be aware of their initial values and where their values change. Keep tabs on the more important ones, especially the ones you suspect are involved in the unwanted behavior.

   


Special Edition Using ASP. NET
Special Edition Using ASP.Net
ISBN: 0789725606
EAN: 2147483647
Year: 2002
Pages: 233

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net