E pluribus unum. (One composed of many.) | ||
--Virgil |
O! call back yesterday, bid time return. | ||
--William Shakespeare |
Call me Ishmael. | ||
--Herman Melville |
Answer me in one word. | ||
--William Shakespeare |
There is a point at which methods devour themselves. | ||
--Frantz Fanon |
In this chapter you’ll learn:
<objective>How static
methods and variables are associated with classes rather than objects.
How the method call/return mechanism is supported by the method-call stack.
</objective> <objective>How to use random-number generation to implement game-playing applications.
</objective> <objective>How the visibility of declarations is limited to specific parts of applications.
</objective> <objective>How to create overloaded methods.
</objective> <objective>How to use optional and named parameters.
</objective> <objective>What recursive methods are.
</objective> <objective>Passing method arguments by value and by reference.
</objective> </feature><feature> <supertitle>Outline</supertitle> </feature>Most computer applications that solve real-world problems are much larger than the applications presented in this book’s first few chapters. Experience has shown that the best way to develop and maintain a large application is to construct it from small, simple pieces. This technique is called divide and conquer. We introduced methods in Chapter 4. In this chapter we study methods in more depth. We emphasize how to declare and use methods to facilitate the design, implementation, operation and maintenance of large applications.
You’ll see that it’s possible for certain methods, called static
methods, to be called without the need for an object of the class to exist. You’ll learn how to declare a method with more than one parameter. You’ll also learn how C# is able to keep track of which method is currently executing, how value-type and reference-type arguments are passed to methods, how local variables of methods are maintained in memory and how a method knows where to return after it completes execution.
We discuss simulation techniques with random-number generation and develop a version of the casino dice game called craps that uses most of the programming techniques you’ve learned to this point in the book. In addition, you’ll learn to declare values that cannot change (i.e., constants). You’ll also learn to write methods that call themselves—this is called recursion.
Many of the classes you’ll use or create while developing applications will have more than one method of the same name. This technique, called method overloading, is used to implement methods that perform similar tasks but with different types and/or different numbers of arguments.
Common ways of packaging code are properties, methods, classes and namespaces. C# applications are written by combining new properties, methods and classes that you write with predefined properties, methods and classes available in the .NET Framework Class Library and in various other class libraries. Related classes are often grouped into namespaces and compiled into class libraries so that they can be reused in other applications. You’ll learn how to create your own namespaces and class libraries in Chapter 15. The Framework Class Library provides many predefined classes that contain methods for performing common mathematical calculations, string manipulations, character manipulations, input/output operations, database operations, networking operations, file processing, error checking, web-application development and more.
Familiarize yourself with the classes and methods provided by the Framework Class Library (msdn.microsoft.com/en-us/library/ms229335.aspx).
Don’t try to “reinvent the wheel.” When possible, reuse Framework Class Library classes and methods. This reduces application development time, avoids introducing programming errors and contributes to good application performance.
Methods (called functions or procedures in other programming languages) allow you to modularize an application by separating its tasks into self-contained units. You’ve declared methods in every application you’ve written. These methods are sometimes referred to as user-defined methods. The actual statements in the method bodies are written only once, can be reused from several locations in an application and are hidden from other methods.
There are several motivations for modularizing an application by means of methods. One is the “divide-and-conquer” approach, which makes application development more manageable by constructing applications from small, simple pieces. Another is software reusability—existing methods can be used as building blocks to create new applications. Often, you can create applications mostly by reusing existing methods rather than by building customized code. For example, in earlier applications, we did not have to define how to read data values from the keyboard—the Framework Class Library provides these capabilities in class Console
. A third motivation is to avoid repeating code. Dividing an application into meaningful methods makes the application easier to debug and maintain.
To promote software reusability, every method should be limited to performing a single, well-defined task, and the name of the method should express that task effectively. Such methods make applications easier to write, debug, maintain and modify.
If you cannot choose a concise name that expresses a method’s task, your method might be attempting to perform too many diverse tasks. It’s usually best to break such a method into several smaller methods.
As you know, a method is invoked by a method call, and when the called method completes its task, it returns a result or simply returns control to the caller. The code that calls a method is also sometimes known as the client code—as it’s a client of the method. An analogy to the method-call-and-return structure is the hierarchical form of management (Figure 7.1). A boss (the caller) asks a worker (the called method) to perform a task and report back (i.e., return) the results after completing the task. The boss method does not know how the worker method performs its designated tasks. The worker may also call other worker methods, unbeknown to the boss. This “hiding” of implementation details promotes good software engineering. Figure 7.1 shows the boss
method communicating with several worker methods in a hierarchical manner. The boss
method divides the responsibilities among the various worker
methods. Note that worker1
acts as a “boss method” to worker4
and worker5
.
Although most methods execute on specific objects in response to method calls, this is not always the case. Sometimes a method performs a task that does not depend on the contents of any object. Such a method applies to the class in which it’s declared as a whole and is known as a static
method. It’s not uncommon for a class to contain a group of static
methods to perform common tasks. For example, recall that we used static
method Pow
of class Math
to raise a value to a power in Fig. 6.6. To declare a method as static
, place the keyword static
before the return type in the method’s declaration. You call any static
method by specifying the name of the class in which the method is declared, followed by the member access (.
) operator and the method name, as in
ClassName.MethodName( arguments ) |
We use various methods of the Math
class here to present the concept of static
methods. Class Math
(from the System
namespace) provides a collection of methods that enable you to perform common mathematical calculations. For example, you can calculate the square root of 900.0
with the static
method call
Math.Sqrt( 900.0 ) |
The preceding expression evaluates to 30.0
. Method Sqrt
takes an argument of type double
and returns a result of type double
. To output the value of the preceding method call in the console window, you might write the statement
Console.WriteLine( Math.Sqrt( 900.0 ) ); |
In this statement, the value that Sqrt
returns becomes the argument to method WriteLine
. We did not create a Math
object before calling method Sqrt
. Also, all of Math
’s methods are static
—therefore, each is called by preceding the name of the method with the class name Math
and the member access (.
) operator. Similarly, Console
method WriteLine
is a static
method of class Console
, so we invoke the method by preceding its name with the class name Console
and the member access (.
) operator.
Method arguments may be constants, variables or expressions. If c = 13.0
, d = 3.0
and f = 4.0
, then the statement
Console.WriteLine( Math.Sqrt( c + d * f ) ); |
calculates and displays the square root of 13.0 + 3.0 * 4.0 = 25.0
—namely, 5.0
. Figure 7.2 summarizes several Math
class methods. In the figure, x and y are of type double
.
Table 7.2. Math
class methods.
Method | Description | Example |
---|---|---|
| absolute value of x |
|
| rounds x to the smallest integer not less than x |
|
| trigonometric cosine of x (x in radians) |
|
| exponential method ex |
|
| rounds x to the largest integer not greater than x |
|
| natural logarithm of x (base e) |
|
| larger value of x and y |
|
| smaller value of x and y |
|
| x raised to the power y (i.e., xy) |
|
| trigonometric sine of x (x in radians) |
|
| square root of x |
|
| trigonometric tangent of x (x in radians) |
|
Class Math
also declares two static
constants that represent commonly used mathematical values: Math.PI
and Math.E
. The constant Math.PI
(3.14159265358979323846) is the ratio of a circle’s circumference to its diameter. The constant Math.E
(2.7182818284590452354) is the base value for natural logarithms (calculated with static Math
method Log
). These constants are declared in class Math
with the modifiers public
and const
. Making them public
allows other programmers to use these variables in their own classes. A constant is declared with the keyword const
—its value cannot be changed after the constant is declared. Both PI
and E
are declared const
because their values never change.
Every constant declared in a class, but not inside a method of the class is implicitly static
, so it’s a syntax error to declare such a constant with keyword static
explicitly.
Because these constants are static
, you can access them via the class name Math
and the member access (.
) operator, just like class Math
’s methods. Recall from Section 4.5 that when each object of a class maintains its own copy of an attribute, the variable that represents the attribute is also known as an instance variable—each object (instance) of the class has a separate instance of the variable. There are also variables for which each object of a class does not have a separate instance of the variable. That’s the case with static
variables. When objects of a class containing static
variables are created, all the objects of that class share one copy of the class’s static
variables. Together the static
variables and instance variables represent the fields of a class.
Why must Main
be declared static
? During application startup, when no objects of the class have been created, the Main
method must be called to begin program execution. The Main
method is sometimes called the application’s entry point. Declaring Main
as static
allows the execution environment to invoke Main
without creating an instance of the class. Method Main
is often declared with the header:
public static void Main( string args[] ) |
When you execute your application from the command line, you type the application name, as in
ApplicationName argument1 argument2 ... |
In the preceding command, argument1 and argument2 are the command-line arguments to the application that specify a list of string
s (separated by spaces) the execution environment will pass to the Main
method of your application. Such arguments might be used to specify options (e.g., a file name) to run the application. As you’ll learn in Chapter 8, Arrays, your application can access those command-line arguments and use them to customize the application.
The header of a Main
method does not need to appear exactly as we’ve shown. Applications that do not take command-line arguments may omit the string[] args
parameter. The public
keyword may also be omitted. In addition, you can declare Main
with return type int
(instead of void
) to enable Main
to return an error code with the return
statement. A Main
method declared with any one of these headers can be used as the application’s entry point—but you can declare only one such Main
method in each class.
In earlier chapters, most applications had one class that contained only Main
, and some examples had a second class that was used by Main
to create and manipulate objects. Actually, any class can contain a Main
method. In fact, each of our two-class examples could have been implemented as one class. For example, in the application in Figs. 6.9 and 6.10, method Main
(lines 6–16 of Fig. 6.10) could have been taken as is and placed in class GradeBook
(Fig. 6.9). The application results would have been identical to those of the two-class version. You can place a Main
method in every class you declare. Some programmers take advantage of this to build a small test application into each class they declare. However, if you declare more than one Main
method among the classes of your project, you’ll need to indicate to the IDE which one you would like to be the application’s entry point. You can do this by selecting Project > [ProjectName] Properties... (where [Project-Name] is the name of your project) and selecting the class containing the Main
method that should be the entry point from the Startup object list box.
Methods often require more than one piece of information to perform their tasks. We now consider how to write your own methods with multiple parameters.
The application in Fig. 7.3 uses a user-defined method called Maximum
to determine and return the largest of three double
values that are input by the user. When the application begins execution, the Main
method (lines 8–22) executes. Lines 11–15 prompt the user to enter three double
values and read them from the user. Line 18 calls method Maximum
(declared in lines 25–38) to determine the largest of the three double
values passed as arguments to the method. When method Maximum
returns the result to line 18, the application assigns Maximum
’s return value to local variable result
. Then line 21 outputs result
. At the end of this section, we’ll discuss the use of operator + in line 21.
Example 7.3. User-defined method Maximum
.
1 // Fig. 7.3: MaximumFinder.cs 2 // User-defined method Maximum. 3 using System; 4 5 public class MaximumFinder 6 { 7 // obtain three floating-point values and determine maximum value 8 public static void Main( string[] args ) 9 { 10 // prompt for and input three floating-point values 11 Console.WriteLine( "Enter three floating-point values, " 12 + " pressing 'Enter' after each one: " ); 13 double number1 = Convert.ToDouble( Console.ReadLine() ); 14 double number2 = Convert.ToDouble( Console.ReadLine() ); 15 double number3 = Convert.ToDouble( Console.ReadLine() ); 16 17 // determine the maximum value 18 double result = Maximum( number1, number2, number3 ); 19 20 // display maximum value 21 Console.WriteLine( "Maximum is: " + result ); 22 } // end Main 23 24 // returns the maximum of its three double parameters 25 public static double Maximum( double x, double y, double z ) 26 { 27 double maximumValue = x; // assume x is the largest to start 28 29 // determine whether y is greater than maximumValue 30 if ( y > maximumValue ) 31 maximumValue = y; 32 33 // determine whether z is greater than maximumValue 34 if ( z > maximumValue ) 35 maximumValue = z; 36 37 return maximumValue; 38 } // end method Maximum 39 } // end class MaximumFinder
Enter three floating-point values, pressing 'Enter' after each one: 2.22 3.33 1.11 Maximum is: 3.33 |
Enter three floating-point values, pressing 'Enter' after each one: 1.11 2.22 867.5309 Maximum is: 867.5309 |
Method Maximum
’s declaration begins with keyword public
to indicate that the method is “available to the public”—it can be called from methods of other classes. The keyword static
enables the Main
method (another static
method) to call Maximum
as shown in line 18 without qualifying the method name with the class name MaximumFinder
—static
methods in the same class can call each other directly. Any other class that uses Maximum
must fully qualify the method name with the class name.
Consider the declaration of method Maximum
(lines 25–38). Line 25 indicates that the method returns a double
value, that the method’s name is Maximum
and that the method requires three double
parameters (x
, y
and z
) to accomplish its task. When a method has more than one parameter, the parameters are specified as a comma-separated list. When Maximum
is called in line 18, the parameter x
is initialized with the value of the argument number1
, the parameter y
is initialized with the value of the argument number2
and the parameter z
is initialized with the value of the argument number3
. There must be one argument in the method call for each required parameter (sometimes called a formal parameter) in the method declaration. Also, each argument must be consistent with the type of the corresponding parameter. For example, a parameter of type double
can receive values like 7.35
(a double
), 22
(an int
) or –0.03456
(a double
), but not string
s like "hello"
. Section 7.7 discusses the argument types that can be provided in a method call for each parameter of a simple type.
To determine the maximum value, we begin with the assumption that parameter x
contains the largest value, so line 27 declares local variable maximumValue
and initializes it with the value of parameter x
. Of course, it’s possible that parameter y
or z
contains the largest value, so we must compare each of these values with maximumValue
. The if
statement at lines 30–31 determines whether y
is greater than maximumValue
. If so, line 31 assigns y
to maximumValue
. The if
statement at lines 34–35 determines whether z
is greater than maximumValue
. If so, line 35 assigns z
to maximumValue
. At this point, the largest of the three values resides in maximumValue
, so line 37 returns that value to line 18. When program control returns to the point in the application where Maximum
was called, Maximum
’s parameters x
, y
and z
are no longer accessible. Methods can return at most one value; the returned value can be a value type that contains many values (implemented as a struct
) or a reference to an object that contains many values.
Variable result
is a local variable in method Main
because it’s declared in the block that represents the method’s body. Variables should be declared as fields of a class (i.e., as either instance variables or static
variables of the class) only if they’re required for use in more than one method of the class or if the application should save their values between calls to the class’s methods.
Recall from Fig. 7.2 that class Math
has a Max
method that can determine the larger of two values. The entire body of our maximum method could also be implemented with nested calls to Math.Max
, as follows:
return Math.Max( x, Math.Max( y, z ) ); |
The leftmost call to Math.Max
specifies arguments x
and Math.Max( y, z )
. Before any method can be called, all its arguments must be evaluated to determine their values. If an argument is a method call, the method call must be performed to determine its return value. So, in the preceding statement, Math.Max( y, z )
is evaluated first to determine the maximum of y
and z
. Then the result is passed as the second argument to the other call to Math.Max
, which returns the larger of its two arguments. Using Math.Max
in this manner is a good example of software reuse—we find the largest of three values by reusing Math.Max
, which finds the larger of two values. Note how concise this code is compared to lines 27–37 of Fig. 7.3.
C# allows string
objects to be created by assembling smaller string
s into larger string
s using operator +
(or the compound assignment operator +=
). This is known as string
concatenation. When both operands of operator +
are string
objects, operator +
creates a new string
object in which a copy of the characters of the right operand is placed at the end of a copy of the characters in the left operand. For example, the expression "hello " + "there"
creates the string "hello there"
without disturbing the original string
s.
In line 21 of Fig. 7.3, the expression "Maximum is: " + result
uses operator +
with operands of types string
and double
. Every value of a simple type in C# has a string
representation. When one of the +
operator’s operands is a string
, the other is implicitly converted to a string
, then the two are concatenated. In line 21, the double
value is implicitly converted to its string
representation and placed at the end of the string "Maximum is: "
. If there are any trailing zeros in a double
value, these will be discarded when the number is converted to a string
. Thus, the number 9.3500 would be represented as 9.35 in the resulting string
.
For values of simple types used in string
concatenation, the values are converted to string
s. If a bool
is concatenated with a string
, the bool
is converted to the string "True"
or "False"
(each is capitalized). All objects have a ToString
method that returns a string
representation of the object. When an object is concatenated with a string
, the object’s ToString
method is implicitly called to obtain the string
representation of the object. If the object is null
, an empty string is written.
Line 21 of Fig. 7.3 could also be written using string
formatting as
Console.WriteLine( "Maximum is: {0}", result ); |
As with string
concatenation, using a format item to substitute an object into a string
implicitly calls the object’s ToString
method to obtain the object’s string
representation. You’ll learn more about method ToString
in Chapter 8.
When a large string
literal is typed into an application’s source code, you can break that string
into several smaller string
s and place them on multiple lines for readability. The string
s can be reassembled using either string concatenation or string formatting. We discuss the details of string
s in Chapter 16.
It’s a syntax error to break a string
literal across multiple lines in an application. If a string
does not fit on one line, split the string
into several smaller string
s and use concatenation to form the desired string
.C# also provides so-called verbatim string
literals, which are preceded by the @
character. Such literals can be split over multiple lines and the characters in the literal are processed exactly as they appear in the literal.
Confusing the +
operator used for string concatenation with the +
operator used for addition can lead to strange results. The +
operator is left-associative. For example, if integer variable y
has the value 5
, the expression "y + 2 = " + y + 2
results in the string "y + 2 = 52"
, not "y + 2 = 7"
, because first the value of y
(5
) is concatenated with the string "y + 2 = "
, then the value 2
is concatenated with the new larger string "y + 2 = 5"
. The expression "y + 2 = " + (y + 2)
produces the desired result "y + 2 = 7"
.
You’ve seen three ways to call a method:
Using a method name by itself to call a method of the same class—such as Maximum(number1, number2, number3)
in line 18 of Fig. 7.3.
Using a variable that contains a reference to an object, followed by the member access (.
) operator and the method name to call a non-static
method of the referenced object—such as the method call in line 13 of Fig. 6.10, myGrade-Book.DisplayMessage()
, which calls a method of class GradeBook
from the Main
method of GradeBookTest
.
Using the class name and the member access (.
) operator to call a static
method of a class—such as Convert.ToDouble(Console.ReadLine())
in lines 13–15 of Fig. 7.3 or Math.Sqrt(900.0)
in Section 7.3.
A static
method can call only other static
methods of the same class directly (i.e., using the method name by itself) and can manipulate only static
variables in the same class directly. To access the class’s non-static
members, a static
method must use a reference to an object of the class. Recall that static
methods relate to a class as a whole, whereas non-static
methods are associated with a specific instance (object) of the class and may manipulate the instance variables of that object. Many objects of a class, each with its own copies of the instance variables, may exist at the same time. Suppose a static
method were to invoke a non-static
method directly. How would the method know which object’s instance variables to manipulate? What would happen if no objects of the class existed at the time the non-static
method was invoked? Thus, C# does not allow a static
method to access non-static
members of the same class directly.
There are three ways to return control to the statement that calls a method. If the method does not return a result, control returns when the program flow reaches the method-ending right brace or when the statement
return; |
is executed. If the method returns a result, the statement
|
evaluates the expression, then returns the result (and control) to the caller.
Declaring a method outside the body of a class declaration or inside the body of another method is a syntax error.
Redeclaring a method parameter as a local variable in the method’s body is a compilation error.
Forgetting to return a value from a method that should return one is a compilation error. If a return type other than void
is specified, the method must contain a return
statement in each possible execution path through the method and each return
statement must return a value consistent with the method’s return type. Returning a value from a method whose return type has been declared void
is a compilation error.
To understand how C# performs method calls, we first need to consider a data structure (i.e., collection of related data items) known as a stack (we discuss data structures in more detail in Chapters 20–23). You can think of a stack as analogous to a pile of dishes. When a dish is placed on the pile, it’s normally placed at the top (referred to as pushing the dish onto the stack). Similarly, when a dish is removed from the pile, it’s always removed from the top (referred to as popping the dish off the stack). Stacks are known as last-in, first-out (LIFO) data structures—the last item pushed (inserted) on the stack is the first item popped off (removed from) the stack.
When an application calls a method, the called method must know how to return to its caller, so the return address of the calling method is pushed onto the program-execution stack (sometimes referred to as the method-call stack). If a series of method calls occurs, the successive return addresses are pushed onto the stack in last-in, first-out order so that each method can return to its caller.
The program-execution stack also contains the memory for the local variables used in each invocation of a method during an application’s execution. This data, stored as a portion of the program-execution stack, is known as the activation record or stack frame of the method call. When a method call is made, the activation record for it is pushed onto the program-execution stack. When the method returns to its caller, the activation record for this method call is popped off the stack, and those local variables are no longer known to the application. If a local variable holding a reference to an object is the only variable in the application with a reference to that object, when the activation record containing that local variable is popped off the stack, the object can no longer be accessed by the application and will eventually be deleted from memory during “garbage collection.” We’ll discuss garbage collection in Section 10.8.
Of course, the amount of memory in a computer is finite, so only a certain amount of memory can be used to store activation records on the program-execution stack. If more method calls occur than can have their activation records stored on the program-execution stack, an error known as a stack overflow occurs.
Another important feature of method calls is argument promotion—implicitly converting an argument’s value to the type that the method expects to receive in its corresponding parameter. For example, an application can call Math
method Sqrt
with an integer argument even though the method expects to receive a double
argument. The statement
Console.WriteLine( Math.Sqrt( 4 ) ); |
correctly evaluates Math.Sqrt(4)
and displays the value 2.0. Sqrt
’s parameter list causes C# to convert the int
value 4
to the double
value 4.0
before passing the value to Sqrt
. Such conversions may lead to compilation errors if C#’s promotion rules are not satisfied. The promotion rules specify which conversions are allowed—that is, which conversions can be performed without losing data. In the Sqrt
example above, an int
is converted to a double
without changing its value. However, converting a double
to an int
truncates the fractional part of the double
value—thus, part of the value is lost. Also, double
variables can hold values much larger (and much smaller) than int
variables, so assigning a double
to an int
can cause a loss of information when the double
value doesn’t fit in the int
. Converting large integer types to small integer types (e.g., long
to int
) can also result in changed values.
The promotion rules apply to expressions containing values of two or more simple types and to simple-type values passed as arguments to methods. Each value is promoted to the appropriate type in the expression. (Actually, the expression uses a temporary copy of each value—the types of the original values remain unchanged.) Figure 7.4 lists the simple types alphabetically and the types to which each can be promoted. Values of all simple types can also be implicitly converted to type object
. We demonstrate such implicit conversions in Chapter 21, Data Structures.
Table 7.4. Implicit conversions between simple types.
Type | Conversion types |
---|---|
| no possible implicit conversions to other simple types |
|
|
|
|
| no possible implicit conversions to other simple types |
| no possible implicit conversions to other simple types |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
By default, C# does not allow you to implicitly convert values between simple types if the target type cannot represent the value of the original type (e.g., the int
value 2000000
cannot be represented as a short
, and any floating-point number with digits after its decimal point cannot be represented in an integer type such as long
, int
or short
). Therefore, to prevent a compilation error in cases where information may be lost due to an implicit conversion between simple types, the compiler requires you to use a cast operator to explicitly force the conversion. This enables you to “take control” from the compiler. You essentially say, “I know this conversion might cause loss of information, but for my purposes here, that’s fine.” Suppose you create a method Square
that calculates the square of an integer and thus requires an int
argument. To call Square
with the whole part of a double
argument named doubleValue
, you’d write Square( (int) doubleValue )
. This method call explicitly casts (converts) the value of doubleValue
to an integer for use in method Square
. Thus, if doubleValue
’s value is 4.5
, the method receives the value 4
and returns 16
, not 20.25
(which does, unfortunately, result in the loss of information).
Many predefined classes are grouped into categories of related classes called namespaces. Together, these namespaces are referred to as the .NET Framework Class Library.
Throughout the text, using
directives allow us to use library classes from the Framework Class Library without specifying their fully qualified names. For example, an application includes the declaration
using System; |
in order to use the class names from the System
namespace without fully qualifying their names. This allows you to use the unqualified class name Console
, rather than the fully qualified class name System.Console
, in your code. A great strength of C# is the large number of classes in the namespaces of the .NET Framework Class Library. Some key Framework Class Library namespaces are described in Fig. 7.5, which represents only a small portion of the reusable classes in the .NET Framework Class Library.
Table 7.5. .NET Framework Class Library namespaces (a subset).
Namespace | Description |
---|---|
| Contains the classes required to create and manipulate GUIs. (Various classes in this namespace are discussed in Chapter 14, Graphical User Interfaces with Windows Forms: Part 1, and Chapter 15, Graphical User Interfaces with Windows Forms: Part 2.) |
System.Windows.Controls System.Windows.Input System.Windows.Media System.Windows.Shapes | Contain the classes of the Windows Presentation Foundation for GUIs, 2-D and 3-D graphics, multimedia and animation. (You’ll learn more about these namespaces in Chapter 24, GUI with Windows Presentation Foundation, Chapter 25, WPF Graphics and Multimedia and Chapter 29, Silverlight and Rich Internet Applications.) |
| Contains the classes that support Language Integrated Query (LINQ). (You’ll learn more about this namespace in Chapter 9, Introduction to LINQ and the |
System.Data System.Data.Linq | Contain the classes for manipulating data in databases (i.e., organized collections of data), including support for LINQ to SQL. (You’ll learn more about these namespaces in Chapter 18, Databases and LINQ.) |
| Contains the classes that enable programs to input and output data. (You’ll learn more about this namespace in Chapter 17, Files and Streams.) |
| Contains the classes used for creating and maintaining web applications, which are accessible over the Internet. (You’ll learn more about this namespace in Chapter 19, Web App Development with ASP.NET.) |
| Contains the classes that support Language Integrated Query (LINQ) for XML documents. (You’ll learn more about this namespace in Chapter 26, XML and LINQ to XML, and several other chapters throughout the book.) |
| Contains the classes for creating and manipulating XML data. Data can be read from or written to XML files. (You’ll learn more about this namespace in Chapter 26.) |
System.Collections System.Collections.Generic | Contain the classes that define data structures for maintaining collections of data. (You’ll learn more about these namespaces in Chapter 23, Collections.) |
| Contains the classes that enable programs to manipulate characters and strings. (You’ll learn more about this namespace in Chapter 16, Strings and Characters.) |
The set of namespaces available in the .NET Framework Class Library is quite large. Besides those summarized in Fig. 7.5, the .NET Framework Class Library contains namespaces for complex graphics, advanced graphical user interfaces, printing, advanced networking, security, database processing, multimedia, accessibility (for people with disabilities) and many other capabilities—over 100 namespaces in all.
You can locate additional information about a predefined C# class’s methods in the .NET Framework Class Library reference (msdn.microsoft.com/en-us/library/ms229335.aspx). When you visit this site, you’ll see an alphabetical listing of all the namespaces in the Framework Class Library. Locate the namespace and click its link to see an alphabetical listing of all its classes, with a brief description of each. Click a class’s link to see a more complete description of the class. Click the Methods link in the left-hand column to see a listing of the class’s methods.
In this and the next section, we develop a nicely structured game-playing application with multiple methods. The application uses most of the control statements presented thus far in the book and introduces several new programming concepts.
There’s something in the air of a casino that invigorates people—from the high rollers at the plush mahogany-and-felt craps tables to the quarter poppers at the one-armed bandits. It’s the element of chance, the possibility that luck will convert a pocketful of money into a mountain of wealth. The element of chance can be introduced in an application via an object of class Random
(of namespace System
). Objects of class Random
can produce random byte
, int
and double
values. In the next several examples, we use objects of class Random
to produce random numbers.
A new random-number generator object can be created as follows:
Random randomNumbers = new Random(); |
The random-number generator object can then be used to generate random byte
, int
and double
values—we discuss only random int
values here.
Consider the following statement:
int randomValue = randomNumbers.Next(); |
Method Next
of class Random
generates a random int
value in the range 0 to +2,147,483,646, inclusive. If the Next
method truly produces values at random, then every value in that range should have an equal chance (or probability) of being chosen each time method Next
is called. The values returned by Next
are actually pseudorandom numbers—a sequence of values produced by a complex mathematical calculation. The calculation uses the current time of day (which, of course, changes constantly) to seed the random-number generator such that each execution of an application yields a different sequence of random values.
The range of values produced directly by method Next
often differs from the range of values required in a particular C# application. For example, an application that simulates coin tossing might require only 0 for “heads” and 1 for “tails.” An application that simulates the rolling of a six-sided die might require random integers in the range 1–6. A video game that randomly predicts the next type of spaceship (out of four possibilities) that will fly across the horizon might require random integers in the range 1–4. For cases like these, class Random
provides other versions of method Next
. One receives an int
argument and returns a value from 0 up to, but not including, the argument’s value. For example, you might use the statement
int randomValue = randomNumbers.Next( 6 ); |
which returns 0, 1, 2, 3, 4 or 5. The argument 6
—called the scaling factor—represents the number of unique values that Next
should produce (in this case, six—0, 1, 2, 3, 4 and 5). This manipulation is called scaling the range of values produced by Random
method Next
.
Suppose we wanted to simulate a six-sided die that has the numbers 1–6 on its faces, not 0–5. Scaling the range of values alone is not enough. So we shift the range of numbers produced. We could do this by adding a shifting value—in this case 1—to the result of method Next
, as in
face = 1 + randomNumbers.Next( 6 ); |
The shifting value (1
) specifies the first value in the desired set of random integers. The preceding statement assigns to face
a random integer in the range 1–6.
The third alternative of method Next
provides a more intuitive way to express both shifting and scaling. This method receives two int
arguments and returns a value from the first argument’s value up to, but not including, the second argument’s value. We could use this method to write a statement equivalent to our previous statement, as in
face = randomNumbers.Next( 1, 7 ); |
To demonstrate random numbers, let’s develop an application that simulates 20 rolls of a six-sided die and displays each roll’s value. Figure 7.6 shows two sample outputs, which confirm that the results of the preceding calculation are integers in the range 1–6 and that each run of the application can produce a different sequence of random numbers. The using
directive (line 3) enables us to use class Random
without fully qualifying its name. Line 9 creates the Random
object randomNumbers
to produce random values. Line 16 executes 20 times in a loop to roll the die. The if
statement (lines 21–22) starts a new line of output after every five numbers, so the results can be presented on multiple lines.
Example 7.6. Shifted and scaled random integers.
1 // Fig. 7.6: RandomIntegers.cs 2 // Shifted and scaled random integers. 3 using System; 4 5 public class RandomIntegers 6 { 7 public static void Main( string[] args ) 8 { 9 Random randomNumbers = new Random(); // random-number generator 10 int face; // stores each random integer generated 11 12 // loop 20 times 13 for ( int counter = 1; counter <= 20; counter++ ) 14 { 15 // pick random integer from 1 to 6 16 face = randomNumbers.Next( 1, 7 ); 17 18 Console.Write( "{0} ", face ); // display generated value 19 20 // if counter is divisible by 5, start a new line of output 21 if ( counter % 5 == 0 ) 22 Console.WriteLine(); 23 } // end for 24 } // end Main 25 } // end class RandomIntegers
3 3 3 1 1 2 1 2 4 2 2 3 6 2 5 3 4 6 6 1 |
6 2 5 1 3 5 2 1 6 5 4 1 6 1 3 3 1 4 3 4 |
To show that the numbers produced by Next
occur with approximately equal likelihood, let’s simulate 6000 rolls of a die (Fig. 7.7). Each integer from 1 to 6 should appear approximately 1000 times.
Example 7.7. Roll a six-sided die 6000 times.
1 // Fig. 7.7: RollDie.cs 2 // Roll a six-sided die 6000 times. 3 using System; 4 5 public class RollDie 6 { 7 public static void Main( string[] args ) 8 { 9 Random randomNumbers = new Random(); // random-number generator 10 11 int frequency1 = 0; // count of 1s rolled 12 int frequency2 = 0; // count of 2s rolled 13 int frequency3 = 0; // count of 3s rolled 14 int frequency4 = 0; // count of 4s rolled 15 int frequency5 = 0; // count of 5s rolled 16 int frequency6 = 0; // count of 6s rolled 17 18 int face; // stores most recently rolled value 19 20 // summarize results of 6000 rolls of a die 21 for ( int roll = 1; roll <= 6000; roll++ ) 22 { 23 face = randomNumbers.Next( 1, 7 ); // number from 1 to 6 24 25 // determine roll value 1-6 and increment appropriate counter 26 switch ( face ) 27 { 28 case 1: 29 ++frequency1; // increment the 1s counter 30 break; 31 case 2: 32 ++frequency2; // increment the 2s counter 33 break; 34 case 3: 35 ++frequency3; // increment the 3s counter 36 break; 37 case 4: 38 ++frequency4; // increment the 4s counter 39 break; 40 case 5: 41 ++frequency5; // increment the 5s counter 42 break; 43 case 6: 44 ++frequency6; // increment the 6s counter 45 break; 46 } // end switch 47 } // end for 48 49 Console.WriteLine( "Face Frequency" ); // output headers 50 Console.WriteLine( 51 "1 {0} 2 {1} 3 {2} 4 {3} 5 {4} 6 {5}", frequency1, 52 frequency2, frequency3, frequency4, frequency5, frequency6 ); 53 } // end Main 54 } // end class RollDie
Face Frequency 1 985 2 985 3 1001 4 1017 5 1002 6 1010 |
As the two sample outputs show, the values produced by method Next
enable the application to realistically simulate rolling a six-sided die. The application uses nested control statements (the switch
is nested inside the for
) to determine the number of times each side of the die occurred. The for
statement (lines 21–47) iterates 6000 times. During each iteration, line 23 produces a random value from 1 to 6. This face
value is then used as the switch
expression (line 26) in the switch
statement (lines 26–46). Based on the face
value, the switch
statement increments one of the six counter variables during each iteration of the loop. (In Section 8.4, we show an elegant way to replace the entire switch
statement in this application with a single statement.) The switch
statement has no default
label because we have a case
label for every possible die value that the expression in line 23 can produce. Run the application several times and observe the results. You’ll see that every time you execute this application, it produces different results.
Previously, we demonstrated the statement
face = randomNumbers.Next( 1, 7 ); |
which simulates the rolling of a six-sided die. This statement always assigns to variable face
an integer in the range 1
≤ face
< 7
. The width of this range (i.e., the number of consecutive integers in the range) is 6
, and the starting number in the range is 1
. Referring to the preceding statement, we see that the width of the range is determined by the difference between the two integers passed to Random
method Next
, and the starting number of the range is the value of the first argument. We can generalize this result as
number = randomNumbers.Next( shiftingValue, shiftingValue + scalingFactor ); |
where shiftingValue specifies the first number in the desired range of consecutive integers and scalingFactor specifies how many numbers are in the range.
It’s also possible to choose integers at random from sets of values other than ranges of consecutive integers. For this purpose, it’s simpler to use the version of the Next
method that takes only one argument. For example, to obtain a random value from the sequence 2, 5, 8, 11 and 14, you could use the statement
number = 2 + 3 * randomNumbers.Next( 5 ); |
In this case, randomNumberGenerator.Next(5)
produces values in the range 0–4. Each value produced is multiplied by 3 to produce a number in the sequence 0, 3, 6, 9 and 12. We then add 2 to that value to shift the range of values and obtain a value from the sequence 2, 5, 8, 11 and 14. We can generalize this result as
number = shiftingValue + differenceBetweenValues * randomNumbers.Next( scalingFactor ); |
where shiftingValue specifies the first number in the desired range of values, difference-BetweenValues represents the difference between consecutive numbers in the sequence and scalingFactor specifies how many numbers are in the range.
As we mentioned earlier in Section 7.9, the methods of class Random
actually generate pseudorandom numbers based on complex mathematical calculations. Repeatedly calling any of Random
’s methods produces a sequence of numbers that appears to be random. The calculation that produces the pseudorandom numbers uses the time of day as a seed value to change the sequence’s starting point. Each new Random
object seeds itself with a value based on the computer system’s clock at the time the object is created, enabling each execution of an application to produce a different sequence of random numbers.
When debugging an application, it’s sometimes useful to repeat the exact same sequence of pseudorandom numbers during each execution of the application. This repeatability enables you to prove that your application is working for a specific sequence of random numbers before you test the application with different sequences of random numbers. When repeatability is important, you can create a Random
object as follows:
Random randomNumbers = new Random( seedValue ); |
The seedValue
argument (type int
) seeds the random-number calculation. If the same seedValue
is used every time, the Random
object produces the same sequence of random numbers.
While an application is under development, create the Random
object with a specific seed value to produce a repeatable sequence of random numbers each time the application executes. If a logic error occurs, fix the error and test the application again with the same seed value—this allows you to reconstruct the same sequence of random numbers that caused the error. Once the logic errors have been removed, create the Random
object without using a seed value, causing the Random
object to generate a new sequence of random numbers each time the application executes.
One popular game of chance is the dice game known as “craps,” which is played in casinos and back alleys throughout the world. The rules of the game are straightforward:
You roll two dice. Each die has six faces, which contain one, two, three, four, five and six spots, respectively. After the dice have come to rest, the sum of the spots on the two upward faces is calculated. If the sum is 7 or 11 on the first throw, you win. If the sum is 2, 3 or 12 on the first throw (called “craps”), you lose (i.e., “the house” wins). If the sum is 4, 5, 6, 8, 9 or 10 on the first throw, that sum becomes your “point.” To win, you must continue rolling the dice until you “make your point” (i.e., roll that same point value). You lose by rolling a 7 before making your point.
The application in Fig. 7.8 simulates the game of craps, using methods to define the logic of the game. The Main
method (lines 24–70) calls the static RollDice
method (lines 73–85) as needed to roll the two dice and compute their sum. The four sample outputs show winning on the first roll, losing on the first roll, winning on a subsequent roll and losing on a subsequent roll, respectively. Variable randomNumbers
(line 8) is declared static
so it can be created once during the program’s execution and used in method RollDice
.
Example 7.8. Craps
class simulates the dice game craps.
1 // Fig. 7.8: Craps.cs 2 // Craps class simulates the dice game craps. 3 using System; 4 5 public class Craps 6 { 7 // create random-number generator for use in method RollDice 8 private static Random randomNumbers = new Random(); 9 10 // enumeration with constants that represent the game status 11 private enum Status { CONTINUE, WON, LOST } 12 13 // enumeration with constants that represent common rolls of the dice 14 private enum DiceNames 15 { 16 SNAKE_EYES = 2, 17 TREY = 3, 18 SEVEN = 7, 19 YO_LEVEN = 11, 20 BOX_CARS = 12 21 } 22 23 // plays one game of craps 24 public static void Main( string[] args ) 25 { 26 // gameStatus can contain CONTINUE, WON or LOST 27 Status gameStatus = Status.CONTINUE; 28 int myPoint = 0; // point if no win or loss on first roll 29 30 int sumOfDice = RollDice(); // first roll of the dice 31 32 // determine game status and point based on first roll 33 switch ( ( DiceNames ) sumOfDice ) 34 { 35 case DiceNames.SEVEN: // win with 7 on first roll 36 case DiceNames.YO_LEVEN: // win with 11 on first roll 37 gameStatus = Status.WON; 38 break; 39 case DiceNames.SNAKE_EYES: // lose with 2 on first roll 40 case DiceNames.TREY: // lose with 3 on first roll 41 case DiceNames.BOX_CARS: // lose with 12 on first roll 42 gameStatus = Status.LOST; 43 break; 44 default: // did not win or lose, so remember point 45 gameStatus = Status.CONTINUE; // game is not over 46 myPoint = sumOfDice; // remember the point 47 Console.WriteLine( "Point is {0}", myPoint ); 48 break; 49 } // end switch 50 51 // while game is not complete 52 while ( gameStatus == Status.CONTINUE ) // game not WON or LOST 53 { 54 sumOfDice = RollDice(); // roll dice again 55 56 // determine game status 57 if ( sumOfDice == myPoint ) // win by making point 58 gameStatus = Status.WON; 59 else 60 // lose by rolling 7 before point 61 if ( sumOfDice == ( int ) DiceNames.SEVEN ) 62 gameStatus = Status.LOST; 63 } // end while 64 65 // display won or lost message 66 if ( gameStatus == Status.WON ) 67 Console.WriteLine( "Player wins" ); 68 else 69 Console.WriteLine( "Player loses" ); 70 } // end Main 71 72 // roll dice, calculate sum and display results 73 public static int RollDice() 74 { 75 // pick random die values 76 int die1 = randomNumbers.Next( 1, 7 ); // first die roll 77 int die2 = randomNumbers.Next( 1, 7 ); // second die roll 78 79 int sum = die1 + die2; // sum of die values 80 81 // display results of this roll 82 Console.WriteLine( "Player rolled {0} + {1} = {2}", 83 die1, die2, sum ); 84 return sum; // return sum of dice 85 } // end method RollDice 86 } // end class Craps
Player rolled 2 + 5 = 7 Player wins |
Player rolled 2 + 1 = 3 Player loses |
Player rolled 4 + 6 = 10 Point is 10 Player rolled 1 + 3 = 4 Player rolled 1 + 3 = 4 Player rolled 2 + 3 = 5 Player rolled 4 + 4 = 8 Player rolled 6 + 6 = 12 Player rolled 4 + 4 = 8 Player rolled 4 + 5 = 9 Player rolled 2 + 6 = 8 Player rolled 6 + 6 = 12 Player rolled 6 + 4 = 10 Player wins |
Player rolled 2 + 4 = 6 Point is 6 Player rolled 3 + 1 = 4 Player rolled 5 + 5 = 10 Player rolled 6 + 1 = 7 Player loses |
In the rules of the game, the player must roll two dice on the first roll and must do the same on all subsequent rolls. We declare method RollDice
(lines 73–85) to roll the dice and compute and display their sum. Method RollDice
is declared once, but it’s called from two places (lines 30 and 54) in method Main
, which contains the logic for one complete game of craps. Method RollDice
takes no arguments, so it has an empty parameter list. Each time it’s called, RollDice
returns the sum of the dice, so the return type int
is indicated in the method header (line 73). Although lines 76 and 77 look the same (except for the die names), they do not necessarily produce the same result. Each of these statements produces a random value in the range 1–6. Variable randomNumbers
(used in lines 76 and 77) is not declared in the method. Rather it’s declared as a private static
variable of the class and initialized in line 8. This enables us to create one Random
object that’s reused in each call to RollDice
.
The game is reasonably involved. The player may win or lose on the first roll or may win or lose on any subsequent roll. Method Main
(lines 24–70) uses local variable gameStatus
(line 27) to keep track of the overall game status, local variable myPoint
(line 28) to store the “point” if the player does not win or lose on the first roll and local variable sumOfDice
(line 30) to maintain the sum of the dice for the most recent roll. Variable myPoint
is initialized to 0
to ensure that the application will compile. If you do not initialize myPoint
, the compiler issues an error, because myPoint
is not assigned a value in every branch of the switch
statement—thus, the application could try to use myPoint
before it’s definitely assigned a value. By contrast, gameStatus
does not require initialization because it is assigned a value in every branch of the switch
statement—thus, it’s guaranteed to be initialized before it’s used. However, as good programming practice, we initialize it anyway.
Local variable gameStatus
is declared to be of a new type called Status
, which we declared in line 11. Type Status
is declared as a private
member of class Craps
, because Status
will be used only in that class. Status
is a user-defined type called an enumeration, which declares a set of constants represented by identifiers. An enumeration is introduced by the keyword enum
and a type name (in this case, Status
). As with a class, braces ( {
and }
) delimit the body of an enum
declaration. Inside the braces is a comma-separated list of enumeration constants. The enum
constant names must be unique, but the value associated with each constant need not be.
Use only uppercase letters in the names of constants. This makes the constants stand out in an application and reminds you that enumeration constants are not variables.
Variables of type Status
should be assigned only one of the three constants declared in the enumeration. When the game is won, the application sets local variable gameStatus
to Status.WON
(lines 37 and 58). When the game is lost, the application sets local variable gameStatus
to Status.LOST
(lines 42 and 62). Otherwise, the application sets local variable gameStatus
to Status.CONTINUE
(line 45) to indicate that the dice must be rolled again.
Line 30 in method Main
calls RollDice
, which picks two random values from 1 to 6, displays the value of the first die, the value of the second die and the sum of the dice, and returns the sum of the dice. Method Main
next enters the switch
statement at lines 33–49, which uses the sumOfDice
value from line 30 to determine whether the game has been won or lost, or whether it should continue with another roll.
The sums of the dice that would result in a win or loss on the first roll are declared in the DiceNames
enumeration in lines 14–21. These are used in the case
s of the switch
statement. The identifier names use casino parlance for these sums. Notice that in the DiceNames
enumeration, a value is explicitly assigned to each identifier name. When the enum
is declared, each constant in the enum
declaration is a constant value of type int
. If you do not assign a value to an identifier in the enum
declaration, the compiler will do so. If the first enum
constant is unassigned, the compiler gives it the value 0
. If any other enum
constant is unassigned, the compiler gives it a value equal to one more than the value of the preceding enum
constant. For example, in the Status
enumeration, the compiler implicitly assigns 0
to Status.WON
, 1
to Status.CONTINUE
and 2
to Status.LOST
.
You could also declare an enum
’s underlying type to be byte
, sbyte
, short
, ushort
, int
, uint
, long
or ulong
by writing
private enum MyEnum : typeName { Constant1, Constant2, ... } |
where typeName represents one of the integral simple types.
If you need to compare a simple integral type value to the underlying value of an enumeration constant, you must use a cast operator to make the two types match. In the switch
statement at lines 33–49, we use the cast operator to convert the int
value in sumOfDice
to type DiceNames
and compare it to each of the constants in DiceNames
. Lines 35–36 determine whether the player won on the first roll with SEVEN
(7
) or YO_LEVEN
(11
). Lines 39–41 determine whether the player lost on the first roll with SNAKE_EYES
(2
), TREY
(3
) or BOX_CARS
(12
). After the first roll, if the game is not over, the default
case (lines 44–48) saves sumOfDice
in myPoint
(line 46) and displays the point (line 47).
If we’re still trying to “make our point” (i.e., the game is continuing from a prior roll), the loop in lines 52–63 executes. Line 54 rolls the dice again. If sumOfDice
matches myPoint
in line 57, line 58 sets gameStatus
to Status.WON
, and the loop terminates because the game is complete. In line 61, we use the cast operator ( int )
to obtain the underlying value of DiceNames.SEVEN
so that we can compare it to sumOfDice
. If sumOfDice
is equal to SEVEN
(7
), line 62 sets gameStatus
to Status.LOST
, and the loop terminates because the game is over. When the game completes, lines 66–69 display a message indicating whether the player won or lost, and the application terminates.
Note the use of the various program-control mechanisms we’ve discussed. The Craps
class uses two methods—Main
and RollDice
(called twice from Main
)—and the switch
, while
, if
...else
and nested if
control statements. Also, notice that we use multiple case
labels in the switch
statement to execute the same statements for sums of SEVEN
and YO_LEVEN
(lines 35–36) and for sums of SNAKE_EYES
, TREY
and BOX_CARS
(lines 39–41). To easily create a switch
statement with all possible values for an enum
type, you can use the switch
code snippet. Type switch
in the C# code then press Tab twice. If you enter an enum
type into the switch
statement’s expression (the highlighted code of the snippet) and press Enter, a case
for each enum
constant will be generated automatically.
You’ve seen declarations of C# entities, such as classes, methods, properties, variables and parameters. Declarations introduce names that can be used to refer to such C# entities. The scope of a declaration is the portion of the application that can refer to the declared entity by its unqualified name. Such an entity is said to be “in scope” for that portion of the application. This section introduces several important scope issues. The basic scope rules are as follows:
The scope of a parameter declaration is the body of the method in which the declaration appears.
The scope of a local-variable declaration is from the point at which the declaration appears to the end of the block containing the declaration.
The scope of a local-variable declaration that appears in the initialization section of a for
statement’s header is the body of the for
statement and the other expressions in the header.
The scope of a method, property or field of a class is the entire body of the class. This enables non-static
methods and properties of a class to use any of the class’s fields, methods and properties, regardless of the order in which they’re declared. Similarly, static
methods and properties can use any of the static
members of the class.
Any block may contain variable declarations. If a local variable or parameter in a method has the same name as a field, the field is hidden until the block terminates. In Chapter 10, we discuss how to access hidden fields. The application in Fig. 7.9 demonstrates scoping issues with fields and local variables.
Example 7.9. Scope
class demonstrates static and local variable scopes.
1 // Fig. 7.9: Scope.cs 2 // Scope class demonstrates static and local variable scopes. 3 using System; 4 5 public class Scope 6 { 7 // static variable that is accessible to all methods of this class 8 private static int x = 1; 9 10 // Main creates and initializes local variable x 11 // and calls methods UseLocalVariable and UseStaticVariable 12 public static void Main( string[] args ) 13 { 14 int x = 5; // method's local variable x hides static variable x 15 16 Console.WriteLine( "local x in method Main is {0}", x ); 17 18 // UseLocalVariable has its own local x 19 UseLocalVariable(); 20 21 // UseStaticVariable uses class Scope's static variable x 22 UseStaticVariable(); 23 24 // UseLocalVariable reinitializes its own local x 25 UseLocalVariable(); 26 27 // class Scope's static variable x retains its value 28 UseStaticVariable(); 29 30 Console.WriteLine( " local x in method Main is {0}", x ); 31 } // end Main 32 33 // create and initialize local variable x during each call 34 public static void UseLocalVariable() 35 { 36 int x = 25; // initialized each time UseLocalVariable is called 37 38 Console.WriteLine( 39 " local x on entering method UseLocalVariable is {0}", x ); 40 ++x; // modifies this method's local variable x 41 Console.WriteLine( 42 "local x before exiting method UseLocalVariable is {0}", x ); 43 } // end method UseLocalVariable 44 45 // modify class Scope's static variable x during each call 46 public static void UseStaticVariable() 47 { 48 Console.WriteLine( " static variable x on entering {0} is {1}", 49 "method UseStaticVariable", x ); 50 x *= 10; // modifies class Scope's static variable x 51 Console.WriteLine( "static variable x before exiting {0} is {1}", 52 "method UseStaticVariable", x ); 53 } // end method UseStaticVariable 54 } // end class Scope
local x in method Main is 5 local x on entering method UseLocalVariable is 25 local x before exiting method UseLocalVariable is 26 static variable x on entering method UseStaticVariable is 1 static variable x before exiting method UseStaticVariable is 10 local x on entering method UseLocalVariable is 25 local x before exiting method UseLocalVariable is 26 static variable x on entering method UseStaticVariable is 10 static variable x before exiting method UseStaticVariable is 100 local x in method Main is 5 |
Use different names for fields and local variables to help prevent subtle logic errors that occur when a method is called and a local variable of the method hides a field of the same name in the class.
Line 8 declares and initializes the static
variable x
to 1
. This static
variable is hidden in any block (or method) that declares local variable named x
. Method Main
(lines 12–31) declares local variable x
(line 14) and initializes it to 5
. This local variable’s value is output to show that static
variable x
(whose value is 1
) is hidden in method Main
. The application declares two other methods—UseLocalVariable
(lines 34–43) and UseStaticVariable
(lines 46–53)—that each take no arguments and do not return results. Method Main
calls each method twice (lines 19–28). Method UseLocalVariable
declares local variable x
(line 36). When UseLocalVariable
is first called (line 19), it creates local variable x
and initializes it to 25
(line 36), outputs the value of x
(lines 38–39), increments x
(line 40) and outputs the value of x
again (lines 41–42). When UseLocalVariable
is called a second time (line 25), it re-creates local variable x
and reinitializes it to 25
, so the output of each UseLocalVariable
call is identical.
Method UseStaticVariable
does not declare any local variables. Therefore, when it refers to x
, static
variable x
(line 8) of the class is used. When method UseStaticVariable
is first called (line 22), it outputs the value (1
) of static
variable x
(lines 48–49), multiplies the static
variable x
by 10
(line 50) and outputs the value (10
) of static
variable x
again (lines 51–52) before returning. The next time method UseStaticVariable
is called (line 28), the static
variable has its modified value, 10
, so the method outputs 10
, then 100
. Finally, in method Main
, the application outputs the value of local variable x
again (line 30) to show that none of the method calls modified Main
’s local variable x
, because the methods all referred to variables named x
in other scopes.
Methods of the same name can be declared in the same class, as long as they have different sets of parameters (determined by the number, types and order of the parameters). This is called method overloading. When an overloaded method is called, the C# compiler selects the appropriate method by examining the number, types and order of the arguments in the call. Method overloading is commonly used to create several methods with the same name that perform the same or similar tasks, but on different types or different numbers of arguments. For example, Math
methods Min
and Max
(summarized in Section 7.3) are overloaded with 11 versions. These find the minimum and maximum, respectively, of two values of each of the 11 numeric simple types. Our next example demonstrates declaring and invoking overloaded methods. You’ll see examples of overloaded constructors in Chapter 10.
In class MethodOverload
(Fig. 7.10), we include two overloaded versions of a method called Square
—one that calculates the square of an int
(and returns an int
) and one that calculates the square of a double
(and returns a double
). Although these methods have the same name and similar parameter lists and bodies, you can think of them simply as different methods. It may help to think of the method names as “Square
of int
” and “Square
of double
,” respectively.
Example 7.10. Overloaded method declarations.
1 // Fig. 7.10: MethodOverload.cs 2 // Overloaded method declarations. 3 using System; 4 5 public class MethodOverload 6 { 7 // test overloaded square methods 8 public static void Main( string[] args ) 9 { 10 Console.WriteLine( "Square of integer 7 is {0}", Square( 7 ) ); 11 Console.WriteLine( "Square of double 7.5 is {0}", Square( 7.5 ) ); 12 } // end Main 13 14 // square method with int argument 15 public static int Square( int intValue ) 16 { 17 Console.WriteLine( "Called square with int argument: {0}", 18 intValue ); 19 return intValue * intValue; 20 } // end method Square with int argument 21 22 // square method with double argument 23 public static double Square( double doubleValue ) 24 { 25 Console.WriteLine( "Called square with double argument: {0}", 26 doubleValue ); 27 return doubleValue * doubleValue; 28 } // end method Square with double argument 29 } // end class MethodOverload
Called square with int argument: 7 Square of integer 7 is 49 Called square with double argument: 7.5 Square of double 7.5 is 56.25 |
Line 10 in Main
invokes method Square
with the argument 7
. Literal integer values are treated as type int
, so the method call in line 10 invokes the version of Square
at lines 15–20 that specifies an int
parameter. Similarly, line 11 invokes method Square
with the argument 7.5
. Literal real-number values are treated as type double
, so the method call in line 11 invokes the version of Square
at lines 23–28 that specifies a double
parameter. Each method first outputs a line of text to prove that the proper method was called in each case.
Notice that the overloaded methods in Fig. 7.10 perform the same calculation, but with two different types. C#’s generics feature provides a mechanism for writing a single “generic method” that can perform the same tasks as an entire set of overloaded methods. We discuss generic methods in Chapter 22.
The compiler distinguishes overloaded methods by their signature—a combination of the method’s name and the number, types and order of its parameters. The signature also includes the way those parameters are passed, which can be modified by the ref
and out
keywords (discussed in Section 7.16). If the compiler looked only at method names during compilation, the code in Fig. 7.10 would be ambiguous—the compiler would not know how to distinguish between the Square
methods (lines 15–20 and 23–28). Internally, the compiler uses signatures to determine whether a class’s methods are unique in that class.
For example, in Fig. 7.10, the compiler will use the method signatures to distinguish between the “Square
of int
” method (the Square
method that specifies an int
parameter) and the “Square
of double
” method (the Square
method that specifies a double
parameter). If Method1
’s declaration begins as
void Method1( int a, float b ) |
then that method will have a different signature than the method declared beginning with
void Method1( float a, int b ) |
The order of the parameter types is important—the compiler considers the preceding two Method1
headers to be distinct.
In discussing the logical names of methods used by the compiler, we did not mention the return types of the methods. This is because method calls cannot be distinguished by return type. The application in Fig. 7.11 illustrates the compiler errors generated when two methods have the same signature but different return types. Overloaded methods can have the same or different return types if the methods have different parameter lists. Also, overloaded methods need not have the same number of parameters.
Example 7.11. Overloaded methods with identical signatures cause compilation errors, even if return types are different.
1 // Fig. 7.11: MethodOverload.cs 2 // Overloaded methods with identical signatures 3 // cause compilation errors, even if return types are different. 4 public class MethodOverloadError 5 { 6 // declaration of method Square with int argument 7 public int Square( int x ) 8 { 9 return x * x; 10 } // end method Square 11 12 // second declaration of method Square with int argument 13 // causes compilation error even though return types are different 14 public double Square( int y ) 15 { 16 return y * y; 17 } // end method Square 18 } // end class MethodOverloadError
As of Visual C# 2010, methods can have optional parameters that allow the calling method to vary the number of arguments to pass. An optional parameter specifies a default value that’s assigned to the parameter if the optional argument is omitted.
You can create methods with one or more optional parameters. All optional parameters must be placed to the right of the method’s non-optional parameters—that is, at the end of the parameter list.
Declaring a non-optional parameter to the right of an optional one is a compilation error.
When a parameter has a default value, the caller has the option of passing that particular argument. For example, the method header
public int Power( int baseValue, int exponentValue = 2 ) |
specifies an optional second parameter. Any call to Power
must pass at least an argument for the parameter baseValue
, or a compilation error occurs. Optionally, a second argument (for the exponentValue
parameter) can be passed to Power
. Consider the following calls to Power
:
Power() Power(10) Power(10, 3) |
The first call generates a compilation error because this method requires a minimum of one argument. The second call is valid because one argument (10
) is being passed—the optional exponentValue
is not specified in the method call. The last call is also valid—10
is passed as the required argument and 3
is passed as the optional argument.
In the call that passes only one argument (10
), parameter exponentValue
defaults to 2
, which is the default value specified in the method’s header. Each optional parameter must specify a default value by using an equal (=
) sign followed by the value. For example, the header for Power
sets 2
as exponentValue
’s default value.
Figure 7.12 demonstrates an optional parameter. The program calculates the result of raising a base value to an exponent. Method Power
(Fig. 7.12, lines 15–23) specifies that its second parameter is optional. In method DisplayPowers
, lines 10–11 of Fig. 7.12 call method Power
. Line 10 calls the method without the optional second argument. In this case, the compiler provides the second argument, 2
, using the default value of the optional argument, which is not visible to you in the call.
Example 7.12. Optional argument demonstration with method Power
.
1 // Fig. 7.12: Power.vb 2 // Optional argument demonstration with method Power. 3 using System; 4 5 class CalculatePowers 6 { 7 // call Power with and without optional arguments 8 public static void Main( string[] args ) 9 { 10 Console.WriteLine( "Power(10) = {0}", Power( 10 ) ) ; 11 Console.WriteLine( "Power(2, 10) = {0}", Power( 2, 10 ) ); 12 } // end Main 13 14 // use iteration to calculate power 15 public int Power( int baseValue, int exponentValue = 2 ) 16 { 17 int result = 1; // initialize total 18 19 for ( int i = 1; i <= exponentValue; i++ ) 20 result *= baseValue; 21 22 return result; 23 } // end method Power 24 } // end class CalculatePowers
Power(10) = 100 Power(2, 10) = 1024 |
Normally, when calling a method that has optional parameters, the argument values—in order—are assigned to the parameters from left to right in the parameter list. Consider a Time
class that stores the time of day in 24-hour clock format as int
values representing the hour (0–23), minute (0–59) and second (0–59). Such a class might provide a SetTime
method with optional parameters like
public void SetTime( int hour = 0, int minute = 0, int second = 0 ) |
In the preceding method header, all of three of SetTime
’s parameters are optional. Assuming that we have a Time
object named t
, we can call SetTime
as follows:
t.SetTime(); // sets the time to 12:00:00 AM t.SetTime( 12 ); // sets the time to 12:00:00 PM t.SetTime( 12, 30 ); // sets the time to 12:30:00 PM t.SetTime( 12, 30, 22 ); // sets the time to 12:30:22 PM |
In the first call, no arguments are specified, so the compiler assigns 0
to each parameter. In the second call, the compiler assigns the argument, 12
, to the first parameter, hour
, and assigns default values of 0
to the minute
and second
parameters. In the third call, the compiler assigns the two arguments, 12
and 30
, to the parameters hour
and minute
, respectively, and assigns the default value 0
to the parameter second
. In the last call, the compiler assigns the three arguments, 12
, 30
and 22
, to the parameters hour
, minute
and second
, respectively.
What if you wanted to specify only arguments for the hour
and second
? You might think that you could call the method as follows:
t.SetTime( 12, , 22 ); // COMPILATION ERROR |
Unlike some programming languages, C# doesn’t allow you to skip an argument as shown in the preceding statement. However, Visual C# 2010 provides a new feature called named parameters, which enable you to call methods that receive optional parameters by providing only the optional arguments you wish to specify. To do so, you explicitly specify the parameter’s name and value—separated by a colon (:
)—in the argument list of the method call. For example, the preceding statement can be implemented in Visual C# 2010 as follows:
t.SetTime( hour: 12, second: 22 ); // sets the time to 12:00:22 |
In this case, the compiler assigns parameter hour
the argument 12
and parameter second
the argument 22
. The parameter minute
is not specified, so the compiler assigns it the default value 0
. It’s also possible to specify the arguments out of order when using named parameters. The arguments for the required parameters must always be supplied.
The applications we’ve discussed thus far are generally structured as methods that call one another in a disciplined, hierarchical manner. For some problems, however, it’s useful to have a method call itself. A recursive method is a method that calls itself, either directly or indirectly through another method.
We consider recursion conceptually first. Then we examine an application containing a recursive method. Recursive problem-solving approaches have a number of elements in common. When a recursive method is called to solve a problem, it actually is capable of solving only the simplest case(s), or base case(s). If the method is called with a base case, it returns a result. If the method is called with a more complex problem, it divides the problem into two conceptual pieces: a piece that the method knows how to do and a piece that it does not know how to do. To make recursion feasible, the latter piece must resemble the original problem, but be a slightly simpler or slightly smaller version of it. Because this new problem looks like the original problem, the method calls a fresh copy of itself to work on the smaller problem; this is referred to as a recursive call and is also called the recursion step. The recursion step normally includes a return
statement, because its result will be combined with the portion of the problem the method knew how to solve to form a result that will be passed back to the original caller.
The recursion step executes while the original call to the method is still active (i.e., while it has not finished executing). The recursion step can result in many more recursive calls, as the method divides each new subproblem into two conceptual pieces. For the recursion to terminate eventually, each time the method calls itself with a slightly simpler version of the original problem, the sequence of smaller and smaller problems must converge on the base case. At that point, the method recognizes the base case and returns a result to the previous copy of the method. A sequence of returns ensues until the original method call returns the result to the caller. This process sounds complex compared with the conventional problem solving we’ve performed to this point.
As an example of recursion concepts at work, let’s write a recursive application to perform a popular mathematical calculation. Consider the factorial of a nonnegative integer n, written n! (and pronounced “n factorial”), which is the product
n · (n – 1) · (n – 2) · ... · 1 |
1! is equal to 1 and 0! is defined to be 1. For example, 5! is the product 5 · 4 · 3 · 2 · 1, which is equal to 120.
The factorial of an integer, number
, greater than or equal to 0 can be calculated iteratively (nonrecursively) using the for
statement as follows:
factorial = 1; for ( int counter = number; counter >= 1; counter-- ) factorial *= counter; |
A recursive declaration of the factorial method is arrived at by observing the following relationship:
n! = n · (n – 1)! |
For example, 5! is clearly equal to 5 · 4!, as is shown by the following equations:
5! = 5 · 4 · 3 · 2 · 1 |
The evaluation of 5! would proceed as shown in Fig. 7.13. Figure 7.13(a) shows how the succession of recursive calls proceeds until 1! is evaluated to be 1, which terminates the recursion. Figure 7.13(b) shows the values returned from each recursive call to its caller until the value is calculated and returned.
Figure 7.14 uses recursion to calculate and display the factorials of the integers from 0 to 10. The recursive method Factorial
(lines 16–24) first tests to determine whether a terminating condition (line 19) is true
. If number
is less than or equal to 1
(the base case), Factorial
returns 1
, no further recursion is necessary and the method returns. If number
is greater than 1
, line 23 expresses the problem as the product of number
and a recursive call to Factorial
evaluating the factorial of number - 1
, which is a slightly simpler problem than the original calculation, Factorial( number )
.
Example 7.14. Recursive Factorial
method.
1 // Fig. 7.14: FactorialTest.cs 2 // Recursive Factorial method. 3 using System; 4 5 public class FactorialTest 6 { 7 public static void Main( string[] args ) 8 { 9 // calculate the factorials of 0 through 10 10 for ( long counter = 0; counter <= 10; counter++ ) 11 Console.WriteLine( "{0}! = {1}", 12 counter, Factorial( counter ) ); 13 } // end Main 14 15 // recursive declaration of method Factorial 16 public static long Factorial( long number ) 17 { 18 // base case 19 if ( number <= 1 ) 20 return 1; 21 // recursion step 22 else 23 return number * Factorial( number - 1 ); 24 } // end method Factorial 25 } // end class FactorialTest
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800 |
Method Factorial
(lines 16–24) receives a parameter of type long
and returns a result of type long
. As you can see in Fig. 7.14, factorial values become large quickly. We chose type long
(which can represent relatively large integers) so that the application could calculate factorials greater than 20!. Unfortunately, the Factorial
method produces large values so quickly that factorial values soon exceed even the maximum value that can be stored in a long
variable. Due to the restrictions on the integral types, variables of type float
, double
or decimal
might ultimately be needed to calculate factorials of larger numbers. This situation points to a weakness in many programming languages—the languages are not easily extended to handle the unique requirements of various applications. As you know, C# allows you to create a type that supports arbitrarily large integers if you wish. For example, you could create a HugeInteger
class (which we ask you to do in Exercise 10.10) that would enable an application to calculate the factorials of arbitrarily large numbers. You can also use the new type BigInteger
from the .NET Framework’s class library.
Either omitting the base case or writing the recursion step incorrectly so that it does not converge on the base case will cause infinite recursion, eventually exhausting memory. This error is analogous to the problem of an infinite loop in an iterative (nonrecursive) solution.
Two ways to pass arguments to functions in many programming languages are pass-by-value and pass-by-reference. When an argument is passed by value (the default in C#), a copy of its value is made and passed to the called function. Changes to the copy do not affect the original variable’s value in the caller. This prevents the accidental side effects that so greatly hinder the development of correct and reliable software systems. Each argument that has been passed in the programs in this chapter so far has been passed by value. When an argument is passed by reference, the caller gives the method the ability to access and modify the caller’s original variable.
Pass-by-reference can weaken security, because the called function can corrupt the caller’s data.
To pass an object by reference into a method, simply provide as an argument in the method call the variable that refers to the object. Then, in the method body, reference the object using the parameter name. The parameter refers to the original object in memory, so the called method can access the original object directly.
Previously, we discussed the difference between value types and reference types. A major difference between them is that value-type variables store values, so specifying a value-type variable in a method call passes a copy of that variable’s value to the method. Reference-type variables store references to objects, so specifying a reference-type variable as an argument passes the method a copy of the actual reference that refers to the object. Even though the reference itself is passed by value, the method can still use the reference it receives to interact with—and possibly modify—the original object. Similarly, when returning information from a method via a return
statement, the method returns a copy of the value stored in a value-type variable or a copy of the reference stored in a reference-type variable. When a reference is returned, the calling method can use that reference to interact with the referenced object.
What if you would like to pass a variable by reference so the called method can modify the variable’s value? To do this, C# provides keywords ref
and out
. Applying the ref
keyword to a parameter declaration allows you to pass a variable to a method by reference—the called method will be able to modify the original variable in the caller. The ref
keyword is used for variables that already have been initialized in the calling method. Normally, when a method call contains an uninitialized variable as an argument, the compiler generates an error. Preceding a parameter with keyword out
creates an output parameter. This indicates to the compiler that the argument will be passed into the called method by reference and that the called method will assign a value to the original variable in the caller. If the method does not assign a value to the output parameter in every possible path of execution, the compiler generates an error. This also prevents the compiler from generating an error message for an uninitialized variable that’s passed as an argument to a method. A method can return only one value to its caller via a return statement, but can return many values by specifying multiple output (ref
and/or out
) parameters.
You can also pass a reference-type variable by reference, which allows you to modify reference-type variable so that it refers to a new object. Passing a reference by reference is a tricky but powerful technique that we discuss in Section 8.8.
The application in Fig. 7.15 uses the ref
and out
keywords to manipulate integer values. The class contains three methods that calculate the square of an integer. Method SquareRef
(lines 37–40) multiplies its parameter x
by itself and assigns the new value to x
. SquareRef
’s parameter is declared as ref int
, which indicates that the argument passed to this method must be an integer that’s passed by reference. Because the argument is passed by reference, the assignment at line 39 modifies the original argument’s value in the caller.
Example 7.15. Reference, output and value parameters.
1 // Fig. 7.15: ReferenceAndOutputParameters.cs 2 // Reference, output and value parameters. 3 using System; 4 5 class ReferenceAndOutputParameters 6 { 7 // call methods with reference, output and value parameters 8 public static void Main( string[] args ) 9 { 10 int y = 5; // initialize y to 5 11 int z; // declares z, but does not initialize it 12 13 // display original values of y and z 14 Console.WriteLine( "Original value of y: {0}", y ); 15 Console.WriteLine( "Original value of z: uninitialized " ); 16 17 // pass y and z by reference 18 SquareRef( ref y ); // must use keyword ref 19 SquareOut( out z ); // must use keyword out 20 21 // display values of y and z after they are modified by 22 // methods SquareRef and SquareOut, respectively 23 Console.WriteLine( "Value of y after SquareRef: {0}", y ); 24 Console.WriteLine( "Value of z after SquareOut: {0} ", z ); 25 26 // pass y and z by value 27 Square( y ); 28 Square( z ); 29 30 // display values of y and z after they are passed to method Square 31 // to demonstrate that arguments passed by value are not modified 32 Console.WriteLine( "Value of y after Square: {0}", y ); 33 Console.WriteLine( "Value of z after Square: {0}", z ); 34 } // end Main 35 36 // uses reference parameter x to modify caller's variable 37 static void SquareRef( ref int x ) 38 { 39 x = x * x; // squares value of caller's variable 40 } // end method SquareRef 41 42 // uses output parameter x to assign a value 43 // to an uninitialized variable 44 static void SquareOut( out int x ) 45 { 46 x = 6; // assigns a value to caller's variable 47 x = x * x; // squares value of caller's variable 48 } // end method SquareOut 49 50 // parameter x receives a copy of the value passed as an argument, 51 // so this method cannot modify the caller's variable 52 static void Square( int x ) 53 { 54 x = x * x; 55 } // end method Square 56 } // end class ReferenceAndOutputParameters
Method SquareOut
(lines 44–48) assigns its parameter the value 6
(line 46), then squares that value. SquareOut
’s parameter is declared as out int
, which indicates that the argument passed to this method must be an integer that’s passed by reference and that the argument does not need to be initialized in advance.
Method Square
(lines 52–55) multiplies its parameter x
by itself and assigns the new value to x
. When this method is called, a copy of the argument is passed to the parameter x
. Thus, even though parameter x
is modified in the method, the original value in the caller is not modified.
Method Main
(lines 8–34) invokes methods SquareRef
, SquareOut
and Square
. We begin by initializing variable y
to 5
and declaring, but not initializing, variable z
. Lines 18–19 call methods SquareRef
and SquareOut
. Notice that when you pass a variable to a method with a reference parameter, you must precede the argument with the same keyword (ref
or out
) that was used to declare the reference parameter. Lines 23–24 display the values of y
and z
after the calls to SquareRef
and SquareOut
. Notice that y
has been changed to 25
and z
has been set to 36
.
Lines 27–28 call method Square
with y
and z
as arguments. In this case, both variables are passed by value—only copies of their values are passed to Square
. As a result, the values of y
and z
remain 25
and 36
, respectively. Lines 32–33 output the values of y
and z
to show that they were not modified.
The ref
and out
arguments in a method call must match the parameters specified in the method declaration; otherwise, a compilation error occurs.
By default, C# does not allow you to choose whether to pass each argument by value or by reference. Value types are passed by value. Objects are not passed to methods; rather, references to objects are passed to methods. The references themselves are passed by value. When a method receives a reference to an object, the method can manipulate the object directly, but the reference value cannot be changed to refer to a new object. In Section 8.8, you’ll see that references also can be passed by reference.
In this chapter, we discussed the difference between non-static
and static
methods, and we showed how to call static
methods by preceding the method name with the name of the class in which it appears and the member access (.
) operator. You saw that the Math
class in the .NET Framework Class Library provides many static
methods to perform mathematical calculations. We presented several commonly used Framework Class Library namespaces. You learned how to use operator +
to perform string
concatenations. You also learned how to declare constant values in two ways—with the const
keyword and with enum
types. We demonstrated simulation techniques and used class Random
to generate sets of random numbers. We discussed the scope of fields and local variables in a class. You saw how to overload methods in a class by providing methods with the same name but different signatures. You learned how to use optional and named parameters. We discussed how recursive methods call themselves, breaking larger problems into smaller subproblems until eventually the original problem is solved. You learned the differences between value types and reference types with respect to how they’re passed to methods, and how to use the ref
and out
keywords to pass arguments by reference.
In Chapter 8, you’ll learn how to maintain lists and tables of data in arrays. You’ll see a more elegant implementation of the application that rolls a die 6000 times and two enhanced versions of our GradeBook
case study. You’ll also learn how to access an application’s command-line arguments that are passed to method Main
when a console application begins execution.
Experience has shown that the best way to develop and maintain a large application is to construct it from small, simple pieces. This technique is called divide and conquer.
Three common ways of packaging code are methods, classes and namespaces.
Methods allow you to modularize an application by separating its tasks into self-contained units.
Dividing an application into meaningful methods makes it easier to debug and maintain.
You can call any static
method by specifying the name of the class in which it’s declared, followed by the member access (.
) operator and the method name, as in
ClassName. MethodName( arguments )
Method arguments may be constants, variables or expressions.
A constant is declared with the keyword const
—its value cannot be changed after the constant is declared.
Class Math
declares constants Math.PI
and Math.E
. Math.PI
(3.14159265358979323846) is the ratio of a circle’s circumference to its diameter. Math.E
(2.7182818284590452354) is the base value for natural logarithms.
When each object of a class maintains its own copy of an attribute, each object (instance) of the class has a separate instance of the variable. When objects of a class containing static
variables are created, all objects of that class share one copy of the class’s static
variables.
Together the static
variables and instance variables represent the fields of a class.
When you execute your application, you can specify a list of string
s (separated by spaces) as command-line arguments. The execution environment will pass these arguments to the Main
method of your application.
If you declare more than one Main
method among all the classes of your project, you’ll need to indicate which one you would like to be the application’s entry point. You can do this by clicking the menu Project > [ProjectName] Properties... and selecting the class containing the Main
method that should be the entry point from the Startup object list box.
Multiple parameters are specified as a comma-separated list.
When a method is called, each parameter is initialized with the value of the corresponding argument. There must be one argument in the method call for each required parameter in the method declaration. Each argument must be consistent with the type of the corresponding parameter.
When program control returns to the point in the application where a method was called, the method’s parameters are no longer accessible.
Methods can return at most one value; the returned value can be a value type that contains many values (implemented as a struct
) or a reference to an object that contains many values.
C# allows string
objects to be created by assembling smaller string
s into larger string
s using operator +
. This is known as string
concatenation.
Every value of a simple type in C# has a string
representation. When one of the +
operator’s operands is a string
, the other is implicitly converted to a string
, then the two are concatenated.
All objects have a ToString
method that returns a string
representation of the object. When an object is concatenated with a string
, the object’s ToString
method is implicitly called to obtain the string
representation of the object.
You’ve seen three ways to call a method—using a method name by itself to call another method of the same class; using a variable that contains a reference to an object, followed by the member access (.
) operator and the method name to call a non-static
method of the referenced object; and using the class name and the member access (.
) operator to call a static
method of a class.
A static
method can call only other static
methods of the same class directly and can manipulate only static
variables in the same class directly.
There are three ways to return control to the statement that calls a method. If the method does not return a result, control returns when the program flow reaches the method-ending right brace or when the statement
return
;
is executed. If the method returns a result, the statement
return
expression;
evaluates the expression, then returns the result to the caller.
Stacks are known as last-in, first-out (LIFO) data structures—the last item pushed (inserted) on the stack is the first item popped off (removed from) the stack.
When an application calls a method, the called method must know how to return to its caller, so the return address of the calling method is pushed onto the program-execution stack. If a series of method calls occurs, the successive return addresses are pushed onto the stack in last-in, first-out order so that each method can return to its caller.
The program-execution stack also contains the memory for the local variables used in each invocation of a method during an application’s execution. This data, stored as a portion of the program-execution stack, is known as the activation record or stack frame of the method call.
If a local variable holding a reference to an object is the only variable in the application with a reference to that object, when the activation record containing that local variable is popped off the stack, the object will eventually be deleted from memory during “garbage collection.”
The amount of memory in a computer is finite. If more method calls occur than can have their activation records stored on the program-execution stack, stack overflow occurs.
Another important feature of method calls is argument promotion—implicitly converting an argument’s value to the type that the method expects to receive in its corresponding parameter.
The argument-promotion rules apply to expressions containing values of two or more simple types and to simple-type values passed as arguments to methods.
In cases where information may be lost due to conversion between simple types, the compiler requires you to use a cast operator to explicitly force the conversion.
Many predefined classes are grouped into categories of related classes called namespaces. Together, these namespaces are referred to as the .NET Framework Class Library.
Random
method Next
generates a random int
value in the range 0 to +2,147,483,646, inclusive.
Class Random
provides other versions of method Next
. One receives an int
and returns a value from 0 up to, but not including, the argument’s value. The other receives two int
s and returns a value from the first argument’s value up to, but not including, the second argument’s value.
The methods of class Random
actually generate pseudorandom numbers based on complex mathematical calculations. The calculation that produces the pseudorandom numbers uses the time of day as a seed value to change the sequence’s starting point.
If the same seed value is used every time, the Random
object produces the same sequence of random numbers.
An enumeration is introduced by the keyword enum
and a type name. Braces delimit the body of an enum
declaration. Inside the braces is a comma-separated list of enumeration constants.
Variables of an enum
type should be assigned only constants of that enum
type.
When an enum
is declared, each constant in the enum
declaration is a constant value of type int
. If you do not assign a value to an identifier in the enum
declaration, the compiler will do so. If the first enum
constant is unassigned, the compiler gives it the value 0
. If any other enum
constant is unassigned, the compiler gives it a value equal to one more than the value of the preceding enum
constant. The enum
constant names must be unique, but their underlying values need not be.
If you need to compare a simple integral type value to the underlying value of an enumeration constant, you must use a cast operator to make the two types match.
The scope of a declaration is the portion of the application that can refer to the declared entity by its unqualified name.
The scope of a parameter declaration is the body of the method in which the declaration appears.
A local variable’s scope is from the point at which the declaration appears to the end of that block.
The scope of a local-variable declaration that appears in the initialization section of a for
statement’s header is the body of the for
statement and the other expressions in the header.
The scope of a method, property or field of a class is the entire body of the class.
Any block may contain variable declarations. If a local variable or parameter in a method has the same name as a field, the field is hidden until the block terminates execution.
Methods of the same name can be declared in the same class, as long as they have different sets of parameters. This is called method overloading. When an overloaded method is called, the C# compiler selects the appropriate method by examining the number, types and order of the arguments in the call.
The compiler distinguishes overloaded methods by their signature—a combination of the method’s name and the number, types and order of its parameters. The signature also includes the way those parameters are passed, which can be modified by the ref
and out
keywords.
The compiler will generate an error when two methods have the same signature but different return types. Overloaded methods can have the same or different return types if the methods have different parameter lists.
As of Visual C# 2010, methods can have optional parameters, which allow the calling method to vary the number of arguments to pass. An optional parameter specifies a default value that’s assigned to the parameter if the optional argument is omitted.
Methods can have one or more optional parameters. All optional parameters must be placed to the right of the method’s non-optional parameters.
When a parameter has a default value, the caller has the option of passing that particular argument.
Normally, when calling a method that has optional parameters, the argument values—in order—are assigned to the parameters from left to right in the parameter list.
Visual C# 2010 provides a new feature called named parameters, which enable you to call methods that receive optional parameters by providing only the optional arguments you wish to specify. To do so, you explicitly specify the parameter’s name and value—separated by a colon (:
)—in the argument list of the method call.
A recursive method calls itself, either directly or indirectly through another method.
When a recursive method is called to solve a problem, the method actually is capable of solving only the simplest case(s), or base case(s). If the method is called with a base case, the method returns a result.
If the method is called with a more complex problem, the method divides the problem into two conceptual pieces: a piece that the method knows how to do and a piece that it does not know how to do. Because this new problem looks like the original problem, the method calls a fresh copy of itself to work on the smaller problem; this procedure is referred to as a recursive call and is also called the recursion step.
A recursive declaration of the factorial method is arrived at by observing the relationship:
n! = n · ( n – 1)!
Two ways to pass arguments to functions in many programming languages are pass-by-value and pass-by-reference.
When an argument is passed by value (the default), a copy of the argument’s value is passed to the called function. Changes to the copy do not affect the original variable’s value in the caller.
When an argument is passed by reference, the caller gives the method the ability to access and modify the caller’s original data directly.
Value-type variables store values, so specifying a value-type variable in a method call passes a copy of that variable’s value to the method. Reference-type variables store references to objects, so specifying a reference-type variable as an argument passes the method a copy of the actual reference that refers to the object.
When returning information from a method via a return
statement, the method returns a copy of the value stored in a value-type variable or a copy of the reference stored in a reference-type variable.
C# provides the keywords ref
and out
to pass variables by reference.
A ref
parameter indicates that an argument will be passed to the method by reference—the called method will be able to modify the original variable in the caller.
An out
parameter indicates that a possibly uninitialized variable will be passed into the method by reference and that the called method will assign a value to the original variable in the caller.
A method can return only one value to its caller via a return statement, but can return many values by specifying multiple output (ref
and/or out
) parameters.
When a variable is passed to a method with a reference parameter, you must precede the variable with the same keyword (ref
or out
) that was used to declare the reference parameter.
enum
keyword
hierarchical boss-method/worker-method relationship
reusable software components
7.1 | Fill in the blanks in each of the following statements:
| |
7.1 |
| |
7.2 | For the class
| |
7.2 |
| |
7.3 | Write an application that tests whether the examples of the | |
7.3 | The following solution demonstrates the 1 // Exercise 7.3 Solution: MathTest.cs 2 // Testing the Math class methods. 3 using System; 4 5 public class MathTest 6 { 7 public static void Main( string[] args ) 8 { 9 Console.WriteLine( "Math.Abs( 23.7 ) = {0}", Math.Abs( 23.7 ) ); 10 Console.WriteLine( "Math.Abs( 0.0 ) = {0}", Math.Abs( 0.0 ) ); 11 Console.WriteLine( "Math.Abs( -23.7 ) = {0}", Math.Abs( -23.7 ) ); 12 Console.WriteLine( "Math.Ceiling( 9.2 ) = {0}", 13 Math.Ceiling( 9.2 ) ); 14 Console.WriteLine( "Math.Ceiling( -9.8 ) = {0}", 15 Math.Ceiling( -9.8 ) ); 16 Console.WriteLine( "Math.Cos( 0.0 ) = {0}", Math.Cos( 0.0 ) ); 17 Console.WriteLine( "Math.Exp( 1.0 ) = {0}", Math.Exp( 1.0 ) ); 18 Console.WriteLine( "Math.Exp( 2.0 ) = {0}", Math.Exp( 2.0 ) ); 19 Console.WriteLine( "Math.Floor( 9.2 ) = {0}", Math.Floor( 9.2 ) ); 20 Console.WriteLine( "Math.Floor( -9.8 ) = {0}", 21 Math.Floor( -9.8 ) ); 22 Console.WriteLine( "Math.Log( Math.E ) = {0}", 23 Math.Log( Math.E ) ); 24 Console.WriteLine( "Math.Log( Math.E * Math.E ) = {0}", 25 Math.Log( Math.E * Math.E ) ); 26 Console.WriteLine( "Math.Max( 2.3, 12.7 ) = {0}", 27 Math.Max( 2.3, 12.7 ) ); 28 Console.WriteLine( "Math.Max( -2.3, -12.7 ) = {0}", 29 Math.Max( -2.3, -12.7 ) ); 30 Console.WriteLine( "Math.Min( 2.3, 12.7 ) = {0}", 31 Math.Min( 2.3, 12.7 ) ); 32 Console.WriteLine( "Math.Min( -2.3, -12.7 ) = {0}", 33 Math.Min( -2.3, -12.7 ) ); 34 Console.WriteLine( "Math.Pow( 2.0, 7.0 ) = {0}", 35 Math.Pow( 2.0, 7.0 ) ); 36 Console.WriteLine( "Math.Pow( 9.0, 0.5 ) = {0}", 37 Math.Pow( 9.0, 0.5 ) ); 38 Console.WriteLine( "Math.Sin( 0.0 ) = {0}", Math.Sin( 0.0 ) ); 39 Console.WriteLine( "Math.Sqrt( 900.0 ) = {0}", 40 Math.Sqrt( 900.0 ) ); 41 Console.WriteLine( "Math.Tan( 0.0 ) = {0}", Math.Tan( 0.0 ) ); 42 } // end Main 43 } // end class MathTest
| |
7.4 | Give the method header for each of the following methods:
| |
7.4 |
| |
7.5 | Find the error in each of the following code segments. Explain how to correct the error.
| |
7.5 |
| |
7.6 | Write a complete C# application to prompt the user for the double volume = ( 4.0 / 3.0 ) * Math.PI * Math.Pow( radius, 3 ) | |
7.6 | The following solution calculates the volume of a sphere, using the radius entered by the user: 1 // Exercise 7.6 Solution: Sphere.cs 2 // Calculate the volume of a sphere. 3 using System; 4 5 public class Sphere 6 { 7 // obtain radius from user and display volume of sphere 8 public static void Main( string[] args ) 9 { 10 Console.Write( "Enter radius of sphere: " ); 11 double radius = Convert.ToDouble( Console.ReadLine() ); 12 13 Console.WriteLine( "Volume is {0:F3}", SphereVolume( radius ) ); 14 } // end Main 15 16 // calculate and return sphere volume 17 public static double SphereVolume( double radius ) 18 { 19 double volume = ( 4.0 / 3.0 ) * Math.PI * Math.Pow( radius, 3 ); 20 return volume; 21 } // end method SphereVolume 22 } // end class Sphere
|
7.7 | What is the value of
| ||||||||||||
7.8 | (Parking Charges) A parking garage charges a $2.00 minimum fee to park for up to three hours. The garage charges an additional $0.50 per hour for each hour or part thereof in excess of three hours. The maximum charge for any given 24-hour period is $10.00. Assume that no car parks for longer than 24 hours at a time. Write an application that calculates and displays the parking charges for each customer who parked in the garage yesterday. You should enter the hours parked for each customer. The application should display the charge for the current customer and should calculate and display the running total of yesterday’s receipts. The application should use method | ||||||||||||
7.9 | (Rounding to Nearest Integer) An application of method y = Math.Floor( x + 0.5 ); will round the number | ||||||||||||
7.10 | (Rounding to a Specific Decimal Place) y = Math.Floor( x * 10 + 0.5 ) / 10; rounds y = Math.Floor( x * 100 + 0.5 ) / 100; rounds
For each value read, your application should display the original value, the number rounded to the nearest integer, the number rounded to the nearest tenth, the number rounded to the nearest hundredth and the number rounded to the nearest thousandth. | ||||||||||||
7.11 | Answer each of the following questions: | ||||||||||||
7.12 | Write statements that assign random integers to the variable n in the following ranges. Assume
| ||||||||||||
7.13 | For each of the following sets of integers, write a single statement that will display a number at random from the set. Assume
| ||||||||||||
7.14 | (Exponentiation) Write a method For example, | ||||||||||||
7.15 | (Hypotenuse of a Right Triangle) Write method Table 7.16. Values for the sides of triangles in Exercise 7.15.
| ||||||||||||
7.16 | (Multiples) Write method | ||||||||||||
7.17 | (Even or Odd) Write method | ||||||||||||
7.18 | (Displaying a Square of Asterisks) Write method **** **** **** **** Incorporate this method into an application that reads an integer value for | ||||||||||||
7.19 | (Displaying a Square of Any Character) Modify the method created in Exercise 7.18 to form the square out of whatever character is contained in character parameter ##### ##### ##### ##### ##### [Hint: Use the expression | ||||||||||||
7.20 | (Circle Area) Write an application that prompts the user for the radius of a circle and uses method | ||||||||||||
7.21 | (Separating Digits) Write code segments that accomplish each of the following tasks:
| ||||||||||||
7.22 | (Temperature Conversions) Implement the following integer methods:
| ||||||||||||
7.23 | (Find the Minimum) Write a method | ||||||||||||
7.24 | (Perfect Numbers) An integer number is said to be a perfect number if its factors, including 1 (but not the number itself), sum to the number. For example, 6 is a perfect number, because 6 = 1 + 2 + 3. Write method | ||||||||||||
7.25 | (Prime Numbers) An integer is said to be prime if it’s greater than 1 and divisible by only 1 and itself. For example, 2, 3, 5 and 7 are prime, but 4, 6, 8 and 9 are not.
| ||||||||||||
7.26 | (Reversing Digits) Write a method that takes an integer value and returns the number with its digits reversed. For example, given the number 7631, the method should return 1367. Incorporate the method into an application that reads a value from the user and displays the result. | ||||||||||||
7.27 | (Greatest Common Divisor) The greatest common divisor (GCD) of two integers is the largest integer that evenly divides each of the two numbers. Write method | ||||||||||||
7.28 | (Converting Grade Averages to a Four-Point Scale) Write method | ||||||||||||
7.29 | (Coin Tossing) Write an application that simulates coin tossing. Let the application toss a coin each time the user chooses the “ | ||||||||||||
7.30 | (Guess the Number Game) Write an application that plays “guess the number” as follows: Your application chooses the number to be guessed by selecting a random integer in the range 1 to 1000. The application displays the prompt | ||||||||||||
7.31 | (Enhanced Guess the Number Game) Modify the application of Exercise 7.30 to count the number of guesses the player makes. If the number is 10 or fewer, display | ||||||||||||
7.32 | (Distance Between Two Points) Write method | ||||||||||||
7.33 | (Craps Game Modification) Modify the craps application of Fig. 7.8 to allow wagering. Initialize variable | ||||||||||||
7.34 | (Binary, Octal and Hexadecimal) Write an application that displays a table of the binary, octal, and hexadecimal equivalents of the decimal numbers in the range 1–256. If you’re not familiar with these number systems, read Appendix D first. | ||||||||||||
7.35 | (Recursive Power Calculation) Write recursive method base exponent For example, base exponent = base · base exponent – 1 The terminating condition occurs when base1 = base Incorporate this method into an application that enables the user to enter the | ||||||||||||
7.36 | (Towers of Hanoi) Every budding computer scientist must grapple with certain classic problems, and the Towers of Hanoi (see Fig. 7.17) is one of the most famous. Legend has it that in a temple in the Far East, priests are attempting to move a stack of disks from one peg to another. The initial stack has 64 disks threaded onto one peg and arranged from bottom to top by decreasing size. The priests are attempting to move the stack from this peg to a second peg under the constraints that exactly one disk is moved at a time and at no time may a larger disk be placed above a smaller disk. A third peg is available for temporarily holding disks. Supposedly, the world will end when the priests complete their task, so there’s little incentive for us to facilitate their efforts. Let’s assume that the priests are attempting to move the disks from peg 1 to peg 3. We wish to develop an algorithm that will display the precise sequence of peg-to-peg disk transfers. If we were to approach this problem with conventional methods, we would rapidly find ourselves hopelessly knotted up in managing the disks. Instead, if we attack the problem with recursion in mind, it immediately becomes tractable. Moving n disks can be viewed in terms of moving only n − 1 disks (hence the recursion) as follows:
The process ends when the last task involves moving n = 1 disk (i.e., the base case). This task is accomplished by simply moving the disk, without the need for a temporary holding area. Write an application to solve the Towers of Hanoi problem. Allow the user to enter the number of disks. Use a recursive
Your application should display the precise instructions it will take to move the disks from the starting peg to the destination peg. For example, to move a stack of three disks from peg 1 to peg 3, your application should display the following series of moves:
| ||||||||||||
7.37 | (What Does This Code Do?) What does the following method do? // Parameter b must be a positive to prevent infinite recursion public static int Mystery( int a, int b ) { if ( b == 1 ) return a; else return a + Mystery( a, b - 1 ); } | ||||||||||||
7.38 | (Find the Error) Find the error in the following recursive method, and explain how to correct it: public static int Sum( int n ) { if ( n == 0 ) return 0; else return n + Sum( n ); } |
As computer costs decline, it becomes feasible for every student, regardless of economic circumstance, to have a computer and use it in school. This creates exciting possibilities for improving the educational experience of all students worldwide as suggested by the next two exercises. [Note: Check out initiatives such as the One Laptop Per Child Project (www.laptop.org). Also, research “green” laptops—and note the key “going green” characteristics of these devices. Look into the Electronic Product Environmental Assessment Tool (www.epeat.net) which can help you assess the “greenness” of desktops, notebooks and monitors to help you decide which products to purchase.]
7.39 | (Computer-Assisted Instruction) The use of computers in education is referred to as computer-assisted instruction (CAI). Write a program that will help an elementary school student learn multiplication. Use a How much is 6 times 7? The student then inputs the answer. Next, the program checks the student’s answer. If it’s correct, display the message |
7.40 | (Computer-Assisted Instruction: Reducing Student Fatigue) One problem in CAI environments is student fatigue. This can be reduced by varying the computer’s responses to hold the student’s attention. Modify the program of Exercise 7.39 so that various comments are displayed for each answer. Possible responses to a correct answer: Very good! Excellent! Nice work! Keep up the good work! Possible responses to an incorrect answer: No. Please try again. Wrong. Try once more. Don't give up! No. Keep trying. Use random-number generation to choose a number from 1 to 4 that will be used to select one of the four appropriate responses to each correct or incorrect answer. Use a |
18.222.182.66