.. _ClasseObj: Les classes *********** Une extension de la notion de type ================================== Un **type de données**, ou simplement **type**, désigne la conjugaison de deux aspects fondamentaux de la programmation : 1. un regroupement de données, on parle **d'agrégation**, et, 2. des méthodes pour intialiser ces données et les manipuler. Par exemple le type ``int`` décrit comment stocker un nombre entier signé sur 32 bits (les données) ainsi que les opérateurs communs pour manipulers ces nombes +, \*, - ... (les méthodes). Notez que le type ``int``, n'existe pas en mémoire, c'est la variable de type ``int`` qui va déclencher la réservation d'une case mémoire pour son stockage. Le type ``int`` est unique mais permet la création de plusieurs variables de type ``int``. En dehors des types de base comme les types numériques, il vous est possible de créer ou d'utiliser d'autres types de données à l'intérieur de vos programmes. Ces types plus complexes seront désignés sous le terme de **classes**. Dit autrement, une classe est une description d'un nouveau type de données. Tout comme pour le type ``int``, une classe est unique mais permet de **créer** (ou **instancier**), plusieurs **objets** (ou **instances**). .. attention:: La classe et l'objet sont des concepts liés mais intrinsèquement différents. Par exemple, prenons un objet réel comme une voiture. Le modèle numérique de la voiture décrit sa forme, ses fonctions, comment elle doit être fabriquée. Ce modèle n'est pas une voiture. De la même manière une classe n'est pas un objet. .. figure:: MagrittePipe.jpg :align: center :alt: Ceci n'est pas une pipe. René Magritte "La Trahison des Images" Il y a 1 type pour 0 à n objets créés. A partir d'un plan, plusieurs pièces peuvent être créées, ou aucune aussi. Les pièces ainsi créées partagent les informations décrites dans le plan. Les pièces existent indépendamment. Une fois la pièce A intégrée dans une voiture, elle n'est plus en relation avec la pièce B assemblée dans un avion. Ces deux pièces sont identiques au sens où elles sont deux pièces du même modèle. Cependant elles sont différentes au sens où elles vont vivre indépendamment. Ainsi la pièce B pourra être remplacée car cassée au bout d'un an alors que l'autre pourra vivre dix ans. Les objets peuvent avoir des attributs propres, dans ce cas, ses attributs doivent être décrits dans le plan. Par exemple, un autoradio contient une liste de stations préférées configurées par l'utilisateur. Le modèle de l'autoradio indique combien de canaux préférés peuvent être choisis cependant aucun n'est établi avant que l'utilisateur ne les choisissent. De même, une voiture C3 est éditée suivant plusieurs couleurs de carrosserie et plusieurs types de moteurs. Pourtant une C3 noire (on parle ici d'un objet) et une C3 blanche (on parle toujours d'un objet) reste des voitures du modèle C3. Il en va de même pour une C3 diesel et une C3 essence. .. note:: Nous dirons parfois *un objet cercle* au lieu d'*un objet de type Cercle*. La déclaration d'une nouvelle classe se fait grâce au mot clef **class** suivi d'un identificateur : le nom de la nouvelle classe. Le corp de la classe (son contenu) est placé entre accolades. En général, une classe *A* est déclarée directement dans un fichier portant le même nom *A.cs* (une classe par fichier). Par convention, les identificateur de classe utiliseront également la norme CamelCase avec la premier lettre en majuscule. .. admonition:: Syntaxe .. code-block:: csharp class Identificateur{ // corps de la classe } .. quiz:: qcmClasse1 :title: Instanciation Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse : * :quiz:`{"type":"TF","answer":"F"}` On ne peut instancier qu'un objet à partir d'une classe. * :quiz:`{"type":"TF","answer":"T"}` Une classe décrit un type de données. * :quiz:`{"type":"TF","answer":"T"}` Une classe permet de créer plusieurs objets de même caractéristiques. * :quiz:`{"type":"TF","answer":"F"}` Une classe peut porter plusieurs noms différents. .. note:: Les recommendations de nommage pour les classes sont traitées `ici `_ . Champs et méthodes d’instance ============================= Une variable propre à un objet (la couleur, l’immatriculation de la voiture) est qualifiée de **champ d’instance** (ou **variable d'instance**, ou **attribut** ou **instance field**). Entre deux objets de type *Voiture*, ces paramètres peuvent avoir des valeurs différentes. Une fonction propre à un objet est qualifiée de **méthode d’instance** (**method**). Cette méthode doit être appelée depuis une instance particulière et n'influe que sur le comportement de cet objet : les différentes instances d'une même classe sont indépendantes. Les champs et les méthodes d'instances d'une classe sont appellés les **membres d'instance** (**instance members**) de la classe. Chaque membre d'instance est précédé d'un **qualificatif** de **visibilité** qui donne les droits d'accès depuis *l'extérieur*, c'est-à-dire depuis du code ne se trouvant pas la classe courante (celle que l'on est en train d'écrire). Le qualificatif **public** ouvre l'accès sans restriction. Pour le moment, on utilisera le qualificatif public mais nous reviendrons là-dessus dans le chapitre **Encapsulation**. La déclaration de champs et de méthodes d'instance se fait à l'intérieur de la classe: .. admonition:: Syntaxe .. code-block:: csharp class Identificateur{ // déclaration des champs d'instances public TypeChamp1 identificateurChamp1; public TypeChamp2 identificateurChamp2; ... // déclaration des méthodes d'instances public TypeDeRetour1 IdentificateurMethode1(TypeParametre11 identificateurParametre11, ...) {...} public TypeDeRetour2 IdentificateurMethode2(TypeParametre21 identificateurParametre21, ...) {...} ... } Exemple: .. code-block:: csharp class Voiture{ public int couleur ; public int puissance ; public void Demarrer() { ... } public void Arreter() { ... } } L'ordre de déclaration des membres d'une classe n'a pas d'importance. Par convention, on déclarera les champs d'instance en premier, puis les méthodes. .. quiz:: qcmClasse1 :title: Membres d'instance * :quiz:`{"type":"TF","answer":"T"}` Tous les objets d'une même classe possèdent les mêmes champs et les mêmes méthodes d'instance. * :quiz:`{"type":"TF","answer":"F"}` Tous les champs des objets d'une même classe possèdent des valeurs identiques. * :quiz:`{"type":"TF","answer":"F"}` Lorsque je déclare un champ d'instance, le CLR va réserver de la mémoire pour ce champs au lancement du programme. * :quiz:`{"type":"TF","answer":"T"}` Variable d'instance est un synonyme de champ d'instance. * :quiz:`{"type":"TF","answer":"T"}` Les champs et les méthodes d'instance font partis des membres d'instance d'une classe. * :quiz:`{"type":"TF","answer":"F"}` Une classe doit contenir des champs d'instance. * :quiz:`{"type":"TF","answer":"F"}` Une classe doit contenir des méthodes d'instance. Afin de facilement identifier le rôle d'un membre d'instance, on pourra s'en tenir aux `conventions de nommage `_ données dans le guide de programmation C#. Création d'un objet =================== La **création** d'un objet, ou **instanciation**, est réalisé avec l'opérateur **new** qui retourne une **référence** vers le nouvel objet créé. .. admonition:: Syntaxe .. code-block:: csharp identificateurVariable = new IdentificateurClasse(parametre1, parametre2, ...); Il y a deux actions effectuées : 1. Création d'un objet de type *IdentificateurClasse* en utilisant les paramètres éventuels : c'est la partie à droite du signe égale; 2. Initialisation de la variable *identificateurVariable* avec la référence retournée par l'opérateur ``new``. Exemple: .. code-block:: csharp /* déclaration d'une variable de type Bitmap et initialisation avec la référence vers un nouvel objet de type Bitmap avec les paramètre (400, 200)*/ Bitmap image = new Bitmap(400,200); /* déclaration d'une variable de type Voiture et initialisation avec une référence vers un nouvel objet de type Voiture */ Voiture voiture = new Voiture(); .. attention:: Les classes font parties de la catégorie **type référence**. Cela signifie qu'une variable dont le type correspond à une classe ne contient pas directement un objet mais une référence vers un objet. Ce lien de *référence* est généralement représenté par une flêche et on dit que la variable *référence* l'objet ou *pointe vers* l'objet (en référence à la notion de pointeur en C/C++). Par abus de langage, une variable de catégorie **type référence** sera simplement appelée **référence**. Ainsi, dans l'exemple précédent que ``image`` est une référence de type ``Bitmap``. Considérons le code : .. code-block:: csharp class C{ public int i; public bool b: } ... int v = 2; C c = new C(); La mémoire contient 2 variables, ``v`` et ``c``, et un objet de type ``C``. Elle peut être représentée ainsi: .. figure:: ex1.svg :align: center :width: 30 % Représentation schématique de l'état de la mémoire du programme. En pratique, la mémoire de l'ordinateur est divisée en octets. Chaque octet reçoit un numéro d'identification unique allant de 0 jusqu'à la taille maximale physiquement disponible (la mémoire peut être vue comme un tableau d'octets). Ce numéro est l'adresse en mémoire. L'adresse identifie ainsi un endroit unique dans la mémoire. Pour stocker un objet, le système réserve un bloc de mémoire. Ainsi, un objet de 10 octets peut occuper les cellules contigües 80, 81, 82... 89. La référence va contenir l'adresse de la première cellule soit la valeur 80. Durant les exercices, plutôt que de créer des adresses fictives, il est plus simple de schématiser la référence comme une flêche vers une autre zone de la mémoire. .. quiz:: qcmClasse2 :title: Référence et création d'instances * :quiz:`{"type":"TF","answer":"F"}` Si je crée trois références alors elles désignent trois objets différents. * :quiz:`{"type":"TF","answer":"T"}` Il peut exister plusieurs variables qui référencent un même objet. * :quiz:`{"type":"TF","answer":"F"}` Il existe trois catégories de variables : valeur, référence et objet. * :quiz:`{"type":"TF","answer":"T"}` La déclaration de variable ``A v;`` ou ``A`` est un identificateur de classe ne provoque pas la création d'un objet de type ``A``. * :quiz:`{"type":"TF","answer":"F"}` Pour créer un nouvel objet à partir d'un objet existant, je déclare une variable que j'initialise avec une référence vers cet objet. * :quiz:`{"type":"TF","answer":"F"}` L'instruction ``Voiture v = v;`` est-elle valide ? * :quiz:`{"type":"TF","answer":"T"}` Les instructions ``Voiture v = new Voiture(); v=v;`` sont-elles valides ? Opérateur d'accès ================= L'**accès aux membres d'instances**, champs et méthodes, se fait à travers l'utilisation d'une référence sur l'objet concerné et de **l'opérateur d'accès point (dot)** ``.`` .. admonition:: Syntaxe .. code-block:: csharp // accès à un champ d'instance reference.identificateurChamps // appel d'une methode d'instance reference.IdentificateurMethode(parametre1, parametre2, ...) Exemple: .. code-block:: csharp Voiture referenceVoiture = new Voiture(); referenceVoiture.Demarre(); int p = referenceVoiture.puissance; Une référence qui contient la valeur spéciale ``null`` ne désigne aucun objet. Tenter d'appliquer l'opérateur d'accès ``.`` sur une référence ``null`` est une erreur de programmation qui provoque un crash du programme à l'exécution. Instance courante ----------------- Nous venons de voir que toutes les méthodes d'instance sont appelées à partir d'une référence (à gauche de l'opérateur ``.`` ). Dans la méthode d'instance, cette référence est appelée **instance courante** et est désignée par le mot clé ``this``. L'instance courante est une référence sur l'objet sur lequel on est en train de travailler. Exemple: .. code-block:: csharp :emphasize-lines: 7 class Compteur { public int valeur; public void SetValeur(int nouvelleValeur) { this.valeur = nouvelleValeur; // this.valeur désigne le champ valeur de l'objet sur lequel la méthode SetValeur est appelée } } Compteur compteur = new Compteur(); compteur.SetValeur(10); Un objet de type ``Compteur`` est créé. Cet objet est référencé par la variable ``compteur``. L'opérateur d'accès est appliqué sur la référence ``compteur``. Il permet ainsi d'accéder à la fonction ``SetValeur()`` de l'objet concerné. L'opérateur d'accès fixe l'objet référencé comme instance courante lors de l'exécution de la fonction ``SetValeur()`` (``this`` est égal à ``compteur``). Afin de simplifier l'écriture, on peut accéder aux membres d'instance de l'instance courante sans utiliser la référence ``this`` et l'opérateur ``.``. Si on ne précise pas de référence avec l'opérateur ``.`` devant un nom d'identificateur qui correspond à un membre d'instance, le compilateur ajoutera automatiquement ``this.`` devant l'identificateur. Le code suivant est donc équivalent au précendent: .. code-block:: csharp :emphasize-lines: 7 class Compteur { public int valeur; public void SetValeur(int nouvelleValeur) { valeur = nouvelleValeur; // équivalent à this.valeur = ... } } .. note:: A l'intérieur du code d'une méthode d'instance, l'accès aux membres de l'instance courante peut donc se faire directement sans passer par l'opérateur d'accès. Les normes d'éciture privilégient généralement cette facilité. Une exception notable apparait lorsqu'il est nécessaire de faire la différence entre un champ d'instance et une variable locale ayant le même identificateur. .. code-block:: csharp :emphasize-lines: 5, 7 class Compteur { public int valeur; public void SetValeur(int valeur) // le paramètre à le même nom que le champ { this.valeur = valeur; // this.valeur désigne le champ d'instance, alors que valeur désigne la variable locale } } Indépendance entre objets ------------------------- Si l’on crée deux instances de la classe ``Compteur`` : .. code-block:: csharp Compteur c1 = new Compteur(); Compteur c2 = new Compteur(); Deux zones mémoires sont réservées pour stocker chaque instance. Deux références ``c1`` et ``c2`` sont créées et associées à chaque objet. On peut représenter la mémoire de la façon suivante : .. image:: ex2a.svg :align: center :width: 30 % Si on exécute l'instruction: .. code-block:: csharp c1.SetValeur(10); L'opérateur d'accès ``.`` appliqué sur la référence ``c1`` m'indique que l'instance courante ``this`` va être l'objet référencé par ``c1``. Ainsi, c'est le champ ``valeur`` de cet objet qui va être modifié et changé pour la valeur 10. Le 2ème objet ne subit aucune modification et la mémoire devient: .. image:: ex2b.svg :align: center :width: 30 % Exercices --------- .. quiz:: qcmClasse3 :title: Membres d'instances * :quiz:`{"type":"TF","answer":"T"}` En modifiant une variable ``v`` de type référence, je ne modifie pas l'objet référencé par ``v``. * :quiz:`{"type":"TF","answer":"F"}` Si je change la valeur d'un champ d'instance, l'objet n'est plus considéré comme appartenant à sa classe d'origine. * :quiz:`{"type":"TF","answer":"F"}` Une fois l'objet créé et ses variables d'instance initialisées, je ne peux plus les modifier. * :quiz:`{"type":"TF","answer":"F"}` Je peux accéder directement à un objet sans passer par une référence. * :quiz:`{"type":"TF","answer":"T"}` Si j'appelle une méthode depuis une référence et que cette référence a pour valeur ``null`` alors le programme plante. * :quiz:`{"type":"TF","answer":"F"}` Soient deux références ``r1`` et ``r2`` qui désignent le même objet, si je change ``r1`` pour qu'elle réfère un autre objet, alors ``r2`` désigne aussi ce nouvel objet. * :quiz:`{"type":"TF","answer":"T"}` Si aucune référence ne désigne un objet, alors je ne peux pas accéder aux attributs de cet objet. * :quiz:`{"type":"TF","answer":"T"}` Dans une méthode d'instance, ``this`` est toujours une référence vers un objet de la classe courante. * :quiz:`{"type":"TF","answer":"T"}` Dans une méthode d'instance, ``this.v`` et ``v`` peuvent désigner 2 choses différentes. Les constructeurs ================= Chaque classe contient un ou plusieurs **constructeurs**. Cette fonction particulière est appelée lors de la création d'un objet par l'opérateur ``new``. Le but du constructeur est d'initialiser les champs d'instances de l'objet. Un constructeur est déclaré comme une méthode sans type de retour et dont le nom est identique à celui de la classe : .. admonition:: Syntaxe .. code-block:: csharp public IdentificateurDeClasse(TypeParametre1 identificateurParametre1, TypeParametre2 identificateurParametre2, ...) {...} Exemple : .. code-block:: csharp class Cercle { public int x; public int y; public Cercle() // constructeur sans argument { x = 10; y = 10; } public Cercle(int x, int y) // constructeur paramétrique { this.x = x ; this.y = y ; } } Une même classe peut avoir plusieurs constructeurs qui doivent alors avoir des paramètres différents. .. attention:: Si une classe ne contient aucun constructeur, le compilateur ajoutera automatiquement un constructeur sans argument. Etapes de construction d'un objet --------------------------------- Avant l'appel au constructeur, le programme initialise de manière implicite tous les champs de l'instance en exécutant les initialiseurs de variables. Ces initialiseurs sont **toujours** exécutés en premier lors de l'instanciation. Ainsi les champs d'instances sont initialisés avant l'exécution du constructeur. Il n'est donc pas possible d'avoir un objet dont un champ n'aurait pas été initialisé. Voici les valeurs par défaut : * Numérique : ``0`` * Booléen : ``false`` * Référence : ``null`` (la référence ne désigne aucun objet) Voici l'ordre des opérations effectuées par l'opérateur ``new``: 1. Réservation d'une zone physique en mémoire pour stocker le nouvel objet, #. Initialisation des champs d'instance, #. Appel d'un constructeur : constructeur sans argument ou constructeur paramétrique, #. Retour d'une référence vers l'objet construit. Chaînage des constructeurs -------------------------- Lorsqu'une classe contient plusieurs constructeurs, le contenu de ces derniers est souvent très proche. Par exemple dans le cas de la classe ``Cercle``, les 2 constructeurs sont identiques à l'exceptions des valeurs à droite des signes égals. Pour éviter ce genre de duplication de code (copier/coller de portion de code), il faut chaîner les constructeurs, c'est à dire appeler un constructeur à partir d'un autre constructeur. En C#, le chaînage se fait lors de la déclaration du constructeur avec le mot clé ``this``: .. admonition:: Syntaxe .. code-block:: csharp public IdentificateurDeClasse(TypeParametre1 identificateurParametre1, TypeParametre2 identificateurParametre2, ...) : this(parametre1, parametre2, ...) {...} Pour la classe ``Cercle``, cela donne: .. code-block:: csharp :emphasize-lines: 6,7 class Cercle { public int x; public int y; public Cercle() : this(0, 0) // Le constructeur sans argument est chaîné avec le constructeur paramétrique { } // tout est fait par l'autre constructeur public Cercle(int x, int y) // constructeur paramétrique { this.x = x ; this.y = y ; } } .. note:: Eviter la duplication de code est un principe important de programmation qui facilite la maintenance et permet d'éviter des erreurs. On parle de `DRY principle `_ pour *Don't Repeat Yourself* en anglais. Exercices --------- .. quiz:: qcmClasse4 :title: Constructeurs * :quiz:`{"type":"TF","answer":"T"}` Une classe peut avoir plusieurs constructeurs. * :quiz:`{"type":"TF","answer":"T"}` Une classe a toujours un constructeur. * :quiz:`{"type":"TF","answer":"F"}` Un constructeur peut avoir un type de retour s'il s'agit d'une référence. * :quiz:`{"type":"TF","answer":"T"}` L'opérateur ``return`` ne peut pas être utilisé dans un constructeur. * :quiz:`{"type":"TF","answer":"F"}` Si le développeur ne déclare pas de constructeur sans argument alors le compilateur en ajoute un automatiquement. * :quiz:`{"type":"TF","answer":"F"}` Lors de l'appel à l'opérateur ``new``, le constructeur sans argument est toujours appelé. * :quiz:`{"type":"TF","answer":"T"}` Lors de la création d'un objet, les champs d'instance sont initialisés avant la 1ère instruction du constructeur. * :quiz:`{"type":"TF","answer":"F"}` A la déclaration d'un constructeur, celui-ci est implicitement chainé avec le constructeur sans argument. * :quiz:`{"type":"TF","answer":"F"}` Je peux réinitialiser un objet grâce au constructeur. * :quiz:`{"type":"TF","answer":"F"}` La référence vers l'objet courant ``this`` n'existe pas dans un constructeur. * :quiz:`{"type":"TF","answer":"T"}` Un constructeur doit avoir le même nom que la classe où il est déclaré. * :quiz:`{"type":"TF","answer":"F"}` On doit initialiser la valeur des champs dans un constructeur. .. quiz:: qcmClasse5 :title: Constructeurs Soit la classe: .. code-block:: csharp class A{ public int b; public string a; public A(int b){ this.b = b; } Quelles instructions sont correctes : * :quiz:`{"type":"TF","answer":"T"}` ``A a;`` * :quiz:`{"type":"TF","answer":"F"}` ``A a = new A();`` * :quiz:`{"type":"TF","answer":"T"}` ``A a = new A(2);`` * :quiz:`{"type":"TF","answer":"F"}` ``A a = new A(2,"toto");`` .. quiz:: classeConstructeurs1 :title: Constructeur Soit la classe ``A``: .. code-block:: csharp class A{ public int i; public bool b; public A r; } Combien la classe ``A`` possède-t-elle de constructeurs ? :quiz:`{"type":"FB","answer":"1"}` Soit l'objet créé par l'instruction : ``new A()``, donnez la valeur de ces 3 attributs: * ``i`` :quiz:`{"type":"FB","answer":"0"}` * ``b`` :quiz:`{"type":"FB","answer":"false"}` * ``r`` :quiz:`{"type":"FB","answer":"null"}` .. quiz:: classeConstructeursChaine :title: Constructeurs chainés Soit la classe ``B``: .. code-block:: csharp class B{ int i; double d; bool b; public B(int i, double d) { this.b = (b == false); this.i = i; this.d = d; } public B(int i) : this(i, i*2.0) {} } Combien la classe ``B`` possède-t-elle de constructeurs ? :quiz:`{"type":"FB","answer":"2"}` Soit l'objet créé par l'instruction : ``new B(1)``, donnez la valeur de ces 3 attributs: * ``i`` :quiz:`{"type":"FB","answer":"1"}` * ``d`` :quiz:`{"type":"FB","answer":"2"}` * ``b`` :quiz:`{"type":"FB","answer":"true"}` Encapsulation ============= L'encapsulation consiste à *cacher* le fonctionnement interne d'un composant pour ne laisser apparaitre que les éléments utils à l'utilisateur. Pour reprendre l'exemple de la voiture, un conducteur est intéressé par certaines fonctions comme *démarrer la voiture*, *accélerer*, *freiner*... Le fait de savoir qu'en appuyant sur la pédale de frein, cela va directement comprimer le liquide du circuit de freinage ou bien enclencher un compresseur auxiliaire n'a pas d'importance de son point de vue. En fait il est même probablement souhaitable que l'utilisateur ne puisse pas interférer avec ces éléments de mécanique interne. L'utilisateur se voit donc présenter une méthode simple pour freiner (appuyer sur la pédale de frein) et les détails de fonctionnement du mécanisme de freinage sont cachés/encapsulés sous le capot. En programmation, l'encapsulation va consister à choisir les éléments que l'on laisse apparent pour l'utilisateur, on parle de **l'interface**, et ceux que l'on cache. Les principaux avantages de cette approche sont : * L'utilisabilité : on présente à l'utilisateur une interface claire et épurée qui ne contient que des fonctions utiles à son niveau. * La souplesse : les éléments de fonctionnement interne étant cachés, on peut les faire évoluer sans toucher à l'interface présentée à l'utilisateur. * La fiabilité : en empêchant l'utilisateur d'interférer avec le fonctionnement interne du programme, on l'empêche également de mal l'utiliser. * La clareté du code : en se forçant à identifier les éléments de l'interface et les éléments de fonctionnement interne, on arrive généralement à un code mieux structuré et de meilleure qualité. Visibilité ---------- En programmation orientée objet, le principal mécanisme de l'encapsulation repose sur les **qualificatifs de visibilité**. Ces qualificatifs permettent de contrôler si une méthode ou un champ peut être utilisé par une autre classe. Nous avons déjà vu le qualitificatif **public** qui signifie qu'un membre est accessible sans restriction à tout le monde (il fait parti de l'interface de l'objet). L'opposé du qualificatif public est le qualitificatif **private** qui signifie qu'un membre n'est accessible que depuis la classe courante, donc caché au monde extérieur. Essayer d'accéder à un membre privé depuis une autre classe génère une erreur de compilation. Par défaut en C#, un membre dont la visibilité n'est pas précisée est privé. Par exemple: .. code-block:: csharp class A{ public int champPublic = 1; private int champPrive = 2; public void MethodePublique(){} private void MethodePrivee(){} public void Test() { A a = new A(); champPublic++; champPrive++; // ok car nous sommes dans la classe A a.champPrive++; // on peut accéder aux membres prives d'un autre objet de type A MethodePublique(); MethodePrivee(); // ok car nous sommes dans la classe A a.MethodePrivee(); // on peut accéder aux membres prives d'un autre objet de type A } } class B{ public void Test() { A a = new A(); a.champPublic++; // ok car champPublic est public // a.champPrive++; ERREUR car nous sommes dans la classe B a.MethodePublique(); // a.MethodePrivee(); ERREUR car nous sommes dans la classe B } } .. attention:: Le mot clé **private** n'est pas un élément de sécurité: c'est un guide *fort* pour les développeurs mais un utilisateur mal intentionné pourra toujours le contourner ! .. quiz:: classeobjet-proprieteqcm :title: Visibilité Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: * :quiz:`{"type":"TF","answer":"T"}` Les membres d'une classe dont la visibilité n'est pas précisée sont considérés comme privés. * :quiz:`{"type":"TF","answer":"F"}` Les seuls membres privés auxquels je peux accéder sont ceux de l'instance courante **this**. * :quiz:`{"type":"TF","answer":"F"}` Une classe ``B`` peut accéder aux membres privés d'une classe ``A`` si elles sont déclarées dans le même fichier. * :quiz:`{"type":"TF","answer":"F"}` Dans une même classe, je ne peux pas accéder à un membre privé depuis un membre public. * :quiz:`{"type":"TF","answer":"T"}` Les règles de visibilité sont vérifiées à la compilation. Les propriétés --------------- La problématique qui nous intéresse ici concerne l'intégrité des données. Par exemple, dans un programme de gestion d'un magasin en ligne, on a une classe ``Article`` avec un champ ``prix``. Comment peut-on faire pour assurer que le prix d'un article est toujours strictement positif ? Si on déclare le champ *prix* public alors une autre personne pourrait, par inadvertance ou par malhonnêteté, assigner un prix négatif à un objet. La solution adoptée dans certains langage (Java/C++) est de déclarer le champ privé avec le mot clé ``private`` et de déclarer des méthodes dites **accesseurs** : * **getter** pour récupérer la valeur du champ, et * **setter** pour modifier sa valeur. .. code-block:: csharp class Article{ private double prix=10; // champ privé public void SetPrix(double nouveauPrix){ // Setter if(nouveauPrix>0) // contrôle de validité prix = nouveauPrix; } public double GetPrix(){ // Getter return prix; // pas de contrôle ici } } ... Article a = new Article(); a.SetPrix(20); // Passage par le setter obligatoire WriteLine("Prix de l'article : " + a.GetPrix()); // Passage par le getter obligatoire a.SetPrix(-10); // Sans effet grâce au contrôle effectué par le setter L'approche par accesseurs comportent des inconvénients: * cela alourdit le code, * la maintenance est compliquée car il est difficile de transformer un champ public en un champ privé avec accesseurs dans un programme (le champ public fait parti de l'interface du programme: toute modification sur l'interface peut *casser* les programmes qui utilise cette classe), * le nommage des accesseurs n'est pas parfaitement standardisé. La solution proposée en C# sont les **propriétés**. Une propriété est une nouvelle catégorie de membre qui s'utilise comme un champ mais génère automatiquement des appels vers des fonctions **accesseurs**. Avec une propriété, l'exemple de la classe ``Article`` s'écrit : .. code-block:: csharp class Article{ double prix ; // champ privé qui contient le prix de base // début de la définition de la propriété public double Prix { get { // définition du getter return prix; } set { // définition du setter if(value>0) // value est un mot clé qui désigne le paramètre du setter prix = value; } } // fin de la définition de la propriété } ... Article a = new Article(); a.Prix = 20; // génère un appel à set avec value=20 WriteLine("Prix de l'article : " + a.Prix); // génère un appel à get La syntaxe générale pour déclarer une proriété est la suivante: .. admonition:: Syntaxe .. code-block:: csharp Visibilite TypeDeDonnees IdentificateurDePropriete { get { ... return valeurDeLaPropriete; } set { ... = value; } } Notez que l'on peut déclarer une propriété sans getter ou sans setter. Par convention, on donnera toujours le même nom à la propriété et au champ privé qu’elle cache, en faisant commencer le nom du champ par une minuscule et celui de la propriété par une majuscule. Les propriétés allègent le code et résolvent les problèmes de nommage, mais elles opacifient le code avec des appels de méthodes implicites. En générale les propriétés ont une visibilité publique, leur but étant de fournir une interface vers l’extérieur de la classe. Néanmoins, on peut modifier individuellement la visibilité des accesseurs. Par exemple, pour avoir une propriété en lecture/écriture pour la classe mais en lecture seule pour les autres classes : .. code-block:: csharp private double prix ; public double Prix { get {...} // getter public private set {...} // setter privé } Dans le cas ou une propriété n'a rien d’autre à faire que de retourner ou affecter directement la valeur d'un champ (sans modification, ni contrôle), on peut utiliser une forme simplifiée dite **propriété auto-implémentée**. Ainsi la définition: .. code-block:: csharp public double Prix { get ; private set ; } // propriété auto-implémentée avec modificateur privé est équivalente à (à la différence qu'aucun champ ``prix`` n'est accessible même en privé): .. code-block:: csharp private double prix ; public double Prix { get { return prix ; } private set { prix = value ; } } .. note:: Il n'est pas toujours évident de choisir le bon type de membre pour ajouter une nouvelle fonctionnalité dans une classe : attribut ? méthode ? propriété ? constructeur ?... Les `recommendation de conception `_ du guide de programmation C# peuvent vous aider à choisir l'approche la plus appropriée. .. quiz:: classeobjet-proprieteqcm :title: Propriétés Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: * :quiz:`{"type":"TF","answer":"T"}` Une propriété peut être de visibilité ``public``. * :quiz:`{"type":"TF","answer":"T"}` Une propriété peut être de visibilité ``private``. * :quiz:`{"type":"TF","answer":"F"}` Une propriété est toujours associée à un champ. * :quiz:`{"type":"TF","answer":"F"}` Une propriété doit posséder un ``get``. * :quiz:`{"type":"TF","answer":"F"}` Une propriété doit posséder un ``set``. * :quiz:`{"type":"TF","answer":"T"}` Dans le setter d'une propriété, le mot clef ``value`` désigne la nouvelle valeur à affecter. * :quiz:`{"type":"TF","answer":"F"}` Dans le getter d'une propriété, il n'est pas obligatoire de retourner une valeur. * :quiz:`{"type":"TF","answer":"F"}` Dans le getter d'une propriété, je n'ai pas le droit de modifier les valeurs des champs de la classe. .. quiz:: classeobjet-proprieteqcm :title: Utilisation des propriétés Une classe ``A`` contient le membre suivant : ``public string Name {get; set;}``. Pour chacune de ces instructions, qu'on suppose écrite dans une méthode de la classe ``A``, indiquez si elle est correcte ou non: * :quiz:`{"type":"TF","answer":"F"}` ``Name.set("foo");`` * :quiz:`{"type":"TF","answer":"F"}` ``Name.Set = "foo";`` * :quiz:`{"type":"TF","answer":"F"}` ``Name.set = "foo";`` * :quiz:`{"type":"TF","answer":"T"}` ``Name = "foo";`` * :quiz:`{"type":"TF","answer":"F"}` ``SetName("foo");`` * :quiz:`{"type":"TF","answer":"F"}` ``set("foo");`` .. quiz:: classeobjet-proprieteqcm :title: Effet de bord Une classe possède une propriété ``P``, si je lis 2 fois de suite la valeur de cette propriété, j'obtiendrai : * :quiz:`{"type":"TF","answer":"F"}` deux fois la même valeur * :quiz:`{"type":"TF","answer":"F"}` deux valeurs différentes * :quiz:`{"type":"TF","answer":"T"}` on ne peut pas savoir Membres statiques ================= Le qualificateur ``static`` permet de déclarer des **membres de classe**, aussi appelés **membres statiques**, qui ne dépendront pas d'une instance particulière de la classe. Par exemple la fonction racine carrée, la constante :math:`\pi`, la fonction ``Main`` (première fonction exécutée du programme) ne dépendent pas d'un objet particulier; elles existent même si aucun objet n'est créé. .. code-block:: csharp class A{ public static int x; // champ statique public static void Bar(){}; // méthode statique public static double UnNombre {get; set;} // propriétée statique } Les champs de classe sont initialisés avant la première utilisation de la classe où ils sont définis. Les membres de classes peuvent être appelés n'importe quand et n'importe où dans votre programme même si aucun objet du type concerné n'a été créé. Pour accéder à un champ statique ou à une méthode statique, on utilise l'opérateur d'accès ``.`` préfixé par le nom de la classe (contrairement aux membres d'instance que l'on préfixe par la référence sur un objet). Lors de l'appel à une méthode statique il n'y a donc pas de référence vers une instance courante : le mot clef ``this`` n'est pas défini. Il n'est donc pas possible d'accéder aux membres d'instances. Un exemple classique d'utilisation d'un champ statique est la définition d'un compteur d'instances créées pour un classe: .. code-block:: csharp :emphasize-lines: 4,8 class A { double valeur; public static int compteur = 0; // nombre d'objets de type A créés public A(double valeur){ this.valeur = valeur; A.compteur++; // on vient de créer un nouvel objet } public static void Test() { // une méthode statique A a1 = new A(3.1); WriteLine(A.compteur); // 1 A a2 = new A(3.2); WriteLine(A.compteur); // 2 WriteLine(a1.valeur); // 3.1 accès à un champ d'instance à partir d'une référence //WriteLine(this.valeur); ERREUR la référence this n'est pas défini dans une méthode statique //WriteLine(a1.compteur); ERREUR accès à partir une référence à un champ statique } } A la fin de l'exécution de la méthode ``Test``, on peut représenter la mémoire de la façon suivante : .. image:: exStatic.svg :align: center :width: 30 % Afin de simplifier l'écriture, on peut accéder aux membres statiques de la classe courrante sans utiliser le nom de la classe et l'opérateur ``.``. Autrement dit, si on ne précise pas de nom de classe avec l'opérateur ``.`` devant un nom d'identificateur qui correspond à un membre statique, le compilateur ajoutera automatiquement ``NomDeLaClasse.`` devant l'identificateur. Le code suivant est donc valide: .. code-block:: csharp :emphasize-lines: 3,7 class A { public static int valeur; public void Test() { valeur = 3; // équivalent à A.valeur = 3 } } .. note:: Le mot **statique** est associé à tout ce qui est calculé au moment de la compilation. Son opposé est le mot **dynamique** qui désigne ce qui est calculé pendant l'exécution du programme. .. quiz:: classeobjet-staticExecution :title: Human VM .. code-block:: csharp class A{ int i = 3; static int si = 2; void M(){ i *= 2; } void static SM(){ si *= 2; } void Test() { M(); SM(); WriteLine(i); WriteLine(si); } static void Main(string [] args) { A a1 = new A(); A a2 = new A(); a1.Test(); a2.Test(); } } Après exécution de la méthode ``Main``, ce programme a affiché les 4 lignes de texte suivantes : 1) :quiz:`{"type":"FB","answer":"6"}` #) :quiz:`{"type":"FB","answer":"4"}` #) :quiz:`{"type":"FB","answer":"6"}` #) :quiz:`{"type":"FB","answer":"8"}` .. quiz:: classeobjet-staticqcm :title: Membres statiques Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: 1) :quiz:`{"type":"TF","answer":"T"}` Une classe peut contenir plusieurs membres statiques. #) :quiz:`{"type":"TF","answer":"T"}` On peut accéder à un membre statique d'une classe sans avoir créé d'objet de cette classe. #) :quiz:`{"type":"TF","answer":"F"}` Une méthode statique peut accéder à un champ d'instance à travers la référence de l'objet courant ``this``. #) :quiz:`{"type":"TF","answer":"F"}` Une méthode d'instance peut accéder à un champ statique à travers la référence de l'objet courant ``this``. #) :quiz:`{"type":"TF","answer":"F"}` On peut initialiser un champ statique en utilisant les valeurs des champs d'instances. #) :quiz:`{"type":"TF","answer":"T"}` On peut initialiser un champ d'instance en utilisant les valeurs des champs statiques. .. quiz:: classeobjet-staticvocabulaire :title: Catégories de variables .. code-block:: csharp class B{ static double a; int b; void F(){ float c; } } Pour chacune des variables ``a``, ``b`` et ``c`` dites si il s'agit d'une variable d'instance et/ou de classe et/ou locale. .. csv-table:: :header: "Variable", "Instance", "Classe", "Locale" :widths: 10, 10, 10, 10 :delim: ! ``a`` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"F"}` ``b`` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"TF","answer":"F"}` ``c`` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"TF","answer":"T"}` .. quiz:: classeobjet-staticacceschamps :title: Statique et règles d'accès aux champs .. code-block:: csharp :linenos: class A{ public static int a; public int b; } class B{ public static void Main(){ A.a++; A.b++; A obj= new A(); obj.a++; obj.b++; } } * :quiz:`{"type":"TF","answer":"T"}` La ligne 8 est correcte. * :quiz:`{"type":"TF","answer":"F"}` La ligne 9 est correcte. * :quiz:`{"type":"TF","answer":"F"}` La ligne 11 est correcte. * :quiz:`{"type":"TF","answer":"T"}` La ligne 12 est correcte. .. quiz:: classeobjet-staticaccesmethodes :title: Statique et règles d'accès aux champs .. code-block:: csharp :linenos: class A{ static int a; int b; public void F1(){ a++; b++; } public static void F2(){ a++; b++; } } * :quiz:`{"type":"TF","answer":"T"}` La ligne 6 est correcte. * :quiz:`{"type":"TF","answer":"T"}` La ligne 7 est correcte. * :quiz:`{"type":"TF","answer":"T"}` La ligne 11 est correcte. * :quiz:`{"type":"TF","answer":"F"}` La ligne 12 est correcte. Exercices ========= .. quiz:: humanVMClass1 :title: Human VM Soit la classe ``C`` et la fonction ``Test`` .. code-block:: csharp class C { public int a, b; public C bb; public C(int _a, int _b) { a=_a; b=_b; } } ... Test() { C[] T = new C[5]; for (int i = 0 ; i < 5 ; i++) { T[i] = new C(i,i); i++; } } Dessinez l'état de la mémoire à la fin de la fonction ``Test`` (un papier et un crayon reste la façon la plus simple de dessiner !) .. quiz:: maPremiereClasse :title: Création d'une classe Sous Visual Studio, créez un nouveau projet de type Console: 1) Créez une classe ``Rectangle``: clique droit sur le nom du projet dans l'explorateur de solution, menu *Ajouter -> Classe*, renseignez le nom de la classe et cliquez sur OK. .. image:: creerClasse.png :align: center :scale: 50% #) Ajoutez des attributs à la classe : sa largeur ``largeur``, sa hauteur ``hauteur`` en pixel et sa position à l’écran ``(positionX,positionY)`` en pixel. #) Ajoutez un constructeur paramétrique qui initialise la taille et la position de l'objet. #) Ajoutez une méthode d'instance ``Aire`` (sans argument) qui retourne l’aire du rectangle. #) Dans la méthode ``Main`` de la classe ``Program`` : A) Créez une nouvelle instance de la classe ``Rectangle`` de dimension 20 par 30 à la position (15,20). #) Affichez les valeurs des attributs de l'objet créé. #) Affichez l'aire de l'objet créé (utilisez la méthode ``Aire``) afin de la calculer. #) Exécutez votre programme et vérifiez que tout fonctionne correctement (ajoutez l'instruction ``Console.Read();`` à la fin du ``Main`` pour éviter que la fenêtre ne se ferme tout de suite). #) Ajoutez un constructeur sans argument qui initialise la forme comme un carré de coté 10 et de position (0,0). Respectez le principe DRY : utilisez le chainage de constructeurs pour que le corps de votre constructeur ne contienne aucune instruction ! #) Ajoutez une méthode ``DoublerTaille`` qui multiplie la largeur et la longueur du rectangle par 2. #) Dans la méthode ``Main`` de la classe ``Program`` : A) Créez une nouvelle instance de la classe ``Rectangle`` avec votre constructeur sans argument. #) Doublez la taille du rectangle créé avec la méthode ``DoublerTaille``. #) Affichez les dimensions du rectangle pour vérifier que tout a bien fonctionné. #) Ajoutez une méthode ``TestePlusPetit`` qui prend en paramètre un rectangle ``r`` et retourne vrai si l'aire de ``r`` est plus petite que l'aire de l'instance courante. #) Dans la méthode ``Main`` de la classe ``Program`` : A) Testez la méthode ``TestePlusPetit`` avec les 2 références de ``Rectangle`` déjà créées. #) Créez le constructeur de recopie : constructeur qui reçoit en paramètre une référence sur un objet du même type et qui initialise à l’identique l’objet en cours de création. Respectez le principe DRY : utilisez le chainage de constructeurs pour que le corps de votre constructeur ne contienne aucune instruction ! #) Dans la méthode ``Main`` de la classe ``Program`` : A) Créez une copie du premier rectangle créé. #) Modifiez la largeur de la copie. #) Vérifiez que la largeur du premier rectangle n'a pas changé. .. quiz:: humanVMClass2 :title: Human VM Soit la classe ``C`` et la fonction ``Test`` .. code-block:: csharp class C { public int a,b; public C(int _a, int _b) { a=_a; b=_b; } } ... Test() { C[] T = new C[5]; for (int i = 0 ; i < 5 ; i++) { T[i] = new C(i,i+1); } T[2] = T[4]; for (int i = 0 ; i < 5 ; i++) { T[i].a += T[i].b; } } Dessinez l'état de la mémoire à la fin de la fonction ``Test`` (un papier et un crayon reste la façon la plus simple de dessiner !) .. quiz:: deepCopy :title: Copie profonde Une pratique courante est de proposer dans chaque classe un constructeur par copie, c'est-à-dire un constructeur qui prend en paramètre un objet du même type que celui à construire et qui construit une **copie profonde** (**deep copy**) de cet objet. Pour réaliser une copie profonde d'objet, il faut non seulement dupliquer l'objet mais également réaliser une copie profonde de tous les objets référencés par cet objet : l'objet d'origine et l'objet copié sont complètement indépendants. On parle de clonage. Téléchargez et ouvrez cette :download:`solution Visual Studio`. 1) Ajoutez à la classe ``Point`` un constructeur prenant en paramètre un objet de type ``Point`` et qui construit une copie profonde de ce point. #) Ajoutez à la classe ``Polygone`` un constructeur prenant en paramètre un objet de type ``Polygone`` et qui construit une copie profonde de ce polygone. #) Décommentez le contenu du ``Main`` et exécutez le programme pour vérifier que tout fonctionne. Donnez l'état de la mémoire à la fin de la fonction suivante (un papier et un crayon reste la façon la plus simple de dessiner !): .. code-block:: csharp Point p1 = new Point(1,1); Point p2 = p1; Point p3 = new Point(p1); Point [] pts = {p1, p2, p3}; Polygone po1 = new Polygone(pts); Polygone po2 = new Polygone(pts); Polygone po3 = new Polygone(po1); p1.x=2; p2.x=3; p3.x=4; po1.points[0].y=5; po2.points[1].y=6; po3.points[2].y=7; .. quiz:: propriete :title: Propriétés Dans un nouveau projet Console sous Visual Studio, écrivez une classe ``Duree`` qui devra stocker une durée avec une précision à la seconde. La classe devra posséder 2 propriétés ``DureeSeconde`` et ``DureeMinute`` qui permettent de lire et modifier la durée avec des valeurs exprimées respectivement en secondes et en minutes. Une durée ne sera jamais négative. Pour exprimer une durée en minutes, on arrondira toujours à la valeur supérieure. Exemple d'utilisation: .. code-block:: csharp Duree d = new Duree(70); // durée de 70 secondes WriteLine(d.DureeSeconde); // 70 d.DureeMinute = -2; WriteLine(d.DureeSeconde); // 70 WriteLine(d.DureeMinute); // 2 d.DureeMinute = 2; WriteLine(d.DureeSeconde); // 120 .. quiz:: coursedelapin :title: Course de lapin Dans un nouveau projet Console sous Visual Studio: 1) Créez une classe ``Lapin`` qui contient les propriétés suivantes: ``Surnom`` (``String``), ``Age`` (``int``), ``Position`` (``int``). Vous implémenterez complètement les propriétés ``Surnom`` et ``Age`` et vous utiliserez une propriété auto-implémentée pour ``Position``. #) Ecrivez un constructeur permettant d'initialiser les propriétés ``Surnom`` et ``Age`` (``Position`` sera toujours initialisée à 0). #) Ajoutez un champ statique ``nombreDeLapins`` qui compte le nombre de lapins créés. Modifiez le constructeur pour que ce champ soit automatiquement incrémenté lors de la création d'un nouveau lapin. #) Ajoutez une méthode statique ``NombreDeLapins()`` qui retourne le nombre lapins créé depuis le lancement du programme. #) Ajoutez une méthode ``void Avancer()`` à la classe ``Lapin``. Cette méthode ajoute une valeur aléatoire comprise entre 0 et 5 à la position du lapin. La méthode d'instance `Next `_ de la classe ``Random`` génère un nombre (pseudo) aléatoire. #) Dans la méthode ``Main`` de la classe ``Program``, créez trois lapins avec des noms et des ages différents. #) Créez un tableau de lapins et rangez-y les trois lapins créés précédemment. #) Appelez la méthode ``Avancer()`` 100 fois sur chacun des 3 lapins. #) Le lapin qui a gagné est celui qui a la position la plus grande à la fin. Affichez le nom du lapin qui a gagné.