• 20 hours
  • Medium

Free online content available in this course.

Certificate of achievement available at the end this course

Got it!

Last updated on 3/10/17

La POO et le C#

Log in or subscribe for free to enjoy all this course has to offer!

Dans ce chapitre, vous allez vous immerger un peu plus dans les subtilités de la POO en utilisant le C#. Il est temps un peu de tourmenter nos objets et de voir ce qu'ils ont dans le ventre. Ainsi, nous allons voir comment les objets héritent les uns des autres ou comment fonctionnent les différents polymorphismes.

Nous allons également voir comment tous ces concepts se retrouvent dans le quotidien d'un développeur C#.

Des types, des objets, type valeur et type référence

Ok, je sais maintenant créer des objets, mais je me rappelle que dans le cours précédent, nous avons manipulé des int et des string et que tu as appelé ça des « types » ; et après, tu nous dis que tout est objet … Tu serais pas en train de raconter n’importe quoi ?

Eh bien non, ô perspicace lecteur !

Précisons un peu maintenant que vous avez de meilleures connaissances. J’ai bien dit que tout était objet, je le maintiens, même sous la torture. :-° C'est-à-dire que même les types simples comme les entiers int ou les chaînes de caractères sont des objets.

J’en veux pour preuve ce simple exemple :

int a = 10;
string chaine = a.ToString();
chaine = "abc" + chaine;
string chaineEnMajuscule = chaine.ToUpper();
Console.WriteLine(chaineEnMajuscule);
Console.WriteLine(chaineEnMajuscule.Length);

La variable a est un entier. Nous appelons la méthode ToString() sur cet entier. Même si nous n’avons pas encore vu à quoi elle servait, nous pouvons supposer qu’elle effectue une action qui consiste à transformer l’entier en chaîne de caractères.
Nous concaténons ensuite la chaîne abc à cette chaîne et nous effectuons une action qui, à travers la méthode ToUpper(), met la chaine en majuscule.
Enfin, la méthode Console.WriteLine nous affiche « ABC10 » puis nous affiche la propriété Length de la chaîne de caractères qui correspond bien sûr à sa taille.

Pour créer une chaîne de caractères, nous utilisons le mot-clé string. Sachez que ce mot-clé est équivalent à la classe String (notez la différence de casse).
En créant une chaîne de caractères, nous avons instancié un objet défini par la classe String.

Mais alors, pourquoi utiliser string et non pas String ?

En fait, le mot-clé string est ce qu’on appelle un alias de la classe String qui se situe dans l’espace de nom System. De même, le mot-clé int est un alias de la structure Int32 qui se situe également dans l’espace de nom System (nous verrons un peu plus loin ce qu'est vraiment une structure).

Ce qui fait que les instructions suivantes :

int a = 10;
string chaine = "abc";

sont équivalentes à celles-ci :

System.Int32 a = 10;
System.String chaine = "abc";

Cependant, les entiers, les booléens et autres types « simples » sont ce qu’on appelle des types intégrés. Et même si ce sont des objets à part entière (méthodes, propriétés,… ), ils ont des particularités, notamment dans la façon dont ils sont gérés par le framework .NET.
On les appelle des types valeur, car les variables de ce type possèdent la vraie valeur de ce qu’on leur affecte a contrario des classes qui sont des types référence dont les variables possèdent simplement un lien vers un objet en mémoire.

Par exemple :

int entier = 5;

Ici, la variable contient vraiment l’entier 5. Alors que pour l’instanciation suivante :

Voiture voitureNicolas = new Voiture();

La variable voitureNicolas contient une référence vers l’objet en mémoire.

On peut imaginer que le type référence est un peu comme si on disait que ma maison se situe « 9 rue des bois ». L’adresse a été écrite sur un bout de papier et référence ma maison qui ne se situe bien sûr pas au même endroit que le bout de papier. Si je veux vraiment voir l’objet maison, il va falloir que j’aille voir où c’est indiqué sur le bout de papier. C’est ce que fait le type référence, il va voir en mémoire ce qu’il y a vraiment dans l’objet.

Alors que le type valeur pourrait ressembler à un billet de banque par exemple. Je peux me balader avec, c’est marqué 500€ dessus (oui, je suis riche !) et je peux payer directement avec sans que le fait de donner le billet implique d’aller chercher le contenu à la banque.

  • Le type valeur contient la vraie valeur qui en général est assez petite et facile à stocker.

  • Le type référence ne contient qu’un lien vers un plus gros objet stocké ailleurs.

Cette manière différente de gérer les types et les objets implique plusieurs choses.
Dans la mesure où les types valeur possèdent vraiment la valeur de ce qu’on y stocke, une copie de la valeur est effectuée à chaque fois que l’on fait une affectation. Cela est possible car ces types sont relativement petits et optimisés. Cela s’avère impossible pour un objet qui est trop gros et trop complexe.
C’est un peu compliqué de copier toute ma maison alors que c’est un peu plus simple de recopier ce qu’il y a sur le bout de papier :) .

Ainsi, l’exemple suivant :

int a = 5;
int b = a;
b = 6;
Console.WriteLine(a);
Console.WriteLine(b);

affichera les valeurs 5 puis 6. Ce qui est le résultat que l’on attend.

  • En effet, la variable « a » a été initialisée à 5.

  • On a ensuite affecté « a » à « b ». La valeur 5 s’est copiée (dupliquée) dans la variable « b ».

  • Puis nous avons affecté 6 à « b ».

Ce qui parait tout à fait logique.

Par contre, l’exemple suivant :

Voiture voitureNicolas = new Voiture();
voitureNicolas.Couleur = "Bleue";
Voiture voitureJeremie = voitureNicolas;
voitureJeremie.Couleur = "Verte";
Console.WriteLine(voitureNicolas.Couleur);
Console.WriteLine(voitureJeremie.Couleur);

affichera verte et verte.

Quoi ? Nous indiquons que la voiture de Nicolas est bleue. Puis nous disons que celle de Jérémie est verte et quand on demande d’afficher la couleur des deux voitures, on nous dit qu’elles sont vertes toutes les deux alors qu’on croyait que celle de Nicolas était bleue ?
Tout à l’heure, le fait de changer b n’avait pas changé la valeur de a …

Eh oui, ceci illustre le fait que les classes (comme Voiture) sont des types référence et ne possèdent qu’une référence vers une instance de Voiture. Quand nous affectons voitureNicolas à voitureJeremie, nous disons en fait que la voiture de Jérémie référence la même chose que celle de Nicolas.
Concrètement, le C# copie la référence de l'objet Voiture qui est contenue dans la variable voitureNicolas dans la variable voitureJeremie. Ce sont donc deux variables différentes qui possèdent tous les deux une référence vers l'objet Voiture, qui est la voiture de Nicolas.
C'est-à-dire que les deux variables référencent le même objet. Ainsi, la modification des propriétés de l’un affectera forcément l’autre.

Inattendu au premier abord, mais finalement, c’est très logique.
Comprendre cette différence entre les types valeur et les types référence est important, nous verrons dans les chapitres suivants quels sont les autres impacts de cette différence.
À noter également qu’il est impossible de dériver d’un type intégré alors que c’est possible de dériver facilement d’une classe.

D’ailleurs, si nous parlions un peu d’héritage ?

Héritage

Nous avons vu pour l’instant la théorie de l’héritage. Que les objets chiens héritaient des comportements des objets Animaux, que les labradors héritaient des comportements des chiens, etc.

Passons maintenant à la pratique et créons une classe Animal et une classe Chien qui en hérite. Nous allons créer des classes relativement courtes et nous nous limiterons dans le nombre d’actions ou de propriétés de celles-ci. Par exemple, nous pourrions imaginer que la classe Animal possède une propriété NombreDePattes qui est un entier et une méthode Respirer qui affiche le détail de l’action. Ce qui donne :

public class Animal
{
    public int NombreDePattes { get; set; }

    public void Respirer()
    {
        Console.WriteLine("Je respire");
    }
}

La classe Chien dérive de la classe Animal et peut donc hériter de certains de ses comportements. En l’occurrence, la classe Chien héritera de tout ce qui est public ou protégé, identifiés comme vous le savez désormais par les mots clés public et protected.
Le chien sait également faire quelque chose qui lui est propre, à savoir aboyer. Il possèdera donc une méthode supplémentaire. Ce qui donne :

public class Chien : Animal
{
    public void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

On représente la notion d’héritage en ajoutant après la classe le caractère « : » suivi de la classe mère.
Ici, nous avons défini une classe publique Chien qui hérite de la classe Animal.

Nous pouvons dès à présent créer des objets Animal et des objets Chien, par exemple :

Animal animal = new Animal { NombreDePattes = 4 };
animal.Respirer();
Console.WriteLine();

Chien chien = new Chien { NombreDePattes = 4 };
chien.Respirer();
chien.Aboyer();

Si nous exécutons ce code, nous aurons :

Image utilisateur

Nous nous rendons bien compte que l’objet Chien, bien que n’ayant pas défini la propriété NombreDePattes ou la méthode Respirer() dans le corps de sa classe, est capable d’avoir des pattes et de faire l’action respirer.

Il a hérité ces comportements de l’objet Animal, en tous cas, ceux qui sont publiques.
Rajoutons deux variables membres de la classe Animal :

public class Animal
{
    private bool estVivant;
    public int age;

    public int NombreDePattes { get; set; }

    public void Respirer()
    {
        Console.WriteLine("Je respire");
    }
}

L’entier age est public alors que le booléen estVivant est privé. Si nous tentons de les utiliser depuis la classe fille Chien, comme ci-dessous :

public class Chien : Animal
{
    public void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }

    public void Vieillir()
    {
        age++;
    }

    public void Naissance()
    {
        age = 0;
        estVivant = true;   // Erreur	> 'MaPremiereApplication.Animal.estVivant' 
                            // est inaccessible en raison de son niveau de protection
    }
}

Nous voyons qu’il est tout à fait possible d’utiliser la variable age depuis la méthode Vieillir() alors que l’utilisation du booléen estVivant provoque une erreur de compilation.
Vous avez bien compris que celui-ci était inaccessible car il est défini comme membre privé. Pour l’utiliser, on pourra le rendre public par exemple.

Il existe par contre un autre mot clé qui permet de rendre des variables/propriétés/méthodes inaccessibles depuis un autre objet tout en le rendant accessible depuis des classes filles. Il s’agit du mot clé protected.
Si nous l’utilisons à la place de private pour définir la visibilité du booléen estVivant, nous pourrons nous rendre compte que la classe Chien peut désormais compiler :

public class Animal
{
    protected bool estVivant;
    [… Extrait de code supprimé …]
}

public class Chien : Animal
{
    [… Extrait de code supprimé …]

    public void Naissance()
    {
        age = 0;
        estVivant = true;   // compilation OK
    }
}

Par contre, cette variable est toujours inaccessible depuis d’autres classes, comme l’est également une variable privée. Dans notre classe Program, l’instruction suivante :

chien.estVivant = true;

provoquera l’erreur de compilation que nous connaissons désormais bien :

'MaPremiereApplication.Animal.estVivant' est inaccessible en raison de son niveau de protection

Le mot clé protected prend tout son intérêt dès que nous avons à faire avec l’héritage. Nous verrons un peu plus bas d’autres exemples de ce mot clé.

Nous avons dit dans l’introduction qu’un objet B qui dérive de l’objet A est « une sorte » d’objet A. Dans notre exemple du dessus, le Chien est une sorte d’Animal.
Cela veut dire que nous pouvons utiliser un chien en tant qu’animal. Par exemple, le code suivant :

Animal animal = new Chien { NombreDePattes = 4 };

est tout à fait correct. Nous disons que notre variable animal, de type Animal est une instance de Chien.

Avec cette façon d’écrire, nous avons réellement instancié un objet Chien mais celui-ci sera traité en tant qu’Animal. Cela veut dire qu’il sera capable de Respirer() et d’avoir des pattes. Par contre, même si en vrai, notre objet serait capable d’aboyer, le fait qu’il soit manipulé en tant qu’Animal nous empêche de pouvoir le faire Aboyer.
Cela veut dire que le code suivant :

Animal animal = new Chien { NombreDePattes = 4 };
animal.Respirer();
animal.Aboyer(); // erreur de compilation

provoquera une erreur de compilation pour indiquer que la classe Animal ne contient aucune définition pour la méthode Aboyer(). Ce qui est normal, un animal ne sait pas forcément aboyer...

Quel est l’intérêt alors d’utiliser le chien en tant qu’animal ?

Bonne question.

Pour y répondre, nous allons enrichir notre classe Animal, garder notre classe Chien et créer une classe Chat qui hérite également d’Animal. Ce pourrait être :

public class Animal
{
    protected string prenom;

    public void Respirer()
    {
        Console.WriteLine("Je suis " + prenom + " et je respire");
    }
}

public class Chien : Animal
{
    public Chien(string prenomDuChien)
    {
        prenom = prenomDuChien;
    }

    public void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

public class Chat : Animal
{
    public Chat(string prenomDuChat)
    {
        prenom = prenomDuChat;
    }

    public void Miauler()
    {
        Console.WriteLine("Miaou");
    }
}

Nous forçons les chiens et les chats à avoir un nom, hérité de la classe Animal, grâce au constructeur afin de pouvoir les identifier facilement.
Le chat garde le même principe que le chien, sauf que nous avons une méthode Miauler() à la place de la méthode Aboyer() … Ce qui est somme toute logique.
L’idée est de pouvoir utiliser nos chiens et nos chats ensemble comme des animaux, par exemple en utilisant une liste.

Pour illustrer ce fonctionnement, donnons vie à quelques chiens et à quelques chats grâce à nos pouvoirs de développeur et mettons-les dans une liste :

List<Animal> animaux = new List<Animal>();
Animal milou = new Chien("Milou");
Animal dingo = new Chien("Dingo");
Animal idefix = new Chien("Idéfix");
Animal tom = new Chat("Tom");
Animal felix = new Chat("Félix");

animaux.Add(milou);
animaux.Add(dingo);
animaux.Add(idefix);
animaux.Add(tom);
animaux.Add(felix);

Nous avons dans un premier temps instancié une liste d’animaux à laquelle nous avons rajouté 3 chiens et 2 chats, chacun étant considéré comme un animal puisqu’ils sont tous des sortes d’animaux, grâce à l’héritage.
Maintenant, nous n’avons plus que des animaux dans la liste.
Il sera donc possible de les faire tous respirer en une simple boucle :

foreach (Animal animal in animaux)
{
    animal.Respirer();
}

ce qui donnera :

Image utilisateur

Et voilà, c’est super simple.
Imaginez le bonheur de Noé sur son arche quand il a compris que grâce à la POO, il pouvait faire respirer tous les animaux en une seule boucle ! Quel travail économisé. :p
Peu importe ce qu’il y a dans la liste, des chiens, des chats, des hamsters, nous savons que ce sont tous des animaux et qu’ils savent tous respirer.

Vous avez sans doute remarqué que nous faisons la même chose dans le constructeur de la classe Chien et dans celui de la classe Chat. Deux fois la même chose… Ce n’est pas terrible. Peut-être y a-t-il un moyen de factoriser tout ça ?
Effectivement, il est possible également d’écrire nos classes de cette façon :

public class Animal
{
    protected string prenom;

    public Animal(string prenomAnimal)
    {
        prenom = prenomAnimal;
    }

    public void Respirer()
    {
        Console.WriteLine("Je suis " + prenom + " et je respire");
    }
}

public class Chien : Animal
{
    public Chien(string prenomDuChien) : base(prenomDuChien)
    {
    }

    public void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

public class Chat : Animal
{
    public Chat(string prenomDuChat) : base(prenomDuChat)
    {
    }

    public void Miauler()
    {
        Console.WriteLine("Miaou");
    }
}

Qu’est-ce qui change ?

Eh bien la classe Animal possède un constructeur qui prend en paramètre un prénom et qui le stocke dans sa variable privée. C’est elle qui fait le travail d’initialisation.
Il devient alors possible pour les constructeurs des classes filles d’appeler le constructeur de la classe mère afin de faire l’affectation du prénom. Pour cela, on utilise les deux points suivis du mot clé base qui signifie « appelle-moi le constructeur de la classe du dessus » auquel nous passons la variable en paramètres.
Avec cette écriture un peu barbare, il devient possible de factoriser des initialisations qui ont un sens pour toutes les classes filles. Dans notre cas, je veux que tous les objets qui dérivent d’Animal puissent facilement définir un prénom.

Il faut aussi savoir que si nous appelons le constructeur par défaut d’une classe qui n'appelle pas explicitement un constructeur spécialisé d'une classe mère, alors celui-ci appellera automatiquement le constructeur par défaut de la classe dont il hérite. Modifions à nouveau nos classes pour avoir :

public class Animal
{
    protected string prenom;

    public Animal()
    {
        prenom = "Marcel";
    }

    public void Respirer()
    {
        Console.WriteLine("Je suis " + prenom + " et je respire");
    }
}

public class Chien : Animal
{
    public void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

public class Chat : Animal
{
    public Chat(string prenomDuChat)
    {
        prenom = prenomDuChat;
    }

    public void Miauler()
    {
        Console.WriteLine("Miaou");
    }
}

Ici, la classe Animal met un prénom par défaut dans son constructeur. Le chien n’a pas de constructeur et le chat en a un qui accepte un paramètre.

Il est donc possible de créer un Chien sans qu’il ait de prénom mais il est obligatoire d’en définir un pour le chat. Sauf que lorsque nous instancierons notre objet chien, il appellera automatiquement le constructeur de la classe mère et tous nos chiens s’appelleront Marcel :

static void Main(string[] args)
{
    List<Animal> animaux = new List<Animal>();
    Animal chien = new Chien();
    Animal tom = new Chat("Tom");
    Animal felix = new Chat("Félix");

    animaux.Add(chien);
    animaux.Add(tom);
    animaux.Add(felix);

    foreach (Animal animal in animaux)
    {
        animal.Respirer();
    }
}

Ce qui donne :

Image utilisateur

Il est également possible d’appeler un constructeur à partir d’un autre constructeur.
Prenons l’exemple suivant :

public class Voiture
{
    private int vitesse;

    public Voiture(int vitesseVoiture)
    {
        vitesse = vitesseVoiture;
    }
}

Si nous souhaitons rajouter un constructeur par défaut qui initialise la vitesse à 10 par exemple, nous pourrons faire :

public class Voiture
{
    private int vitesse;

    public Voiture()
    {
        vitesse = 10;
    }

    public Voiture(int vitesseVoiture)
    {
        vitesse = vitesseVoiture;
    }
}

Ou encore :

public class Voiture
{
    private int vitesse;

    public Voiture() : this(10)
    {
    }

    public Voiture(int vitesseVoiture)
    {
        vitesse = vitesseVoiture;
    }
}

Ici, l’utilisation du mot clé this, suivi d’un entier permet d’appeler le constructeur qui possède un paramètre entier au début du constructeur par défaut.
Inversement, nous pouvons appeler le constructeur par défaut d’une classe depuis un constructeur possédant des paramètres afin de pouvoir bénéficier des initialisations de celui-ci :

public class Voiture
{
    private int vitesse;
    private string couleur;

    public Voiture()
    {
        vitesse = 10;
    }

    public Voiture(string couleurVoiture) : this()
    {
        couleur = couleurVoiture;
    }
}

Puisque nous sommes à parler d’héritage, il faut savoir que tous les objets que nous créons ou qui sont disponibles dans le framework .NET héritent d’un objet de base. On parle en général d’un « super objet ». L’intérêt de dériver d’un tel objet est de permettre à tous les objets d’avoir certains comportements en commun, mais également de pouvoir éventuellement tous les traiter en tant qu’objet.
Notre super objet est représenté par la classe Object qui définit plusieurs méthodes. Vous les avez déjà vues si vous avez regardé dans la complétion automatique après avoir créé un objet.
Prenons une classe toute vide, par exemple :

public class ObjetVide
{ 
}

Si nous instancions cet objet et que nous souhaitons l’utiliser, nous verrons que la complétion automatique nous propose des méthodes que nous n’avons jamais créées :

Image utilisateur

Nous voyons plusieurs méthodes, comme la méthode Equals ou GetHashCode ou GetType ou encore ToString.
Comme vous l’avez compris, ce sont des méthodes qui sont définies dans la classe Object. La méthode ToString par exemple permet d’obtenir une représentation de l’objet sous la forme d’une chaine de caractères.
C’est une méthode qui va souvent nous servir, nous y reviendrons un peu plus tard.
Ce super-objet est du type Object, mais on utilise généralement son alias object.

Ainsi, il est possible d’utiliser tous nos objets comme des object et ainsi utiliser les méthodes qui sont définies sur la classe Object. Ce qui nous permet de faire :

static void Main(string[] args)
{
    ObjetVide monObjetVide = new ObjetVide();
    Chien chien = new Chien();
    int age = 30;
    string prenom = "Nicolas";

    AfficherRepresentation(monObjetVide);
    AfficherRepresentation(chien);
    AfficherRepresentation(age);
    AfficherRepresentation(prenom);
}

private static void AfficherRepresentation(object monObjetVide)
{
    Console.WriteLine(monObjetVide.ToString());
}

Ce qui donne :

Image utilisateur

Comme indiqué, la méthode ToString() permet d’afficher la représentation par défaut d’un objet. Vous aurez remarqué qu’il y a une différence suivant ce que nous passons. En effet, la représentation par défaut des types référence correspond au nom du type, à savoir son espace de nom suivi du nom de sa classe. Pour ce qui est des types valeur, il contient en général la valeur du type, à l’exception des structures que nous n’avons pas encore vues et que nous aborderons un peu plus loin.

L’intérêt dans cet exemple de code est de voir que nous pouvons manipuler tout comme un object. D’une manière générale, vous aurez peu l’occasion de traiter vos objets en tant qu’object car il est vraiment plus intéressant de profiter pleinement du type, l’object étant peu utilisable.

J’en profite maintenant que vous connaissez la méthode ToString() pour parler d’un point qui a peut-être titillé vos cerveaux.
Dans la première partie, nous avions fait quelque chose du genre :

int vitesse = 20;
string chaine = "La vitesse est " + vitesse + " km/h";

La variable vitesse est un entier. La chaîne La vitesse est est une chaine de caractères. Nous essayons d’ajouter un entier à une chaîne alors que j’ai dit qu’ils n’étaient pas compatibles entre eux ! Et pourtant cela fonctionne.
Effectivement, c’est bizarre. o_O Nous concaténons une chaîne à un entier avec l’opérateur + et nous concaténons encore une chaîne.

Et si je fais l’inverse :

int vitesse = 20 + "40";

cela provoque une erreur de compilation. C’est logique, on ne peut pas ajouter un entier et une chaîne de caractères. Alors pourquoi cela fonctionne dans l’autre sens ?
Ce qu’il se passe en fait dans l’instruction :

string chaine = "La vitesse est " + vitesse + " km/h";

c’est que le compilateur se rend compte que nous concaténons une chaîne avec un autre objet, peu importe que ce soit un entier ou un objet complexe. Alors pour que ça fonctionne, il demande une représentation de l’objet sous la forme d’une chaîne de caractères. Nous avons vu que ceci se faisait en appelant la méthode ToString() qui est héritée de l’objet racine Object.

L’instruction est donc équivalente à :

string chaine = "La vitesse est " + vitesse.ToString() + " km/h";

Dans le cas d’un type valeur comme un entier, la méthode ToString() renvoie la représentation interne de la valeur, à savoir "20". Dans le cas d’un objet complexe, elle aurait renvoyé le nom du type de l’objet.

Avant de terminer, il est important d’indiquer que le C# n’autorise pas l’héritage multiple.
Ainsi, si nous possédons une classe Carnivore et une classe EtreVivant, il ne sera pas possible de faire hériter directement un objet Homme de l’objet Carnivore et de l’objet EtreVivant.
Ainsi, le code suivant :

public class Carnivore
{
}
public class EtreVivant
{
}

public class Homme : Carnivore, EtreVivant
{
}

provoquera l’erreur de compilation suivante :

La classe 'MaPremiereApplication.Homme' ne peut pas avoir plusieurs classes de base : 'MaPremiereApplication.Carnivore' et 'EtreVivant'

Si par contre, cela est pertinent, nous pourrons faire un héritage en cascade afin que Carnivore dérive de EtreVivant et que Homme dérive de Carnivore :

public class Carnivore : EtreVivant
{
}
public class EtreVivant
{
}

public class Homme : Carnivore
{
}

Cependant, il n’est pas toujours pertinent d'opérer de la sorte. Notre Homme pourrait être à la fois Carnivore et Frugivore, cependant cela n’a pas de sens qu’un carnivore soit également frugivore, ou l’inverse.

Oui mais tu as dit que chaque objet dérivait du super objet Object, s’il dérive d’une autre classe comme un chien dérive d’un animal, ça fait bien deux classes dont il dérive…

Effectivement, mais dans ce cas-là, ce n’est pas pareil. Comme il est automatique de dériver de object, c’est comme si on avait le chien qui hérite de animal qui hérite lui-même de object. Le C# est assez malin pour ça. ^^

Substitution

Nous avons vu juste avant l’utilisation de la méthode ToString() qui permet d’obtenir la représentation d’un objet sous forme de chaine de caractères. En l’occurrence, vous conviendrez avec moi que la représentation de notre classe Chien n’est pas particulièrement exploitable. Le nom du type c’est bien, mais ce n’est pas très parlant.

Ça serait pas mal que quand nous demandons d’afficher un chien, nous obtenions le nom du chien, vous ne trouvez pas ?
C’est là qu’intervient la substitution.
Nous en avons parlé dans l’introduction à la POO, la substitution permet de redéfinir un comportement dont l’objet a hérité afin qu’il corresponde aux besoins de l’objet fils.
Typiquement, ici, la méthode ToString() du super-objet ne nous convient pas et dans le cas de notre chien, nous souhaitons la redéfinir, en écrire une nouvelle version.

Pour cet exemple, simplifions notre classe Chien afin qu’elle n’ait qu’une propriété pour stocker son prénom :

public class Chien
{
    public string Prenom { get; set; }
}

Pour redéfinir la méthode ToString() nous allons devoir utiliser le mot clé override qui signifie que nous souhaitons substituer la méthode existante afin de remplacer son comportement, ce que nous pourrons écrire en C# avec :

public class Chien
{
    public string Prenom { get; set; }

    public override string ToString()
    {
        return "Je suis un chien et je m'appelle " + Prenom;
    }
}

Le mot clé override se met avant le type de retour de la méthode, comme on peut le voir ci-dessus.
Si nous appelons désormais la méthode ToString de notre objet Chien :

Chien chien = new Chien { Prenom = "Max" };
Console.WriteLine(chien.ToString());

notre programme va utiliser la nouvelle version de la méthode ToString() et nous aurons :

Image utilisateur

Et voilà un bon moyen d’utiliser la substitution, la représentation de notre objet est quand même plus parlante.
Adaptons désormais cet exemple à nos classes.
Pour montrer comment faire, reprenons notre classe Chien qui possède une méthode Aboyer() :

public class Chien
{
    public void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

Nous pourrions imaginer créer une classe ChienMuet qui dérive de la classe Chien et qui hérite donc de ses comportements.
Mais, que penser d’un chien muet qui serait capable d’aboyer ? Cela n’a pas de sens !
Il faut donc redéfinir cette fichue méthode.

Utilisons alors le mot clé override comme nous l’avons vu pour obtenir :

public class ChienMuet : Chien
{
    public override void Aboyer()
    {
        Console.WriteLine("...");
    }
}

Créons un chien muet puis faisons-le aboyer, cela donne :

ChienMuet pauvreChien = new ChienMuet();
pauvreChien.Aboyer();

Sauf que nous rencontrons un problème. Si nous tentons de compiler ce code, Visual Studio Express nous génère une erreur de compilation :

'MaPremiereApplication.Program.ChienMuet.Aboyer()' : ne peut pas substituer le membre hérité 'MaPremiereApplication.Program.Chien.Aboyer()', car il n'est pas marqué comme virtual, abstract ou override.

En réalité, pour pouvoir créer une méthode qui remplace une autre, il faut qu’une condition supplémentaire soit vérifiée : il faut que la méthode à remplacer s’annonce comme candidate à la substitution. Cela veut dire que l’on ne peut pas substituer n’importe quelle méthode, mais seulement celles qui acceptent de l’être.
C’est le cas pour la méthode ToString que nous avons vue précédemment. Les concepteurs du framework .NET ont autorisé cette éventualité. Heureusement, sinon, nous serions bien embêtés :) .

Pour marquer notre méthode Aboyer de la classe Chien comme candidate éventuelle à la substitution, il faut la préfixer du mot clé virtual. Ainsi, elle annonce à ses futures filles que si elles le souhaitent, elles peuvent redéfinir cette méthode.

Cela se traduit ainsi dans le code :

public class Chien
{
    public virtual void Aboyer()
    {
        Console.WriteLine("Wouaf !");
    }
}

public class ChienMuet : Chien
{
    public override void Aboyer()
    {
        Console.WriteLine("...");
    }
}

Désormais, l’instanciation de l’objet est possible et nous pourrons avoir notre code :

ChienMuet pauvreChien = new ChienMuet();
pauvreChien.Aboyer();

qui affichera :

Image utilisateur

Parfait !

Tout est rentré dans l’ordre.

Le message d’erreur, quoique peu explicite, nous mettait quand même sur la bonne voie. Visual Studio Express nous disait qu’il fallait que la méthode soit marquée comme virtual, ce que nous avons fait. Il proposait également qu’elle soit marquée abstract, nous verrons un peu plus loin ce que ça veut dire. Visual Studio Express indiquait enfin que la méthode pouvait être marquée override.
Cela veut dire qu’une classe fille de ChienMuet peut également redéfinir la méthode Aboyer() afin qu’elle colle à ses besoins. Elle n’est pas marquée virtual mais elle est marquée override. Par exemple :

public class ChienMuetAvecSyntheseVocale : ChienMuet
{
    public override void Aboyer()
    {
        Console.WriteLine("bwarf !");
    }
}

Il y a encore un dernier point que nous n’avons pas abordé. Il s’agit de la capacité pour une classe fille de redéfinir une méthode tout en conservant la fonctionnalité de la méthode de la classe mère.
Imaginons notre classe Animal qui possède une méthode Manger() :

public class Animal
{
    public virtual void Manger()
    {
        Console.WriteLine("Mettre les aliments dans la bouche");
        Console.WriteLine("Mastiquer");
        Console.WriteLine("Avaler");
        Console.WriteLine("...");
    }
}

Notre classe Chien pourra s’appuyer sur le comportement de la méthode Manger() de la classe Animal pour créer sa propre action personnelle.
Cela se passe en utilisant à nouveau le mot clé base qui représente la classe mère. Nous pourrons par exemple appeler la méthode Manger() de la classe mère afin de réutiliser son fonctionnement. Cela donne :

public class Chien : Animal
{
    public override void Manger()
    {
        Console.WriteLine("Réclamer à manger au maître");
        base.Manger();
        Console.WriteLine("Remuer la queue");
    }
}

Dans cet exemple, je fais quelque chose avant d’appeler la méthode de la classe mère, puis je fais quelque chose d’autre après. Maintenant, si nous faisons manger notre chien :

Chien chien = new Chien();
chien.Manger();

nous aurons :

Image utilisateur

Nous voyons bien avec cet exemple comment la classe fille peut réutiliser les méthodes de sa classe mère.

Polymorphisme

Nous avons dit qu’une manifestation du polymorphisme était la capacité d'une classe à effectuer la même action sur différents types d’intervenants. Il s'agit de la surcharge, appelé aussi polymorphisme ad hoc.

Concrètement, cela veut dire qu’il est possible de définir la même méthode avec des paramètres en entrée différents.
Si vous vous rappelez bien, c’est quelque chose que nous avons déjà fait sans le savoir. Devinez …
Oui, c’est ça, avec la méthode Console.WriteLine.

Nous avons pu afficher des chaines de caractères, mais aussi des entiers, même des types double, et plus récemment des objets. Comment est-ce possible alors que nous avons déjà vu qu’il était impossible de passer des types en paramètres d’une méthode qui ne correspondent pas à sa signature ?

Ainsi, l’exemple suivant :

public class Program
{
    static void Main(string[] args)
    {
        Math math = new Math();
        int a = 5;
        int b = 6;
        int resultat = math.Addition(a, b);

        double c = 1.5;
        double d = 5.0;
        resultat = math.Addition(c, d); // erreur de compilation
    }
}

public class Math
{
    public int Addition(int a, int b)
    {
        return a + b;
    }
}

provoquera une erreur de compilation lorsque nous allons essayer de passer des variables du type double à notre méthode qui prend des entiers en paramètres.
Pour que ceci fonctionne, nous allons rendre polymorphe cette méthode en définissant à nouveau cette même méthode mais en lui faisant prendre des paramètres d’entrées différents :

public class Program
{
    static void Main(string[] args)
    {
        Math math = new Math();
        int a = 5;
        int b = 6;
        int resultat = math.Addition(a, b);

        double c = 1.5;
        double d = 5.0;
        double resultatDouble = math.Addition(c, d); // ca compile, youpi
    }
}

public class Math
{
    public int Addition(int a, int b)
    {
        return a + b;
    }

    public double Addition(double a, double b)
    {
        return a + b;
    }
}

Nous avons ainsi écrit deux formes différentes de la même méthode. Une qui accepte des entiers et l’autre qui accepte des double.
Ce code fonctionne désormais correctement.

Il est bien sûr possible d’écrire cette méthode avec beaucoup de paramètres de types différents, même une classe Chien, en imaginant que le fait d’additionner 2 chiens corresponde au fait d’additionner leur nombre de pattes :

public class Math
{
    public int Addition(int a, int b)
    {
        return a + b;
    }

    public double Addition(double a, double b)
    {
        return a + b;
    }

    public int Addition(Chien c1, Chien c2)
    {
        return c1.NombreDePattes + c2.NombreDePattes;
    }
}

Attention, j’ai toujours indiqué qu’il était possible d’ajouter une nouvelle forme à la même méthode en changeant les paramètres d’entrée. Vous ne pourrez pas le faire en changeant uniquement le paramètre de retour. Ce qui fait que cet exemple ne pourra pas compiler :

public class Math
{
    public int Addition(int a, int b)
    {
        return a + b;
    }

    public double Addition(int a, int b)
    {
        return a + b;
    }
}

Les deux méthodes acceptent deux entiers en paramètres et renvoient soit un entier, soit un double. Le compilateur ne sera pas capable de choisir quelle méthode utiliser lorsque nous essayerons d’appeler cette méthode. Les méthodes doivent se différencier avec les paramètres d’entrée.
Lorsque nous avons plusieurs signatures possibles pour la même méthode, vous remarquerez que la complétion automatique nous propose alors plusieurs alternatives :

Image utilisateur

Visual Studio Express nous indique qu’il a trois méthodes possibles qui s’appellent Math.Addition. Pour voir la signature des autres méthodes, il suffit de cliquer sur les flèches, ou d’utiliser les flèches du clavier :

Image utilisateur

C’est ce qu’il se passe dans la méthode Console.WriteLine :

Image utilisateur

Nous voyons ici qu’il existe 19 écritures de la méthode WriteLine, la cinquième prenant en paramètres un décimal.
Notez que pour écrire plusieurs formes de cette méthode, nous pouvons également jouer sur le nombre de paramètres.
La méthode :

public int Addition(int a, int b, int c)
{
    return a + b + c;
}

sera bien une nouvelle forme de la méthode Addition.

Nous avons également vu dans le chapitre sur les constructeurs d’une classe qu’il était possible de cumuler les constructeurs avec des paramètres différents. Il s’agit à nouveau du polymorphisme. Il nous permet de définir différents constructeurs sur nos objets.

La conversion entre les objets avec le casting

Nous avons déjà vu dans le cours précédent qu’il était possible de convertir les types qui se ressemblent entre eux.
Cela fonctionne également avec les objets.
Plus précisément, cela veut dire que nous pouvons convertir un objet en un autre seulement s’il est une sorte de l’autre objet. Nous avons vu dans les chapitres précédents qu’il s’agissait de la notion d’héritage.

Ainsi, si nous avons défini une classe Animal et que nous définissons une classe Chien qui hérite de cette classe Animal :

public class Animal
{
}

public class Chien : Animal
{
}

nous pourrons alors convertir le chien en animal dans la mesure où le chien est une sorte d’animal :

Chien medor = new Chien();
Animal animal = (Animal)medor;

Nous utilisons pour ce faire un cast, comme nous l’avons déjà fait pour les types intégrés (int, bool, etc.).
Il suffit de préfixer la variable à convertir du type entre parenthèses dans lequel nous souhaitons le convertir.
Ici, nous pouvons convertir facilement notre Chien en Animal.

Par contre, il est impossible de convertir un chien en voiture, car il n’y a pas de relation d’héritage entre les deux. Ainsi les instructions suivantes :

Chien medor = new Chien();
Voiture voiture = (Voiture)medor;

provoqueront une erreur de compilation.
Nous avons précédemment utilisé l’héritage afin de mettre des chiens et des chats dans une liste d’animaux.
Nous avions fait quelque chose du genre :

List<Animal> animaux = new List<Animal>();
Animal chien = new Chien();
Animal chat = new Chat();

animaux.Add(chien);
animaux.Add(chat);

Il serait plus logique en fait d’écrire les instructions suivantes :

List<Animal> animaux = new List<Animal>();
Chien chien = new Chien();
Chat chat = new Chat();

animaux.Add((Animal)chien);
animaux.Add((Animal)chat);

Dans ce cas, nous créons un objet Chien et un objet Chat que nous mettons dans une liste d’objets Animal grâce à une conversion utilisant un cast.
En fait, ce cast est inutile et nous pouvons simplement écrire :

List<Animal> animaux = new List<Animal>();
Chien chien = new Chien();
Chat chat = new Chat();

animaux.Add(chien);
animaux.Add(chat);

La conversion est implicite, comme lorsque nous avions utilisé un object en paramètres d’une méthode et que nous pouvions lui passer tous les types qui dérivent d’ object.
Nous avions également vu que nous ne pouvions traiter les chiens et les chats que comme des animaux à partir du moment où nous les mettions dans une liste. Avec les objets suivants :

public class Animal
{
    public void Respirer()
    {
        Console.WriteLine("Je respire");
    }
}

public class Chien : Animal
{
    public void Aboyer()
    {
        Console.WriteLine("Waouf");
    }
}

public class Chat : Animal
{
    public void Miauler()
    {
        Console.WriteLine("Miaou");
    }
}

Nous pouvions utiliser une boucle pour faire respirer tous nos animaux :

List<Animal> animaux = new List<Animal>();
Chien chien = new Chien();
Chat chat = new Chat();

animaux.Add(chien);
animaux.Add(chat);

foreach (Animal animal in animaux)
{
    animal.Respirer();
}

Mais impossible de faire aboyer le chien, ni miauler le chat.
Si vous tentez de remplacer dans la boucle Animal par Chien, avec :

foreach (Chien c in animaux)
{
    c.Aboyer();
}

Vous pourrez faire aboyer le premier élément de la liste qui est effectivement un chien, par contre il y aura un plantage au deuxième élément de la liste car il s’agit d’un chat :

Image utilisateur

Lorsque notre programme a tenté de convertir un animal qui est un chat en chien, il nous a fait comprendre qu’il n’appréciait que moyennement. Les chiens n’aiment pas trop les chats d’une manière générale, alors en plus, un chat qui essaie de se faire passer pour un chien : c’est une déclaration de guerre !
Voilà pourquoi notre programme a levé une exception. Il lui était impossible de convertir un Chat en Chien.

Il est cependant possible de tester si une variable correspond à un objet grâce au mot-clé is. Ce qui nous permettra de faire la conversion adéquate et de nous éviter une erreur à l’exécution :

foreach (Animal animal in animaux)
{
    if (animal is Chien)
    {
        Chien c = (Chien)animal;
        c.Aboyer();
    }
    if (animal is Chat)
    {
        Chat c = (Chat)animal;
        c.Miauler();
    }
}

Nous testons avec le mot-clé is si l’animal est une instance d’un Chien ou d’un chat. Le code du dessus nous permettra d’utiliser dans la boucle l’animal courant comme un chien ou un chat en fonction de ce qu’il est vraiment, grâce au test.

Ce qui produira :

Image utilisateur

Le fait de tester ce qu’est vraiment l’animal avant de le convertir est une sécurité indispensable pour éviter le genre d’erreur du dessus.
C’est l’inconvénient du cast explicite. Il convient très bien si nous sommes certains du type dans lequel nous souhaitons en convertir un autre. Par contre, si la conversion n’est pas possible, alors nous aurons une erreur.
Lorsque nous ne sommes pas certains du résultat du cast, mieux vaut tester si l’instance d’un objet correspond bien à l’objet lui-même.

Cela peut se faire comme nous l’avons vu avec le mot-clé is, mais également avec un autre cast qui s’appelle le cast dynamique. Il se fait en employant le mot-clé as.
Ce cast dynamique vérifie que l’objet est bien convertible. Si c’est le cas, alors il fait un cast explicite pour renvoyer le résultat de la conversion, sinon, il renvoie une référence nulle.
Le code du dessus peut donc s’écrire :

foreach (Animal animal in animaux)
{
    Chien c1 = animal as Chien;
    if (c1 != null)
    {
        c1.Aboyer();
    }
    Chat c2 = animal as Chat;
    if (c2 != null)
    {
        c2.Miauler();
    }
}

On utilise le mot-clé as en le faisant précéder de la valeur à tenter de convertir et en le faisant suivre du type dans lequel nous souhaitons la convertir.

Fonctionnellement, nous faisons la même chose dans les deux codes. Vous pouvez choisir l’écriture que vous préférez, mais sachez que c’est ce dernier qui est en général utilisé car il est préconisé par Microsoft et est un tout petit peu plus performant.

Un petit détail encore. Il est possible de convertir un type valeur, comme un int en type référence en utilisant ce qu’on appelle le boxing. Rien à voir avec le fait de taper sur les types valeur. Comme nous l’avons vu, les types valeur et les types référence sont gérés différemment par .NET. Aussi, si nous convertissons un type valeur en type référence, .NET fait une opération spéciale automatiquement.
Ainsi le code suivant :

int i = 5;
object o = i; // boxing

effectue un boxing automatique de l’entier en type référence. C’est ce boxing automatique qui nous permet de manipuler les types valeur comme des object.
C’est aussi ce qui nous a permis plus haut de passer un entier en paramètre à une méthode qui acceptait un object.
En interne, ce qu’il se passe c’est que object se voit attribuer une référence vers une copie de la valeur de i.
Ainsi, modifier o ne modifiera pas i. Ce code :

int i = 5;
object o = i; // boxing
o = 6;
Console.WriteLine(i);
Console.WriteLine(o);

affiche 5 puis 6.
Le contraire est également possible, ce qu’on appelle l’unboxing. Seulement, celui-ci a besoin d’un cast explicite afin de pouvoir compiler. C'est-à-dire :

int i = 5;
object o = i; // boxing
int j = (int)o; // unboxing

ici nous reconvertissons la référence vers la valeur de o en entier et nous effectuons à nouveau une copie de cette valeur pour la mettre dans j. Ainsi le code suivant :

int i = 5;
object o = i; // boxing
o = 6;
int j = (int)o; // unboxing
j = 7;

Console.WriteLine(i);
Console.WriteLine(o);
Console.WriteLine(j);

affichera en toute logique 5 puis 6 puis 7.

En résumé
  • Les objets peuvent être des types valeur ou des types référence. Les variables de type valeur possèdent la valeur de l'objet, comme un entier. Les variables de type référence possèdent une référence vers l'objet en mémoire.

  • Tous les objets dérivent de la classe de base Object.

  • On peut substituer une méthode grâce au mot-clé override si elle s'est déclarée candidate à la substitution grâce au mot-clé virtual.

  • La surcharge est le polymorphisme permettant de faire varier les types des paramètres d'une même méthode ou leurs nombres.

  • Il est possible grâce au cast de convertir un type en un autre type, s'ils ont une relation d'héritage.

Example of certificate of achievement
Example of certificate of achievement