Зміст курсу
C# Beyond Basics
C# Beyond Basics
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:
index
#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:
index
#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:
index
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:
index
// 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:
index
#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:
index
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:
index
class DerivedClassName : ParentClassName { // ... attributes // ... methods public DerivedClassName(int arg1, int arg2, ...) : base(arg1, arg2, ...) { // code here } }
Example:
index
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:
index
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:
index
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:
index
// 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:
index
// 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:
index
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:
index
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:
index
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:
index
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:
index
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 } } }
Дякуємо за ваш відгук!