lundi 24 mars 2014

8 - Les classes

0 commentaires

Présentation

Une classe est tout simplement un moule pour faire des objets.
Un objet est composé de membres ; parmi ces membres, on dispose de champs (les variables qui lui sont caractéristiques), de méthodes, de propriétés, ainsi que d'autres éléments que nous verrons plus tard. On a tendance à croire que "champ" et "membre" désignent la même chose alors que c'est faux : il faut bien voir qu'il existe plusieurs sortes de membres, dont les champs.



C'est dans la classe que sont définis les membres (dont les champs et les méthodes). Tout objet créé à partir d'une classe possède les membres que propose cette classe, vous comprenez donc pourquoi je parle de "moule à objet".
Une classe simple se présente sous cette forme :
class nomDeLaClasse
{
    // Déclaration des champs

    // Déclaration des méthodes
}
Les champs sont de simples variables, vous savez donc les déclarer. ;)

Le constructeur

C'est le nom que l'on donne à une méthode spéciale dans une classe. Le constructeur (c'est aussi un membre) d'une classe est appelé à chaque fois que vous voulez créer un objet à partir de cette classe. Vous pouvez donc écrire du code dans cette méthode et il sera exécuté à chaque création d'un nouvel objet.
Pour filer la métaphore du moule, les objets seraient les gâteaux que l'on peut faire avec et le constructeur serait en quelque sort notre cuisinier.



Lorsqu'il est appelé, le constructeur réserve un emplacement mémoire pour votre objet et si vous n'avez pas initialisé ses champs, il les initialise automatiquement à leur valeur par défaut.
Sachez aussi que vous n'êtes pas obligés d'écrire vous-mêmes le code du constructeur ; dans ce cas, un constructeur "par défaut" est utilisé. Si vous faites ainsi, lorsque l'objet est créé, tous ses champs qui ne sont pas déjà initialisés dans le code de la classe sont initialisés à leur valeur par défaut.
L'intérêt du constructeur est d'offrir au développeur la possibilité de personnaliser ce qui doit se passer au moment de la création d'un objet. Il rajoute en outre un aspect dynamique au code : vous pouvez affecter vos champs à l'aide de variables passées en paramètres.

Le destructeur

Le destructeur (c'est aussi un membre) est une méthode appelée lors de la destruction d'un objet. Son nom est celui de la classe, précédé d'un tilde '~'.
Là-aussi, rien ne vous oblige à mettre un destructeur. C'est seulement si vous voulez faire quelque chose de particulier à sa destruction. Cela peut notamment servir à libérer de la mémoire et à bien gérer les ressources ; c'est bien trop compliqué pour l'instant alors nous en parlerons en temps et en heure.

Exemple

Nous allons étudier une classe Person (qui représente une personne, vous l'aurez deviné :-° ). Dans cet exemple, vous ne comprendrez pas le code à la première lecture ; pas d'affolement, les explications arrivent juste après. ;)
1public class Person
2{
3    private string m_name;
4    public string Name
5    {
6        get { return m_name; }
7        set { m_name = value; }
8    }
9
10    private ushort m_age;
11    public ushort Age
12    {
13        get { return m_age; }
14        set { m_age = value; }
15    }
16
17    public Person()
18    {
19        Console.WriteLine("Nouvelle personne créée.");
20    }
21
22    public Person(string name, ushort age)
23    {
24        this.m_age = age;
25        this.m_name = name;
26        Console.WriteLine("Nouvelle personne créée. Cette personne s'appelle " + name + " et a " + age + " an(s).");
27    }
28
29    ~Person()
30    {
31        Console.WriteLine("Objet détruit.");
32    }
33
34    public void SayHi()
35    {
36        Console.WriteLine("Bonjour ! Je m'appelle " + this.m_name + " et j'ai " + this.m_age + " ans.");
37    }
38}

Je me dois de vous expliquer quelques nouveautés.
Les modificateursprivate et public se mettent devant un type (par exemple : une classe) ou un membre (par exemple : un champ ou une méthode).
private restreint l'accès de ce qui suit à l'usage exclusif dans le bloc où il a été déclaré.
public autorise quant à lui l'accès de ce qui suit depuis l'extérieur.
Le constructeur doit impérativement être précédé de public si vous voulez pouvoir l'appeler et créer un objet.
Par défaut, champs et méthodes utilisent le modificateur private, mais pour bien voir ce que l'on fait, il est préférable de toujours préciser. Nous verrons plus tard qu'il existe d'autres possibilités quepublic pour les classes elles-mêmes, mais ne nous y attardons pas pour l'instant.



Vous pouvez cependant accéder publiquement à des champs privés, en ayant recours à des propriétés, comme dans cette classe avec la propriété Age :
1public ushort Age
2{
3    get { return m_age; }
4    set { m_age = value; }
5}


À l'intérieur du bloc get, vous définissez comment se fait l'accès en lecture. Dans ce cas, si l'on veut récupérer la valeur de l'âge, on pourra écrire ushort userAge = toto.Age; (toto étant un objet de type Person).
À l'intérieur du bloc set, vous définissez comment se fait l'accès en écriture. Dans ce cas, si l'on veut changer la valeur de l'âge, on pourra écrire toto.Age = 10; (10 étant ici implicitement converti en ushort).
Que vient faire value dans tout ça ?
La variable value représente la valeur que l'on veut donner à m_age. Si l'on écrit toto.Age = 10;,value est un ushort qui vaut 10.



Les propriétés ont un statut particulier ; en fait ce ne sont pas des variables mais des moyens d'accéder à des variables. D'ailleurs, get et set sont ce qu'on appelle des accesseurs. Utiliser une propriété en définissant ses accesseurs revient exactement à créer une méthode d'accès en lecture (que l'on peut ici nommer GetAge) et une méthode d'accès en écriture (que l'on peut ici nommer SetAge) :
1public ushort GetAge()
2{
3    return m_age;
4}
5
6public void SetAge(ushort value)
7{
8    m_age = value;
9}

J'ai sciemment employé le nom "value" pour que vous voyiez comment la variable value est traitée dans le code d'une propriété. Autant j'aurais pu nommer ce paramètre différemment, autant dans un bloc set on est obligé d'utiliser le nom "value" (si on n'avait pas de nom standardisé, le compilateur ne pourrais pas s'en sortir !).
Revenons à l'exemple de classe que je vous ai fourni. J'ai créé deux méthodes portant le même nomPerson. Ce n'est pas une erreur, en fait j'ai surchargé le constructeur.
this est un mot-clef du langage C# qui désigne l'objet lui-même ("this" veut dire "ceci" en anglais). L'écriture this.m_age permet d'accéder au champ m_age de l'objet désigné par this.
this.m_age = age; aura pour effet d'initialiser l'âge de ma nouvelle personne avec l'entier que je passe comme paramètre au constructeur.

Rappelez-vous, en introduisant les variables nous avons évoqué l'existence de variables de type valeuret de variables de type référence. Jusqu'à présent, nous n'avions rencontré que des types valeurs (int,string, etc.). Comme les classes sont des types références, il est temps de s'y intéresser plus en détail.
Qu'est-ce qu'une variable de type référence ?
Une variable de type référence (ou tout simplement, une référence) est une variable dont la valeur est l'adresse d'un emplacement mémoire.
Cet emplacement contient des informations utiles à notre programme, comme par exemple une instance de classe. Les habitués du C/C++ retrouveront beaucoup de similitudes avec le concept depointeur^^
En quoi cela va-t-il nous servir ?
Un objet est toujours issu d'une classe. On dit que c'est une instance de cette classe. La particularité des instances de classe est qu'elles se baladent toujours quelque part dans la mémoire, plus librement que les autres variables. Nous avons donc besoin d'une référence pour savoir où elles se trouvent et ainsi pouvoir les manipuler. Contrairement aux variables de type valeur qui contiennent une valeur que l'on manipule directement, les références ne font que désigner une instance qui, elle, peut contenir une ou plusieurs valeurs. L'accès à ces valeurs se fait donc indirectement.
Pas de panique, c'est plus simple que ça en a l'air :) Voyons ce que ça donne en pratique avec la classePerson que nous avons définie plus haut.
Pour manipuler une instance de cette classe, je vais devoir faire deux choses :
  1. déclarer une référence qui servira à désigner mon instance ;
  2. créer mon instance, c'est-à-dire instancier ma classe.

1. Déclarer une référence

Une référence est avant tout une variable, et se déclare comme toutes les variables. Son type est le nom de la classe que l'on compte instancier :
1Person toto;

On vient de déclarer une référence, appelée toto, qui est prête à désigner un objet de type Person:)
Quelle est la valeur de cette référence?
Nous n'avons fait que déclarer une référence sans préciser de valeur ; elle a dans ce cas été initialisée à sa valeur par défaut, qui est null pour les références. Ce code est donc équivalent à :
1Person toto = null;

Lorsqu'une référence vaut null, cela signifie qu'elle ne désigne aucune instance. Elle est donc inutilisable. Si vous tentez de vous en servir, vous obtiendrez une erreur à la compilation :
1Person toto;
2// Erreur à la compilation: "Use of unassigned local variable 'toto'".
3toto.SayHi();
Le compilateur vous indique que vous essayez d'utiliser une variable qui n'a pas été assignée. Rappelez-vous, nous avons vu ça dans le chapitre "Les variables", paragraphe "Utiliser des variables".
En revanche, vous pouvez tout à fait déclarer un champ sans l'instancier : le constructeur de la classe se charge tout seul d'instancier à leur valeur par défaut tous les champs qui ne sont pas déjà instanciés. C'est pourquoi dans la classe Person ci-dessus, j'ai pu écrire private string m_name; sans écrireprivate string m_name = string.Empty; ou encore private string m_name = "";.
Petite piqure de rappel sur les valeurs par défaut :
La valeur par défaut de tout type numérique est 0, adapté au type (0.0 pour un float ou un double).
La valeur par défaut d'une chaîne de caractères (string) est null, et non pas la chaîne vide, qui est représentée par string.Empty ou encore "".
La valeur par défaut d'un caractère (char) est '\0'.
La valeur par défaut d'un objet de type référence est null.
Peut-on initialiser une référence avec une adresse mémoire explicite (comme en C/C++) ?
Non, de base c'est impossible en C# ; pour le faire, il faut utiliser le mot-clef unsafe, mais c'est une notion avancée et nous aurons peut-être l'occasion de la rencontrer plus tard. De base, il n'est d'ailleurs pas non plus possible de lire l'adresse contenue dans une référence.
Voyons alors comment assigner une valeur à notre référence.

2. Instancier une classe

Pour instancier la classe Person, et ainsi pouvoir initialiser notre référence avec une nouvelle instance, on utilise le mot-clef new :
1// Déclaration de la référence.
2Person toto;
3// Instanciation de la classe.
4toto = new Person();

Comme pour toute initialisation de variable, on peut fusionner ces deux lignes :
1// Déclaration + instanciation
2Person toto = new Person();

À ce stade nous avons créé une nouvelle instance de la classe Person, que nous pouvons manipuler grâce à la référence toto:)
Lorsque l'opérateur new est utilisé, le constructeur de la classe est appelé. Nous avons vu qu'il pouvait y avoir plusieurs surcharges du constructeur dans une même classe, comme c'est le cas dans la classePerson. La version du constructeur appelée grâce à new est déterminée par les paramètres spécifiés entre les parenthèses.
Jusqu'à présent nous nous sommes contentés d'écrire new Person(), et nous avons ainsi utilisé implicitement le constructeur sans paramètre (on l'appelle constructeur par défaut).
La classe Person possède un autre constructeur qui nous permet de préciser le nom et l'âge de la personne. Profitons-en pour préciser que toto s'appelle Toto et a 10 ans, au moment de créer notre instance :
1Person toto = new Person("Toto", 10);

Il était aussi possible de faire cela en plusieurs temps, comme ceci :
1Person toto = new Person();
2toto.Name = "Toto";
3toto.Age = 10;

Si vous ne comprenez pas tout de suite la syntaxe des deux dernières lignes du bout de code précédent, c'est normal : nous n'avons pas encore vu comment utiliser des objets (ça ne saurait tarder).
Maintenant que nous savons créer des objets, voyons plus en détail comment nous en servir. ;)

Accéder aux membres d'un objet

Un fois l'objet créé, pour accéder à ses membres il suffit de faire suivre le nom de l'objet par un point.
toto.m_age n'est pas accessible car m_age est défini avec private. On peut en revanche accéder à la propriété publique toto.Age et à la méthode publique toto.SayHi.
Voyons ce que donne la méthode SayHi() de notre ami Toto :
1Person toto = new Person("Toto", 10);
2toto.SayHi();
Et nous voyons apparaître comme prévu :
Bonjour ! Je m'appelle Toto et j'ai 10 ans.

Les propriétés

Pourquoi utiliser des propriétés alors que l'on peut utiliser public à la place de private ?
Cela permet de rendre le code plus clair et d'éviter les erreurs. On laisse en général les champs enprivate pour être sûr qu'ils ne seront pas modifiés n'importe comment.
Ensuite, on peut néanmoins vouloir accéder à ces champs. Si tel est le cas, on utilise les propriétés pour contrôler l'accès aux champs. Dans certains langages, on parle d'accesseurs. En C#, les accesseurs sontget et set. Ils sont utilisés au sein d'une propriété.
Je reprends l'exemple ci-dessus :
1private ushort m_age;
2public ushort Age
3{
4    get { return m_age; }
5    set { m_age = value; }
6}

Comme je vous l'ai dit plus haut, get gère l'accès en lecture alors que set gère l'accès en écriture. Si vous ne voulez pas autoriser l'accès en écriture, il suffit de supprimer le set :
1private ushort m_age;
2public ushort Age
3{
4    get { return m_age; }
5}

Le 1er morceau de code peut se simplifier en utilisant les accesseurs auto-implémentés :
1public ushort Age { get; set; }

Dans ce cas, vous n'avez plus besoin de m_age. Le compilateur comprend que vous autorisez l'accès en lecture et en écriture.
1// Erreur du compilateur :
2// 'ConsoleApplication1.Person.Age.get' must declare a body because it is not marked abstract or extern.
3// Automatically implemented properties must define both get and set accessors.
4public ushort Age { get; }
Comment faire si je ne veux pas autoriser l'accès en écriture dans le code simplifié ?
Il suffit de rajouter private devant set pour indiquer que Age n'aura le droit d'être modifié qu'à l'intérieur de la classe :
1public ushort Age { get; private set; }

Dans l'écriture simplifiée, il n'est plus question de m_age. Considérons le code suivant :
1private ushort m_age;
2public ushort Age { get; private set; }

Si je modifie Age, cela ne va pas affecter m_age ! En effet, dans l'écriture simplifiée on ne fait pas de lien entre m_age et Age : ils vivent leur vie chacun de leur côté.

La comparaison d'objets

Vous pouvez comparer des objets de diverses manières suivant ce que vous voulez vérifier.
Peut-on utiliser l'opérateur == ?
Oui et non ; en fait cela dépend de ce que vous voulez savoir. Considérons l'exemple suivant :
1Person p1 = new Person("Toto", 10);
2Person p2 = new Person("Toto", 10);
3Person p3 = p1;
Dans cet exemple, p1 n'est pas égal à p2, mais p1 est égal à p3 :
1Console.WriteLine(p1 == p2 ? "p1 est égal à p2." : "p1 n'est pas égal à p2.");
2Console.WriteLine(p1 == p3 ? "p1 est égal à p3." : "p1 n'est pas égal à p3.");

Résultat :
p1 n'est pas égal à p2.
p1 est égal à p3.
En effet, les références p1 et p2 désignent chacune une instance différente de la classe Person. Ces deux instances sont identiques du point de vue de leurs valeurs, mais elles sont bien distinctes. Ainsi, si je modifie la deuxième instance, cela ne va pas affecter la première :
1Console.WriteLine(p1.Age);
2Console.WriteLine(p2.Age);
3p2.Age = 5;
4Console.WriteLine(p1.Age);
5Console.WriteLine(p2.Age);

Ce code affichera :
10
10
10
5

Par contre, les références p1 et p3 sont identiques et désignent donc la même instance. Si je modifie p3, cela affecte donc p1 :
1Console.WriteLine(p1.Age);
2Console.WriteLine(p3.Age);
3p3.Age = 42;
4Console.WriteLine(p1.Age);
5Console.WriteLine(p3.Age);

Ce code affichera :
10
10
42
42

Des méthodes importantes

Equals

Pour reprendre ce qui vient d'être dit, plutôt que d'utiliser l'opérateur ==, vous pouvez utiliser la méthode Equals (que possède tout objet). La seule différence est que == ne compare que des objets de même type, alors que Equals permet de comparer un objet d'un certain type avec un autre objet d'un autre type.
Quel est l'intérêt de Equals, sachant que de toute façon si deux variables ne sont pas du même type, elles ne peuvent pas être égales ?
Cela sert si, à l'écriture du code, vous ne connaissez pas le type des variables concernées.

ToString

Tout objet possède aussi cette méthode. Par défaut, elle renvoie le type de l'objet en question :
1Console.WriteLine(p1.ToString());

Résultat :
ConsoleApplication1.Person
La méthode ToString peut être modifiée pour retourner un autre résultat. Par exemple ici on pourrait vouloir renvoyer le nom de la personne en question. Cela se fait avec le modificateur override, que nous étudierons plus tard.
Vous devez commencer à mieux comprendre ce que veut dire Programmation Orientée Objet.
Tant mieux, c'est le but ! :lol:

Leave a Reply