• Aucun résultat trouvé

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

Ok, je sais maintenant créer des objets, mais je me rappelle qu’au début du tutoriel, 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 :

Code : C#

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 : Code : C#

int a = 10;

Code : C#

System.Int32 a = 10;

System.String chaine = "abc";

En pratique, comme on l’a déjà fait, on utilise plutôt les alias que les classes qu’ils représentent.

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 : Code : C#

int entier = 5;

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

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 : Code : C#

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 :

Code : C#

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

Code : C#

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 :

Code : C#

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 : Code : C#

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

Console.WriteLine();

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

chien.Aboyer();

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 :

Code : C#

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 :

Code : C#

public class Chien : Animal {

public void Aboyer() {

Console.WriteLine("Wouaf !"); }

public void Vieillir() {

age++; }

{

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 rentre 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 :

Code : C#

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 :

Code : C#

chien.estVivant = true;

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

'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é.

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 : Code : C#

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 : Code : C#

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 :

Code : C#

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 !"); }

}

{

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 :

Code : C#

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 :

Code : C#

foreach (Animal animal in animaux) {

animal.Respirer(); }

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é.

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 : Code : C#

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 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 :

Code : C#

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 :

Code : C#

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 :

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

Code : C#

public class Voiture {

private int vitesse;

public Voiture(int vitesseVoiture) {

}

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

public class Voiture {

private int vitesse; public Voiture() {

vitesse = 10; }

public Voiture(int vitesseVoiture) { vitesse = vitesseVoiture; } } Ou encore : Code : C#

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 :

Code : C#

public class Voiture {

private int vitesse; private string couleur; public Voiture()

{

vitesse = 10; }

public Voiture(string couleurVoiture) : this() {

} }

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 : Code : C#

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 :

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 :

Code : C#

static void Main(string[] args) {

ObjetVide monObjetVide = new ObjetVide(); Chien chien = new Chien();

int age = 30;

string prenom = "Nicolas";

}

private static void AfficherRepresentation(object monObjetVide) {

Console.WriteLine(monObjetVide.ToString()); }

Ce qui donne :

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.