Contenu du cours
C# au-delà des Bases
C# au-delà des Bases
Héritage
Nous avons examiné le concept de classes dérivées dans la dernière section. Cette fonctionnalité d'une classe pour hériter des propriétés d'une autre classe est appelée Héritage.
Bien que nous connaissions déjà le concept d'Héritage, nous allons l'aborder de manière un peu plus complète cette fois-ci pour le comprendre plus en profondeur.
Pour une révision rapide, voici un exemple d'Héritage :
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(); } }
Le code ci-dessus contient une classe parente appelée Mammal
et deux classes dérivées appelées Cat
et Dog
.
Notez qu'aucune des classes n'a de constructeur explicitement défini, ce qui signifie que ces classes utiliseront un constructeur par défaut lorsqu'un objet est créé.
Créons un constructeur pour la classe Mammal
manuellement, qui initialise un objet Mammal
avec certaines données :
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); } }
Si nous essayons de compiler ce programme, il affichera des erreurs dans la console. Pour comprendre ces erreurs, nous devons d'abord comprendre deux concepts importants liés aux constructeurs.
Le premier est qu'une fois que nous définissons explicitement un constructeur pour une classe, cette classe n'a plus de constructeur par défaut, et donc le constructeur défini explicitement devient le constructeur principal de cette classe qui, dans ce cas, est :
index
public Mammal(int age, float weight) { this.age = age; this.weight = weight; }
Par conséquent, lors de la création d'un nouvel objet, nous devons toujours passer les arguments requis du constructeur dans le bon ordre :
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);
Deuxièmement, les classes dérivées peuvent également avoir des constructeurs, cependant avant que le constructeur d'une classe dérivée ne soit appelé, le constructeur de la base (parent) est également appelé :
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(); } }
Lorsque nous exécutons ce code, nous voyons que la méthode WriteLine()
du constructeur 'Mammal', qui est la classe parente, est automatiquement appelée. Ce qui signifie qu'il est une règle que le constructeur de la classe de base (également appelé le constructeur de base) est toujours appelé avant le constructeur de la classe dérivée.
Cette règle est également vraie dans le cas de l'héritage multiniveau :
Dans le diagramme ci-dessus, le constructeur Kitten
appelle le constructeur Cat
avant le sien, cependant, comme Cat
est également une classe dérivée, il appelle le constructeur Mammal
avant lui-même, et Mammal
appelle le constructeur Animal
avant son constructeur, donc globalement le premier constructeur qui est exécuté est le constructeur de la super classe - qui est le constructeur de la classe Animal
et ensuite cela descend de là.
Si le constructeur de la classe parente ne prend aucun argument, il est automatiquement appelé par le compilateur automatiquement, c'est la raison pour laquelle le constructeur 'Mammal' dans l'exemple ci-dessus a été appelé automatiquement. Cependant, examinons à nouveau le code défectueux :
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); } }
Dans le code ci-dessus, nous obtenons deux erreurs qui signifient essentiellement que nous n'avons pas appelé manuellement les constructeurs de base - puisqu'il nécessite certains arguments, nous devons l'appeler manuellement. La syntaxe de base pour appeler manuellement le constructeur de la classe parente est la suivante :
index
class DerivedClassName : ParentClassName { // ... attributes // ... methods public DerivedClassName(int arg1, int arg2, ...) : base(arg1, arg2, ...) { // code here } }
Exemple :
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); } }
En utilisant cette syntaxe, nous pouvons passer toutes les données requises au constructeur Mammal
à travers les constructeurs Cat
et Dog
pour corriger l'erreur que nous avions auparavant :
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"); } }
Une autre caractéristique importante des constructeurs est que nous pouvons surcharger les constructeurs tout comme nous surchargeons toute autre méthode. Nous pouvons créer plusieurs constructeurs avec un nombre d'arguments variable :
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; } }
Dans ce cas, la classe Mammal
a 3 constructeurs. Nous pouvons donc initialiser ou créer un objet mammifère de 3 manières différentes et le compilateur choisira quel constructeur appeler en fonction du nombre et du type d'arguments :
index
// All Correct var m1 = new Mammal(); var m2 = new Mammal(10); var m3 = new Mammal(10, 42.5f);
Cela signifie également que nous pouvons appeler n'importe lequel des 3 constructeurs à partir des constructeurs de la classe dérivée. Par exemple, tous ceux-ci sont valides :
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; }
Assemblons les deux extraits ci-dessus et ajoutons quelques instructions Console.WriteLine
pour voir dans quel ordre les constructeurs sont exécutés afin de voir pratiquement les résultats :
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(); } }
Maintenant que vous connaissez les différentes fonctionnalités de l'héritage, vous devez également savoir comment ou quand les utiliser correctement. Voici quelques éléments à garder à l'esprit lors de la considération d'une structure de classe basée sur l'héritage :
Équilibre entre simplicité et flexibilité : La surcharge de constructeur vous permet d'avoir de nombreux constructeurs différents qui prennent différents types d'arguments, mais en abuser peut rendre le code plus compliqué et difficile à maintenir. Il est préférable de garder le code de la classe court, concis et pratique. Évitez de créer trop de constructeurs pour une classe afin de maintenir un équilibre entre simplicité et flexibilité.
Gardez les constructeurs simples : Les constructeurs doivent principalement être responsables de l'initialisation d'un objet avec des données de base. Il est préférable d'éviter les traitements inutiles et la logique complexe à l'intérieur d'un constructeur. Si un calcul ou une logique est nécessaire, il est préférable de créer une méthode séparée pour cela.
Mauvaise pratique :
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; } } }
Bonne Pratique:
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; } } }
Initialiser les attributs importants : Il est nécessaire d'initialiser tous les attributs importants d'un objet avec des valeurs correctes pour s'assurer qu'ils fonctionnent correctement - même s'il s'agit d'un constructeur sans arguments.
Mauvaise pratique :
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"); } }
Bonne pratique :
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 } } }
1. Quelle fonctionnalité nous permet de créer plusieurs constructeurs pour une classe ?
2. Vous pourriez avoir besoin d'utiliser l'un des concepts des sections précédentes dans ce quiz. Le code ci-dessous a une erreur aux lignes 15 et 16. Regardez attentivement le code et décidez quelle est une solution efficace pour cette erreur :
Merci pour vos commentaires !