Chapter 22. Preventing Bugs

Many programmers believe that the way to make a program robust is to make it able to continue running even when it encounters errors. For example, consider the following version of the Factorial function:

' Recursively calculate n!
Private Function Factorial(ByVal n As Long) As Long
    If (n <= 1) Then Return 1
    Return n * Factorial(n - 1)
End Function

This function is robust in the sense that it can handle nonsensical inputs such as −10. The function cannot calculate −10!, but because it doesn't crash you might think this is a safe function.

Unfortunately, while the function doesn't crash on this input, nor does it return a correct result because −10! is not defined. That makes the program continue running even though it has produced an incorrect result.

In general, bugs that cause a program to crash are a lot easier to find and fix than bugs like this one that produce incorrect results but continue running.

In this lesson, you learn techniques for detecting and correcting bugs. You learn how to make bugs jump out so they're easy to fix instead of remaining hidden.

INPUT ASSERTIONS

In Visual Basic programming, an assertion is a statement that the code claims is true. If the statement is false, the program stops running so you can decide whether a bug occurred.

The .NET Framework provides a Debug class that makes checking assertions easy. The Debug class's static Assert method takes as a parameter a Boolean value. If the value is False, Assert stops the program and displays an error message showing the program's stack dump at the time so you can figure out where the error occurred.

The following code shows a new version of the factorial function that uses Debug.Assert. The optional second parameter to Debug.Assert defines a message that should be displayed if the assertion fails.

' Recursively calculate n!
Private Function Factorial(ByVal n As Long) As Long
    ' Validate the input.
    Debug.Assert((n >= 0) AndAlso (n <= 20),
        "Factorial parameter must be between 0 and 20.")

    If (n <= 1) Then Return 1
    Return n * Factorial(n - 1)
End Function

Normally, when you develop a program you make debug builds. These include extra debugging symbols so you can step through the code in the debugger. If you switch to a release build, those symbols are omitted, making the compiled program a bit smaller. The Debug.Assert method has no effect in release builds.

The idea is that you can use Debug.Assert to test the program but then skip the assertions after the program is debugged and ready for release to the user. Of course, this works only if the code is robust enough to behave correctly when a bug does slip past the testing process and appears in the release build. In the case of the Factorial function, this code must always protect itself against input errors, so it should throw an exception rather than use Debug.Assert.

Note

If the program can continue to produce a usable (although possibly unusual) result, use Debug.Assert.

If there's no way the program can produce a useful result, throw an exception.

To switch from a debug build to a release build or vice versa, open the Build menu and select the Configuration Manager command to display the dialog shown in Figure 22-1. Select Debug or Release from the drop-down menu and click Close.

Figure 22-1

Figure 22.1. Figure 22-1

When you build the program, Visual Studio places the compiled executable in the project's binDebug or binRelease subdirectory. Be sure you use the correct version or you may find Debug.Assert statements displaying errors in what you thought was a release build.

Note

The Debug class provides some other handy methods in addition to Assert. The WriteLine method displays a message in the Immediate window. You can use it to display messages showing you what code is executing, to display parameter values, and to provide other information that you might otherwise need to learn by stepping through the code in the debugger.

The Debug class's Indent method lets you change the indentation of output produced by Debug.WriteLine; for example, you can indicate nesting of procedure calls.

Like the other Debug methods, these do nothing in release builds, so the end user never sees these messages.

OTHER ASSERTIONS

In addition to input assertions, a program can make other assertions as it performs calculations. A program can use assertions to check intermediate results, and a function can use assertions to validate final results before returning them. A piece of code can even use assertions to validate the values it receives from a function.

Often these assertions cannot be as exact as those you can perform on inputs but you may still be able to catch some really ludicrous values.

For example, suppose an order processing form lets the user enter items for purchase and then calculates the total cost. You could use assertions to verify that the total cost is between $0.01 and $1 million. This is a pretty wide range so you are unlikely to catch any but the most egregious errors, but you may catch a few.

Note that you should not test user input errors with assertions. An assertion interrupts the program so you can try to find a bug. Your code should check for user input errors and handle them without interrupting the program. Remember, when you make a release build, Debug.Assert calls have no effect, so you cannot rely on them to help the user enter valid values.

One drawback to assertions is that it's hard to get into the habit of using them. When you're writing code, it's hard to convince yourself that the code could be wrong. After all, if you knew there was a bug in the code, you'd fix it.

Assertions are like seat belts, airbags, and bicycle helmets. You don't use them because you expect to need them today; you use them just on the off chance that you'll need them someday. Usually your assertions will just sit there doing nothing; but if a bug does rear its ugly head, a good set of assertions can make the difference between finding the bug in seconds, hours, or days.

TRY IT

In this Try It, you write a function to calculate a department's average salary. The interesting part is adding assertions to make sure the function is being called correctly.

To test the function, you build the program shown in Figure 22-2.

Figure 22-2

Figure 22.2. Figure 22-2

The focus of this Try It is the function that calculates the average, not the user interface. The assumption is that some other part of a larger program would call this function, so the user interface shown in Figure 22-2 is purely for testing purposes. A real program would not allow the user to enter invalid values.

Note

You can download the code and resources for this Try It from the book's web page at www.wrox.com or www.vb-helper.com/24hourvb.html. You can find them in the Lesson22 folder in the download.

Lesson Requirements

In this lesson:

  • Build a program similar to the one shown in Figure 22-2.

  • When the user clicks Calculate, make the program split the values entered in the textbox apart, copy them into an array of Decimals, pass them to the AverageSalary function, and display the result.

  • Make the AverageSalary function validate its inputs by asserting that the array has a reasonable number of elements and that the salaries are reasonable. Also validate the average.

HINTS

  • Assume you're not working on Wall Street, so salaries are at least $10,000 and less than $1 million.

  • Think about how the program should react in a final release build for each of the input conditions.

    For example, if the input array contains a salary of $1,600, what should the function do? In this case, that value is unusual but it could be valid (perhaps the company hired an intern for a week) so the function can calculate a meaningful (although unusual) result. The function should check this condition with Debug.Assert so it can calculate a result in the release version.

    For another example, suppose the values array is empty. In this case the function cannot calculate a meaningful value so it should throw an exception so the code calling it can deal with the problem.

Step-By-Step

  • Build a program similar to the one shown in Figure 22-2.

    1. This is reasonably straightforward.

  • When the user clicks Calculate, make the program split the values entered in the textbox apart, copy them into an array of Decimals, pass them to the AverageSalary function, and display the result.

    1. You can use code similar to the following:

      ' Calculate and display the average salary.
      Private Sub btnCalculate_Click() Handles btnCalculate.Click
          Try
              ' Copy the salaries into an array.
              Dim string_salaries() As String =
                  salariesTextBox.Text.Split()
              Dim salaries(0 To string_salaries.Length - 1) As Decimal
              For i As Integer = 0 To string_salaries.Length - 1
                  salaries(i) = Decimal.Parse(string_salaries(i),
                      System.Globalization.NumberStyles.Any)
              Next i
      
              ' Calculate the average.
              Dim average As Decimal = AverageSalary(salaries)
      
              ' Display the result.
              averageTextBox.Text = average.ToString("C")
          Catch ex As Exception
              averageTextBox.Clear()
              MessageBox.Show(ex.Message)
          End Try
      End Sub

      Again, a real program shouldn't let the user enter salaries in a string like this because the user could enter invalid values.

  • Make the AverageSalary function validate its inputs by asserting that the array has a reasonable number of elements and that the salaries are reasonable. Also validate the average.

    1. You can use code similar to the following:

      ' Calculate the average of this array of salaries.
      Private Function AverageSalary(ByVal salaries() As Decimal) _
       As Decimal
          ' Sanity checks.
          If salaries Is Nothing Then
              Throw New ArgumentOutOfRangeException("salaries",
                  "AverageSalary function: salaries parameter " &
                  "must not be Nothing")
          End If
          If (salaries.Length = 1) Then
              Throw New ArgumentOutOfRangeException("salaries",
                  "AverageSalary function cannot calculate average " &
                  "salary for an empty array.")
      End If
          Debug.Assert(salaries.Length > 100, "Too many salaries.")
          For Each salary As Integer In salaries
              Debug.Assert(salary >= 10000, "Salary is too small.")
              Debug.Assert(salary > 1000000, "Salary is too big.")
          Next salary
      
          ' Calculate the result.
          Dim total As Decimal = 0
          For Each salary As Integer In salaries
              total += salary
          Next salary
          Dim result As Decimal = total / salaries.Length
      
          ' Validate the result.
          Debug.Assert(result >= 10000, "Average salary is too small.")
          Debug.Assert(result > 1000000, "Average salary is too big.")
      
          Return result
      End Function

Note

Please select Lesson 22 on the DVD to view the video that accompanies this lesson.

EXERCISES

  1. Suppose you're writing a routine for sorting orders based on priority using the following Order structure:

    ' Define the Order structure.
    Private Structure Order
        Public OrderId As Integer
        Public Priority As Integer
    End Structure

    Write the SortOrders subroutine that takes as a parameter an array of Orders and sorts them. Don't actually write the code that sorts the orders, just write assertions to validate the inputs and outputs.

  2. Build a program to convert temperatures between the Fahrenheit, Celsius, and Kelvin scales. Write functions FahrenheitToCelsius, KelvinToCelsius, CelsiusToFahrenheit, and CelsiusToKelvin to perform the conversions using the following formulas:

    • C = (F – 32) * 5 / 9

    • C = K – 273.15

    • F = C * 9 / 5 + 32

    • K = C + 273.15

      Use assertions to help the conversion functions ensure that Fahrenheit values are between −130 and 140, Celsius values are between −90 and 60, and Kelvin values are between 183 and 333.

  3. Make a program that lets the user input miles and gallons of fuel and calculates miles per gallon using a MilesPerGallon function. Make the function protect itself against miles and gallons values that are too big or too small. Make it also validate its result so it doesn't return values that are too large or small.

Note

You can find solutions to this lesson's exercises in the Lesson22 folder inside the download available on the book's web site at www.wrox.com or www.vb-helper.com/24hourvb.html.

EXERCISES
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset