420-202-RE – Structures de données et programmation orientée objet

Quelques raccourcis :

Ceci est un petit site de support pour le cours 420-202-RE – Structures de données et programmation orientée objet.

Vous trouverez aussi des liens sur divers langages (dont C#, notre outil de prédilection dans ce cours) un peu partout dans http://h-deb.clg.qc.ca/

Les diverses sections de cette page (en fonction desquelles vous trouverez quelques liens dans l'encadré à droite) vous mèneront elles-aussi sur des pistes qui vous permettront d'explorer un peu plus par vous-mêmes, de valider vos acquis et d'enrichir votre apprentissage

Cliquez sur cette cible pour le plan de cours, sous forme électronique

Pratiques de correction

Je corrige les programmes en appliquant des codes de correction. Vous trouverez ici la liste des codes les plus fréquents.

Ma stratégie de correction en tant que telle (pour le code, à tout le moins) est résumée ici.

Cliquez sur cette cible pour un résumé des principales règles de programmatique en vigueur dans ce cours.

Cliquez sur cette cible pour les normes appliquées dans ce cours, en ce qui a trait au pseudocode

Détail des séances en classe

Date Séance Détails

26 janvier

T00

Au menu :

  • Présentation du cours et du plan de cours
  • Petit exercice de remise en forme

Notez qu'en ce premier cours, nous investissons nos efforts sur deux aspects, soit :

  • Reprendre la forme intellectuelle après le congé des Fêtes, et
  • Mettre en place le vocabulaire et les idées qui guideront nos réflexions en ce début de session

Ça fait un gros cours pour démarrer la session, mais nous réinvestirons le tout cours après cours, alors ça va entrer doucement dans notre tête, et s'intégrer à notre praxis émergente 🙂.

  • Mise en place du vocabulaire de base de la POO, mais sous forme d'un survol seulement, et avec accent sur l'encapsulation. Ainsi, nous avons identifié et situé les termes et idées suivant(e)s :
    • respect des invariants, entre chaque appel d'un service d'un objet
    • identification et garantie du respect des préconditions pour les services d'un objet
    • identification et garantie du respect des postconditions pour les services d'un objet
    • une maxime importante : « un objet est responsable de son intégrité, du début à la fin de sa vie »
    • quelques mots clés qui aident à encadrer la capacité d'un objet à assurer son encapsulation, soit les qualifications d'accès private, protected, public
      • nous n'avons pas vraiment parlé de protected, qui a une utilité réelle mais plus limitée que des deux autres, et dont nous ne pouvons pas traiter pour le moment
      • nous avons toutefois insisté sur le fait que private, la qualification par défaut des membres dans une classe, est ce que nous souhaitons utiliser le plus possible, et nous avons donné quelques raisons pour soutenir cette prise de position
      • nous avons démontré que public, du moins pour les états d'un objet, devrait être évité dans presque tous les cas, cette qualification empêchant de facto l'objet d'assurer son encapsulation
  • Nous avons mis de l'avant quelques mots de vocabulaire plus techniques, soit :
    • les méthodes, qui sont les services (les fonctions, les comportements) d'un objet
    • les attributs, qui sont les états (variables, constantes) d'un objet
    • les constructeurs, qui sont les méthodes un peu spéciales mais Ô combien importantes qui permettent de déterminer l'état initial d'un objet – sans eux, pas d'état initial connu pour un objet, donc pas d'encapsulation pour lui
  • Nous avons discuté du glissement sémantique entre F(obj), où on applique une opération sur une entité , ce qui rejoint l'approche procédurale que nous avons mis en application dans le cours 420-201-RE, et obj.F(), qui sollicite la méthode de l'objet et rend ce dernier actif
  • Nous avons discuté brièvement des accesseurs (méthodes de consultation) et des mutateurs (méthodes de modification), qui sont des familles de services typiques pour les objets
    • La partie get d'une propriété est un exemple d'accesseur
    • La partie set d'une propriété est un exemple de mutateur
    • Il est évidemment possible d'écrire des méthodes qui ont on comportement d'accesseur ou un comportement de mutateur
  • Nous avons montré que C# permet une écriture concise pour ces deux familles importantes de services, soit les propriétés, et nous avons indiqué qu'il s'agit d'une pratique idiomatique dans ce langage, donc d'une pratique que nous allons mettre en application même si elle est plus « décorative » que nécessaire
  • Nous avons abordé le mot static, qui en C# a le sens de « membre de classe » alors qu'un membre qui n'est pas qualifié static est un « membre d'instance »
  • Enfin, nous avons écrit une petite classe Carré telle que le programme suivant :
// ...
static void Main()
{
   Carré c = new Carré(3);
   c.Dessiner();
}
// ...

    ... affichera à la console la chose suivante :

###
###
###

À titre de « petit bonbon », nous avons vu en début de séance que les fonctions qui se limitent à un seul return peuvent être écrites de manière simplifiée, et nous avons appris que votre chic prof, souhaitant vous encourager à écrire de courtes fonctions qui font une et une seule chose (et le font bien!), acceptera cette syntaxe si elle est bien utilisée.

Ainsi, ceci :

class Nom
{
   static string NOM_DÉFAUT = "Inconnu(e)";
   string valeur;
   public string Valeur
   {
      get
      {
         return valeur;
      }
      private set
      {
         if (value != null && value.Length > 0)
         {
            valeur = value;
         }
         else
         {
            valeur = NOM_DÉFAUT;
         }
      }
   }
   public string Crié
   {
      get
      {
         return Valeur.ToUpper();
      }
   }
   public Nom(string valeur)
   {
      Valeur = valeur;
   }
}

... pourra s'écrire comme suit 

class Nom
{
   static string NOM_DÉFAUT = "Inconnu(e)";
   string valeur;
   public string Valeur
   {
      get => valeur;
      private set
      {
         if (value != null && value.Length > 0)
         {
            valeur = value;
         }
         else
         {
            valeur = NOM_DÉFAUT;
         }
      }
   }
   public string Crié
   {
      get => Valeur.ToUpper();
   }
   public Nom(string valeur)
   {
      Valeur = valeur;
   }
}

À propos des termes utilisés, les notes informelles prises en classe étaient comme suit (je vous mets ça « brut ») :

Groupe 01Groupe 02
classe et instance
   Point est une classe
   pt est une instance de la classe Point
   
pt.x est un attribut d'instance du Point nommé pt
   variable membre de pt
   
pt.X est une propriété d'instance du Point nommé pt
   le get est ce qui permet d'en lire la valeur
   le set est ce qui permet d'en modifier la valeur
   
   (une propriété, typiquement, encadre l'accès à un attribut)
   
Point() est un constructeur par défaut

Point(int x, int y) est un constructeur paramétrique

public : tout le monde y a accès
private : seul un Point y a accès

static : membre de classe

(non-static : membre d'instance)

Encapsulation :

* principe selon lequel un objet se porte garant de son
  intégrité, du début à la fin de sa vie
* principe selon lequel un objet garantit le respect de
  ses invariants

Invariant : ce qui s'avère pour un objet entre deux
appels de ses méthodes
classe
  Point

instance
  pt0, pt1
  
attributs d'instance
  pt.x, pt.y
  variables membres d'une instance
  
propriétés d'instance
   pt.X, pt.Y
   get pour lire
   set pour modifier / écrire
   
constructeur
  par défaut, p.ex. Point.Point()
  paramétrique, p.ex. Point.Point(int x, int y)
  
private si possible
public si nécessaire  

encapsulation :

* principe selon lequel un objet garantit son intégrité
  du début à la fin de sa vie
  
* principe selon lequel un objet se porte garant du
  respect de ses invariants

invariant : état qui s'avère en tout temps entre deux
appels de méthodes

précondition : ce à quoi l'appelant d'une fonction s'engage

postcondition : ce à quoi la fonction appelée s'engage

static : membre de classe

(non-static) : membre d'instance

28 janvier

L00

Au menu :

  • Petite séance de programmation collective. Écrivons ensemble un programme qui :
    • Lit un nombre de personnes. Ce nombre doit être un entier strictement positif
    • Pour chaque personne :
      • lit son nom, qui devra être une chaîne de caractères non-vide
      • lit son âge (un entier positif, donc supérieur ou égal à zéro)
      • crée une Personne ayant ce nom et cet âge
      • l'ajoute dans un tableau
  • Une fois toutes les personnes lues, ce programme :
    • Trouvera et affichera le nom de la personne dont le nom est le plus long
    • Affichera l'âge moyen des personnes
  • Pour y arriver, nous définirons une classe Personne telle que :
    • Une Personne aura un nom et un âge
    • Le nom d'une Personne ne pourra être vide. Par défaut, nous utiliserons le nom "INCONNU(E)"
    • L'âge d'une Personne devra se situer entre 0 et 140 inclusivement. Par défaut, nous considérerons qu'une Personne est d'âge zéro

À titre de simplification (et de bonbon de fin de séance), j'ai aussi montré :

  • Comment utiliser foreach dans le cas où une répétitive doit itérer sur tous les éléments d'une séquence et n'a pas besoin de la valeur des indices
  • Comment utiliser l'opérateur ternaire dans les cas où nous souhaitons choisir l'une de deux valeur sur la base d'une condition
  • Comment faire des propriétés automatiques, dans les cas où une classe n'a pas d'invariant garantir

Enfin, suivant une question de la classe, nous avons examiné pourquoi une classe a implicitement un constructeur par défaut quand elle n'a aucun constructeur, mais pourquoi elle n'en a plus (à moins que vous ne l'écriviez explicitement) dès qu'au moins un autre constructeur est écrit

Une solution possible est disponible ici.

2 février

T01

Au menu :

4 février

L01

Au menu, bases du cycle de vie d'un objet, membres et qualifications d'accès :

  • Constructeurs et cycle de vie d'un objet
  • Membres de classe (static) et membres d'instance (non-static)
    • exemples et exercices
  • Propriétés et méthodes, plus en détail :
    • getters et setters
    • propriétés en tant que protection pour un attribut
    • propriétés autogénérées
    • propriétés modifiables à la construction seulement
    • enjeux philosophiques
    • immuabilité
  • Le type string de C#

S'il reste du temps, ce sera pour travailler sur le labo du Mastermind.

J'ai fait des exemples en classe, soit une classe Rectangle (que nous avons rendue immuable) et des exemples de manipulation de string et de StringBuilder.

Le code de Rectangle suit :

Groupe 1 Groupe 2
using System;
namespace z202gr1
{
   class Program
   {
      // objet immuable : qui ne peut être modifié une fois construit
      // invariant : largeur > 0
      // invariant : hauteur > 0
      class Rectangle
      {
         const int LARGEUR_MIN = 1,
                   LARGEUR_DÉFAUT = LARGEUR_MIN;
         const int HAUTEUR_MIN = 1,
                   HAUTEUR_DÉFAUT = HAUTEUR_MIN;
         // attributs d'instance
         int largeur;
         int hauteur;
         public int Largeur // propriété d'instance
         {
            get => largeur;
            private init
            {
               largeur = value < LARGEUR_MIN ? LARGEUR_DÉFAUT : value;
            }
         }
         public int Hauteur
         {
            get => hauteur;
            private init
            {
               hauteur = value < HAUTEUR_MIN ? HAUTEUR_DÉFAUT : value;
            }
         }
         // public int CalculerSurface() => Largeur * Hauteur;
         // propriétés de second ordre (propriétés calculées)
         public int Surface => Largeur * Hauteur;
         public int Périmètre => 2 * (Largeur + Hauteur);
         //public void Gonfler()
         //{
         //   Largeur *= 2;
         //   Hauteur *= 2;
         //}
         public Rectangle(int hauteur, int largeur)
         {
            Hauteur = hauteur;
            Largeur = largeur;
         }
         public void Dessiner() // méthode d'instance (non-static)
         {
            for(int i = 0; i != Hauteur; ++i)
            {
               for (int j = 0; j != Largeur; ++j)
                  Console.Write('#');
               Console.WriteLine();
            }
         }
         public static Rectangle Gonfler(Rectangle r) => // méthode de classe (static)
            new Rectangle(r.Hauteur * 2, r.Largeur * 2);
      }
using System;
namespace z202gr2
{
   class Program
   {
      // objet immuable : objet qui n'est plus modifiable une fois construit
      // invariant : largeur > 0
      // invariant : hauteur > 0
      class Rectangle
      {
         const int LARGEUR_MIN = 1, // constantes de classe (pas d'autre option en C#)
                   LARGEUR_DÉFAUT = LARGEUR_MIN,
                   HAUTEUR_MIN = 1,
                   HAUTEUR_DÉFAUT = HAUTEUR_MIN;
         // attributs d'instance
         int largeur;
         int hauteur;
         // propriétés d'instance
         public int Largeur
         {
            get => largeur;
            private init
            {
               largeur = value < LARGEUR_MIN ? LARGEUR_DÉFAUT : value;
            }
         }
         public int Hauteur
         {
            get => hauteur;
            private init
            {
               hauteur = value < HAUTEUR_MIN ? HAUTEUR_DÉFAUT : value;
            }
         }
         // propriétés de second ordre (propriétés calculées à partir d'autres propriétés)
         public int Périmètre => Largeur * 2 + Hauteur * 2;
         public int Surface => Largeur * Hauteur;
         public Rectangle(int largeur, int hauteur) // constructeur
         {
            Largeur = largeur;
            Hauteur = hauteur;
         }
         //public void Gonfler()
         //{
         //   Hauteur *= 2;
         //   Largeur *= 2;
         //}
         public void Dessiner() // méthode d'instance
         {
            for(int i = 0; i != Hauteur; ++i)
            {
               for (int j = 0; j != Largeur; ++j)
                  Console.Write('#');
               Console.WriteLine();
            }
         }
         public static Rectangle Gonfler(Rectangle r) => // méthode de classe (static)
            new Rectangle(r.Largeur * 2, r.Hauteur * 2);
      }
      // C# (et Java, et plusieurs autres) sont des langages qui reposent
      // sur un "ramasse-miettes" (anglais : Garbage Collector)
      static Rectangle CréerRectangle(int hau, int lar) // méthode de classe (static)
      {
         Rectangle r = new Rectangle(hau, lar);
         // ... utiliser r...
         return r;
      } // r meurt ici

      //static void Vilain(Rectangle r)
      //{
      //   r.Gonfler();
      //}

      static void Main(string[] args)
      {
         Rectangle r = CréerRectangle(4, 10);
         r.Dessiner();
         Rectangle r2 = Rectangle.Gonfler(r);
         r2.Dessiner();
         r.Dessiner();
         Console.WriteLine($"La surface de r est {r.Surface}");
         Console.WriteLine($"Le périmètre de r est {r.Périmètre}");
         //Rectangle r2 = new Rectangle(3, 5);
         //r2.Dessiner();
      } // r meurt
   }
}
      // ramasse-miettes (Garbage Collector) : mécanisme qui, à certains moments,
      // élimine les objets vers lesquels plus rien ne réfère
      static Rectangle CréerRectangle(int lar, int hau) // méthode de classe
      {
         Rectangle r = new Rectangle(lar, hau);
         // manipuler r...
         return r;
      } // r meurt ici
      static void Vilain(int n)
      {
         n *= 2;
      }
      //static void Vilain(Rectangle r)
      //{
      //   r.Largeur *= 2;
      //}
      static void Main(string[] args)
      {
         int n = 3;
         Console.WriteLine(n);
         Vilain(n);
         Console.WriteLine(n);
         Rectangle r = CréerRectangle(10,4);
         r.Dessiner();
         Rectangle g = Rectangle.Gonfler(r);
         g.Dessiner();
         r.Dessiner();
         Console.WriteLine($"Périmètre de r : {r.Périmètre}, périmètre de g : {g.Périmètre}");
         Console.WriteLine($"Surface de r : {r.Surface}, surface de g : {g.Surface}");
      } // r meurt, g aussi
   }
}

Le code de string et StringBuilder suit :

Groupe 1 Groupe 2
using System;
using System.Diagnostics; // Stopwatch
using System.Text; // StringBuilder
namespace z202gr1
{
   class Program
   {
      static string CréerChaîneLent(char c, int n)
      {
         string s = "";
         for (int i = 0; i < n; ++i)
            s += c;
         return s;
      }
      static string CréerChaîneRapide(char c, int n)
      {
         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < n; ++i)
            sb.Append(c);
         return sb.ToString();
      }
using System;
using System.Diagnostics; // Stopwatch
using System.Text; // StringBuilder
namespace z202gr2
{
   class Program
   {
      static string CréerChaîneLent(char c, int n)
      {
         string s = "";
         for (int i = 0; i < n; ++i)
            s += c;
         return s;
      }
      static string CréerChaîneRapide(char c, int n)
      {
         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < n; ++i)
            sb.Append(c);
         return sb.ToString();
      }
      static void Main(string[] args)
      {
         int n = 100;
         for(int i = 0; i != 5; ++i)
         {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            string s = CréerChaîneRapide('A', n); // ou CréerChapineLent
            sw.Stop();
            Console.WriteLine($"Chaîne de {n} caractères en {sw.ElapsedMilliseconds} ms");
            n *= 10;
         }
      }
   }
}
      static void Main(string[] args)
      {
         int n = 100;
         for(int i = 0; i != 5; ++i)
         {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            string s = CréerChaîneRapide('A', n); // ou CréerChaîneLent :)
            sw.Stop();
            Console.WriteLine($"CréerChaîneRapide('A',{n}) : {sw.ElapsedMilliseconds} ms");
            n *= 10;
         }
      }
   }
}

Le code du paquet de cartes que nous avons produit collectivement en classe est disponible sur PaquetCartes.html si vous souhaitez le consulter / le critiquer / le retravailler

Voir ceci de même que ceci pour des détails.

9 février

T02

Au menu :

  • Le mystère des pinouches blanches...

Ensuite, deux grandes thématiques :

  • Les exceptions, qui permettent entre autres de découpler la détection d'une situation atypique (une erreur, dans la grande majorité des cas) de son traitement. Plus en détail :
    • Pourquoi distinguer le moment / le lieu où un problème est détecté du moment / du lieu où il est traité (le cas échéant)
    • Trois mots clés : throw (signaler une situation exceptionelle), try (exécuter du code à risque, avec pour objectif de réagir si un problème survient), catch (gérer la situation signalée)
      • Un quatrième mot, finally, est très important (plus que catch!), mais nous y reviendrons
    • Exceptions et postconditions
    • Exceptions et respect des invariants
    • Exceptions et constructeurs
  • L'héritage d'implémentation (dans le respect des limites de C#), effleuré seulement, par lequel une classe peut être une spécialisation d'une autre
    • Relation entre parent et enfant
    • Impact des qualifications private et public
    • Héritage et construction
    • Le mot clé base (introduction)

S'il reste du temps, ce sera pour travailler sur le labo du Mastermind.

Pour des exemples de classes utilisant des exceptions pour fins de validation, voir :

Pour l'exemple de division entière :

// ...
class DivisionParZéroException : Exception // en pratique, préférez la classe standard DivideByZeroException :)
{
}
static int DivisionEntière(int num, int dénom)
{
   if(dénom == 0)
      throw new DivisionParZéroException();
   return num / dénom;
}
// ...
static void Main()
{
   Console.Write("Numérateur? ");
   int n = int.Parse(Console.ReadLine());
   Console.Write("Dénominateur? ");
   int d = int.Parse(Console.ReadLine());
   //
   // Exemple : appel sans try... catch... Rapide, mais plante si une exception est levée
   //
   Console.WriteLine($"{n} / {d} == {DivisionEntière(n,d)}");
   //
   // Exemple : appel avec try... catch...
   //
   try
   {
      Console.WriteLine($"{n} / {d} == {DivisionEntière(n,d)}");
   }
   catch(DivisionParZéroException)
   {
      Console.WriteLine("Ouf, on l'a échappé belle!");
   }
}
// ...

11 février

L02

Cours en présence Pour des raisons hors de mon contrôle, le premier cours en présence est reporté à L03.

N'oubliez pas de remettre le code de votre Mastermind aujourd'hui!

Au menu : des exercices, des exercices, et des exercices!

Exercice 1a

Soit une classe représentant un point de l'espace 2D. Chaque instance de cette classe :

  • Comporte deux attributs réels : un x et un y
  • Donne accès en lecture aux valeurs de x et y par le moyen d'une propriété X et Y respectivement
  • Donne accès en écriture aux valeurs de x et de y par le moyen des propriétés X et Y sans les valider puisque toute valeur est acceptable
  • Comporte un constructeur paramétrique qui s'assure que l'instance en création est dans un état valide
  • Comporte un constructeur par défaut
  • Offre une fonction pour calculer la distance entre deux points

Travail à réaliser :

  • En précisant leur qualificateur d'accès, et en précisant s'il s'agit de membres d'instance (non-static) ou de membres de classe (static) :
    • Dressez la liste des attributs de cette classe
    • Dressez la liste des méthodes de cette classe
    • Dressez la liste des propriétés de cette classe
  • Rédigez cette classe et testez-la à l'aide du code client disponible sur le site Web du cours.

Exercice 1b

Soit une classe représentant un point du quadrant 1 de l'espace 2D. Chaque instance de cette classe :

  • Comporte deux attributs réels : un x et un y
  • Donne accès en lecture aux valeurs de x et y par le moyen d'une propriété X et Y respectivement
  • Donne accès en écriture aux valeurs de x et de y par le moyen des propriétés X et Y en validant que la valeur est valide pour le quadrant 1
  • Comporte un constructeur paramétrique qui s'assure que l'instance en création est dans un état valide

En cas d'erreur du programme client dans l'utilisation d'une instance de cette classe, la classe doit level une exception :

  • Dans le cas où le code client tenterait de mettre une valeur négative en X, votre classe devra lever une exception de type CoordonnéeXInvalideException
  • Dans le cas où le code client tenterait de mettre une valeur négative en Y, votre classe devra lever une exception de type CoordonnéeYInvalideException

Travail à réaliser :

  • En précisant leur qualificateur d'accès, et en précisant s'il s'agit de membres d'instance (non-static) ou de membres de classe (static) :
    • Dressez la liste des attributs de cette classe
    • Dressez la liste des méthodes de cette classe
    • Dressez la liste des propriétés de cette classe
  • Rédigez cette classe et testez-la à l'aide du code client disponible sur le site Web du cours.

Exercice 2

Soit une classe représentant un compte bancaire (très simple). Les instances de cette classe devront offrir les services suivants :

  • Une propriété Solde permettant d'en connaître le solde
  • Une méthode Déposer permettant d'effectuer un dépôt
  • Une méthode Retirer permettant d'effectuer un retrait
  • Un constructeur par défaut
  • Un constructeur paramétrique permettant de préciser le solde initial du compte

Vous devez faire en sorte que par défaut, ce compte bancaire soit créé avec un solde à zéro; si le constructeur paramétrique est utilisé, la valeur précisée lors du processus d'instanciation doit être positive ou égale à 0.

Les méthodes permettant d'effectuer les dépôts et les retraits recevront en paramètre le montant à déposer ou à retirer selon le cas, qui doit être un entier positif strictement plus grand que 0, et ne retourneront rien.

Ce type de compte bancaire a un invariant : il ne pourra à aucun moment avoir un solde négatif. En vertu de l'encapsulation, vous devez vous assurer du respect de cet invariant, donc faire en sorte qu'en tout temps votre objet reste valide.

Dans le cas où le code client tenterait de créer une instance en y attribuant un solde négatif, votre classe devra lever une exception de type SoldeInvalideException.

Dans le cas où le code client tenterait de retirer un montant supérieur au solde du compte, ou encore de déposer un montant négatif ou nul, votre objet devra évidemment refuser l'opération et lever une exception de type OpérationInvalideException.

Travail à réaliser :

  • En précisant leur qualificateur d'accès, et en précisant s'il s'agit de membres d'instance (non-static) ou de membres de classe (static) :
    • Dressez la liste des attributs de cette classe
    • Dressez la liste des méthodes de cette classe
    • Dressez la liste des propriétés de cette classe
  • Rédigez cette classe et testez-la.

Une solution possible serait ceci : ClasseCompteBancaire.html

Exercice 3

Plus difficile, car moins directif. Vous développez un jeu de impliquant des héros et des monstres. Les règles sont les suivantes :

  • Tout héros a des points de vie, représentés par un nombre entier
  • Tout héros a un nom
  • Tout monstre a des points de vie, représentés par un nombre entier
  • Tout monstre a un nom
  • Le nom d'un monstre doit être d'une longueur maximale de cinq caractères, et ne doit contenir que des consonnes
  • À la construction, un héros a un nombre de points de vie choisi aléatoirement entre 50 et 100 inclusivement
  • À la construction, un monstre a un nombre de points de vie indiqué par un paramètre passé au constructeur
  • Tout héros a une force, un entier dont la valeur est indiquée par un paramètre passé à la construction. Cette valeur doit être entre 10 et 20 inclusivement
  • Un héros peut frapper un autre personnage. Ceci blessera ce personnage en réduisant ses points de vie par une valeur pseudoaléatoire entre et de la force du héros
  • Tout monstre a une force, un entier dont la valeur est indiquée par un paramètre passé à la construction. Cette valeur doit être entre 15 et 25 inclusivement
  • Un monstre peut frapper un autre personnage. Ceci blessera ce personnage en réduisant ses points de vie par une valeur pseudoaléatoire entre et de la force du monstre
  • Un héros est un personnage
  • Un monstre est un personnage
  • Si les points de vie d'un personnage sont inférieurs ou égaux à zéro, alors ce personnage est mort, sinon il est vivant

Travail à réaliser :

  • Rédigez les classes Héros et Monstre
  • Identifiez ce qu'elles ont en commun et placez ces attributs, propriétés et méthodes dans une classe Personnage qui leur servira toutes deux de parent
  • Identifiez les attributs, propriétés, méthodes et constructeurs de chacune des classes que vous envisagez
  • Écrivez un petit programme de test représentant un combat entre un Héros et un Monstre et faisant la démonstration que votre design fonctionne, et respecte les consignes

Exercice 4

Sachant que la couleur de l'affichage de texte dans un écran console est donnée par la propriété Console.ForegroundColor, et sachant qu'il est possible de positionner le curseur où l'affichage de texte se fera à l'aide de la méthode Console.SetCursorPosition, écrivez une classe Carré telle que :

  • Un Carré a une position décrite par un point 2D
    • Note : à l'écran console, le point est le coin en haut et à gauche de l'écran, et les coordonnées en et en sont positives vers la droite et vers le bas respectivement
  • Un Carré a une longueur de côté
  • Un Carré a une couleur, de type ConsoleColor
  • Les caractéristiques d'une instance de Carré sont déterminées à la construction de cet objet
  • Dessiner un Carré dessinera ce carré à la position choisie, de la taille choisie, et à la couleur choisie

Travail à réaliser :

  • Rédigez la classe Carré
  • Identifiez les attributs, propriétés, méthodes et constructeurs de cette classe
  • Écrivez un programme de test dans lequel on trouvera un tableau de références sur des Carré et qui, en itérant à travers ce tableau, affichera ces divers objets à l'écran

16 février

T03

Au menu :

  • Retour sur l'exercice 3 de la séance L02
  • Retour sur l'exercice 4 de la séance L02
  • Petit quiz de vocabulaire. Soit le code ci-dessous :
using System;
public class Program
{
   class ContientBlancsException : Exception
   {
   }
   class MotVideException : Exception
   {
   }
   class Mot
   {
      private string valeur;
      public string Valeur
      {
         get => valeur;
         private set
         {
            if (value == null || value.Length == 0)
            {
               throw new MotVideException();
            }
            if (Contient(value.ToCharArray(), ' '))
            {
               throw new ContientBlancsException();
            }
            valeur = value.ToLower();
         }
      }
      public int Longueur => Valeur.Length;
      public Mot(string valeur)
      {
         Valeur = valeur;
      }
      private static char[] ObtenirVoyelles() => new char[] { 'a', 'e', 'i', 'o', 'u', 'y' };
      public int NbVoyelles => CompterOccurrences(Valeur, ObtenirVoyelles());
      public int NbConsonnes => Longueur - NbVoyelles;
      private static bool Contient(char [] cs, char c)
      {
         for(int i = 0; i != cs.Length; ++i)
            if (cs[i] == c)
               return true;
         return false;
      }
      private static int CompterOccurrences(string chaîne, char [] caractères)
      {
         int n = 0;
         foreach(char c in châine)
            if (Contient(caractères, c))
               ++n;
         return n;
      }
      private static int TrouverPremièreDifférence(string s0, string s1)
      {
         int plusPetit = Math.Min(s0.Length, s1.Length);
         int i = 0;
         while (i != plusPetit && s0[i] == s1[i])
         {
            ++i;
         }
         return i;
      }
      public bool Précède(Mot autre)
      {
         int pos = TrouverPremièreDifférence(Valeur, autre.Valeur);
         /*
         bool résultat;
         if (pos == Math.Min(Longueur, autre.Longueur))
         {
            résultat = Longueur < autre.Longueur;
         }
         else
         {
            résultat = Valeur[pos] < autre.Valeur[pos];
         }
         return résultat;
         */
         return pos == Math.Min(Longueur, autre.Longueur) ? Longueur < autre.Longueur : Valeur[pos] < autre.Valeur[pos];
      }
   }
   public static void Main()
   {
      // ...
   }
}

Répondez aux questions suivantes :

  • Que représente une instance de la classe Mot? Soyez le plus précis possible
  • Quelles sont les règles qui assurent la validité d'un Mot? Soyez le plus précis possible
  • Quelles sont les méthodes d'instance de la classe Mot? (listez leurs noms seulement)
  • Quelles sont les méthodes de classe de la classe Mot? (listez leurs noms seulement)
  • Quelles sont les propriétés d'une instance de la classe Mot? (listez leurs noms seulement)
  • Quels sont les attributs d'une instance de la classe Mot? (listez leurs noms seulement)
  • Dans Main, créez deux instances m0 et m1 de Mot avec des chaînes de caractères valides et distinctes l'une de l'autre, puis appelez la méthode Précède correctement pour ensuite afficher laquelle de ces deux instances de Mot apparaîtrait en premier dans un dictionnaire
  • Est-ce que NbConsonnes donnerait la bonne valeur pour un Mot créé de la manière suivante : new Mot("plate-forme")? Expliquez votre réponse
  • Est-ce que NbVoyelles donnerait la bonne valeur pour un Mot créé de la manière suivante : new Mot("Yogourt") ? Expliquez votre réponse

À titre de solutionnaire pour cet exercice :

  • Une instance de la classe Mot représente une chaîne de caractères non-nulle et non-vide, ne contenant que des lettres minuscules et exempte du caractère ' '
  • Un Mot est toujours valide, car son constructeur (et ses propriétés) le garantissent. Cela dit, une string utilisée pour construire un Mot est acceptable si elle est non-nulle, non-vide et est exempte du caractère ' '. Notez que cette string peut contenir d'autres caractères que des lettres minuscules, car la transformation en minuscules est faite par l'instance de Mot elle-même
  • La classe Mot a les méthodes d'instance suivantes : Précède et (à la limite) le constructeur paramétrique

À titre de rappel, une méthode d'instance est une fonction membre non-static

  • La classe Mot a les méthodes de classe suivantes :
    • ObtenirVoyelles
    • Contient
    • CompterOccurrences
    • TrouverPremièreDifférence

À titre de rappel, une méthode de classe est une fonction membre static

  • La classe Mot a les propriétés d'instance suivantes :
    • Valeur
    • Longueur
    • NbVoyelles
    • NbConsonnes

À titre de rappel, une propriété d'instance est une pseudo-variable membre non-static offrant un contrôle d'accès en lecture (get) ou en écriture (set, init)... ou les deux

  • La classe Mot a l'attribut d'instance suivant : valeur

À titre de rappel, un attribut d'instance est variable membre non-static

  • Pour la question suivante : « Dans Main, créez deux instances m0 et m1 de Mot avec des chaînes de caractères valides et distinctes l'une de l'autre, puis appelez la méthode Précède correctement pour ensuite afficher laquelle de ces deux instances de Mot apparaîtrait en premier dans un dictionnaire », une solution possible serait :
// ...
static void Main()
{
   Mot m0 = new Mot(Console.ReadLine()); // n'entrez pas d'espace!
   Mot m1 = new Mot(Console.ReadLine()); // n'entrez pas d'espace non plus!
   if(m0.Précède(m1))
   {
      Console.WriteLine($"{m0.Valeur} précède {m1.Valeur}");
   }
   else
   {
      Console.WriteLine($"{m1.Valeur} précède {m0.Valeur}");
   }
}
  • Pour la question suivante : « Est-ce que NbConsonnes donnerait la bonne valeur pour un Mot créé de la manière suivante : new Mot("plate-forme")? », la réponse est non, car le mot contient un '-' et la propriété NbConsonnes comptera ce caractère comme une consonne (c'est un bogue qu'il faudrait corriger)
  • Pour la question suivante : « Est-ce que NbVoyelles donnerait la bonne valeur pour un Mot créé de la manière suivante : new Mot("Yogourt") ? », la réponse est oui car, même si les voyelles considérées sont toutes minuscules alors que le 'Y' de "Yogourt" est majuscule, le set de la propriété Valeur transforme la chaîne en minuscules avant de la déposer dans l'attribut valeur

Pour ce que nous avons fait en classe :

Quoi? Groupe 1 Groupe 2
Exercice 3
using System;
using System.Diagnostics;

namespace z202gr1
{
   class Algos // un peu vague, mais bon, pour cette fois...
   {
      public static bool EstVoyelle(char c)
      {
         c = char.ToUpper(c);
         string voyelles = "AEIOUY";
         // for(int i = 0; i != voyelles.Length; ++i)
         // {
         //    char ch = voyelles[i]
         //    ...
         // }
         foreach (char ch in voyelles)
            if (c == ch)
               return true;
         return false;
      }
      public static int CompterVoyelles(string s)
      {
         int n = 0;
         foreach (char c in s)
            if (EstVoyelle(c))
               ++n;
         return n;
      }
   }
   class Personnage
   {
      public int PointsVie
      {
         get;
         private set;
      }
      public bool EstVivant => PointsVie > 0;
      public bool EstMort => !EstVivant;
      public Personnage(int vie)
      {
         PointsVie = vie;
      }
      public void Blesser(int dégâts)
      {
         PointsVie -= dégâts;
      }
   }
   //
   // new Héros(...)
   //  ==> créer la base (la partie Personnage)
   //  ==> créer les attributs du Héros
   class Héros : Personnage
   {
      const int VIE_MIN = 50,
                VIE_MAX = 100;
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      static Random CréerDé () => new Random();
      int force;
      public int Force
      {
         get => force;
         private init
         {
            force = FORCE_MIN <= value && value <= FORCE_MAX ? value : throw new ArgumentException();
         }
      }
      public Héros(string nom, int force) : base(CréerDé().Next(VIE_MIN, VIE_MAX + 1))
      {
         Nom = nom;
         Force = force;
      }
      public string Nom
      {
         get;
         private init;
      }
      public void Frapper(Personnage ennemi)
      {
         int dégâts = (int)(new Random().Next(50, 100 + 1) / 100.0 * Force);
         // int dégâts = (int)(new Random().NextDouble() % 0.5 + 0.5 * Force);
         ennemi.Blesser(dégâts);
      }
   }
   class Monstre : Personnage
   {
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstNomValide(string s) =>
         s.Length <= 5 && Algos.CompterVoyelles(s) == 0;
      string nom;
      public string Nom
      {
         get => nom;
         private init
         {
            nom = EstNomValide(value) ? value : throw new ArgumentException();
         }
      }
      int force;
      public int Force
      {
         get => force;
         private init
         {
            force = FORCE_MIN <= value && value <= FORCE_MAX ? value : throw new ArgumentException();
         }
      }
      public Monstre(string nom, int force, int vie) : base(vie)
      {
         Nom = nom;
         Force = force;
      }
      public void Frapper(Personnage ennemi)
      {
         int dégâts = (int) (new Random().Next(50, 75 + 1) / 100.0 * Force);
         // int dégâts = (int)(new Random().NextDouble() % 0.25 + 0.5 * Force);
         ennemi.Blesser(dégâts);
      }
   }
   class Program
   {
      static void Main()
      {
         Héros héros = new Héros("Robaire", 18);
         Monstre monstre = new Monstre("GRFKZ", 23, 66);
         bool tourHéros = new Random().Next() % 2 == 0;
         while(héros.EstVivant && monstre.EstVivant)
         {
            if(tourHéros)
            {
               Console.WriteLine($"Avant que {héros.Nom} ne frappe {monstre.Nom}, ce dernier a {monstre.PointsVie} vie");
               héros.Frapper(monstre);
               Console.WriteLine($"Après que {héros.Nom} ne frappe {monstre.Nom}, ce dernier a {monstre.PointsVie} vie");
            }
            else
            {
               Console.WriteLine($"Avant que {monstre.Nom} ne frappe {héros.Nom}, ce dernier a {héros.PointsVie} vie");
               monstre.Frapper(héros);
               Console.WriteLine($"Après que {monstre.Nom} ne frappe {héros.Nom}, ce dernier a {héros.PointsVie} vie");
            }
            Console.WriteLine(new string('-', 70));
            tourHéros = !tourHéros;
         }
         if (héros.EstVivant)
            Console.WriteLine($"Victoire de {héros.Nom}");
         else
            Console.WriteLine($"Victoire de {monstre.Nom}");
      }
   }
}
using System;
using System.Diagnostics;

namespace z202gr2
{
   class Algos // nom à retravailler
   {
      public static bool EstVoyelle(char c)
      {
         c = char.ToUpper(c);
         string voyelles = "AEIOUY";
         foreach (char ch in voyelles)
            if (c == ch)
               return true;
         return false;
      }
      public static int CompterVoyelles(string s)
      {
         int n = 0;
         foreach (char c in s)
            if (EstVoyelle(c))
               ++n;
         return n;
      }
   }
   class Personnage
   {
      public int PtsVie
      {
         get;
         private set;
      }
      public bool EstMort => PtsVie <= 0;
      public bool EstVivant => !EstMort;
      public Personnage(int vie)
      {
         PtsVie = vie;
      }
      public void Blesser(int dégâts)
      {
         PtsVie -= dégâts;
      }
   }
   class Héros : Personnage
   {
      const int VIE_MIN = 50,
                VIE_MAX = 100;
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      public string Nom
      {
         get;
         private init;
      }
      int force;
      public int Force
      {
         get => force;
         private init
         {
            force = FORCE_MIN <= value && value <= FORCE_MAX ? value : throw new ArgumentException();
         }
      }
      // Random Dé { get; } = new Random();
      static Random CréerDé() => new Random();
      public Héros(string nom, int force) : base(CréerDé().Next(VIE_MIN, VIE_MAX + 1))
      {
         Nom = nom;
         Force = force;
      }
      public void Frapper(Personnage ennemi)
      {
         int dégâts = (int)(new Random().Next(50, 100 + 1) * Force / 100.0);
         // int dégâts = (int)((new Random().NextDouble() % 0.5 + 0.5) * Force);
         ennemi.Blesser(dégâts);
      }
   }
   class Monstre : Personnage
   {
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstNomValide(string s) =>
         s.Length <= 5 && Algos.CompterVoyelles(s) == 0;
      string nom;
      public string Nom
      {
         get => nom;
         private init
         {
            nom = EstNomValide(value) ? value : throw new ArgumentException();
         }
      }
      int force;
      public int Force
      {
         get => force;
         private init
         {
            force = FORCE_MIN <= value && value <= FORCE_MAX ? value : throw new ArgumentException();
         }
      }
      public Monstre(string nom, int force, int vie) : base(vie)
      {
         Nom = nom;
         Force = force;
      }
      public void Frapper(Personnage ennemi)
      {
         int dégâts = (int)(new Random().Next(50, 75 + 1) * Force / 100.0);
         // int dégâts = (int)((new Random().NextDouble() % 0.25 + 0.5) * Force);
         ennemi.Blesser(dégâts);
      }
   }
   class Program
   {
      static void Main()
      {
         Héros héros = new Héros("Luke", 18);
         Monstre monstre = new Monstre("GRGHK", 23, 80);
         bool tourHéros = new Random().Next() % 2 == 0;
         while(héros.EstVivant && monstre.EstVivant)
         {
            if(tourHéros)
            {
               Console.WriteLine($"Avant l'assaut, {héros.Nom} a {héros.PtsVie} vie et {monstre.Nom} a {monstre.PtsVie}");
               héros.Frapper(monstre);
               Console.WriteLine($"Après l'assaut, {héros.Nom} a {héros.PtsVie} vie et {monstre.Nom} a {monstre.PtsVie}");
            }
            else
            {
               Console.WriteLine($"Avant l'assaut, {monstre.Nom} a {monstre.PtsVie} vie et {héros.Nom} a {héros.PtsVie}");
               monstre.Frapper(héros);
               Console.WriteLine($"Après l'assaut, {monstre.Nom} a {monstre.PtsVie} vie et {héros.Nom} a {héros.PtsVie}");
            }
            tourHéros = !tourHéros;
            Console.WriteLine(new string('-', 70));
         }
         if (héros.EstVivant)
            Console.WriteLine($"Victoire de {héros.Nom}");
         else
            Console.WriteLine($"Victoire de {monstre.Nom}");
      }
   }
}
Exercice 4
using System;
using System.Diagnostics;

namespace z202gr1
{
   class Program
   {
      public class Point
      {
         public int X { get; set; }
         public int Y { get; set; }
         public Point() : this(0,0)
         {
         }
         public Point(int x, int y)
         {
            X = x;
            Y = y;
         }
      }
      public class Carré
      {
         Point position;
         public Point Position
         {
            get => new Point(position.X, position.Y);
            private set
            {
               position = value;
            }
         }
         public int Côté
         {
            get; private init;
         }
         public ConsoleColor Couleur
         {
            get; private init;
         }
         public Carré(Point position, int côté, ConsoleColor couleur)
         {
            Position = position;
            Côté = côté;
            Couleur = couleur;
         }
         public void Dessiner()
         {
            ConsoleColor avant = Console.ForegroundColor;
            // ...
            Console.ForegroundColor = Couleur;
            for (int ligne = 0; ligne != Côté; ++ligne)
               for (int colonne = 0; colonne != Côté; ++colonne)
               {
                  Console.SetCursorPosition(colonne + Position.X, ligne + Position.Y);
                  Console.Write('#');
               }
            // ...
            Console.ForegroundColor = avant;
         }
         public void Déplacer(int dx, int dy)
         {
            Position.X += dx;
            Position.Y += dy;
         }
      }
      static void Main()
      {
         Carré[] carrés = new Carré[]
         {
            new Carré(new Point(), 3, ConsoleColor.Yellow),
            new Carré(new Point(5, 2), 8, ConsoleColor.Red),
            new Carré(new Point(7, 10), 5, ConsoleColor.Cyan)
         };
         foreach (Carré c in carrés)
            c.Dessiner();
         for(int i = 0; i != 10; ++i)
         {
            System.Threading.Thread.Sleep(500);
            Console.Clear();
            foreach (Carré c in carrés)
            {
               c.Déplacer(2, 1);
               c.Dessiner();
            }
         }
      }
   }
}
using System;
using System.Diagnostics;

namespace z202gr2
{
   class Program
   {
      class Point
      {
         public int X { get; set; }
         public int Y { get; set; }
         public Point() : this(0,0)
         {
         }
         public Point(int x, int y)
         {
            X = x;
            Y = y;
         }
      }
      class Carré
      {
         public int Côté
         {
            get; private init;
         }
         private Point position;
         public Point Position
         {
            get => new Point(position.X, position.Y);
            private set
            {
               position = value;
            }
         }
         public ConsoleColor Couleur
         {
            get; private init;
         }
         public Carré(Point position, int côté, ConsoleColor couleur)
         {
            Position = position;
            Côté = côté;
            Couleur = couleur;
         }
         public void Dessiner()
         {
            ConsoleColor avant = Console.ForegroundColor;
            // ...
            Console.ForegroundColor = Couleur;
            for(int ligne = 0; ligne != Côté; ++ligne)
               for(int colonne = 0; colonne != Côté; ++colonne)
               {
                  Console.SetCursorPosition(Position.X + colonne, Position.Y + ligne);
                  Console.Write('#');
               }
            // ...
            Console.ForegroundColor = avant;
         }
         public void Déplacer (int dx, int dy)
         {
            Position.X += dx;
            Position.Y += dy;
         }
      }
      static void Main()
      {
         Carré[] carrés = new Carré[]
         {
            new Carré(new Point(), 3, ConsoleColor.Yellow),
            new Carré(new Point(10, 10), 8, ConsoleColor.Red),
            new Carré(new Point(5, 7), 8, ConsoleColor.Blue)
         };

         foreach (Carré c in carrés)
            c.Dessiner();
         for(int i = 0; i != 10; ++i)
         {
            System.Threading.Thread.Sleep(1000);
            Console.Clear();
            foreach (Carré c in carrés)
            {
               c.Déplacer(3, 0);
               c.Position.X += 3; // sans effet sur c :)
               c.Dessiner();
            }
         }
      }
   }
}

Une variante sur le thème de l'exerice 3 serait ce programme mettant en relief la tension entre le bien et le mal.

Nous avons survolé la question des relations entre classes et objets. Sommairement :

Héritage (d'implémentation)

  • Relation au sens du verbe être
  • Un Héros est un Personnage

Quelques exemples :

// ...
class Personnage
{
   // ...
   public int Vie { get; private set; }
   public string Nom{ get; }
   // ...
}
class Héros : Personnage 
{
   // ...
   public int Force{ get; private init; }
   // ...
}
class Bibliothécaire : Personnage 
{
   // ...
}
static void F(Personnage p)
{
   // ...
   Console.Write(p.Vie); // Ok
   Console.Write(p.Nom); // Ok
   // Console.Write(p.Force); // Non
}
static void G(Héros h)
{
   // ...
}
static void Main()
{
   Héros h = new Héros();
   F(h); // Ok
   F(new Bibliothécaire()); // Ok
   G(h); // Ok
   Personnage p = new Personnage();
   p = h; // Ok
   G(p); // non
}
// ...

Composition / agrégation

  • Relations au sens du verbe avoir
  • Composition : la vie de l'objet englobant ne chevauche pas la vie de l'objet englobé
  • Agrégation : La vie de l'objet englobant chevauche / peut chevaucher la vie de l'objet englobé
  • Un Carré a une Position
  • Un Carré possède une Position

Association

  • Deux objets se connaissent (mutuellement ou pas)
  • Pour deux objets A et B :
    • A connaît B, ou
    • B connaît A, ou
    • A et B se connaissent mutuellement

Couplage

  • Le couplage est l'impact qu'une modification apportée à un objet ou à une classe aura sur le reste du programme (sur d'autres classes, sur d'autres fonctions, etc.)
  • Exprimé autrement, le couplage est ce qui fait qu'un changement apporté à un objet / une classe a des répercussions ailleurs
  • On veut :
    • Un faible couplage
    • Une forte cohérence

18 février

L03

Au menu :

  • Q00
  • Récit d'un programme
    Récit d'un programme
  • Relations entre objets (première approche) :
    • Association
    • Composition
    • Agrégation
    • Choix d'une relation et idée de couplage
    • Principe de faible couplage et de forte cohésion
  • Quelques très petits éléments de notation UML :
    • Boîte décrivant une classe ou une de ses instances
    • Symbole + pour un membre public
    • Symbole - pour un membre privé
    • Nom de la classe
    • Noms des attributs
    • Signatures des méthodes
    • Soulignement pour les membres de classe
  • Énumérations (parce que c'était pas du connu)
  • Classes statiques
  • Si le temps le permet, tri avec comparateurs

23 février

T04

Au menu :

  • Correction de Q00
  • Suivi de la question du tri avec comparateurs (ou : comment rendre le comparateur plus charmant)
    • Qu'est-ce qu'un three-way-compare?
    • Comment l'implémenter efficacement?
  • Introduction au polymorphisme

Le chemin que nous avons suivi fut « classique » :

  • Une Forme a une Couleur
  • Un Carré est une Forme
  • Un Carré peut se dessiner
  • Un Rectangle est une Forme
  • Un Rectangle peut se dessiner

Ce serait cool de pouvoir écrire une fonction Dessiner(Forme f) qui (a) fait une copie de sauvegarde de la couleur du texte à l'écran, (b) la remplace par la couleur de la Forme, (c) dessine la Forme, puis (d) remet la couleur originale pour le texte à l'écran... sauf que f.Dessiner n'existe pas.

Pour résoudre ce problème, nous avons d'abord inséré une méthode Dessiner non virtuelle dans Forme, pour se rendre compte que ça ne fait rien de pertinent.

Nous avons ensuite virtual (dans la classe Forme) et override (dans les classes Carré et Rectangle), suite à quoi l'affichage s'est mis à dessiner des trucs colorés à l'écran, pour notre plus grand bonheur!

Une question soulevée par la classe fut : pourquoi Forme.Dessiner ne fait-elle pas elle-même la petite danse qui implique mettre la couleur en place avant affichange, puis remettre la couleur d'origine après affichage? Pour répondre à cette question, nous avons examiné l'idiome NVI par lequel :

  • On place une méthode publique non-virtuelle Dessiner dans Forme
  • On place une méthode (publique pour commencer) virtuelle DessinerImpl dans Forme
  • On spécialise la méthode DessinerImpl dans les enfants (ceci montre au passage l'impact de override quand on change le nom de la méthode à spécialiser).

Le parent fait le pré-travail, appelle l'implémentation polymorphique de DessinerImpl, puis « défait » le pré-travail. Autre avantage de l'idiome : la méthode publique et non-virtuelle sert à titre de point d'entrée unique, ce qui facilite le débogage.

Nous avons ensuite abordé la question de DessinerImpl qui, si elle est publique, permet de contourner l'idiome NVI, et nous avons introduit la qualification d'accès protected. Attention de ne pas abuser de protected; l'utilité de cette qualification est réelle, mais limitée et nichée.

Enfin, la question de Forme.DessinerImpl a été posée : quel code devrait-on y insérer? Et la réponse est simple : rien de pertinent, car c'est une méthode bien trop abstraite... Ceci nous a fait amener le mot abstract dans la discussion.

En bout de ligne, on arrive à quelque chose comme :

using System;

namespace z202gr1
{
   class Program
   {
      // classe abstraite : a au moins une méthode abstraite et ne peut pas être instanciée
      abstract class Forme
      {
         public ConsoleColor Couleur { get; private init; }
         public Forme(ConsoleColor couleur)
         {
            Couleur = couleur;
         }
         // méthode virtuelle : _peut_ être spécialisée par les enfants
         // méthode abstraite : _doit_ être spécialisée par les enfants
         protected abstract void DessinerImpl();
         // idiome NVI pour Non-Virtual Interface
         public void Dessiner()
         {
            // travail préparatoire : on installe les préconditions
            ConsoleColor pre = Console.ForegroundColor;
            Console.ForegroundColor = Couleur;
            // travail effectif, spécialisé par les enfants
            DessinerImpl();
            // travail "terminal" : on valide les postconditions, etc.
            Console.ForegroundColor = pre;
         }
      }
      class Carré : Forme
      {
         int côté;
         public int Côté
         {
            get => côté;
            private init
            {
               côté = value > 0 ? value : throw new ArgumentException();
            }
         }
         public Carré(ConsoleColor couleur, int côté) : base(couleur)
         {
            Côté = côté;
         }
         // override : je fais le choix explicite de spécialiser une méthode
         // d'un parent (ici : Forme.Dessiner)
         protected override void DessinerImpl()
         {
            for (int i = 0; i != Côté; ++i)
            {
               for (int j = 0; j != Côté; ++j)
                  Console.Write('#');
               Console.WriteLine();
            }
         }
      }
      class Triangle : Forme
      {
         int hauteur;
         public int Hauteur
         {
            get => hauteur;
            private init
            {
               hauteur = value > 0 ? value : throw new ArgumentException();
            }
         }
         public Triangle(ConsoleColor couleur, int hauteur) : base(couleur)
         {
            Hauteur = hauteur;
         }
         protected override void DessinerImpl()
         {
            for (int i = 0; i != Hauteur; ++i)
            {
               for (int j = 0; j <= i; ++j)
                  Console.Write('A');
               Console.WriteLine();
            }
         }
      }
      // invariants :
      // - Largeur strictement positive
      // - Hauteur strictement positive
      class Rectangle : Forme
      {
         int largeur;
         int hauteur;
         public int Largeur
         {
            get => largeur;
            private init
            {
               largeur = value > 0 ? value : throw new ArgumentException();
            }
         }
         public int Hauteur
         {
            get => hauteur;
            private init
            {
               hauteur = value > 0 ? value : throw new ArgumentException();
            }
         }
         public Rectangle(ConsoleColor couleur, int largeur, int hauteur) : base(couleur)
         {
            Largeur = largeur;
            Hauteur = hauteur;
         }
         protected override void DessinerImpl()
         {
            for (int i = 0; i != Hauteur; ++i)
            {
               for (int j = 0; j != Largeur; ++j)
                  Console.Write('%');
               Console.WriteLine();
            }
         }
      }
      // Polymorphisme : à partir d'une abstraction (ici : Forme), on
      // appelle le service de l'objet réellement pointé (p.ex. Carré)
      static void Dessiner(Forme f)
      {
         f.Dessiner();
      }
      static void Main()
      {
         Carré c = new Carré(ConsoleColor.Red, 5);
         //c.Dessiner();
         Rectangle r = new Rectangle(ConsoleColor.Yellow, 10, 4);
         //r.Dessiner();
         Triangle t = new Triangle(ConsoleColor.Green, 7);
         Dessiner(c);
         Dessiner(r);
         Dessiner(t);
         // Dessiner(new Forme(ConsoleColor.Magenta)); // illégal (Forme est abstraite)
      }
   }
}

En fin de compte, le prof a fait la démonstration qu'un Carré n'est pas un Rectangle, sur la base du principe de Liskov (le 'L' de SOLID), que l'on doit à Barbara Liskov.

Résumé des nouvelles idées de ce (gros!) cours :

Héritage d'implémentation (enfant est un cas particulier du parent). Bon, c'est pas nouveau d'aujourd'hui, mais...

Polymorphisme :

  • Par une abstraction (p.ex. : le parent), on appelle le service le plus spécialisé de l'objet pointé (p.ex. : enfant)
  • virtual (l'enfant peut spécialiser la méthode)
  • override (l'enfant choisit de spécialiser le service)

Abstraction :

  • Une méthode abstract doit être spécialisée par l'enfant
  • Une classe avec au moins une méthode abstract est une classe abstraite (abstract)

Idiome NVI :

  • Méthode publique non-virtuelle dans le parent (point d'entrée unique)
  • Méthode virtuelle (ou abstraite) protégée, spécialisée par les enfants

Principe de Liskov (principe de substitution) :

  • L'héritage public (le seul que supporte C#) ne tient la route que si l'enfant et le parent ont les mêmes invariants
  • P.ex.: un Carré n'est pas un Rectangle (désolé!)

Faut mettre le tout en pratique, mais heureusement, le laboratoire 01 s'en vient...

25 février

L04

Au menu :

  • Trucs remarqués lors de la correction de Q00 :
    • Syntaxe des propriétés (confusion)
    • Types des propriétés
    • Syntaxe d'une levée d'exception
    • Types de retour des fonctions
    • Opérateurs relationnels (brrrr...)
    • Multiples points de sortie acceptables... et inacceptables
    • Lecture des consignes (ou : comment se simplifier soi-même la vie!)
    • Syntaxe des constructeurs
    • Importance des majuscules et des minuscules (et : comment dompter Word!)
    • ... faut pratiquer plus!
  • Introduction à List<T>, qui vous sera utile sous peu...
    • https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-5.0
    • Ce type est dans System.Collections.Generic (vous voudrez donc un using pour en profiter, sinon le nom sera long à écrire)
    • Il offre l'accès à un élément avec [], comme un tableau ou une string
    • Il permet d'ajouter un élément à la fin avec Add
    • Il permet de savoir combien d'éléments ont été ajoutés avec Count
    • Il permet d'enlever un élément avec Remove ou RemoveAt
    • Il permet de savoir si la collection contient un élément avec Contains
    • etc.
  • Présentation du laboratoire 01 : générateurs d'identifiants. Faut pratiquer, après tout!
    • Notez que vous n'aurez pas de tri à faire dans ce travail pratique (ce sera pour la prochaine fois), car j'ai changé d'idée sur le thème à la dernière minute

2 mars

 

Jour de mise à niveau (cours suspendus)

4 mars

 

Jour de mise à niveau (cours suspendus)

9 mars

T05

Au menu : retour sur le laboratoire 00

  • Quelques remarques d'ordre général...

À propos du recours aux constantes symboliques

Vous avez tendance à abuser de constantes littérales (3, 4, 7, 8, etc.). Nommez les constantes qui le méritent, et utilisez ces noms par la suite

À propos du formatage de messages de manière à faciliter l'entretien et la mise à jour

La plupart d'entre vous oublient d'utiliser ces constantes dans le formatage de leurs chaînes de caractères. Par exemple, au lieu de ceci :

Console.WriteLine("Entrez une chaîne de 4 caractères");

... préférez :

Console.WriteLine($"Entrez une chaîne de {Mastermind.NB_CHEVILLES} caractères");

À propos de la différence entre valeur et sens

Deux constantes peuvent avoir la même valeur mais pas le même sens; dans ce cas, il ne faut pas les confondre. Je pense en particulier au nombre de couleurs et à la longueur d'une combinaison : dans ce travail, les deux valaient quatre, mais on pourrait changer l'une sans changer l'autre alors les confondre, c'est une bien mauvaise idée!

À propos des fonctions

Les fonctions void devraient être relativement rares dans un programme (il peut y en avoir, mais peu). Leur rôle n'est pas clair (que produisent-elles comme résultat?), et le fait qu'elles ne retournent rien signifie que soit vous affichez, soit leur effet est de modifier une variable invisible (un attribut?). Ça rend l'entretien du code inutilement complexe et obscur.

Si vous avez une fonction qui affiche et fait d'autres traitements, elle en fait probablement trop.

À propos du principe de localité

Petite discussion en vue...

Pour le reste :

11 mars

L05

Au menu :

  • Q01
  • Exemple de tri composite
  • Duplication d'objets polymorphiques (clonage)
  • Deux petits bonbons :
    • La méthode ToString de la classe object, que nos classes peuvent spécialiser
    • Le mot clé var, pour alléger (parfois) les déclarations de variables

Un exemple de tri d'instances de Personne à l'aide d'une List<Personne> suit :

using System;
using System.Collections.Generic;

public class Program
{
   class Personne : IComparable<Personne>
   {
      public string Nom { get; }
      public string Prénom { get; }
      public Personne(string nom, string prénom)
      {
         Nom = nom;
         Prénom = prénom;
      }
      public int CompareTo(Personne p)
      {
         int résultat = Nom.CompareTo(p.Nom);
         return résultat != 0 ? résultat : Prénom.CompareTo(p.Prénom);
      }
   }
   // static int Comparer(Personne p0, Personne p1) => p0.Nom.CompareTo(p1.Nom);
   public static void Main()
   {
      List<Personne> euxZautres = new List<Personne>()
      {
         new Personne("Gariépy", "Bill"),
         new Personne("Mama", "Joe"),
         new Personne("LeBrico", "Gaétan"),
         new Personne("LeBrico", "Bob"),
         new Personne("L'Éponge", "Bob"),
         new Personne("LeTimide", "Timmy")
      };
      foreach (Personne p in euxZautres)
         Console.WriteLine($"{p.Nom} {p.Prénom}");
      Console.WriteLine();
      euxZautres.Sort();
      // euxZautres.Sort(Comparer);
      foreach (Personne p in euxZautres)
         Console.WriteLine($"{p.Nom} {p.Prénom}");
      Console.WriteLine();
   }
}

L'exemple de clonage (version finale) fait en classe est :

using System;

public class Program
{
   abstract class Image
   {
      public abstract Image Cloner();
      public ConsoleColor Couleur { get; set; }
      public string Path { get; private init; } // nom de fichier de l'image
      // autres guidis propres à toute image, quel que
      // soit son format
      protected Image(string path) // un choix (protected Ok : classe abstrait)
      {
         Path = path;
         Couleur = ConsoleColor.Cyan;
      }
      protected Image(Image autre) // protected est la chose à faire ici
      {
         Path = autre.Path;
         Couleur = autre.Couleur;
      }
      public abstract void Dessiner();
   }
   class JPeg : Image
   {
      public override JPeg Cloner()
      {
         return new JPeg(this);
      }
      protected JPeg(JPeg autre) : base(autre)
      {
         Sorte = autre.Sorte;
         Hauteur = autre.Hauteur;
         Largeur = autre.Largeur;
      }
      public string Sorte { get; private set; }
      public int Hauteur { get; init; }
      public int Largeur { get; init; }
      // guidis propres à un JPeg
      public JPeg(string path, int hauteur, int largeur) : base(path)
      {
         Sorte = "JPeg";
         Hauteur = hauteur;
         Largeur = largeur;
      }
      public override void Dessiner()
      {
         Console.WriteLine($"{Sorte} {Hauteur}x{Largeur} de nom {Path} et de couleur {Couleur}");
      }
   }
   class Png : Image
   {
      public override Png Cloner()
      {
         return new Png(this);
      }
      public string Sorte { get; private set; }
      // guidis propres à un Png
      public Png(string path) : base(path)
      {
         Sorte = "Png";
      }
      protected Png(Png autre) : base(autre)
      {
         Sorte = autre.Sorte;
      }
      public override void Dessiner()
      {
         Console.WriteLine($"{Sorte} de nom {Path} et de couleur {Couleur}");
      }
   }
   // extrapolez : Bmp, Gif, Ppm, etc.
   static Image ModifierPeutÊtre(Image img)
   {
      // A) faire un backup de img
      Image backup = img.Cloner(); // img, donne-moi un double de toi
      // B) modifier img
      img.Couleur = ConsoleColor.Red;
      // C) demander à l'usager s'il veut conserver les modifs
      Console.Write("Conserver les modifs? ");
      char réponse = char.Parse(Console.ReadLine());
      // D) si oui, retourner img
      // E) sinon, retourner backup
      return char.ToUpper(réponse) == 'O'? img : backup;
   }

   static void Main()
   {
      Image img = new JPeg("image.jpeg", 10, 5);
      img.Dessiner();
      img = ModifierPeutÊtre(img);
      img.Dessiner();
      img = new Png("William.png");
      img.Dessiner();
      img = ModifierPeutÊtre(img);
      img.Dessiner();
   }
}

N'oubliez pas de remettre le laboratoire 01!

16 mars

T06

Au menu :

  • Retour sur Q01
  • Qu'est-ce qu'une structure de données?
    • Quelques exemples classiques de structures de données (pile, file)
  • Exemple concret : une liste simplement chaînée de string
    • Organisation interne des données
    • Interface d'utilisation (à ne pas confondre avec le mot clé interface)
  • Opérations clés :
    • Constructeur par défaut (rôle? sens?)
    • Est-elle vide?
    • Ajouter un élément (à quel endroit?)
    • Supprimer un élément
    • Taille (exprimée en termes de nombre d'éléments)
    • Comparer (p. ex. : implémenter IComparable<ListeString>)
    • Dupliquer la collection
  • Opérations auxiliaires :
    • Afficher les éléments à la console
    • Construire à partir d'un string[]

Ce que nous faisons aujourd'hui est incomplet et perfectible, mais c'est un premier pas vers quelque chose de plutôt amusant... et de très, très important!

Les classes que nous avons faites ce matin sont : une ListeString :

   class ListeVideException : Exception { }
   // modélise une liste simplement chaînée dont chaque noeud
   // "contient" une string
   class ListeString : IComparable<ListeString>
   {
      public int CompareTo(ListeString autre)
      {
         // on veut comparer (avec CompareTo(string)) les valeurs des
         // noeuds, une paire à la fois. Dès qu'on remarque une
         // différence, on en retourne la valeur
         var p = Tête;
         var q = autre.Tête;
         // ...
         while(p != null && q != null)
         {
            int comp = p.Valeur.CompareTo(q.Valeur);
            if (comp != 0)
               return comp;
            p = p.Succ;
            q = q.Succ;
         }
         if (p == null && q == null)
            return 0;
         if (p == null)
            return -1;
         return 1;
      }
      class Noeud
      {
         public Noeud Succ { get; set; } = null;
         public string Valeur { get; private init; }
         public Noeud(string valeur)
         {
            Valeur = valeur;
         }
      }
      Noeud Tête { get; set; }
      Noeud Queue { get; set; } // optimisation pour connaître le dernier noeud
                                // coût : grossit un peu chaque ListeString
      public ListeString() // modélise une liste vide
      {
         Tête = null;
         Queue = null;
         Count = 0;
      }
      public bool EstVide => Tête == null; // ou Count == 0
      public void AjouterDébut(string valeur)
      {
         var p = new Noeud(valeur);
         if (EstVide)
            Queue = p;
         p.Succ = Tête;
         Tête = p;
         ++Count;
      }
      public void AjouterFin(string valeur)
      {
         if (EstVide)
            AjouterDébut(valeur);
         else
         {
            Queue.Succ = new Noeud(valeur);
            Queue = Queue.Succ;
            ++Count;
         }
      }
      public void SupprimerDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         Tête = Tête.Succ;
         if (Tête == null)
            Queue = null;
         --Count;
      }
      public void Afficher()
      {
         for (var p = Tête; p != null; p = p.Succ)
            Console.Write($"{p.Valeur} ");
         Console.WriteLine();
      }
      public int Count
      {
         get; private set;
      }
      public ListeString(ListeString autre) : this() // appelle d'abord le ctor par défaut
      {
         for (var p = autre.Tête; p != null; p = p.Succ)
            AjouterFin(p.Valeur);
      }
      public string ConsulterDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         return Tête.Valeur;
      }
      public ListeString(string []strs) :this()
      {
         foreach (string s in strs)
            AjouterFin(s);
      }
   }

... sur laquelle nous avons conçu une PileString (rappel : une pile est une structure de données LIFO, pour Last in, First out) :

   class PileString
   {
      ListeString Substrat { get; } = new ListeString();
      public void Push(string s)
      {
         Substrat.AjouterDébut(s);
      }
      public string Pop()
      {
         string s = Peek();
         Substrat.SupprimerDébut();
         return s;
      }
      public string Peek() => Substrat.ConsulterDébut();
      public bool EstVide => Substrat.EstVide;
   }

... nous avons aussi fait une PileString basée sur une List<string> pour montrer que le concept est (en partie) indépendant du substrat :

   class PileString
   {
      List<string> Substrat { get; } = new List<string>();
      public void Push(string s)
      {
         Substrat.Add(s);
      }
      public string Pop()
      {
         string s = Peek() ;
         Substrat.RemoveAt(Substrat.Count - 1);
         return s;
      }
      public string Peek() => Substrat[Substrat.Count - 1];
      public bool EstVide => Substrat.Count == 0;
   }

... puis nous avons conçu une FileString (rappel : une file est une structure de données FIFO, pour First in, First out):

   class FileString
   {
      ListeString Substrat { get; } = new ListeString();
      public void Push(string s)
      {
         Substrat.AjouterFin(s);
      }
      public string Pop()
      {
         string s = Substrat.ConsulterDébut();
         Substrat.SupprimerDébut();
         return s;
      }
      public string Peek() => Substrat.ConsulterDébut();
      public bool EstVide => Substrat.EstVide;
   }

18 mars

L06

Au menu :

  • Remarques sur la remise des minitests
    • Mettez votre nom dans le nom du document
    • ... mettez votre nom dans le document!
  • Ne confondez pas abstract (doit spécialiser) et virtual (peut spécialiser). Les mots sont importants, en langue naturelle (français, anglais) comme en langue formelle (maths, C++, C#, Java, etc.)
  • Retour sur le fonctionnement des appels de constructeurs
    • Sens de base
    • Sens de this
    • Ce qui se passe dans Q01

Note : il y aura un minitest Q02 à L07

Ensuite : on code Labo 01 ensemble!

À la fin : petite introduction au code générique. On retravaille ListeString pour en faire une Liste<T> pour un type T quelconque. Ça nous donne ce qui suit :

using System;
using System.Collections.Generic;
namespace z
{
   class ListeVideException : Exception { }
   // modélise une liste simplement chaînée dont chaque noeud
   // "contient" un T
   class Liste<T> : IComparable<Liste<T>> where T : IComparable<T>
   {
      public int CompareTo(Liste<T> autre)
      {
         // on veut comparer (avec CompareTo(T)) les valeurs des
         // noeuds, une paire à la fois. Dès qu'on remarque une
         // différence, on en retourne la valeur
         var p = Tête;
         var q = autre.Tête;
         // ...
         while (p != null && q != null)
         {
            int comp = p.Valeur.CompareTo(q.Valeur);
            if (comp != 0)
               return comp;
            p = p.Succ;
            q = q.Succ;
         }
         if (p == null && q == null)
            return 0;
         if (p == null)
            return -1;
         return 1;
      }
      class Noeud
      {
         public Noeud Succ { get; set; } = null;
         public T Valeur { get; private init; }
         public Noeud(T valeur)
         {
            Valeur = valeur;
         }
      }
      Noeud Tête { get; set; }
      Noeud Queue { get; set; } // optimisation pour connaître le dernier noeud
                                // coût : grossit un peu chaque Liste<T>
      public Liste() // modélise une liste vide
      {
         Tête = null;
         Queue = null;
         Count = 0;
      }
      public bool EstVide => Tête == null; // ou Count == 0
      public void AjouterDébut(T valeur)
      {
         var p = new Noeud(valeur);
         if (EstVide)
            Queue = p;
         p.Succ = Tête;
         Tête = p;
         ++Count;
      }
      public void AjouterFin(T valeur)
      {
         if (EstVide)
            AjouterDébut(valeur);
         else
         {
            Queue.Succ = new Noeud(valeur);
            Queue = Queue.Succ;
            ++Count;
         }
      }
      public void SupprimerDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         Tête = Tête.Succ;
         if (Tête == null)
            Queue = null;
         --Count;
      }
      public void Afficher()
      {
         for (var p = Tête; p != null; p = p.Succ)
            Console.Write($"{p.Valeur} ");
         Console.WriteLine();
      }
      public int Count
      {
         get; private set;
      }
      public Liste(Liste<T> autre) : this() // appelle d'abord le ctor par défaut
      {
         for (var p = autre.Tête; p != null; p = p.Succ)
            AjouterFin(p.Valeur);
      }
      public T ConsulterDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         return Tête.Valeur;
      }
      public Liste(T[] objs) : this()
      {
         foreach (T x in objs)
            AjouterFin(x);
      }
   }

   class Pile<T> where T : IComparable<T>
   {
      Liste<T> Substrat { get; } = new Liste<T>();
      public void Push(T elem)
      {
         Substrat.AjouterDébut(elem);
      }
      public T Pop()
      {
         T obj = Substrat.ConsulterDébut();
         Substrat.SupprimerDébut();
         return obj;
      }
      public T Peek() => Substrat.ConsulterDébut();
   }
   class Program
   {
      static void Afficher<T>(T [] tab)
      {
         foreach (T n in tab)
            Console.Write($"{n} ");
         Console.WriteLine();
      }
      static void Main(string[] args)
      {
         var lst0 = new Liste<string>();
         string[] strs = new[] { "allo", "toi", "genre" };
         Afficher(strs);
         foreach (string s in strs)
            lst0.AjouterFin(s);
         lst0.Afficher();
         var lst1 = new Liste<int>();
         int[] ints = new[] { 2,3,5,7,11 };
         Afficher(ints);
         foreach (int n in ints)
            lst1.AjouterFin(n);
         lst1.Afficher();
      }
   }
}

23 mars

T07

Au menu :

25 mars

L07

Au menu :

Le code de la Pile<T> de capacité fixe tel que fait en classe est ci-dessous :

class PileVideException : Exception { }
class PilePleineException : Exception { }
class Pile<T>
{
   T [] Substrat { get; init; }
   int Tête { get; set; }
   public Pile(int capacité)
   {
      Substrat = new T[capacité];
      Tête = 0;
   }
   public bool EstVide => Tête == 0;
   public bool EstPlein => Tête == Substrat.Length;
   public void Push(T val)
   {
      if (EstPlein)
         throw new PilePleineException();
      Substrat[Tête] = val;
      ++Tête;
   }
   public T Peek()
   {
      if (EstVide)
         throw new PileVideException();
      return Substrat[Tête - 1];
   }
   public T Pop()
   {
      if (EstVide)
         throw new PileVideException();
      T val = Substrat[Tête - 1];
      --Tête;
      return val;
   }
}

30 mars

L08

Au menu :

Attention : mardi selon l'horaire du jeudi

1 avril

 

Journée pédagogique (cours suspendus)

6 avril

T08

Au menu :

  • Une Liste<T> qui est IEnumerable<T> (pour être parcourue avec foreach)
  • Comment harmoniser notre Liste<T> avec les autres collections de C#
    • Construire à partir d'un IEnumerable<T>
    • Opérer sur des IEnumerable<T>
  • Travail sur le labo 02

Le code de la Liste<T> énumérable, donc implémentant IEnumerable<T>, suit :

      class ListeVideException : Exception { }
      // modélise une liste simplement chaînée dont chaque noeud
      // "contient" un T
      class Liste<T> : IEnumerable<T>, IComparable<Liste<T>> where T : IComparable<T>
      {
         class Énumérateur : IEnumerator<T>
         {
            Noeud Courant { get; set; }
            public Énumérateur(Noeud tête)
            {
               Courant = new Noeud(default); // noeud bidon, "avant" la tête
               Courant.Succ = tête;
            }
            public bool MoveNext()
            {
               if (Courant.Succ == null)
                  return false;
               Courant = Courant.Succ;
               return true;
            }
            public void Reset() { }
            public void Dispose() { }
            public T Current => Courant.Valeur;
            object IEnumerator.Current => Current;
         }

         public int CompareTo(Liste<T> autre)
         {
            var p = Tête;
            var q = autre.Tête;
            // ...
            while (p != null && q != null)
            {
               int comp = p.Valeur.CompareTo(q.Valeur);
               if (comp != 0)
                  return comp;
               p = p.Succ;
               q = q.Succ;
            }
            if (p == null && q == null)
               return 0;
            if (p == null)
               return -1;
            return 1;
         }
         class Noeud
         {
            public Noeud Succ { get; set; } = null;
            public T Valeur { get; private init; }
            public Noeud(T valeur)
            {
               Valeur = valeur;
            }
         }
         Noeud Tête { get; set; }
         Noeud Queue { get; set; }
         public Liste()
         {
            Tête = null;
            Queue = null;
            Count = 0;
         }
         public bool EstVide => Tête == null;
         public void AjouterDébut(T valeur)
         {
            var p = new Noeud(valeur);
            if (EstVide)
               Queue = p;
            p.Succ = Tête;
            Tête = p;
            ++Count;
         }
         public void AjouterFin(T valeur)
         {
            if (EstVide)
               AjouterDébut(valeur);
            else
            {
               Queue.Succ = new Noeud(valeur);
               Queue = Queue.Succ;
               ++Count;
            }
         }
         public void SupprimerDébut()
         {
            if (EstVide)
               throw new ListeVideException();
            Tête = Tête.Succ;
            if (Tête == null)
               Queue = null;
            --Count;
         }
         public void Afficher()
         {
            for (var p = Tête; p != null; p = p.Succ)
               Console.Write($"{p.Valeur} ");
            Console.WriteLine();
         }
         public int Count
         {
            get; private set;
         }
         public void AddRange(IEnumerable<T> autre)
         {
            foreach (T elem in autre)
               AjouterFin(elem);
         }
         public Liste(IEnumerable<T> autre) : this()
         {
            AddRange(autre);
         }
         public T ConsulterDébut()
         {
            if (EstVide)
               throw new ListeVideException();
            return Tête.Valeur;
         }
         public IEnumerator<T> GetEnumerator() =>
            new Énumérateur(Tête);
         IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
      }

8 avril

L09

Au menu :

  • Aperçu du reste de la session :
    • Séance T09 mardi prochain
    • Prochains minitests (Q03, Q04, Q05)
    • Dernier laboratoire (proposé à L10)
    • PFI (à venir)
  • Principes SOLID :
    • Single Responsibility Principle (une classe, une vocation)
    • Open-Closed Principle (une classe est ouverte pour extensions, mais fermée pour modifications)
    • Liskov Substitution Principle (l'héritage d'implémentation fonctionne si l'enfant et le parent partagent les mêmes invariants)
    • Interface Segregation Principle (mieux vaut des interfaces spécifiques plutôt qu'une interface trop générale)
    • Dependency Inversion Principle (mieux vaut écrire du code qui dépende d'abstraction que du code qui dépende de « concrétions » – notez que le terme « concrétions » est un anglicisme ici)
  • Méthodes d'extension
  • Introduction à LiNQ
  • Exercices

Le premier exercice va comme suit :

Écrivez un programme qui :

  • À partir d'une séquence d'entiers de valeurs 1 à 100 :
var lst = new List<int>();
for(int i = 1; i <= 100; ++i)
   lst.Add(i);
  • Place les pairs au début et les impairs à la fin (OrderBy)
  • Élimine les multiples de 3 (Where)
  • Inverse l'ordre des éléments restants (Reverse)
  • Trouve les dix premiers de la liste résultante (Take)
  • Affiche la somme de leurs valeurs (Sum)

Solution : (à venir) https://dotnetfiddle.net/VD4ftJ

Le second exercice va comme suit :

Écrivez un programme qui :

  • À partir d'un tableau de paires (nom,prénom) d'étudiant(e)s :
(string nom, string prénom) [] gens = new[]
{
   ("Nguyen", "Bill"), ("roy", "Patrice"), ("Roberge", "William"),
   ("Rousseau", "Nathan"), ("gendron", "Étienne"),
   ("barakatt", "maxime"), ("Charland", "Alexandre")
   ("Griffith", "Kieran"), ("Rivest", "Thomas"), ("Fréchette", "Jérôme")
};
  • Élimine ceux dont le nom ne débute pas par une majuscule (Where/IsUpper/IsLower)
  • Trie les (nom,prénom) restants en ordre lexicographique de nom (OrderBy)
  • Affiche les trois derniers (TakeLast)

Solution : (à venir) https://dotnetfiddle.net/a4vsKy

13 avril

T09

Au menu :

  • Rendez-vous individuels de 10 à 12 minutes par Zoom pour chacune et chacun d'entre vous
  • L'horaire des rencontres vous sera envoyé par Colnet
  • On fera le tour de vos labos 01 et labo 02 ensemble

15 avril

L10

Au menu :

  • Q03
  • Main et ses paramètres
  • Entrées / sorties sur des flux
    • Type StreamReader
    • Type StreamWriter
    • Petit exemple d'un programme qui copie le contenu d'un fichier texte
  • Mot clé finally
  • Interface IDisposable
  • Blocs using
    • Petit exemple d'un programme qui copie le contenu d'un fichier texte (revisité)
  • Méthode Split du type string
  • Les dictionnaires (type Dictionary<K,V>)
  • Présentation informelle du labo 03, qui impliquera des entrées / sorties sur un flux et de la manipulation de chaînes de caractères
    • La présentation officielle du labo 03 se fera à la séance T10

Pour un exemple de fonction lisant un fichier

20 avril

T10

Au menu :

22 avril

L11

Au menu :

27 avril

T11

Au menu :

L'exemple de classe Tableau vu en classe fut :

class Tableau : IEquatable<Tableau>
{
   public bool EstVide => Length == 0;
   // modélise un tableau vide
   public Tableau()
   {
      Length = 0;
      elems = null;
   }
   // modélise un tableau de taille éléments de valeur valInit
   public Tableau(int taille, int valInit = 0)
   {
      Length = taille;
      elems = new int[Length];
      for (int i = 0; i != Length; ++i)
         elems[i] = valInit;
   }
   int[] elems;
   public int Length { get; private set; }
   public int this[int indice]
   {
      get => elems[indice];
      set
      {
         elems[indice] = value;
      }
   }
   public bool Equals(Tableau autre)
   {
      if (Length != autre.Length)
         return false;
      for (int i = 0; i != Length; ++i)
         if (this[i] != autre[i])
            return false;
      return true;
   }
   public override bool Equals(object obj) =>
      obj is Tableau autre && Equals(autre);
   public static bool operator ==(Tableau t0, Tableau t1) => t0.Equals(t1);
   public static bool operator !=(Tableau t0, Tableau t1) =>
      !(t0 == t1);
   public override int GetHashCode() =>
      base.GetHashCode(); // bof...
   public static bool operator<(Tableau t0, Tableau t1)
   {
      int seuil = Math.Min(t0.Length, t1.Length);
      for (int i = 0; i < seuil; ++i)
         if (t0[i] != t1[i])
            return t0[i] < t1[i];
      return t0.Length < t1.Length;
   }
   public static bool operator >(Tableau t0, Tableau t1) => t1 < t0;
   public static bool operator <=(Tableau t0, Tableau t1) => !(t1 < t0);
   public static bool operator >=(Tableau t0, Tableau t1) => !(t0 < t1);
}

Pour vous pratiquer :

  • Transformez cette classe en classe générique (Tableau<T> plutôt que Tableau de int)
  • Ajoutez une méthode Add (comment la rendre efficace?)
  • Faites en sorte qu'un Tableau<T> implémente IComparable<T> (et repensez les opérateurs relationnels sur cette base)
  • Faites en sorte qu'un Tableau<T> implémente IEnumerable<T>

L'exemple de classe Entier vu en classe fut :

class Entier
{
   public int Valeur { get; set; }
   public Entier() : this(0)
   {
   }
   public Entier(int val)
   {
      Valeur = val;
   }
   public static Entier operator +(Entier e0, Entier e1) =>
      new Entier(e0.Valeur + e1.Valeur);
   public static Entier operator -(Entier e0, Entier e1) =>
      new Entier(e0.Valeur - e1.Valeur);
   public static Entier operator ++(Entier e) => new Entier(e.Valeur + 1);
   public static explicit operator int(Entier e) => e.Valeur;
   public override string ToString() => Valeur.ToString();
}

Pour vous pratiquer :

  • Adaptez Entier pour construire la classe EntierNaturel, représentant un entier du domaine ℕ
  • Adaptez Entier pour construire la classe EntierModulaire qui permet d'exprimer une arithmétique modulo un certain entier. Par exemple, un EntierModulaire(n,10) permettra de représenter des entiers de 0 à 9 inclusivement de manière telle que :
var e0 = new EntierModulaire(9,10);
var e1 = new EntierModulaire(1,10);
// 9 mod 10 + 1 mod 10 == 0 mod 10
Console.WriteLine($"{e0} + {e1} == {e0 + e1}");

... affiche 9 mod 10 + 1 mod 10 == 0 mod 10

29 avril

L12

Au menu :

4 mai

T12

Au menu :

  • Travail sur la PFI

6 mai

L13

Au menu :

  • Comprendre Console.ReadKey
  • Introduction aux sélectives (switch)
  • Qu'est-ce qu'un « blanc » quand on parle de caractères?
  • Si la classe le souhaite, mini intro à la multiprogrammation
  • Travail sur la PFI

Pour un exemple de test de touches du clavier avec une séquence d'alternatives (if ... else if ... else if ... else), un seul point de sortie :

static string ChoisirMessage(ConsoleKey clé)
{
   string msg = "";
   if(clé == ConsoleKey.RightArrow || clé == ConsoleKey.D)
   {
      msg = "Est";
   }
   else if (clé == ConsoleKey.UpArrow || clé == ConsoleKey.W)
   {
      msg = "Nord";
   }
   else if (clé == ConsoleKey.LeftArrow || clé == ConsoleKey.A)
   {
      msg = "Ouest";
   }
   else if (clé == ConsoleKey.DownArrow || clé == ConsoleKey.S)
   {
      msg = "Sud";
   }
   else
   {
      msg = "... inconnu";
   }
   return msg;
}

Pour un exemple de test de touches du clavier avec une séquence d'alternatives (if ... else if ... else if ... else), multiple points de sortie :

static string ChoisirMessage(ConsoleKey clé)
{
   if(clé == ConsoleKey.RightArrow || clé == ConsoleKey.D)
      return "Est";
   if (clé == ConsoleKey.UpArrow || clé == ConsoleKey.W)
      return "Nord";
   if (clé == ConsoleKey.LeftArrow || clé == ConsoleKey.A)
      return "Ouest";
   if (clé == ConsoleKey.DownArrow || clé == ConsoleKey.S)
      return "Sud";
   return "... inconnu";
}

Pour un exemple équivalent avec une sélective (switch) classique, un seul point de sortie :

static string ChoisirMessage(ConsoleKey clé)
{
   string msg = "";
   switch(clé)
   {
      case ConsoleKey.RightArrow:
      case ConsoleKey.D:
         msg = "Est";
         break;
      case ConsoleKey.UpArrow:
      case ConsoleKey.W:
         msg = "Nord";
         break;
      case ConsoleKey.LeftArrow:
      case ConsoleKey.A:
         msg = "Ouest";
         break;
      case ConsoleKey.DownArrow:
      case ConsoleKey.S:
         msg = "Sud";
         break;
      default:
         msg = "... inconnu";
         break;
   }
   return msg;
}

Pour un exemple équivalent avec une sélective (switch) classique, plusieurs points de sortie :

static string ChoisirMessage(ConsoleKey clé)
{
   switch(clé)
   {
      case ConsoleKey.RightArrow:
      case ConsoleKey.D:
         return "Est";
      case ConsoleKey.UpArrow:
      case ConsoleKey.W:
         return "Nord";
      case ConsoleKey.LeftArrow:
      case ConsoleKey.A:
         return "Ouest";
      case ConsoleKey.DownArrow:
      case ConsoleKey.S:
         return "Sud";
      default:
         return  "... inconnu";
   }
}

Pour un exemple équivalent avec une expression sélective (switch en tant qu'expression) :

static string ChoisirMessage(ConsoleKey clé) =>
   clé switch
   {
      ConsoleKey.RightArrow => "Est",
      ConsoleKey.D => "Est",
      ConsoleKey.UpArrow => "Nord",
      ConsoleKey.W => "Nord",
      ConsoleKey.LeftArrow => "Ouest",
      ConsoleKey.A => "Ouest",
      ConsoleKey.DownArrow => "Sud",
      ConsoleKey.S => "Sud",
      _ => "... inconnu"
   }

Pour un exemple équivalent avec une expression sélective (switch en tant qu'expression), forme plus concise (C# permet d'utiliser or – attention, pas || – pour combiner des Patterns ici) :

static string ChoisirMessage(ConsoleKey clé) =>
   clé switch
   {
      ConsoleKey.RightArrow or ConsoleKey.D => "Est",
      ConsoleKey.UpArrow or ConsoleKey.W => "Nord",
      ConsoleKey.LeftArrow or ConsoleKey.A => "Ouest",
      ConsoleKey.DownArrow or ConsoleKey.S => "Sud",
      _ => "... inconnu"
   }

11 mai

T13

Au menu :

  • Q05
  • Présentation des types valeurs (les struct), en prévision de la prochaine session
  • Travail sur la PFI

13 mai

 

Journée d'examen de français / formation générale (cours suspendus)

18 mai

T14

Remise de la PFI

20 mai

L14

Examen final

Petits coups de pouces

Vous trouverez ici quelques documents, la plupart petits, qui peuvent vous donner un petit coup de pouce occasionnel.

Vous trouverez aussi des exemples de code C# dans la section Divers – C# du site, mais notez que je n'ai pas nécessairement harmonisé ces exemples (écrits pour des cours plus avancés, sous forme de survols) aux standards de programmation appliqués dans le présent cours. À lire avec prudence et discrimination, donc.

Travaux pratiques

Si vous cherchez les énoncés des travaux pratiques, vous pourrez entre autres les trouver ci-dessous.

Travail Nom Rôle

Labo 00

Mastermind

Travail sur l'encapsulation et la conception de classes

Labo 01

Générateurs

Travail sur l'héritage et le polymorphisme

Du code de test est disponible sur Labo01-Test.html

Labo 02

Deviner la structure de données

Travail sur les interfaces, l'encapsulation et le code générique, où il faut deviner des caractéristiques sur des structures de données à partir du comportement manifesté par l'mplémentation des services d'une interface

Du code de test est disponible sur Labo02-Test.html. Pour le code de test de la version « bonus », voir Labo02-Test.html#bonus

Labo 03

Entrées / sorties et transformations

Travail sur les dictionnaires, les fonctions génériques et les entrées / sorties

PFI

Production finale d'intégration

L'archive .zip contenant les fichiers à partir desquels la PFI doit être réalisée est PFI-420202-H2021.zip

Solutionnaires et exemples

Quelques solutionnaires suivent. En espérant que ça vous aide à organiser vos idées!

ExempleSéance

Classe Carré

T00

À venir

 

À venir

 


Valid XHTML 1.0 Transitional

CSS Valide !