The Structure of a Method
Local Variables
Method Invocations
Return Values
Parameters
Value Parameters
Reference Parameters
Output Parameters
Parameter Arrays
Summary of Parameter Types
Stack Frames
Recursion
Method Overloading
A method is a block of code with a name. You can execute the code from somewhere else in the program by using the method’s name. You can also pass data into a method and receive data back as output.
As you saw in the previous chapter, a method is a function member of a class. Methods have two major sections, as shown in Figure 5-1—the method header and the method body.
Figure 5-1. The structure of a method
The following example shows the form of the method header. I’ll cover each part in the following pages.
For example, the following code shows a simple method called MyMethod
that, in turn, calls the WriteLine
method several times:
void MyMethod()
{
Console.WriteLine("First");
Console.WriteLine("Last");
}
Although these first few chapters describe classes, there’s another user-defined type called a struct
, which I’ll cover in Chapter 12. Most of what this chapter covers about class methods is also true for struct
methods.
The method body is a block, which (as you will recall from Chapter 2) is a sequence of statements between curly braces. A block can contain the following items:
Figure 5-2 shows an example of a method body and some of its components.
Figure 5-2. Method body example
Like fields, local variables store data. While fields usually store data about the state of the object, local variables are usually created to store data for local, or transitory, computations. Table 5-1 compares and contrasts local variables and instance fields.
The following line of code shows the syntax of local variable declarations. The optional initializer consists of the equals sign followed by a value to be used to initialize the variable.
The following example shows the declaration and use of two local variables. The first is of type int
, and the second is of type SomeClass
.
static void Main( )
{
int myInt = 15;
SomeClass sc = new SomeClass();
...
}
Table 5-1. Instance Fields vs. Local Variables
Instance Field | Local Variable | |
Lifetime | Starts when the class instance is created. Ends when the class instance is no longer accessible. | Starts at the point in the block where it is declared. Ends when the block completes execution. |
Implicit initialization | Initialized to a default value for the type. | No implicit initialization. The compiler produces an error message if the variable isn’t assigned to before use. |
Storage area | All the fields of a class are stored in the heap, regardless of whether they’re value types or reference types. | Value type: Stored on the stack. Reference type: Reference stored on the stack and data stored in the heap. |
If you look at the following code, you’ll see that when you supply the type name at the beginning of the declaration, you are supplying information that the compiler should already be able to infer from the right side of the initialization.
15
is an int
.MyExcellentClass
.So in both cases, including the explicit type name at the beginning of the declaration is redundant.
static void Main( )
{
int total = 15;
MyExcellentClass mec = new MyExcellentClass();
...
}
Starting with C# 3.0 you can use the new keyword var
in place of the explicit type name at the beginning of the variable declaration, as follows:
The var
keyword does not signal a special kind of variable. It’s just syntactic shorthand for whatever type can be inferred from the initialization on the right side of the statement. In the first declaration, it is shorthand for int
. In the second, it is shorthand for MyExcellentClass
. The preceding code segment with the explicit type names and the code segment with the var
keywords are semantically equivalent.
Some important conditions on using the var
keyword are the following:
Note The var
keyword is not like the JavaScript var
that can reference different types. It’s shorthand for the actual type inferred from the right side of the equals sign. The var
keyword does not change the strongly typed nature of C#.
Method bodies can have other blocks nested inside them.
Figure 5-3 illustrates the lifetimes of two local variables, showing the code and the state of the stack. The arrows indicate the line that has just been executed.
var1
is declared in the body of the method, before the nested block.var2
is declared inside the nested block. It exists from the time it’s declared, until the end of the block in which it was declared.Figure 5-3. The lifetime of a local variable
Note In C and C++ you can declare a local variable, and then within a nested block you can declare another local variable with the same name. The inner name masks the outer name while within the inner scope. In C#, however, you cannot declare another local variable with the same name within the scope of the first name regardless of the level of nesting.
A local constant is much like a local variable, except that once it is initialized, its value can’t be changed. Like a local variable, a local constant must be declared inside a block.
The two most important characteristics of a constant are the following:
The core declaration for a constant is shown following. The syntax is the same as that of a field or variable declaration, except for the following:
const
before the type.null
reference, but it cannot be a reference to an object, because references to objects are determined at run time. Note The keyword const
is not a modifier but part of the core declaration. It must be placed immediately before the type.
A local constant, like a local variable, is declared in a method body or code block, and it goes out of scope at the end of the block in which it is declared. For example, in the following code, local constant PI
goes out of scope at the end of method DisplayRadii
.
void DisplayRadii()
{
const double PI = 3.1416; // Declare local constant
for (int radius = 1; radius <= 5; radius++)
{
double area = radius * radius * PI; // Read from local constant
Console.WriteLine
("Radius: {0}, Area: {1}" radius, area);
}
}
Methods contain most of the code for the actions that comprise a program. The remainder is in other function members, such as properties and operators—but the bulk is in methods.
The term flow of control refers to the flow of execution through your program. By default, program execution moves sequentially from one statement to the next. The control statements allow you to modify the order of execution.
In this section, I’ll just mention some of the available control statements you can use in your code. Chapter 9 covers them in detail.
if
: Conditional execution of a statementif…else
: Conditional execution of one statement or anotherswitch
: Conditional execution of one statement from a setfor
: Loop—testing at the topwhile
: Loop—testing at the topdo
: Loop—testing at the bottomforeach
: Execute once for each member of a setbreak
: Exit the current loop.continue
: Go to the bottom of the current loop.goto
: Go to a named statement.return
: Return execution to the calling method.For example, the following method shows two of the flow-of-control statements. Don’t worry about the details.
You can call other methods from inside a method body.
For example, the following class declares a method called PrintDateAndTime
, which is called from inside method Main
:
Figure 5-4 illustrates the sequence of actions when a method is called:
Figure 5-4. Flow of control when calling a method
A method can return a value to the calling code. The returned value is inserted into the calling code at the position in the expression where the invocation occurred.
void
.The following code shows two method declarations. The first returns a value of type int
. The second doesn’t return a value.
A method that declares a return type must return a value from the method by using the following form of the return statement, which includes an expression after the keyword return
. Every path through the method must end with a return
statement of this form.
For example, the following code shows a method called GetHour
, which returns a value of type int
.
You can also return objects of user-defined types. For example, the following code returns an object of type MyClass
:
As another example, in the following code, method GetHour
is called in the WriteLine
statement in Main
and returns an int
value to that position in the WriteLine
statement.
In the previous section, you saw that methods that return a value must contain return statements. Void methods do not require return statements. When the flow of control reaches the closing curly brace of the method body, control returns to the calling code, and no value is inserted back into the calling code.
Often, however, you can simplify your program logic by exiting the method early when certain conditions apply.
void
method at any time by using the following form of the return statement, with no parameters:
return;
void
.For example, the following code shows the declaration of a void method called SomeMethod
, which has three possible places it might return to the calling code. The first two places are in branches called if
statements, which are covered in Chapter 9. The last place is the end of the method body.
The following code shows an example of a void method with a return statement. The method writes out a message only if the time is after noon. The process, which is illustrated in Figure 5-5, is as follows:
WriteLine
statement, which writes an informative message to the screen.Figure 5-5. Using a return statement with a void return type
So far, you've seen that methods are named units of code that can be called from many places in a program and can return a single value to the calling code. Returning a single value is certainly valuable, but what if you need to return multiple values? Also, it would be useful to be able to pass data into a method when it starts execution. Parameters are special variables that allow you to do both these things.
Formal parameters are local variables that are declared in the method declaration's parameter list, rather than in the body of the method.
The following method header shows the syntax of parameter declarations. It declares two formal parameters—one of type int
and the other of type float
.
The formal parameters are used throughout the method body, for the most part, just like other local variables. For example, the following declaration of method PrintSum
uses two formal parameters, x
and y
, and a local variable, sum
, all of which are of type int
.
public void PrintSum( int x, int y )
{
int sum = x + y;
Console.WriteLine("Newsflash: {0} + {1} is {2}", x, y, sum);
}
When your code calls a method, the values of the formal parameters must be initialized before the code in the method begins execution.
For example, the following code shows the invocation of method PrintSum
, which has two actual parameters of data type int
:
When the method is called, the value of each actual parameter is used to initialize the corresponding formal parameter. The method body is then executed. Figure 5-6 illustrates the relationship between the actual parameters and the formal parameters.
Figure 5-6. Actual parameters initialize the corresponding formal parameters.
Notice that in the previous example code, and in Figure 5-6, the number of actual parameters must be the same as the number of formal parameters (with the exception of params parameters, which I'll discuss later). Parameters that follow this pattern are called positional parameters. We'll look at some other options shortly.
In the following code, class MyClass
declares two methods—one that takes two integers and returns their sum and another that takes two float
s and returns their average. In the second invocation, notice that the compiler has implicitly converted the two int
values—5
and someInt
—to the float
type.
This code produces the following output:
Newsflash: Sum: 5 and 6 is 11
Newsflash: Avg: 5 and 6 is 5.5
There are several kinds of parameters, which pass data to and from the method in slightly different ways. The kind we've looked at so far is the default type and is called a value parameter.
When you use value parameters, data is passed to the method by copying the value of the actual parameter to the formal parameter. When a method is called, the system does the following:
An actual parameter for a value parameter doesn't have to be a variable. It can be any expression evaluating to the matching data type. For example, the following code shows two method calls. In the first, the actual parameter is a variable of type float
. In the second, it's an expression that evaluates to float
.
Before you can use a variable as an actual parameter, that variable must have been assigned a value (except in the case of output parameters, which I'll cover shortly). For reference types, the variable can be assigned either an actual reference or null
.
Note Chapter 3 covered value types, which, as you will remember, are types that contain their own data. Don't be confused that I'm now talking about value parameters. They're entirely different. Value parameters are parameters where the value of the actual parameter is copied to the formal parameter.
For example, the following code shows a method called MyMethod
, which takes two parameters—a variable of type MyClass
and an int
.
int
type field belonging to the class and to the int
.MyMethod
uses the modifier static
, which I haven't explained yet. You can ignore it for now. I'll explain static methods in Chapter 6.Figure 5-7 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method:
a1
and a2
, which will be used as the actual parameters, are already on the stack.a1
is a reference type, the reference is copied, resulting in both the actual and formal parameters referring to the same object in the heap.a2
is a value type, the value is copied, producing an independent data item.f2
and the field of object f1
have been incremented by 5.
a2
, the value type, is unaffected by the activity in the method.a1
, the reference type, however, has been changed by the activity in the method.Figure 5-7. Value parameters
The second type of parameter is called a reference parameter.
ref
modifier in both the declaration and the invocation of the method.null
.For example, the following code illustrates the syntax of the declaration and invocation:
In the previous section you saw that for value parameters, the system allocates memory on the stack for the formal parameters. In contrast, for reference parameters:
Since the formal parameter name and the actual parameter name are acting as if they reference the same memory location, clearly any changes made to the formal parameter during method execution are visible after the method is completed, through the actual parameter variable.
Note Remember to use the ref
keyword in both the method declaration and the invocation.
For example, the following code shows method MyMethod
again, but this time the parameters are reference parameters rather than value parameters:
Figure 5-8 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method:
a1
and a2
, which will be used as the actual parameters, are already on the stack.a1
and f1
as if they referred to the same memory location and a2
and f2
as if they referred to the same memory location.f2
and the field of the object of f1
have been incremented by 5.a2
, which is the value type, and the value of the object pointed at by a1
, which is the reference type, have been changed by the activity in the method.Figure 5-8. With a reference parameter, the formal parameter behaves as if it were an alias for the actual parameter.
Output parameters are used to pass data from inside the method back out to the calling code. Their behavior is very similar to reference parameters. Like reference parameters, output parameters have the following requirements:
out
, rather than ref
.For example, the following code declares a method called MyMethod
, which takes a single output parameter.
Like reference parameters, the formal parameters of output parameters act as if they were aliases for the actual parameters. Any changes made to a formal parameter inside the method are visible through the actual parameter variable after the method completes execution.
Unlike reference parameters, output parameters require the following:
Since the code inside the method must write to an output parameter before it can read from it, it is impossible to send data into a method using output parameters. In fact, if there is any execution path through the method that attempts to read the value of an output parameter before the method has assigned it a value, the compiler produces an error message.
public void Add2( out int outValue )
{
int var1 = outValue + 2; // Error! Can't read from an output parameter
} // before it has been assigned to by the method.
For example, the following code again shows method MyMethod
, but this time using output parameters:
Figure 5-9 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method.
a1
and a2
, which will be used as the actual parameters, are already on the stack.a1
and f1
as if they referred to the same memory location, and you can think of a2
and f2
as if they referred to the same memory location. The names a1
and a2
are out of scope and cannot be accessed from inside MyMethod
.MyClass
and assigns it to f1
. It then assigns a value to f1
's field and also assigns a value to f2
. The assignments to f1
and f2
are both required, since they're output parameters.a1
, the reference type, and a2
, the value type, have been changed by the activity in the method.Figure 5-9. With an output parameter, the formal parameter behaves as if it were an alias for the actual parameter, but with the additional requirement that it must be assigned to inside the method.
In the parameter types I've covered so far, there must be exactly one actual parameter for each formal parameter. Parameter arrays are different in that they allow zero or more actual parameters for a particular formal parameter. Important points about parameter arrays are the following:
To declare a parameter array, you must do the following:
params
modifier before the data type.The following method header shows the syntax for the declaration of a parameter array of type int
. In this example, formal parameter inVals
can represent zero or more actual int
parameters.
The empty set of square brackets after the type name specifies that the parameter will be an array of int
s. You don't need to worry about the details of arrays here. They're covered in detail in Chapter 14. For our purposes here, though, all you need to know is the following:
You can supply the actual parameters for a parameter array in two ways. The forms you can use are the following:
ListInts( 10, 20, 30 ); // Three ints
int[] intArray = {1, 2, 3};
ListInts( intArray ); // An array variable
Notice in these examples that you do not use the params
modifier in the invocation. The usage of the modifier in parameter arrays doesn't fit the pattern of the other parameter types.
params
modifier is the following:
The first form of method invocation, where you use separate actual parameters in the invocation, is sometimes called the expanded form.
For example, the declaration of method ListInts
in the following code matches all the method invocations below it, even though they have different numbers of actual parameters.
void ListInts( params int[] inVals ) { ... } // Method declaration
...
ListInts( ); // 0 actual parameters
ListInts( 1, 2, 3 ); // 3 actual parameters
ListInts( 4, 5, 6, 7 ); // 4 actual parameters
ListInts( 8, 9, 10, 11, 12 ); // 5 actual parameters
When you use an invocation with separate actual parameters for a parameter array, the compiler does the following:
For example, the following code declares a method called ListInts
, which takes a parameter array. Main
declares three int
s and passes them to the array.
This code produces the following output:
50
60
70
5, 6, 7
Figure 5-10 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method:
inVals
.null
and then processes the array by multiplying each element in the array by 10 and storing it back.inVals
, is out of scope.Figure 5-10. Parameter array
An important thing to remember about parameter arrays is that when an array is created in the heap, the values of the actual parameters are copied to the array. In this way, they're like value parameters.
You can also create and populate an array before the method call and pass the single array variable as the actual parameter. In this case, the compiler uses your array, rather than creating one.
For example, the following code uses method ListInts
, declared in the previous example. In this code, Main
creates an array and uses the array variable as the actual parameter, rather than using separate integers.
static void Main()
{
int[] myArr = new int[] { 5, 6, 7 }; // Create and initialize array.
MyClass mc = new MyClass();
mc.ListInts(myArr); // Call method to print the values.
foreach (int x in myArr)
Console.WriteLine("{0}", x); // Print out each element.
}
This code produces the following output:
50
60
70
50
60
70
Since there are four parameter types, it's sometimes difficult to remember their various characteristics. Table 5-2 summarizes them, making it easier to compare and contrast them.
Table 5-2. Summary of Parameter Type Syntactic Usage
Parameter Type | Modifier | Used at Declaration? |
Used at Invocation? |
Implementation |
Value | None | The system copies the value of the actual parameter to the formal parameter. | ||
Reference | ref |
Yes | Yes | The formal parameter aliases the actual parameter. |
Output | out |
Yes | Yes | The formal parameter aliases the actual parameter. |
Array | params |
Yes | No | This allows passing a variable number of actual parameters to a method. |
A class can have more than one method with the same name. This is called method overloading. Each method with the same name must have a different signature than the others.
For example, the following four methods are overloads of the method name AddValues
:
class A
{
long AddValues( int a, int b) { return a + b; }
long AddValues( int c, int d, int e) { return c + d + e; }
long AddValues( float f, float g) { return (long)(f + g); }
long AddValues( long h, long m) { return h + m; }
}
The following code shows an illegal attempt at overloading the method name AddValues
. The two methods differ only on the return types and the names of the formal parameters. But they still have the same signature, because they have the same method name; and the number, types, and order of their parameters are the same. The compiler would produce an error message for this code.
So far in our discussion of parameters we've used positional parameters, which, as you'll remember, means that the position of each actual parameter matches the position of the corresponding formal parameter.
Starting with C# 4.0, you can list the actual parameters in your method invocation in any order, as long as you explicitly specify the names of the parameters. The details are the following:
a
, b
, and c
are the names of the three formal parameters of method Calc
:Figure 5-11 illustrates the structure of using named parameters.
Figure 5-11. When using named parameters, include the parameter name in the method invocation. No changes are needed in the method declaration.
You can use both positional and named parameters in an invocation, but all the positional parameters must be listed first. For example, the following code shows the declaration of a method called Calc
, along with five different calls to the method using different combinations of positional and named parameters:
class MyClass
{
public int Calc( int a, int b, int c )
{ return ( a + b ) * c; }
static void Main()
{
MyClass mc = new MyClass( );
int r0 = mc.Calc( 4, 3, 2 ); // Positional Parameters
int r1 = mc.Calc( 4, b: 3, c: 2 ); // Positional and Named Parameters
int r2 = mc.Calc( 4, c: 2, b: 3 ); // Switch order
int r3 = mc.Calc( c: 2, b: 3, a: 4 ); // All named parameters
int r4 = mc.Calc( c: 2, b: 1 + 2, a: 3 + 1 ); // Named parameter expressions
Console.WriteLine("{0}, {1}, {2}, {3}, {4}", r0, r1, r2, r3, r4);
}
}
This code produces the following output:
14, 14, 14, 14, 14
Named parameters are useful as a means of self-documenting a program, in that they can show, at the position of the method call, what values are being assigned to which formal parameters. For example, in the following two calls to method GetCylinderVolume
, the second call is a bit more informative and less prone to error.
Another feature introduced in C# 4.0, is called optional parameters. An optional parameter is a parameter that you can either include or omit when invoking the method.
To specify that a parameter is optional, you need to include a default value for that parameter in the method declaration. The syntax for specifying the default value is the same as that of initializing a local variable, as shown in the method declaration of the following code. In this example:
b
is assigned the default value 3.This code produces the following output:
11, 8
There are several important things to know about declaring optional parameters:
null
.Figure 5-12. Optional parameters can only be value parameter types.
params
parameter, it must be declared after all the optional parameters. Figure 5-13 illustrates the required syntactic order.Figure 5-13. In the method declaration, optional parameters must be declared after all the required parameters and before the params parameter, if one exists.
As you saw in the previous example, you use the default value of an optional parameter by leaving out the corresponding actual parameter from the method invocation. You can't, however, omit just any combination of optional parameters because in many situations it would be ambiguous as to which optional parameters to use. The rules are the following:
class MyClass
{
public int Calc( int a = 2, int b = 3, int c = 4 )
{
return (a + b) * c;
}
static void Main()
{
MyClass mc = new MyClass( );
int r0 = mc.Calc( 5, 6, 7 ); // Use all explicit values.
int r1 = mc.Calc( 5, 6 ); // Use default for c.
int r2 = mc.Calc( 5 ); // Use default for b and c.
int r3 = mc.Calc( ); // Use all defaults.
Console.WriteLine( "{0}, {1}, {2}, {3}", r0, r1, r2, r3 );
}
}
This code produces the following output:
77, 44, 32, 20
To omit optional parameters from arbitrary positions within the list of optional parameters, rather than from the end of the list, you must use the names of the optional parameters to disambiguate the assignments. You are therefore using both the named parameters and optional parameters features, as illustrated in the following code:
class MyClass
{
double GetCylinderVolume( double radius = 3.0, double height = 4.0 )
{
return 3.1416 * radius * radius * height;
}
static void Main()
{
MyClass mc = new MyClass();
double volume;
volume = mc.GetCylinderVolume( 3.0, 4.0 ); // Positional
Console.WriteLine( "Volume = " + volume );
volume = mc.GetCylinderVolume( radius: 2.0 ); // Use default height
Console.WriteLine( "Volume = " + volume );
volume = mc.GetCylinderVolume( height: 2.0 ); // Use default radius
Console.WriteLine( "Volume = " + volume );
volume = mc.GetCylinderVolume( ); // Use both defaults
Console.WriteLine( "Volume = " + volume );
}
}
This code produces the following output:
Volume = 113.0976
Volume = 50.2656
Volume = 56.5488
Volume = 113.0976
So far, you know that local variables and parameters are kept on the stack. Let's look at that organization a little further.
When a method is called, memory is allocated at the top of the stack to hold a number of data items associated with the method. This chunk of memory is called the stack frame for the method.
For example, the following code declares three methods. Main
calls MethodA
, which calls MethodB
, creating three stack frames. As the methods exit, the stack unwinds.
class Program
{
static void MethodA( int par1, int par2)
{
Console.WriteLine("Enter MethodA: {0}, {1}", par1, par2);
MethodB(11, 18); // Call MethodB.
Console.WriteLine("Exit MethodA");
}
static void MethodB(int par1, int par2)
{
Console.WriteLine("Enter MethodB: {0}, {1}", par1, par2);
Console.WriteLine("Exit MethodB");
}
static void Main( )
{
Console.WriteLine("Enter Main");
MethodA( 15, 30); // Call MethodA.
Console.WriteLine("Exit Main");
}
}
This code produces the following output:
Enter Main
Enter MethodA: 15, 30
Enter MethodB: 11, 18
Exit MethodB
Exit MethodA
Exit Main
Figure 5-14 shows how the stack frames of each method are placed on the stack when the method is called and how the stack is unwound as the methods complete.
Figure 5-14. Stack frames in a simple program
Besides calling other methods, a method can also call itself. This is called recursion.
Recursion can produce some very elegant code, such as the following method for computing the factorial of a number. Notice that inside the method, the method calls itself with an actual parameter of one less than its input parameter.
The mechanics of a method calling itself are exactly the same as if it had called another, different method. A new stack frame is pushed onto the stack for each call to the method.
For example, in the following code, method Count
calls itself with one less than its input parameter and then prints out its input parameter. As the recursion gets deeper, the stack gets larger.
This code produces the following output:
1
2
3
Figure 5-15 illustrates the code. Notice that with an input value of 3, there are four different, independent stack frames for method Count
. Each has its own value for input parameter inVal
.
Figure 5-15. Example of recursion
3.144.17.137