C H A P T E R  5

Methods

The Structure of a Method

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.

  • The method header specifies the method's characteristics, including the following:
    • Whether the method returns data and, if so, what type
    • The name of the method
    • What types of data can be passed to and from the method and how that data should be treated
  • The method body contains the sequence of executable code statements. Execution starts at the first statement in the method body and continues sequentially through the method.
Image

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.

    int MyMethod ( int par1, string par2 )
        ↑                         
   Return  Method         Parameter
    type    name              list

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 10. Most of what this chapter covers about class methods is also true for struct methods.

Code Execution in the Method Body

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:

  • Local variables
  • Flow-of-control constructs
  • Method invocations
  • Blocks nested within it

Figure 5-2 shows an example of a method body and some of its components.

Image

Figure 5-2. Method body example

Local Variables

Like fields, which I covered in Chapter 4, 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 an equal sign followed by a value to be used to initialize the variable.

     Variable name     Optional initializer
                        ↓   
   Type Identifier = Value;
  • The existence and lifetime of a local variable is limited to the block in which it is created and the blocks nested within that block.
    • The variable comes into existence at the point at which it is declared.
    • It goes out of existence when the block completes execution.
  • You can declare local variables at any position in the method body, but you must declare them before you can use them.

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();
      ...
   }

Image

Type Inference and the var Keyword

If you look at the following code, you'll see that in supplying the type name at the beginning of the declaration, you are supplying information that the compiler can already infer from the right side of the initialization.

  • In the first variable declaration, the compiler can infer that 15 is an int.
  • In the second declaration, the object-creation expression on the right side returns an object of type 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();
      ...
   }

To avoid this redundancy, C# allows you to use the keyword var in place of the explicit type name at the beginning of the variable declaration, as follows:

   static void Main( )
   { Keyword
       
      var total = 15;
      var mec   = new MyExcellentClass();
      ...
   }

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's shorthand for int. In the second, it's 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:

  • You can use it only with local variables—not with fields.
  • You can use it only when the variable declaration includes an initialization.
  • Once the compiler infers the type of a variable, it is fixed and unchangeable.

Image 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 equal sign. The var keyword does not change the strongly typed nature of C#.

Local Variables Inside Nested Blocks

Method bodies can have other blocks nested inside them.

  • There can be any number of blocks, and they can be sequential or nested further. Blocks can be nested to any level.
  • Local variables can be declared inside nested blocks, and like all local variables, their lifetime and visibility are limited to the block in which they're declared and the blocks nested within it.

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.

  • Variable var1 is declared in the body of the method, before the nested block.
  • Variable 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.
  • When control passes out of the nested block, its local variables are popped from the stack.
Image

Figure 5-3. The lifetime of a local variable

Image 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.

Local Constants

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:

  • A constant must be initialized at its declaration.
  • A constant cannot be changed after its declaration.

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:

  • The addition of the keyword const before the type.
  • The mandatory initializer. The initializer value must be determinable at compile time and is usually one of the predefined simple types or an expression made up of them. It can also be the null reference, but it cannot be a reference to an object, because references to objects are determined at run time.

Image Note The keyword const is not a modifier but part of the core declaration. It must be placed immediately before the type.

   Keyword
      
    const Type Identifier = Value;
                              
                  Initializer required

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, of built-in type double, 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);
      }
   }

Flow of Control

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.

The term flow of control refers to the flow of execution through a program. By default, program execution moves sequentially from one statement to the next. The flow-of-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.

  • Selection statements: These statements allow you to select which statement or block of statements to execute.
    • if: Conditional execution of a statement
    • if...else: Conditional execution of one statement or another
    • switch: Conditional execution of one statement from a set
  • Iteration statements: These statements allow you to loop, or iterate, on a block of statements.
    • for: Loop—testing at the top
    • while: Loop—testing at the top
    • do: Loop—testing at the bottom
    • foreach: Execute once for each member of a set
  • Jump statements: These statements allow you to jump from one place in the block or method to another.
    • break: 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.

   void SomeMethod()
   {
      int intVal = 3;
        Equality comparison operator
                 
      if( intVal == 3 )                                  // if statement
         Console.WriteLine("Value is 3. ");
   
      for( int i=0; i<5; i++ )                           // for statement
         Console.WriteLine("Value of i: {0}", i);
   }

Method Invocations

You can call other methods from inside a method body.

  • The phrases call a method and invoke a method are synonymous.
  • You call a method by using its name, along with the parameter list, which I'll discuss shortly.

For example, the following class declares a method called PrintDateAndTime, which is called from inside method Main:

   class MyClass
   {
      void PrintDateAndTime()                  // Declare the method.
      {
         DateTime dt = DateTime.Now;            // Get the current date and time.
         Console.WriteLine("{0}", dt);          // Write it out.
      }
   
      static void Main()                        // Declare the method.
      {
         MyClass mc = new MyClass();
         mc.PrintDateAndTime();                // Invoke the method.
    }                       
   }            Method      Empty
                name      parameter list

Figure 5-4 illustrates the sequence of actions when a method is called:

  1. Execution of the current method suspends at that point of the invocation.
  2. Control transfers to the beginning of the invoked method.
  3. The invoked method executes until it completes.
  4. Control returns to the calling method.
Image

Figure 5-4. Flow of control when calling a method

Return Values

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.

  • To return a value, the method must declare a return type before the method name.
  • If a method doesn't return a value, it must declare a return type of void.

The following code shows two method declarations. The first returns a value of type int. The second doesn't return a value.

   Return type
     
    int  GetHour()     { ... }
    void DisplayHour() { ... }
     
   No value is returned.

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.

   return Expression;                           // Return a value.
               
   Evaluates to a value of the return type

For example, the following code shows a method called GetHour, which returns a value of type int.

   Return type
     
    int GetHour( )
    {
      DateTime dt = DateTime.Now;              // Get the current date and time.
      int hour    = dt.Hour;                   // Get the hour.
   
      return hour;                             // Return an int.
    }      
     Return  statement

You can also return objects of user-defined types. For example, the following code returns an object of type MyClass:

   Return type — MyClass
       
    MyClass method3( )
    {
       MyClass mc = new MyClass();
         ...
       return mc;                                 // Return a MyClass object.
    }

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.

   class MyClass
   {            Return type
      public int GetHour()
      {
         DateTime dt = DateTime.Now;            // Get the current date and time.
         int hour    = dt.Hour;                 // Get the hour.
   
         return hour;                           // Return an int.
      }           
   }          Return value
   
   
   class Program
   {
      static void Main()
      {                                 Method invocation
         MyClass mc = new MyClass();              
         Console.WriteLine("Hour: {0}", mc.GetHour());
      }                                      
   }                                 Instance  Method
                                      name    name

The Return Statement and Void Methods

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.

  • You can exit a void method at any time by using the following form of the return statement, with no parameters:
    return;
  • This form of the return statement can be used only with methods declared 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.

  Void return type
     
   void SomeMethod()
   {
      ...
      if ( SomeCondition )                 // If ...
         return;                           // return to the calling code.
      ...
   
      if ( OtherCondition )                // If ...
         return;                           // return to the calling code.
   
      ...
   }                                       // Default return to the calling code.

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:

  • First the method gets the current date and time. (Don't worry about understanding the details of this right now.)
  • If the hour is less than 12 (that is, before noon), the return statement is executed, and control immediately returns to the calling method without writing anything to the screen.
  • If the hour is 12 or greater, the return statement is skipped, and the code executes the WriteLine statement, which writes an informative message to the screen.
   class MyClass
   {     Void return type
      void TimeUpdate()
      {
         DateTime dt = DateTime.Now;          // Get the current date and time.
            if (dt.Hour < 12)                 // If the hour is less than 12,
               return;                        // then return.
                  
               Return to calling method
         Console.WriteLine("It's afternoon!");   // Otherwise, print message.
      }

      static void Main()
      {
         MyClass mc = new MyClass();       // Create an instance of the class.
         mc.TimeUpdate();                  // Invoke the method.
      }
   }
Image

Figure 5-5. Using a return statement with a void return type

Parameters

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 a special kind of variable that can allow you to do both these things.

Formal Parameters

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.

   public void PrintSum( int x, float y )
   {                              
      ...           Formal parameter declarations
   }
  • Because formal parameters are variables, they have a data type and a name, and they can be written to and read from.
  • Unlike a method's other local variables, the parameters are defined outside the method body and are initialized before the method starts (except for one type, called output parameters, which I'll cover shortly).
  • The parameter list can have any number of formal parameter declarations, and the declarations must be separated by commas.

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);
   }

Actual Parameters

When your code calls a method, the values of the formal parameters must be initialized before the code in the method begins execution.

  • The expressions or variables used to initialize the formal parameters are called the actual parameters. They are also sometimes called arguments.
  • The actual parameters are placed in the parameter list of the method invocation.
  • Each actual parameter must match the type of the corresponding formal parameter, or the compiler must be able to implicitly convert the actual parameter to that type. I'll explain the details of conversion from one type to another in Chapter 16.

For example, the following code shows the invocation of method PrintSum, which has two actual parameters of data type int:

   PrintSum( 5, someInt );
                  
       Expression   Variable of 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.

Image

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 matches the number of formal parameters, and each actual parameter matches the type of the corresponding formal parameter. Parameters that follow this pattern are called positional parameters. We'll look at some other options shortly. But first we'll look at positional parameters in more detail.

An Example of Methods with Positional Parameters

In the following code, class MyClass declares two methods—one that takes two integers and returns their sum and another that takes two floats 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.

   class MyClass    Formal parameters
   {                             
      public int Sum(int x, int y)                        // Declare the method.
      {
         return x + y;                                    // Return the sum.
      }
                           Formal parameters
                                                
      public float Avg(float input1, float input2)        // Declare the method.
      {
         return (input1 + input2) / 2.0F;                 // Return the average.
      }
   }
   
   
   class Program
   {
      static void Main()
      {
         MyClass myT = new MyClass();
         int someInt = 6;
   
         Console.WriteLine
            ("Newsflash:  Sum: {0} and {1} is {2}",
                 5, someInt, myT.Sum( 5, someInt ));        // Invoke the method.
                                           
                                     Actual parameters
         Console.WriteLine
            ("Newsflash:  Avg: {0} and {1} is {2}",
                 5, someInt, myT.Avg( 5, someInt ));        // Invoke the method.
      }                                    
   }                                 Actual parameters

This code produces the following output:


Newsflash:  Sum: 5 and 6 is 11
Newsflash:  Avg: 5 and 6 is 5.5

Value Parameters

There are several kinds of parameters, each of which passes 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:

  • It allocates space on the stack for the formal parameters.
  • It copies the values of the actual parameters to the formal parameters.

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.

   float func1( float val )                              // Declare the method.
   {               
               Float data type
      float j = 2.6F;
      float k = 5.1F;
          ...
   }

                       Variable of type float
                             
      float fValue1 = func1( k );                        // Method call
      float fValue2 = func1( (k + j) / 3 );              // Method call
      ...                         
                       Expression that evaluates to a float

Before you can use a variable as an actual parameter, that variable must be 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.

Image 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.

  • The method adds 5 to both the int type field belonging to the class and to the int.
  • You might also notice that 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.
   class MyClass
   {
      public int Val = 20;                      // Initialize the field to 20.
   }

   class Program            Formal parameters
   {                                         
      static void MyMethod( MyClass f1, int f2 )
      {
         f1.Val = f1.Val + 5;                   // Add 5 to field of f1 param.
         f2     = f2 + 5;                       // Add 5 to second param.
         Console.WriteLine( "f1.Val: {0}, f2: {1}", f1.Val, f2 );
      }

      static void Main()
      {
         MyClass a1 = new MyClass();
         int     a2 = 10;

                Actual parameters
                        
         MyMethod( a1, a2 );                    // Call the method.
         Console.WriteLine( "f1.Val: {0}, f2: {1}", a1.Val, a2 );
      }
   }

This code produces the following output:


f1.Val: 25, f2: 15
f1.Val: 25, f2: 10

Figure 5-7 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method:

  • Before the method call, variables a1 and a2, which will be used as the actual parameters, are already on the stack.
  • By the beginning of the method, the system will have allocated space on the stack for the formal parameters and copied the values from the actual parameters.
    • Since 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.
    • Since a2 is a value type, the value is copied, producing an independent data item.
  • At the end of the method, both f2 and the field of object f1 have been incremented by 5.
    • After method execution, the formal parameters are popped off the stack.
    • The value of a2, the value type, is unaffected by the activity in the method.
    • The value of a1, the reference type, however, has been changed by the activity in the method.
Image

Figure 5-7. Value parameters

Reference Parameters

The second type of parameter is called a reference parameter.

  • When using a reference parameter, you must use the ref modifier in both the declaration and the invocation of the method.
  • The actual parameter must be a variable, and it must be assigned to before being used as the actual parameter. If it's a reference type variable, it can be assigned either an actual reference or the value null.

For example, the following code illustrates the syntax of the declaration and invocation:

               Include the ref modifier.
                   
   void MyMethod( ref int val )           // Method declaration
   { ... }

   int y = 1;                             // Variable for the actual parameter
   MyMethod ( ref y );                    // Method call
               
           Include the ref modifier.
   
   MyMethod ( ref 3+5 );                  // Error!
                   
                 Must use a variable

In the previous section you saw that for value parameters, the system allocates memory on the stack for the formal parameters. In contrast, reference parameters have the following characteristics:

  • They do not allocate memory on the stack for the formal parameters.
  • Instead, a formal parameter name acts as an alias for the actual parameter variable, referring to the same memory location.

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.

Image 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:

   class MyClass
   {
      public int Val = 20;                     // Initialize field to 20.
   }
   
   class Program         ref modifier         ref modifier
   {                                        
      static void MyMethod(ref MyClass f1, ref int f2)
      {
         f1.Val = f1.Val + 5;                  // Add 5 to field of f1 param.
         f2     = f2 + 5;                      // Add 5 to second param.
         Console.WriteLine( "f1.Val: {0}, f2: {1}", f1.Val, f2 );
      }
   
      static void Main()
      {
         MyClass a1 = new MyClass();
         int a2     = 10;
                   ref modifiers
                          
         MyMethod(ref a1, ref a2);             // Call the method.
         Console.WriteLine( "f1.Val: {0}, f2: {1}", a1.Val, a2 );
      }
   }

This code produces the following output:


f1.Val: 25, f2: 15
f1.Val: 25, f2: 15

Figure 5-8 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method:

  • Before the method call, variables a1 and a2, which will be used as the actual parameters, are already on the stack.
  • By the beginning of the method, the names of the formal parameters will have been set as aliases for the actual parameters. You can think of variables 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.
  • At the end of the method, both f2 and the field of the object of f1 have been incremented by 5.
  • After method execution, the names of the formal parameters are gone (“out of scope”), but both the value of 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.
Image

Figure 5-8. With a reference parameter, the formal parameter acts as an alias for the actual parameter.

Reference Types As Value and Reference Parameters

In the previous sections you saw that for a reference type object, you can modify its members inside the method call, regardless of whether you send the object in as a value parameter or as a reference parameter. We didn't, however, assign to the formal parameter itself, inside the method. In this section we'll look at what happens when you assign to the formal parameter of a reference type inside the method. The answer is the following:

  • Passing a reference type object as a value parameter: If you create a new object inside the method and assign it to the formal parameter, it breaks the connection between the formal parameter and the actual parameter, and the new object does not persist after the method call.
  • Passing a reference type object as a reference parameter: If you create a new object inside the method and assign it to the formal parameter, that new object persists after the method is ended, and is the value referenced by the actual parameter.

The following code shows the first case—using a reference type object as a value parameter:

   class MyClass { public int Val = 20; }

   class Program
   {
      static void RefAsParameter( MyClass f1 )
      {
         f1.Val = 50;
         Console.WriteLine( "After member assignment:    {0}", f1.Val );
         f1 = new MyClass();
         Console.WriteLine( "After new object creation:  {0}", f1.Val );
      }

      static void Main( )
      {
         MyClass a1 = new MyClass();

         Console.WriteLine( "Before method call:         {0}", a1.Val );
         RefAsParameter( a1 );
         Console.WriteLine( "After method call:          {0}", a1.Val );
      }
   }

This code produces the following output:


Before method call:         20
After member assignment:    50
After new object creation:  20
After method call:          50

Figure 5-9 below illustrates the following about the code:

  • At the beginning of the method, both the actual parameter and the formal parameter point to the same object in the heap.
  • After the assignment to the object's member, they still point at the same object in the heap.
  • When the method allocates a new object and assigns it to the formal parameter, the actual parameter (outside the method) still points at the original object, and the formal parameter points at the new object.
  • After the method call, the actual parameter points to the original object, and the formal parameter and new object are gone.
Image

Figure 5-9. Assigning to a reference type object used as a value parameter

The following code illustrates the case where a reference type object is used as a reference parameter. The code is exactly the same except for the ref keyword in the method declaration and the method invocation.

   class MyClass
   {
      public int Val = 20;
   }

   class Program
   {

      static void RefAsParameter( ref MyClass f1 )
      {
         // Assign to the object member.
         f1.Val = 50;
         Console.WriteLine( "After member assignment:    {0}", f1.Val );

         // Create a new object and assign it to the formal parameter.
         f1 = new MyClass();
         Console.WriteLine( "After new object creation:  {0}", f1.Val );
      }

      static void Main( string[] args )
      {
         MyClass a1 = new MyClass();

         Console.WriteLine( "Before method call:         {0}", a1.Val );
         RefAsParameter( ref a1 );
         Console.WriteLine( "After method call:          {0}", a1.Val );
      }
   }

This code produces the following output:


Before method call:         20
After member assignment:    50
After new object creation:  20
After method call:          20

As you remember, a reference parameter behaves as if the actual parameter were an alias for the formal parameter. This makes the explanation of the previous code easy. Figure 5-10 illustrates the following about the code:

  • When the method is invoked, the formal and actual parameters point at the same object in the heap.
  • The modification of the member value is seen by both the formal and actual parameter.
  • When the method creates a new object and assigns it to the formal parameter, the references of both the formal and actual parameters point to the new object.
  • After the method, the actual parameter is left pointing at the object that was created inside the method.
Image

Figure 5-10. Assigning to a reference type object used as a reference parameter

Output Parameters

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:

  • You must use a modifier in both the method declaration and the invocation. With output parameters, the modifier is out, rather than ref.
  • Like reference parameters, the actual parameter must be a variable—it cannot be another type of expression. This makes sense, since the method needs a memory location to store the value it's returning.

For example, the following code declares a method called MyMethod, which takes a single output parameter.

              out modifier
                                           
   void MyMethod( out int val )          // Method declaration
   { ... }

   ...
   int y = 1;                            // Variable for the actual parameter
   MyMethod ( out y );                   // Method call
               
           out modifier

Like reference parameters, the formal parameters of output parameters act as aliases for the actual parameters. Both the formal parameter and the actual parameter are names for the same memory location. Clearly, then, 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:

  • Inside the method, an output parameter must be assigned to before it can be read from. This means that the initial values of the parameters are irrelevant and that you don't have to assign values to the actual parameters before the method call.
  • Inside the method, every possible path through the code must assign a value to every output parameter before the method can exit.

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 assigns 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:

   class MyClass
   {
      public int Val = 20;                    // Initialize field to 20.
   }

   class Program       out modifier         out modifier
   {                                       
      static void MyMethod(out MyClass f1, out int f2)
      {
         f1 = new MyClass();                  // Create an object of the class.
         f1.Val = 25;                         // Assign to the class field.
         f2     = 15;                         // Assign to the int param.
      }

      static void Main()
      {
         MyClass a1 = null;
         int a2;

         MyMethod(out a1, out a2);            // Call the method.
      }                   
   }               out modifiers

Figure 5-11 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method.

  • Before the method call, variables a1 and a2, which will be used as the actual parameters, are already on the stack.
  • At the beginning of the method, the names of the formal parameters are set as aliases for the actual parameters. You can think of variables 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.
  • Inside the method, the code creates an object of type 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.
  • After method execution, the names of the formal parameters are out of scope, but the values of both a1, the reference type, and a2, the value type, have been changed by the activity in the method.
Image

Figure 5-11. With an output parameter, the formal parameter acts as an alias for the actual parameter, but with the additional requirement that it must be assigned to inside the method.

Parameter Arrays

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 of a particular type for a particular formal parameter. Important points about parameter arrays are the following:

  • There can be only one parameter array in a parameter list.
  • If there is one, it must be the last parameter in the list.
  • All the parameters represented by the parameter array must be of the same type.

To declare a parameter array, you must do the following:

  • Use the params modifier before the data type.
  • Place a set of empty square brackets after 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.

                      Array of ints
                             
   void ListInts( params int[] inVals )
   { ...                         
                  Modifier       Parameter name

The empty set of square brackets after the type name specifies that the parameter will be an array of ints. You don't need to worry about the details of arrays here. They're covered in detail in Chapter 12. For our purposes here, though, all you need to know is the following:

  • An array is an ordered set of data items of the same type.
  • An array is accessed by using a numerical index.
  • An array is a reference type and therefore stores all its data items in the heap.

Method Invocation

You can supply the actual parameters for a parameter array in two ways. These are

  • A comma-separated list of elements of the data type. All the elements must be of the type specified in the method declaration.
       ListInts( 10, 20, 30 );              // Three ints
  • A one-dimensional array of elements of the data type.
       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.

  • The other parameter types are consistent in that they either use a modifier or do not use a modifier.
    • Value parameters take no modifier in either the declaration or the invocation.
    • Reference and output parameters require the modifier in both places.
  • The summary for the usage of the params modifier is the following:
    • It is required in the declaration.
    • It is not allowed in the invocation.
Expanded Form

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:

  • It takes the list of actual parameters and uses them to create and initialize an array in the heap.
  • It stores the reference to the array in the formal parameter on the stack.
  • If there are no actual parameters at the position corresponding to the formal parameter array, the compiler creates an array with zero elements and uses that.

For example, the following code declares a method called ListInts, which takes a parameter array. Main declares three ints and passes them to the array.

   class MyClass                  Parameter array

   {                                                            
      public void ListInts( params int[] inVals )
      {
         if ( (inVals != null) && (inVals.Length != 0))
            for (int i = 0; i < inVals.Length; i++)     // Process the array.
            {
               inVals[i] = inVals[i] * 10;
               Console.WriteLine("{0}", inVals[i]);   // Display new value.
            }
      }
   }

   class Program
   {
      static void Main()
      {
         int first = 5, second = 6, third = 7;          // Declare three ints.

         MyClass mc = new MyClass();
         mc.ListInts( first, second, third );           // Call the method.
                               
                          Actual parameters
         Console.WriteLine("{0}, {1}, {2}", first, second, third);
      }
   }

This code produces the following output:


50
60
70
5, 6, 7

Figure 5-12 illustrates the following about the values of the actual and formal parameters at various stages in the execution of the method:

  • Before the method call, the three actual parameters are already on the stack.
  • By the beginning of the method, the three actual parameters will have been used to initialize an array in the heap, and the reference to the array will have been assigned to formal parameter inVals.
  • Inside the method, the code first checks to make sure the array reference is not null and then processes the array by multiplying each element in the array by 10 and storing it back.
  • After method execution, the formal parameter, inVals, is out of scope.
Image

Figure 5-12. Parameter array example

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.

  • If the array parameter is a value type, the values are copied, and the actual parameters cannot be affected inside the method.
  • If the array parameter is a reference type, the references are copied, and the objects referenced by the actual parameters can be affected inside the method.

Arrays As Actual 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

Summary of Parameter Types

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.

Image

Method Overloading

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.

  • The signature of a method consists of the following information from the method header of the method declaration:
    • The name of the method
    • The number of parameters
    • The data types and order of the parameters
    • The parameter modifiers
  • The return type is not part of the signature—although it's a common mistake to believe that it is.
  • Notice that the names of the formal parameters are not part of the signature.
    Not part of signature
       
   long AddValues( int a, out int b) { ... }
                         
                 Signature

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 in 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, type, and order of their parameters are the same. The compiler would produce an error message for this code.

   class B           Signature

   {                                 
      long AddValues( long  a, long  b) { return a+b; }
      int  AddValues( long  c, long  d) { return c+d; }  // Error, same signature
   }                                                          
                        Signature

Named Parameters

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.

Alternatively, C# allows you to use named parameters. Named parameters allow you to 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:

  • Nothing changes in the declaration of the method. The formal parameters already have names.
  • In the method invocation, however, you use the formal parameter name, followed by a colon, in front of the actual parameter value or expression, as shown in the following method invocation. Here a, b, and c are the names of the three formal parameters of method Calc:
            Actual parameter values
                        
   c.Calc ( c: 2, a: 4, b: 3);
                        
              Named parameters

Figure 5-13 illustrates the structure of using named parameters.

Image

Figure 5-13. 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 if you do, 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.

   class MyClass
   {
      double GetCylinderVolume( double radius, double height )
      {
         return 3.1416 * radius * radius * height;
      }

      static void Main( string[] args )
      {
         MyClass mc = new MyClass();
         double volume;
                                                                  
         volume = mc.GetCylinderVolume( 3.0, 4.0 );
         ...
         volume = mc.GetCylinderVolume( radius: 3.0, height: 4.0 );
         ...                                              

      }                                       More informative
   }

Optional Parameters

C# also allows 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

  • Formal parameter b is assigned the default value 3.
  • Therefore, if the method is called with only a single parameter, the method will use the value 3 as the initial value of the second parameter.
   class MyClass                Optional parameter
   {                                ↓   
      public int Calc( int a, int b = 3 )
      {                               
         return a + b;       Default value assignment
      }

      static void Main()
      {
         MyClass mc = new MyClass();

         int r0 = mc.Calc( 5, 6 );             // Use explicit values.
         int r1 = mc.Calc( 5 );                // Use default for b.

         Console.WriteLine( "{0}, {1}", r0, r1 );
      }
   }

This code produces the following output:


11, 8

There are several important things to know about declaring optional parameters:

  • Not all types of parameters can be used as optional parameters. Figure 5-14 illustrates when optional parameters can be used.
    • You can use value types as optional parameters as long as the default value is determinable at compile time.
    • You can only use a reference type as an optional parameter if the default value is null.
Image

Figure 5-14. Optional parameters can only be value parameter types.

  • All required parameters must be declared before any optional parameters are declared. If there is a params parameter, it must be declared after all the optional parameters. Figure 5-15 illustrates the required syntactic order.
Image

Figure 5-15. 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 instruct the program to 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 the method is supposed to use. The rules are the following:

  • You must omit parameters starting from the end of the list of optional parameters and work toward the beginning.
  • That is, you can omit the last optional parameter, or the last n optional parameters, but you can't pick and choose to omit any arbitrary optional parameters; they must be taken off the end.
   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. In this case, you're using both the named-parameters and optional-parameters features. The following code illustrates this use of positional, optional, and named parameters.

   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

Stack Frames

So far, you know that local variables and parameters are kept on the stack. In this section, we'll look at that organization a bit 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.

  • The stack frame contains memory to hold the following:
    • The return address—that is, where to resume execution when the method exits
    • Those parameters that allocate memory—that is, the value parameters of the method, and the parameter array if there is one
    • Various other administrative data items relevant to the method call
  • When a method is called, its entire stack frame is pushed onto the stack.
  • When the method exits, its entire stack frame is popped from the stack. Popping a stack frame is sometimes called unwinding the stack.

For example, the following code declares three methods. Method Main calls MethodA, which calls MethodB, thus 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-16 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.

Image

Figure 5-16. Stack frames in a simple program

Recursion

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 in this example that inside the method, the method calls itself with an actual parameter of one less than its input parameter.

   int Factorial(int inValue)
   {
      if (inValue <= 1)
         return inValue;
      else
         return inValue * Factorial(inValue - 1);     // Call Factorial again.
   }                                
                              Calls itself

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.

   class Program
   {
      public void Count(int inVal)
      {
         if (inVal == 0)
            return;
         Count(inVal - 1);             // Invoke this method again.
                 
              Calls itself
         Console.WriteLine("{0}", inVal);
      }
   
      static void Main()
      {
         Program pr = new Program();
         pr.Count(3);
      }
   }

This code produces the following output:


1
2
3

Figure 5-17 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.

Image

Figure 5-17. Example of building and unwinding the stack with a recursive method

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.234.24