Sometimes a program needs to perform the same action in several places. For example, consider the UFO shooting gallery game you wrote for Exercise 19-12 and shown in Figure 20.1.
When a laser bolt hits a UFO, the program takes these steps:
Label
.PictureBox
.BoltIsAway = false
to remember that no laser bolt is currently on the form.Timer
.NewHighScoreForm
.NewHighScoreForm
.NewHighScoreForm
.HighScoreForm
.HighScoreForm
.HighScoreForm
.Now suppose a laser bolt moves off the top edge of the form without hitting a UFO. In that case the program must perform the same steps 3 through 5. The way I wrote my program, those steps take 33 lines of code (not counting blank lines and comments). That's a lot of repeated code to write, debug, and maintain.
In fact, the program contains even more repetition. If the user opens the File menu and selects High Scores, the program repeats the last three steps to display a HighScoresForm
.
Instead of repeating code wherever it was needed, it would be nice if you could centralize the code in a single location and then invoke that code when you need it. In fact, you can do exactly that by using methods.
A method is a group of programming statements wrapped in a neat package so you can invoke it as needed. A method can take parameters that the calling code can use to give it extra information. The method can perform some actions and then optionally return a single value to pass information back to the calling code.
In this lesson, you learn how to use methods. You learn why they are useful, how to write them, and how to call them from other pieces of code.
The shooting gallery scenario described in the previous section illustrates one of the key advantages to methods: code reuse. By placing commonly needed code in a single method, you can reuse that code in many places. Clearly that saves you the effort of writing the code several times.
Much more important, it also saves you the trouble of debugging the code several times. Often debugging a piece of complex code takes much longer than typing in the code in the first place, so being able to debug the code in only one place can save you a lot of time and trouble.
Reusing code also greatly simplifies maintenance. If you later find a bug in the code, you only need to fix it in one place. If you had several copies of the code scattered around, you'd need to fix each one individually and make sure all of the fixes were the same. That may sound easy enough, but making synchronized changes is actually pretty hard, particularly in big projects. It's just too easy to miss one change or to make slightly different changes that later cause big problems.
Methods can also sometimes make finding and fixing bugs much easier. For example, suppose you're working on an inventory program that can remove items from inventory for one of many reasons: external sales, internal sales, ownership transfer, spoilage, and so forth. Unfortunately the program occasionally “removes” items that don't exist, leaving you with negative inventory. If the program has code in many places that can remove items from inventory, figuring out which place is causing the problem can be tricky. If all of the code uses the same method to remove items, you can set breakpoints inside that single method to see what's going wrong. When you see the problem occurring, you can trace the program's flow to see where the problem originated.
A final advantage to using methods is that it makes the pieces of the program easier to understand and use. Breaking a complex calculation into a series of simpler method calls can make the code easier to understand. No one can keep all of the details of a large program in mind all at once. Breaking the program into methods makes it possible to understand the pieces separately.
A well-designed method also encapsulates an activity at an abstract level so other developers don't need to know the details. For example, you could write a FindItemForPurchase
method that searches through a database of vendors to find the best possible deal on a particular item. Now developers writing other parts of the program can call that method without needing to understand exactly how the search works. The method might perform an amazingly complex search to minimize price with sales tax, shipping charges, and long-term expected maintenance costs, but the programmer calling the method doesn't need to know or care how it works.
In summary, some of the key benefits to using methods are:
In C#, all methods must be part of some class. In many simple programs, the main form contains all of the program's code, including all of its methods.
The syntax for defining a method is:
accessibility returnType methodName(parameters)
{
...statements…
[return [returnValue];]
}
Where:
accessibility
is an accessibility keyword such as public
or private
. This keyword determines what other code in the project can invoke the method.returnType
is the data type that the method returns. It can take normal values such as int
, bool
, or string
. It can also take the special value void
to indicate that the method won't return a result to the calling code.methodName
is the name that you want to give the method. You can give the method any valid name. Valid names must start with a letter or underscore and include letters, underscores, and numbers. A valid name also cannot be a keyword such as if
, for
, or while
.parameters
is an optional parameter list that you can pass into the method. I'll say more about this shortly.statements
are the statements that the method should execute.returnValue
is the value returned to the calling code. You can use return
without a parameter to return from a void
method. The method also returns if the program executes its last line of code and reaches the closing curly bracket (}
).
The method's parameters allow the calling code to pass information into the method. The parameters in the method's declaration give names to the parameters while they are in use inside the method.
For example, recall the definition of the factorial function. The factorial of a number N is written N! and pronounced N factorial. The definition of N! is 1 * 2 * 3 * … * N.
The following C# code defines a Factorial
method:
// Return value!
private long Factorial(long value)
{
long result = 1;
for (long i = 2; i <= value; i++)
{
result *= i;
}
return result;
}
The method is declared private
so only code within this class can use it. For simple programs, that's all of the code anyway so this isn't an issue.
The method's data type is long
so it must return a value of type long
.
The method's name is Factorial
. You should try to give each method a name that is simple and that conveys the method's purpose so it's easy to remember what it does.
The method takes a single parameter of type long
named value
. Parameters have method scope so value
is only defined inside the method. In that sense parameters are similar to variables declared inside the method.
The method creates a variable result
and multiplies it by the values 2, 3, … , value
.
The method finishes by executing the return
statement, passing it the final value of result
.
The following code shows how a program might call the Factorial
method:
long number = long.Parse(numberTextBox.Text);
long answer = Factorial(number);
resultTextBox.Text = answer.ToString();
This code starts by creating a long
variable named number
and initializing it to whatever value is in numberTextBox
.
The code then calls the Factorial
method, passing it the value number
and saving the returned result in the new long
variable named answer
.
Notice that the names of the variables in the calling code (number
and answer
) have no relation to the names of the parameters and variables used inside the method (value
and result
). The method's parameter declaration determines the names the parameters have while inside the method.
The code finishes by displaying the result.
A method's parameter list can include zero, one, or more parameters separated by commas. For example, the following code defines the method Gcd
, which returns the greatest common divisor (GCD) of two integers. (The GCD of two integers is the largest integer that evenly divides them both.)
// Calculate GCD(a, b).
private long Gcd(long a, long b)
{
long remainder;
do
{
remainder = a % b;
if (remainder != 0)
{
a = b;
b = remainder;
}
} while (remainder > 0);
return b;
}
The following code shows how you might call the Gcd
method:
// Get the input values.
long a = long.Parse(aTextBox.Text);
long b = long.Parse(bTextBox.Text);
// Calculate the GCD.
long result = Gcd(a, b);
// Display the result.
resultTextBox.Text = b.ToString();
The code initializes two integers, passes them to the Gcd
method, and saves the result. It then displays the two integers and their GCD.
Parameter lists have one more feature that's confusing enough to deserve its own section. Parameters can be passed to a method by value or by reference.
When you pass a parameter by value, C# makes a copy of the value and passes the copy to the method. The method can then mess up its copy without damaging the value used by the calling code.
In contrast, when you pass a value by reference, C# passes the location of the value's memory into the method. If the method modifies the parameter, the value is changed in the calling code as well.
Normally values are passed by value. That's less confusing because changes that are hidden inside the method cannot mess up the calling code.
Sometimes, however, you may want to pass a parameter by reference. To do that, add the keyword ref
before the parameter's declaration.
To tell C# that you understand that a parameter is being passed by reference and that it's not just a terrible mistake, you must also add the keyword ref
before the value you are passing into the method.
For example, suppose you want to write a method named GetMatchup
that selects two chess players to play against each other. The method should return true
if it can find a match and false
if no other matches are possible (because you've played them all). The method can only return one value (true
or false
) so it must find some other way to return the two matched players.
The following code shows how the method might be structured:
private bool GetMatchup(ref string player1, ref string player2)
{
// Do complicated stuff to pick an even match.
…
// Somewhere in here the code should set player1 and player2.
…
// We found a match.
return true;
}
The method takes two parameters, player1
and player2
, that are string
s passed by reference. The method performs some complex calculations not shown here to assign values to the variables player1
and player2
. It then returns true
to indicate that it found a match.
The following code shows how a program might call this method:
string playerA = null, playerB = null;
if (GetMatchup(ref playerA, ref playerB))
{
MessageBox.Show(playerA + " versus " + player);
}
else
{
MessageBox.Show("No match is possible");
}
This code declares variables playerA
and playerB
to hold the selected players' names. It calls the method, passing it the two player name variables preceded with the ref
keyword. Depending on whether the method returns true
or false
, the program announces the match or says that no match is possible.
The out
keyword works similarly to the ref
keyword except it doesn't require that the input variables be initialized. For example, in the preceding example if you don't initialize playerA
and playerB
to some value, Visual Studio will warn you that the variables are not initialized and won't let you run the program. The idea is that the method might need to use the input values of those variables to do its work.
In contrast, if you use the out
keyword instead of ref
, the values are assumed to be output-only parameters from the method, and you are not required to initialize them.
If you use the out
keyword for a parameter, be sure that the method does not try to use the value passed in for that parameter because it may not be initialized. In fact, if the method does try to use the parameter's incoming value, Visual Studio will warn you that it may not be initialized.
In this Try It, you make a method that calculates the minimum, maximum, and average values for an array of double
s. You build the program shown in Figure 20.2 to test the method.
In this lesson, you:
double
s, and three more return double
s. It should loop through the array to find the minimum and maximum and to calculate the average.ref
or out
?double
s, and three more return double
s. It should loop through the array to find the minimum and maximum and to calculate the average.
out
keyword instead of the ref
keyword.The following code shows how you might build this method:
// Calculate the minimum, maximum, and average values for the array.
private void FindMinimumMaximumAverage(double[] values,
out double minimum, out double maximum, out double average)
{
// Initialize the minimum, maxiumum, and total values.
minimum = values[0];
maximum = values[0];
double total = values[0];
// Loop through the rest of the array.
for (int i = 1; i < values.Length; i++)
{
if (values[i] < minimum) minimum = values[i];
if (values[i] > maximum) maximum = values[i];
total += values[i];
}
// Calculate the average.
average = total / values.Length;
}
TextBox
's text and use its Split
method to break the user's values into an array of string
s.double
array and use a for
loop to parse the text values into it.Display the results.
The following code shows how you might build the button's event handler:
// Find and display the minimum, maximum, and average of the values.
private void calculateButton_Click(object sender, EventArgs e)
{
// Get the values.
string[] textValues = valuesTextBox.Text.Split();
double[] values = new double[textValues.Length];
for (int i = 0; i < textValues.Length; i++)
{
values[i] = double.Parse(textValues[i]);
}
// Calculate.
double smallest, largest, average;
FindMinimumMaximumAverage(values,
out smallest, out largest, out average);
// Display the results.
minimumTextBox.Text = smallest.ToString();
maximumTextBox.Text = largest.ToString();
averageTextBox.Text = average.ToString("0.00");
}
0! = 1
N! = N * (N-1)!
Hint: Be sure to check the stopping condition N = 0 so the method doesn't call itself forever. (Also note that recursive methods can be very confusing to understand and debug so often it's better to write the method without recursion if possible. Some problems have natural recursive definitions, but usually a non-recursive method is better.)
Fibonacci(0) = 0
Fibonacci(1) = 1
Fibonacci(N) = Fibonacci(N - 1) + Fibonacci(N - 2)
Compare the performance of the recursive factorial and Fibonacci methods when N is around 30 or 40.
IsDataSafe
. The IsDataSafe
method should perform the same checks as before and return true
if it is safe to continue with whatever operation the user is about to perform (new file, open file, or exit).
Other code that needs to decide whether to continue should call IsDataSafe
. For example, the fileNewMenuItem_Click
event handler can now look like this:
private void fileNewMenuItem_Click(object sender, EventArgs e)
{
// See if it's safe to continue.
if (IsDataSafe())
{
// Make the new document.
contentRichTextBox.Clear();
// There are no unsaved changes now.
contentRichTextBox.Modified = false;
}
}
MoveUfo
.MoveLaserBolt
.BoltHitUfo
method that returns true
if the laser bolt hits the UFO with a particular index (passed into the method as a parameter). Use that method in the MoveLaserBolt
method.RemoveLaserBolt
method. Modify the program to call RemoveLaserBolt
in two places: if the laser bolt hits a UFO and if the laser bolt moves off the top of the form.GameOver
method.ShowHighScores
method. The program should call this method in two places: once in the GameOver
method if the user has a new high score and once if the user selects the File menu's High Scores command.UpdateHighScores
method.RandomizeUfo
method.
Usually it's better to start with a solid design in mind and write methods as you need them rather than refactor an older program as was done in the last several exercises, but at this point the UFO shooting gallery should have no big chunks of duplicated code and no methods that are so long they are hard to understand. It should be much easier to maintain and improve in the future.
Many of the other examples and exercises shown in earlier lessons also contain duplicated code. For further practice, rewrite some of them to move the duplicated code into methods.