.. _ClasseHeritage: L'héritage ********** L'héritage permet de construire une classe ``B`` à partir d'une classe existante ``A``. Il s'agit d'une sorte d'héritage biologique. La classe ``B`` va ainsi hériter des variables, des méthodes, bref de tous les membres de ``A`` puis, on va ajouter des éléments supplémentaires propres à la classe ``B``. * Si ``B`` **hérite** de ``A``, on dit que ``B`` est une **classe fille**, une **sous-classe**, une **classe enfant** ou encore une **classe dérivée** de ``A``. * A l'inverse, ``A`` se nomme la **classe mère**, la **classe de base**, la **classe parent** ou encore la **super classe** de ``B``. On peut examiner l'héritage de deux façons différentes : * Dans la vision par **spécialisation/redéfinition**, la classe ``B`` est vue comme un cas particulier de la classe ``A``. Par exemple, un 4x4 est un cas particulier d'une Voiture. Dans cette vision, l'ensemble des Voitures est l'ensemble englobant et les 4x4 représentent un sous-ensemble des Voitures. * Dans la vision par **extension**, la classe ``B`` est vue comme une version étendue de la classe ``A``. Par exemple, un 4x4 est une voiture disposant de 2 roues motrices supplémentaires. Du point de vue des fonctionnalités, la classe 4x4 englobe les fonctionnalités de la classe Voiture, la classe Voiture a moins de paramètres internes que la classe 4x4. .. image:: demo.svg :align: center :width: 70 % Un héritage entre plusieurs classes donne naissance à une hiérarchie dans laquelle une classe peut avoir * des ancêtres : sa classe mère, la classe mère de sa classe mère, et ainsi de suite... * des descendants : ses classes filles, les classes filles de ses classes filles, et ainsi de suite... L'héritage est une approche puissante pour la structuration des programmes et la réutilisation du code. Comme toute technique de réutilisation, elle facilite la maintenance et améliore la productivité. De plus, la structuration du programme induite par l'utilisation du mécanisme d'héritage facilite grandement la compréhension. Nous pouvons ainsi rencontrer une méthode Dessiner(...) recevant en paramètre un objet de type Crayon. En examinant la classe Crayon, nous remarquons que diverses classes en hérite : CrayonGras, CrayonFeutre, CrayonUni, CrayonDégradé, CrayonFluo... Intuitivement, on comprend que pour dessiner, on peut utiliser différents types de crayons et le tour est joué. Si nous n'avions pas utilisé un mécanisme d'héritage, nous aurions dû créer une méthode Dessiner pour chaque type d'objets Crayon. Si de plus, votre fonction Dessiner requiert un objet Pinceau pour colorier l'intérieur de l'objet, le nombre de fonctions à créer devient conséquent ! Pour une dizaine de crayons et de pinceaux différents, cela donne une centaine de fonctions Dessiner(Crayon,Pinceau,...) à créer pour couvrir chaque scénario. Beaucoup de travail inutile en vue et une documentation illisible en perspective ! La documentation MSDN permet de voir la hiérarchie de classe pour chaque classe du framework .net : par exemple pour la classe `System.Windows.Shapes.Rectangle `_ on obtient : .. image:: heritageRectangleMSDN.png :align: center Ce qui signifie que la classe Rectangle hérite de Shape, qui elle-même hérite de FrameworkElement et ainsi de suite. .. important:: On dit souvent que la relation d'héritage doit être compatible avec la relation **... est une sorte de ...**. Cela signifie que si la classe ``B`` **hérite de la classe** ``A`` alors je dois pouvoir dire que ``B`` **est une sorte de** ``A``. Par exemple un Lapin est une sorte d'Animal, un 4x4 est une sorte de Voiture. Par contre je ne peux pas dire qu'une Voiture est une sorte de Roue: il n'est donc pas possible que Voiture hérite de Roue. L'inverse n'est par contre pas vrai: ce n'est pas par ce que ``B`` est une sorte de ``A`` que ``B`` **doit** hériter de ``A``. Principe ======== L'héritage est déclaré lors de la déclaration de la classe avec la notation ``:``. .. admonition:: Syntaxe .. code-block:: csharp class IdentificateurClasse : IdentificateurClasseParent { } Exemple: .. code-block:: csharp class Vehicule { public string nom; public void AfficheNom(){ WriteLine(nom); } } class Voiture : Vehicule { public int puissance; public void Info() { AfficheNom(); // on hérite des méthodes de Vehicule WriteLine(puissance); // on hérite des champs de Vehicule } } Vehicule vehicule = new Vehicule(); vehicule.nom = "Proto2"; vehicule.AfficheNom(); // Proto2 // vehicule.puissance = 4; ERREUR // vehicule.Info(); ERREUR Voiture voiture = new Voiture(); voiture.nom = "Buggy"; voiture.puissance = 4; voiture.Info(); // Buggy 4 voiture.AfficheNom(); // Buggy Règles : * Une classe ne peut avoir qu'une seule classe parent. L'héritage multiple est interdit. * Les cycles sont interdits : ``A`` ne peut hériter de ``B`` si ``B`` est un descendant de ``A``. Considérons les 2 classes ``A`` et ``B`` ci dessous. .. code-block:: csharp class A { public void MethodeA(){}; } class B : A { public void MethodeB(){}; } Un aspect fondamental de l'héritage est que si une classe ``B`` hérite d'une classe ``A``, alors les objets de type ``B`` peuvent aussi être vus comme des objets de type ``A``. On dit que le type ``A`` *englobe*/*est plus grand que* le type ``B``, ce qui peut se représenter par un diagramme de Venn: .. figure:: diagrammepatate.svg :align: center Si la classe ``B`` hérite de la classe ``A`` alors, du point de vue du typage, les objets de type ``B`` forment un sous ensemble des objets de type ``A``. Je peux créer des variables et des objets de type ``A`` et ``B``, et comme les objets de type ``B`` peuvent également être vus comme étant du type parent ``A``, je peux les référencer par des variables de type ``A``: .. code-block:: csharp A a1 = new A(); B b1 = new B(); A a2 = new B(); // a2 est de type A mais référence un objet de type B A a3 = b1; // idem pour a3 On est alors dans la situation où la variable ``a2`` a pour **type déclaré** ``A`` (le type de la variable tel qu'on l'a écrit dans le code) et pour **type constaté** ``B`` (le type de l'objet qui est référencé par la variable au moment de l'exécution). .. image:: typeDeclConst.svg :align: center Lorsqu'on utilise l'opérateur d'accès ``.``, c'est le type déclaré qui est utilisé par le compilateur pour déterminer si on accède à un membre valide: .. code-block:: csharp b1.MethodeA(); // OK car b1 est de type déclaré B qui hérite de A b1.MethodeB(); // OK car b1 est de type déclaré B a2.MethodeA(); // OK car a2 est de type déclaré A // a2.MethodeB(); ERREUR de compilation car a2 est de type déclaré A (même si le type constaté est B) Lorsque qu'une classe ``B`` hérite d'une classe ``A``, ``B`` n'a pas accès aux membres privés de ``A``. Afin d'avoir un contrôle plus fin sur la visibilité, l'héritage introduit le nouveau niveau de visibilité **protected** qui ouvre l'accès aux enfants mais pas à l'extérieur. On se retouve ainsi avec 3 niveaux de visibilités possibles pour les membres de la classe ``A``: * public : accès ouvert pour l'extérieur et pour les classes dérivées * protected : accès fermé pour l'extérieur mais ouvert pour les classes dérivées * private : accès fermé pour l'extérieur et fermé pour les classes dérivées .. note:: Comment faire lorsqu'une classe semble hériter de plusieurs autres classes. Par exemple : un smartphone est à la fois un téléphone et un lecteur MP3. Une première solution serait de créer une fusion des deux classes Téléphone et Lecteur MP3. On parle alors d'héritage multiple. Ce choix fut longtemps plébiscité par le langage C++. Ainsi, la nouvelle classe rassemble tous les membres des classes parentes. Une contrainte apparaît au niveau des conflits entre les membres de même nom. Il faudra alors les discerner explicitement. Tous les membres des classes parents sont conservés. Un problème apparaît réellement si des membres ont le même rôle. Ils deviennent alors redondants ce qui peut être source de confusion : par exemple, un téléphone et un lecteur MP3 possèdent respectivement une batterie, un objet SmartPhone possède-t-il deux batteries ? Si l'on veut conserver une seule batterie, laquelle faut-il conserver ? L'héritage multiple peut poser des problèmes délicats, il y a donc controverse sur le fait de savoir si ses avantages surpassent ses inconvénients. Pour cette raison, il a été retiré des langages modernes (Java, C#). Face à cette situation, on peut essayer d'utiliser une agrégation. .. quiz:: heritage1 :title: Hiérarchie d'héritage Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: 1) :quiz:`{"type":"TF","answer":"F"}` Une classe peut avoir plusieurs classes mères. #) :quiz:`{"type":"TF","answer":"T"}` Une classe peut avoir plusieurs ancêtres. #) :quiz:`{"type":"TF","answer":"T"}` Une classe peut avoir plusieurs classes filles. #) :quiz:`{"type":"TF","answer":"T"}` Une classe peut avoir plusieurs descendants. #) :quiz:`{"type":"TF","answer":"T"}` Plusieurs classes peuvent hériter d'une même classe. .. quiz:: heritageVisibilite :title: Visibilité et héritage .. code-block:: csharp class A { public int attributPublicA; protected int attributProtectedA; private int attributPrivateA; } class B : A {} class C {} Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: 1) :quiz:`{"type":"TF","answer":"T"}` La classe ``B`` hérite de ``attributPublicA`` #) :quiz:`{"type":"TF","answer":"T"}` Dans la classe ``B`` on peut accéder à ``attributPublicA`` depuis l'instance courante. #) :quiz:`{"type":"TF","answer":"T"}` La classe ``B`` hérite de ``attributProtectedA`` #) :quiz:`{"type":"TF","answer":"T"}` Dans la classe ``B`` on peut accéder à ``attributProtectedA`` depuis l'instance courante. #) :quiz:`{"type":"TF","answer":"T"}` La classe ``B`` hérite de ``attributPrivateA`` #) :quiz:`{"type":"TF","answer":"F"}` Dans la classe ``B`` on peut accéder à ``attributPrivateA`` depuis l'instance courante. #) :quiz:`{"type":"TF","answer":"T"}` Dans la classe ``C`` on peut accéder à ``attributPublicA`` depuis un référence sur un objet de type ``A``. #) :quiz:`{"type":"TF","answer":"F"}` Dans la classe ``C`` on peut accéder à ``attributProtectedA`` depuis un référence sur un objet de type ``A``. #) :quiz:`{"type":"TF","answer":"F"}` Dans la classe ``C`` on peut accéder à ``attributPrivateA`` depuis un référence sur un objet de type ``A``. #) :quiz:`{"type":"TF","answer":"T"}` Dans la classe ``C`` on peut accéder à ``attributPublicA`` depuis un référence sur un objet de type ``B``. #) :quiz:`{"type":"TF","answer":"F"}` Dans la classe ``C`` on peut accéder à ``attributProtectedA`` depuis un référence sur un objet de type ``B``. #) :quiz:`{"type":"TF","answer":"F"}` Dans la classe ``C`` on peut accéder à ``attributPrivateA`` depuis un référence sur un objet de type ``B``. .. quiz:: heritage2 :title: Héritage .. code-block:: csharp class Mere{ public int attributMere; public void methodeMere(){}; } class Fille : Mere { public int attributFille; public void methodeFille(){}; } ... Mere a = new Mere(); Fille b = new Fille(); Mere c = new Fille(); Pour chacune des 3 variables ``a``, ``b`` et ``c``, donnez son type déclaré et son type constaté, puis dites si il est possible d'accéder aux membres des classes ``Mere`` et ``Fille`` avec l'opérateur ``.`` à partir de cette variable: .. csv-table:: :header: "Variable", ``a``, ``b``, ``c`` :widths: 10, 10, 10, 10 :delim: ! Type déclaré ! :quiz:`{"type":"SC","answer":"Mere","values":"Mere,Fille"}` ! :quiz:`{"type":"SC","answer":"Fille","values":"Mere,Fille"}` ! :quiz:`{"type":"SC","answer":"Mere","values":"Mere,Fille"}` Type constaté ! :quiz:`{"type":"SC","answer":"Mere","values":"Mere,Fille"}` ! :quiz:`{"type":"SC","answer":"Fille","values":"Mere,Fille"}` ! :quiz:`{"type":"SC","answer":"Fille","values":"Mere,Fille"}` ``attributMere`` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"T"}` ``MethodeMere`` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"T"}` ``attributFille`` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"F"}` ``MethodeFille`` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"TF","answer":"F"}` Fonctions Polymorphes ===================== Le terme polymorphisme désigne la capacité d'une méthode à pouvoir prendre un comportement différent suivant le contexte: types des paramètres et type de l'objet courant. Par exemple, si l'on veut créer une fonction calculant le maximum de deux nombres, il serait pratique qu'elle puisse porter le même nom quels que soient les paramètres utilisés. On voudrait créer ces fonctions de la manière suivante : .. code-block:: csharp int Max(int a, int b) { ... } float Max(float a, float b) { ... } double Max(double a, double b) { ... } ... k = Max(u,v) ; Le compilateur connaît les types des variables ``u`` et ``v``, il va donc chercher la fonction ``Max`` prenant ces types là en arguments. Les différentes fonctions ``Max`` ne traitent pas les mêmes éléments, pourtant conceptuellement, elles effectuent le même calcul. C'est un des aspects du polymorphisme, on parle dans ce cas de **polymorphisme ad hoc**. Ce concept de polymorphisme s'étend à l'héritage. Par exemple si vous faîtes un jeu de simulation affichant plusieurs types de véhicules à l'écran, il serait agréable d'avoir une unique fonction AfficheToi(). Ainsi pour dessiner l'écran du jeu, je demande à chaque objet de s'afficher en utilisant le même nom de fonction. Cette action est similaire pour une voiture, un avion, un vélo, chacun va s'afficher à l'écran, mais avec un comportement différent. Un objet A320 dessinera spécifiquement un Airbus A320, chaque voiture Peugeot 205 dessinera une Peugeot 205… Il est possible, mais plus lourd à gérer, de créer des fonctions différentes comme AfficheA320(), AfficheC3(), AffichePeugeot205()... Ainsi une fonction peut exister sous le même nom dans une hiérarchie mais avoir un comportement différent suivant le type de l'objet instancié. On parle alors de **polymorphisme d'héritage**. Polymorphisme ad hoc -------------------- Le **polymorphisme ad hoc** (ou **surcharge**, ou **overload**) consiste à définir plusieurs fonctions avec le même nom au sein de la même classe. On appelle **signature** d'une fonction le n-uplet formé de son nom et de l'ensemble des types de ses arguments (l'ordre est important). Par exemple, la méthode .. code-block:: csharp double Foo(int a, string b, float [] c){} a pour signature ``(foo, int, string, float [])``. Le C# autorise la définition d'une méthode si sa signature est différente de celles des autres méthodes de la classe. Il est important de noter que le type de retour ne fait pas parti de la signature, on ne peut donc pas avoir deux méthodes portant le même nom, prenant le même type d'argument mais avec un type de retour différent. Exemples: .. code-block:: csharp int F(int a, double b) {} // définition de f avec 2 arguments int et double int F() {} // redéfinition de f sans argument double F(double a) {} // redéfinition de f avec un argument double void F(double a, int b) {} // redéfinition de f avec 2 arguments double et int // double F() {} ERREUR : la signature de cette fonction est la même que "int f()" Lors de l'appel à la fonction ``F`` polymorphe, le compilateur détermine quelle version doit être utilisée en suivant son algorithme de **résolution de nom**. .. code-block:: csharp F(1, 3.0); // 1ere definition utilisée, signature (F, int, double) F(); // 2e definition utilisée, signature (F) F(2.0); // 3e definition utilisée, signature (F, double) F(3.0, 1); // 4e definition utilisée, signature (F, double, int) Si aucune signature ne correspond exactement aux arguments passés à la fonction le compilateur essaye de trouver la solution la plus proche à partir des règles de conversion implicites autorisées. .. code-block:: csharp F(2); // appel équivalent à F(2.0) par promotion de 2 en double Si le compilateur ne peut pas trouver de version de ``F`` correspondant à l'appel ou si la version à utiliser est ambigüe il indique une erreur. .. code-block:: csharp F("toto"); // ERREUR : aucune définition ne correspond F(1,1); // ERREUR : le compilateur ne sait pas si il faut utiliser F(int,double) ou F(double,int) F((double)1,1); // on peut lever l'ambigüité avec un cast explicite. .. quiz:: classeobj-overload :title: Polymorphisme ad-hoc Dans une classe qui possède déjà une unique méthode ``F`` ont peut re-déclarer la fonction ``F`` en modifiant uniquement: 1) :quiz:`{"type":"TF","answer":"T"}` Le nombre de paramètres dans la nouvelle définition. #) :quiz:`{"type":"TF","answer":"F"}` Le type de retour dans la nouvelle définition. #) :quiz:`{"type":"TF","answer":"F"}` Les noms de paramètres dans la nouvelle définition. #) :quiz:`{"type":"TF","answer":"T"}` Le type des paramètres dans la nouvelle définition. .. quiz:: classeobj-functioncallcast :title: Conversions implicites et appel de fonction La classe ``A`` possède une unique méthode statique ``public static bool F(String a, bool b, double c, int d){}``, quelles sont les instructions correctes ? 1) :quiz:`{"type":"TF","answer":"T"}` ``A.F("" + new A(), true, 2.0, 1);`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.F("", false, 2.0f, 1);`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.F("F", 1>2, 2, 0);`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.F("" + 1.0, "foo".Equals("bar"), 2 , 1);`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.F("FF", A.F("", true, 1.0, 1), 2.0, 1);`` .. quiz:: classeobj-signature :title: Signature de fonctions et résolution de nom Soit le programme suivant: .. code-block:: csharp :linenos: class A{ public static void F(){} // 1ère public static int F(int i){return i;} // 2ème public static double F(double i, double j){return i;} // 3ème public static double F(int i, double j){return j;} // 4ème public static double F(double i, int j){return i;} // 5ème public static void F(string b){} // 6ème public static void F(string b, string a){} // 7ème } Pour chaque chaque appel de fonction, indiquez quelle définition de ``F`` est utilisée ou "Erreur" si l'appel n'est pas valide. .. csv-table:: :header: Appel de fonction, Définition utilisée, Appel de fonction, Définition utilisée :widths: 10, 10, 10, 10 :delim: ! ``F(1)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"2ème"}` ! ``F("2", 1)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"Erreur"}` ``F(2.0)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"Erreur"}` ! ``F("", 2 + ".0")`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"7ème"}` ``F("" + 2.0)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"6ème"}` ! ``F(true)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"Erreur"}` ``F(1, 2)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"Erreur"}` ! ``F("" + false)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"6ème"}` ``F(1.0, 2)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"5ème"}` ! ``F()`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"1ère"}` ``F(1.0, 2.0)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"3ème"}` ! Polymorphisme d'héritage ------------------------ Le polymorphisme d'héritage va consister à redéclarer dans une classe fille, une méthode déjà déclarée dans une classe parent. Contrairement au polymorphisme ad hoc qui n'utilise pas de mot clé particulier, on utilisera ici le couple **virtual/override**. La première déclaration de la méthode doit porter le qualificatif **virtual** et toutes les classes enfants modifiant son comportement doivent précéder sa déclaration par le qualificatif **override**. .. code-block:: csharp :emphasize-lines: 2,11,18 class A { public virtual void Afficher(){ WriteLine("Bonjour"); } } class B : A { } class C : B { public override void Afficher() { WriteLine("Coucou"); } } class D : C { public override void Afficher(){ WriteLine("Hello"); } } A obj1 = new A(); C obj2 = new C(); obj1.Afficher(); // résultat => Bonjour obj2.Afficher(); // résultat => Coucou Jusqu'ici, tout va bien. Il est facile de lever l'ambiguïté de l'appel de la méthode ``Afficher`` car le type de l'objet est connu lors de la création de ``obj1`` et ``obj2``: les types déclarés et constatés sont identiques. Cependant, pour créer une liste d'objets cela devient plus difficile. Pour fonctionner, la liste stocke des références vers les objets concernés. En créant cette liste, la syntaxe va imposer de choisir un type unique pour toutes les références stockées. Par bon sens, nous choisissons comme type celui de l'ancêtre de la hiérarchie : .. code-block:: csharp A[] Liste = new A[4] ; Liste[0] = new A(); Liste[1] = new B(); Liste[2] = new C(); Liste[3] = new D(); On peut alors parcourir la liste et appeler la méthode ``Afficher`` sur chacun de ses éléments: .. code-block:: csharp foreach(A m in Liste) m.Afficher(); // Résultats : Bonjour Bonjour Coucou Hello Dans ce contexte, les éléments de la liste sont de type ``A``. Néanmoins lors de l'exécution, le CLR va regarder le type constaté de chaque élément pour déterminer quelle méthode ``Afficher`` doit être exécutée. .. note:: Quand on utilise une méthode qualifiée avec virtual/override, le CLR doit donc décider, à chaque fois que la méthode est appellée, quelle version doit être exécutée selon le type constaté de l'objet. Cette décision ne peut pas être prise au moment de la compilation du programme (seul le type déclaré est connu à ce moment) et a donc un coût lors de l'exécution du programme. Ce coût est généralement négligeable mais peut devenir important lorsque la méthode est appellée très fréquemment. Pour en savoir plus, vous pouvez regarder comment sont généralement implémentées les méthodes virtuelles avec les `vtables `_ .. attention:: Si l'on oublie les mots clés virtual/override, cela ne provoquera pas d'erreur (ni à la compilation, ni à l'exécution) mais le comportement ne sera pas celui souhaité: .. code-block:: csharp :emphasize-lines: 2,11,18,30 class A { public void Afficher(){ WriteLine("Bonjour"); } } class B : A { } class C : B { public void Afficher() { WriteLine("Coucou"); } } class D : C { public void Afficher(){ WriteLine("Hello"); } } A[] Liste = new A[4]; Liste[0] = new A(); Liste[1] = new B(); Liste[2] = new C(); Liste[3] = new D(); foreach(A m in Liste) m.Afficher(); // Résultats : Bonjour Bonjour Bonjour Bonjour Cela pose problème car les objets sont gérés comme s'ils étaient **TOUS** de type ``A`` : c'est uniquement le type déclaré qui est pris en compte. En l'absence de toute syntaxe particulière, le langage considère que le type de l'objet référencé est donné par le type de la référence. On parle de **masquage**. .. quiz:: polyHeritage :title: Polymorphisme d'héritage .. code-block:: csharp class A { public double F1 ( double v ) { return v-1 ; } public int F1 ( int v ) { return v+1; } public virtual int F2 ( int v ) { return v+2 ; } public virtual int F3 ( int v ) { return v+3 ; } } class B : A { public override int F2 ( int v ) { return v+4 ; } } class C : B { public int F1 ( int v ) { return v+5 ; } public override int F2 ( int v ) { return v+6 ; } public override int F3 ( int v ) { return v+7 ; } } Pour chaque prototype de fonction: * Indiquez s'il s'agit de polymorphisme d'héritage : Oui/Non * Indiquez ce qu'il se passe dans chaque classe pour cette fonction : Définition, Héritage (de qui ?), Masquage (pas d'héritage) ou Polymorphisme .. csv-table:: :header: Prototype, Polymorphisme d'heritage, Dans la classe ``A``, Dans la classe ``B``, Dans la classe ``C`` :widths: 10, 10, 10, 10, 10 :delim: ! ``double F1(double)`` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Définition"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Héritage de A"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Héritage de A"}` ``int F1(int)`` ! :quiz:`{"type":"TF","answer":"F"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Définition"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Héritage de A"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Masquage"}` ``int F2(int)`` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Définition"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Polymorphisme"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Polymorphisme"}` ``int F3(int)`` ! :quiz:`{"type":"TF","answer":"T"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Définition"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Héritage de A"}` ! :quiz:`{"type":"SC","values":"Définition,Héritage de A,Héritage de B,Polymorphisme,Masquage", "answer":"Polymorphisme"}` .. quiz:: polyHeritage2 :title: Polymorphisme d'héritage 2 Soient les classes ``A``, ``B`` et ``C`` décrites dans l'exercice ci-dessus et le programme suivant: .. code-block:: csharp class Program { public static void Main(string[] args) { A aa = new A() ; A ab = new B(); A ac = new C() ; B bb = new B() ; B bc = new C(); C cc = new C() ; ... } } Donnez la valeur retour des appels de fonction suivants : .. csv-table:: :header: "Appel", "Résultat", "Appel", "Résultat", "Appel", "Résultat" :widths: 5, 5, 5, 5, 5, 5 :delim: ! aa.F1(0) ! :quiz:`{"type":"FB","size":"3","answer":"1"}` ! ab.F3(0) ! :quiz:`{"type":"FB","size":"3","answer":"3"}` ! bb.F3(0) ! :quiz:`{"type":"FB","size":"3","answer":"3"}` ab.F1(0.0) ! :quiz:`{"type":"FB","size":"3","answer":"-1"}`! ac.F3(0) ! :quiz:`{"type":"FB","size":"3","answer":"7"}` ! bc.F3(0) ! :quiz:`{"type":"FB","size":"3","answer":"7"}` ac.F1(0) ! :quiz:`{"type":"FB","size":"3","answer":"1"}` ! bb.F1(0) ! :quiz:`{"type":"FB","size":"3","answer":"1"}` ! cc.F1(0) ! :quiz:`{"type":"FB","size":"3","answer":"5"}` aa.F2(0) ! :quiz:`{"type":"FB","size":"3","answer":"2"}` ! bc.F1(0.0)! :quiz:`{"type":"FB","size":"3","answer":"-1"}`! cc.F1(0.0)! :quiz:`{"type":"FB","size":"3","answer":"-1"}` ab.F2(0) ! :quiz:`{"type":"FB","size":"3","answer":"4"}` ! bb.F2(0) ! :quiz:`{"type":"FB","size":"3","answer":"4"}` ! cc.F2(0) ! :quiz:`{"type":"FB","size":"3","answer":"6"}` ac.F2(0) ! :quiz:`{"type":"FB","size":"3","answer":"6"}` ! bc.F2(0) ! :quiz:`{"type":"FB","size":"3","answer":"6"}` ! cc.F3(0) ! :quiz:`{"type":"FB","size":"3","answer":"7"}` aa.F3(0) ! :quiz:`{"type":"FB","size":"3","answer":"3"}` ! ! ! ! Constructeurs et chaînage ========================= Prenons une classe ``B`` enfant d'une classe ``A``. Lors de l'écriture des constructeurs de la classe ``B``, on doit se poser la question de l'initialisation des éléments provenant de ``A``. Pour cela nous allons chainer les constructeurs de la classe ``B`` avec les constructeurs de la classe ``A`` : contrairement au chainage d'un constructeur d'une même classe qui est optionnel, le chainage des constructeurs avec les constructeurs de la classe mère est une **obligation**. Le chainage d'un constructeur avec un constructeur de la classe mère utilise une syntaxe similaire au chainage dans la même classe mais avec le mot clé **base**: .. admonition:: Syntaxe .. code-block:: csharp :emphasize-lines: 6 class A{ public A(){}: } class B : A{ public B() : base() // chainage avec le constructeur sans argument de A {} } Lors de la construction de l'objet de type ``B``, le fait que le constructeur de ``A`` soit appelé ne veut pas dire qu'un objet de type ``A`` soit créé ! Il s'agit uniquement de l'initialisation des membres hérités de ``A`` au travers du constructeur de ``A``. Au final, il n'y a qu'un seul objet de type ``B`` créé. .. note:: Les constructeurs d'une même classe peuvent toujours être chainés entre eux mais chaque chaine doit se terminer par un constructeur chainé sur un constructeur de la classe mère. Lorsque l'on construit un objet d'une classe fille, le constructeur chainé (donc celui de la classe mère) est exécuté en premier: .. code-block:: csharp class A { public A() { WriteLine("construction de A"); } } class B : A { public B() : base() { WriteLine("construction de B"); } } B toto = new B() ; // construction de A construction de B Le constructeur de la classe parent est lancé AVANT les instructions du constructeur de la classe ``B``. Si la classe ``A`` possède un parent, son constructeur sera appelé avant que celui de ``A`` soit exécuté et ainsi de suite. Au final, c'est le constructeur du parent initial dans la hiérarchie qui sera le premier lancé, puis ceux de ses enfants successivement. Ce comportement logique provient du fait que pour fonctionner, la classe ``B`` peut nécessiter des ressources venant de sa classe mère, le parent doit donc être créé avant. .. important:: Si vous ne chaînez pas explicitement un constructeur de ``B``, le compilateur génèrera automatiquement un **chainage implicite** avec le constructeur sans argument de la classe parent. Si la classe parent ne possède pas de constructeur sans argument cela génèrera une erreur de compilation. .. quiz:: chainageCTRHeritage :title: Constructeurs et héritage Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: 1) :quiz:`{"type":"TF","answer":"F"}` Dans une classe fille, je dois déclarer un constructeur. #) :quiz:`{"type":"TF","answer":"F"}` Le chainage avec un constructeur de la classe mère se fait avec le mot clé ``this``. #) :quiz:`{"type":"TF","answer":"F"}` Dans une classe fille, un constructeur doit initialiser lui-même les attributs de la classe mère. #) :quiz:`{"type":"TF","answer":"F"}` Dans une classe fille, un constructeur doit initialiser lui-même les attributs de toutes les classes ancêtres. #) :quiz:`{"type":"TF","answer":"F"}` Si je ne chaine pas un constructeur vers un constucteur de la classe mère alors les attributs hérités seront initialisés par défaut. #) :quiz:`{"type":"TF","answer":"F"}` Dans une classe fille, je ne peux pas chainer un constructeur vers un constructeur de la même classe. #) :quiz:`{"type":"TF","answer":"F"}` Dans une classe fille, si je ne déclare pas de constructeur, le compilateur en ajoutera un, ainsi il ne peut pas y avoir de problème. #) :quiz:`{"type":"TF","answer":"T"}` Un constructeur de la classe mère est toujours exécuté avant l'exécution d'un constructeur de la classe fille. Chaînage des méthodes ===================== Il est toujours possible d'appeler la fonction qui a été redéfinie à partir de la redéfinition. Pour cela on utilise le mot clé **base**. Cela permet de construire des chaînages de fonctions dans le même principe que le chaînage des constructeurs. Par exemple, supposons que la classe ``Voiture`` dispose d'une fonction ``TestAllumage()`` qui vérifie que tous les composants électriques (ordinateur de bord, ABS, ESP, air bag, ...) de la voiture fonctionnent bien. Dans le cas d'un ``Cabriolet`` qui ajoute un toit amovible, il faudra contrôler l'ensemble des systèmes d'une voiture simple et le système qui rentre/sort le toit amovible. .. code-block:: csharp :emphasize-lines: 5 class Cabriolet : Voiture { public override void TestAllumage() { base.TestAllumage(); // test des éléments de la classe Voiture // par appel a TestAllumage() de Voiture TestToitAmovible(); // test des éléments spécifiques au Cabriolet } ... } .. quiz:: chainageMethodeHeritage :title: Constructeurs et héritage Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: 1) :quiz:`{"type":"TF","answer":"T"}` Par défaut une méthode *override* n'est pas chainée avec la méthode qu'elle redéfinit. #) :quiz:`{"type":"TF","answer":"F"}` Si je chaine une méthode *override* avec la méthode redéfinie, cela doit être la première instruction de la méthode. #) :quiz:`{"type":"TF","answer":"F"}` Je peux chainer une méthode avec une méthode privée de la classe mère. #) :quiz:`{"type":"TF","answer":"T"}` Le chainage est effectué avec le mot clé **base** et l'opérateur ``.``. Classes et méthodes abstraites ============================== Considérons un logiciel de dessin vectoriel. Ce logiciel de dessin doit être capable de manipuler différents objets : des ellipses, des rectangles... Il va donc être util de les regrouper dans une même hiérarchie avec un parent commun : ``ObjetVectoriel``. Il sera naturel d'utiliser une méthode polymorphe ``Draw()`` pour ces objets. Cependant la classe ``ObjetVectoriel`` ne contient pas d'information géométrique et elle ne sait pas se dessiner : on ne sait pas quoi écrire dans la méthode ``Draw`` de la classe ``ObjetVectoriel``. Lorsque l'on a identifié un cas tel que la classe ``ObjectVectoriel`` qui doit posséder une méthode dont on ne peut déterminer le contenu mais qui devra être définie dans les classes filles, il s'agit d'une **classe abstraite** et d'une **méthode abstraite**. On ajoute alors le qualificatif **absract** à la classe et à la méthode: .. code-block:: csharp :emphasize-lines: 1, 4 abstract class Shape { protected string nom ; public abstract void Draw() ; // méthode abstraite : pas de block {} } class Triangle : Shape { protected Point p1, p2, p3 ; public override void Draw() { ... } // (re)définition de la méthode Draw } On remarque qu'une méthode abstraite est automatiquement virtuelle. Par contre, il faut utiliser le qualificatif override lors de sa définition dans les sous-classes. Une classe abstraite ne peut pas être instanciée (elle est incomplète car les méthodes abstraites n'ont pas de corps), ce qui ne l'empêche pas de posséder un ou plusieurs constructeurs appelés par le mécanisme de chaînage des constructeurs. .. important:: Une classe non abstraite fille d'une classe abstraite doit implémenter toutes les méthodes abstraites de sa classe mère. .. quiz:: abstract :title: Classe et méthode abstraites Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: 1) :quiz:`{"type":"TF","answer":"T"}` Une classe abstraite ne peut pas être instanciée. #) :quiz:`{"type":"TF","answer":"T"}` Une méthode abstraite ne possède pas de corps. #) :quiz:`{"type":"TF","answer":"F"}` Une classe non abstraite peut posséder des méthodes abstraites. #) :quiz:`{"type":"TF","answer":"F"}` Une classe fille n'hérite pas des méthodes abstraites de sa classe mère. #) :quiz:`{"type":"TF","answer":"T"}` Une classe fille non abstraite doit redéfinir toutes les méthodes abstraites dont elle hérite. #) :quiz:`{"type":"TF","answer":"F"}` Une méthode dont le corps ne contient pas d'instruction est abstraite. #) :quiz:`{"type":"TF","answer":"T"}` Une méthode abstraite peut avoir un type de retour différent de ``void``. Une classe mère commune ======================= En C#, toute classe sans parent explicite hérite implicitement de la classe mère ``Object``. La class ``Object`` est ainsi la racine de la hiérarchie de toutes les classes, les vôtres comme celles de .net. La classe ``Object`` déclare cinq méthodes d'instance accessibles à partir de tous les objets. .. csv-table:: :header: "Méthode", "Description" :widths: 20, 60 public virtual string ToString(), Retourne une chaîne de caratères qui représente l'objet public virtual bool Equals(Object), Détermine si l'objet spécifié est égal à l'objet en cours public virtual void Finalize(), Fonction exécutée par le ramasse miette avant la destruction de l'objet public virtual int GetHashCode(), Retourne le code de hashage de l'objet public Type GetType(), Retourne le type de l'objet public Object MemberWiseClone(), Créé une copie superficielle de l'objet .. note:: La méthode ``ToString`` est automatiquement invoquée lors de la conversion implicite d'un objet en chaine de caractères. C'est une pratique courante de redéfinir cette méthode pour faciliter les affichages. .. code-block:: csharp class A{ public override String ToString(){ return "Objet de la classe A"; } } ... A a = new A(); WriteLine(a); // équivalent à WriteLine(a.ToString()); String s = "a est un " + a; // équivalent à String s = "a est un " + a.ToString(); .. Identification dynamique de type ================================ On peut trouver des collections dans lesquelles sont stockés toutes sortes d'objets : des véhicules, des téléviseurs, des personnes… Cela est techniquement possible puisque nous venons de voir que tout objet en C# appartient implicitement à la même famille dont la racine est Object. Si certains objets appartiennent à une même hiérarchie (Voiture), d'autres sont sans rapport entre eux. Dans cette situation, il peut être utile de déterminer le type d'un objet à la volée, en C# le mot clef **is** fournit cette facilité. .. code-block:: csharp class A { ... } class B : A { ... } class C { ... } A a = new B(); if ( a is A ) WriteLine("Je suis de type A"); // => Je suis de type A if ( a is B ) WriteLine("Je suis de type B"); // => Je suis de type B if ( a is C ) WriteLine("Je suis de type C"); // - rien Une instance de la classe ``B`` est considérée comme étant de type ``A`` car elle est une descendante de ``A``. Si nous prenons un exemple d'utilisation, nous obtenons : .. code-block:: csharp foreach (Object o in Liste) { if ( o is Voiture ) { Voiture v = (Voiture) o ; v. Demarre() ; } } Quelle que soit le modèle de voiture (C3, Corsa, SCUDA), nous déterminons si ``o`` est un objet héritant de la classe ``Voiture``. Nous effectuons une conversion explicite et nous avons ainsi accès à toutes les méthodes polymorphes des Voitures. On peut utiliser une syntaxe équivalente avec le mot clef **as**. Ce mot clef effectue une tentative de transtypage et si celle-ci échoue le résultat est null. .. code-block:: csharp Voiture v = o as B ; if ( v != null ) v.Demarre() ; .. quiz:: rtti :title: Identification dynamique de type .. code-block:: csharp class A { public void M1(){}; } class B : A{ public void M2(){}; } class C {} A a = new B(); B b = new B(); Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse: 1) :quiz:`{"type":"TF","answer":"T"}` ``a is A`` vaut ``true``. #) :quiz:`{"type":"TF","answer":"T"}` ``a is B`` vaut ``true``. #) :quiz:`{"type":"TF","answer":"F"}` ``a is C`` vaut ``true``. #) :quiz:`{"type":"TF","answer":"T"}` ``b is A`` vaut ``true``. #) :quiz:`{"type":"TF","answer":"T"}` ``b is B`` vaut ``true``. #) :quiz:`{"type":"TF","answer":"F"}` ``b is C`` vaut ``true``. #) :quiz:`{"type":"TF","answer":"T"}` ``a.M1()`` est un appel de fonction valide. #) :quiz:`{"type":"TF","answer":"F"}` ``a.M2()`` est un appel de fonction valide. #) :quiz:`{"type":"TF","answer":"T"}` ``((B)a).M1()`` est un appel de fonction valide. #) :quiz:`{"type":"TF","answer":"T"}` ``((B)a).M2()`` est un appel de fonction valide. #) :quiz:`{"type":"TF","answer":"F"}` ``C c = (C)a;`` est un instruction valide. Exercices ========= .. quiz:: EncapsulationPrivatePublicProtected :title: Visibilité Les qualificatifs de visibilité (public, protected et private) sont là pour gérer l'encapsulation. Il est souvent difficile de déterminer le qualificatif de visibilité idéal pour les membres d'une classe. Néanmoins, on peut tout de même donner quelques règles générales : * Si une méthode correspond à un des services que rend la classe, elle doit pouvoir être appelée de l'extérieur et doit être qualifiée de public. * Si une méthode ne doit pas être accessible de l'extérieur, il faut il faut choisir entre le qualificatif protected et private. Ici, le choix devient plus complexe car il faut pouvoir se projeter dans le futur : les classes filles de notre classe doivent-elles pouvoir accéder à ce membre ? Si les enfants doivent avoir accès, on choisit protected. Si l'on reste fermé des enfants et de l'extérieur, ce sera private. Pour les champs, si un champ contient une information qui doit être accessible depuis l'extérieur de la classe alors il vaut mieux le qualifier de private et définir une propriété publique associée. Cette propriété sera publique et permettra de contrôler de manière transparente l'accès à ce champ pour vérifier par exemple la validité des valeurs passées en paramètres. Considérons l'exemple suivant composé de deux classes. La classe ``StandardBankAccount`` représente le compte bancaire d'un client lambda n'ayant pas le droit de dépenser plus qu'il ne possède (pas de solde négatif). La classe ``PremiumBankAccount`` qui hérite de ``StandardBankAccount`` représente le compte bancaire d'un client ayant le droit d'avoir un solde négatif (avec une certaine limite). .. code-block:: csharp class StandardBankAccount { string owner; // propriétaire du compte double balance; // solde du compte StandardBankAccount(string owner, double balance) { this.owner = owner ; this.balance = Math.max(0, balance) ; } boolean Deposit( double amount ) { bool flag = false ; if ( amount > 0 ) { balance += amount ; flag = true ; } return flag; } boolean Withdraw( double amount ) { bool flag = false ; if ( checkPermission( amount ) ) { balance -= amount ; flag = true ; } return flag; } virtual bool CheckPermission( double withdrawAmount ) { return withdrawAmount > 0 && balance - withdrawAmount >= 0 ; } } class PremiumBankAccount : StandardBankAccount { double allowedDebt; // encours négatif permis PremiumBankAccount( string owner, double amount, double allowedDebt ) : base( owner, amount ) { this.allowedDebt = Math.max( 0, allowedDebt ) ; } override bool CheckPermission( double withdrawAmount ) { return withdrawAmount > 0 && balance - withdrawAmount >= -allowedDebt ; } } Dites pour chacun des membres de la classe ``StandardBankAccount`` quel qualificatif de visibilité est le plus adapté. Dans le cas des champs, dites en plus s'il vaudrait mieux les remplacer par une propriété en réfléchissant à la visibilité des accesseurs. .. csv-table:: :header: Membre, Mode d'encapsulation :widths: 10, 10 :delim: ! ``owner`` ! :quiz:`{"type":"SC", "values":"public,protected,private,Propriété{public get; public set},Propriété{public get; private set}","answer":"Propriété{public get; private set}"}` ``balance`` ! :quiz:`{"type":"SC", "values":"public,protected,private,Propriété{public get; public set},Propriété{public get; private set}","answer":"Propriété{public get; private set}"}` ``StandardBankAccount`` ! :quiz:`{"type":"SC", "values":"public,protected,private,Propriété{public get; public set},Propriété{public get; private set}","answer":"public"}` ``Deposit`` ! :quiz:`{"type":"SC", "values":"public,protected,private,Propriété{public get; public set},Propriété{public get; private set}","answer":"public"}` ``Withdraw`` ! :quiz:`{"type":"SC", "values":"public,protected,private,Propriété{public get; public set},Propriété{public get; private set}","answer":"public"}` ``CheckPermission`` ! :quiz:`{"type":"SC", "values":"public,protected,private,Propriété{public get; public set},Propriété{public get; private set}","answer":"protected"}` .. quiz:: HeritageCourseAnimaux :title: La grande course des animaux Pour sa nouvelle édition, notre grande course de lapins a décidé de mettre un peu de piquant dans la compétition en invitant des tortues et la course devient, de fait, la grande course des animaux. Dans notre nouveau modèle, les lapins possèdent les caractéristiques suivantes : surnom, age, couleur de la fourrure et position (un nombre entier qui mesure la distance depuis la ligne de départ). Les tortues possèdent quant à elle les caractéristiques suivantes : surnom, age, épaisseur de la carapace et position. Les deux animaux possèdent les méthodes Avancer et ToString (représenter l'animal sous forme de chaine de caractères). On constate immédiatement qu’une tortue et un lapin sont des sortes d’animaux qui possèdent des éléments en commun, nous allons donc créer une classe Animal qui regroupe tout ce qui est commun. Nous pourrons alors écrire deux classes : Lapin et Tortue qui héritent de Animal. Chacune de ces classes ajoutera les caractéristiques propres à l’animal et redéfinira les méthodes de manières adéquates. Dans un nouveau projet Console sous Visual Studio: 1) Écrivez la classe ``Animal`` contenant tous les éléments commun aux lapins et aux tortues. Écrivez un constructeur paramétrique permettant de facilement initialiser les propriétés. La méthode ``sting ToString()`` retourne une chaîne de caractère contenant les informations sur l’animal : "surnom, age, position". La méthode ``void Avancer()`` est connue des lapins et des tortues mais l’action à réaliser n’est pas déterminée pour la classe ``Animal`` que faut-il faire ? #) Écrivez les classes ``Lapin`` et ``Tortue`` qui héritent de ``Animal``. Ajoutez les champs nécessaires et définissez des constructeurs paramétriques pour les initialiser (pensez aux contraintes de chaînage des constructeurs). #) Redéfinissez la méthode ``ToString`` dans les classes ``Lapin`` et ``Tortue`` afin d'ajouter les informations spécifiques à la description de l’animal en question. Par exemple, l’appel de ``ToString`` sur un objet de type ``Tortue`` renvoie une chaîne de la forme "surnom, age, position, épaisseur de la carapace". #) Redéfinissez la méthode ``Avancer`` dans les classes ``Lapin`` et ``Tortue``. Dans le cas des lapins la méthode ``Avancer`` fait avancer le lapin d’une distance aléatoire entre 0 et 10. La tortue avance toujours de 6 lorsque l’on appelle la méthode ``Avancer``. La méthode d'instance ``int Next(int maxValue)`` de la classe ``Random`` génère un nombre (pseudo) aléatoire compris entre 0 (inclus) et ``maxValue`` (exclus). #) Réalisez les actions suivantes dans la méthode ``Main`` : Créez deux lapins et deux tortues et les ranger dans un tableau d’animaux. Faites avancer chacun des animaux 100 fois. Déterminez quel animal a remporté la course et affichez ses caractéristiques.