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.
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);
}
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 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.
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.
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.
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.
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.
In this lesson, you:
TextBox
apart, copy them into an array of decimal
s, pass them to the AverageSalary
method, and display the result.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.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.
TextBox
apart, copy them into an array of decimal
s, pass them to the AverageSalary
method, and display the result.
// Calculate and display the average salary.
private void calculateButton_Click(object sender, EventArgs e)
{
try
{
// Copy the salaries into an array.
string[] string_salaries = salariesTextBox.Text.Split();
decimal[] salaries = new decimal[string_salaries.Length];
for (int i = 0; i < string_salaries.Length; i++)
{
salaries[i] =
decimal.Parse(string_salaries[i], NumberStyles.Any);
}
// Calculate the average.
decimal averageSalary = AverageSalary(salaries);
// Display the result.
averageTextBox.Text = averageSalary.ToString("C");
}
catch (Exception ex)
{
averageTextBox.Clear();
MessageBox.Show(ex.Message);
}
}
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.
// Calculate the average of this array of salaries.
private decimal AverageSalary(decimal[] salaries)
{
// Sanity checks.
if (salaries.Length < 1)
{
throw new ArgumentOutOfRangeException("salaries",
"AverageSalary method cannot calculate average " +
"salary for an empty array.");
}
Debug.Assert(salaries.Length < 100, "Too many salaries.");
for (int i = 0; i < salaries.Length; i++)
{
Debug.Assert(salaries[i] >= 10000, "Salary is too small.");
Debug.Assert(salaries[i] < 1000000, "Salary is too big.");
}
// Calculate the result.
decimal total = 0;
for (int i = 0; i < salaries.Length; i++)
{
total += salaries[i];
}
decimal result = 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;
}
Order
structure:
private struct Order
{
public int OrderId;
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.
Write the methods FahrenheitToCelsius
, KelvinToCelsius
, CelsiusToFahrenheit
, and CelsiusToKelvin
to perform the conversions using the following formulas:
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.
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.Hints:
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.)PictureBox
on the form. Make its Resize
event handler refresh the PictureBox
. Make its Paint
event handler call a DrawGraph
method.DrawGraph
method do the following:
Graphics
object on which to draw, and the test scores.DrawGraph
method to make sure the available size and test scores are reasonable.
18.116.21.152