Parameters and Methods
From what you have learned about C# programming so far, you should be able to write straightforward programs consisting of a list of statements, similar to the way programs were created in the 1970s. Programming has come a long way since the 1970s, however; as programs have become more complex, new paradigms have emerged to manage that complexity. Procedural or structured programming provides constructs by which statements are grouped together to form units. Furthermore, with structured programming, it is possible to pass data to a group of statements and then have data returned once the statements have executed.
Besides the basics of calling and defining methods, this chapter covers some slightly more advanced concepts—namely, recursion, method overloading, optional parameters, and named arguments. All method calls discussed so far and through the end of this chapter are static (a concept that Chapter 6 explores in detail).
Even as early as the HelloWorld program in Chapter 1, you learned how to define a method. In that example, you defined the Main() method. In this chapter, you will learn about method creation in more detail, including the special C# syntaxes (ref and out) for parameters that pass variables rather than values to methods. Lastly, we will touch on some rudimentary error handling.
Up to this point, almost all of the statements in the programs you have written have appeared together in a list without any grouping outside of what the statement or code block provided. When programs become any more complex, this approach quickly becomes difficult to maintain and complicated to read through and understand.
A method is a means of grouping together sequence a of statements to perform a particular action or compute a particular result. This provides greater structure and organization for the statements that compose a program. Consider, for example, a Main() method that counts the lines of source code in a directory. Instead of having one large Main() method, you can provide a shorter version that allows you to hone in on the details of each method implementation as necessary. Listing 5.1 shows an example.
Instead of placing all of the statements into Main(), the listing breaks them into groups called methods. The System.Console.WriteLine() statements that display the help text have been moved to the DisplayHelpText() method. All of the statements used to determine which files to count appear in the GetFiles() method. To actually count the lines, the code calls the CountLines() method before displaying the results using the DisplayLineCount() method. With a quick glance, it is easy to review the code and gain an overview, because the method name describes the purpose of the method.
Functions are virtually identical to methods, used for grouping together a sequence of statements to perform a particular action or compute a particular result. In fact, frequently, method and function are used interchangably. Technically, however, methods are always associated with a type—such as Program in Listing 5.1. In contrast, functions have no such association. A function doesn’t necessarily have an owning type.
A method provides a means of grouping related code together. Methods can receive data via arguments that are passed to the method parameters. Parameters are variables used for passing data from the caller (the code containing the method call or invocation) to the called or invoked method (Write(), WriteLine(), GetFiles(), CountLines(), and so on). In Listing 5.1, files and lineCount are examples of arguments passed to the CountLines() and DisplayLineCount() methods via their parameters. Methods can also return data to the caller via a return value (in Listing 5.1, the GetFiles() method call has a return value that is assigned to files).
To begin, we reexamine System.Console.Write(), System.Console.WriteLine(), and System.Console.ReadLine() from Chapter 1. This time we look at them as examples of method calls in general instead of looking at the specifics of printing and retrieving data from the console. Listing 5.2 shows each of the three methods in use.
The parts of the method call include the method name, argument list, and returned value. A fully qualified method name includes a namespace, type name, and method name; a period separates each part of a fully qualified method name. As we saw in type names in Chapter 2, methods may also be called with only the last part of their fully qualified name.
Namespaces are a categorization mechanism for grouping all types related to a particular area of functionality. Namespaces are hierarchical and can have arbitrarily many levels in the hierarchy, though namespaces with more than half a dozen levels are rare. Typically, the hierarchy begins with a company name, and then a product name, and then the functional area. For example, in Microsoft.Win32.Networking, the outermost namespace is Microsoft, which contains an inner namespace Win32, which in turn contains an even more deeply nested Networking namespace.
Namespaces are primarily used to organize types by area of functionality so that they can be more easily found and understood. However, they can also be used to avoid type name collisions. For example, the compiler can distinguish between two types with the name Button as long as each type has a different namespace. Thus you can disambiguate types System.Web.UI.WebControls.Button and System.Windows.Controls.Button.
In Listing 5.2, the Console type is found within the System namespace. The System namespace contains the types that enable the programmer to perform many fundamental programming activities. Almost all C# programs use types within the System namespace. Table 5.1 provides a listing of other common namespaces.
Namespace |
Description |
System |
Contains the fundamental types and types for conversion between types, mathematics, program invocation, and environment management. |
System.Collections.Generics |
Contains strongly typed collections that use generics. |
System.Data.Entity |
A framework designed to store and retrieve data from a relational database through code generation that maps entities into tables. |
System.Drawing |
Contains types for drawing to the display device and working with images. Note, however, it is mostly Windows specific. |
System.IO |
Contains types for working with directories and manipulating, loading, and saving files. |
System.Linq |
Contains classes and interfaces for querying data in collections using a Language Integrated Query. |
System.Text |
Contains types for working with strings and various text encodings and for converting between those encodings. |
System.Text.RegularExpressions |
Contains types for working with regular expressions. |
System.Threading |
Contains types for multithreaded programming. |
System.Threading.Tasks |
Contains types for task-based asynchrony. |
System.Windows |
Contains types for creating rich user interfaces when working with UI technologies such as Windows Presentation Foundation (WPF), or Windows UI Library (WinUI), leveraging Extensible Application Markup Language (XAML) for declarative design of the UI. |
System.Xml.Linq |
Contains standards-based support for XML processing using an object query language called LINQ (See Chapter 15). |
It is not always necessary to provide the namespace when calling a method. For example, if the call expression appears in a type in the same namespace as the called method, the compiler can infer the namespace to be the namespace that contains the type. Later in this chapter, you will see how the using directive eliminates the need for a namespace qualifier as well.
Calls to static methods require the type name qualifier as long as the target method is not within the same type.1 (As discussed later in the chapter, a using static directive allows you to omit the type name.) For example, a call expression of Console.WriteLine() found in the method HelloWorld.Main() requires the type, Console, to be specified. However, just as with the namespace, C# allows the omission of the type name from a method call whenever the method is a member of the type containing the call expression. (Examples of method calls such as this appear in Listing 5.4.) The type name is unnecessary in such cases because the compiler infers the type from the location of the call. If the compiler can make no such inference, the name must be provided as part of the method call.
At their core, types are a means of grouping together methods and their associated data. For example, Console is the type that contains the Write(), WriteLine(), and ReadLine() methods (among others). All of these methods are in the same group because they belong to the Console type.
In Chapter 4, you learned that the scope of a program element is the region of text in which it can be referred to by its unqualified name. A call that appears inside a type declaration to a method declared in that type does not require the type qualifier because the method is in scope throughout its containing type. Similarly, a type is in scope throughout the namespace that declares it; therefore, a method call that appears in a type in a particular namespace need not specify that namespace in the method call name.
Every method call contains a method name, which might or might not be qualified with a namespace and type name, as we have discussed. After the method name comes the argument list, which is a parenthesized, comma-separated list of the values that correspond to the parameters of the method.
A method can take any number of parameters, and each parameter is of a specific data type. The values that the caller supplies for parameters are called the arguments; every argument must correspond to a particular parameter. For example, the following method call has three arguments:
System.IO.File.Copy(
oldFileName, newFileName, false)
The method is found on the class File, which is in the namespace System.IO. It is declared to have three parameters, with the first and second being of type string and the third being of type bool. In this example, we use variables (oldFileName and newFileName) of type string for the old and new filenames, and then specify false to indicate that the copy should fail if the new filename already exists.
In contrast to Console.WriteLine(), the method call Console.ReadLine() in Listing 5.2 does not have any arguments because the method is declared to take no parameters. However, this method happens to have a method return value. The method return value is a means of transferring results from a called method back to the caller. Because Console.ReadLine() has a return value, it is possible to assign the return value to the variable firstName. In addition, it is possible to pass this method return value itself as an argument to another method call, as shown in Listing 5.3.
Instead of assigning the returned value to a variable and then using that variable as an argument to the call to Console.WriteLine(), Listing 5.3 calls the Console.ReadLine() method within the call to Console.WriteLine(). At execution time, the Console.ReadLine() method executes first, and its return value is passed directly into the Console.WriteLine() method, rather than into a variable.
Not all methods return data. Both versions of Console.Write() and Console.WriteLine() are examples of such methods. As you will see shortly, these methods specify a return type of void, just as the HelloWorld declaration of Main returned void.
Listing 5.3 provides a demonstration of the difference between a statement and a method call. Although Console.WriteLine($"Hello { System.Console.ReadLine()}!"); is a single statement, it contains two method calls. A statement often contains one or more expressions, and in this example, two of those expressions are method calls. Therefore, method calls form parts of statements.
Although coding multiple method calls in a single statement often reduces the amount of code, it does not necessarily increase the readability and seldom offers a significant performance advantage. Developers should favor readability over brevity.
________________________________________