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.
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 Refactor→Extract 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.