2.13. Function Parameter Semantics

The parameters of a function serve as placeholders within the function body. With each invocation, the parameters are bound to the actual arguments passed to the function. By default, this binding is carried out by value. We can override the default by modifying the parameter with either the ref or the out keyword. These kinds of parameters are bound by reference.

Each parameter minimally consists of a type specifier, such as int, string, or Matrix, and a name. The parameter name is visible only within the function, so the name can be reused outside the function without conflict. A comma separates multiple parameters. We write

f( int i, int j ){ ... }

not

// error!
f( int i, j ){ ... }

An empty parameter list indicates that the function takes no parameters.

Consider the following static method. It accepts two parameters: a Matrix object, and a value of type double. It multiplies each matrix element of the first parameter by the second parameter, storing the result in a new Matrix object. It then returns the new Matrix object:

public static
Matrix multiplyByDouble( Matrix mat, double dval )
{
    Matrix result = new Matrix( mat.rows, mat.cols );

    for ( int ix = 0; ix < mat.rows; ix++ )
          for ( int iy = 0; iy < mat.cols; iy++ )
                result[ ix, iy ] = mat[ ix, iy ] * dval;

    return result;
}

This function can be invoked as follows:

Matrix mat = Matrix.multiplyByDouble( location, modval );

where location represents a Matrix object and modval represents an object of type double—both of which have been defined earlier in the program.

What is the relationship between the formal parameters (mat and dval) and the actual arguments (location and modval)? Each formal parameter represents a local object within the function, the same as with objects defined within the function body. Five local objects are defined within multiplyByDouble(): the two local Matrix objects (mat and result), the second parameter (dval), and the two integer objects (ix and iy) used as indices into the matrix.

Each local object needs to be initialized (or at least assigned to) before it is used. In the case of result, ix, and iy, the initializations are explicit. What about the parameters? When and how are they initialized?

2.13.1. Pass by Value

The parameters are initialized at each call point of the function. By default, they are initialized through a mechanism called pass by value. This means that each parameter is initialized with a copy of the value of the actual argument. dval, for example, is initialized with a copy of the value that modval holds when the function is invoked. Because dval and modval represent separate objects, any changes made to dval from within the function have no effect on modval. This is both good and not so good.

The good part is that we don't have to protect the actual argument from being unintentionally modified. Within the function, we are manipulating a local copy of the argument. Any changes made to the local object disappear with the disappearance of the function. The actual argument is not changed.

It is not so good if we need the changes that we make to the local object to be reflected in the actual argument of the call. In this case, we have to override the pass-by-value mechanism. We'll see how to do that in the next subsection.

Pass by value becomes slightly more confusing when the formal parameter is a reference type. When we pass a reference type by value, a separate local instance is created just as for a value type. The difference is that both the argument and the local instances refer to the same object on the heap.

Recall that a reference type consists of a handle/object pair. The handle of a reference type contains the address of the object to which the handle refers. The object itself resides on the managed heap. When the reference type is passed by value, its handle part is copied into the local instance. The object on the heap is now referenced by both the handle and the object. In our example, both location and mat refer to the same Matrix object on the heap.

This means that any modification made to the heap object through the local instance permanently changes the object. This is not true of any changes made to the handle portion of the reference type, which affect only the local instance and are discarded with completion of the function.

To make sure this is clear, let's look at an example.

byValue() takes a single string parameter by value. Inside the function, its characters are turned to all uppercase using ToUpper(). Because strings are immutable, ToUpper() returns a string object. The parameter is modified to address this new object. At this point, the parameter and the local object refer to different string objects:

static public void byValue( string s )
{
   Console.Write( "
Inside byValue: " );
   Console.WriteLine( "original parameter: 
	" + s );

   // now refers to a different string object!
   s = s.ToUpper();

   Console.Write( "
Inside byValue: " );
   Console.WriteLine("modified parameter: 
	" + s );
}

The local instance refers to the new string object, in which the letters have been changed to uppercase. Because we have modified the handle of a value parameter, this change is not reflected in the actual argument. This is illustrated in the program's output:

string to be passed by value:
        A fine and private place

Inside byValue: original parameter:
        A fine and private place

Inside byValue: modified parameter:
        A FINE AND PRIVATE PLACE

back from call -- string:
        A fine and private place

As you can see, the string object appears the same both before and after the invocation of byValue(). In such cases our first thought is that we somehow fouled up the string transformation. However, when we display the string object from within byValue(), we see that the method actually works fine. The problem isn't with our method, but with how we pass parameters in and out of the function. We'll override the default pass-by-value behavior in the next section.

Before we do that, however, I'd like to review again briefly the impact of shallow copy on reference types. Consider the following code:

public static
Matrix multiplyByDouble( Matrix mat, double dval )
{
    Matrix result = new matrix( mat.rows, mat.cols );
    for ( int ix = 0; ix < mat.rows; ix++ )
          for ( int iy = 0; iy < mat.cols; iy++ )
                 result[ ix, iy ] = mat[ ix, iy ] * dval;

    return result;
}

Why does this code create a new Matrix object on the heap rather than just copying mat into result? After all, just copying mat into result simplifies the multiplication:

public static
Matrix multiplyByDouble( Matrix mat, double dval )
{
   Matrix result = mat;
   for ( int ix = 0; ix < mat.rows; ix++ )
          for ( int iy = 0; iy < mat.cols; iy++ )
                 result[ ix, iy ] *= dval;

   return result;
}

If we initialize result with mat, we get a shallow copy. result is simply a second handle to the actual Matrix object. Later, when we multiply each element by dval, we are directly modifying the original Matrix object, which is not what we want. To create a modified copy of the Matrix object, we simulate a deep copy by invoking the new expression.

Whenever we pass around or copy a reference type, we need to be sure whether a shared or independent instance is appropriate.

2.13.2. Pass by Reference: The ref Parameter

The negative aspect of pass by value is the same as its positive aspect: We lose all the changes made to the local instance of the argument. When we need to have these local changes persist, we must tell the compiler to pass the actual argument by reference rather than by value. We do this by prefixing the parameter declaration with the ref keyword:

static public void byRef( ref string s )
  { /* the body of the function is the same ... */ }

A reference parameter serves as an alias to the actual argument passed in.[2] The reference parameter does not represent an independent local entity. This form of parameter binding is called pass by reference. A change to the reference parameter directly changes the associated argument. For example, when we run byRef(), modifying the string internally as before, we see that the modification of the parameter within the function has caused the actual argument that we passed in to be modified:

[2] It does not result in the boxing of value types, however! (Boxing was discussed in Section 1.14.1.)

string to be passed by ref:
        A fine and private place

Inside byRef: original parameter:
        A fine and private place

Inside byRef: modified parameter:
        A FINE AND PRIVATE PLACE

back from call -- string:
        A FINE AND PRIVATE PLACE

The ref keyword must also prefix the actual argument in the invocation of the function:

string str = "A text string";
byRef( ref str );

If we forget to include the ref keyword when we invoke the function, its absence is flagged as a compile-time error. Although this may initially seem overly fussy, it allows us to overload and resolve methods solely on the presence (or absence) of the ref keyword. (We look at function overloading in Section 2.14.)

2.13.3. Pass by Reference: The out Parameter

The actual argument we pass to either a value parameter or a ref parameter must be recognized by the compiler as having a value assigned to it prior to invocation of the function. Otherwise, a compile-time error is triggered.

Sometimes, however, we implement a function such that a parameter is assigned a value within the function body. In effect, this allows us multiple return values. We still need to pass the argument in by reference; otherwise the change made to the parameter would be discarded at the completion of the function. But we don't really want to initialize the argument to a dummy value just to satisfy the compiler; after all, that value is overwritten when we invoke the function.

We accomplish this by prefixing a parameter with the out keyword, thereby alerting the complier that this parameter will be assigned to within the function body. In fact, the compiler requires that an out parameter be assigned to at every exit point of the function. Otherwise, a compile-time error is triggered. In the following example, multiplyByDouble() is reimplemented to accept an out parameter:

public static
void multiplyByDouble( out Matrix mat, double dval )
{
    for ( int ix = 0; ix < mat.rows; ix++ )
          for ( int iy = 0; iy < mat.cols; iy++ )
                mat[ ix, iy ] *= dval;
}

As with the ref keyword, the out keyword must be specified both in the signature of the function and in the call of the function:

MultiplyByDouble( out scaleMat, factor );

The argument passed to an out parameter may or may not already have had a value assigned. Within the function declaring an out parameter, however, the assumption is that the parameter is uninitialized. An out parameter cannot be used within its function until the parameter is explicitly assigned a value. For example, the reimplementation of byValue() using an out parameter is illegal:

static public void byValue( out string s )
{
    Console.Write( "
Inside byValue: " );

    // error: s is treated as being uninitialized
    Console.WriteLine( "original parameter: 
	" + s );

    // ... rest of the function
}

As an example of a situation in which we might wish to use an out parameter, consider the scenario of entering a word into a lookup table. We would want to “normalize” the word's case and suffixing—Fly, flies, and fly should all match, for example—but without modifying the original text word.

We could easily do this by passing in the original word by value, and passing in a second string as an out parameter assigned the modified word. Why would we do this rather than simply return the new string? Perhaps we want the formal return value of the function to indicate whether we were able to suffix it, or attempt to indicate its part of speech.

An out parameter provides us with an alternative way of returning a value from a function—of allowing us in effect to have multiple return values.

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

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