• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_certifying

Got it!

Last updated on 3/10/17

Notions avancées de POO en C#

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

Dans ce chapitre, nous allons continuer à découvrir comment nous pouvons faire de l'orienté objet avec le C#. Nous allons pousser un peu plus loin en découvrant les interfaces et en manipulant les classes statiques et abstraites.

À la fin de ce chapitre, vous serez capables de faire des objets encore plus évolués et vous devriez être capables de créer un vrai petit programme orienté objet… !

Comparer des objets

Nous avons vu dans la première partie qu’il était possible de comparer facilement des types valeur grâce aux opérateurs de comparaison. En effet, vu que des variables de ces types possèdent directement la valeur que nous lui affectons, on peut facilement comparer un entier avec la valeur 5, ou un entier avec un autre entier. Par contre, cela ne fonctionne pas avec les objets. En effet, nous avons vu que les variables qui représentent des instances d’objet contiennent en fait une référence vers l’instance.

Cela n’a pas vraiment de sens de comparer des références. De plus, en imaginant que je veuille vraiment comparer deux voitures, sur quels critères puis-je déterminer qu’elles sont égales ? La couleur ? La vitesse ?

Sans rien faire, la comparaison en utilisant par exemple l’opérateur d’égalité « == » permet simplement de vérifier si les références pointent vers le même objet.

Pour les exemples suivants, nous nous baserons sur la classe Voiture suivante :

public class Voiture
{
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }
}

Ainsi, si nous écrivons :

Voiture voitureNicolas = new Voiture();
voitureNicolas.Couleur = "Bleue";
Voiture voitureJeremie = voitureNicolas;
voitureJeremie.Couleur = "Verte";
if (voitureJeremie == voitureNicolas)
{
    Console.WriteLine("Les objets référencent la même instance");
}

Ce code affichera la chaine « Les objets référencent la même instance » car effectivement, nous avons affecté la référence de voitureNicolas à voitureJeremie. (Ce qui implique également que la modification de la voiture de Jérémie affecte également la voiture de Nicolas, comme nous l’avons déjà vu).

Par contre, le code suivant :

Voiture voitureNicolas = new Voiture();
Voiture voitureJeremie = new Voiture();
if (voitureJeremie == voitureNicolas)
{
    Console.WriteLine("Les objets référencent la même instance");
}

n'affichera évidemment rien car ce sont deux instances différentes.

S’il s’avère qu’il est vraiment pertinent de pouvoir comparer deux voitures entre elles, il faut savoir que c’est quand même possible. La première chose à faire est de définir les critères de comparaison.
Par exemple, nous n’avons qu’à dire que deux voitures sont identiques quand la couleur, la marque et la vitesse sont égales. Je sais, c’est un peu irréel, mais c’est pour l’exemple.

Ainsi, nous pourrons par exemple vérifier que deux voitures sont égales avec l’instruction suivante :

if (voitureNicolas.Couleur == voitureJeremie.Couleur && voitureNicolas.Marque == voitureJeremie.Marque && voitureNicolas.Vitesse == voitureJeremie.Vitesse)
{
    Console.WriteLine("Les deux voitures sont identiques");
}

La comparaison d’égalité entre deux objets, c’est en fait le rôle de la méthode Equals() dont chaque objet hérite de la classe mère Object. A part pour les types valeur, le comportement par défaut de la méthode Equals() est de comparer les références des objets. Seulement, il est possible de définir un comportement plus approprié pour notre classe Voiture, grâce à la fameuse substitution.

Comme on l’a déjà vu, on utilise le mot clé override. Ceci est possible dans la mesure où la classe « Object » a défini la méthode Equals comme virtuelle, avec le mot clé virtual.
Ce qui donne :

public class Voiture
{ 
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }

    public override bool Equals(object obj)
    {
        Voiture v = obj as Voiture;
        if (v == null)
            return false;
        return Vitesse == v.Vitesse && Couleur == v.Couleur && Marque == v.Marque;
    }
}

Remarquons que la méthode Equals prend en paramètre un object. La première chose à faire est donc de vérifier que nous avons réellement une voiture, grâce au cast dynamique.
Ensuite, il ne reste qu’à comparer les propriétés de l’instance courante et de l’objet passé en paramètre.

Pour faire une comparaison entre deux voitures, nous pourrons utiliser le code suivant :

Voiture voitureNicolas = new Voiture { Vitesse = 10, Marque = "Peugeot", Couleur = "Grise"};
Voiture voitureJeremie = new Voiture { Vitesse = 10, Marque = "Peugeot", Couleur = "Grise" };
if (voitureNicolas.Equals(voitureJeremie))
{
    Console.WriteLine("Les objets ont les mêmes valeurs dans leurs propriétés");
}

Nos deux voitures sont identiques car leurs marques, leurs couleurs et leurs vitesses sont identiques :

Image utilisateur

Facile de comparer :) .
Sauf que vous aurez peut-être remarqué que la compilation de ce code provoque un avertissement. Il ne s’agit pas d’une erreur, mais Visual Studio Express nous informe qu’il faut faire attention :

Image utilisateur

Il nous dit que nous avons substitué la méthode Equals() sans avoir redéfini la méthode GetHashCode(). Nous n'avons pas besoin ici de savoir à quoi sert vraiment la méthode GetHashCode(), mais si vous voulez en savoir plus, n'hésitez pas à consulter la documentation.

Toujours est-il que nous devons rajouter une substitution de la méthode GetHashCode(), dont le but est de renvoyer un identifiant plus ou moins unique représentant l'objet, ce qui donnera :

public class Voiture
{
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }

    public override bool Equals(object obj)
    {
        Voiture v = obj as Voiture;
        if (v == null)
            return false;
        return Vitesse == v.Vitesse && Couleur == v.Couleur && Marque == v.Marque;
    }

    public override int GetHashCode()
    {
        return Couleur.GetHashCode() * Marque.GetHashCode() * Vitesse.GetHashCode();
    }
}

Nous nous servons du fait que chaque variable de la classe possède déjà un identifiant obtenu avec la méthode GetHashCode() . En combinant chaque identifiant de chaque propriété nous pouvons en créer un nouveau.

Ici, la classe est complète et prête à être comparée. Elle pourra donc fonctionner correctement avec tous les algorithmes d’égalité du framework .NET. Notez quand même que devoir substituer ces deux méthodes est une opération relativement rare, nous l’avons étudié pour la culture.

Ce qui est important à retenir c’est ce fameux warning. La conclusion à tirer est que notre façon de comparer, bien que fonctionnelle pour notre voiture, n’est pas parfaite.

Pourquoi ? Parce qu’en ayant substitué la méthode Equals(), nous croyons que la comparaison est bonne sauf que le compilateur nous apprend que ce n’est pas le cas. Heureusement qu’il était là ce compilateur. Comme c’est une erreur classique, il est capable de la détecter. Mais si c’est autre chose et qu’il ne le détecte pas ?

Cela manque d’une uniformisation tout ça, vous ne trouvez pas ? Il faudrait quelque chose qui nous assure que la classe est correctement comparable. Une espèce de contrat que l’objet s’engagerait à respecter pour être sûr que toutes les comparaisons soient valides.

Un contrat ? Un truc qui finit par « able » ? Ça me rappelle quelque chose ça. Mais oui, les interfaces.

Les interfaces

Une fois n’est pas coutume, plutôt que de commencer par étudier le plus simple, nous allons étudier le plus logique puis nous reviendrons sur le plus simple.
C’est-à-dire que nous allons pousser un peu plus loin la comparaison en nous servant des interfaces et nous reviendrons ensuite sur comment créer une interface.

Nous avons donc dit que les interfaces étaient un contrat que s’engageait à respecter un objet. C’est tout à fait ce dont on a besoin ici. Notre objet doit s’engager à fonctionner pour tous les types de comparaison. Il doit être comparable. Mais comparable ne veut pas forcément dire « égal », nous devrions être aussi capables d’indiquer si un objet est supérieur à un autre.

Pourquoi ? Imaginons que nous possédions un tableau de voitures et que nous souhaitions le trier comme on a vu dans le cours précédent. Pour les entiers c’était une opération plutôt simple, avec la méthode Array.Sort() ils étaient automatiquement triés par ordre croissant. Mais dans notre cas, comment un tableau sera capable de trier nos voitures ?

Nous voici donc en présence d’un cas concret d’utilisation des interfaces. L’interface IComparable permet de définir un contrat de méthodes destinées à la prise en charge de la comparaison entre deux instances d’un objet.
Une fois ces méthodes implémentées, nous serons certains que nos objets seront comparables correctement.

Pour cela, nous allons faire en sorte que notre classe « implémente » l’interface.

Reprenons notre classe Voiture avec uniquement ses propriétés et faisons lui implémenter l’interface IComparable.
Pour implémenter une interface, on utilisera la même syntaxe que pour hériter d’une classe, c'est-à-dire qu’on utilisera les deux points suivis du type de l’interface. Ce qui donne :

public class Voiture : IComparable
{
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }
}

Si vous tentez de compiler ce code, vous aurez le message d’erreur suivant :

Image utilisateur

Le compilateur nous rappelle à l’ordre : nous annonçons que nous souhaitons respecter le contrat de comparaison, sauf que nous n’avons pas la méthode adéquate !
Eh oui, le contrat indique ce que nous nous engageons à faire mais pas la façon de le faire. L’implémentation de la méthode est à notre charge.

Toujours dans l’optique de simplifier la tâche du développeur, Visual Studio Express nous aide pour implémenter les méthodes d’un contrat. Faites un clic droit sur l’interface et choisissez dans le menu contextuel « Implémenter l’interface » et « Implémenter l’interface » :

Image utilisateur

Visual Studio Express nous génère le code suivant :

public class Voiture : IComparable
{
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }

    public int CompareTo(object obj)
    {
        throw new NotImplementedException();
    }
}

C'est-à-dire la signature de la méthode qu’il nous manque pour respecter le contrat de l’interface et un contenu que nous ne comprenons pas pour l’instant. Nous y reviendrons plus tard, pour l’instant, vous n’avez qu’à supprimer la ligne :

throw new NotImplementedException();

Il ne reste plus qu’à écrire le code de la méthode.
Pour ce faire, il faut définir un critère de tri. Trier des voitures n’a pas trop de sens, aussi nous dirons que nous souhaitons les trier suivant leurs vitesses.

Pour respecter correctement le contrat, nous devons respecter la règle suivante qui se trouve dans la documentation de la méthode de l’interface :

  • Si une voiture est inférieure à une autre, alors nous devons renvoyer une valeur inférieure à 0, disons -1.

  • Si elle est égale, alors nous devons renvoyer 0.

  • Enfin, si elle est supérieure, nous devons renvoyer une valeur supérieure à 0, disons 1.

Ce qui donne :

public int CompareTo(object obj)
{
    Voiture voiture = (Voiture)obj;
    if (this.Vitesse < voiture.Vitesse)
        return -1;
    if (this.Vitesse > voiture.Vitesse)
        return 1;
    return 0;
}

À noter que la comparaison s’effectue entre l’objet courant et un objet qui lui est passé en paramètres. Pour que ce soit un peu plus clair, j’ai utilisé le mot-clé this qui permet de bien identifier l’objet courant et l’objet passé en paramètres.
Comme il est facultatif, nous pouvons le supprimer.

Vous aurez également remarqué que j’utilise un cast explicite avant de comparer. Ceci permet de renvoyer une erreur si jamais l’objet à comparer n’est pas du bon type. En effet, que devrais-je renvoyer si jamais l’objet qu’on me passe n’est pas une voiture ? Une erreur, c’est très bien. :)

Le code est suffisamment explicite pour que nous comprenions facilement ce que l’on doit faire : comparer les vitesses.

Il est possible de simplifier grandement le code car pour comparer nos deux voitures, nous effectuons la comparaison sur la valeur d’un entier, ce qui est plutôt trivial.
D’autant plus que l’entier, en bon objet comparable, possède également la méthode CompareTo().
Ce qui fait qu’il est possible d’écrire notre méthode de comparaison de cette façon :

public int CompareTo(object obj)
{
    Voiture voiture = (Voiture)obj;
    return Vitesse.CompareTo(voiture.Vitesse);
}

En effet, Vitesse étant un type intégré, il implémente déjà correctement la comparaison. C’est d’ailleurs pour ça que le tableau d’entier que nous avons vu précédemment a été capable de se trier facilement.

En ayant implémenté cette interface, nous pouvons désormais trier des tableaux de Voiture :

Voiture[] voitures = new Voiture[] { new Voiture { Vitesse = 100 }, new Voiture { Vitesse = 40 }, new Voiture { Vitesse = 10 }, new Voiture { Vitesse = 40 }, new Voiture { Vitesse = 50 } };
Array.Sort(voitures);
foreach (Voiture v in voitures)
{
    Console.WriteLine(v.Vitesse);
}

Ce qui donne :

Image utilisateur

Voilà pour le tri, mais si je peux me permettre, je trouve que ce code-là est un peu moche.
J’y reviendrai un peu plus tard.

Nous avons donc implémenté notre première interface. Finalement, ce n’était pas si compliqué. Voyons à présent comment créer nos propres interfaces.
Une interface se définit en C# comme une classe, sauf qu’on utilise le mot-clé interface à la place de class.
En tant que débutant, vous aurez rarement besoin de créer des interfaces. Cependant, il est utile de savoir le faire. Par contre, il sera beaucoup plus fréquent que vos classes implémentent des interfaces existantes du framework .NET, comme nous venons de le faire.

Voyons à présent comment créer une interface et examinons le code suivant :

public interface IVolant
{
    int NombrePropulseurs { get; set; }
    void Voler();
}

Nous définissons ici une interface IVolant qui possède une propriété de type int et une méthode Voler() qui ne renvoie rien.
Voilà, c’est tout simple. :)

Nous avons créé une interface. Les objets qui choisiront d’implémenter cette interface seront obligés d’avoir une propriété entière NombrePropulseurs et une méthode Voler() qui ne renvoie rien. Rappelez-vous que l'interface ne contient que le contrat et aucune implémentation. C'est-à-dire que nous ne verrons jamais de corps de méthode dans une interface ni de variables membres ; uniquement des méthodes et des propriétés. Un contrat.

Notez quand même qu’il ne faut pas définir de visibilité sur les membres d’une interface. Nous serons obligés de définir les visibilités en public sur les objets implémentant l’interface.

Créons désormais deux objets Avion et Oiseau qui implémentent cette interface, ce qui donne :

public class Oiseau : IVolant
{
    public int NombrePropulseurs { get; set; }

    public void Voler()
    {
        Console.WriteLine("Je vole grâce à " + NombrePropulseurs + " ailes");
    }
}

public class Avion : IVolant
{
    public int NombrePropulseurs { get; set; }
    public void Voler()
    {
        Console.WriteLine("Je vole grâce à " + NombrePropulseurs + " moteurs");
    }
}

Grâce à ce contrat, nous savons maintenant que n’importe lequel de ces objets saura voler.

Il est possible de traiter ces objets comme des objets volants, un peu comme ce que nous avions fait avec les classes mères, en utilisant l’interface comme type pour la variable. Par exemple :

IVolant oiseau = new Oiseau { NombrePropulseurs = 2 };
oiseau.Voler();

Nous instancions vraiment un objet Oiseau, mais nous le manipulons en tant que IVolant.
Un des intérêts dans ce cas sera de pouvoir manipuler des objets qui partagent un comportement de la même façon :

Oiseau oiseau = new Oiseau { NombrePropulseurs = 2 };
Avion avion = new Avion { NombrePropulseurs = 4 };

List<IVolant> volants = new List<IVolant> { oiseau, avion };
foreach (IVolant volant in volants)
{
    volant.Voler();
}

Ce qui produira :

Image utilisateur

Grâce à l’interface, nous avons pu mettre dans une même liste des objets différents, qui n’héritent pas entre eux mais qui partagent une même interface, c'est-à-dire un même comportement : IVolant. Pour accéder à ces objets, nous devrons utiliser leurs interfaces.

Il sera possible quand même de caster nos IVolant en Avion ou en Oiseau, si jamais nous souhaitons rajouter une propriété propre à l’avion.
Par exemple je rajoute une propriété NomDuCommandant à mon avion mais qui ne fait pas partie de l’interface :

public class Avion : IVolant
{
    public int NombrePropulseurs { get; set; }
    public string NomDuCommandant { get; set; }
    public void Voler()
    {
        Console.WriteLine("Je vole grâce à " + NombrePropulseurs + " moteurs");
    }
}

Cela veut dire que l’objet Avion pourra affecter un nom de commandant mais qu’il ne sera pas possible d’y accéder par l’interface :

IVolant avion = new Avion { NombrePropulseurs = 4, NomDuCommandant = "Nico" };
Console.WriteLine(avion.NomDuCommandant); // erreur de compilation

L’erreur de compilation nous indique que IVolant ne possède pas de définition pour NomDuCommandant. Ce qui est vrai !

Pour accéder au nom du commandant, nous pourrons tenter de caster nos IVolant en Avion. Si le cast est valide, alors nous pourrons accéder à notre propriété :

Oiseau oiseau = new Oiseau { NombrePropulseurs = 2 };
Avion avion = new Avion { NombrePropulseurs = 4, NomDuCommandant = "Nico" };

List<IVolant> volants = new List<IVolant> { oiseau, avion };
foreach (IVolant volant in volants)
{
    volant.Voler();
    Avion a = volant as Avion;
    if (a != null)
    {
        Console.WriteLine(a.NomDuCommandant);
    }
}

Voilà, c’est tout simple et ça ressemble un peu à ce qu’on a déjà vu.

Il faut également noter que les interfaces peuvent hériter entre elles, comme c'est le cas avec les objets.
C'est-à-dire que je vais pouvoir déclarer une interface IVolantMotorise qui hérite de l'interface IVolant.

public interface IVolant
{
    int NombrePropulseurs { get; set; }
    void Voler();
}

public interface IVolantMotorise : IVolant
{
    void DemarrerLeMoteur();
}

Ainsi, ma classe Avion qui implémentera IVolantMotorise devra obligatoirement implémenter les méthodes/propriétés de IVolant ainsi que la méthode de IVolantMotorise :

public class Avion : IVolantMotorise
{
    public void DemarrerLeMoteur()
    {
    }

    public int NombrePropulseurs { get; set;}

    public void Voler()
    {
    }
}

Enfin, et nous nous arrêterons là pour les interfaces, il est possible pour une classe d'implémenter plusieurs interfaces. Il suffira pour cela de séparer les interfaces par une virgule et d'implémenter bien sûr tout ce qu'il faut derrière. Par exemple :

public interface IVolant
{
    void Voler();
}

public interface IRoulant
{
    void Rouler();
}

public class Avion : IVolant, IRoulant
{
    public void Voler()
    {
        Console.WriteLine("Je vole");
    }

    public void Rouler()
    {
        Console.WriteLine("Je Roule");
    }
}

Les classes et les méthodes abstraites

Une classe abstraite est une classe particulière qui ne peut pas être instanciée. Concrètement, cela veut dire que nous ne pourrons pas utiliser l’opérateur new.
De la même façon, une méthode abstraite est une méthode qui ne contient pas d’implémentation, c’est-à-dire pas de code.

Pour être utilisables, les classes abstraites doivent être héritées et les méthodes redéfinies.
En général, les classes abstraites sont utilisées comme classe de base pour d’autres classes. Ces classes fournissent des comportements mais n’ont pas vraiment d’utilité à avoir une vie propre. Ainsi, les classes filles qui en héritent pourront bénéficier de leurs comportements et devront éventuellement en remplacer d’autres.

C’est comme une classe incomplète qui ne demande qu’à être complétée.

Si une classe possède une méthode abstraite, alors la classe doit absolument être abstraite. L’inverse n’est pas vrai, une classe abstraite peut posséder des méthodes non abstraites.
Vous aurez rarement besoin d’utiliser les classes abstraites en tant que débutant mais elles pourront vous servir pour combiner la puissance des interfaces à l’héritage.

Bon, voilà pour la théorie, passons un peu à la pratique.

Rappelez-vous nos chiens et nos chats qui dérivent d’une classe mère Animal. Nous savons qu’un Animal est capable de se déplacer. C’est un comportement qu’ont en commun tous les animaux. Il est tout à fait logique de définir une méthode SeDeplacer() au niveau de la classe Animal. Sauf qu’un chien ne se déplace pas forcément comme un dauphin. L’un a des pattes, l’autre des nageoires. Et même si le chien et le chat semblent avoir un déplacement relativement proche, ils ont chacun des subtilités. Le chat a un mouvement plus gracieux, plus félin.

Bref, on se rend compte que nous sommes obligés de redéfinir la méthode SeDeplacer() dans chaque classe fille de la classe Animal. Et puis de toute façon, sommes-nous vraiment capables de dire comment se déplace un animal dans l’absolu ?

La méthode SeDeplacer est une candidate parfaite pour être une méthode abstraite. Rappelez-vous, la méthode abstraite ne possède pas d’implémentation et chaque classe fille de la classe possédant cette méthode abstraite devra la spécialiser. C’est exactement ce qu’il nous faut.

Pour déclarer une méthode comme étant abstraite, il faut utiliser le mot-clé abstract et ne fournir aucune implémentation de la méthode, c'est-à-dire que la déclaration de la méthode doit se terminer par un point-virgule :

public class Animal
{
    public abstract void SeDeplacer();
}

Si nous tentons de compiler cette classe, nous aurons l’erreur suivante :

'MaPremiereApplication.Animal.SeDeplacer()' est abstrait, mais est contenu dans la classe non abstraite 'MaPremiereApplication.Animal'

Ah oui c’est vrai, on a dit qu’une classe qui contient au moins une méthode abstraite était forcément abstraite. C’est le même principe que pour la méthode, il suffit d’utiliser le mot-clé abstract devant le mot-clé class :

public abstract class Animal
{
    public abstract void SeDeplacer();
}

Voilà, notre classe peut compiler tranquillement.

Nous avons également dit qu’une classe abstraite pouvait contenir des méthodes concrètes et que c’était d’ailleurs une des grandes forces de ce genre de classes. En effet, il est tout à fait pertinent que des animaux puissent mourir. Et là, ça se passe pour tout le monde de la même façon, le cœur arrête de battre et on ne peut plus rien faire. C’est triste, mais c’est ainsi.

Cela veut dire que nous pouvons créer une méthode Mourir() dans notre classe abstraite qui possède une implémentation.

Cela donne :

public abstract class Animal
{
    private Coeur coeur;
    public Animal()
    {
        coeur = new Coeur();
    }

    public abstract void SeDeplacer();

    public void Mourir()
    {
        coeur.Stop();
    }
}

Avec une classe Cœur qui ne fait pas grand-chose :

public class Coeur
{
    public void Battre()
    {
        Console.WriteLine("Boom boom");
    }

    public void Stop()
    {
        Console.WriteLine("Mon coeur s'arrête de battre");
    }
}

Comme prévu, il n’est pas possible d’instancier un objet Animal. Si nous tentons l’opération :

Animal animal = new Animal();

nous aurons l’erreur de compilation suivante :

Impossible de créer une instance de la classe abstraite ou de l'interface 'MaPremiereApplication.Animal'

Par contre, il est possible de créer une classe Chien qui dérive de la classe Animal :

public class Chien : Animal
{
}

Cette classe ne pourra pas compiler dans cet état car il faut obligatoirement redéfinir la méthode abstraite SeDeplacer(). Cela se fait en utilisant le mot-clé override, comme on l’a déjà vu.

Vous aurez sûrement remarqué que la méthode abstraite n’utilise pas le mot-clé virtual comme cela doit absolument être le cas lors de la substitution d’une méthode dans une classe non-abstraite. Il est ici implicite et ne doit pas être utilisé, sinon nous aurons une erreur de compilation. Et puis cela nous arrange, un seul mot-clé, c’est largement suffisant !

Nous devons donc spécialiser la méthode SeDeplacer, soit en écrivant la méthode à la main, soit en utilisant encore une fois notre ami Visual Studio Express.
Il suffit de faire un clic droit sur la classe dont hérite Chien (en l’occurrence Animal) et de cliquer sur « Implémenter une classe abstraite » :

Image utilisateur

Visual Studio Express nous génère donc la signature de la méthode à substituer :

public class Chien : Animal
{
    public override void SeDeplacer()
    {
        throw new NotImplementedException();
    }
}

Il ne reste plus qu’à écrire le corps de la méthode SeDeplacer, par exemple :

public class Chien : Animal
{
    public override void SeDeplacer()
    {
        Console.WriteLine("Waouf ! Je me déplace avec mes 4 pattes");
    }
}

Ainsi, nous pourrons créer un objet Chien et le faire se déplacer puis le faire mourir car il hérite des comportements de la classe Animal. Paix à son âme.

Chien max = new Chien();
max.SeDeplacer();
max.Mourir();

ce qui donnera :

Image utilisateur

Vous pouvez désormais vous rendre compte de la réalité de la phrase que j’ai écrite en introduction :

« [Les classes abstraites] pourront vous servir pour combiner la puissance des interfaces à l’héritage ».

En effet, la classe abstraite peut fournir des implémentations alors que l’interface ne propose qu’un contrat. Cependant, une classe concrète ne peut hériter que d’une seule classe mais peut implémenter plusieurs interfaces.

La classe abstraite est un peu à mi-chemin entre l’héritage et l’interface.

Les classes partielles

Les classes partielles offrent la possibilité de définir une classe en plusieurs fois. En général, ceci est utilisé pour définir une classe sur plusieurs fichiers, si par exemple la classe devient très longue. Il pourra éventuellement être judicieux de découper la classe en plusieurs fichiers pour regrouper des fonctionnalités qui se ressemblent. On utilise pour cela le mot-clé partial.

Par exemple, si nous avons un fichier qui contient la classe Voiture suivante :

public partial class Voiture
{
    public string Couleur { get; set; }
    public string Marque { get; set; }
    public int Vitesse { get; set; }
}

Nous pourrons compléter sa définition dans un autre fichier pour lui rajouter par exemple des méthodes :

public partial class Voiture
{
    public string Rouler()
    {
        return "Je roule à " + Vitesse + " km/h";
    }
}

À la compilation, Visual Studio Express réunit les deux classes en une seule et l’objet fonctionne comme toute autre classe qui ne serait pas forcément partielle.

À noter qu’il faut impérativement que les deux classes possèdent le mot-clé partial pour que cela soit possible, sinon Visual Studio Express génèrera une erreur de compilation :

Modificateur partiel manquant sur la déclaration de type 'MaPremiereApplication.Voiture' ; une autre déclaration partielle de ce type existe

Vous allez me dire que ce n’est pas super utile comme fonctionnalité. Et je vous dirais que vous avez raison. Sauf dans un cas particulier.

Les classes partielles prennent de l’intérêt quand une partie du code de la classe est généré par Visual Studio Express. C’est le cas pour la plupart des plateformes qui servent à développer des vraies applications. Par exemple ASP.NET pour un site internet, WPF pour une application Windows, Silverlight pour un client riche, etc.
C’est aussi le cas lorsque nous générons de quoi permettre d’accéder à une base de données.

Dans ce cas-là, disons pour simplifier que Visual Studio Express va nous générer tout un tas d’instructions pour nous connecter à la base de données ou pour récupérer des données. Toutes ces instructions seront mises dans une classe partielle que nous pourrons enrichir avec nos besoins.

L’intérêt est que si nous re-générons une nouvelle version de notre classe, seul le fichier généré sera impacté. Si nous avions modifié le fichier pour enrichir la classe avec nos besoins, nous aurions perdu tout notre travail. Vu que grâce aux classes partielles, ce code est situé dans un autre fichier, il n’est donc pas perdu. Pour notre plus grand bonheur !

À noter que le mot-clé partial peut se combiner sans problèmes avec d’autres mots-clés, comme abstract par exemple que nous venons de voir.
Il est fréquent aussi de voir des classes partielles utilisées quand plusieurs développeurs travaillent sur la même classe. Le fait de séparer la classe en deux fichiers permet de travailler sans se marcher dessus.

Nous aurons l’occasion de voir des classes partielles générés dans un autre cours.

Classes statiques et méthodes statiques

Nous avons déjà vu le mot-clé static dans le cours précédent. Il nous a bien encombrés. Nous nous le sommes trimballé pendant un moment, puis il a disparu au début de ce cours !
Il est temps de revenir sur ce mot-clé afin de comprendre exactement de quoi il s’agit, maintenant que nous avons plus de notions et que nous connaissons les objets et les classes.

Jusqu’à présent, nous avons utilisé le mot-clé static uniquement sur les méthodes et j’ai vaguement expliqué qu’il servait à indiquer que la méthode est toujours disponible et prête à être utilisée. Pas très convaincant, comme explication… mais comme vous êtes polis, vous ne m’aviez rien dit. ;)

En fait, le mot-clé static permet d’indiquer que la méthode d’une classe n’appartient pas à une instance de la classe. Nous avons vu que jusqu’à présent, nous devions instancier nos classes avec le mot-clé new pour avoir des objets.

Ici, static permet de ne pas instancier l’objet mais d’avoir accès à cette méthode en dehors de tout objet.
Nous avons déjà utilisé beaucoup de méthodes statiques, je ne sais pas si vous avez fait attention, mais maintenant que vous connaissez les objets, la méthode suivante ne vous paraît pas bizarre ?

Console.WriteLine("Bonjour");

Nous utilisons la méthode WriteLine de la classe Console sans avoir créé d’objet Console. Étrange. o_O

Il s’agit, vous l’aurez deviné, d’une méthode statique qui est accessible en dehors de toute instance de Console. D’ailleurs, la classe entière est une classe statique. Si nous essayons d’instancier la classe Console avec :

Console c = new Console();

Nous aurons les messages d’erreurs suivant :

Impossible de déclarer une variable de type static 'System.Console'
Impossible de créer une instance de la classe static 'System.Console'

Nous avons dit que la méthode spéciale Main() est obligatoirement statique. Cela permet au CLR, qui va exécuter notre application, de ne pas avoir besoin d’instancier la classe Program pour démarrer notre application. Il a juste à appeler la méthode Program.Main() afin de démarrer notre programme.

Comme la méthode Main() est utilisable en dehors de toute instance de classe, elle ne peut appeler que des méthodes statiques. En effet, comment pourrait-elle appeler des méthodes d’un objet alors qu’elle n’en a même pas conscience ?
C’est pour cela que nous avons été obligés de préfixer chacune de nos premières méthodes par le mot-clé static.

Revenons à nos objets. Ils peuvent contenir des méthodes statiques ou des variables statiques. Si une classe ne contient que des choses statiques alors elle peut devenir également statique.

Une méthode statique est donc une méthode qui ne travaille pas avec les membres (variables ou autres) non statiques de sa propre classe.
Rappelez-vous un peu plus haut, nous avions créé une classe Math qui servait à faire des additions, afin d’illustrer le polymorphisme :

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

que nous utilisions de cette façon :

Math math = new Math();
int resultat = math.Addition(5, 6);

Ici, la méthode addition sert à additionner 2 entiers. Elle est complètement indépendante de la classe Math et donc des instances de l’objet Math.
Nous pouvons donc en faire une méthode statique, pour cela il suffira de préfixer du mot-clé static son type de retour :

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

Et nous pourrons alors utiliser l’addition sans créer d’instance de la classe Math, mais simplement en utilisant le nom de la classe suivi du nom de la méthode statique :

int resultat = Math.Addition(5, 6);

Exactement comme nous avons fait pour Console.WriteLine.
De la même façon, nous pouvons rajouter d’autres méthodes, comme la multiplication :

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

    public static long Multiplication(int a, int b)
    {
        return a * b;
    }
}

Que nous appellerons de la même façon :

long resultat = Math.Multiplication(5, 6);

À noter que la classe Mathest toujours instanciable mais qu’il n’est pas possible d’appeler les méthodes qui sont statiques depuis un objet Math. Le code suivant :

Math math = new Math();
long resultat = math.Multiplication(5, 6);

provoquera l’erreur de compilation :

Le membre 'MaPremiereApplication.Math.Multiplication(int, int)' est inaccessible avec une référence d'instance ; qualifiez-le avec un nom de type

Ok, mais à quoi ça sert de pouvoir instancier un objet Math si nous ne pouvons accéder à aucune de ses méthodes, vu qu’elles sont statiques ?

Absolument à rien ! Si une classe ne possède que des membres statiques, alors il est possible de rendre cette classe statique grâce au même mot-clé. Celle-ci deviendra non-instanciable, comme la classe Console :

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

Ainsi, si nous tentons d’instancier l’objet Math, nous aurons une erreur de compilation.
En général, les classes statiques servent à regrouper des méthodes utilitaires qui partagent une même fonctionnalité. Ici, la classe Math permettrait d’y ranger toutes les méthodes du style addition, multiplication, racine carrée, etc …

Ah, on me fait signe que cette classe existe déjà dans le framework .NET et qu’elle s’appelle également Math. Elle est rangée dans l’espace de nom System. Souvenez-vous, nous l’avons utilisée pour calculer la racine carrée. Cette classe est statique, c’est la version aboutie de la classe que nous avons commencé à écrire.

Par contre, ce n’est pas parce qu’une classe possède des méthodes statiques qu’elle est obligatoirement statique. Il est aussi possible d’avoir des membres statiques dans une classe qui possède des membres non statiques.

Par exemple notre classe Chien, qui possède un prénom et qui sait aboyer :

public class Chien
{
    private string prenom;

    public Chien(string prenomDuChien)
    {
        prenom = prenomDuChien;
    }

    public void Aboyer()
    {
        Console.WriteLine("Wouaf ! Je suis " + prenom);
    }
}

pourrait posséder une méthode permettant de calculer l’âge d’un chien dans le référentiel des humains.
Comme beaucoup le savent, il suffit de multiplier l’âge du chien par 7.

public class Chien
{
    private string prenom;

    public Chien(string prenomDuChien)
    {
        prenom = prenomDuChien;
    }

    public void Aboyer()
    {
        Console.WriteLine("Wouaf ! Je suis " + prenom);
    }

    public static int CalculerAge(int ageDuChien)
    {
        return ageDuChien * 7;
    }
}

Ici, la méthode CalculerAge() est statique car elle ne travaille pas directement avec une instance d’un chien.
Nous pouvons l’appeler ainsi :

Chien hina = new Chien("Hina");
hina.Aboyer();
int ageReferentielHomme = Chien.CalculerAge(4);
Console.WriteLine(ageReferentielHomme);

Ce qui donne :

Image utilisateur

Vous me direz qu’il est possible de faire en sorte que la méthode travaille sur une instance d’un objet Chien, ce qui serait peut-être plus judicieux ici. Il suffirait de rajouter une propriété Age au Chien et de transformer la méthode pour qu’elle ne soit plus statique. Ce qui donnerait :

public class Chien
{
    private string prenom;

    public int Age { get; set; }

    public Chien(string prenomDuChien)
    {
        prenom = prenomDuChien;
    }

    public void Aboyer()
    {
        Console.WriteLine("Wouaf ! Je suis " + prenom);
    }

    public int CalculerAge()
    {
        return Age * 7;
    }
}

Que l’on pourrait appeler de cette façon :

Chien hina = new Chien("Hina") { Age = 5 };
int ageReferentielHomme = hina.CalculerAge();
Console.WriteLine(ageReferentielHomme);

Ici, c’est plus une histoire de conception. C’est à vous de décider, mais sachez que c’est possible.
Il est également possible d’utiliser le mot-clé static avec des propriétés. Imaginions que nous souhaitions avoir un compteur sur le nombre d’instances de la classe Chien. Nous pourrions créer une propriété statique de type entier qui s’incrémente à chaque fois que l’on crée un nouveau chien :

public class Chien
{
    public static int NombreDeChiens { get; set; }

    private string prenom;

    public Chien(string prenomDuChien)
    {
        prenom = prenomDuChien;
        NombreDeChiens++;
    }

    public void Aboyer()
    {
        Console.WriteLine("Wouaf ! Je suis " + prenom);
    }
}

Ici, la propriété NombreDeChiens est statique et est incrémentée à chaque passage dans le constructeur.
Ainsi, si nous créons plusieurs chiens :

Chien chien1 = new Chien("Max");
Chien chien2 = new Chien("Hina");
Chien chien3 = new Chien("Laika");

Console.WriteLine(Chien.NombreDeChiens);

Nous pourrons voir combien de fois nous sommes passés dans le constructeur :

Image utilisateur

Pour une variable statique, cela se passe de la même façon qu'avec les propriétés statiques.

Les classes internes

Les classes internes (nested class en anglais) sont un mécanisme qui permet d’avoir des classes définies à l’intérieur d’autres classes.
Cela peut être utile si vous souhaitez restreindre l’accès d’une classe uniquement à sa classe mère.

Par exemple :

public class Chien
{
    private Coeur coeur = new Coeur();

    public void Mourir()
    {
        coeur.Stop();
    }

    private class Coeur
    {
        public void Stop()
        {
            Console.WriteLine("The end");
        }
    }
}

Ici, la classe Cœur ne peut être utilisée que par la classe Chien car elle est privée. Nous pourrions également mettre la classe en protected afin qu’une classe dérivée de la classe Chien puisse également utiliser la classe Cœur :

public class ChienSamois : Chien
{
    private Coeur autreCoeur = new Coeur();
}

Avec ces niveaux de visibilité, une autre classe comme la classe Chat ne pourra pas se servir de ce cœur.
Si nous mettons la classe Cœur en public ou internal, elle sera utilisable par tout le monde ; comme une classe normale. Dans ce cas, nos chats pourront avoir :

public class Chat
{
    private Chien.Coeur coeur = new Chien.Coeur();

    public void Mourir()
    {
        coeur.Stop();
    }
}

Notez que nous préfixons la classe par le nom de la classe qui contient la classe Cœur.
Dans ce cas, l’intérêt d’utiliser une classe interne est moindre. Cela permet éventuellement de regrouper les classes de manière sémantique, et encore, c’est plutôt le but des espaces de noms.
Voilà pour les classes internes. C’est une fonctionnalité souvent peu utilisée, mais voilà, vous la connaissez désormais :D .

Les types anonymes et le mot clé var

Le mot-clé var est un truc de feignant ! :D
Il permet de demander au compilateur de déduire le type d’une variable au moment où nous la déclarons.

Par exemple le code suivant :

var prenom = "Nicolas";
var age = 30;

est équivalent au code suivant :

string prenom = "Nicolas";
int age = 30;

Le mot-clé var sert à indiquer que nous ne voulons pas nous préoccuper de ce qu’est le type et que c’est au compilateur de le trouver.

Cela implique qu’il faut que la variable soit initialisée (et non nulle) au moment où elle est déclarée afin que le compilateur puisse déduire le type de la variable, en l’occurrence, il devine qu’en lui affectant « Nicolas », il s’agit d’une chaîne de caractères.

Je ne recommande pas l’utilisation de ce mot-clé, car le fait de ne pas mettre le type de la variable fait perdre de la clarté au code. Ici, c’est facile. On déduit facilement nous aussi que le type de prenom est string et que le type de age est int. Mais c’est aussi parce qu’on est trop fort ! :p

Par contre, si jamais la variable est initialisée grâce à une méthode :

var calcul = GetCalcul();

il va falloir aller regarder la définition de la méthode afin de savoir le type de retour et connaitre ainsi le type de la variable. Des contraintes dont on n’a pas besoin alors qu’il est aussi simple d’indiquer le vrai type et que c’est d’autant plus lisible.

Mais alors, pourquoi en parler ?

Parce que ce mot-clé peut servir dans un cas précis, celui des types anonymes. Lorsqu’il est conjointement utilisé avec l’opérateur new, il permet de créer des types anonymes, c'est-à-dire des types dont on n’a pas défini la classe au préalable. Une classe sans nom.

Cette classe est déduite grâce à son initialisation :

var unePersonneAnonyme = new { Prenom = "Nico", Age = 30 };

Ici nous créons une variable qui contient une propriété Prenom et une propriété Age. Forcément, il est impossible de donner un type à cette variable vu qu’elle n’a pas de définition. C’est pour cela qu’on utilise le mot-clé var. On sait juste que la variable unePersonneAnonyme possède deux propriétés, un prénom et un âge. En interne, le compilateur va générer un nom de classe pour ce type anonyme, mais il n’a pas de sens pour nous.

En l’occurrence, si nous écrivons le code suivant où GetType() (hérité de la classe object) renvoie le nom du type :

var unePersonneAnonyme = new { Prenom = "Nico", Age = 30 };
Console.WriteLine(unePersonneAnonyme.GetType());

nous aurons :

Image utilisateur

Ce qui est effectivement sans intérêt pour nous !

Jusqu’ici, les types anonymes peuvent sembler ne pas apporter d’intérêt, tant il est simple de définir une classe Personne possédant une propriété Prenom et une propriété Age. Mais cela permet d’utiliser ces classes comme des classes à usage unique lorsque nous ne souhaitons pas nous encombrer d’un fichier possédant une classe qui va nous servir uniquement à un seul endroit.

Paresse, souci de clarté du code, … tout ceci est un peu mêlé dans la création d’un type anonyme. À vous de l’utiliser quand bon vous semble.
Vous verrez plus tard que les types anonymes sont souvent utilisés dans les méthodes d’extensions Linq. Nous en reparlerons dans un prochain cours.

À noter que lorsque nous créons un tableau, par exemple :

string[] jours = new string[] { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };

il est également possible de l'écrire ainsi :

string[] jours = new[] { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };

c'est-à-dire sans préciser le type du tableau après le new.
Le compilateur déduit le type à partir de l'initialisation. Ce n'est pas un type anonyme en soit, mais le principe de déduction est le même.

En résumé
  • Une interface est un contrat que s'engage à respecter un objet.

  • Il est possible d'implémenter plusieurs interfaces dans une classe.

  • Une classe abstraite est une classe qui possède au moins une méthode ou propriété abstraite. Elle ne peut pas être instanciée.

  • Une classe concrète dérivant d'une classe abstraite est obligée de substituer les membres abstraits.

  • Il est possible de créer des types anonymes grâce à l'opérateur new suivi de la description des propriétés de ce type. Les instances sont manipulées grâce au mot-clé var.

Example of certificate of achievement
Example of certificate of achievement