7. Classes

En C#, un programme est bâti en définissant de nouveaux types, chacun avec un ensemble de données membres et de fonctions membres.
Dans l'exemple suivant, nous simulons un spationaute se deplaçant sur différentes planètes, en utilisant 3 classes - Planete, Spationaute et Test - pour tester notre simulation.
Tout d'abord, définissons la classe (class) Planete. Par convention, nous définissons les données membres de la classe au sommet de la déclaration de la classe. Il y a 2 données membres ici, les champs nom et gravite, qui stockent le nom et la gravité (constante de l'accélération de la pesanteur) d'une planète. Puis, nous définissons le constructeur pour la planète. Les constructeurs sont des fonctions membres qui vous permettent d'initialiser une instance de classe. Nous initialisons les données membres avec des valeurs qui alimentent les paramètres du constructeur. Enfin, nous définissons 2 fonctions membres supplémentaires qui sont les propriétés qui nous permettent d'obtenir le nom et la gravité d'une planète.
La classe Planete ressemble à cela :
Code Remarques
using System;

namespace Exemples
{
    class Planete
    {
        // champs
        private string nom;
        private double gravite;

        // constructeur
        public Planete(string n, double g)
        {
            nom = n;
            gravite = g;
        }

        // propriétés
        public string Nom
        {
            get { return nom; }
        }
        public double Gravite
        {
            get { return gravite; }
        }
    }
}
Voilà la classe Planete...
Ensuite, nous définissons la classe Spationaute. A l'instar de la classe Planete, nous définissons d'abord nos données membres. Ici, un spationaute a deux champs : la masse du spationaute et la planète courante sur laquelle il se trouve. Ensuite, nous fournissons un constructeur, qui initialise la masse d'un spationaute. Puis, nous définissons une propriété PlaneteCourante qui nous permet d'obtenir ou de définir la planète sur laquelle est notre spationaute. Finalement, nous définissons un méthode Poids qui exprime le poids (en N) du spationaute en utilisant sa masse (en kg) et la constante de l'accélération de la pesanteur (en m/s2) de la planète où il se trouve.
Code Remarques
using System;

namespace Exemples
{
    class Spationaute
    {
        // champs
        private double masse;
        private Planete planeteCourante;

        // constructeur
        public Spationaute(double m)
        {
            masse = m;
        }

        // propriété
        public Planete PlaneteCourante
        {
            get { return planeteCourante; }
            set { planeteCourante = value; }
        }

        // méthode
        public double Poids()
        {
            double poids = 0;
            if (planeteCourante == null)
            {
                Console.WriteLine("On est bien dans l'espace...");
            }
            else
            {
                poids = masse * planeteCourante.Gravite;
                Console.WriteLine("Le poids est de {0} N sur {1}", poids, planeteCourante.Nom);
            }
            return poids;
        }
    }
}
Voilà la classe Spationaute
Enfin, nous définissons la classe Test, qui utilise les classes Planete et Astronaute. Ici, nous créons 2 planètes, terre et lune et un spationaute, rudy. Puis, nous voyons le poids de rudy sur ces planètes.
Code Résultat Remarques
using System;

namespace Exemples
{
    class Spationaute
    {
        // champs
        private double masse;
        private Planete planeteCourante;

        // constructeur
        public Spationaute(double m)
        {
            masse = m;
        }

        // propriété
        public Planete PlaneteCourante
        {
            get { return planeteCourante; }
            set { planeteCourante = value; }
        }

        // méthode
        public double Poids()
        {
            double poids = 0;
            if (planeteCourante == null)
            {
                Console.WriteLine("On est bien dans l'espace...");
            }
            else
            {
                poids = masse * planeteCourante.Gravite;
                Console.WriteLine("Le poids est de {0} N sur {1}", poids, planeteCourante.Nom);
            }
            return poids;
        }
    }
}
Terre : 9,78 m/s²
Lune : 1,622 m/s²
On est bien dans l'espace...
Le poids est de 880,2 N sur Terre
Le poids est de 145,98 N sur Lune
Voilà le résultat de ce petit test...
Si vous sauvegardez ces trois fichiers sous Planete.cs, Spationaute.cs et Test.cs, vous pouvez les compiler en Test.exe avec cette ligne de commande :
csc Test.cs Planete.cs Astronaute.cs
Dans les sections suivantes, nous regarderons chacun des types membres qu'une classe peut avoir, tels que :

7.1. Le mot-clé this

le mot-clé this désigne une variable qui référence une instance de classe ou de structure et qui est seulement accessible aux fonctions membres non statiques de la classe ou de la structure. Le mot-clé this est également utilisé par un constructeur pour appeler un constructeur surchargé (expliqué plus tard) ou déclarer ou accéder aux indexeurs (également expliqué plus loin). Une utilisation commune de la varible this est de distinguer un nom de champ d'un nom de paramètre (lorsqu'ils sont identiques).
Code Résultat Remarques
using System;

namespace Exemples
{
    class Personne
    {
        // champ
        private string nom;

        // constructeur
        public Personne(string nom)
        {
            this.nom = nom;
        }

        // méthode
        public void Presenter(Personne p)
        {
            if (p != this)
            {
                Console.WriteLine("Bonjour, je m'appelle " + nom);
            }
        }
    }
    class MotCleThis
    {
        static void Main(string[] args)
        {
            Personne rudy = new Personne("Rudy");
            Personne eric = new Personne("Eric");
            rudy.Presenter(rudy);
            rudy.Presenter(eric);
            Console.ReadKey();
        }
    }
}
Bonjour, je m'appelle Rudy
Voilà le résultat d'une "auto-présentation" (rien) et de la présentation de Rudy à Eric...

7.2. Champs

Les champs contiennent les données d'une classe ou d'une structure.
Code Résultat Remarques
using System;

namespace Exemples
{
    class MaClasse
    {
        // champs
        public int x;
        public float y = 1, z = 2;
        public static readonly int maxSize = 33;
        public const double pi = 3.14159265358979323846;
        // ...

        // méthodes
        public static void AfficheChampsStatic()
        {
            Console.WriteLine("maxSize = " + maxSize);
            Console.WriteLine("pi = " + MaClasse.pi);
        }
        public void AfficheChamps()
        {
            Console.WriteLine("x = " + x + ", y = " + y + ", z = " + z);
        }
    }
    class Champs
    {
        static void Main(string[] args)
        {
            MaClasse.AfficheChampsStatic();
            MaClasse c = new MaClasse();
            c.AfficheChamps();
            c.x++;
            c.y++;
            c.z++;
            c.AfficheChamps();
            Console.ReadKey();
        }
    }
}
maxSize = 33
pi = 3.14159265358979
x = 0, y = 1, z = 2
x = 1, y = 2, z = 3
Dans cet exemple, on à 4 champs (public) dont un static et readonly. Ce champ static est accessible directement à partir de la classe (et donc même sans instance). On a également une constante...

7.2.1. Le modificateur readonly

Comme son nom le suggère, le modificateur readonly interdit à un champ d'être modifié après avoir été assigné. Un tel champ est nommé un champ en lecture seule. Un champ en lecture seule est toujours évalué au moment de l'exécution, et non au moment de la compilation. Il doit être défini dans sa déclaration ou dans le constructeur du type pour être compilé. D'autre part, les champs en lecture seule produisent un avertissement lorsqu'ils ne sont pas définis.

7.3. Constantes

Une constante (const) est un champ qui est évalué durant la compilation et qui est implicitement statique. La conséquence logique de cela est qu'une constante ne peut pas déférer une évaluation à une méthode ou un constructeur et ne peut être que d'un type standard de C#.
L'avantage d'une constante est que son évaluation se fait lors de la compilation, permettant ainsi une optimisation additionnelle par le compilateur.

7.4. Propriétés

Les propriétés peuvent être caractérisées en champs orientés objet. Elles favorisent l'encapsulation en permettant aux classes ou aux stuctures de contrôler l'accès à leurs données en cachant la representation interne de leurs données.
Code Résultat Remarques
using System;

namespace Exemples
{
    public class Tirelire
    {
        // champ privé
        decimal euros;

        // Propriété
        public int Centimes
        {
            get { return (int)(euros * 100); }
            set { euros = (decimal)value / 100; }
        }
    }
    class Proprietes
    {
        static void Main(string[] args)
        {
            Tirelire t = new Tirelire();
            t.Centimes = 25; // définit (set)
            Console.WriteLine(t.Centimes);
            int x = t.Centimes; // obtient (get)
            Console.WriteLine(x);
            t.Centimes += 50; // obtient et définit (met une pièce dans la tirelire)
            Console.WriteLine(t.Centimes);
            Console.ReadKey();
        }
    }
}
25
25
75
Utilisation des accesseurs get et set...
L'accesseur get retourne une valeur du type de la propriété.
L'accesseur set a un paramètre implicite nommé value qui est du type de la propriété.
Une propriété peut être en lecture seule si seule sa méthode get est spécifiée.
Une propriété peut être en écriture seule si seule sa méthode set est spécifiée.

7.5. Indexeurs

Les indexeurs fournissent un moyen naturel d'indexer des éléments dans une classe ou une structure qui encapsule une collection, via la syntaxe [ ] d'un tableau. Les indexeurs sont similaires aux propriétés, mais ils sont accédés via un index et non via le nom de la propriété. L'index peut avoir n'importe quel nombre de paramètres.
Code Résultat Remarques
using System;

namespace Exemples
{
    public class Resultats
    {
        // 5 juges => 5 notes
        int[] notes = new int[5];

        // indexeur
        public int this[int index]
        {
            get { return notes[index]; }
            set
            {
                if (value >= 0 && value <= 10)
                    notes[index] = value;
            }
        }

        // propriété (lecture seule)
        public double Moyenne
        {
            get
            {
                double somme = 0;
                foreach (int note in notes)
                    somme += note;
                return somme / notes.Length;
            }
        }
    }
    class Indexeurs1
    {
        static void Main(string[] args)
        {
            Resultats r = new Resultats();
            r[0] = 10;
            r[1] = 8;
            r[2] = 7;
            r[3] = r[4] = r[1];
            Console.WriteLine(r.Moyenne);
            Console.ReadKey();
        }
    }
}
8,2
Dans cet exemple, la classe Resultats préserve les notes données par 5 juges. L'indexeur utilise un simple index int pour obtenir ou définir la note particulière donnée par un juge. La moyenne est calculé dans la propriété Moyenne en lecture seule.
Un type peut déclarer de multiples indexeurs qui prennent différents paramètres (ou de multiples paramètres pour des indexeurs multi-dimensionnels).
Code Résultat Remarques
using System;

namespace Exemples
{
    public class Resultats
    {
        // 5 juges
        string[] juges = new string[5];
        // 5 notes
        int[] notes = new int[5];

        // indexeur
        public int this[int index, string juge]
        {
            get { return notes[index]; }
            set
            {
                if (value >= 0 && value <= 10)
                {
                    notes[index] = value;
                    juges[index] = juge;
                }
            }
        }

        // propriété (lecture seule)
        public double Moyenne
        {
            get
            {
                double somme = 0;
                foreach (int note in notes)
                    somme += note;
                return somme / notes.Length;
            }
        }

        // Méthode
        public void Synthese()
        {
            for (int index = 0; index < 5; index++)
                Console.WriteLine(juges[index] + " : " + notes[index]);
            Console.WriteLine("MOYENNE : " + this.Moyenne);
        }
    }
    class Indexeurs2
    {
        static void Main(string[] args)
        {
            Resultats r = new Resultats();
            r[0, "Rudy"] = 10;
            r[1, "Michael"] = 8;
            r[2, "Antoine"] = 7;
            r[3, "Valentin"] = 8;
            r[4, "Eric"] = 8;
            r.Synthese();
            Console.ReadKey();
        }
    }
}
Rudy : 10
Michael : 8
Antoine : 7
Valentin : 8
Eric : 8
MOYENNE : 8,2
Dans cet exemple, l'indexeur possède deux paramètres : le numéro du juge (l'index) et son nom.

7.6. Méthodes

Tout le code C# est exécuté dans une méthode ou une forme particulière de méthode. Les constructeurs, destructeurs et opérateurs sont des types spéciaux de méthodes et les propriétés et les indexeurs sont implémentés de manière interne avec les méthodes get et set.

7.6.1. Signatures

Une signature de méthode est caractérisée par le type et le modificateur de chaque paramètre dans sa liste de paramètres. Les modificateurs de paramètres ref et out permettent aux arguments d'être passés par référence plutôt que par valeur. Ces caractéristiques sont désignées en tant que signature de méthode car elles distinguent de manière unique une méthode d'une autre.

7.6.2. Surcharger les méthodes

Un type peut surcharger des méthodes (avoir de multiples méthodes avec le même nom), tant que les signatures sont différentes.
Code Résultat Remarques
using System;

namespace ConsoleApplication2
{
    class Methodes
    {
        public static void Methode(int x)
        {
            Console.WriteLine("int x : " + x);
        }
        public static void Methode(double x)
        {
            Console.WriteLine("double x : " + x);
        }
        public static void Methode(int x, float y)
        {
            Console.WriteLine("int x : " + x + ", float y : " + y);
        }
        public static void Methode(float x, int y)
        {
            Console.WriteLine("float x : " + x + ", int y : " + y);
        }
        public static void Methode(ref int x)
        {
            Console.WriteLine("ref int x : " + x);
        }
    }
    class SurchargerMethodes
    {
        static void Main(string[] args)
        {
            Methodes.Methode(33);
            Methodes.Methode(33D);
            Methodes.Methode(33, 91F);
            Methodes.Methode(33F, 91);
            int i = 33;
            Methodes.Methode(ref i);
            Console.ReadKey();
        }
    }
}
int x : 33
double x : 33
int x : 33, float y : 91
float x : 33, int y : 91
ref int x : 33
Par exemple, ces 5 méthodes peuvent coexister dans le même type...
Attention, le type de retour et le modificateur params ne rentrent pas dans la définition de la signature...
De même, deux signatures identiques ne peuvent coexister si l'un des paramètres a le modificateur ref dans une méthode et out dans l'autre...

7.7. Instances de constructeurs

Les constructeurs permettent d'initialiser le code pour une classe ou une structure. Un constructeur de classe crée d'habord une nouvelle instance de cette classe sur le tas puis exécute l'initialisation, tandis qu'un constructeur de structure exécute seulement l'initialisation.
Contrairement aux méthodes ordinaires, un constructeur possède le même nom que la classe ou la structure et n'a pas de type de retour :
using System;

namespace Exemples
{
    class MaClasse
    {
        // constructeur
        public MaClasse()
        {
            // code d'initialisation
        }
    }
    class Constructeur
    {
        static void Main(string[] args)
        {
            MaClasse c = new MaClasse();
            Console.ReadKey();
        }
    }
}
Une classe ou une structeur peut surcharger les constructeurs et peut appeler l'un de ses constructeurs surchargés avant d'exécuter la méthode en utilisant le mot-clé this :
Code Résultat Remarques
using System;

namespace Exemples
{
    class MaClasse
    {
        // champ
        public int x;

        // constructeurs
        public MaClasse() : this(33) {}
        public MaClasse(int v)
        {
            x = v;
        }
    }
    class InstanceConstructeurs
    {
        static void Main(string[] args)
        {
            MaClasse c1 = new MaClasse();
            MaClasse c2 = new MaClasse(91);
            Console.WriteLine(c1.x); // affiche 33
            Console.WriteLine(c2.x); // affiche 91
            Console.ReadKey();
        }
    }
}
33
91
Le constructeur sans argument lance le constructeur avec argument (avec comme valeur 33).
Si une classe ne définit aucun constructeur, un constructeur par défaut sans paramètre est alors créé. Une structure ne peut pas définir un constructeur sans paramètre, puisqu'un constructeur qui initialise chaque champ avec une valeur par défaut (soit 0) est toujours implicitement défini.

7.7.1. Ordre d'initialisation des champs

Une autre manière pratique d'initialiser les champs est de leur assigner une valeur initiale dans leur déclaration :
Code Résultat Remarques
using System;

namespace Exemples
{
    class MaClasse
    {
        // champ
        public int x = 33;
    }
    class OrdreInitialisationChamps
    {
        static void Main(string[] args)
        {
            MaClasse c = new MaClasse();
            Console.WriteLine(c.x); // affiche 33
            Console.ReadKey();
        }
    }
}
33
Le champ x est initialiser à 33...
L'assignation des champs est effectuée avant que le constructeur ne soit exécuté et ils sont assignés dans l'ordre textuel dans lequel ils apparaissent.

7.7.2. Modificateurs d'accès des constructeurs

Une classe ou une structure peut choisir n'importe quel modificateur d'accès pour un constructeur. Il est parfois utile de spécifier un constructeur privé pour prévenir une construction de la classe. Ceci est approprié pour les classes utilitaires qui sont faites entiérement de membres statiques, telle que la classe System.Math.

7.8. Constructeurs statiques

Un constructeur statique permet l'exécution de l'initialisation du code avant que la première instance d'une classe ou d'une structure ne soit créée ou avant que n'importe quel membre statique d'une classe ou d'une structure ne soit accédé. Une classe ou une structure ne peut définir qu'un seul constructeur statique. Il doit être sans paramètre et avoir le même nom que la classe ou la structure :
Code Résultat Remarques
using System;

namespace Exemples
{
    class MaClasse
    {
        // champ
        public static int x = 33;

        // constructeur statique
        static MaClasse()
        {
            Console.WriteLine("MaClasse initialisée");
        }
    }
    class ConstructeursStatiques
    {
        static void Main(string[] args)
        {
            Console.WriteLine(MaClasse.x); // affiche MaClasse initialisée et 33
            Console.ReadKey();
        }
    }
}
MaClasse initialisée
33
Voilà un exemple de constructeur statique. L'accès à MaClasse.x assigne 33 à x et affiche MaClasse initialisée.

7.8.1. Ordre d'initialisation des champs statiques

Chaque assignation de champ statique est effectuée avant qur tout constructeur statique ne soit appelé. Ils sont initialisés dans l'ordre textuel dans lequel ils apparaissent.
Code Résultat Remarques
using System;

namespace Exemples
{
    class MaClasse
    {
        // champ
        public static int x = 33;

        // constructeur statique
        static MaClasse()
        {
            Console.WriteLine("MaClasse initialisée");
        }

        // méthode
        public static void AfficheX()
        {
            Console.WriteLine(MaClasse.x);
        }
    }
    class OrdreInitialisationChampsStatiques
    {
        static void Main(string[] args)
        {
            MaClasse.AfficheX(); // affiche MaClasse initialisée et 33
            Console.ReadKey();
        }
    }
}
MaClasse initialisée
33
Voilà un exemple de constructeur statique. L'accès à MaClasse.AfficheX() assigne 33 à x et affiche MaClasse initialisée.

7.8.2. Non-déterminisme des constructeurs statiques

les constructeurs statiques ne peuvent pas être appelés explicitement et l'environnement d'exécution peut très bien les invoquer avant qu'ils ne soient utilisés la première fois. Les programmes de doivent donc pas faire d'hypothèse quant au départ de l'invocation d'un constructeur statique.
Code Résultat Remarques
using System;

namespace Exemples
{
    class MaClasse1
    {
        // champ
        public static int x = 33;

        // constructeur statique
        static MaClasse1()
        {
            Console.WriteLine("MaClasse1 initialisée");
        }

        // méthode
        public static void AfficheX()
        {
            Console.WriteLine(x);
        }
    }
    class MaClasse2
    {
        // champ
        public static int x = 91;

        // constructeur statique
        static MaClasse2()
        {
            Console.WriteLine("MaClasse2 initialisée");
        }

        // méthode
        public static void AfficheX()
        {
            Console.WriteLine(x);
        }
    }
    class NonDeterminismeConstructeursStatiques
    {
        static void Main(string[] args)
        {
            MaClasse1.AfficheX(); // affiche MaClasse1 initialisée et 33
            MaClasse2.AfficheX(); // affiche MaClasse2 initialisée et 33
            Console.ReadKey();
        }
    }
}
MaClasse1 initialisée
33
MaClasse2 initialisée
91
Dans cet exemple, MaClasse1 initialisée peut être affiché après MaClasse2 initialisée (même si je n'ai pas réussi à rencontrer ce cas après plus d'une dizaine d'essais :-)

7.9. Destructeurs et finaliseurs

Les destructeurs sont des méthodes uniquement de classe qui sont utilisées pour nettoyer les ressources non-mémoire juste avant que le ramasse-miettes ne réclame la mémoire pour un objet :
Code Résultat Remarques
using System;

namespace Exemples
{
    class MaClasse
    {
        // champ
        public int x = 33;

        // constructeur
        public MaClasse()
        {
            Console.WriteLine("MaClasse initialisée");
        }

        // méthode
        public void AfficheX()
        {
            Console.WriteLine(x);
        }

        // destructeur
        ~MaClasse()
        {
            Console.WriteLine("MaClasse detruite");
        }
    }
    class DestructeursFinaliseurs
    {
        static void Main(string[] args)
        {
            MaClasse c = new MaClasse();
            c.AfficheX(); // affiche MaClasse initialisée et 33
            Console.ReadKey();
            // MaClasse detruite s'affiche à la fin du programme...
        }
    }
}
MaClasse initialisée
33
MaClasse detruite s'affiche à la fin du programme...
Le déstructeur est lancé automatiquement par le ramasse-miettes...
Alors qu'un constructeur est appelé lorsqu'un objet est créé, un destructeur est appelé lorsqu'un objet est détruit. La mémoire est automatiquement récupérée par le ramasse-miettes. Ainsi, un destructeur C# est utilisé seulement pour des ressources non-mémoire. Les appels de destructeurs sont non-déterministes. Le ramasse-miettes appelle le destructeur d'un objet lorsqu'il détermine qu'il n'est plus référencé; toute-fois, il peut le détecter après une période de temps indéterminée après que la dernière référence à l'objet ait disparu.

7.10. Types imbriqués

Un type imbriqué est déclaré dans la portée d'un autre type.
Imbriquer un type apporte 3 avantages :
Code Résultat Remarques
using System;

namespace Exemples
{
    class A
    {
        int x = 33; // membre privé
        protected internal class Imbrique
        {
            public void AfficheX()
            {
                A a = new A();
                Console.WriteLine(a.x);
            }
        }
    }
    class B : A
    {
        new public class Imbrique { } // cache le membre hérité
    }
    class TypesImbriques
    {
        static void Main(string[] args)
        {
            A.Imbrique i1 = new A.Imbrique();
            i1.AfficheX(); // affiche 33
            B.Imbrique i2 = new B.Imbrique(); // i2.AfficheX() n'existe pas...
            Console.ReadKey();
        }
    }
}
33
Voici un exemple d'utilisation de type imbriqué...