Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Inheritance | OOP Principles
C# Beyond Basics
course content

Course Content

C# Beyond Basics

C# Beyond Basics

1. Additional Structures & File Handling
2. Structs & Enumerators
3. Introduction to Object-Oriented Programming (OOP)
4. OOP Essentials
5. OOP Principles

Inheritance

We looked at the concept of derived classes in the last section. This feature of a class to inherit properties from another class is called Inheritance.

Although we already know the concept of Inheritance, we will go through it in a slightly more comprehensive manner this time to understand it more thoroughly.

As a quick revision, following is an example of Inheritance:

cs

index

copy
1234567891011121314151617181920212223242526272829303132333435363738394041424344
#pragma warning disable CS0169 // To disable some unnecessary compiler warnings for this example. Using this is not a recommended practice. using System; class Mammal { int age; float weight; // kilogram (1 kg = 2.2 pounds) } class Dog : Mammal { string breed; public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { Cat myCat = new Cat(); Dog myDog = new Dog(); myCat.speak(); myDog.speak(); } }

The code above contains one parent class called Mammal and two derived classes called Cat and Dog.

Note that none of the classes have any constructor explicitly defined, which means that these classes will use a default constructor when an object is created.

Lets create constructor for the Mammal class manually, which initializes a Mammal object with some data:

cs

index

copy
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
#pragma warning disable CS0169 // To disable some unnecessary compiler warnings for this example. Using this is not a recommended practice. using System; class Mammal { int age; float weight; // kg public Mammal(int age, float weight) { this.age = age; this.weight = weight; } } class Dog : Mammal { string breed; public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object with some data Mammal m1 = new Mammal(10, 42.0f); } }

If we try to compile this program, it will show some errors in the console. To understand those errors we first need to understand two important concepts related to constructors.

The first is that once we explicitly define a constructor for a class, that class no longer has a default constructor, and hence the explicitly defined constructor becomes the main constructor of that class which in this case is:

cs

index

copy
12345
public Mammal(int age, float weight) { this.age = age; this.weight = weight; }

Therefore, when creating a new object, we must always pass the required arguments of the constructor in the right order:

cs

index

copy
1234567
// Incorrect ways to create 'Mammal', will show an error Mammal m1 = new Mammal(); Mammal m1 = new Mammal(10); Mammal m1 = new Mammal(42.0f); // Correct way to create 'Mammal', will execute fine. Mammal m1 = new Mammal(10, 42.0f);

Secondly, derived classes can have constructors as well, however before the constructor of a derived class is called, the constructor of the base (parent) is called as well:

cs

index

copy
1234567891011121314151617181920212223242526272829303132333435
#pragma warning disable CS0169 // To disable some unnecessary warnings, using this is not a recommended practice. using System; class Mammal { int age; float weight; // kg // No attribute is initialized explicitly in this constructor // Hence, all attributes will take up "zero" values // It is similar to a "default" constructor except it outputs a message public Mammal() { Console.WriteLine("Mammal Constructor Called"); } } class Dog : Mammal { string breed; public Dog() { Console.WriteLine("Dog Constructor Called"); } } class ConsoleApp { static void Main() { Dog myDog = new Dog(); } }

When we run this code we see that the WriteLine() method from the 'Mammal' constructor, which is the parent class, is automatically called. Which means that it is a rule that the base class's constructor (also called the base constructor) is always called before the derived class's constructor.

This rule is also true in case of multilevel inheritance:

In the above diagram, the Kitten constructor calls Cat constructor before its own, however since Cat is also a derived class, it calls Mammal constructor before itself, and Mammal calls Animal constructor before its constructor, therefore overall the first constructor which is executed is the constructor of the super class - which is the Animal class's constructor and then it goes down from there.

If the parent class's constructor doesn't take any argument, it is automatically called by the compiler automatically, this is the reason why the 'Mammal' constructor in the above example was called automatically. However let's take a look at the flawed code again:

cs

index

copy
12345678910111213141516171819202122232425262728293031323334353637383940414243
using System; class Mammal { int age; float weight; // kg public Mammal(int age, float weight) { this.age = age; this.weight = weight; } } class Dog : Mammal { string breed; public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object with some data Mammal m1 = new Mammal(10, 42.0f); } }

In the above code we get two errors which basically mean that we have not manually called the base constructors - since it requires some arguments, we need to manually call it. The basic syntax of manually calling the parent class's constructor is the following:

cs

index

copy
12345678910
class DerivedClassName : ParentClassName { // ... attributes // ... methods public DerivedClassName(int arg1, int arg2, ...) : base(arg1, arg2, ...) { // code here } }

Example:

cs

index

copy
1234567891011121314151617181920212223242526272829303132
using System; class ExampleParentClass { int value1; int value2; public ExampleParentClass(int value1, int value2) { this.value1 = value1; } } class ExampleDerivedClass : ExampleParentClass { int value3; // The value1 and value2 arguments are passed to the base class's contructor public ExampleDerivedClass(int value1, int value2, int value3) : base (value1, value2) { this.value3 = value3; } } class ConsoleApp { static void Main() { var testObject = new ExampleDerivedClass(5, 7, 9); } }

Using this syntax, we can pass all the required data to the Mammal constructor through the Cat and Dog constructors to fix the error we were getting before:

cs

index

copy
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
using System; class Mammal { int age; float weight; // kg public Mammal(int age, float weight) { this.age = age; this.weight = weight; } } class Dog : Mammal { string breed; public Dog(int age, float weight, string breed) : base(age, weight) { this.breed = breed; } public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public Cat(int age, float weight, string furPattern) : base(age, weight) { this.furPattern = furPattern; } public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object with some data Mammal m1 = new Mammal(10, 42.0f); // Creating a "Dog" object with some data Dog d1 = new Dog(10, 42.5f, "Dobermann"); Console.WriteLine("Executed Successfully"); } }

Another important feature of constructors is that we can overload constructors just like how we overload any other method. We can create a multiple constructors with varying number of arguments:

cs

index

copy
12345678910111213141516171819202122232425
class Mammal { int age; float weight; // kg // 1st constructor public Mammal() { // We leave it empty for this example // Since it's empty, it mimics the "default" constructor } // 2nd constructor public Mammal(int age) { this.age = age; } // 3rd constructor public Mammal(int age, float weight) { this.age = age; this.weight = weight; } }

In this case the Mammal class has 3 constructors. So we can initialize or create a mammal object in 3 different ways and the compiler will choose which constructor to call based on the number and type of arguments:

cs

index

copy
1234
// All Correct var m1 = new Mammal(); var m2 = new Mammal(10); var m3 = new Mammal(10, 42.5f);

This also means we can call any of the 3 constructors from the derived class' constructors. For-example, all of these are valid:

cs

index

copy
123456789101112131415161718
// Using 3rd base constructor public Dog(int age, float weight, string breed) : base(age, weight) { this.breed = breed; } // Using 2nd base constructor public Dog(int age, string breed) : base(age) { this.breed = breed; } // Using 1st base constructor // If the base constructor has no arguments then it is automatically called (similar to the default constructor), so we don't necessarily need to write 'base()' public Dog(string breed) { this.breed = breed; }

Lets piece together the above two snippets and add some Console.WriteLine statements to see in which order are the constructors executed to practically see the results:

cs

index

copy
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
using System; class Mammal { int age; float weight; // kg // 1st Constructor public Mammal() { // We leave it empty for this example // Since it's empty, it mimics the "default" constructor // The attributes are initialized with zero values Console.WriteLine("Mammal - Constructor 1 Called"); } // 2nd Constructor public Mammal(int age) { this.age = age; Console.WriteLine("Mammal - Constructor 2 Called"); } // 3rd Constructor public Mammal(int age, float weight) { this.age = age; this.weight = weight; Console.WriteLine("Mammal - Constructor 3 Called"); } } class Dog : Mammal { string breed; public Dog() { Console.WriteLine("Dog - Constructor 1 Called"); } // Using 1st Mammal constructor // We don't necessarily need to write 'base()' in this case // It automatically finds and calls the constructor with no arguments public Dog(string breed) { this.breed = breed; Console.WriteLine("Dog - Constructor 2 Called"); } // Using 2nd Mammal constructor public Dog(int age, string breed) : base(age) { this.breed = breed; Console.WriteLine("Dog - Constructor 3 Called"); } // Using 3rd Mammal constructor public Dog(int age, float weight, string breed) : base(age, weight) { this.breed = breed; Console.WriteLine("Dog - Constructor 4 Called"); } public void speak() { Console.WriteLine("Woof!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object using different constructors Mammal m1 = new Mammal(10, 42.0f); Mammal m2 = new Mammal(10); Mammal m3 = new Mammal(); Console.WriteLine("----------"); // Seperator, for ease of reading output // Creating a "Dog" object using different constructors Dog d1 = new Dog(10, 42.0f, "Dobermann"); Console.WriteLine(""); Dog d2 = new Dog(10, "Dobermann"); Console.WriteLine(""); Dog d3 = new Dog("Dobermann"); Console.WriteLine(""); Dog d4 = new Dog(); } }

Now that you know about different features of inheritance, you should also know how or when to use them correctly. Following are some things to keep in mind when considering an inheritance based class structure:

Balance Between Simplicity and Flexibility: Constructor Overloading allows you to have many different constructors that take in different types of arguments but overdoing it can make the code more complicated and hard to maintain. It is a best practice to keep the class code short, concise and convenient. Avoid making too many constructors for a class to keep a balance between simplicity and flexibility.

Keep Constructors Simple: Constructors should mainly be responsible for initilizing an object with basic data. It is a best practice to avoid unnecessary processing and complex logic inside a constructor. If some calculation or logic is needed, it is better to create a separate method for it.

Bad Practice:

cs

index

copy
123456789101112131415161718192021222324252627
class Customer { string name; string accountType; double balance; public Customer (string name, string accountType, double balance) { this.name = name; this.accountType = accountType; if (accountType == "Savings") { // Plus 1 Percent this.balance = balance + balance * 0.01; } else if (accountType == "HighYieldSavings") { // Plus 5 percent this.balance = balance + balance * 0.05; } else { this.balance = balance; } } }

Good Practice:

cs

index

copy
123456789101112131415161718192021222324252627282930
class Customer { string name; string accountType; double balance; public Customer (string name, string accountType, double balance) { this.name = name; this.accountType = accountType; this.balance = balance; monthlyInterest(); } // This method might be used in other places too private void monthlyInterest() { if(accountType == "Savings") { // Plus 1 Percent balance += balance * 0.01; } else if(accountType == "HighYieldSavings") { // Plus 5 percent balance += balance * 0.05; } } }

Initialize Important Attributes: It is necessary to initialize all the important attributes of an object with correct values to make sure they function correctly - even if it's a constructor with no arguments.

Bad Practice:

cs

index

copy
123456789101112131415
public class Car { private string brand; private string model; private int year; private double price; // Constructor does not initialize important attributes // It is also generally not a good idea to have constructors without any arguments if they're not needed. public Car() { // No initialization of attributes Console.WriteLine("Car Created"); } }

Good Practice:

cs

index

copy
123456789101112131415161718192021222324252627282930313233343536373839
public class Car { private string brand; private string model; private int year; private double price; // Good: Constructor initializes important attributes // It also checks if the values are correct // In this case the if-else statements are not unnecessary since they are important for ensuring that the object functions correctly. public Car(string brand, string model, int year, double price) { this.brand = brand; this.model = model; // Validate and set the year // The first public car was created in 1886 :) if (year > 1886) { this.year = year; } else { Console.WriteLine("Invalid year. Setting year to default."); this.year = DateTime.Now.Year; // Set to current year as default } // Validate and set the price if (price >= 0) { this.price = price; } else { Console.WriteLine("Invalid price. Setting price to default."); this.price = 0; // Set to a default value } } }
1. What feature allows us to create multiple constructors for a class?
2. You might need to use one of the concepts from the previous sections in this quiz. The code below has an error at lines 15 and 16. Look at the code thoroughly and decide what is an efficient fix for this error:

What feature allows us to create multiple constructors for a class?

Select the correct answer

You might need to use one of the concepts from the previous sections in this quiz. The code below has an error at lines 15 and 16. Look at the code thoroughly and decide what is an efficient fix for this error:

Select the correct answer

Everything was clear?

Section 5. Chapter 2
We're sorry to hear that something went wrong. What happened?
some-alt