Reusing Code with Methods - Program Statements - C# 24-Hour Trainer (2015)

C# 24-Hour Trainer (2015)

Section III

Program Statements

Lesson 20

Reusing Code with Methods

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.

Screenshot of Ufo window with three UFOs atop and six rectangular objects at the bottom left corner of the window. Another rectangular object is above an inverted T-shaped object.

Figure 20.1

When a laser bolt hits a UFO, the program takes these steps:

1. Plays the “hit a UFO” sound effect.

2. Increases the player's score and shows it in the score Label.

3. Hides the laser bolt PictureBox.

4. Sets BoltIsAway = false to remember that no laser bolt is currently on the form.

5. If that was the player's last laser bolt:

a. Plays the “game over” sound effect.

b. Displays the “game over” label showing the player's final score.

c. Disables the Timer.

d. If the player's score is greater than the smallest high score:

i. Creates a NewHighScoreForm.

ii. Places the player's score on the NewHighScoreForm.

iii. Displays the NewHighScoreForm.

iv. If the user enters a name and clicks OK:

1. Replaces the lowest high score with the player's current score.

2. Sorts the high scores.

3. Creates a new HighScoreForm.

4. Places the high scores on the HighScoreForm.

5. Displays the 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.

NOTE

In programming languages other than C#, methods are sometimes known as routines, subroutines, procedures, subprocedures, subs, or functions (particularly when the method returns a value).

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.

Method Advantages

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:

· Code reuse—You write the code once and use it many times.

· Centralized debugging—You only need to debug the shared code once.

· Centralized maintenance—If you need to fix the code, you only need to do so in the method, not everywhere it is used.

· Problem decomposition—Methods can break complex problems into simpler pieces.

· Encapsulation—The method can hide complex details from developers.

Method Syntax

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 (}).

NOTE

You can use the return statement as many times as you like in a method. For example, some of the branches in an if-else sequence could lead to return statements.

If the method has a non-void return type, the C# compiler tries to guarantee that all paths through the code end at a return statement and will warn you if the code might not return a value.

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.

Using ref Parameters

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 strings 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.

Using out Parameters

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.

NOTE

In general it's considered good practice to avoid returning results through parameters passed by reference because it can be confusing. It's better to use output parameters if possible.

An even better approach is to pass the method inputs through parameters and make the method return all of its return values with the return statement. For instance, the chess matchup example could return a structure or instance of a class that contains the names of the two players.

Try It

In this Try It, you make a method that calculates the minimum, maximum, and average values for an array of doubles. You build the program shown in Figure 20.2 to test the method.

Screenshot of MinMaxAverage window displaying 19 20 29 9 37 8 21 in the Values textbox; the Calculate button is below. 8, 37, and 20.43 are in Minimum, Maximum, and Average textboxes, respectively.

Figure 20.2

Lesson Requirements

In this lesson, you:

1. Build the program shown in Figure 20.2.

2. Build a method that takes four parameters: an array of doubles, and three more return doubles. It should loop through the array to find the minimum and maximum and to calculate the average.

3. Write code to test the method.

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 method needs to use the return parameters. Should they be declared ref or out?

Step-by-Step

· Build the program shown in Figure 20.2.

1. This is reasonably straightforward.

· Build a method that takes four parameters: an array of doubles, and three more return doubles. It should loop through the array to find the minimum and maximum and to calculate the average.

1. This method calculates its results purely by examining the values in the input array so it doesn't need to use whatever values are passed in through its other parameters. That means the minimum, maximum, and average parameters should use the out keyword instead of the ref keyword.

2. Initialize the minimum and maximum variables to the first entry in the array.

3. Initialize a total variable to the first entry in the array.

4. Loop through the rest of the array (skipping the first entry because it has already been considered), updating the minimum and maximum variables as needed and adding the values in the array to the total.

5. After finishing the loop, divide the total by the number of values to get the average.

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;

}

· Write code to test the method.

1. When the user clicks the button, take the TextBox's text and use its Split method to break the user's values into an array of strings.

2. Make a double array and use a for loop to parse the text values into it.

3. Call the method to calculate the necessary results.

4. 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");

}

NOTE

This lesson mentions that returning values through parameters passed by reference isn't a good practice. So how could you modify this example to avoid that?

You could break the FindMinimumMaximumAverage method into three separate methods: FindMinimum, FindMaximum, and FindAverage. Then each method could return its result via a return statement. In addition to avoiding parameters passed by reference, that makes each routine perform a single well-focused task so it makes them easier to understand and use. It also makes them easier to use separately in case you only wanted to find the array's minimum and not the maximum or average.

(Also note that arrays provide methods that can find these values for you, so you really don't need to write these functions anyway. They're here purely to demonstrate parameters passed by reference.)

Exercises

1. Make a program that calculates the least common multiple (LCM) of two integers. (The LCM of two integers is the smallest integer that the two numbers divide into evenly.) Hints: LCM(a, b) = a * b / GCD(a, b). Also don't write the LCM method from scratch. Instead, make it call the GCD method described earlier in this lesson.

2. A recursive method is one that calls itself. Write a recursive factorial method by using the definition:

3. 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.)

4. Write a program that recursively calculates the Nth Fibonacci number using the definition:

5. Fibonacci(0) = 0

6. 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.

7. [SimpleEdit] Copy the SimpleEdit program you built for Exercise 18-3 (or download the version on the book's website) and move the code that checks for unsaved changes into a method named 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;

}

}

8. [Games] Copy the program you wrote for Exercise 19-12 (or download the version on the book's website) and extract the code that moves a UFO into a new method. To do that:

· Select the code that moves a UFO.

· Right-click the code and select Quick Actions.

· Click Extract Method.

· Change the new method's name to MoveUfo.

9. [Games] Copy the program you wrote for Exercise 5 and extract the code that moves the laser bolt into a new method named MoveLaserBolt.

10.[Games] Copy the program you wrote for Exercise 6 and write a 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.

11.[Games] Copy the program you wrote for Exercise 7. Find the code that removes a laser bolt, determines whether the game is over, and updates the high scores if necessary. Extract that code into a new RemoveLaserBolt method. Modify the program to callRemoveLaserBolt in two places: if the laser bolt hits a UFO and if the laser bolt moves off the top of the form.

12.[Games] Copy the program you wrote for Exercise 8. Find the code that executes when the game is over. (It plays the “game over” sound effect and updates and displays the high scores if necessary.) Extract that code into a new GameOver method.

13.[Games] Copy the program you wrote for Exercise 9 and extract the code that displays the high score form into a new 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.

14.[Games] Copy the program you wrote for Exercise 10. Find the code that determines whether the user got a new high score and, if so, updates and displays the high scores. Extract that code into a new UpdateHighScores method.

15.[Games] Copy the program you wrote for Exercise 11 and extract the code that randomizes a UFO into a new 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.

NOTE

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