Two very important concepts in object-oriented programming are overriding and overloading. Overloading is about creating multiple methods with the same name, but different signatures, in the same scope. Overriding is about changing the behavior of a certain method in the child class from the way it is behaving in the parent class. The rest of this article will explore these two concepts in detail.
Contributed by Ayad Boudiab Rating: / 24 June 10, 2008
Overloading occurs when a method has more than one definition in the same scope. It's important to remember two key points from the previous statement: same name and same scope. The method implementations have the same name because they do similar tasks. For instance, if we need to implement a method that gets the student name, there are many ways to do that. We can get the name using an id and we can get the name using a social security number. One way to implement the methods is as follows:
class Student
{
...
public string GetName( int id) {...}
public string GetName( string ssn) {...}
...
}
The method GetName() is overloaded since the two implementations are in the same scope (class scope) and have the same name. If the methods are declared in different scopes (for example, different classes), then we are not talking about overloading.
The first question that comes to mind is: do we have to declare them with the same name? The answer is no. We could have named them GetName1() and GetName2(), or even GetNameById() and GetNameBySSN(). So overloading is not mandatory; it is just a helpful feature in object-oriented languages (like C++, Java, and C#). It is common sense since both methods are doing pretty much the same thing.
Because the methods have the same name, the compiler will use the signature to determine what method to call under different scenarios. The compiler will be able to tell the difference using the method signature, which has to do with the method parameters. Be definition, the method signature is the name of the method and its parameters. Since the name in this case is the same, what is left is the parameters. There are three ways to distinguish one set of parameters from the other:
Parameter count - one method could have 2 parameters while the other has 3. This way, the compiler will call the method with the correct number of parameters.
Parameter type - if two methods have the same parameter count, but different types (int, string, long …), then it is easy for the compiler to know which method to call.
Parameter order - when we have a match in the parameter count and parameter type, but with a different order, then it is again easy for the compiler to know which method to call. For example:
public string GetName( int id, string match) {...}
public string GetName( string match, int id) {...}
Here is a full example that illustrates method overloading:
namespace OverloadingMethods
{
class Program
{
static void Main( string [] args)
{
Point p1 = new Point ();
Console .WriteLine( "p1 -> {0}" , p1);
p1.Move();
Console .WriteLine( "p1 -> {0}" , p1);
p1.Move(2);
Console .WriteLine( "p1 -> {0}" , p1);
p1.Move(4, 7);
Console .WriteLine( "p1 -> {0}" , p1);
Point p2 = new Point (2, 5);
Console .WriteLine( "p2 -> {0}" , p2);
p2.Move(10, 9);
Console .WriteLine( "p2 -> {0}" , p2);
p2.Move();
Console .WriteLine( "p2 -> {0}" , p2);
}
}
class Point
{
private int x;
private int y;
Random rand = new Random ();
public Point()
{
x = y = 0;
}
public Point( int x_value, int y_value)
{
x = x_value;
y = y_value;
}
//Move x with a random number between 1 and 100 (inclusive)
//Move y with a random number between 1 and 50 (inclusive)
public void Move()
{
x += rand.Next(1, 101);
y += rand.Next(1, 51);
}
//move x and y by the same number (delta)
public void Move( int delta)
{
x += delta;
y += delta;
}
//move x and y by the specified values
public void Move( int delta_x, int delta_y)
{
x += delta_x;
y += delta_y;
}
public override string ToString()
{
return string .Format( "[{0},{1}]" , x, y);
}
}
}
Notice that the Point class is flexible in the way a point can be moved. By overloading the Move() method, we are able to move a point via different x and y values, via the same value (delta), or via random x and y values. This way, our class gives its user different ways of moving a point using the same friendly Move() method.
In addition to overloading a method, we are also able to overload a constructor. In the same way we give the user different ways of calling a method, we can provide different ways to construct an object. In the Point class, we did just that when we created two constructors: a default constructor (no parameters) and a constructor that takes x and y values. Here is another example that illustrates the way we can overload a constructor:
namespace Overloading
{
class Program
{
static void Main( string [] args)
{
Fraction f1 = new Fraction ();
Console .WriteLine(f1.ToString());
Fraction f2 = new Fraction (3);
Console .WriteLine(f2.ToString());
Fraction f3 = new Fraction (2, 5);
Console .WriteLine(f3.ToString());
}
}
class Fraction
{
private int num;
private int denom;
//constructors
public Fraction()
{
num = denom = 1;
}
public Fraction( int n)
{
num = n;
denom = 1;
}
public Fraction( int n, int d)
{
num = n;
denom = d;
}
public override string ToString()
{
return string .Format( "Fraction {0}/{1} = {2}" ,
num, denom, ( double )num / denom);
}
}
}
In the Fraction class, we provided three different constructors:
public Fraction(): This is the default constructor. It allows the user to create the simplest fraction, 1 for the numerator and 1 for the denominator (1/1).
public Fraction( int n): This constructor is similar to the previous one, but it allows the user to set the numerator (denominator still 1).
public Fraction( int n, int d): This constructor gives the fullest flexibility because it allows the user to provide values for the numerator and the denominator.
Having these three constructors, now we can create different fractions based on our needs:
Fraction f1 = new Fraction ();
Fraction f2 = new Fraction (3);
Fraction f3 = new Fraction (2, 5);
Final note: The return type method cannot be used in the method signature to distinguish one method call from the other. So the following is not considered overloading; it is actually a compile time error:
public void Move( int delta_x, int delta_y) {...}
public int Move( int delta_x, int delta_y) {...}
The only difference between the two methods is that one returns a void, and the other returns an int.
As mentioned earlier, overriding has to do with parent and child classes. If you are not satisfied with the implementation of a method in the parent class, you can keep the same declaration (signature and return type), but provide a different implementation. For example, a Rectangle class inherits the drawing functionality from a Shape class, but it overrides this functionality in order to be able to draw a rectangle.
We have been using overriding all along in our examples. In C#, everything is an object, so when we create a class ( Employee , Fraction …) that does not specifically inherit from another class, it automatically inherits the functionality of an Object. Notice that in our previous examples, we were able to use the ToString() method to print an object as a string. The ToString() method is defined in the Object class. Since the Object class is too general, this method cannot give us anything useful, so we override it.
Ponder the following example:
namespace OverridingExample
{
class Program
{
static void Main( string [] args)
{
Employee e = new Employee ( "Jim" , "Tester" , 38, 15.95);
Console .WriteLine(e);
Manager m = new Manager ( "Jennifer" , "Jones" , 40, 25, 350);
Console .WriteLine(m);
}
}
class Employee
{
protected string firstName;
protected string lastName;
protected int hoursWorked;
protected double ratePerHour;
public Employee( string f_name, string l_name,
int hours, double rate)
{
firstName = f_name;
lastName = l_name;
hoursWorked = hours;
ratePerHour = rate;
}
public virtual double CalculatePay()
{
return hoursWorked * ratePerHour;
}
public override string ToString()
{
return string .Format( "Name: {0} {1}nPay: {2}" ,
firstName, lastName, CalculatePay());
}
}
class Manager : Employee //Manager inherits from Employee
{
private double bonus;
public Manager( string f_name, string l_name,
int hours, double rate, double bonus)
: base (f_name, l_name, hours, rate)
{
this .bonus = bonus;
}
public override double CalculatePay()
{
return base .CalculatePay() + bonus;
}
}
}
In the Employee class, we are overriding the ToString() method in order to print the Employee in a nicely formatted string (notice the keyword override in the method declaration). The Employee class did not specifically inherit from any class. So by default, it inherits from the Object class, and we are able to override the ToString() method. The CalculatePay() method calculates the Employee ’s pay (rate * hoursWorked).
In the Manager class, on the other hand, we did not override the ToString() method because the one inherited from the Employee class is sufficient. But a Manager’s pay is different from a regular Employee’s pay. Based on that, we can override the CalculatePay() method to account for the bonus. To make this work, however, we need to declare the CalculatePay() method in the Employee class as virtual (which means it is able to be overridden). Now, when we have a reference pointing to an Employee class, it will call that Employee’s CalculatePay() method. When that reference is pointing to a Manager class, it will call the Manager’s Calculatepay() method.
Conclusion
Overriding and overloading are very important features in an object-oriented language like C#. When used correctly, they can lead to very powerful and readable code. When we have methods with similar functionality, it is not necessary to give them different names when we can overload them instead. And when we are not satisfied with the functionality provided in a parent class, we can override the method to meet our needs.