2.14. Function Overloading

Two or more functions can be given the same name if the parameter list of each function is unique either by the type or by the number of parameters. For example, the following five declarations represent overloaded instances of a method called message():

class MessageHandler
{
   public void message(){ ... }
   public void message( char ch ){ ... }
   public void message( string msg ){ ... }
   public void message( string msg,int val ){ ... }
   public void message( string msg,int v1,int v2 ){ ... }
}

How does the compiler know which instance of an overloaded function to invoke? It compares the actual arguments supplied to the function invocation against the parameters of each overloaded instance, choosing the best match.

The return type of a function by itself cannot be used to distinguish two overloaded instances of that function. The reason is that the return type cannot guarantee a sufficient context for distinguishing between the overloaded instances. For example, imagine multiple instances of message() distinguished only by the return type. The following call provides no context to determine which instance the user wished to have invoked:

// which one?
message( '	' );

Overloading a set of functions that have unique implementations but perform similar tasks simplifies the use of these functions for our users. Without overloading, we would have to provide each function with a unique name.

2.14.1. Resolving Overloaded Functions

Resolving the selection of an overloaded function requires finding the best match between the actual arguments passed to the call and the formal parameters of an overloaded instance. The first step involves identifying the candidate functions that can be considered for resolving the call. The next step is to select the viable functions—those that can be invoked given the number and type of the actual arguments. Finally, from the list of viable functions, the final step is to select the function that best matches the call.

As illustration, consider the following set of overloaded functions:

public static void f( int   i1, int i2 )  { ... }
public static void f( float f1, float f2 ){ ... }
public static void f( string s ){ ... }

Now assume that we invoke f() with two integer arguments:

f( 1024, 2048 );

The set of candidate functions consists of the three instances listed above. The viable candidates, however, include only the first two. Both of the viable candidates accept two parameters, and two integer arguments can be passed to either one. The final step is deciding which of the two viable instances is the best match. The two integer arguments exactly match the formal parameters of the first instance. In order for them to match the two float parameters of the second instance, each argument must be implicitly converted from int to float. An exact match is always better than a match requiring conversion, so f(int,int) is selected.

Do things always go correctly? No. In the first step, it may be that no candidate functions are found. The result is a compile-time error: The name of the function is unknown. For example, if we mistype f() as F(), the call cannot be resolved. Similarly, in the second step, there may be no viable functions, because of either a type mismatch or a mismatch of the number of arguments. For example, if we invoke f() with three arguments, there are no viable instances, and the call results in a compile-time error.

2.14.2. Determining a Best Match

A viable function has the same number of formal parameters as the number of actual arguments passed to the call. In addition, either the argument type must exactly match the type of its corresponding parameter, or an implicit conversion from the argument type to the type of its corresponding parameter must exist. For ref and out parameters, the argument type must match exactly; no conversions are considered.

In C#, conversions are either implicit or explicit. Only implicit conversions—that is, conversions carried out automatically by the compiler—are considered in attempts to resolve an overloaded function call.

C# defines a set of standard implicit conversions among the built-in numeric types. The rule is that a numeric type only implicitly converts to a type the same size or larger. So, for example, long implicitly converts to float, double, or decimal. It does not implicitly convert to int. For instance, the invocation

int ival;
long lval;
// ...
f( ival, lval ); // which one?

results in a single viable function. Because long does not implicitly convert to the smaller int type, it is not possible to invoke f(int,int); therefore, the only viable function is f(float,float).

Let's modify the invocation of f() to give us two viable functions:

int ival;
short sval;
// ...
f( ival, sval ); // which one?

How do we decide the best viable function? The decision involves ranking the necessary conversions between each parameter and actual argument. How is one conversion better than another? The reference manual describes the criteria as follows:

If an implicit conversion from T1 to T2 exists, and no implicit conversion from T2 to T1 exists, then the conversion of S to T1 is better.

where S represents the actual argument type, and T1 and T2 represent the formal parameter types of two viable functions. Let's see if we can make sense of this statement.

Our second argument, sval, a short, does not exactly match either the formal int parameter of the first viable function or the formal float parameter of the second. An implicit conversion is required for both matches. For the first, short is promoted to int. For the second, short is promoted to float. Which is better?

To apply the rule stated in the reference manual, we have to map the actual types to S, T1, and T2. The actual type of the argument, short, maps to S; int maps to T1; and float maps to T2.

Does an implicit conversion from T1 to T2 (i.e., from int to float) exist? Yes. Then does an implicit conversion from T2 to T1 (i.e., from float to int) also exist? No. Therefore, the conversion of short to int is better than the conversion of short to float.

In general, this rule means that the conversion requiring the smallest effort is always the preferred conversion. A conversion of long to float, for example, is always preferred to a conversion of long to double, although they are both viable. This rule parallels our intuitive understanding of how conversions should work.

Sometimes there is no best viable function. (One function might be better for one parameter, another for a second.) In this case, the invocation is tagged as ambiguous, and a compile-time error results—for example,

public static void g( long l, float f  ){ ... }
public static void g( int i,  double d ){ ... }

g( 0, 0 ); // ambiguous

The first argument exactly matches the first parameter of the second function but requires an implicit conversion to match the first parameter of the first function. Therefore, for the first parameter the second function has the best match.

The second argument requires a conversion for both parameters. However, the standard conversion of int to float is better than that of int to double. So for the second parameter, the first function has the best match.

The result is literally a draw: Each function has one best match. There is no best viable function, so the call results in an ambiguity error. To resolve it, we can explicitly convert one or the other argument—for example,

// OK: public static void g(int i, double d)
g( 0, (double)0 );

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

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