Methods

As we saw earlier, a method is a named block of code. We wrote a method already—the Main method that runs when our program starts. And we used methods provided by the .NET Framework class library, such as Console.WriteLine and File.ReadAllLines. But we haven’t looked at how and why you would introduce new methods other than Main into your own code.

Methods are an essential mechanism for reducing your code’s complexity and enhancing its readability. By putting a section of code into its own method with a carefully chosen name that describes what the method does, you can make it much easier for someone looking at the code to work out what your program is meant to do. Also, methods can help avoid repetition—if you need to do similar work in multiple places, a method can help you reuse code.

In our race car example, there’s a job we may need to do multiple times: reading in numeric values from a file. We did this for timing information, but we’re going to need to do the same with fuel consumption and distance. Rather than writing three almost identical bits of code, we can put the majority of the code into a single method.

The first thing we need to do is declare the method—we need to pick a name, define the information that comes into the method, and optionally define the information that comes back out. Let’s call the method ReadNumbersFromFile, since that’s what it’s going to do. Its input will be a text string containing the filename, and it will return an array of double-precision floating-point numbers. The method declaration, which will go inside our Program class, will look like this:

static double[] ReadNumbersFromFile(string fileName)

As you may recall from the discussion of Main earlier, the static keyword indicates that we do not need an instance of the containing Program type to be created for this method to run. (We’ll be looking at nonstatic methods in the next chapter when we start dealing with objects.) C# follows the C-family convention that the kind of data coming out of the method is specified before the name and the inputs, so next we have double[], indicating that this method returns an array of numbers. Then we have the name, and then in parentheses, the inputs required by this method. In this example there’s just one, the filename, but this would be a comma-separated list if more inputs were required.

After the method declaration comes the method body—the statements that make up the method, enclosed in braces. The code isn’t going to be quite the same as what we’ve seen so far—up until now, we’ve converted the text to numbers one at a time immediately before processing them. But this code is going to return an array of numbers, just like File.ReadAllLines returns an array of strings. So our code needs to build up that array. Example 2-17 shows one way of doing this.

Example 2-17. A method for reading numbers from a file

static double[] ReadNumbersFromFile(string fileName)
{
    List<double> numbers = new List<double>();
    using (StreamReader file = File.OpenText(fileName))
    {
        while (!file.EndOfStream)
        {
            string line = file.ReadLine();
            // Skip blank lines
            if (!string.IsNullOrEmpty(line))
            {
                numbers.Add(double.Parse(line));
            }
        }
    }
    return numbers.ToArray();
}

This looks pretty similar to the example while loop we saw earlier, with one addition: we’re creating an object that lets us build up a collection of numbers one at a time—a List<double>. It’s similar to an array (a double[]), but an array needs you to know how many items you want up front—you can’t add more items onto an existing array. The advantage of a List<double> is that you can just keep adding new numbers at will. That matters here because if you look closely you’ll see we’ve modified the code to skip over blank lines, which means that we actually don’t know how many numbers we’re going to get until we’ve read the whole file.

Once you’re done adding numbers to a list, you can call its ToArray() method to get an array of the correct size. This list class is an example of a collection class. .NET offers several of these, and they are so extremely useful that Chapters 7, 8, and 9 are related to working with collections.

Notice the return keyword near the end of Example 2-17. This is how we return the information calculated by our method to whatever code calls the method. As well as specifying the value to return, the return keyword causes the current method to exit immediately, and for execution to continue back in the calling method. (In methods with a void return type, which do not return any value, you can use the return keyword without an argument to exit the method. Or you can just let execution run to the end of the method, and it will return implicitly.) If you’re wondering how the method remembers where it’s supposed to go back to, see the sidebar on the next page.

With the ReadNumbersFromFile method in place, we can now write this sort of code:

double[] lapTimes = ReadNumbersFromFile("LapTimes.txt");
double[] fuelLevels = ReadNumbersFromFile("FuelRemainingByLap.txt");

It doesn’t take a lot of effort to understand that this code is reading in numbers for lap times and fuel levels from a couple of text files—the code makes this aspect of its behavior much clearer than, say, Example 2-12. When code does what it says it does, you make life much easier for anyone who has to look at the code after you’ve written it. And since that probably includes you, you’ll make your life easier in the long run by moving functionality into carefully named methods.

Note

This idea of moving code out of the middle of one method and into a separate method is very common, and is an example of refactoring. Generally speaking, refactoring means restructuring code without changing its behavior, to either simplify it, make it easier to understand and maintain, or avoid duplication. There are so many ways to refactor code that whole books have been written on the topic, but this particular refactoring operation is so useful that Visual Studio can automate it. If you select some code and then right-click on the C# editor window, it offers a RefactorExtract Method menu item that does this for you. In practice, it’s not always that straightforward—you might need to restructure the code a little first, before you’re in a position to factor out the pieces you’d like to move into a method. Example 2-17 had to work slightly differently from any of the previous examples to package the code into a reusable method. But while it may require some work, it’s a useful technique to apply.