Function Members

   


C# has a rich set of function members, as you saw in the introduction of this chapter, but even though they are called different names and are meant for different purposes, they all contain a block of statements that are executed when called, just like our conventional method, which we will focus on in the next section.

Methods

Methods can be divided into two main groups instance methods (defined without the static keyword) that belong to the group of instance functions (see the note in Syntax Box 12.2) and static methods (defined with the static keyword) that belong to the group of static functions. An instance method is executed in relation to a specific object; it needs an object to be executed. In contrast, a static method is executed in relation to the class and does not need any particular object to be invoked, but is invoked through the classname.

All Objects of a Class Share Just One Copy of Each Instance Method

graphics/bulb.gif

Because each instance method is executed in relation to a specific object, it seems that there exists one particular instance method copy in the memory per object. However, this is not the case, because it would consume far too much memory. Instead, all objects of the same class share just one copy of each instance method.

The Rocket class shown next illustrates this point. Two Rocket objects are created and their references assigned to rocketA and rocketB, respectively (lines 30 and 31). Even though there is only one copy of the Launch method, rocketA.Launch() (line 40) still executes Launch in relation to rocketA, and rocketB.Launch() (line 41) executes Launch in relation to rocketB.

 01: class Rocket 02: { 03:     private double speed;     ... 10:     public void Launch() 11:     {             ... 15:         speed = 3000.9;             ... 20:     }         ... 25:    } 30: Rocket rocketA = new Rocket(); 31: Rocket rocketB = new Rocket(); 40: rocketA.Launch(); 41: rocketB.Launch(); 

How can C# make this possible? The compiler secretly passes the reference of the object for which the method is called along as an argument to the instance method. If you could look down into the engine room of the compiler, it might look like the following lines:

 Rocket.Launch (rocketA); Rocket.Launch (rocketB); 

The compiler then (also secretly) attaches the object reference to any code inside the method that is specific to the object. For example, during the Rocket.Launch(rocketA) call, the following line

 speed = 3000.9 

becomes equivalent to

 rocketA.speed = 3000.9 


A method definition consists of a method header and a method body (see Syntax Box 12.3). The method header specifies important attributes that determine how other parts of the program can access and invoke this method (for example, methods of other objects or methods of the same class). The method body consists of the statements executed when the method is invoked.

The accessibility of a method is specified by an optional access modifier in the method header (see Syntax Box 12.3) and has, in the examples we have seen so far, been comprised of the public and private keywords. public specifies that any part of the program has access to the method; private indicates that the method only can be reached from within the class itself. As you can see in Syntax Box 12.3, C# offers other ways to control the access of methods (and class members in general) in the form of the access modifiers protected, internal, and protected internal. However, because they are relevant in the context of assemblies and an important OO mechanism not yet discussed called inheritance, we will defer their discussion until these elements have been discussed sufficiently.

Syntax Box 12.3 The Method Definition

 Method::=       <Method_header>       <Method_body> 

where

 <Method_header>::=  [<Method_modifiers>] <Return_type> <Method_identifier>  ([<Formal_parameter_list>]) <Method_body>::= {     <Statements> }             ::=    ;  <Method_modifiers>                   ::= <Access_modifier>                   ::= new                   ::= static                   ::= virtual                   ::= sealed                   ::= override                   ::= abstract                   ::= extern <Access_modifier>                   ::=  public                   ::=  private                   ::=  protected                   ::=  internal                   ::=  protected internal <Return_type>               ::= void               ::= <Type> 

Notes:

  • The method modifiers new, static, virtual, sealed, override, and abstract are related to the object-oriented mechanism called inheritance and will be discussed along with this important concept in Chapter 16.

  • A <Method body> can either consist of a block of statements or simply a semicolon representing the empty statement.

A method is called by writing the method name followed by a pair of parentheses enclosing an argument list (see Syntax Box 12.4) that must match the formal parameter list specified in the method header of the method definition shown in Syntax Box 12.3.

Syntax Box 12.4 The Method Call

 Method_call::= [<Class_identifier>.|<Object_identifier>.]<Method_identifier>([<Argument_list>] graphics/ccc.gif) 

Notes:

  • The optional <Class_identifier> (for calling a static method) and <Object_identifier> (for calling an instance method) trailed by the dot operator are only needed when the method is called from outside the class where it resides.

  • The type of each individual argument in <Argument_list> must have an implicit conversion path leading to the type specified by the corresponding formal parameters in <Formal_parameter_list> that is part of the method header shown earlier in Syntax Box 12.3.

static Methods

Recall that a program is started by the .NET runtime with an initial call to the Main() method. The runtime does not create any objects and must invoke Main() without relating it to any particular object. When we declare Main() to be a static method, we specify that it belongs to a class, rather than an object of this class, and thereby allow the runtime to call Main() directly through the classname:

 MyClass.Main() 

where Main() belongs to the class MyClass specified in:

 class MyClass() {     ...     public static void Main()     {         ...     }     ... } 

Syntax Box 12.5 Declaring a static Method

A static method is defined by adding the reserved word static before the return type of the method header as in the following:

  Static_method_definition::= <Method_modifier> static <Return_type> <Method_identifier>  ([<Formal_parameter_list>]) <Method_body> 

Note:

  • The static method definition must be positioned inside the class body, just like any other method.

Calling a static Method

graphics/bulb.gif

A static method can be called in three different ways:

  • From within an object of the class it belongs to, in which case it doesn't need any classname or object name as a prefix.


    graphics/12infig01.gif

  • From outside the class in which it resides. Two possibilities then exist:

  • If any objects of the class exist where the method is defined, it can be called by applying the dot operator to the name of the object followed by the name of the method:

     Rocket rocketA = new Rocket(); ... ...rocketA.Average(...)... 
  • Disregarding the existence of any objects of the class where the method is defined, it can be called by applying the dot operator to the classname followed by the name of the method:

     ...Rocket.Average(...)... 

    In general, it is better to use the class name Rocket.Average(...) than the object name Rocket.Average(...), because the class name format clearly signals that we are calling a static method.


When to Use static Methods

There is one compelling reason for making the Main() method static the program won't execute if you don't. Unfortunately, the choice is not that easy for the rest of the methods in our programs. However, what follows are a couple of examples where you should consider making the methods static.

1) Methods that cannot be pinpointed to belong to a specific object should be static.

Consider a method used to calculate the average of a list of numbers provided to it as arguments. This method does not interact with any instance variables/instance methods, but merely mingles with its own confined world (read method body) where it follows its own little average-calculation-algorithm. So, theoretically, it doesn't belong to any particular class.

Several different classes and objects throughout your program might need to use the functionality of this method. If it resided inside the MyMath class and was declared non-static as follows

 class MyMath {     ...     public double Average (params double[] numbers)     {         ...     }     ... } 

Then every method calling Average would have to first create an object of class MyMath and then call Average through this object as in the following:


graphics/12infig02.gif

So the only reason for creating the mathObject is to call Average. As soon as we are finished with Average, the mathObject is forgotten and never used again. Not only did we waste time writing the extra line of source code to create the object, this same line also cluttered our code and the runtime had to waste time and memory creating an object with its instance variables and its accompanying reference variable. Had we instead declared Average to be static, as in the following

 ... public static double Average (params double[] numbers) ... 

then our call to Average would have been simplified to

 ...MyMath.Average(...) ... 

static methods are used throughout the .NET Framework and often for the reason already given. For example, the System.Math class contains 24 static math-related methods, and the System.Console class contains the well-known Read, ReadLine, Write and WriteLine methods that allow us to simply write

 Console.WriteLine(...) 

Tip

graphics/bulb.gif

If your program contains several methods not related to any particular object but that provide functionality belonging to the same category, create a special class for these methods and let them all be static. For example, if you had other statistics related methods apart Average from those already discussed, you could create a class called Statistics and equip it with a range of static methods, all providing various statistics calculations. Any programmer in your project in need of calculating various statistics will then first look inside this class before he or she spends time writing his or her own methods.


2) In some cases, use a static method to access a static member variable.

In general, all static variables should, as mentioned earlier in this chapter, be declared private. When accessed from outside its class, a private static variable can only be accessed via a static method or via instance methods defined inside the same class as the variable. To use an instance method requires an object, so to access a private static variable at all times, even when no objects of the relevant class are created, you need a static method. For example, suppose that the static variable totalAccountsCreated of the Account class in Listing 12.1 presented earlier more appropriately was declared private instead of public. The Account class would then look like the code shown in Figure 12.5 and need the static method GetTotalAccountsCreated to be accessed at all times.

Figure 12.5. Accessing a static variable with a static method.
graphics/12fig05.gif

Tip

graphics/bulb.gif

Some programmers believe that static methods are non-object-oriented because they don't belong to a specific object and, hence, become global in nature. This is a valid point, but most programmers still find that they need static methods from time to time. However, if you find yourself using many static methods, you might need to review your overall software design.


You cannot directly access and invoke instance variables and instance methods from within a static method of the same class.

static methods are associated with a class and not any particular object. Instance methods and instance variables belong to a particular object of a class. A practical consequence of this gap between static and instance class members is the inability of directly calling an instance method, or directly accessing an instance variable from within a static method of the same class. Let's illustrate this by continuing our previous story about the Solar system model and the Light class.

Listing 12.2 contains a simple Light class with the speed-of-light constant in line 6 called Speed. Remember that any constant by default also is static. Light further contains two instance variables (lines 7 and 8), two static methods (lines 10 20) and four instance methods (lines 22 40). By mixing static and instance methods and variables in the same class, we can explore the boundaries for the kind of calls we can validly make between these entities.

Listing 12.2 SolarSystem.cs
01: using System; 02: 03: class Light 04: { 05:      // Speed of light in kilometers per second 06:     public const double Speed = 299792; 07:     private double timeTraveled; 08:     private double distanceTraveled; 09: 10:     public static double CalculateDistance (double seconds) 11:     { 12:         return seconds * Speed; 13:     } 14: 15:     public static double CalculateTime (double distance) 16:     { 17:         Light someLight = new Light(); 18:         someLight.SetDistanceTraveled(distance); 19:         return someLight.GetTimeTraveled(); 20:     } 21: 22:     public void SetTimeTraveled (double newTimeTraveled) 23:     { 24:         timeTraveled = newTimeTraveled; 25:     } 26: 27:     public void SetDistanceTraveled (double newDistanceTraveled) 28:     { 29:         distanceTraveled = newDistanceTraveled; 30:     } 31: 32:     public double GetDistanceTraveled () 33:     { 34:         return CalculateDistance (timeTraveled); 35:     } 36: 37:     public double GetTimeTraveled () 38:     { 39:         return distanceTraveled / Speed; 40:     } 41: } 42: 43: class SolarSystem 44: { 45:     public static void Main() 46:     { 47:         Light sunRay = new Light(); 48: 49:         Console.WriteLine("Speed of light: { 0} kilometers per hour", Light.Speed); 50:         sunRay.SetTimeTraveled(240); 51:         Console.WriteLine("The sunray has traveled { 0}  kilometers after 240  graphics/ccc.gifseconds", 52:             sunRay.GetDistanceTraveled()); 53:         Console.WriteLine("Light travels { 0}  kilometers after 480 seconds", 54:             Light.CalculateDistance(480)); 55:         Console.WriteLine("It takes { 0:N2}  seconds for a ray of sunshine", 56:             Light.CalculateTime(150000000)); 57:         Console.WriteLine("to travel from the sun to the earth"); 58:     } 59: } Speed of light: 299792 kilometers per hour The sunray has traveled 71950080 kilometers after 240 seconds Light travels 143900160 kilometers after 480 seconds It takes 500.35 seconds for a ray of sunshine to travel from the sun to the earth 

The static method CalculateDistance (lines 10 13) is able to call Speed because Speed is a const and, therefore, a static variable by default. However, an attempt to use the instance variable timeTraveled (line 8) inside CalculateDistance, as suggested in the following lines, would be absurd and trigger an error message from the compiler.

 public static double CalculateDistance () {     return timeTraveled * Speed   // Invalid } 

The CalculateDistance method is not associated with any particular object, so the compiler does not know to which instance timeTraveled belongs.

On the other hand, the SetTimeTraveled method (lines 22 25) is an instance method. It is called through a particular instance object and can be used to access the timeTraveled instance variable of that same object, as shown in line 24.

It is also possible to call a static method from an instance method. The GetDistanceTraveled method defined in lines 32 35 calls the static method CalculateDistance with the argument timeTraveled. CalculateDistance does not have to determine which object timeTraveled belongs to, this has already been determined by the GetDistanceTraveled method. CalculateDistance just receives timeTraveled as an argument and does not care about its origin as long as its type matches that of its formal parameter.

Is it possible to invoke an instance method from within a static method? Yes, but only indirectly by first creating an instance of the class and then calling the non-static method through this particular object. The CalculateTime method of lines 15 20 performs this trick. It first creates an instance of Light called someLight in line 17 and can then call the non-static method SetDistanceTraveled through this object by applying the dot operator, as shown in line 18. At the same time, it also accesses the non-static variable distanceTraveled by assigning the argument distance to it. By finally calling the non-static method GetTimeTraveled and returning this method's return value back to the caller, CalculateTime is able to indirectly utilize the functionality of two non-static methods to calculate the time it takes light to travel a certain distance.

Had we inside CalculateTime attempted to call SetDistanceTraveled and GetTimeTraveled instead, without creating someLight and called them through this object, CalculateTime would have looked like the following lines and triggered a compiler error.

 15:     public static double CalculateTime (double distance) 16:     { 17:         SetDistanceTraveled(distance);  // Invalid. Cannot call instance 18:         return GetTimeTraveled();       // methods directly from within 19:     }                                    // a static method. 

Invalidly attempting to directly call instance methods and variables from within static methods is a common mistake made by programmers.

Note

graphics/common.gif

The distance from the sun to the earth is approximately 150,000,000 kilometers (equivalent to approximately 3409 times around the earth). According to the sample output from Listing 12.2 (and modern scientific calculations), it takes a little more than 500 seconds (eight minutes and twenty seconds) for a sunray to reach the earth.

Little wonder the sun needs a good sleep like the rest of us after a long day.


Reference Parameters

Recall from Chapter 6 that C#'s types can be divided into value and reference types. When a value of a reference type is passed to a method, the changes made to this value within the confines of the method are reflected in the original value that was passed as an argument. This fact was utilized by the Swap method in the BubbleSort.cs program contained in Chapter 11, "Arrays Part II: Multidimensional Arrays Searching and Sorting Arrays", which contained an array (a reference type) as a formal parameter, allowing it to swap array elements of the original array that was passed to it.

On the other hand, if a variable of a value type, such as one of the simple types, is passed as an argument to a method, the variable's value is copied to the corresponding formal parameter. Consequently, the original variable value and the value accessed inside the method are totally separate and changes made within the confines of the method are not reflected on the original variable value. This prevents us from performing the same act on a value type as Swap did on the array. We have lived happily without this ability so far because any value calculated by a method has been returned to the caller with the return value of the method. But what if we need to return more than one value of a value type back to the caller? With the return value, we can only return one value. Fortunately, reference parameters allow us to implement this functionality by letting any changes made to it be reflected in the original argument of a value type. A reference parameter is declared with the ref keyword, as in the following method header

 public static void Swap(ref int a, ref int b) 

which declares a and b to be reference parameters. To ensure that the programmers who write calls to the Swap method are aware that Swap contains reference parameters that likely will change the arguments passed to it, a method call must also include the ref keyword. It must be positioned in front of any argument passed to a reference parameter:

 Swap(ref myValue, ref yourValue) 

Failure to comply with this protocol will trigger the compiler to issue an error message alerting the unaware or forgetful programmer.

Listing 12.3 provides the full source code accompanying the previous two lines of code. The Swap method is used to swap myValue with yourValue and prints out the result.

Listing 12.3 Swapper.cs
01: using System; 02: 03: class Swapper 04: { 05:     public static void Main() 06:     { 07:         int myValue = 10; 08:         int yourValue = 20; 09: 10:         Swap(ref myValue, ref yourValue); 11:         Console.WriteLine("My value: { 0}    Your value: { 1} ",              myValue, yourValue); 12:     } 13: 14:     public static void Swap(ref int a, ref int b) 15:     { 16:         int temp; 17: 18:         temp = a; 19:         a = b; 20:         b = temp; 21:     } 22: } My value: 20   Your value: 10 

The Swap method (lines 14 21) uses the familiar swapping algorithm applied in previous examples to swap a and b. Because a and b are reference parameters, their swap will cause any original pair of arguments passed along with a method call to Swap to be swapped. This is verified through the sample output of this listing.

Note

graphics/common.gif

Any argument passed to a reference parameter must be an assignable variable. For example, the following call to the Swap method of Listing 12.3 would be invalid

 Swap(ref 10, ref 20) 

because it is impossible to assign a value to 10 or 20.


Output Parameters

Up until now, it has been a requirement that any variable used as an argument in a method call must be initialized prior to this call. For example, the following source code will not execute because the unassigned variable distance is used as an argument when Initialize is invoked.


graphics/12infig03.gif

If you attempt to compile the preceding code, you will get an error message similar to the following line

 Use of unassigned local variable 'distance' 

Sometimes, you might want a method to set the initial value of a variable by might it as an argument to this method. You need exactly the same functionality as a reference parameter but with the additional ability to use an uninitialized variable as an argument. The output parameter provides us with this ability. You declare an output parameter by including the out keyword in front of the type of the parameter in the method header

 public static void Initialize(out int newDistance) 

which states that newDistance is an output parameter. If you substitute this line with line 10 in the previous source code and insert the Keyword out before distance in line 7, the program becomes valid and the call to initialize in line 7 will cause distance to hold the value 100.

Note

graphics/common.gif

An output parameter is the same as a reference parameter with one difference the output parameter accepts unassigned variables.


Note

graphics/common.gif

An output parameter must be assigned a value before control leaves its method.


Accommodating for Varying Number of Arguments: The Parameter Array

Recall the method called Sum from the SimpleCalculator.cs program in Chapter 4, "A Guided Tour Through C#: Part II," shown next:

 24:      // Sum calculates the sum of two int's 25:     public static int Sum(int a, int b) 26:     { 27:         int sumTotal; 28: 29:         sumTotal = a + b; 30:         return sumTotal; 31:     } 

Sum calculates the sum of exactly two arguments. Suppose you also needed to calculate the sum of three arguments and four arguments and even ten arguments. You could attempt to cater to this need by writing a sum method for each number of arguments. Their headers could look like the following:

 public static int SumThree(int a, int b, int c) public static int SumFour(int a, int b, int c, int d) public static int SumTen(int a, int b, int c, int d, int e, int f, int g, int h, int i,  graphics/ccc.gifint j) 

Hmm…this is starting to look cumbersome, and I haven't even included the method bodies. It would be much easier if we could write a single method that would accept any number of arguments and still perform the correct calculations. The parameter array provides a solution to our problem. A parameter array is declared by writing the params keyword in front of a one-dimensional array in the method header

 public static int Sum(params int [] numbers) 

which is identical to line 20 of Listing 12.4 shown shortly. It allows us to call Sum and include any number of arguments as demonstrated in lines 10 12 of Listing 12.4.

Note

graphics/common.gif

The parameter array must be one-dimensional, so you cannot use arrays of this format with the params keyword int [ , ]. However, arrays of the format int [], int [] [], or even int [] [] [], and so on are valid because they all contain one-dimensional arrays within them.


By including the params keyword, we ask the runtime to automatically insert the arguments provided into the numbers array in the order they are written in the method call. If the Sum method, for example, is called with the following line (see line 11 of Listing 12.4)

 ...Sum(10,20,30) 

the numbers array will be of length 3 and have the following content when the execution of Sum's method body is about to begin:

numbers array:

Index 0 1 2
Value 10 20 30

The numbers array is of base type int, so all arguments provided must be of a type implicitly convertible to type int.

The call to Sum can also contain zero arguments as demonstrated in line 14, which simply results in numbers being of length zero during the execution of Sum.

Finally, the argument provided to Sum can be a one-dimensional array of a base type implicitly convertible to the base type of numbers. This is demonstrated by line 8, which declares the array myNumbers and line 13 where myNumbers is provided as an argument for the call to Sum. As execution of Sum begins, numbers will have the same array element values as myNumbers.

Listing 12.4 SumCalculator.cs
01: using System; 02: 03: class SumCalculator 04: { 05:     public static void Main() 06:     { 07:         int result1, result2, result3, result4, result5; 08:         int[] myNumbers = { 4, 10, 6, 8, 2} ; 09: 10:         result1 = Sum(3,5); 11:         result2 = Sum(10,20,30); 12:         result3 = Sum(1,2,3,4,5,6,7,8,9,10); 13:         result4 = Sum(myNumbers); 14:         result5 = Sum(); 15: 16:         Console.WriteLine("\nEnd results: { 0} , { 1} , { 2} , { 3} , { 4} ", 17:             result1, result2, result3, result4, result5); 18:     } 19: 20:     public static int Sum(params int[] numbers) 21:     { 22:         int sum = 0; 23: 24:         Console.Write("\nThe numbers array contains { 0}  elements:", numbers.Length); 25:         foreach (int number in numbers) 26:         { 27:             Console.Write(" { 0} ", number); 28:             sum += number; 29:         } 30:         Console.WriteLine("\nThe sum is: { 0} ", sum); 31:         return sum; 32:     } 33: } The numbers array contains 2 elements: 3 5 The sum is: 8 The numbers array contains 3 elements: 10 20 30 The sum is: 60 The numbers array contains 10 elements: 1 2 3 4 5 6 7 8 9 10 The sum is: 55 The numbers array contains 5 elements: 4 10 6 8 2 The sum is: 30 The numbers array contains 0 elements: The sum is: 0 End results: 8, 60, 55, 30, 0 

The relevant points regarding the parameter array have already been discussed.

It is worth noticing how the Sum method (lines 20 32) adapts to any number of parameters provided through the use of the foreach statement (lines 25 29) that traverses through the entire numbers array.

Note

graphics/common.gif

It is not possible to use the ref and out keywords with the params keyword.


Note

graphics/common.gif

If the list of formal parameters in the method header needs to include other parameters apart from the parameter array, the parameter array must be last in the list, rendering the following method header invalid

 public static int Sum (params int[] numbers, int a, int b) //Invalid 

whereas the following is valid

 public static int Sum (int a, int b, params int[] numbers) //Valid 

Another immediate consequence of this rule is that only one parameter array can be included in any one method header.


Method Overloading: Calling Different Methods with the Same Name, Different Arguments

Before I attempt to explain what method overloading is, take a look at the following example. It gives you an idea of why method overloading was ever invented in the first place.

Chapter 4 presented you with Pythagoras's formula for calculating the distance between two locations in a Map class. The following is a brief refresher of the formula:

If L1(x1, y1) and L2(x2, y2) are two locations on a map, the distance between them can be calculated as:

graphics/12equ01.gif

We never wrote the code for a Distance method in Chapter 4, so let's write a version now:

 class Map {     ...     public double DistanceInt (int x1, int y1, int x2, int y2)     {         return Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2));     }     ... } 

The formal parameter list of DistanceInt accepts four arguments of type int or of types that are implicitly convertible to int. However, if we also need to calculate the distance between two locations defined by four arguments of, say, type double, we must write another method in the Map class. If we call it DistanceDouble, it could look like

 class Map {     ...     public double DistanceDouble (double x1, double y1, double x2, double y2)     {         return Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2));     }     ... } 

Another programmer then tells us about her newly created class called Location (shown next) that is used to represent locations. Each Location object represents a location with the private instance variables x and y. They can be accessed and modified with the accompanying accessor and mutator methods.

 class Location {     private int x = 0;     private int y = 0;     public void setX(int newX)     {         x = newX;     }     public void setY(int newY)     {         y = newY;     }     public int getX()     {         return x;     }     public int getY()     {         return y;     } } 

The programmer needs to perform distance calculations and would like to use the functionality of the methods we have already written. However, instead of providing four numeric arguments as required by the previously defined methods, she would find it more convenient to pass two Location objects. To accommodate the request of our fellow programmer, we then set out to write a method called DistanceLocationObjects. It looks as follows:

 public double DistanceLocationObjects (Location L1, Location L2) {     return Math.Sqrt(Math.Pow(L1.getX() - L2.getX(), 2) + Math.Pow(L1.getY() - L2.getY(),  graphics/ccc.gif2)); } 

We now have three different methods with three different names. However, every method essentially does the same thing calculate a distance. So even though the code works fine as is, it would be easier and more natural if each method call, whether providing arguments of type int, double, or Location, could use the same method name Distance. If this was possible, the following three method calls would be valid


graphics/12infig04.gif

and the compiler would automatically choose the correct way to handle the provided arguments. This scenario is made possible with method overloading, which allows us to define several methods in the same class with the same name but with different sets of formal parameters and different implementations. To implement method overloading in our case and make the three previous method calls valid and with correct return values, we simply need to change our three methods to have the same name Distance. Their method headers now look like the following lines, while their original method bodies stay unchanged:

 public double Distance(int x1, int y1, int x2, int y2) public double Distance(double x1, double y1, double x2, double y2) public double Distance(Location L1, Location L2) 

When the compiler now meets the following call

 myMap.Distance(10, 10, 20, 30) 

it will go through the following process to determine which method to execute. First it looks for a method in myMap with a name that matches Distance. It finds three. Of those three, it chooses the method with the set of formal parameters that matches the set of arguments provided. A match requires that

  • The amount of arguments provided fit the number of arguments accepted.

  • Each argument must have a type compatible with its corresponding formal parameter. Thus, the first argument must have the same type as specified by the first formal parameter, the second argument must have the same type as specified by the second formal parameter, and so on.

By following this procedure, the compiler chooses to execute the Distance method with the header

 public double Distance (int x1, int y1, int x2, int y2) 

because it matches the four int arguments we provided in the method call. This is illustrated in Figure 12.6.

Figure 12.6. Finding the appropriate Distance method to execute .
graphics/12fig06.gif

The complete source code of the example is provided in Listing 12.5, where all three overloaded methods are being called in lines 17, 19, and 21.

Listing 12.5 DistanceCalculator.cs
01: using System; 02: 03: class DistanceCalculator 04: { 05:     public static void Main() 06:     { 07:         Map myMap = new Map(); 08:         Location location1 = new Location(); 09:         Location location2 = new Location(); 10: 11:         location1.setX(10); 12:         location1.setY(10); 13:         location2.setX(10); 14:         location2.setY(20); 15: 16:         Console.WriteLine("Distance integers: " + 17:             myMap.Distance(5,10,5,30)); 18:         Console.WriteLine("Distance doubles: " + 19:             myMap.Distance(15.4, 20.6, 15.4, 30.60)); 20:         Console.WriteLine("Distance location objects: " + 21:             myMap.Distance(location1, location2)); 22:     } 23: } 24: 25: class Map 26: { 27:     public double Distance (int x1, int y1, int x2, int y2) 28:     { 29:         Console.WriteLine("\nUsing the integer version"); 30:         return Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2)); 31:     } 32:  33:     public double Distance (double x1, double y1, double x2, double y2) 34:     { 35:         Console.WriteLine("\nUsing the double version"); 36:         return Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2)); 37:     } 38: 39:     public double Distance (Location L1, Location L2) 40:     { 41:         Console.WriteLine("\nUsing the Location objects version"); 42:         return Math.Sqrt(Math.Pow(L1.getX() - L2.getX(), 2) + 43:             Math.Pow(L1.getY() - L2.getY(), 2)); 44:     } 45: } 46: 47: class Location 48: { 49:     private int x = 0; 50:     private int y = 0; 51: 52:     public void setX(int newX) 53:     { 54:         x = newX; 55:     } 56: 57:     public void setY(int newY) 58:     { 59:         y = newY; 60:     } 61: 62:     public int getX() 63:     { 64:         return x; 65:     } 66: 67:     public int getY() 68:     { 69:         return y; 70:     } 71: } Using the integer version Distance integers: 20 Using the double version Distance doubles: 10 Using the Location objects version Distance location objects: 10 

Three methods with the same name but different sets of formal parameters are defined in the Map class (lines 25 45). Three method calls are made to Distance (lines 17, 19, and 21), each containing a different set of arguments matching different sets of formal parameters and, therefore, different overloaded methods. Each overloaded method has been equipped with a WriteLine statement indicating when this particular method is being executed (lines 29, 35, and 41). The resulting sample output confirms our claims that each distinct Distance method version is being called.

An important concept directly related to method overloading is the method signature concept.

The name of a method, along with its amount, types, and sequence of formal parameters, constitute the signature of a method. The following is an example.


graphics/12infig05.gif

Notes:

  • The return type of a method is not included in the method signature. Thus, the following three method headers represent the same signatures, even though the first method returns a double value, the second method an int value, and the third is void:

     public double Sum (double a, double b) public int Sum (double a, double b) public void Sum (double a, double b) 

    The params keyword is ignored when establishing a methods signature. Consequently, the following two method headers have exactly the same signature:

     public double Sum (params double[] numbers) public double Sum (double[] numbers) 
  • The names of the formal parameters are not included in the method's signature, so the two displayed method headers have the same signature.

     public int Sum (double x, double y) public int Sum (double a, double b) 

All methods in a class must have different signatures, but because the method name is just one of several attributes defining the method signature, it is possible to define many methods in the same class with the same name, as long as the rest of their signature differs. These methods are referred to as overloaded methods.

The pre-defined classes in the .NET Framework makes heavy use of method overloading. For example, the familiar IndexOf method of the String class has nine overloaded versions. Their method headers are listed next. The meaning of each overloaded method is described in the .NET Framework Reference, but we can recognize the second header, which simply takes a string as an argument and returns the index of the first match of this string.

 public int IndexOf(char[]) public int IndexOf(string) public int IndexOf(char) public int IndexOf(string, int) public int IndexOf(char, int) public int IndexOf(char[], int) public int IndexOf(char, int, int) public int IndexOf(char[], int, int) public int IndexOf(string, int, int) 

Unknowingly, we have in many instances made use of the method overloading mechanism by simply calling the methods of the .NET Framework and by providing a list of arguments compatible with just one of these signatures.

Overloaded Methods and Implicit Conversions

For every argument in a method call that does not have a type exactly matching that specified by its corresponding formal parameter, the compiler will try to find an implicit conversion pathway that leads from the type of the argument to the type of the formal parameter.

The following example illustrates. As discussed in Chapter 6, a value of type float is implicitly convertible to a value of type double. With this in mind, let's inspect what happens if we attempt to call Distance in Listing 12.5 with four arguments of type float, as in the last call of the following lines:

 float myX1 = 10f; float myY1 = 15f; float myX2 = 20f; float myY2 = 25f; ...myMap.Distance (myX1, myY1, myX2, myY2)... 

No direct match is found in any of the three method signatures (lines 27, 33, and 39) for any of the arguments, so C# moves to the next step where it attempts to find a match by implicitly converting the types of myX1, myY1, myX2, and myY2 to the types present in the given formal parameters of one of the overloaded methods. Because float is implicitly convertible to double, a match is found in the method with the header:

 33:     public double Distance (double x1, double y1, double x2, double y2) 

Consequently, the compiler chooses to execute this particular overloaded method. Each of the values contained in myX1, myY1, myX2, and myY2 are implicitly converted to double and assigned to x1, y1, x2, and y2, respectively, before execution of the method commences.

During the process of finding a matching type for an argument through implicit type conversion, the compiler follows three important rules worth keeping in mind. The next three sections focus on these rules.

Overloaded Methods and Implicit Conversion Rule One

If two or more overloaded methods contain formal parameters specifying types along the implicit conversion path of the argument type, the compiler evidently has to choose from several possible matches. Rule one then says: Choose the type that is closest to the argument type in the conversion hierarchy.

Remember the implicit conversion hierarchy from Chapter 6 where it is possible to implicitly convert an sbyte to not only a short, but also to an int, long, float, double, and decimal. This is illustrated with highlighted arrows in Figure 12.7.

Figure 12.7. Implicit conversion path for sbyte.
graphics/12fig07.gif

Listing 12.6 contains two overloaded methods called DoIt to demonstrate the effect of this first rule.

Listing 12.6 MethodMatcher.cs
01: using System; 02: 03: class MethodMatcher 04: { 05:     public void DoIt (int x) 06:     { 07:         Console.WriteLine("Executing int: " + x); 08:     } 09: 10:     public void DoIt (double x) 11:     { 12:         Console.WriteLine("Executing double: " + x); 13:     } 14: } 15: 16: class Tester 17: { 18:     public static void Main() 19:     { 20:         MethodMatcher myMatcher = new MethodMatcher(); 21:         sbyte sbyteValue = 10; 22:         int intValue = 1000; 23:         long longValue = 30000; 24: 25:         Console.Write("Calling with an sbyte value --> "); 26:         myMatcher.DoIt(sbyteValue); 27:         Console.Write("Calling with an int value --> "); 28:         myMatcher.DoIt(intValue); 29:         Console.Write("Calling with a long value --> "); 30:         myMatcher.DoIt(longValue); 31:     } 32: } Calling with an sbyte value --> Executing int: 10 Calling with an int value --> Executing int: 1000 Calling with a long value --> Executing double: 30000 

The first DoIt method (lines 5 8) contains a formal parameter that accepts an argument of type int. The second DoIt method in lines 10 13 accepts an argument of type double. Which overloaded method will then be executed if we call DoIt with an argument of type sbyte, as in line 26? The first step of finding an exact type match fails. The second step reveals that both method signatures contain types (int and double) that exist along the implicit conversion path of sbyte. According to the rule specified earlier, the compiler chooses the type closest to sbyte in the conversion path in this case, int. This is confirmed by the sample output.

Line 28 calls DoIt with an argument of type int. This call is easily resolved because the DoIt method of lines 5 8 contains the signature DoIt (int x), constituting an exact match to this call.

Finally, DoIt is called with an argument of type long. No exact match is found, so the compiler must resort to implicit conversion. The compiler moves along the implicit conversion path from long (see Figure 12.7) and finds a match between the last type on the path (double) and the signature DoIt (double x) of line 10, resulting in the execution of the corresponding method.

Overloaded Methods and Implicit Conversion Rule Two

Two arrows leave the long box of Figure 12.7. One points to float, the other to decimal. In other words, a value of type long can either be implicitly converted to a value of type float, or to a value of type decimal. Now, consider a situation where we substitute the two DoIt methods of Listing 12.6 with two other DoIt methods containing the following two headers:

 void DoIt (float x) void DoIt (decimal x) 

If we now make the call shown in the second line

 sbyte mySbyte = 10; DoIt (mySbyte) 

how can the compiler choose which overloaded method to execute once the search reaches the long box? It can't. There is no rule stating which of the two arrows the compiler should follow as it leaves the long box to find an appropriate match. The method call is deemed unambiguous and triggers a compiler error.

Rule 2 generalizes the DoIt example just mentioned. Consider two overloaded methods called MyMethod. The two signatures of these two methods are identical, apart from two corresponding formal parameters of which one is a float type and the other a decimal type, as shown in the following two lines.


graphics/12infig06.gif

We then zoom in on the argument called myArgument included in a call to MyMethod, which corresponds to the highlighted formal parameters in the two MyMethods:

 MyMethod (..., myArgument, ...) 

The call to MyMethod will be deemed ambiguous if myArgument is of any of the sbyte, short, int, long, byte, ushort, uint, ulong, or char types. This is because any of these types will lead the compiler's search for a matching type through either the long or the ulong type boxes of the implicit conversion pathway. Two arrows leave from both the long and the ulong type boxes, as mentioned before and shown in Figure 12.7, so the compiler can only resolve the call if, during its search through the implicit conversion path, it does not have to travel through the types long or ulong. The method call is only acceptable if myArgument is of type float, double, or decimal.

Exactly the same conditions as described before hold if the two method headers look like the following:


graphics/12infig07.gif

where the formal parameter of type float has been substituted with a formal parameter of type double.

Overloaded Methods and Implicit Conversion Rule Three

We have seen how two arrows leaving both the long and the ulong boxes cause certain method calls to be ambiguous. But two arrows also leave the char, byte, ushort, and uint boxes; doesn't this then cause the same problems? No! This time, C# has a rule (here called rule three) to remove the apparent ambiguousness.

Rule three: If the compiler on its search for a matching method signature moves through the implicit conversion pathway and finds two equally suited signatures, one containing a signed integer type, the other containing an unsigned integer type, it chooses the signature containing the signed integer type. The program of Listing 12.7 illustrates.

Listing 12.7 MethodMatcherTwo.cs
01: using System; 02: 03: class MethodMatcherTwo 04: { 05:     public void DoIt (uint x) 06:     { 07:         Console.WriteLine("Executing uint: " + x); 08:     } 09: 10:     public void DoIt (int x) 11:     { 12:         Console.WriteLine("Executing int: " + x); 13:     } 14: } 15: 16: class Tester 17: { 18:     public static void Main() 19:     { 20:         MethodMatcherTwo myMatcher = new MethodMatcherTwo(); 21:         byte byteValue = 10; 22: 23:         Console.Write("Calling with a byte value  > "); 24:         myMatcher.DoIt(byteValue); 25:     } 26: } Calling with a byte value --> Executing int: 10 

MethodMatcherTwo.cs is similar to MethodMatcher.cs in that the overloaded method DoIt residing in the MethodMatcherTwo class is called from the Main() method. This time, the two overloaded methods contain a formal parameter of the unsigned type uint (line 5) and a formal parameter of the signed type int (line 10), respectively. DoIt is called with an argument of type byte (line 24). The compiler cannot find any exact match to byte in any of the overloaded methods, so the next step is to move along the implicit conversion path beginning at the byte box of Figure 12.7. When it arrives at the ushort box, it has two options. It can either choose the signature with the unsigned uint or the signed int. Rule three then dictates that the compiler choose the signed int as confirmed by the sample output.

Often, overloaded methods provide flexibility and convenience for the programmer calling them. A programmer only has to remember one method name and can then just pass whatever information he or she has available along as arguments. C# then automatically works out which particular overloaded method is suitable to be executed. Hmm…so the more overloaded methods we provide for one method name, the better, right? Not necessarily! Method overloading can be your good friend if implemented properly, or your great enemy if misused or overused. The next Common Pitfall gives an example of the latter.

Avoid overloading methods that essentially perform different actions. Instead call them different names.

Recall that 10 is an int and 10.0 is a double. Then consider the following two headers of overloaded methods:

  1. public int Sum (int a, int b)

  2. public double Sum (double a, double b)

The call

 Sum (10, 10) 

containing two int's causes the method in 1 to be executed, but

 Sum (10.0, 10.0) 

with two doubles causes the method in 2 to be executed. A minute detail like .0 decides which method is executed. This is fine here because the two methods essentially perform the same action. Apart perhaps concerns regarding precision, we don't care whether (10 + 10) or (10.0 + 10.0) is being calculated. However, if the two methods are performing significantly different actions, it is not advisable to let the type of a number (read a decimal point) decide which action is taken.

Listing 12.8 shows how things can go wrong when a method is over-overloaded. A programmer, who has just read about method overloading (but who skipped the Common Pitfall boxes), is working on the underlying computer program for a new virtual bookshop on the Internet. At the moment, he or she is writing the prototype for a Book class (lines 3 44) that eventually will hold all the relevant information about a book in the final program. The analysis of Listing 12.8 reveals the flaws of the Book class design.

Listing 12.8 VirtualBookshop.cs
01: using System; 02: 03: class Book 04: { 05:     private string title; 06:     private uint numberOfPages; 07:     private double weight; 08: 09:     public void Set(string newTitle) 10:     { 11:         title = newTitle; 12:     } 13: 14:     public void Set(uint newNumberOfPages) 15:     { 16:         numberOfPages = newNumberOfPages; 17:     } 18: 19:     public void Set(double newWeight) 20:     { 21:         weight = newWeight; 22:     } 23: 24:     public void Set(string newTitle, uint newNumberOfPages, double newWeight) 25:     { 26:         title = newTitle; 27:         numberOfPages = newNumberOfPages; 28:         weight = newWeight; 29:     } 30: 31:     public void Set(string newTitle, double newWeight, uint newNumberOfPages) 32:     { 33:         title = newTitle; 34:         numberOfPages = newNumberOfPages; 35:         weight = newWeight; 36:     } 37: 38:     public void PrintDetails () 39:     { 40:         Console.WriteLine("\nTitle: " + title); 41:         Console.WriteLine("Number of pages: " + numberOfPages); 42:         Console.WriteLine("Weight: { 0}  pounds", weight); 43:     } 44: } 45:  46: class VirtualBookshop 47: { 48:     public static void Main() 49:     { 50:         Book myBook = new Book(); 51: 52:         myBook.Set("The latest tourist attraction: Planet Blipos"); 53:         myBook.Set(1.3); 54:         myBook.Set(300); 55:         myBook.PrintDetails(); 56: 57:         myBook.Set("Great places to visit on planet Blipos", 0.1, 4); 58:         myBook.PrintDetails(); 59:         myBook.Set("Traveling to planet Blipos: A survival guide", 2000, 10.0); 60:         myBook.PrintDetails(); 61: 62:         myBook.Set(11); 63:         myBook.PrintDetails(); 64: 65:         //myBook.Set("Program your own space rocket", 8070, 3); 66:     } 67: } Title: The latest tourist attraction: Planet Blipos Number of pages: 300 Weight: 1.3 pounds Title: Great places to visit on planet Blipos Number of pages: 4 Weight: 0.1 pounds Title: Traveling to planet Blipos: A survival guide Number of pages: 2000 Weight: 10 pounds Title: Traveling to planet Blipos: A survival guide Number of pages: 11 Weight: 10 pounds 

The program is still in its infancy, so only three pieces of information are represented in the Book class (lines 5 7) the title, number of pages, and the weight (used when calculating shipping cost). As usual, the instance variables are declared private, so mutator methods must be provided to set their values. Excited about method overloading, the programmer believes it will be easier for the user of the class to apply just one method name called Set and, through overloading (headers in lines 9, 14, 19, 24, and 31), let C# second guess the intention of the method call and automatically choose the correct method. Calling Set with a single argument of type uint (or int) will assign the argument to numberOfPages (lines 14 17). An argument of type double will set the weight of the book (lines 19 22), and a single argument of type string will set the title (lines 9 12). This makes reasonable sense because the number of pages is always an integer number (an int or uint), weight most often contains a fraction (double), and a title consists of text (string).

Line 53 with the argument 1.3 correctly executes the double version of the overloaded methods, and line 54 with the argument 300 executes the uint version. Smooth sailing so far.

The programmer further wrote two overloaded Set methods containing three formal parameters (lines 24 29 and lines 31 36). They enable a programmer to assign values to all instance variables with just one method call, and lets him or her choose between inserting the number of pages before the weight (line 24) or vice versa (line 31) when specifying the arguments of a call to Set. Line 57 takes advantage of this facility and provides the weight (0.1) as the second argument, whereas line 59 positions the weight as the third argument (10.0). Both statements are correctly executed.

However, the first problem is encountered in line 62 where we intend to set the weight to 11.0 pounds, but forget to include the .0. As a result, C# sees an int (11), which is interpreted as the number of pages and is assigned as such with the method spanning lines 14 17. The sample output reveals the problem a book with eleven pages weighing 10 pounds.

The double slash of line 65 has been included to avoid a compiler error. Removing them causes the following compiler error:

 VirtualBookshop.cs(65,9): error CS0121: The call is ambiguous between the following methods or properties:'Book.Set(string, double, uint)'  and 'Book.Set(string, uint, double)' 

We intended to assign 3 to weight but forgot to include .0 and thereby confused the compiler that no longer can choose between the signatures of line 24 and line 31.

To avoid the shown problems the programmer should have done the following:

  • Limited the number of overloaded methods and instead created methods with names reflecting their essential actions, such as

     public void SetTitle(string newTitle) public void SetNumberOfPages (uint newNumberOfPages) public void SetWeight (double newWeigth) 
  • Limited the number of multi-assignment methods to one instead of two. The header of the remaining method accepting three arguments could look as follows:

     public void SetAll (string newTitle, uint newNumberOfPages,  double newWeight) 
Self-Referencing with the this Keyword

Every method of an object is automatically equipped with a reference called this that refers to the current object for which the method has been called. this is a keyword and is written inside a method body when a reference to the current object is needed.

Figure 12.8 illustrates the meaning of this. Two classes Book and VirtualBookshop are defined. A new Book object is created in line 70 of the VirtualBookshop class and its reference is assigned to myBook. myBook now references this new Book object (See 1 in Figure 12.8). AnyMethod (lines 20 40) is invoked through myBook in line 75. During the execution of AnyMethod, this holds a reference identical to that of myBook, and so is referencing the same object as myBook (See 2 in Figure 12.8). this can be applied like any other object reference, such as myBook with the small difference that this can also access private class members. For example, we can refer to an instance variable of the current object by applying the dot operator to this followed by the name of the instance variable, as shown in line 30.

Figure 12.8. The this reference.
graphics/12fig08.gif

But wait a minute, previous examples only needed to use the name itself (title, in this case) when specifying an instance variable from within its class, so

 30:            this.title 

would be the same as

 30:            title 

This has been correct for the examples presented so far in this book, but Listing 12.9 provides a scenario where this is not the case and where this is needed to clarify our intentions. Listing 12.9 is similar to Listing 12.8 in that it contains a Book class with three instance variables (lines 5 7). Only one mutator method is provided for simplicity (lines 9 14). Instead, the class has been equipped with the accessor method GetWeight (lines 16 19) and a method called PrintShippingCost to print the shipping cost (lines 21 25). The GetHeavierBook (lines 27 33) compares the weight of the book represented by the current object with a Book object referenced by the argument passed to it. A reference to the heavier book is returned to the caller.

The Dispatcher class contains a method to calculate the cost of shipping a particular Book passed to it as an argument. The functionality provided by the Book class methods is applied by the Main() method of the VirtualBookshop class.

this is used for three different purposes in VirtualBookShopTwo.cs. We zoom in on those in the analysis after the sample output.

Listing 12.9 VirtualBookshopTwo.cs
01: using System; 02: 03: class Book 04: { 05:     private string title; 06:     private uint numberOfPages; 07:     private double weight; 08: 09:     public void SetAll(string title, uint numberOfPages, double weight) 10:     { 11:         this.title = title; 12:         this.numberOfPages = numberOfPages; 13:         this.weight = weight; 14:     } 15: 16:     public double GetWeight() 17:     { 18:         return weight; 19:     } 20: 21:     public void PrintShippingCost() 22:     { 23:         Console.WriteLine("\nCost of shipping\"{ 0}\": { 1:C} ", 24:             title, Dispatcher.ShippingCost(this)); 25:     } 26: 27:     public Book GetHeavierBook (Book aBook) 28:     { 29:         if (weight > aBook.GetWeight()) 30:             return this; 31:         else 32:             return aBook; 33:     } 34: } 35: 36: class Dispatcher 37: { 38:     public static decimal ShippingCost (Book bookToSend) 39:     { 40:         return 5m + (decimal)(bookToSend.GetWeight() * 3); 41:     } 42: } 43: 44: class VirtualBookshop 45: { 46:     public static void Main() 47:     { 48:         Book myBook = new Book(); 49:         Book yourBook = new Book(); 50:         Book heavierBook; 51: 52:         myBook.SetAll("The Bliposians: Customs and Etiquette", 400, 2.3); 53:         Console.WriteLine("Shipping cost: { 0:C} ", Dispatcher.ShippingCost (myBook)); 54:         myBook.PrintShippingCost(); 55: 56:         yourBook.SetAll("Speak Blipolish in twenty days", 610, 3.1); 57:         heavierBook = yourBook.GetHeavierBook(myBook); 58:         heavierBook.PrintShippingCost(); 59:     } 60: } Shipping cost: $11.90 Cost of shipping "The Bliposians: Customs and Etiquette": $11.90 Cost of shipping "Speak Blipolish in twenty days": $14.30 

Note

graphics/common.gif

You might see another currency symbol than the dollar sign ($) in the sample output. The displayed symbol depends on the settings of your Windows operating system.


Listing 12.9 demonstrates three common uses for this.

  • To differentiate formal parameters and local variables from instance variables The scope of a method is part of the scope of the class within which this method is defined. Therefore, the formal parameters and local variables defined for a method are in the same scope as the instance variables of the class where the method resides, as illustrated in Figure 12.9. Usually, C# does not permit two variables belonging to the same scope to have the same name (see the "The Scope of Variables" section in Chapter 8). However, when one is an instance variable and the other a formal parameter or local variable, the compiler accepts it.

    Figure 12.9. The scope of local variables, formal parameters, and instance variables.
    graphics/12fig09.gif

    Sometimes, we might want to take advantage of this possibility and declare formal parameters and local variables with the same names as their similarly scoped instance variables. Formal parameters in mutator methods used to assign new values to instance variables is a typical scenario exemplified in Listing 12.9 shown before. Before we look closer at Listing 12.9, we need to briefly revisit the Set mutator method in lines 24 29 of the VirtualBookshop.cs program in Listing 12.8. Its formal parameter names (newTitle, newNumberOfPages, and newWeight) were constructed by adding new in front of each of the corresponding instance variables to avoid a name clash with those. Name clashes would have resulted in problems that could only have been solved by the then unknown this keyword. That was then. Armed with the knowledge of this, we can now apply another line of thought saying, "Give the name to the formal parameter/local variable that is most suitable, disregarding name-clashes with instance variables." I have decided that title is a better name than newTitle (otherwise, I couldn't demonstrate the use of this), so we end up with the names written in line 9 of Listing 12.9. And here is the problem I alluded to before. How does the compiler know what title refers to when written inside the method body of SetAll. Is it the instance variable title or the formal parameter title? Because local variables and formal parameters take precedence over instance variables, title represents the formal parameter. But how do we then specify the instance variable title? Because this is a reference to the object containing the instance variables,

     this.title 

    refers to the instance variable title. Line 11 thus instructs the compiler to assign the formal parameter title to the instance variable title.

  • To pass the reference of the current object as an argument to another method The ShippingCost method in lines 38 41 of the Dispatcher class takes an argument of type reference-to-Book-object and calculates the shipping cost of the book represented by the referenced object. It uses a simple formula saying "Fixed cost ($5.00) plus the weight in pounds times $3.00." Mathematically expressed, it looks as follows:

     5 + (weight x 3) 

    The weight is obtained by calling the accessor method GetWeight of the Book object that is referenced by the formal parameter bookToSend.

    ShippingCost is used in line 53 as part of a statement that uses WriteLine to print out the shipping cost of myBook. ShippingCost is a static method, so it is called through the Dispatcher class itself. myBook holds a reference to a Book object (see line 48) and is used as an argument in line 53 for the call to ShippingCost. We manage to print out all the information we want with line 53; however, it requires us to fiddle around with the WriteLine method along with appropriate text and shipping cost to make the output look nice onscreen. So why not let the Book object take care of this? Then we can simply call a method like PrintShippingCost through myBook, as in line 54, everything else is taken care of. Let's look closer at how the PrintShippingCost method is designed in lines 21 25. PrintShippingCost needs to obtain the shipping cost of the current Book object. Luckily, the ShippingCost method of the Dispatcher class provides this information. But the argument must be a book object reference, so we need the reference of the object we are currently working inside as an argument. This argument is identical to the reference held by myBook. However, because myBook is in a different scope and not accessible from within the object it is referencing, myBook is useless for our purposes. Fortunately, this is the equivalent to myBook when inside Book, just as illustrated in Figure 12.8 shown earlier, so this is provided as a suitable argument in line 24.

  • To return the reference of the current object back to the caller of the method The GetHeavierBook method of the book class compares the weight of the current object (represented by the instance variable weight in line 29) and the weight (represented by aBook.GetWeight()) of a book object referenced by the formal parameter aBook. GetHeavierBook must return a reference to the Book object representing the heavier book. If aBook is heavier than the current object, the task is easy. We just return aBook (line 32). But what if the current object is heavier? Then we need to return a reference to the current object, in other words, we need to return this (line 30).

    The Main() method utilizes the GetHeavierBook method in line 57 to find the heavier book of myBook and yourBook. The GetHeavierBook is invoked by applying the dot operator to yourBook. Thus, yourBook is referencing the same object as this inside the GetHeavierBook method during the execution of this call. Because myBook is passed as an argument to GetHeavierBook, it holds the same reference as the formal parameter aBook. Line 30 is executed if weight is greater than aBook.GetWeight(), causing this to be returned and thereby assigned to heavierBook. Because this is equal to yourBook, heavierBook will, in this case, reference the same object as yourBook. Conversely, if line 32 is executed because weight is smaller than or equal to aBook.GetWeight(), aBook is returned and causes heavierBook to reference the same object as myBook.

Note

graphics/common.gif

Some programmers adhere to the following rule: Don't change the names of local variables or formal parameters to avoid name clashes with instance variables. Instead, use the this keyword to indicate which of the variables are instance variables.

Other programmers will actively avoid name clashes to avoid the use of this.

Whichever side you belong to, remember to be consistent.


Avoid Cluttering the Code With this

graphics/bulb.gif

To call a method of a class from within another method of the same class, we have been used to simply write the name of the method we are calling. For example, in the following code snippet, the method Average in lines 20 30 is called from line 42 of the PrintAverage method by writing the following:

 Average(numbers) 

The compiler is clever enough to figure out that it must look for the method within the confines of the current object. In fact, it automatically inserts this. in front of the method call during compilation. Consequently, the two method calls

 Average(numbers) 

and

 this.Average(numbers) 

have the same meaning when residing inside the Calculator class shown here. However, from a style point of view, you should avoid cluttering the code with unnecessary thiss. So stick with the style used in line 42 and use this only when you explicitly need to specify the reference to the current object.

 10: class Calculator 11: {         ... 20:     private double Average (params int[] numbers) 21:     {             ... 30:     } 40:     private PrintAverage (params int[] numbers) 41:     { 42:         Console.WriteLine("The average is: { 0}  ", Average(numbers)); 43:     } 44: } 



   


C# Primer Plus
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2000
Pages: 286
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net