Preventing Bugs - Program Statements - C# 24-Hour Trainer (2015)

C# 24-Hour Trainer (2015)

Section III

Program Statements

Lesson 22

Preventing Bugs

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

// Recursively calculate n!

private long Factorial(long n)

{

if (n <= 1) return 1;

return n * Factorial(n - 1);

}

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

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

The method also has a problem if its input is greater than 20. In that case, the result is too big to fit in the long data type so the calculations cause an integer overflow. By default, the program silently ignores the error, and the result you get uses whatever bits are left after the overflow. In this case, the result looks like a large negative number. Again the method doesn't crash but it doesn't return a useful result, either.

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 that 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 remain hidden.

Input Assertions

In C# 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.

One way to make an assertion is to evaluate the statement and, if it is false, throw an exception. That guarantees that the program cannot continue running if the assertion is false.

The following code shows a Factorial method with assertions. If the method's parameter is less than 0 or greater than 20, the code throws an exception:

// Recursively calculate n!

private long Factorial(long n)

{

// Validate the input.

if ((n < 0) || (n > 20))

throw new ArgumentOutOfRangeException(

"n", "Factorial parameter must be between 0 and 20.");

if (n <= 1) return 1;

return n * Factorial(n - 1);

}

To make this kind of assertion easier, the .NET Framework provides a Debug class. The Debug class's static Assert method takes as a parameter a boolean value. If the value is false, Assert 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 method that uses Debug.Assert. The optional second parameter to Debug.Assert gives a message that should be displayed if the ­assertion fails:

// Recursively calculate n!

private long Factorial(long n)

{

// Validate the input.

Debug.Assert((n >= 0) && (n <= 20),

"Factorial parameter must be between 0 and 20.");

if (n <= 1) return 1;

return n * Factorial(n - 1);

}

NOTE

The Debug class is in the System.Diagnostics namespace. If you want to use it without including the namespace, as in the preceding code, you should include the following using directive at the top of the file:

using System.Diagnostics;

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. TheDebug.Assert method also 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 end users. Of course this works only if the code is robust enough to behave correctly even if a bug does slip past the testing process and appears in the release build. In the case of the Factorial method, this code must always protect itself against input errors so it should throw an exception rather than use Debug.Assert.

To switch from a debug 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 dropdown and click Close.

Screenshot of Configuration Manager dialog box presenting Debug in the Active solution configuration field and Any CPU in Active solution platform field with a list of project contexts to build or deploy.

Figure 22.1

When you build the program, Visual Studio places the compiled executable in the project's bin\Debug or bin\Release 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 Output window. You can use it to display messages showing you what methods are executing, to display parameter values, and to give you 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 so, for example, you can indicate nesting of method 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 method can make other assertions as it performs calculations. A method can use assertions to check intermediate results and to validate final results before returning them. A method can even use assertions to validate the value it receives from another method.

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 validate user inputs 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. Instead of using assertions, you should use TryParse, try-catch blocks, and if statements to determine whether the user's input makes sense. Remember, when you make a release build, Debug.Assert calls go away so you cannot rely on them to validate the user's values.

One drawback to assertions is that it's hard to make programmers use 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.

To summarize, you can use assertions to protect a method against invalid inputs and to validate its outputs. If you want an assertion to only occur in debug builds, use Debug.Assert. If you want a test to be included in release builds, use your own if statement to check the condition and throw an exception if the condition fails. In particular, use Debug.Assert to catch unusual but valid values so you can decide whether they are bugs during testing.

Try It

In this Try It, you write a method to calculate the average of a set of salaries. Calculating the average is easy. The interesting part is adding assertions to make sure the method is being used correctly.

To test the method, you build the program shown in Figure 22.2.

Screenshot of Average Salaries window displaying the Salaries textbox (top), highlighted Calculate button (middle), and Average textbox (bottom).

Figure 22.2

The focus of this Try It is on the method that calculates the average, not on the user interface. The assumption is that some other part of a larger program would call this method, 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. Instead it might take the values from a database. In that case, the method's assertions protect it from invalid data in the database.

Lesson Requirements

In this lesson, you:

· 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 method, and display the result.

· Make the AverageSalary method validate its inputs by asserting that the array has a reasonable number of elements and that the salaries are reasonable. (Assume you're not working on Wall Street so salaries are at least $10,000 and less than $1 million.) Also validate the average.

NOTE

You can download the code and resources for this lesson from the website at www.wrox.com/go/csharp24hourtrainer2e.

Hints

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

For example, if the values array contains a salary of $1,600, what should the method do? In this case, that value is unusual but it could be valid (perhaps the company hired an intern for a week) so the method can calculate a meaningful (although unusual) result. The method 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 method cannot calculate a meaningful value so it should throw an exception to make the calling code 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 method, and display the result.

1. 1. You can use code similar to the following:

2. // Calculate and display the average salary.

3. private void calculateButton_Click(object sender, EventArgs e)

4. {

5. try

6. {

7. // Copy the salaries into an array.

8. string[] string_salaries = salariesTextBox.Text.Split();

9. decimal[] salaries = new decimal[string_salaries.Length];

10. for (int i = 0; i < string_salaries.Length; i++)

11. {

12. salaries[i] =

13. decimal.Parse(string_salaries[i], NumberStyles.Any);

14. }

15. // Calculate the average.

16. decimal averageSalary = AverageSalary(salaries);

17. // Display the result.

18. averageTextBox.Text = averageSalary.ToString("C");

19. }

20. catch (Exception ex)

21. {

22. averageTextBox.Clear();

23. MessageBox.Show(ex.Message);

24. }

}

· Make the AverageSalary method validate its inputs by asserting that the array has a reasonable number of elements and that the salaries are reasonable. (Assume you're not working on Wall Street so salaries are at least $10,000 and less than $1 million.) Also validate the average.

1. 2. You can use code similar to the following:

2. // Calculate the average of this array of salaries.

3. private decimal AverageSalary(decimal[] salaries)

4. {

5. // Sanity checks.

6. if (salaries.Length < 1)

7. {

8. throw new ArgumentOutOfRangeException("salaries",

9. "AverageSalary method cannot calculate average " +

10. "salary for an empty array.");

11. }

12. Debug.Assert(salaries.Length < 100, "Too many salaries.");

13. for (int i = 0; i < salaries.Length; i++)

14. {

15. Debug.Assert(salaries[i] >= 10000, "Salary is too small.");

16. Debug.Assert(salaries[i] < 1000000, "Salary is too big.");

17. }

18. // Calculate the result.

19. decimal total = 0;

20. for (int i = 0; i < salaries.Length; i++)

21. {

22. total += salaries[i];

23. }

24. decimal result = total / salaries.Length;

25. // Validate the result.

26. Debug.Assert(result >= 10000, "Average salary is too small.");

27. Debug.Assert(result < 1000000, "Average salary is too big.");

28. return result;

}

Exercises

1. Suppose you're writing a method to sort orders based on priority. Use the following definition for an Order structure:

2. private struct Order

3. {

4. public int OrderId;

5. public int Priority;

}

Write the SortOrders method, which 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.

6. Build the program shown in Figure 22.3 to convert temperatures between the Fahrenheit, Celsius, and Kelvin scales.Screenshot of Temperatures window displaying textboxes for Fahrenheit, Celsius, and Kelvin with Set button below each conversion factor.

Figure 22.3

Write the methods FahrenheitToCelsius, KelvinToCelsius, CelsiusToFahrenheit, and CelsiusToKelvin to perform the conversions using the following formulas:

equation

Make the conversion methods use assertions to ensure that Fahrenheit values are between –130 and 140, Celsius values are between –90 and 60, and Kelvin values are between 183 and 333.

7. Make a program that lets the user input miles and gallons of fuel and calculates miles per gallon using a MilesPerGallon method. Make the method 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.

8. Copy the Fibonacci program you wrote for Exercise 20-3 (or download the version on the book's website). Because of the recursive way the program calculates Fibonacci numbers, it takes a noticeable amount of time to calculate values larger than around the 35th Fibonacci number. It can still calculate larger values, however. Add appropriate input validation to the Fibonacci method.

9. Exercise 12-11 asks you to debug a program that calculates interest. Copy the fixed program (or download the version on the book's website) and add appropriate input validation.

10.Exercise 12-12 asks you to debug a program that uses several methods to calculate the amount of time needed to double an investment at various interest rates. Copy the fixed program (or download the version on the book's website) and add appropriate input validation.

11.[Graphics, Hard] Make a program similar to the one shown in Figure 22.4 to display a histogram showing ­student test scores.Screenshot of Test Scores window displaying a histogram.

Figure 22.4

Hints:

· Make a class-level Scores array and initialize it to random values in the form's Load event handler. (Hint: For each score, I used the sum of three random values in the ranges 10–25, 10–25, and 10–50 to get a somewhat curved distribution.)

· Place a PictureBox on the form. Make its Resize event handler refresh the PictureBox. Make its Paint event handler call a DrawGraph method.

· Make the DrawGraph method do the following:

§ Take as parameters the available size in which to draw the bar chart, the Graphics object on which to draw, and the test scores.

§ Make 10 bins to count scores in the ranges 0–19, 20–29, 30–39, … , 90–100. (Hint: Make the number of bins a constant so you can change it easily.)

§ Loop through the scores and increment the corresponding bins. (Hint: Be sure to place scores of 100 in the last bin.)

§ Loop through the bins and find the largest count. Use that value to calculate a scale factor that makes the largest count fill the available height. (Hint: scale = available height / largest count.)

§ Calculate the bar width. (Hint: width = available width / number of bars.)

§ Loop through the bins and draw their bars. (Hint: Remember that drawing coordinates start with (0, 0) in the upper-left corner and increase down and to the right.)

12.[Graphics] Copy the program you wrote for Exercise 7. (Or download the version on the book's website if you didn't do it. I warned you that it was hard.) Add validation code to the DrawGraph method to make sure the available size and test scores are reasonable.

NOTE

Please select the videos for Lesson 22 online at www.wrox.com/go/csharp24hourtrainer2evideos.