2. Defining Methods

EVERY METHOD IN C# MUST BE DEFINED inside some class. This makes C# rather unusual among programming languages, since most languages allow free-floating, independent methods. One purpose of a class is to group together related methods and variables. Perhaps the designers of C# felt that everything must be related to something. As a less philosophical motivation, C#'s designers wanted to place firm controls on the ways things are named, since a C# program potentially has access to a huge number of methods scattered all over the Internet. The fact that those methods are grouped into named classes (and classes are grouped into named "packages") helps control the confusion that might result from so many different names.

This chapter will deal with static methods almost exclusively. We'll turn to non-static methods and object-oriented programming in the next chapter.

2.1. Method Definition Syntax

A method definition in C# takes the form:

      modifiers  return-type  method-name  ( parameter-list ) {
          statements
      }

It will take us a while -- most of the chapter -- to get through what all this means in detail. Of course, you've already seen examples of methods in previous chapters, such as the Main() routine of a program. So you are familiar with the general format. Let's do a brief overview of the parts of a method definition.

The statements between the braces, { and }, make up the body of the method. These statements are the inside, or implementation part, of the "black box", as discussed in the previous section. They are the instructions that the computer executes when the method is called. Methods can contain any statement.

The modifiers that can occur at the beginning of a method definition are words that set certain characteristics of the method, such as whether it is static or not. The modifiers that you've seen so far are "static" and "public". There are only about a half-dozen possible modifiers altogether.

If the method is a function whose job is to compute some value, then the return-type is used to specify the type of value that is returned by the function. We'll be looking at functions and return types in some detail in Section 4. If the method is not a function, then the return-type is replaced by the special value void, which indicates that no value is returned. The term "void" is meant to indicate that the return value is empty or non-existent.

Finally, we come to the parameter-list of the method. Parameters are part of the interface of a method. They represent information that is passed into the method from outside, to be used by the method's internal computations. For a concrete example, imagine a class named Television that includes a method named changeChannel(). The immediate question is: What channel should it change to? A parameter can be used to answer this question. Since the channel number is an integer, the type of the parameter would be int, and the declaration of the changeChannel() method might look like

public void changeChannel(int channelNum) {...}

This declaration specifies that changeChannel() has a parameter named channelNum of type int. However, channelNum does not yet have any particular value. A value for channelNum is provided when the method is called; for example: changeChannel(17);

The parameter list in a method can be empty, or it can consist of one or more parameter declarations of the form:

type parameter-name

If there are several declarations, they are separated by commas. Note that each declaration can name only one parameter. For example, if you want two parameters of type double, you have to say "double x, double y", rather than "double x, y".

Parameters are covered in more detail in the next section.

Here are a few examples of method definitions, leaving out the statements that define what the methods do:

public static void PlayGame() {
   // "public" and "static" are modifiers; "void" is the 
   // return-type; "playGame" is the method-name; 
   // the parameter-list is empty
   . . .  // statements that define what playGame does go here
}

int GetNextN(int N) {
   // there are no modifiers; "int" in the return-type
   // "getNextN" is the method-name; the parameter-list 
   // includes one parameter whose name is "N" and whose 
   // type is "int"
   . . .  // statements that define what getNextN does go here
}

static bool LessThan(double x, double y) {
   // "static" is a modifier; "bool" is the
   // return-type; ":essThan" is the method-name; the 
   // parameter-list includes two parameters whose names are 
   // "x" and "y", and the type of each of these parameters 
   // is "double"
   . . .  // statements that define what lessThan does go here
}

Remember as you look at these examples that all of these method definitions would have to appear inside a class definition. Also, note that the second example given here, GetNextN, is a non-static method, since its definition does not include the modifier "static" -- and so it's not an example that we should be looking at in this chapter!

Note, by the way, that the Main() routine of a program follows the usual syntax rules for a method. In

static void Main() { .... }

the modifier is static, the return type is void, the method name is Main, and the parameter list is empty.

You've already had some experience with filling in the statements of the Main() method. In this chapter, you'll learn all about writing your own complete method definitions, including the interface part.

2.2. Organizing a Program in Methods

Have a look through the following simple program:

class MethodDemo {
  static void Main() {
    
    Console.WriteLine(
      "Menu\n" +
      "----\n" +
      "1) Stomp\n" +
      "2) Meow\n"
    );
    
    Console.Write("Enter your choice: ");
    int choice = Convert.ToInt32(Console.ReadLine());
    
    if (choice == 1) {
      Console.WriteLine("The elephant stomped.");
    } else {
      Console.WriteLine("The cat meowed.");
    }
  }
} // class MethodDemo

The menu of choices this program displays is fairly short. Despite its brevity, it occupies several lines of code. Let's remove that code to a separate method, like this:

class MethodDemo2 {

  static void ShowMenu() {
    Console.WriteLine(
      "Menu\n" +
      "----\n" +
      "1) Stomp\n" +
      "2) Meow\n"
    );
  }

  static void Main() {

    ShowMenu();
    
    Console.Write("Enter your choice: ");
    int choice = Convert.ToInt32(Console.ReadLine());
    
    if (choice == 1) {
      Console.WriteLine("The elephant stomped.");
    } else {
      Console.WriteLine("The cat meowed.");
    }
  }
} // class MethodDemo

I've highlighted the lines that changed. Notice the differences:

  • The code to print the menu was removed from the Main method and replaced by a ShowMenu() method call

  • A new method named ShowMenu was created

I want to emphasize that simply removing the menu printing code from the Main method and putting it in a different method would not have worked. Adding the ShowMenu() method call is required. Here's why. When you run a C# program, the computer looks for the Main method, and executes only the statements contained in that method. Other methods defined in the program don't get executed unless the Main method invokes them with a method call statement like ShowMenu().

On a related note, the order in which you define the methods doesn't matter. In this example, the definition of ShowMenu comes before the definition of Main, but I could have defined Main before ShowMenu.

Finally, focus on the code in the Main method. By replacing the messy details of displaying the menu with a one-line ShowMenu() method call, I have applied the concept of abstraction (hiding detail to manage complexity). As you read through the code in Main(), your eye isn't distracted with the details of the code that displays the menu; instead, the steps the main program is performing are a little clearer because the name ShowMenu clearly conveys what happens at that point. The details of ShowMenu aren't actually hidden, of course; but you can ignore them more easily as you focus on the main program flow since they've been moved to a location outside the Main method.

[Tip]Tip

One of the keys to making abstraction work is inventing good method names. If you're going to replace a bunch of detail with a method call so that the person reading the program understands what happens at the point of the call, the name of the method needs to convey clearly what it does. Since methods do things, the best names are usually short verb phrases (ex. "print", "readLine", "ShowMenu").

Now, take another look at the ShowMenu() method call. Earlier in this book, I taught you that when you call a static method, you must prefix the name of the method with the name of its class, like this:

MethodDemo2.ShowMenu();

Since the ShowMenu() method is defined in the same class as Main, the method call doesn't have to specify the class name.

2.3. Putting Methods to Work

Let's write a program that plays a guessing game with the user. The computer will choose a random number between 1 and 100, and the user will try to guess it. The computer tells the user whether the guess is high or low or correct. If the user gets the number after six guesses or fewer, the user wins the game. After each game, the user has the option of continuing with another game.

Since playing one game can be thought of as a single, coherent task, it makes sense to write a method that will play one guessing game with the user. The Main() routine will use a loop to call the playGame() method over and over, as many times as the user wants to play. We approach the problem of designing the playGame() method the same way we write a Main() routine: Start with an outline of the algorithm and apply stepwise refinement. Here is a short pseudocode algorithm for a guessing game program:

      Pick a random number
      while the game is not over:
          Get the user's guess
          Tell the user whether the guess is high, low, or correct.

The test for whether the game is over is complicated, since the game ends if either the user makes a correct guess or the number of guesses is six. As in many cases, the easiest thing to do is to use a boolean flag variable. Also, if we are going to end the game after six guesses, we'll have to keep track of the number of guesses that the user has made. Filling out the algorithm gives:

       Let computersNumber be a random number between 1 and 100
       Let guessCount = 0
       Let done = false
       while (done == false):
           Get the user's guess
           Count the guess by adding 1 to guess count
           if the user's guess equals computersNumber:
               Tell the user he won
               Let done = true
           else if the number of guesses is 6:
               Tell the user he lost
               Let done = true
           else give the user a hint

With variable declarations added and translated into C#, this becomes the definition of the playGame() routine. A random integer between 1 and 100 can be computed as (int)(100 * Math.random()) + 1. I've cleaned up the interaction with the user to make it flow better.

It's pretty easy to write the Main routine. You've done things like this before. Here's what the complete program looks like (except that a serious program needs more comments than I've included here).

using System;

class GuessingGame {
  
  static void Main() {
    Console.WriteLine("Let's play a game.  I'll pick a number between");
    Console.WriteLine("1 and 100, and you try to guess it.");
    
    string answer;
    do {
      PlayGame();  // call method to play one game
      Console.Write("Would you like to play again? (y/n) ");
      answer = Console.ReadLine();      
    } while (answer == "y");
    Console.WriteLine("Thanks for playing.  Goodbye.");
  } // Main()            
  
  static void PlayGame() {
    int guessCount = 0;    // Number of guesses the user has made.
    Random generator = new Random();
    int computersNumber = generator.Next(1, 101);
             // The value assigned to computersNumber is a randomly
             //    chosen integer between 1 and 100, inclusive.
  
    Console.WriteLine();
    Console.Write("What is your first guess? ");
    bool done = false;
    while (done == false) {
       // get the user's guess
       int usersGuess = Convert.ToInt32(Console.ReadLine());  
       guessCount++;
       if (usersGuess == computersNumber) {
          Console.WriteLine("You got it in " + guessCount
                  + " guesses!  My number was " + 
                  computersNumber);
          done = true;  // the game is over; the user has won
       } else if (guessCount == 6) {
          Console.WriteLine("You didn't get the number in 6 guesses.");
          Console.WriteLine("You lose.  My number was " + computersNumber);
          done = true;  // the game is over; the user has lost
       } else {
         // If we get to this point, the game continues.
         // Tell the user if the guess was too high or too low.
         if (usersGuess < computersNumber) {
            Console.Write("That's too low.  Try again: ");
         } else if (usersGuess > computersNumber) {
            Console.Write("That's too high.  Try again: ");
         }
       }
    }
    Console.WriteLine();
  } // PlayGame()
            
} // class GuessingGame

Take some time to read the program carefully and figure out how it works. And try to convince yourself that even in this relatively simple case, breaking up the program into two methods makes the program easier to understand.