Till now, we have learned about the fundamentals of classes and objects which are the core building blocks of object-oriented programming. Soon we will learn more about other object-oriented concepts and how to implement them in C# language.
Object Oriented Programming Principles
For a language to be object-oriented, it must implement the three principles of an object-oriented language, which are Encapsulation, Polymorphism, and Inheritance. Let's quickly see what these are.
- Encapsulation: This states that both the data members and methods must be wrapped or encapsulated inside a class. This we have already seen how classes can have data members and how the methods can take those data members and do some action over them and provide the results.
- Polymorphism: This states that the programming constructs can have multiple (poly) forms (morphs). By having multiple forms we mean that a set of statements of a construct can be reused in various forms in accordance with the needs and requirements. Polymorphism is achieved with the help of two concepts - Overloading and Overriding.
- Inheritance: This states that a class can include the functionalities of another class as its own i.e. it can inherit from another class which we refer to as parent class. We often call this implementation as a parent-child relationship between classes. The parent class and child class, in this case, is also called base class and derived class respectively.
So far, we have covered Encapsulation in previous chapters by learning how to design a class and wrap data members and methods together. We have also learned how those methods utilize the data members, process them, and provide output. We will cover inheritance and Overriding in the next chapter.
In object-oriented programming languages, we achieve polymorphism in two ways. One is compile-time polymorphism (also called Overloading) and the other is run-time polymorphism (also called Overriding). In this chapter, we will focus on compile time polymorphism or Overloading. In Overloading, we can have the same method names with different method signatures. Constructors and some operators can also be overloaded. First, let's talk about method overloading.
Method Overloading
If two or more methods within the same class have the same name but different method signatures then this is called method overloading. For example, in the real world, a person can do multiple tasks provided what the characteristics of the task are. This means that a task can have many forms. To incorporate such a scenario in object-oriented programming, method overloading is used.
So let's see an example to understand it better. Let's say we want to write a code to send an email so we will write a SendMail(..) method. An email can be sent to one person or multiple persons means the SendMail(..) can have two forms. So we are going to write two methods with same name "SendMail" but with different method signatures.
using System;
class Email
{
public void SendMail(string fromEmailId, string toEmailId, string subject, string body)
{
Console.WriteLine("Sending email to one");
Console.WriteLine($"Email send to {toEmailId}");
}
public void SendMail(string fromEmailId, string[] toEmailList, string subject, string body)
{
Console.WriteLine("Sending email to many");
for(var i=0; i<toEmailList.Length; i++)
{
Console.WriteLine($"Email send to {toEmailList[i]}");
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Method Overloading");
Console.WriteLine("");
Email email = new Email();
email.SendMail("[email protected]", "[email protected]", "Important message", "This is an important message");
var emailList = ["[email protected]", "[email protected]", "[email protected]"];
email.SendMail("[email protected]", emailList, "Important message", "This is an important message");
Console.Write("Press any key to continue...");
Console.ReadKey(true);
}
}
In the above program, we can see that we are overloading the SendMail(..) method by changing its signature (precisely one of the variables from toEmailId (string type) to toEmailList (string array)). This gives us a clean way of using the same method based on different use cases. It also reduces the number of important method names we need to remember.
Constructor Overloading
Similar to methods, constructors can be overloaded. A class can have multiple definitions of the constructor. From the previous chapters, we came to know that we use constructors to initializing data members. However, there are use cases where we do not need to initialize every data member or need to work with only specific operations that need only specific data members. To cater to such scenarios, we need to provide multiple definitions of the constructor, or in other terms, we can say that we need to overload constructors.
So, let's see an example.
using System;
class ImageEditor
{
int width;
int height;
string backgroundColor;
public ImageEditor()
{
width = 100;
height = 200;
backgroundColor = "white";
}
public ImageEditor(int width, int height)
{
this.width = width;
this.height = height;
backgroundColor = "white";
}
public ImageEditor(int width, int height, string backgroundColor)
{
this.width = width;
this.height = height;
this.backgroundColor = backgroundColor;
}
public void GetImage()
{
Console.WriteLine("Getting Image");
Console.WriteLine($"Width : {this.width}");
Console.WriteLine($"Height : {this.height}");
Console.WriteLine($"BackgroundColor : {this.backgroundColor}");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Constructor Overloading");
Console.WriteLine("");
var imageEditor1 = new ImageEditor();
var imageEditor2 = new ImageEditor(400, 500);
var imageEditor3 = new ImageEditor(150, 250, "grey");
Console.WriteLine("First Image");
imageEditor1.GetImage();
Console.WriteLine();
Console.WriteLine("Second Image");
imageEditor2.GetImage();
Console.WriteLine();
Console.WriteLine("Third Image");
imageEditor3.GetImage();
Console.WriteLine();
Console.Write("Press any key to continue...");
Console.ReadKey(true);
}
}
In the above example, there are three constructors or we can say two more overloaded constructors. So as you can see in the Main() method, there are three ways we can initialize the instance of ImageEditor class depending on the use case and requirement. The first constructor takes no argument and initialize the data members length, width and backgroundColor to 100, 200 and white respectively. The second constructor takes two arguments length and width and initialize the corresponding data members with the parameter values keeping the backgroundColor as default. The third constructor takes three arguments and initialize all three data members with the values provided by the constructor parameters. So overloading constructors gives us such kind of flexibility to create objects with different initializations.
Operator Overloading
Operator Overloading is one of an amazing and interesting features of C# but its not frequently used in general programming and application development as it serves very specific use cases. Having said that, it can give us a very clean operation on objects. When we overload an operator, we change the default behaviour of the operator and make it work on objects rather than usual operands.
The general syntax of operator overloading is:
public static return_type operator op (argument list);
To understand it further let's take a real world example. Suppose if I say that we need to add two objects of EmployeeSalary class. Here the salaries is not of type integer or float or double. It is of the type of user defined class "EmployeeSalary".
Now, let's say we created two objects "salary_alice" and "salary_bob" of Salary class, can we write the statement "salary_alice + salary_bob" which can give the combined salary of both Alice and Bob? The answer is "yes". Let's see with the help of an example as how salary objects can be added and return the sum of actual salary of both these people.
using System;
class EmployeeSalary
{
string name;
double baseSalary;
double hra;
double da;
public EmployeeSalary(string name, double baseSalary, double hra, double da)
{
this.name = name;
this.baseSalary = baseSalary;
this.hra = hra;
this.da = da;
}
double GetSalary()
{
return this.baseSalary + this.hra + this.da;
}
public static double operator + (EmployeeSalary a, EmployeeSalary b)
{
return a.GetSalary() + b.GetSalary();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Operator Overloading");
Console.WriteLine("");
var aliceSalary = new EmployeeSalary("Alice", 3000, 1000, 1000);
var bobSalary = new EmployeeSalary("Bob", 3200, 1000, 700);
var costToCompany = aliceSalary + bobSalary;
Console.WriteLine($"Total cost to company = {costToCompany}");
Console.Write("Press any key to continue...");
Console.ReadKey(true);
}
}
In the above example, if we see inside main method, we are adding two objects "aliceSalary" and "bobSalary" to get the desired result and that was made possible by overloading the "+" operator. The following line of code was the "+" operator overloading in the class EmployeeSalary.
public static double operator + (EmployeeSalary a, EmployeeSalary b)
{
return a.GetSalary() + b.GetSalary();
}
Similarly, there are few other operators that can be overloaded. These are:
Operator Type | Operators |
---|---|
Unary Operators | +, -, !, ~, ++, --, true, false |
Binary Operators | +, -, *, /, %, &, |, ^, <<, >> |
Comparison Operators | ==, !=, <, >, <e;, >e; (These must be overloaded in pairs for e.g. if we overload == & then != must be overloaded as well. Similarly, <, > and <e;, >e;) |