Loading [MathJax]/jax/output/HTML-CSS/jax.js

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

Quelques raccourcis :

Ceci est un petit site de support pour le cours 420-SF2-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.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

21 janvier

S00

Au menu :

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

Notre exercice fut :

Écrivez un programme console C# qui :

  • Lit une hauteur de rectangle, qui doit être entre 1 et 20 inclusivement
  • Lit une largeur de rectangle, qui doit être entre 1 et 70 inclusivement
  • Lit un symbole qui servira à dessiner le rectangle. Ce symbole sera un caractère non-blanc (voir char.IsWhiteSpace pour valider ceci)

Ensuite, dessinez à l'écran le rectangle correspondant

Nous en avons profité pour faire une brève révision de 420SF1, puis pour apporter des ajustements menant à un glissement vers la matière au menu de notre cours.

Notre version initiale fut :

int hauteur = LireHauteur();
int largeur = LireLargeur();
char symbole = LireSymbole();
DessinerRectangle(hauteur, largeur, symbole);

static void DessinerRectangle(int hau, int lar, char sym)
{
   for(int ligne = 0; ligne != hau; ++ligne)
   {
      for(int col= 0; col != lar; ++col)
      {
         Console.Write(sym);
      }
      Console.WriteLine();
   }
}

static char LireSymbole()
{
   Console.Write("Symbole? ");
   char symbole = char.Parse(Console.ReadLine());
   while(char.IsWhiteSpace(symbole))
   {
      Console.WriteLine($"Entrée erronnée. Symbole? ");
      symbole = char.Parse(Console.ReadLine());
   }
   return symbole;
}


static int LireEntierBorné(string msg, int min, int max)
{
   int valeur;
   Console.Write($"{msg}? ");
   valeur = int.Parse(Console.ReadLine());
   while (!EstEntreInclusif(valeur, min, max))
   {
      Console.WriteLine($"Entrée erronnée. {msg}? ");
      valeur = int.Parse(Console.ReadLine());
   }
   return valeur;
}
static int LireHauteur()
{
   const int HAUTEUR_MIN = 1,
             HAUTEUR_MAX = 20;
   return LireEntierBorné("Hauteur", HAUTEUR_MIN, HAUTEUR_MAX);
}
static int LireLargeur()
{
   const int LARGEUR_MIN = 1,
             LARGEUR_MAX = 70;
   return LireEntierBorné("Largeur", LARGEUR_MIN, LARGEUR_MAX);
}
static bool EstEntreInclusif(int val, int min, int max)
{
   return min <= val && val <= max;
}

Nous avons ensuite introduit l'idée d'une classe Rectangle, pour que le code qui dessine des rectangles ait bel et bien... des rectangles! Le code du programme principal résultant fut :

using z;
Rectangle[] rects = new Rectangle[]
{
   new (5, 12, 'L'), new(7, 20, '0'), new(17, 43, '*')
};
for (int i = 0; i != rects.Length; ++i)
   rects[i].Dessiner();

... et celui de la classe Rectangle en tant que telle fut :

namespace z
{
   internal class Rectangle
   {
      // variables membres : attributs
      int hauteur;
      int largeur;
      char symbole;
      // propriété : mécanisme de contrôle d'accès
      // de fine granularité aux états d'un objet
      public int Hauteur
      {
         // permet de consulter la variable en lecture
         get { return hauteur; }
         private init /*set*/ // permet de modifier la variable
         {
            hauteur = value;
         }
      }
      public int Largeur
      {
         get { return largeur; }
         private init /*set*/ { largeur = value; }
      }
      public char Symbole
      {
         get { return symbole; }
         private set { symbole = value; }
      }

      // constructeur : méthode qui sert à placer
      // l'objet nouvellement construit dans un état
      // initial correct
      public Rectangle(int hau, int lar, char sym)
      {
         Hauteur = hau;
         Largeur = lar;
         Symbole = sym;
      }
      // méthode d'instance Dessiner, qui a pour rôle
      // d'afficher _ce_ Rectangle à la console
      public void Dessiner()
      {
         for (int ligne = 0; ligne != hauteur; ++ligne)
         {
            for (int col = 0; col != largeur; ++col)
            {
               Console.Write(symbole);
            }
            Console.WriteLine();
         }
      }
   }
}

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 (sans la nommer, mais vous ne perdez rien pour attendre!). Ainsi, nous avons identifié et situé les termes et idées suivant(e)s :
    • 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 et 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 la plupart des 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 f sur une entité obj, ce qui rejoint l'approche procédurale que nous avons mis en application dans le cours 420-SF1-RE, et obj.F(), qui sollicite la méthode f de l'objet obj 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
    • La partie init d'une propriété est aussi un exemple de mutateur, mais qui ne fonctionne que durant la construction d'un objet. Évidemment, une propriété ne peut pas avoir à la fois un set et un init (c'est l'un ou l'autre)
    • 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é très sommairement 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 »
// ...
Triangle t = new(5, 'A');
t.Dessiner();
// ...
    ... affichera à la console la chose suivante :
    A
   AAA
  AAAAA
 AAAAAAA
AAAAAAAAA

En fait, notre Triangle n'était pas centré lors de l'affichage, mais considérez cet ajout comme un bonbon!

À 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 :

static int Carré(int n)
{
   return n * n;
}

... peut s'écrire de manière équivalente sous la forme suivante :

static int Carré(int n) => n * n;

... alors que ceci :

class Nom
{
   const 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
{
   const 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é => Valeur.ToUpper(); // note : un get seulement, pas de set
   public Nom(string valeur)
   {
      Valeur = valeur;
   }
}

D'autres simplifications d'écriture viendront plus tard dans la session.

Quelques nouveaux termes de vocabulaire utilisés aujourd'hui : classe, instance, attribut d'instance, propriété d'instance (avec volets get, set et init), constructeur par défaut, constructeur paramétrique, qualifications d'accès private et public (il y en a d'autres), encapsulation (à peine), membre d'instance (non-static), membre de classe : static, invariant, précondition et postcondition... Ouf!

23 janvier

S01

Au menu :

  • Poursuite de la 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
    • Rappel d'une maxime importante : « un objet est responsable de son intégrité, du début à la fin de sa vie »
  • 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

Quelques nouveaux termes de vocabulaire utilisés aujourd'hui : invariant, précondition et postcondition

Une solution possible est disponible ici. Vous remarquerez que les mutateurs (les volets set et init des propriétés) sont exprimés un peu différemment sur cet exemple que ce que nous avons fait en classe, mais je vous explique ce que ça signifie dès la séance S02.

28 janvier

S02

Au menu :

  • Que faire pour signaler qu'une fonction ne pourra pas rencontrer ses postconditions : introduction (très brève) aux exceptions, un sujet important que nous abordons aujourd'hui mais aussi sur lequel nous reviendrons bientôt, et au mot clé throw

À titre bonbons aujourd'hui, j'ai aussi montré :

  • Comment utiliser l'opérateur ternaire dans les cas où nous souhaitons choisir l'une de deux expressions de même type 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
  • Présentation du labo 00 – Le cryptographe
  • Travail sur le labo 00 – Le cryptographe

Quelques trucs pour vous aider...

Si vous cherchez un exemple simpliste de programme de test interactif, vous pouvez utiliser celui-ci (vous pouvez aussi vous en écrire un plus à votre goût; je ne ramasserai que la classe Crypteur après tout) :

// ...
do
{
   Console.Write("Message à chiffrer? ");
   string texte = Console.ReadLine();
   Console.Write("Clé de chiffrement? ");
   int cléChiffrement = int.Parse(Console.ReadLine());
   Crypteur crypteur = new (texte, cléChiffrement);
   Console.Write("Clé à utiliser pour déchiffrer? ");
   int cléDéchiffrement = int.Parse(Console.ReadLine());
   Console.WriteLine($"Message \"déchiffré\" : {crypteur.Déchiffrer(cléDéchiffrement)}");
}
while (Poursuivre());
// ...

... notez que je suppose que vous pouvez écrire la fonction Poursuivre puisque nous l'avons fait à quelques reprises cet automne.

Si vous souhaitez transformer une string en char[], examinez les méthodes d'instance de cette string (portez attention en particulier à celles dont le nom commence par To...).

Si vous souhaitez transformer une string en majuscules, examinez les méthodes d'instance de cette string (portez attention en particulier à celles dont le nom commence par To...).

Si vous souhaitez transformer un char[] en string, sachez que string expose un constructeur paramétrique acceptant un char[] en paramètre. Par exemple, comparez les deux exemples ci-dessous (l'un des deux est plus pertinent que l'autre pour vos fins) :

// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = cs.ToString();
Console.Write(s);
// ...
// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = new string(cs);
Console.Write(s);
// ...

Si vous souhaitez modifier un char, alors... que diriez-vous de la simple arithmétique? Par exemple :

// ...
char c = 'A';
Console.WriteLine(c); // A
c = (char)(c + 3);
Console.WriteLine(c); // D
// ...

30 janvier

S03

Au menu :

  • Comment demander de l'aide
  • Comment convertir un objet en string, et pourquoi, si tabChar est un char[], l'expression tabChar.ToString() ne donne pas le même résultat que l'expression new string(tabChar) (note : nous y reviendrons)
  • Bref rappel sur 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 préconditions
    • Exceptions et postconditions
    • Exceptions et respect des invariants
    • Exceptions et constructeurs
  • Définir un type d'exception « maison »
    • Introduction à la syntaxe (qui, en C#, implique l'héritage d'implémentation, sujet que nous ne ferons qu'effleurer aujourd'hui)
  • Revisiter des exemples vus précédemment à la lueur de la matière d'aujourd'hui
  • De plus :
    • jasette informelle sur le choix d'une université (c'est le temps des portes ouvertes!)
    • mesurer l'impact en temps de levées d'exceptions
    • mesurer l'impact en temps d'ajouter un caractère à une string
    • pourquoi le type string est immuable en C#
    • pour celles et ceux qui ne l'ont pas vu la session précédente : la méthode TryParse
  • Quelques exemples concrets :
    • division entière avec levée d'exception
    • coût des exceptions
    • coût de l'ajout de caractères à une string (version naïve)
    • classe Cercle avec invariants
    • manipulation de string et passage de string à char[] (et inversement)

À titre bonbons aujourd'hui, j'ai aussi montré :

  • Le côté optionnel des accolades dans certaines structures de contrôle (if, for, while, foreach, mais pas try ni catch) quand le corps se limite à une seule instruction
  • Le droit à placer plus d'un return par fonction, mais seulement si c'est justifié
  • La possibilité d'utiliser des constructeurs de délégation quand le corps des constructeurs est répétitif

S'il reste du temps :

  • Travail sur le labo 00 – Le cryptographe

Le code proposé aujourd'hui était ce qui suit, ou une variante de ce qui suit (avec quelques menus ajouts) :

  • Exemple de division entière avec levée d'exception :
bool ok = false;
do
{
   try
   {
      Console.Write("Numérateur ?  ");
      int num = int.Parse(Console.ReadLine());
      Console.Write("Dénominateur? ");
      int denom = int.Parse(Console.ReadLine());
      Console.WriteLine($"{num} / {denom} == {DivisionEntière(num, denom)}");
      ok = true;
   }
   catch(FormatException)
   {
      Console.WriteLine("Oups, je refuse (pas un entier); on réessaie");
   }
   catch (DivisionParZéroException)
   {
      Console.WriteLine("Oups, je refuse (dénominateur nul); on réessaie");
   }
}
while (!ok);

// précondition : dénominateur != 0
// postcondition : retourne le quotient du numérateur par le dénominateur
// exception : la fonction ne peut pas satisfaire ses postconditions
//             (en gros, elle ne peut pas faire son travail)
static int DivisionEntière(int numérateur, int dénominateur)
{
   if(dénominateur == 0)
   {
      throw new DivisionParZéroException();
   }
   return numérateur / dénominateur;
}

class DivisionParZéroException : Exception { }
  • Mesurer le coût en temps de levées d'exceptions :
const int N = 300_000_000;
System.Diagnostics.Stopwatch sw = new();

int[] tab = CréerTableau(N);

sw.Start();
int combien = CompterImpairsPositifs(tab);
sw.Stop();
Console.WriteLine($"Compté {combien} impairs positifs et {sw.ElapsedMilliseconds} ms");


static bool EstImpairPositif(int n)
{
   if (n < 0)
   {
      throw new ZutException();
   }
   return n % 2 != 0;
}
static int CompterImpairsPositifs(int[] tab)
{
   int n = 0;
   foreach (int nb in tab)
   {
      try
      {
         if (EstImpairPositif(nb))
         {
            ++n;
         }
      }
      catch (ZutException)
      {
      }
   }
   return n;
}

static int[] CréerTableau(int n)
{
   int[] tab = new int[n];
   for (int i = 0; i != n; ++i)
   {
      tab[i] = i % 10 == 0 ? -(2 * i + 1) : 2 * i + 1;
   }
   return tab;
}

class ZutException : Exception { }
  • Mesurer le coût en temps d'ajout naïf de caractères à une string (rappel : en C#, les instances de string sont immuables, ce qui fait que l'expression s += c est équivalente à s = new string(...texte de s + caractère c...) ce qui crée à chaque itération une string plus grande que la précédente... Ouf!
System.Diagnostics.Stopwatch sw = new();
const int N = 10_000_000;

for(int i = 1; i <= N; i *= 10)
{
   sw.Start();
   string s = CréerChaîne(i, 'S');
   sw.Stop();
   Console.WriteLine($"Créé string de {s.Length} caractères en {sw.ElapsedMilliseconds} ms");
}

static string CréerChaîne(int n, char c)
{
   string s = "";
   for(int i = 0; i != n; ++i)
   {
      s += c;
   }
   return s;
}

Quelques exemples complémentaires, si ça vous intéresse :

  • Exemple de classe Cercle avec invariant sur le Rayon :
Cercle c0 = LireCercle();
Afficher(c0);

static void Afficher(Cercle c)
{
   Console.WriteLine($"Centre : {c.Centre.X},{c.Centre.Y}, rayon : {c.Rayon}");
}

static Point LirePoint()
{
   Console.Write("X? ");
   int x = int.Parse(Console.ReadLine());
   Console.Write("Y? ");
   int y = int.Parse(Console.ReadLine());
   return new(x, y);
}
static Cercle LireCercle()
{
   Cercle cercle = null;
   do
   {
      Point centre = LirePoint();
      Console.Write("Rayon? ");
      float rayon = float.Parse(Console.ReadLine());
      try
      {
         cercle = new(centre, rayon);
      }
      catch(RayonInvalideException)
      {
         Console.WriteLine("Rayon invalide, on recommence");
      }
   }
   while (cercle == null);
   return cercle;
}

class Point
{
   public double X { get; init; }
   public double Y { get; init; }
   public Point()
   {
      X = 0;
      Y = 0;
   }
   public Point(double x, double y)
   {
      X = x;
      Y = y;
   }
}

class RayonInvalideException : Exception { }

// invariant : Rayon > 0
class Cercle
{
   public Point Centre { get; init; }
   private float rayon;
   public float Rayon
   {
      get => rayon;
      set
      {
         if(value <= 0)
         {
            throw new RayonInvalideException();
         }
         rayon = value;
      }
   }
   public Cercle()
   {
      Centre = new();
      Rayon = 1.0f;
   }
   public Cercle(Point centre, float rayon)
   {
      Centre = centre;
      Rayon = rayon;
   }
}
  • Exemple (simpliste) de manipulation de chaînes de caractères :
string s = "J'aime mon prof";
Console.WriteLine(s);
s = s.ToUpper();
Console.WriteLine(s);
//for(int i = 0; i != s.Length; ++i)
//{
//   Console.Write($"{s[i]} ");
//}
foreach (char c in s)
{
   Console.Write($"{c} ");
}
Console.WriteLine();
char[] tab = s.ToArray();
for (int i = 0; i != tab.Length; ++i)
{
   if (char.IsWhiteSpace(tab[i]))
   {
      tab[i] = '#';
   }
}
// notez la différence entre les deux conversions ci-dessous...
s = new string(tab);
Console.WriteLine(s);
s = tab.ToString();
Console.WriteLine(s);

4 février

S04

Au menu :

  • Dernière séance pour fignoler le labo 00. C'est le moment idéal pour :
    • Vous assurer de respecter les consignes (relisez-les avec attention!)
    • Tester les cas limites (p. ex. : notre Crypteur rejette les chaînes vides, mais il reste que chiffrer une chaîne vide n'est pas une erreur... Ça donne simplement une autre chaîne vide alors faudrait que votre algorithme tienne la route!)
    • Si vous ne l'avez pas encore fait, vous pouvez essayer les tests proposés dans l'énoncé... Si vous ne passez pas tous les tests, c'est qu'il vous reste encore au moins un bogue (et si vous passez tous les tests, ça ne veut pas dire que le code est propre! 🙂)

Vous pouvez aussi essayer le programme de test suivant (ajoutez le using requis pour que votre Crypteur soit accessible) :

/// Programme client de tests pour le laboratoire 00
/// classe Crypteur - Hiver 2024
/// 
/// par Vincent Echelard, 2013
/// révisé et amélioré par Pierre Prud'homme, février 2022
/// Ajusté et mis à jour par Patrice Roy, février 2023
/// --------------------------------------------------------------------
using System;

Console.SetWindowSize(Console.LargestWindowWidth, Console.LargestWindowHeight);
Console.SetWindowPosition(0,0);
Console.BackgroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.Clear();

const int NB_TESTS = 10;
Console.Write("Nom de l'étudiant(e) : ");
string s = Console.ReadLine();
Console.WriteLine(new string('-', 72));

int résultatDesTests = 0;
résultatDesTests += Tester("ALLO", 2, "CNNQ", "Test simple (majuscules)");
résultatDesTests += Tester("Allo", 2, "CNNQ", "Test simple (majuscule/minuscules)");
résultatDesTests += Tester("allo", 2, "CNNQ", "Test simple (minuscules)");
résultatDesTests += Tester("allo", -2, "CNNQ", "Test clef négative (chiffrement)");
résultatDesTests += Tester("ALLO", 1328, "CNNQ", "Test clef très grande (chiffrement)");
résultatDesTests += Tester("Veux-tu 1 café?", 2, "XGWZ-VW 1 ECHÉ?", "Test caractères non-alphabétique (chiffrement)");
// oui, il y a une faute dans la phrase suivante, je sais
résultatDesTests += Tester("Zorglub veux 1 café!", -1332, "FUXMRAH BKAD 1 IGLÉ!", "Test complet (chiffrement)");
résultatDesTests += Tester("Élu par cette crapule", 27, "ÉMV QBS DFUUF DSBQVMF", "Test décalage +1");
résultatDesTests += Tester("Élu par cette crapule", 26, "ÉLU PAR CETTE CRAPULE", "Test de chiffrement sans effet");
résultatDesTests += Tester("élu par cette crapule", 25, "ÉKT OZQ BDSSD BQZOTKD", "Test décalage -1");
Console.Write($"A eu : {résultatDesTests} / {NB_TESTS}");
Console.ReadKey();

static int Tester(string messageÀCrypter, int clefDeChiffrement,
                  string messageCrypté, string nomDuTest)
{
   string espacement = new string(' ', 13);
   int résultatDuTest = 0;
   Crypteur objTest = null;

   try
   {
      objTest = new Crypteur(messageÀCrypter, clefDeChiffrement);
      if (objTest.MessageSecret == messageCrypté)
      {
         résultatDuTest = 1;
         Console.WriteLine($"Réussite --> {nomDuTest} " + 
                           $"Msg à crypter: {messageÀCrypter} - " +
                           $"Clef: {clefDeChiffrement}  " + Environment.NewLine +
                           $"{espacement}Prévu:{messageCrypté} - Obtenu:{objTest.MessageSecret} " + Environment.NewLine);
      }
      else
      {
         Console.WriteLine($"ÉCHEC    --> {nomDuTest} " +
                           $"Msg à crypter: {messageÀCrypter} - " +
                           $"Clef: {clefDeChiffrement}  " + Environment.NewLine +
                           $"{espacement}Prévu:{messageCrypté} - Obtenu:{objTest.MessageSecret} " + Environment.NewLine);
      }
   }
   catch (Exception e)
   {
      Console.WriteLine($"Erreur lors du test " + nomDuTest + " : " + e.Message);
      if (objTest != null)
      {
         Console.WriteLine($"Msg à crypter : {messageÀCrypter}  - Clef : {clefDeChiffrement}   -- Msg obtenu : {objTest.MessageSecret}");
      }
   }
   return résultatDuTest;
}

Pour le reste : 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 lever 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 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 50% et 100% 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 50% et 75% 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 0,0 est le coin en haut et à gauche de l'écran, et les coordonnées en x et en y 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

6 février

S05

Au menu :

  • Minitest 00
  • Quelques exercices choisis de S04
    • Nous avons investi principalement du temps sur l'exercice 3 (une variante du code vu en classe est disponible ici; notez qu'elle ne comprend pas la méthode Frapper, pas plus que le code de test)
  • 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)

N'oubliez pas de remettre la version imprimée du labo 00 – Le cryptographe au début du cours.

11 février

S06

Pas de séance en personne cette semaine (mais vous pouvez m'écrire!) car je passe la semaine selon le fuseau horaire de Hagenberg (Autriche). Vous pouvez suivre le déroulement de nos travaux sur ../../../Sujets/Orthogonal/wg21-2025-Hagenberg.html

Séance à distance par Teams (le lien vous a été envoyé par Colnet) de 19 h à 21 h. La séance sera enregistrée, mais je vous invite tout de même à vous joindre à moi pour mettre un peu d'ambiance et poser des questions « live ». Au menu :

  • Finir l'exercice 3 de S04
    • Le problème de la méthode Frapper
    • Qualification protected
  • Parler de soi : le mot clé this
    • Constructeurs de délégation
  • Discussion de quelques relations entre classes
    • Héritage d'implémentation
    • Composition
    • Agrégation
    • Association
    • Sens de chacun
  • Quelle relation privilégier quand plusieurs sont possibles
    • Idée de couplage
    • Idée de cohésion
  • Faire l'exercice 4 de S04

Pour les éléments de vocabulaire quant aux relations, informellement, ce que nous avons relevé est :

  • Héritage d'implémentation : verbe être
    • Un Héros est un Personnage
    • Les membres publics (et protégés) du parent (Personnage) sont accessibles pour l'enfant (Héros)
    • En C#, un enfant parle de sa partie parent avec le mot base
    • On peut traiter un enfant comme un cas particulier de son parent (ici : si une fonction prend un Personnage en paramètre, je peux lui passer un Héros)
    • Quand on construit un enfant, il faut d'abord construire la partie parent
  • Composition : verbe avoir
    • Un objet contient d'autres objets
    • Une voiture contient un moteur (mettons)
    • Un objet est composé d'autres objets
    • L'objet contenu a une vie délimitée par l'objet qui le contient
  • Agrégation : verbe avoir / utiliser
    • Un peu comme la composition
    • L'objet « contenu » a une vie qui peut chevaucher celle de l'objet qui le « contient »
    • Pensez à des pneus dans une voiture
  • Association :
    • Deux objets se connaissent, et peuvent se parler
  • On veut : faible couplage, forte cohésion
    • Quand on a des choix, on vise les relations au plus faible couplage possible
    • Couplage des relations ci-dessus, du plus fort au plus faible :
      • Héritage d'implémentation
      • Agrégation / composition
      • Association
  • Couplage
    • Essayer d'isoler les changements pour qu'ils aient un impact le plus local possible
    • Plus les trucs sont locaux, mieux c'est (p.ex. : variable locale)
    • Plus les trucs sont privés, mieux c'est
  • Cohésion
    • « Les trucs qui vont ensemble... vont ensemble »

Pour vous faire pratiquer...

  • Petit quiz de vocabulaire. Soit le code ci-dessous :
using System;

// ... programme principal (omis par simplicité)


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 chaîne)
         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);
      return pos == Math.Min(Longueur, autre.Longueur) ? Longueur < autre.Longueur : Valeur[pos] < autre.Valeur[pos];
   }
}

Répondez aux questions suivantes :

  • Que représente une instance de la classe Mot? Soyez aussi précise ou précis que possible
  • Quelles sont les règles qui assurent la validité d'un Mot? Soyez aussi précise ou précis que 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 un programme principal de votre cru, 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

13 février

S07

Pas de séance en personne cette semaine (mais vous pouvez m'écrire!) car je passe la semaine selon le fuseau horaire de Hagenberg (Autriche). Vous pouvez suivre le déroulement de nos travaux sur ../../../Sujets/Orthogonal/wg21-2025-Hagenberg.html

Ce que je compte faire est enregister des classes et vous les offrir à travers Teams. Je vous annoncerai le moment où je ferai les enregistrements, alors vous pourrez vous joindre à moi pour mettre un peu d'ambiance et poser des questions « live »!

18 février

S08

Aujourd'hui, je devrai m'absenter pour raisons médicales

20 février

S09

Au menu :

Le code en exemple ce matin la classe Entier était :

Entier e0 = new(2);
Entier e1 = new(2);

Console.WriteLine($"e0 vaut {e0.Valeur}");
Console.WriteLine($"e1 vaut {e1.Valeur}");
Console.WriteLine($"e0 + e1 == {(e0 + e1).Valeur}");
Console.WriteLine($"e0 - e1 == {(e0 - e1).Valeur}");
Console.WriteLine($"-e0 == {-e0.Valeur}");
if (e0 < e1)
   Console.WriteLine($"{e0.Valeur} < {e1.Valeur}");
if (e0 > e1)
   Console.WriteLine($"{e0.Valeur} > {e1.Valeur}");
if (e0 <= e1)
   Console.WriteLine($"{e0.Valeur} <= {e1.Valeur}");
if (e0 >= e1)
   Console.WriteLine($"{e0.Valeur} >= {e1.Valeur}");
if (e0 == e1)
   Console.WriteLine($"{e0.Valeur} == {e1.Valeur}");
if (e0 != e1)
   Console.WriteLine($"{e0.Valeur} != {e1.Valeur}");

class Entier
{
   public int Valeur { get; private init; }
   public Entier() : this(0) { }
   public Entier(int val)
   {
      Valeur = val;
   }
   public static Entier operator +(Entier e0, Entier e1) =>
      new(e0.Valeur + e1.Valeur);
   // opérateur - binaire (deux opérandes) : a - b
   public static Entier operator -(Entier e0, Entier e1) =>
      new(e0.Valeur - e1.Valeur);
   // *, / et % laissés en exercice :)
   // opérateur - unaire (un opérande) : -a
   public static Entier operator -(Entier e) => new(-e.Valeur);
   // relationnels d'inégalité
   public static bool operator <(Entier gauche, Entier droite) =>
      gauche.Valeur < droite.Valeur;
   public static bool operator >(Entier gauche, Entier droite) =>
      droite < gauche;
   public static bool operator <=(Entier gauche, Entier droite) =>
      !(droite < gauche);
   public static bool operator >=(Entier gauche, Entier droite) =>
      !(gauche < droite);
   // opérateur d'équivalence
   public static bool operator ==(Entier e0, Entier e1) =>
      e0.Valeur == e1.Valeur;
   public static bool operator !=(Entier e0, Entier e1) =>
      !(e0 == e1);
}

L'ébauche de classe Rationnel ce matin était comme suit (rappel : ce code est incomplet) :

class Rationnel
{
   public int Numérateur { get; private init; }
   int dénominateur;
   public int Dénominateur
   {
      get => dénominateur;
      private init
      {
         dénominateur = value == 0? throw new ArgumentException("Dénominateur nul") : value;
      }
   }
   public Rationnel()
   {
      Numérateur = 0;
      Dénominateur = 1;
   }
   public Rationnel(int num, int dénom)
   {
      // normalisation
      if(dénom < 0)
      {
         num = -num;
         dénom = -dénom;
      }
      Numérateur = num;
      Dénominateur = dénom;
   }
   public string ConvertirEnString() => $"{Numérateur}/{Dénominateur}";
   public static Rationnel operator+(Rationnel gauche, Rationnel droite)
   {
      if (gauche.Dénominateur == droite.Dénominateur)
         return new(gauche.Numérateur + droite.Numérateur,
                    gauche.Dénominateur);
      int dénomCommun = gauche.Dénominateur * droite.Dénominateur;
      int numGauche = gauche.Numérateur * droite.Dénominateur;
      int numDroite = droite.Numérateur * gauche.Dénominateur;
      return new (numGauche + numDroite, dénomCommun);
   }
   public static Rationnel operator -(Rationnel r) =>
      new(-r.Numérateur, r.Dénominateur);
   public static Rationnel operator -(Rationnel r0, Rationnel r1) =>
      r0 + -r1;
   public static explicit operator double(Rationnel r) =>
      (double) r.Numérateur / r.Dénominateur;
   public static bool operator==(Rationnel gauche, Rationnel droite)
   {
      int dénomCommun = gauche.Dénominateur * droite.Dénominateur;
      int numGauche = gauche.Numérateur * droite.Dénominateur;
      int numDroite = droite.Numérateur * gauche.Dénominateur;
      return numGauche == numDroite;
   }
   public static bool operator !=(Rationnel r0, Rationnel r1) =>
      !(r0 == r1);
}

 

25 février

S10

Au menu :

  • Finir la classe Rationnel débutée à la séance S09
  • Présentation du labo 01 – Polynômes et leur dérivation
  • Travail sur le labo 01 – Polynômes et leur dérivation

Classe Rationnel vue en classe :

class Rationnel
{
   public int Numérateur { get; private init; }

   private int dénominateur;
   public int Dénominateur
   {
      get => dénominateur;
      private init
      {
         if (value == 0)
            throw new ArgumentOutOfRangeException("Dénominateur nul");
         dénominateur = value;
      }
   }
   public Rationnel() : this(0) { }
   public Rationnel(int entier)
   {
      Numérateur = entier;
      Dénominateur = 1;
   }
   public Rationnel(int num, int dénom)
   {
      // On inverse les signes du numérateur et dénominateur si
      // le dénominateur est négatif
      if (dénom < 0)
      {
         num = -num;
         dénom = -dénom;
      }
      int pgcd = Math.Abs(CalculerPGCD(num, dénom));
      Numérateur = num / pgcd;
      Dénominateur = dénom / pgcd;
   }
   public string ConvertirEnString()
   {
      string résultat;
      if (Numérateur == 0)
         résultat = "0";
      else if (Dénominateur == 1)
         résultat = Numérateur.ToString(); // ou $"{Numérateur}"
      else
         résultat = $"{Numérateur}/{Dénominateur}";
      return résultat;
   }
   public Rationnel AppliquerPuissance(int puissance) =>
      new((int)Math.Pow(Numérateur, puissance),
           (int)Math.Pow(Dénominateur, puissance));
   public static Rationnel operator +(Rationnel gauche, Rationnel droite) =>
      new(gauche.Numérateur * droite.Dénominateur +
          gauche.Dénominateur * droite.Numérateur,
          gauche.Dénominateur * droite.Dénominateur);
   public static Rationnel operator -(Rationnel r) =>
      new(-r.Numérateur, r.Dénominateur);
   public static Rationnel operator -(Rationnel gauche, Rationnel droite) =>
      gauche + -droite;
   public static Rationnel operator *(Rationnel gauche, Rationnel droite) =>
      new(gauche.Numérateur * droite.Numérateur,
          gauche.Dénominateur * droite.Dénominateur);
   public static Rationnel operator /(Rationnel gauche, Rationnel droite)
   {
      // Remarquez qu'une exception serait quand même levée à la
      // construction de l'objet si on omettait celle-ci. Lever une
      // exception plus précise ici clarifie toutefois le message
      if (droite.Numérateur == 0)
         throw new DivideByZeroException();
      return new(gauche.Numérateur * droite.Dénominateur,
                 gauche.Dénominateur * gauche.Numérateur);
   }
   public static bool operator ==(Rationnel gauche, Rationnel droite) =>
      (object) gauche == null && (object) droite == null ||
      ((object) gauche != null && (object) droite != null &&
       gauche.Numérateur == droite.Numérateur &&
       gauche.Dénominateur == droite.Dénominateur);
   public static bool operator !=(Rationnel gauche, Rationnel droite) =>
      !(gauche == droite);
   public static implicit operator Rationnel(int i) => new(i);
   public static explicit operator double(Rationnel r) =>
      r.Numérateur / (double)r.Dénominateur;
   // Algorithme d'Euclide (qui date d'y a ~2300 ans), vous n'avez pas
   // à le comprendre. 
   private static int CalculerPGCD(int n1, int n2)
   {
      while (n2 != 0)
      {
         int reste = n1 % n2;
         n1 = n2;
         n2 = reste;
      }
      return n1;
   }
}

Code de test proposé pour le labo 01 :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Labo01
{
   static class Tests
   {
      static Rationnel[] coefsVide = new Rationnel[0];
      static Rationnel[] coefsNulls = new Rationnel[3];
      static Rationnel[] coefJuste1 = { new(1)};
      static Rationnel[] coefs0 = { new(1, 2), new(0), new(2,3) };
      static Rationnel[] coefs1 = { new(1, 2), new(2), new(-1, 3) };
      static Rationnel[] coefs2 = { new(2, 3), new(5, 2), new(-2, 10), new(2, 7) };
      static Rationnel[] coefs3 = { new(2), new(5), new(-2), new(3) };
      public static void Tests1()
      {
         Console.WriteLine("============= Tests1 =============");
         Polynome pVide = new ();
         Console.WriteLine(pVide.ConvertirEnString()); // Affiche 0
         Console.WriteLine(pVide.ConvertirEnStringÉpurée()); // Affiche 0
         Polynome p0 = new (coefs0);
         Console.WriteLine(p0.ConvertirEnString()); // Affiche 1/2 + 0x + 2/3x^2
         Console.WriteLine(p0.ConvertirEnStringÉpurée()); // Affiche 1/2 + 2/3x^2
         Console.WriteLine(pVide.ÉvaluerAvec(2.5)); // Affiche 0
         Console.WriteLine(p0.ÉvaluerAvec(2.5)); // Affiche 4,66666666
         Console.WriteLine
         (
            pVide.ÉvaluerAvec(new Rationnel(1, 2)).ConvertirEnString()
         ); // Affiche 0
         Console.WriteLine
         (
            p0.ÉvaluerAvec(new Rationnel(1, 2)).ConvertirEnString()
         ); // Affiche 2/3
         //Polynome pException = new(coefsVide); // lève exception
         //Polynome pException2 = new(coefsNulls); // lève exception
         // N'oubliez pas d'ajouter des tests de votre cru. 
      }
      public static void Tests2()
      {
         Console.WriteLine("============= Tests2 =============");
         Polynome p0 = new(coefs0);
         Console.WriteLine
         (
            (p0 + p0).ConvertirEnStringÉpurée()
         ); // Affiche 1 + 4/3x^2
         Console.WriteLine
         (
            (-p0).ConvertirEnStringÉpurée()
         ); // Affiche -1/2 + -2/3x^2
         Console.WriteLine
         (
            (p0 - p0).ConvertirEnStringÉpurée()
         ); // Affiche 0 
         Polynome p2 = new(coefs2);
         // La surcharge de operator+ devra fonctionner pour
         // des polynomes de degrées différents pour que cette
         // addition fonctionne
         Console.WriteLine
         (
            (p0 + p2).ConvertirEnStringÉpurée()
         ); // Affiche 7/6 + 5/2x + 7/15x^2 + 2/7x^3
         Console.WriteLine(p0 == new Polynome(coefs0)); // Affiche True
         Console.WriteLine(p0 == new Polynome(coefs0) + new Polynome(coefJuste1)); // Affiche False
         Console.WriteLine(p0 == p2); // Affiche False
         // N'oubliez pas d'ajouter des tests de votre cru. 
      }
      public static void Tests3()
      {
         Console.WriteLine("============= Tests3 =============");
         Polynome p3 = new(coefs3);
         Console.WriteLine
         (
            p3.ConvertirEnStringÉpurée()
         );  // Affiche 2 + 5x + -2x^2 + 3x^3
         Polynome p3Prime = p3.Dériver();
         Console.WriteLine
         (
            p3Prime.ConvertirEnStringÉpurée()
         ); // Affiche 5 + -4x + 9x^2
         Console.WriteLine
         (
            p3Prime.Dériver().ConvertirEnStringÉpurée()
         ); // Affiche -4 + 18x
         Console.WriteLine
         (
            p3.Dériver(2).ConvertirEnStringÉpurée()
         ); // Le même que la ligne plus haut
         // N'oubliez pas d'ajouter des tests de votre cru. 
      }
   }
}

27 février

S11

Au menu :

  • Minitest 01
  • Travail sur le labo 01 – Polynômes et leur dérivation

4 mars

Jour de mise à niveau (cours suspendus)

6 mars

 

Jour de mise à niveau (cours suspendus)

11 mars

S12

Au menu :

  • Retour sur le minitest 01
  • Questions sur le labo 01 – Polynômes et leur dérivation
  • Classes statiques et leur rôle
    • Comprendre un petit allègement syntaxique sympathique (ou : ce qu'on fait parfois mécaniquement sans le comprendre...)
  • Un Carré est-il un Rectangle? (ou : le principe de Liskov)

Notre petit exemple était :

Rectangle r = new(10, 4);
Console.WriteLine($"Rectangle de {r.Hauteur} x {r.Largeur}");
Console.WriteLine($"Aire : {r.Aire}, périmètre : {r.Périmètre}");
r.Dessiner();
Carré c = new(5);
Console.WriteLine($"Carré de {c.Hauteur} x {c.Largeur}");
Console.WriteLine($"Aire : {c.Aire}, périmètre : {c.Périmètre}");
c.Dessiner();

Vilaine(c);
Console.WriteLine($"Carré de {c.Hauteur} x {c.Largeur}");
Console.WriteLine($"Aire : {c.Aire}, périmètre : {c.Périmètre}");
c.Dessiner();

static void Vilaine(Rectangle r)
{
   r.Largeur *= 2;
}

class Rectangle
{
   public int Périmètre => 2 * (Hauteur + Largeur);
   public int Aire => Hauteur * Largeur;
   public int Hauteur { get; set; }
   public int Largeur { get; set; }
   public Rectangle(int hau, int lar)
   {
      Hauteur = hau;
      Largeur = lar;
   }
   public void Dessiner()
   {
      for(int ligne = 0; ligne != Hauteur; ++ligne)
      {
         for(int col = 0; col != Largeur; ++col)
            Console.Write('#');
         Console.WriteLine();
      }
   }
}

class Carré : Rectangle
{
   public int Hauteur
   {
      get => base.Hauteur;
      set
      {
         base.Hauteur = value;
         base.Largeur = value;
      }
   }
   public int Largeur
   {
      get => base.Largeur;
      set
      {
         base.Hauteur = value;
         base.Largeur = value;
      }
   }
   public Carré(int côté) : base(côté, côté)
   {
   }
}
  • Premier coup d'oeil sur une classe très utile : la classe List<T> qui modélise un tableau dynamique
  • Petit exemple avec un List<T>  T est int :
    • Créer une List<int> vide
    • Ajouter des valeurs dans une List<int>
    • Créer une List<int> avec des valeurs initiales
    • Parcourir une List<int> (for, foreach)
    • Trier une List<int>

Par exemple :

using System;
using System.Collections.Generic;

// alternative : List<int> lst = new() { 3,7,11,2,5 };
List<int> lst = new();
foreach(int n in new[]{ 3, 7, 11, 2, 5 })
   lst.Add(n);
// parcourir avec foreach
foreach(int n in lst)
   Console.Write($"{n} ");
Console.WriteLine();
// parcourir avec for
for(int i = 0; i != lst.Count; ++i)
   Console.Write($"{lst[i]} ");
Console.WriteLine();
// trier les éléments en ordre croissant
lst.Sort();
// etc.
  • Très brève introduction au polymorphisme, un sujet riche sur lequel nous reviendrons...

Notre petit exemple introductoire était :

Jim jim = new("Tremblay");
jim.Présenter();
Joe joe = new("Biden");
joe.Présenter();
Bob bob = new("Nguyen");
bob.Présenter();

Personne[] p = new Personne[] { jim, joe, bob };
foreach (Personne pers in p)
   pers.Présenter();

class Personne
{
   public string Nom { get; init; }
   public Personne(string nom)
   {
      Nom = nom;
   }
   public virtual void Présenter()
   {
      Console.WriteLine($"Mon nom est {Nom}");
   }
}
class Jim : Personne
{
   public Jim(string nom) : base(nom) { }
   public override void Présenter()
   {
      Console.WriteLine($"Mon nom est Jim {Nom}");
   }
}
class Joe : Personne
{
   public Joe(string nom) : base(nom) { }
   public override void Présenter()
   {
      Console.WriteLine($"My name is Joe {Nom}");
   }
}
class Bob : Personne
{
   public Bob(string nom) : base(nom) { }
   public override void Présenter()
   {
      Console.WriteLine($"Euh... chu Bob {Nom}");
   }
}

S'il reste du temps :

  • Travail sur le labo 01 – Polynômes et leur dérivation

Le chemin que nous avons suivi fut « classique » :

  • Une Forme a un Symbole
  • Un Triangle est une Forme
  • Un Triangle 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 Triangle et Rectangle), suite à quoi l'affichage s'est mis à dessiner des trucs colorés à l'écran, pour notre plus grand bonheur!

En bout de ligne, on arrive à quelque chose comme ce qui suit. J'ai ajouté un peu de validation (note : c'est une première étape; nous raffinerons le modèle sous peu) :

using System;

Forme [] formes = new Forme[]
{
   new Rectangle('#', 30, 10),
   new Triangle('A', 7)
};
// Polymorphisme : à partir d'une abstraction (ici : Forme), on
// appelle le service de l'objet réellement pointé (p.ex. : Triangle)
foreach(Forme f in formes)
   f.Dessiner();

class Forme
{
   public char Symbole { get; private init; }
   public Forme(char symbole)
   {
      Symbole = symbole;
   }
   // méthode virtuelle : _peut_ être spécialisée par les enfants
   public void Dessiner()
   {
      // ... quoi mettre ici? on y reviendra ...
   }
}

// invariants :
// - Hauteur strictement positive
class Triangle : Forme
{
   int hauteur;
   public int Hauteur
   {
      get => hauteur;
      private init
      {
         hauteur = value > 0 ? value : throw new ArgumentException();
      }
   }
   public Triangle(char symbole, int hauteur) : base(symbole)
   {
      Hauteur = hauteur;
   }
   // override : je fais le choix explicite de spécialiser une méthode
   // d'un parent (ici : Forme.Dessiner)
   protected override void Dessiner()
   {
      for (int ligne = o; ligne != Hauteur; ++ligne)
      {
         for (int col = 0; col <= ligne; ++col)
            Console.Write(Symbole);
         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(char symbole, int largeur, int hauteur) : base(symbole)
   {
      Largeur = largeur;
      Hauteur = hauteur;
   }
   protected override void Dessiner()
   {
      for (int ligne = 0; ligne != Hauteur; ++ligne)
      {
         for (int col = 0; col != Largeur; ++col)
            Console.Write(Symbole);
         Console.WriteLine();
      }
   }
}

Résumé des « nouvelles » idées de cette partie du 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)

13 mars

S13

Au menu :

  • Tri avec comparateurs (ou : comment rendre le comparateur plus charmant)
    • Qu'est-ce qu'un three-way-compare?
    • Comment l'implémenter efficacement?
  • Suivi de notre petite introduction au polymorphisme
  • Abstraction
    • Que faire avec Forme.Dessiner?
    • Interfaces
      • Que faire quand on veut définir un contrat opératoire?

N'oubliez pas de remettre la version imprimée du labo 01 – Polynômes et leur dérivation au début du cours.

Activité pour se pratiquer

La base de cette activité sera l'exercice 3 de S04 pour lequel une solution est disponible ici. Nous allons construire sur cette base un programme amusant (du moins, c'est le souhait!). Il se peut que vous vouliez utiliser du polymorphisme à un endroit, mais pour l'essentiel vous devriez pouvoir résoudre ce problème avec de l'héritage d'implémentation et de la composition.

  • Un Héros peut posséder une Arme. Si un Héros n'a pas d'Arme, alors son Arme sera null.
  • Une Arme peut AppliquerEffet lorsqu'un Héros frappe un autre Personnage. Par exemple, un Gourdin aura pour effet d'ajouter 3 aux dégâts causés quand un Héros frappe un Personnage alors qu'un Halebarde aura pour effet d'ajouter 8 aux dégâts dans ces circonstances.
  • Ajoutez une classe Armée qui prendra en paramètre à la construction un tableau de Héros et les entreposera d'une manière qui vous semblera pertinente. Une Armée ne peut pas être vide; elle doit contenir au moins un Héros, il s'agit d'un de ses invariants. Un autre de ses invariants est qu'elle doit contenir un nombre pair de Héros. Note : un Héros mort demeure un Héros.
  • Ajoutez une classe Horde qui prendra en paramètre à la construction un tableau de Monstre et les entreposera d'une manière qui vous semblera pertinente. Une Horde ne peut pas être vide; elle doit contenir au moins un Monstre, il s'agit d'un de ses invariants. Note : un Monstre mort demeure un Monstre.
  • Si un Armée attaque un groupe adverse de Personnage (par exemple, une Horde), alors un membre vivant de l'Armée pris au hasard frappera un de ses adversaires vivants pris au hasard. Si un Horde attaque un groupe adverse de Personnage (par exemple, une Armée), alors un membre vivant de la Horde pris au hasard frappera un de ses adversaires vivants pris au hasard.
  •  Exceptionnellement, lors d'une attaque, vous pouvez afficher à la console ce qui se passe dans le combat, p. ex.: qui attaque vers qui; combien de vie la cible de l'attaque possède avant le coup, et combien de vie la cible de l'attaque possède après le coup. J'écris « exceptionnellement » car on ne voudrait normalement pas enchevêtrer attaque et affichage, mais je souhaite que le problème à résoudre demeure relativement simple.

Un exemple de programme de test serait :

using ActivitéS13;
using z;
//
// armée qui contiendra :
// - Bill le petit, 15 de force
// - Valentin, 18 de force
// - Galahad, 17 de force armé d'une halebarde causant un dégât supplémentaire de huit points
// - Brute immonde, 20 de force armé d'un gourdin causant un dégât supplémentaire de trois points
//
Armée armée = new(new Héros[]
{

   // ...


   new Héros("Bill le petit", 15),
   new ("Valentin", 18),
   new ("Galahad", 17, new Arme("Halebarde", 8)),
   new ("Brute immonde", 20, new Arme("Gourdin", 3))

});
//
// horde qui contiendra :
// - GRRR, 24 de force et 50 de vie
// - HSSSS, 22 de force et 44 de vie
// - BRK, 20 de force et 40 de vie
// - FFFT, 19 de force et 15 de vie
// - prkkk, 21 de force et 38 de vie
//
Horde horde = new(new Monstre[]
{

   // ...


   new("GRRR", 24, 50),
   new("HSSSS", 22, 44),
   new("BRK", 20, 40),
   new("FFFT", 19, 15),
   new("prkkk", 21, 38)

});

bool tourHéros = new Random().Next() % 2 == 0;
while (!armée.EstDéfait && !horde.EstDéfait)
{
   if (tourHéros)
   {
      armée.Attaquer(horde);
   }
   else
   {
      horde.Attaquer(armée);
   }
   Console.WriteLine(new string('-', 70));
   tourHéros = !tourHéros;
}
if (horde.EstDéfait)
{
   Console.Write("Victoire de l'armée. Encore vivants : ");
   armée.PrésenterVivants();
}
else
{
   Console.Write("Victoire de la horde. Encore vivants : ");
   horde.PrésenterVivants();
}

Un exemple d'exécution de ce programme serait :

Valentin frappe GRRR
        Avant le coup, GRRR a 50 vie
        Après le coup, GRRR a 40 vie
----------------------------------------------------------------------
HSSSS frappe Bill le petit
        Avant le coup, Bill le petit a 75 vie
        Après le coup, Bill le petit a 64 vie
----------------------------------------------------------------------
Galahad frappe prkkk
        Avant le coup, prkkk a 38 vie
        Après le coup, prkkk a 15 vie
----------------------------------------------------------------------
BRK frappe Brute immonde
        Avant le coup, Brute immonde a 64 vie
        Après le coup, Brute immonde a 50 vie
----------------------------------------------------------------------
Bill le petit frappe prkkk
        Avant le coup, prkkk a 15 vie
        Après le coup, prkkk a 0 vie
----------------------------------------------------------------------
HSSSS frappe Galahad
        Avant le coup, Galahad a 55 vie
        Après le coup, Galahad a 44 vie
----------------------------------------------------------------------
Galahad frappe FFFT
        Avant le coup, FFFT a 15 vie
        Après le coup, FFFT a -10 vie
----------------------------------------------------------------------
HSSSS frappe Bill le petit
        Avant le coup, Bill le petit a 64 vie
        Après le coup, Bill le petit a 52 vie
----------------------------------------------------------------------
Galahad frappe GRRR
        Avant le coup, GRRR a 40 vie
        Après le coup, GRRR a 24 vie
----------------------------------------------------------------------
GRRR frappe Galahad
        Avant le coup, Galahad a 44 vie
        Après le coup, Galahad a 28 vie
----------------------------------------------------------------------
Galahad frappe HSSSS
        Avant le coup, HSSSS a 44 vie
        Après le coup, HSSSS a 27 vie
----------------------------------------------------------------------
HSSSS frappe Galahad
        Avant le coup, Galahad a 28 vie
        Après le coup, Galahad a 13 vie
----------------------------------------------------------------------
Galahad frappe HSSSS
        Avant le coup, HSSSS a 27 vie
        Après le coup, HSSSS a 3 vie
----------------------------------------------------------------------
BRK frappe Bill le petit
        Avant le coup, Bill le petit a 52 vie
        Après le coup, Bill le petit a 40 vie
----------------------------------------------------------------------
Valentin frappe HSSSS
        Avant le coup, HSSSS a 3 vie
        Après le coup, HSSSS a -6 vie
----------------------------------------------------------------------
GRRR frappe Galahad
        Avant le coup, Galahad a 13 vie
        Après le coup, Galahad a -1 vie
----------------------------------------------------------------------
Brute immonde frappe GRRR
        Avant le coup, GRRR a 24 vie
        Après le coup, GRRR a 9 vie
----------------------------------------------------------------------
GRRR frappe Valentin
        Avant le coup, Valentin a 85 vie
        Après le coup, Valentin a 70 vie
----------------------------------------------------------------------
Valentin frappe GRRR
        Avant le coup, GRRR a 9 vie
        Après le coup, GRRR a -3 vie
----------------------------------------------------------------------
BRK frappe Valentin
        Avant le coup, Valentin a 70 vie
        Après le coup, Valentin a 58 vie
----------------------------------------------------------------------
Brute immonde frappe BRK
        Avant le coup, BRK a 40 vie
        Après le coup, BRK a 21 vie
----------------------------------------------------------------------
BRK frappe Valentin
        Avant le coup, Valentin a 58 vie
        Après le coup, Valentin a 47 vie
----------------------------------------------------------------------
Valentin frappe BRK
        Avant le coup, BRK a 21 vie
        Après le coup, BRK a 6 vie
----------------------------------------------------------------------
BRK frappe Valentin
        Avant le coup, Valentin a 47 vie
        Après le coup, Valentin a 32 vie
----------------------------------------------------------------------
Valentin frappe BRK
        Avant le coup, BRK a 6 vie
        Après le coup, BRK a -7 vie
----------------------------------------------------------------------
Victoire de l'armée. Encore vivants : Bill le petit avec 40 vies; Valentin avec 32 vies; Brute immonde avec 50 vies;

Un exemple de code pour cette activité est ici.

Quelques exemples couverts dans ce cours :

  • Introduction au polymorphisme avec les types Pers (pour Personne), Jim, Joe et Bob :
Jim jim = new("Richard");
Joe joe = new("Francis");
Bob bob = new("Boulanger");
jim.Présenter();
joe.Présenter();
bob.Présenter();
Pers[] personnes = { jim, joe, bob };
foreach (Pers p in personnes)
   p.Présenter();

class Pers
{
   public string Nom { get; init; }
   public Pers(string nom)
   {
      Nom = nom;
   }
   // virtual : les enfants _peuvent_ spécialiser le service
   public virtual void Présenter()
   {
      Console.WriteLine($"Bonjour, je suis {Nom}");
   }
}
class Jim : Pers
{
   public Jim(string nom) : base($"Jim {nom}")
   {
   }
   // override : je choisis de spécialiser un service virtuel
   public override void Présenter()
   {
`     Console.WriteLine($"Mon nom est Jim, {Nom}");
   }
}
class Joe : Pers
{
   public Joe(string nom) : base($"Joe {nom}")
   {
   }
   public override void Présenter()
   {
      Console.WriteLine($"S'lut! Chu {Nom}");
   }
}
class Bob : Pers
{
   public Bob(string nom) : base($"Bob {nom}")
   {
   }
   public override void Présenter()
   {
      Console.WriteLine($"Ouais, euh... J'm'appelle {Nom}");
   }
}
  • De la violence avec des instances de divers types de Monstre assaillant une victime de type Humain :
Humain victime = new("Patrice");
List<Monstre> monstres = new()
{
   new Hydre("Réjean"),
   new Yéti("Jean"),
   new Banshee("Jeannette")
};
Random dé = new();
while (!victime.EstMort)
{
   int qui = dé.Next(0, monstres.Count);
   Console.WriteLine($"{monstres[qui].Nom} attaque {victime.Nom}");
   monstres[qui].Attaquer(victime);
}
Console.WriteLine($"C'est la fin... Pauvre {victime.Nom}");

class Pers
{
   public int Vie { get; private set; }
   public string Nom { get; init; }
   public Pers(string nom, int vie)
   {
      Nom = nom;
      Vie = vie;
   }
   public void Blesser(int dégâts) { Vie -= dégâts; }
   public bool EstVivant => Vie > 0;
   public bool EstMort => !EstVivant;
}
class Humain : Pers
{
   public Humain(string nom) : base(nom, 100) { }
}
class Monstre : Pers
{
   public Monstre(string nom, int vie) : base(nom, vie) { }
   public virtual void Attaquer(Humain victime)
   {
      // ?
   }
}
class Hydre : Monstre
{
   public Hydre(string nom) : base(nom, 1000) { }
   public override void Attaquer(Humain victime)
   {
      Console.WriteLine("HYYYYYYRRRRRRRAAHHHHH!");
      victime.Blesser(30);
   }
}
class Yéti : Monstre
{
   public Yéti(string nom) : base(nom, 150) { }
   public override void Attaquer(Humain victime)
   {
      Console.WriteLine("Oh, je hurle!");
      victime.Blesser(10);
   }
}
class Banshee : Monstre
{
   public Banshee(string nom) : base(nom, 150) { }
   public override void Attaquer(Humain victime)
   {
      Console.WriteLine("OooooOOhhhhh!");
      victime.Blesser(8);
   }
}
  • Petite introduction au polymorphisme par voie d'héritage d'interfaces :
OiseauVolant fred = new();
Décoller(fred);

static void Décoller(IVolant v)
{
   v.Voler();
}


interface IVolant
{
   void Voler();
}
class Animal
{
   // ...
}
class Oiseau : Animal
{
   // ...
}
class Insecte : Animal
{
   // ...
}
class OiseauVolant : Oiseau, IVolant
{
   public void Voler()
   {
      Console.WriteLine("Cui cui je vole");
   }
   // ...
}
class OiseauNonVolant : Oiseau
{
   // ...
}
class InsecteVolant : Insecte, IVolant
{
   public void Voler()
   {
      Console.WriteLine("Bzzz bzzz je vole");
   }
   // ...
}
class InsecteNonVolant : Insecte
{
   // ...
}
  • Introduction aux classes abstraites sur la base d'une version retouché de la hiérarchie de classes prenant Forme pour racine (avec introduction à l'idiome NVI, en prime) et qui dessine de chics formes colorées :
// Forme frm = new(ConsoleColor.Magenta); // <-- serait illégal
// frm.Dessiner();                        //     ... car ceci est abstrait

List<Forme> formes = new()
{
   new Rectangle(ConsoleColor.Red, 10, 5, '/'),
   new Triangle(ConsoleColor.Yellow, 6, 'A'),
   new Carré(ConsoleColor.Blue, 7, '#')
};
foreach (Forme f in formes)
   f.Dessiner();

abstract class Forme
{
   public ConsoleColor Couleur { get; init; }
   public Forme(ConsoleColor couleur)
   {
      Couleur = couleur;
   }
   // idiome NVI : non-virtual interface
   public void Dessiner()
   {
      // ce qui doit être fait avant le dessin
      ConsoleColor avant = Console.ForegroundColor;
      Console.ForegroundColor = Couleur;
      // le dessin en soi
      DessinerImpl();
      // ce qui doit être fait après le dessin
      Console.ForegroundColor = avant;
   }
   // abstract : l'enfant _doit_ spécialiser
   protected abstract void DessinerImpl();
}

class Rectangle : Forme
{
   public int Hauteur { get; private init; }
   public int Largeur { get; private init; }
   public char Symbole { get; private init; }
   public int Périmètre => 2 * Hauteur + 2 * Largeur;
   public int Aire => Hauteur * Largeur;
   public Rectangle(ConsoleColor couleur, int hau, int lar, char sym)
      : base(couleur)
   {
      Hauteur = hau;
      Largeur = lar;
      Symbole = sym;
   }
   protected override void DessinerImpl()
   {
      for (int ligne = 0; ligne != hauteur; ++ligne)
      {
         for (int col = 0; col != largeur; ++col)
            Console.Write(symbole);
         Console.WriteLine();
      }
   }
}

class Carré : Forme
{
   Rectangle Impl { get; init; }
   public Carré(ConsoleColor couleur, int côté, char symbole)
      : base(couleur)
   {
      Impl = new(couleur, côté, côté, symbole);
   }
   public int Largeur => Impl.Largeur;
   public int Hauteur => Impl.Hauteur;
   public int Périmètre => Impl.Périmètre;
   public int Aire => Impl.Aire;
   protected override void DessinerImpl() => Impl.Dessiner();
}

class SymboleInvalideException : Exception { }
class HauteurInvalideException : Exception { }
class Triangle : Forme
{
   int hauteur;
   char symbole;
   static bool EstSymboleValide(char symbole) =>
      !char.IsWhiteSpace(symbole);
   static bool EstHauteurValide(int val) => val > 0;
   public int Hauteur
   {
      get => hauteur; 
      private init
      {
         hauteur = EstHauteurValide(value) ? value : throw new HauteurInvalideException();
      }
   }
   public char Symbole
   {
      get => symbole;
      private init
      {
         symbole = EstSymboleValide(value) ? value : throw new SymboleInvalideException();
      }
   }
   public Triangle(ConsoleColor couleur, int hauteur,char symbole)
      : base(couleur)
   {
      Hauteur = hauteur;
      Symbole = symbole;
   }
   protected override void DessinerImpl()
   {
      for (int ligne = 0; ligne != hauteur; ++ligne)
      {
         for (int col = 0; col <= ligne; ++col)
            Console.Write(symbole);
         Console.WriteLine();
      }
   }
}

18 mars

S14

Au menu :

  • Implémenter ToString
    • En quoi ça peut être utile
    • Exemple avec une classe Point
  • Implémenter une interface standard
    • Exemple de IEquatable<T>
    • Exemple de IComparable<T>
    • Impact sur List<T>.Sort
  • Implémenter un operator== correct
    • Attention, c'est pas joli
    • Enjeux de la hiérarchie imposée de .NET, en particulier de la classe object (alias pour System.Object)
  • Présentation du labo 02 – Le conjugueur
  • Travail sur le labo 02 – Le conjugueur

20 mars

S15

Au menu :

  • Introduction à la conception de structures de données
    • Concevoir un tableau dynamique de int (sorte de List<int> simplifiée)
    • Si le temps le permet, concevoir une pile de int à l'aide de ce tableau dynamique
  • Les exemples ont été mis à votre disposition :
  • S'il reste du temps, travail sur le labo 02 – Le conjugueur

25 mars

S16

Au menu :

  • Concevoir une liste simplement chaînée d'entiers
  • Concevoir une liste doublement chaînée d'entiers
  • Concevoir une file d'entiers, utilisant notre liste doublement chaînée d'entiers à titre de substrat
    • Services implémentés : EstVide, AjouterDébut, AjouterFin, PeekDébut, PeekFin, SupprimerDébut, SupprimerFin, et – de manière temporaire – Afficher
    • On visera à enrichir cette gamme des services suivants lors de notre prochaine rencontre : Trouver, Contient, Compter et Supprimer

Le code de la classe ListeSimple conçue en classe était :

namespace z
{
   class ListeVideException : Exception;
   internal class ListeSimple
   {
      class Noeud
      {
         public int Val { get; init; }
         public Noeud Succ { get; set; } = null;
         public Noeud (int val)
         {
            Val = val;
         }
      }
      Noeud Tête { get; set; } = null;
      Noeud Queue { get; set; } = null;
      public bool EstVide => Tête == null;
      // complexité : O(1)
      public void AjouterDébut(int val)
      {
         Noeud p = new(val);
         if (EstVide)
            Queue = p;
         p.Succ = Tête;
         Tête = p;
         ++Count;
      }
      // note : privée
      // précondition : !EstVide
      // complexité : O(n)
      Noeud TrouverDernier()
      {
         Noeud p = Tête;
         for (; p.Succ != null; p = p.Succ)
            ;
         return p;
      }
      // complexité : O(1) :)
      public void AjouterFin(int val)
      {
         if(EstVide)
            AjouterDébut (val);
         else
         {
            Noeud p = new(val);
            Noeud q = Queue;
            q.Succ = p;
            Queue = p;
            ++Count;
         }
      }
      // complexité : O(1)
      public void SupprimerDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         Tête = Tête.Succ;
         if (EstVide)
            Queue = null;
         --Count;
      }
      public int Count { get; private set; } = 0;
      // complexité : O(1)
      public int PeekDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         return Tête.Val;
      }
      // complexité : O(n)
      public int PeekFin()
      {
         if (EstVide)
            throw new ListeVideException();
         return TrouverDernier().Val;
      }
      // TEMPORAIRE ARK ARK ARK
      public void Afficher()
      {
         for (Noeud p = Tête; p != null; p = p.Succ)
            Console.Write($"{p.Val} ");
         Console.WriteLine();
      }
      // duplication (version méthode d'instance)
      // complexité : O(n)... :)
      public ListeSimple Dupliquer()
      {
         ListeSimple lst = new();
         for(Noeud p = Tête; p != null; p = p.Succ)
            lst.AjouterFin(p.Val);
         return lst;
      }
      // duplication (version méthode de classe)
      //public static ListeSimple Dupliquer(ListeSimple src)
      //{
      //   ListeSimple lst = new();
      //   for (Noeud p = src.Tête; p != null; p = p.Succ)
      //      lst.AjouterFin(p.Val);
      //   return lst;
      //}
   }
}

Le code de la classe ListeDouble conçue en classe était :

namespace z
{
   class ListeVideException : Exception;
   internal class ListeDouble
   {
      class Noeud
      {
         public int Val { get; init; }
         public Noeud Succ { get; set; } = null;
         public Noeud Pred { get; set; } = null;
         public Noeud(int val)
         {
            Val = val;
         }
      }
      public int Count { get; private set; } = 0;
      Noeud Tête { get; set; } = null;
      Noeud Queue { get; set; } = null;
      public bool EstVide => Count == 0;
      // AjouterDébut
      public void AjouterDébut(int val)
      {
         Noeud p = new(val);
         if (EstVide)
            Tête = Queue = p;
         else
         {
            Tête.Pred = p;
            p.Succ = Tête;
            Tête = p;
         }
         ++Count;
      }
      // AjouterFin
      public void AjouterFin(int val)
      {
         Noeud p = new(val);
         if (EstVide)
            Tête = Queue = p;
         else
         {
            Queue.Succ = p;
            p.Pred = Queue;
            Queue = p;
         }
         ++Count;
      }
      // SupprimerDébut
      public void SupprimerDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         Tête = Tête.Succ;
         if (Tête == null)
            Queue = null;
         else
            Tête.Pred = null;
         --Count;
      }
      // SupprimerFin
      public void SupprimerFin()
      {
         if (EstVide)
            throw new ListeVideException();
         Queue = Queue.Pred;
         if (Queue == null)
            Tête = null;
         else
            Queue.Succ = null;
         --Count;
      }
      // PeekDébut
      public int PeekDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         return Tête.Val;
      }
      // PeekFin
      public int PeekFin()
      {
         if (EstVide)
            throw new ListeVideException();
         return Queue.Val;
      }
      // Dupliquer
      public ListeDouble Dupliquer()
      {
         ListeDouble lst = new();
         for (Noeud p = Tête; p != null; p = p.Succ)
            lst.AjouterFin(p.Val);
         return lst;
      }
   }
}

Le code de la classe File construite en classe sur les fondations du substrat ListeEntiers était :

namespace z
{
   class FileVideException : Exception;
   class File
   {
      ListeDouble Substrat { get; init; } = new();
      public bool EstVide => Substrat.EstVide;
      public void Enfiler(int valeur)
      {
         Substrat.AjouterDébut(valeur);
      }
      public int Peek() =>
         EstVide? throw new FileVideException() : Substrat.PeekFin();
      public int Défiler()
      {
         int val = Peek();
         Substrat.SupprimerFin();
         return val;
      }
   }
}

Rappel : avec Visual Studio, vos fichiers font quelques using implicites, dont un using de System.IO qui contient une classe nommée... File. Si vous souhaitez utiliser notre classe File, il se peut que vous deviez écrire le nom du namespace explicitement (p. ex. : z.File au lieu de File).

En espérant que le tout vous ait diverti!

27 mars

S17

Au menu :

  • Enrichir les services de ListeDouble
    • Trouver
    • Contient
    • Compter
    • Supprimer
  • Introduction (brève) au code générique
    • de ListeDouble à ListeDouble<T>
    • conséquences de cette transformation
    • quelques mots sur les fonctions génériques
    • quelques mots sur les expressions lambda

La classe ListeDouble<T> à laquelle nous sommes parvenus était :

internal class ListeDouble<T> where T : IEquatable<T> 
{
   class Noeud
   {
      public T Val { get; init; }
      public Noeud Succ { get; set; } = null;
      public Noeud Pred { get; set; } = null;
      public Noeud(T val)
      {
         Val = val;
      }
   }
   public int Count { get; private set; } = 0;
   Noeud Tête { get; set; } = null;
   Noeud Queue { get; set; } = null;
   public bool EstVide => Count == 0;
   // AjouterDébut
   public void AjouterDébut(T val)
   {
      Noeud p = new(val);
      if (EstVide)
         Tête = Queue = p;
      else
      {
         Tête.Pred = p;
         p.Succ = Tête;
         Tête = p;
      }
      ++Count;
   }
   // AjouterFin
   public void AjouterFin(T val)
   {
      Noeud p = new(val);
      if (EstVide)
         Tête = Queue = p;
      else
      {
         Queue.Succ = p;
         p.Pred = Queue;
         Queue = p;
      }
      ++Count;
   }
   // SupprimerDébut
   public void SupprimerDébut()
   {
      if (EstVide)
         throw new ListeVideException();
      Tête = Tête.Succ;
      if (Tête == null)
         Queue = null;
      else
         Tête.Pred = null;
      --Count;
   }
   // SupprimerFin
   public void SupprimerFin()
   {
      if (EstVide)
         throw new ListeVideException();
      Queue = Queue.Pred;
      if (Queue == null)
         Tête = null;
      else
         Queue.Succ = null;
      --Count;
   }
   // PeekDébut
   public T PeekDébut()
   {
      if (EstVide)
         throw new ListeVideException();
      return Tête.Val;
   }
   // PeekFin
   public T PeekFin()
   {
      if (EstVide)
         throw new ListeVideException();
      return Queue.Val;
   }
   // Dupliquer
   public ListeDouble<T> Dupliquer()
   {
      ListeDouble<T> lst = new();
      for (Noeud p = Tête; p != null; p = p.Succ)
         lst.AjouterFin(p.Val);
      return lst;
   }
   // ARK ARK ARK
   public void Afficher()
   {
      for (Noeud p = Tête; p != null; p = p.Succ)
         Console.Write($"{p.Val} ");
      Console.WriteLine();
   }
   // Contient : retourne true seulement si la liste
   //            contient une valeur donnée
   // complexité : O(n)
   public bool Contient(T val) => Trouver(val) != null;
   //{
   //   for(Noeud p = Tête; p != null; p = p.Succ)
   //      if(p.Val.Equals(val)/*p.Val == val*/)
   //         return true;
   //   return false;
   //}
   // Compter : retourne le nombre d'occurrences d'une
   //           valeur donnée dans la liste
   // complexité : O(n)
   public int Compter(T val)
   {
      int n = 0;
      for (Noeud p = Tête; p != null; p = p.Succ)
         if (p.Val.Equals(val)/*p.Val == val*/)
            ++n;
      return n;
   }
   // Supprimer : supprimer la première occurrence
   //             d'une valeur dans la liste
   public void Supprimer(T val)
   {
      Noeud p = Trouver(val);
      // ici, soit p est null, soit p.Val vaut val
      if(p != null)
      {
         if (p.Succ == null) // dernier noeud
            Queue = p.Pred;
         else
            p.Succ.Pred = p.Pred;
         if (p.Pred == null) // premier noeud
            Tête = p.Succ;
         else
            p.Pred.Succ = p.Succ;
         --Count;
      }
   }
   // (privée) Trouver : retourne une référence sur
   //          la première occurrence d'un noeud d'une
   //          valeur donnée, null sinon
   Noeud Trouver(T val)
   {
      for (Noeud p = Tête; p != null; p = p.Succ)
         if (p.Val.Equals(val)/*p.Val == val*/)
            return p;
      return null;
   }
}

La fonction Contient<T> quant à elle était :

static bool Contient<T>(T [] tab, T val) where T : IEquatable<T>
{
   foreach(T elem in tab)
      if (elem.Equals(val))
         return true;
   return false;
}

1 avril

S18

Au menu :

  • Minitest 02
  • Ensuite : quelques exercices

Exercice 0

Écrivez une classe FilePrioritaire<T> ayant les caractéristiques suivantes :

  • Elle utilisera un List<T> (le type List de C#) comme substrat
  • Une FilePrioritaire<T> nouvellement construite sera vide
  • Elle exposera une propriété EstVide qui vaudra true seulement si elle est vide
  • Elle inclura un critère de tri déterminé à la construction
    • Note : un critère de tri sera une fonction de comparaison trilatérale (voir S13), prenant en paramètre deux T retournant un int
  • Elle exposera une méthode Add acceptant en paramètre un T et l'ajoutant au substrat
  • Elle exposera une méthode Prochain retournant le premier T dans le substrat et l'en extrayant
  • Invariant : les éléments d'une FilePrioritaire<T> seront triés en fonction de son critère de tri
    • Note : utilisez la fonction Trier dans l'exemple ci-dessous pour réaliser ce tri

Code de test possible :

using System;
using System.Collections.Generic;

FilePrioritaire<int> file = new (TriDécroissant);
foreach(int n in new []{ 2, 3, 5, 7, 11 })
   file.Add(n);

while(!file.EstVide)
   Console.WriteLine(file.Prochain()); // 11 7 5 3 2

// ...
static void Trier<T>(List<T> lst, Func<T,T,int> critère) // tri à bulles, pour fins de simplicité
{
   for(int i = 0; i < lst.Count - 1; ++i)
      for(int j = i + 1; j < lst.Count; ++j)
         if(critère(lst[i], lst[j]) > 0)
            PermuterÉléments(lst, i, j);
}
static void PermuterÉléments<T>(List<T> lst, int i, int j)
{
   T elem = lst[i];
   lst[i] = lst[j];
   lst[j] = elem;
}
// ...
static int TriDécroissant(int x, int y) => y - x;
// ...

Affichage attendu :

11
7
5
3
2

Pour le plaisir : pouvez-vous faire une FilePrioritaire<Client> pour une classe Client où chaque Client a un nom, un prénom et une dette, et où les clients les plus endettés sont prioritaires sur les moins endettés? Si deux clients sont aussi endettés l'un que l'autre, alors la priorité doit respecter l'ordre lexicographique (ordre croissant de nom, puis de prénom si deux noms sont équivalents).

Solution possible :

class FilePrioritaire<T>
{
   Func<T,T,int> Critère;
   List<T> Elems{ get;init; } = new();
   public FilePrioritaire(Func<T,T,int> critère)
   {
      Critère = critère;
   }
   public void Add(T elem)
   {
      Elems.Add(elem);
      Trier(Elems, Critère);
   }
   public T Prochain()
   {
      T elem = Elems[0];
      Elems.RemoveAt(0);
      return elem;
   }
   public bool EstVide => Elems.Count == 0;
}

Question : est-ce que le substrat est bien choisi? Expliquez votre réponse.

Exercice 01

Ajoutons quelques algorithmes génériques à notre banque d'outils :

EX01.0 – Écrivez l'algorithme Permuter<T>(ref T a, ref T b) qui permutera les valeurs de a et de b, de telle sorte que l'exécution du programme suivant :

int i0 = 2, i1 = 3;
Console.WriteLine($"Avant permutation : {i0}, {i1}");
Permuter(ref i0, ref i1);
Console.WriteLine($"Après permutation : {i0}, {i1}");
double d0 = 2.5, d1 = 3.5;
Console.WriteLine($"Avant permutation : {d0}, {d1}");
Permuter(ref d0, ref d1);
Console.WriteLine($"Après permutation : {d0}, {d1}");
string s0 = "allo", s1 = "toi";
Console.WriteLine($"Avant permutation : \"{s0}\", \"{s1}\"");
Permuter(ref s0, ref s1);
Console.WriteLine($"Après permutation : \"{s0}\", \"{s1}\"");

... affiche ce qui suit :

Avant permutation : 2,3
Après permutation : 3,2
Avant permutation : 2.5,3.5
Après permutation : 3.5,2.5
Avant permutation : "allo","toi"
Après permutation : "toi","allo"

EX01.1 – Écrivez l'algorithme RotaterGauche<T>(List<T> src) qui retourne une List<T> contenant un équivalent des éléments de la List<T> reçue en paramètre, mais où les éléments de src ont été décalés à gauche d'une position de manière cyclique (l'élément à la position 0 dans src est placée à la position Count-1 dans la List<T> résultante), de telle sorte que le programme suivant :

using System;
using System.Collections.Generic;
Afficher(RotaterGauche(new List<int>()));
Afficher(RotaterGauche(new List<int>(){ 2,3,5,7,11 }));

static void Afficher<T>(List<T> lst)
{
   foreach(T obj in lst)
      Console.Write($"{obj} ");
   Console.WriteLine();
}

... affiche ce qui suit (notez la première ligne qui est vide, car nous avons opéré sur... une séquence vide!) :


3 5 7 11 2 

Note : essayez d'implémenter cet algorithme sans allouer de mémoire (sans utiliser new).

EX01.2 – Écrivez l'algorithme RotaterDroite<T>(List<T> src) qui retourne une List<T> contenant un équivalent des éléments de la List<T> reçue en paramètre, mais où les éléments de src ont été décalés à droite d'une position de manière cyclique (l'élément à la position Count-1 dans src est placée à la position 0 dans la List<T> résultante), de telle sorte que le programme suivant :

using System;
using System.Collections.Generic;
Afficher(RotaterDroite(new List<int>()));
Afficher(RotaterDroite(new List<int>(){ 2,3,5,7,11 }));

static void Afficher<T>(List<T> lst)
{
   foreach(T obj in lst)
      Console.Write($"{obj} ");
   Console.WriteLine();
}

... affiche ce qui suit (notez la première ligne qui est vide, car nous avons opéré sur... une séquence vide!) :


11 2 3 5 7 

Note : essayez d'implémenter cet algorithme sans allouer de mémoire (sans utiliser new).

EX01.3 – Écrivez la fonction Concaténer<T>(List<T> lst0, List<T> lst1) qui retournera une List<T> contenant les mêmes éléments que lst0, dans l'ordre, suivis des éléments de lst1, dans l'ordre.

EX01.4 – Écrivez l'algorithme Transformer<T,U>(List<T> src, Func<T,U> fct) qui retourne une List<U> contenant un équivalent des éléments de la List<T> reçue en paramètre, mais transformés par application de la fonction fct, de telle sorte que le programme suivant :

using System;
using System.Collections.Generic;

List<int> lst0 = new List<int>(){ 2,3,5,7,11 };
List<double> lst1 = Transformer(lst0, CarréNégatif);
foreach(double x in lst1)
   Console.Write($"{x} ");
Console.WriteLine();
foreach(string s in Transformer(new List<string>(){ "j'aime", "mon", "prof" }, Exclamer))
   Console.Write($"{s} ");
Console.WriteLine();

static double CarréNégatif(int x) => -1.0 * x * x; // nom pas terrible, je sais
static string Exclamer(string s) => s.ToUpper() + "!";

... affiche ce qui suit :

-4 -9 -25 -49 -121 
J'AIME! MON! PROF!

EX01.5 – Écrivez l'algorithme Cumuler<T,U>(List<T> src, Func<U,T,U> accum, U init) qui reçoit en paramètre une List<T>, une fonction applicable à un U et à un T et retournant un U, de même qu'une valeur initiale de type U, et retourne l'accumulation, de telle sorte que le programme suivant :

using System;
using System.Collections.Generic;

Console.WriteLine($"1+2+3+4+5 == {Cumuler(new List<int>(){ 1,2,3,4,5 }, Somme, 0)}");
Console.WriteLine($"1*2*3*4*5 == {Cumuler(new List<int>(){ 1,2,3,4,5 }, Produit, 1.0)}");
Console.WriteLine($"min(2,-3,5,-7,11) == {Cumuler(new List<int>(){ 2,-3,5,-7,9 }, Minimum, int.MaxValue)}");
var mots = new List<string>() { "yo", "man", "genre" };
Console.Write("La somme des longueurs des mots (");
foreach (string s in mots)
   Console.Write($"{s} ");
Console.WriteLine($"\b) est : {Cumuler(mots, CumulLongueur, 0)}");

static int Somme(int x, int y) => x + y;
static double Produit(double x, int y) => x * y;
static int Minimum(int x, int y) => Math.Min(x, y);
static int CumulLongueur(int lg, string s) => lg + s.Length;

... affiche ce qui suit :

1+2+3+4+5 == 15
1*2*3*4*5 == 120
min(2,-3,5,-7,11) == -7
La somme des longueurs des mots (yo man genre) est : 10

EX01.6 – Écrivez la fonction Trouver<T>(List<T> src, T val) qui retournera l'indice de la première occurrence de val dans src, ou -1 si aucune occurrence n'est trouvée.

EX01.7 – Écrivez la fonction TrouverSi<T>(List<T> src, Func<T,bool> pred) qui retournera l'indice du premier élément de src satisfaisant le prédicat pred, ou -1 si aucun élément ne satisfaisant pred n'est trouvé.

EX01.8 – Écrivez la fonction Filtrer<T>(List<T> src, T val) qui retournera une List<T> contenant les mêmes éléments que src, dans le même ordre, à ceci près que toutes les occurrences de la valeur val en auront été supprimées.

EX01.9 – Écrivez la fonction FiltrerSi<T>(List<T> src, Func<T,bool> pred) qui retournera une List<T> contenant les mêmes éléments que src, dans le même ordre, à ceci près que tous les éléments respectant le prédicat pred auront été supprimés.

EX01.10 – Écrivez la fonction Remplacer<T>(List<T> src, T pré, T post) qui retournera une List<T> contenant les mêmes éléments que src, dans l'ordre, mais dont chaque occurrence de pré aura été remplacée par post.

EX01.11 – Écrivez la fonction RemplacerSi<T>(List<T> src, Func<T,bool> pred, T post) qui retournera une List<T> contenant les mêmes éléments que src, dans l'ordre, mais dont chaque élément satisfaisant le prédicat pred aura été remplacé par post.

3 avril

S19

Au menu :

  • Retour sur le minitest 02
  • Retour sur les exercices de S18
    • Note : si vous voulez vraiment profiter de la séance d'aujourd'hui, assurez-vous d'avoir vous-mêmes faits ces exercices au préalable, ou du moins d'avoir sincèrement essayé de les faire!

Certaines et certains d'entre vous ont exprimé le souhait de faire d'autres exercices, alors pour votre bon plaisir. Si vous l'estimez pertinent, vous pouvez réutiliser d'autres algorithmes de votre cru dans vos implémentations (vous ne pouvez pas utiliser des fonctions déjà implémentées dans la bibliothèque standard du langage, le but étant de vous pratiquer et d'apprendre!).

EX02.0 – Écrivez l'algorithme SupprimerDoublons<T>(List<T> src) qui reçoit en paramètre une List<T> préalablement triée (il s'agit d'une précondition de la fonction), et retourne une List<T> contenant les mêmes valeurs que la List<T> originale, mais exempte de doublons, de telle sorte que le programme suivant :

Afficher(SupprimerDoublons(new List<int>()));
Afficher(SupprimerDoublons(new List<int>(){ 2 }));
Afficher(SupprimerDoublons(new List<int>(){ 2, 2 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,3,3,3,5,7,11 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,2,3,5,5,7,11 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,3,5,7,7,11 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,3,5,7,11,11 }));

static void Afficher<T>(List<T> lst)
{
   foreach(T obj in lst)
      Console.Write($"{obj} ");
   Console.WriteLine();
}

... affiche ce qui suit (notez la première ligne qui est vide, car nous avons supprimé les doublons... d'une séquence vide!) :


2 
2 
2 3 5 7 11 
2 3 5 7 11 
2 3 5 7 11 
2 3 5 7 11

EX02.1 – Écrivez l'algorithme Inverser<T>(List<T> lst) qui modifie lst et ne retourne rien. Cette fonction doit inverser l'ordre des éléments de lst, de telle sorte que le programme suivant :

List<int> lst = new();
Inverser(lst);
Afficher(lst);
lst = new(){ 1 };
Inverser(lst);
Afficher(lst);
lst = new(){ 1, 2 };
Inverser(lst);
Afficher(lst);
lst = new(){ 2,3,5,7,11 };
Inverser(lst);
Afficher(lst);

static void Afficher<T>(List<T> lst)
{
   foreach(T obj in lst)
      Console.Write($"{obj} ");
   Console.WriteLine();
}

... affiche ce qui suit (notez la première ligne qui est vide, car nous avons inversé les éléments... d'une séquence vide!) :


1 
2 1 
11 7 5 3 2 

EX02.2 – Écrivez le prédicat EstPalindrome<T>(List<T> lst) qui retourne true seulement si lst est un palindrome. Un palindrome est une séquence qui a la même forme si on l'observe de gauche à droite ou de droite à gauche, comme par exemple un tableau contenant 1,3,3,1 ou le texte "laval". Implémentez cette fonction sans allouer de mémoire.

EX02.3 – Écrivez le prédicat SontTous<T> prenant en paramètre une List<T> et un prédicat applicable à un T, et retournant true seulement si tous les éléments de la List<T> satisfont le prédicat.

EX02.4 – Écrivez le prédicat AuMoinsUn<T> prenant en paramètre une List<T> et un prédicat applicable à un T, et retournant true seulement si au moins un élément de la List<T> satisfait le prédicat.

EX02.5 – Écrivez le prédicat Aucun<T> prenant en paramètre une List<T> et un prédicat applicable à un T, et retournant true seulement si aucun élément de la List<T> ne satisfait le prédicat.

Une implémentation possible de FilePrioritaire<T> serait la suivante (note :  Trier est dans Algos, plus bas) :

class FileVideException : Exception;
internal class FilePrioritaire<T>
{
   List<T> Substrat { get; init; } = new();
   public bool EstVide => Substrat.Count == 0;
   Func<T,T,int> Critère { get; init; }
   public FilePrioritaire(Func<T,T,int> critère)
   {
      Critère = critère;
   }
   public void Add(T elem)
   {
      Substrat.Add(elem);
      Algos.Trier(Substrat, Critère);
   }
   public T Prochain()
   {
      if(EstVide)
         throw new FileVideException();
      T elem = Substrat[0];
      Substrat.RemoveAt(0);
      return elem;
   }
}

Une implémentation possible de des algorithmes proposés suit. Notez que pour Concaténer<T> j'ai utilisé le mot clé params :

internal static class Algos
{
   //
   // ... autres algos du passé (omis par souci d'économie) ...
   //
   // Trier et PermuterÉléments servent à l'implémentation de FilePrioritaire<T>, mais
   // PermuterÉléments peut aussi servir à d'autres algorithmes comme RotaterGauche<T>
   // et RotaterDroite<T> à titre d'exemple
   //
   public static void Trier<T>(List<T> lst, Func<T, T, int> critère) // tri à bulles, pour fins de simplicité
   {
      for (int i = 0; i < lst.Count - 1; ++i)
         for (int j = i + 1; j < lst.Count; ++j)
            if (critère(lst[i], lst[j]) > 0)
               PermuterÉléments(lst, i, j);
   }
   public static void PermuterÉléments<T>(List<T> lst, int i, int j)
   {
      T elem = lst[i];
      lst[i] = lst[j];
      lst[j] = elem;
   }
   //
   // algorithmes demandés à titre d'exercices
   //
   public static void Permuter<T>(ref T a, ref T b)
   {
      T temp = a;
      a = b;
      b = temp;
   }

   //
   // note : j'ai fait plus d'une implémentation de RotaterGauche<T>, ce qui
   //        explique celle laissée en commentaire
   //
   //public static T[] RotaterGauche<T>(T[] src)
   //{
   //   T[] dest = new T[src.Length];
   //   for(int i = 0; i != src.Length; ++i)
   //      dest[i] = src[i];
   //   for (int i = 1; i < dest.Length; ++i)
   //      Permuter(ref dest[i], ref dest[i - 1]);
   //   return dest;
   //}
   //public static List<T> RotaterGauche<T>(List<T> src) =>
   //   RotaterGauche(src.ToArray()).ToList();

   public static List<T> RotaterGauche<T>(List<T> src)
   {
      List<T> dest = new(src);
      for (int i = 1; i < src.Count; ++i)
         PermuterÉléments(dest, i, i - 1);
      return dest;
   }

   public static List<T> RotaterDroite<T>(List<T> src)
   {
      List<T> dest = new(src);
      for (int i = src.Count - 1; i > 0; --i)
         PermuterÉléments(dest, i, i - 1);
      return dest;
   }

   public static List<T> Concaténer<T>(params List<T>[] lsts)
   {
      List<T> dest = new();
      foreach (List<T> lst in lsts)
         dest.AddRange(lst);
      return dest;
   }

   public static List<U> Transformer<T, U>(List<T> src, Func<T, U> f)
   {
      List<U> dest = new();
      foreach (T e in src)
         dest.Add(f(e));
      return dest;
   }

   public static U Cumuler<T, U>(List<T> src, Func<U, T, U> f, U init)
   {
      foreach (T e in src)
         init = f(init, e);
      return init;
   }

   public static int Trouver<T>(List<T> src, T val)
      where T : IEquatable<T>
         => TrouverSi(src, e => e.Equals(val));

   public static int TrouverSi<T>(List<T> src, Func<T, bool> pred)
   {
      for(int i = 0; i != src.Count; ++i)
         if(pred(src[i]))
            return i;
      return -1;
   }

   public static List<T> Filtrer<T>(List<T> src, T val)
      where T : IEquatable<T>
         => FiltrerSi(src, e => e.Equals(val));

   public static List<T> FiltrerSi<T>(List<T> src, Func<T,bool> pred)
   {
      List<T> dest = new();
      foreach(T e in src)
         if(!pred(e))
            dest.Add(e);
      return dest;
   }

   public static List<T> Remplacer<T>(List<T> src, T pre, T post)
      where T : IEquatable<T>
         => RemplacerSi(src, e => e.Equals(pre), post);

   public static List<T> RemplacerSi<T>(List<T> src, Func<T, bool> pred, T post)
      => Transformer(src, e => pred(e) ? post : e);
}

8 avril

S20

Au menu, cours un peu disparate :

  • Exercices supplémentaires de la S19
  • Introduction aux uplets
  • Revisiter notre Tableau de S15 pour en faire un Tableau<T>
  • Entrées / sorties sur des flux
    • Gestion des erreurs
    • Blocs try...finally
    • Blocs using
  • Passer des paramètres à un programme
  • Introduction aux dictionnaires

L'exemple de dictionnaire d'enregistrements vu en classe était, essentiellement :

List<Enregistrement> enr = new();

string nomSource = args.Length != 0 ? args[0] : "../../../test.csv";
using (StreamReader lecteur = new(nomSource))
for (string s = lecteur.ReadLine(); s != null; s = lecteur.ReadLine())
{
   string[] elems = s.Split(';');
   // nom ; prénom ; mois ; année
   enr.Add(new(elems[0], elems[1], elems[2], int.Parse(elems[3])));
   //   Console.WriteLine(s);
}

Dictionary<string, int> d = new();
foreach (var e in enr)
   if (d.ContainsKey(e.MoisNaissance))
      d[e.MoisNaissance]++;
   else
      d.Add(e.MoisNaissance, 1);

foreach (var p in d)
   Console.WriteLine($"Il y a {p.Value} personnes au mois de {p.Key}");

//using (StreamWriter scripteur = new("résultats.txt"))
//   foreach (var e in enr)
//   {
//      scripteur.WriteLine(e);
//      // F(e); // mystère...
//   }


//StreamWriter scripteur = null;
//try
//{
//   scripteur = new("résultats.txt");
//   foreach (var e in enr)
//   {
//      scripteur.WriteLine(e);
//      // F(e); // mystère...
//   }
//}
//finally
//{
//   if(scripteur != null)
//      scripteur.Close();
//}


class Enregistrement
{
   public override string ToString() =>
      $"{Prénom} {Nom} est né(e) en {MoisNaissance} {AnnéeNaissance}";
   public string Nom { get; init; }
   public string Prénom { get; init; }
   public string MoisNaissance { get; init; }
   public int AnnéeNaissance { get; init; }
   public Enregistrement(string nom, string prénom, string mois, int an)
   {
      Nom = nom;
      Prénom = prénom;
      MoisNaissance = mois;
      AnnéeNaissance = an;
   }
}

10 avril

S21

Au menu :

  • Présentation du labo 03 – Le colorieur
  • Travail sur le labo 03 – Le colorieur

Le texte du programme principal imposé est le suivant (vous pouvez ajouter des using et des using static) :

using Labo03;
using static Labo03.Algos;
using static Labo03.OutilsTexte;

Dictionary<Mot, int> mots = new();
List<Mot> motsCles = LireMots("../../../mots_cles.txt");
foreach(string nomFichier in args)
{
   string texte = LireFichier(nomFichier);
   Dictionary<Mot, int> m = Analyser(texte, motsCles);
   texte = Colorier(texte, motsCles);
   ÉcrireHtml(nomFichier, texte, m);
   mots = Fusionner(mots, m);
}
Console.WriteLine(Formater(mots));

Le texte imposé de la méthode OutilsTexte.Colorier, qui vous est donné, est :

      // ...
      public static string Colorier(string s, List<Mot> motsClés)
      {
         StringBuilder sb = new();
         int pos = 0;
         while (pos != s.Length)
         {
            int début = pos;
            for (; début < s.Length && !PeutDébuterMot(s[début]); ++début)
               sb.Append(TraiterMétacaractère(s[début]));
            if (début == s.Length)
            {
               pos = début;
               continue;
            }
            int fin = début;
            for (++fin; fin < s.Length && PeutPoursuivreMot(s[fin]); ++fin)
               ;
            Mot mot = new
            (
               fin == s.Length ?
               s.Substring(début) : s.Substring(début, fin - début)
            );
            if (motsClés.Contains(mot))
               sb.Append(mot.ToStringColorié());
            else
               sb.Append(mot);
            pos = fin;
         }
         return sb.ToString();
      }
      // ...

15 avril

S22

Au menu :

  • Du code générique encore plus utile : l'interface IEnumerable<T>
    • Aujourd'hui, on se limite à son utilisation
    • Au prochain cours, on regarde comment c'est fait!
    • Ça pourrait vous aider dans votre labo 🙂
  • Sélectives (switch) :
    • Forme « énoncé »
    • Forme « expression »
  • Travail sur le labo 03 – Le colorieur

Attention : mardi selon l'horaire du jeudi

17 avril

s/o

Journée pédagogique, cours suspendus

22 avril

S23

Au menu :

  • Les interfaces IEnumerable<T> et IEnumerator<T>, ou comment faire en sorte qu'une collection puisse être traversée à l'aide de foreach
    • Application à ListeSimple, notre liste simplement chaînée de int
    • Application à ListeDouble<T>, notre liste doublement chaînée de T
    • Application à Tableau<T>, notre tableau dynamique de T

Pour ListeSimple, on avait :

class ListeVideException : Exception;
internal class ListeSimple : IEnumerable<int>
{
   class Noeud
   {
      public int Val { get; init; }
      public Noeud Succ { get; set; } = null;
      public Noeud (int val)
      {
         Val = val;
      }
   }
   Noeud Tête { get; set; } = null;
   Noeud Queue { get; set; } = null;
   public bool EstVide => Tête == null;
   // complexité : O(1)
   public void AjouterDébut(int val)
   {
      Noeud p = new(val);
      if (EstVide)
         Queue = p;
      p.Succ = Tête;
      Tête = p;
      ++Count;
   }
   // note : privée
   // précondition : !EstVide
   // complexité : O(n)
   Noeud TrouverDernier()
   {
      Noeud p = Tête;
      for (; p.Succ != null; p = p.Succ)
         ;
      return p;
   }
   // complexité : O(1) :)
   public void AjouterFin(int val)
   {
      if(EstVide)
         AjouterDébut (val);
      else
      {
         Noeud p = new(val);
         Noeud q = Queue;
         q.Succ = p;
         Queue = p;
         ++Count;
      }
   }
   // complexité : O(1)
   public void SupprimerDébut()
   {
      if (EstVide)
         throw new ListeVideException();
      Tête = Tête.Succ;
      if (EstVide)
         Queue = null;
      --Count;
   }
   public int Count { get; private set; } = 0;
   // complexité : O(1)
   public int PeekDébut()
   {
      if (EstVide)
         throw new ListeVideException();
      return Tête.Val;
   }
   // complexité : O(n)
   public int PeekFin()
   {
      if (EstVide)
         throw new ListeVideException();
      return TrouverDernier().Val;
   }
   // TEMPORAIRE ARK ARK ARK
   //public void Afficher()
   //{
   //   for (Noeud p = Tête; p != null; p = p.Succ)
   //      Console.Write($"{p.Val} ");
   //   Console.WriteLine();
   //}
   // duplication (version méthode d'instance)
   // complexité : O(n)... :)
   public ListeSimple Dupliquer()
   {
      ListeSimple lst = new();
      for(Noeud p = Tête; p != null; p = p.Succ)
         lst.AjouterFin(p.Val);
      return lst;
   }
   public IEnumerator<int> GetEnumerator() => // :)
      new Énumérateur(this);
   IEnumerator IEnumerable.GetEnumerator() => // relique du passé
      new Énumérateur(this);
   class Énumérateur : IEnumerator<int>
   {
      Noeud Cur { get; set; }
      public Énumérateur(ListeSimple src)
      {
         Cur = new(0); // 0 : bof
         Cur.Succ = src.Tête;
      }
      public int Current => Cur.Val;
      object IEnumerator.Current => Cur.Val;
      public void Dispose() { }
      public bool MoveNext()
      {
         // si je ne peux plus progresser : faux
         if(Cur.Succ == null)
            return false;
         // sinon, je progresse, puis vrai
         Cur = Cur.Succ;
         return true;
      }
      public void Reset() { }
   }
   // duplication (version méthode de classe)
   //public static ListeSimple Dupliquer(ListeSimple src)
   //{
   //   ListeSimple lst = new();
   //   for (Noeud p = src.Tête; p != null; p = p.Succ)
   //      lst.AjouterFin(p.Val);
   //   return lst;
   //}
}

Pour ListeDouble<T>, on avait :

internal class ListeDouble<T> : IEnumerable<T>
   where T : IEquatable<T> 
{
   class Noeud
   {
      public T Val { get; init; }
      public Noeud Succ { get; set; } = null;
      public Noeud Pred { get; set; } = null;
      public Noeud(T val)
      {
         Val = val;
      }
   }
   public int Count { get; private set; } = 0;
   Noeud Tête { get; set; } = null;
   Noeud Queue { get; set; } = null;
   public bool EstVide => Count == 0;
   // AjouterDébut
   public void AjouterDébut(T val)
   {
      Noeud p = new(val);
      if (EstVide)
         Tête = Queue = p;
      else
      {
         Tête.Pred = p;
         p.Succ = Tête;
         Tête = p;
      }
      ++Count;
   }
   // AjouterFin
   public void AjouterFin(T val)
   {
      Noeud p = new(val);
      if (EstVide)
         Tête = Queue = p;
      else
      {
         Queue.Succ = p;
         p.Pred = Queue;
         Queue = p;
      }
      ++Count;
   }
   // SupprimerDébut
   public void SupprimerDébut()
   {
      if (EstVide)
         throw new ListeVideException();
      Tête = Tête.Succ;
      if (Tête == null)
         Queue = null;
      else
         Tête.Pred = null;
      --Count;
   }
   // SupprimerFin
   public void SupprimerFin()
   {
      if (EstVide)
         throw new ListeVideException();
      Queue = Queue.Pred;
      if (Queue == null)
         Tête = null;
      else
         Queue.Succ = null;
      --Count;
   }
   // PeekDébut
   public T PeekDébut()
   {
      if (EstVide)
         throw new ListeVideException();
      return Tête.Val;
   }
   // PeekFin
   public T PeekFin()
   {
      if (EstVide)
         throw new ListeVideException();
      return Queue.Val;
   }
   // Dupliquer
   public ListeDouble<T> Dupliquer()
   {
      ListeDouble<T> lst = new();
      for (Noeud p = Tête; p != null; p = p.Succ)
         lst.AjouterFin(p.Val);
      return lst;
   }
   // ARK ARK ARK
   //public void Afficher()
   //{
   //   for (Noeud p = Tête; p != null; p = p.Succ)
   //      Console.Write($"{p.Val} ");
   //   Console.WriteLine();
   //}
   // Contient : retourne true seulement si la liste
   //            contient une valeur donnée
   // complexité : O(n)
   public bool Contient(T val) => Trouver(val) != null;
   //{
   //   for(Noeud p = Tête; p != null; p = p.Succ)
   //      if(p.Val.Equals(val)/*p.Val == val*/)
   //         return true;
   //   return false;
   //}
   // Compter : retourne le nombre d'occurrences d'une
   //           valeur donnée dans la liste
   // complexité : O(n)
   public int Compter(T val)
   {
      int n = 0;
      for (Noeud p = Tête; p != null; p = p.Succ)
         if (p.Val.Equals(val)/*p.Val == val*/)
            ++n;
      return n;
   }
   // Supprimer : supprimer la première occurrence
   //             d'une valeur dans la liste
   public void Supprimer(T val)
   {
      Noeud p = Trouver(val);
      // ici, soit p est null, soit p.Val vaut val
      if(p != null)
      {
         if (p.Succ == null) // dernier noeud
            Queue = p.Pred;
         else
            p.Succ.Pred = p.Pred;
         if (p.Pred == null) // premier noeud
            Tête = p.Succ;
         else
            p.Pred.Succ = p.Succ;
         --Count;
      }
   }
   // (privée) Trouver : retourne une référence sur
   //          la première occurrence d'un noeud d'une
   //          valeur donnée, null sinon
   Noeud Trouver(T val)
   {
      for (Noeud p = Tête; p != null; p = p.Succ)
         if (p.Val.Equals(val)/*p.Val == val*/)
            return p;
      return null;
   }
   public IEnumerator<T> GetEnumerator() =>
      new Énumérateur(this);
   IEnumerator IEnumerable.GetEnumerator() =>
      new Énumérateur(this);
   class Énumérateur : IEnumerator<T>
   {
      Noeud Cur { get; set; }
      public Énumérateur(ListeDouble<T> src)
      {
         Cur = new(default) ;
         Cur.Succ = src.Tête;
      }
      public T Current => Cur.Val;
      object IEnumerator.Current => Cur.Val;
      public void Dispose() { }
      public bool MoveNext()
      {
         if (Cur.Succ == null)
            return false;
         Cur = Cur.Succ;
         return true;
      }
      public void Reset() { }
   }
}

Pour Tableau<T>, on avait :

internal class Tableau<T> : IEnumerable<T>
   where T : IEquatable<T>, IComparable<T>
{
   T[] Substrat { get; set; }
   public int Count { get; private set; }
   // invariant : Substrat != null
   public int Capacity => Substrat.Length;
   public bool EstVide => Count == 0;
   public bool EstPlein => Count == Capacity;
   // modélise un Tableau vide
   // complexité : O(1) (constante)
   public Tableau()
   {
      Substrat = new T[0];
      Count = 0;
   }
   // modélise un ajout d'une valeur à la fin du Tableau
   // complexité : O(1) amortie
   //              - constante d'habitude
   //              - linéraire à l'occasion (malchance)
   public void Add(T val)
   {
      if (EstPlein)
         Croître();
      Substrat[Count] = val;
      ++Count;
   }
   // accroît la capacité du substrat
   // complexité : O(n) (linéaire)
   void Croître()
   {
      int nouvCap = Capacity == 0 ? 8 : Capacity * 2;
      T[] tab = new T[nouvCap];
      for(int i = 0; i != Count; ++i)
         tab[i] = Substrat[i];
      Substrat = tab;
   }
   // TEMPORAIRE (ark ark ark)
   //public void Afficher()
   //{
   //   for(int i = 0; i != Count; ++i)
   //      Console.Write($"{Substrat[i]} ");
   //   Console.WriteLine();
   //}
   // complexité : O(n)
   public void InsertAt(int indice, T val)
   {
      if (indice < 0 || Count <= indice)
         throw new IndexOutOfRangeException();
      if (EstPlein)
         Croître();
      for (int i = Count - 1; i >= indice; --i)
         Substrat[i + 1] = Substrat[i];
      Substrat[indice] = val;
      ++Count;
   }
   // complexité : O(n)
   public void RemoveAt(int indice)
   {
      if (indice < 0 || Count <= indice)
         throw new IndexOutOfRangeException();
      for (int i = indice + 1; i < Count; ++i)
         Substrat[i - 1] = Substrat[i];
      --Count;
   }
   // trouve la première occurrence de val dans substrat
   // complexité : O(n)
   int TrouverPremièreOccurrence(T val)
   {
      for(int i = 0; i != Count; ++i)
         if(Substrat[i].Equals(val))
            return i;
      return -1;
   }
   // complexité : O(n)
   public void Remove(T val)
   {
      int indice = TrouverPremièreOccurrence(val);
      RemoveAt(indice);
   }
   // complexité : O(n^2) (quadratique)
   public void Trier() // tri à bulles : ne faites pas ça...!
   {
      for(int i = 0; i < Count - 1; ++i) // main gauche
         for(int j = i + 1; j < Count; ++j) // main droite
            if(!(Substrat[i].CompareTo(Substrat[j]) <0))
            {
               T temp = Substrat[i];
               Substrat[i] = Substrat[j];
               Substrat[j] = temp;
            }
   }
   public IEnumerator<T> GetEnumerator() =>
      new Énumérateur(this);
   IEnumerator IEnumerable.GetEnumerator() =>
      new Énumérateur(this);
   class Énumérateur : IEnumerator<T>
   {
      Tableau<T> Src { get; init; }
      int Indice { get; set; } = -1;
      public Énumérateur(Tableau<T> src)
      {
         Src = src;
      }
      public T Current => Src[Indice];
      object IEnumerator.Current => Src[Indice];
      public bool MoveNext()
      {
         // si je ne peux pas avancer : faux
         if (Indice == Src.Count - 1)
            return false;
         // sinon, j'avance puis vrai
         ++Indice;
         return true;
      }
      public void Reset() { }
      public void Dispose() { }
   }
   // en attendant mieux...
   //public T GetÉlément(int indice) => Substrat[indice];
   public T this[int indice]
   {
      get => Substrat[indice];
      set { Substrat[indice] = value; }
   }
}

24 avril

S24

Au menu : votre chic prof doit s'absenter pour des raisons personnelles

29 avril

S25

Au menu :

  • Votre chic prof est à l'hôpital avec un de ses enfants, mais il vous laisse un enregistrement sur Teams et un chic laboratoire à faire (le labo 04 – Cobayes)

1 mai

S26

Au menu :

  • Minitest Q03
  • Remise du labo 03 – Colorieur
  • Travail sur le labo 04 – Cobayes

6 mai

S27

Au menu :

  • Minitest Q04
    • À faire à la maison, mais individuellement
  • Travail sur le labo 04 – Cobayes

Allo!

On a eu la gentillesse de me demander quelques exercices pour pratiquer en vue de la PFI et de l'examen final cette semaine. Les quatre sujets suivants m'ont été mentionnés (je peux en ajouter, mais le temps file alors essayez de m'avertir rapidement si vous voulez quelque chose... et essayez d'être spécifiques!)

Sujet: Encapsulation et conception de classes

  • Question : dans quelle situation devrait-on utiliser set?

Réponse : quand la propriété est modifiable pendant la vie de l'objet. Par exemple :

class Voiture
{
   // ...
   public int Kilométrage{ get; private set; }; // ← peut changer quand la voiture se déplace, donc set
   // ...
   public Voiture()
   {
      // ...
      Kilométrage = 0; // kilométrage initial
      // ...
   }
   public void Avancer(int nbMètres)
   {
      Kilométrage += nbMètres / 1000; // grossière approximation, juste pour illustrer le propos
   }
   // ...
}
  • Dans quelle situation devrait-on utiliser init?

Réponse : quand la propriété est n'est plus modifiable une fois l'objet construit. Par exemple :

class Voiture
{
   // ...
   public string Marque{ get; private init; }; // ← fixé pour la vie de la voiture
   // ...
   public Voiture(string marque)
   {
      // ...
      Marque = marque; // ça fait « partie de son ADN » désormais :)
      // ...
   }
   // ...
}
  • Question : comment choisir entre set et init?

Réponse : un bon truc à retenir est que, dans un langage comme C# où les classes sont des « types références » donc où les référés sont partagés jusqu'à preuve du contraire, tout ce qui est modifiable demande de la prudence. Ainsi, c'est probablement mieux de préférer init si possible, et d'utiliser set si nécessaire. Ça évite des surprises comme :

class Point
{
   public int X { get; set; } // ← set semble innocent ici, mais...
   public int Y { get; set; }
   public Point() : this(0,0) {}
   public Point(int x, int y)
   {
      X = x;
      Y = y;
   }
}
class Cercle
{
   public float Rayon{ get; init; }
   public Point Centre { get; init; } // ← note : init, mais Point n'est pas immuable
   // supposons qu'on tienne à ce que Centre soit dans le premier quadrant
   static bool EstAcceptable(Point p) => p.X >=0 && p.Y >= 0;
   public Cercle(float rayon, Point centre)
   {
      Rayon = rayon;
      Centre = EstAcceptable(centre) ? centre : throw new ArgumentException();
   }
}
// ...
// on arrive au code client

Point pt = new(); // 0,0
Cercle c = new(1.0f, pt); // Ok, pt est dans le premier quadrant
pt.X = -15; // OUPS! pt est partagé par c et par le code client...
            // un nvariant de c vient d'être brisé
  • Question : comment identifier les préconditions, postconditions et les invariants dans un programme existant?

Réponse : il n'y a pas de réponse universelles à celle-là, mais si le code est bien écrit, on a quelques indices :

  • Les constructeurs, les set et les init qui font de la validation tendent à nous dire quelles sont les conditions de validité de la classe, donc à décrire les invariants. Évidemment, si on éparpille la validation partout, c'est plus difficile à voir
  • Les préconditions sont les trucs que la fonction appelée tient pour acquis. Par exemple :
    • si une fonction accepte un indice en paramètre et l'utilise aveuglément pour accéder un tableau, alors une précondition de cette fonction est que cet indice soit valide
    • si une fonction accepte une référence sur un objet et s'en sert sans valider qu'il soit non-nul, alors c'est une précondition de cette fonction qu'il soit non-nul
    • si une fonction validant si un paramètre est entre deux bornes min et max se sert de min et max sans s'assurer que min<=max au préalable, alors il s'agit d'une de ses préconditions
  • Les postconditions sont les effets que garantit la fonction appelée, dans la mesure où les préconditions sont respectées bien sûr. Par exemple, dans le code de la classe Tableau<T> faite en classe, les postconditions de Add sont :
    • dans le cas d'un succès sont (a) que l'élément à ajouter soit bien ajouté à la fin du substrat, (b) que le Count ait incrémenté de un, et (c) que la Capacité après le Add soit au moins aussi grande qu'elle ne l'était avant le Add
    • dans le cas d'un échec, (a) que l'élément à  ajouter ne sont pas ajouté, (b) que le Count demeure inchangé, (c) que la Capacité demeure inchangée, et (d) qu'une exception soit levée pour rapporter la raison de l'insuccès (probablement un manque de mémoire lors du new)

Dans du code bien écrit, les préconditions, postconditions et, dans le cas des classes, les invariants devraient être clairement identifiés, au moins dans des commentaires. C'est pas toujours aussi clair dans le cas d'une évaluation à l'école, je vous l'accorde.

Sujet: Surchage d'opérateurs

  • Question : comment lire (et comprendre) un message d'erreur lorsque le programme ne compile pas?

Réponse : c'est difficile de répondre à ceci parce qu'il y a beaucoup de cas de figure selon les erreurs!

Essayons une réponse avec un exemple simple. Voici la version correcte, qui compile et affiche la bonne valeur (https://dotnetfiddle.net/rVwi7V) :

using System;

X a = new(3), b = new(2);
X c = a + b;
Console.Write(c);

class X
{
   int Val{ get; init; }
   public X (int val) { Val = val; }
   public static X operator+(X x0, X x1) => new(x0.Val + x1.Val); // ← ici
   public override string ToString() => $"{Val}";
}

Quelles sont les erreurs possibles? Essayons quelques options (note : j'utilise .NET Fiddle ici alors les messages d'erreurs sont en anglais, mais ils me semblent lisibles tout de même) :

  • Oublier le mot public (https://dotnetfiddle.net/jMki22). Le message d'erreur est « Compilation error (line 11, col 30): User-defined operator 'X.operator +(X, X)' must be declared static and public ».
  • Oublier le mot static (https://dotnetfiddle.net/lCnrje). Le message d'erreur est encore une fois « Compilation error (line 11, col 30): User-defined operator 'X.operator +(X, X)' must be declared static and public ».
  • L'écrire de manière plus longue, avec accolades et return explicite, mais oublier le return (https://dotnetfiddle.net/wzhlXA). Le message d'erreur est « Compilation error (line 11, col 26): 'X.operator +(X, X)': not all code paths return a value ».
  • Essayer de retourner un int plutôt qu'un X (https://dotnetfiddle.net/gefGft). Le message d'erreur est « Compilation error (line 11, col 43): Cannot implicitly convert type 'int' to 'X' ».
  • On pourrait aussi oublier un paramètre, mais faites attention car l'opérateur + unaire (à un seul opérande) existe et est légal, tout comme l'est l'opérateur - unaire que nous avons utilisé à quelques reprises.

Les messages me semble plutôt clairs (ça dit la ligne, la colonne, et la raison précise du problème dans chaque cas). Faut surtout les lire avec attention.

Sujet: Héritage et polymorphisme

  • Question : comment les manipuler, en profiter, écrire du code qui en tient compte?

Réponse : il y a ../../../Sujets/Divers--cdiese/Polymorphisme.html qui est très détaillé, mais pour une idée plus simple... Imaginez tout cas où (a) il y a le verbe « être » qui intervient dans la relation, (b) il y a au moins un comportement commun à plusieurs classes, et (c) il y a des variations dans les manières d'implémenter ce comportement...

Par exemple, vous avez des clowns dans un cirque. Tout clown fait des grimaces, et par défaut ça fait le son "Blah" :

class Clown
{
   public string Nom{ get; init; }
   public Clown(string nom) { Nom = nom; }
   public virtual string Grimace() => "Blah";
}

Toutefois, il y a des clowns plus spécialisés. Par exemple, un clown triste qui, dans ses grimaces, fait "Bouhouhou" :

class ClownTriste : Clown
{
   public ClownTriste(string nom) : base(nom) { }
   public override string Grimace() => "Bouhouhou";
}

... et des clowns qui font un peu plus peur :

class Pennywise : Clown
{
   public Pennywise(string nom) : base(nom) { }
   public override string Grimace() => "NYAHAHA!";
}

Armé de cette hiérarchie de clowns, on peut faire un programme... divertissant (https://dotnetfiddle.net/T8TUF6) :

using System;
using System.Collections.Generic;

foreach(var clown in CréerAmuseurs(10))
   AmuserAvec(clown);

static List<Clown> CréerAmuseurs(int n)
{
   List<Clown> lst = new();
   Random dé = new();
   for(int i = 0; i != n; ++i)
	   lst.Add((dé.Next() % 3) switch {
	      0 => new Clown($"Clown {n + 1}"),
		  1 => new ClownTriste($"Clown {n + 1}"),
		  2 => new Pennywise($"Clown {n + 1}"),
		  _ => null // impossible mais faut faire taire le compilateur
	   });
   return lst;
}

static void AmuserAvec(Clown c)
{
   Console.WriteLine($"{c.Nom} fait {c.Grimace()}");
}
// ...

Sujet: Interface, encapsulation et code générique

  • Demande : un peu de pratique ne ferait pas de tort

Réponse : il y a quand même pas mal d'exercices aux séances S17, S18 et S19. Pour en ajouter quelques-uns :

EX00 – Écrivez la fonction générique Séparer<T> acceptant en paramètre un IEnumerable<T> et un prédicat applicable à un T, et retournant un uplet fait de deux List<T>. La première doit contenir les éléments de l'énumérable qui satisfont le prédicat, alors que la seconde doit contenir ceux qui ne satisfont pas le prédicat.

Par exemple, avec le code client suivant :

var (oui, non) = Séparer(new []{ 1,2,3,4,5 }, n => n % 2 == 0);

... la List<int> nommée oui contiendra 2,4 alors que la List<int> nommée non contiendra 1,3,5.

EX01 -- Écrivez la classe Ensemble<T> qui est telle que chacun de ses éléments est unique (il n'est égal à aucun autre). On aimerait que le code suivant :

Ensemble<int> e0 = new(), e1 = new();
foreach(int n in new[]{ 2,3,5,7,11 })
   e0.Add(n);
foreach(int n in new[]{ 2,3,3,5,7,11,5,2,3 })
   e1.Add(n);
Console.WriteLine(e0 == e1 ? "OUI" : "NON");
Console.WriteLine(e0.Count == 5 ? "OUI" : "NON");
Console.WriteLine(e1.Count == 9 ? "OUI" : "NON)";
Console.WriteLine(e1.Count == 5 ? "OUI" : "NON");

... compile et affiche "OUI", "OUI", "NON", "OUI" dans l'ordre.

  • Petit bonus, enregistré a posteriori à travers Teams et envoyé par Colnet. Petit tour d'horizon des services de LiNQ, et liens avec notre classe Algos, soit  :
    • Sum
    • Average
    • Aggregate
    • All
    • Any
    • Chunk
    • Concat
    • Distinct (malheureusement inefficace)
    • Except
    • Max
    • Min
    • Order,
    • Where et Select, etc.

Quelques exemples choisis, incluant des liens avec nos fonctions dans Algos :

List<int> lst = new() { 2, 3, 5, 7, 11 };

//
// Certains algorithmes sont limités à des types primitifs
// dû aux limites de C# (p.ex.: Sum, Average)
//
// D'autres sont plus généraux
//

// équivalent de Algos.Cumuler(lst, (x,y) => x + y, 0);
Console.WriteLine(lst.Sum()); // 28
// équivalent de Algos.Cumuler(lst, (x,y) => x * y, 1);
Console.WriteLine(lst.Aggregate(1, (x, y) => x * y)); // 2310
// équivalent de Algos.CompterSi(lst, n => n % 2 == 0) == lst.Count;
Console.WriteLine(lst.All(n => n % 2 == 0) ? "Oui" : "Non"); // Non
// équivalent de Algos.TrouverSi(lst, n => n % 2 == 0) != -1;
Console.WriteLine(lst.Any(n => n % 2 == 0) ? "Oui" : "Non"); // Oui
// équivalent à (double) lst.Sum() / lst.Count
Console.WriteLine(lst.Average()); // 5,6
try
{
   Console.WriteLine(new List<int>().Average());
}
catch(InvalidOperationException) // illégal sur une séquence vide
{
   Console.WriteLine("Ouf"); // <-- ICI
}
// grouper les éléments en blocs de 3 éléments ou moins
foreach (var chunk in lst.Chunk(3))
{
   foreach (var e in chunk)
      Console.Write($"{e} ");
   Console.WriteLine();
}
// équivalent à Concaténer(lst, new int[]{ 1,2,3 });
lst = lst.Concat(new int[] { 1, 2, 3 }).ToList();
foreach (int n in lst)
   Console.Write($"{n} "); // 2 3 5 7 11 1 2 3
Console.WriteLine();
// nous n'avons pas codé celle-ci, mais elle est intéressante
// (note : ce qui suit est une preuve que le code est inefficace;
// on s'en reparle?)
lst = lst.Distinct().ToList();
foreach (int n in lst)
   Console.Write($"{n} "); // 2 3 5 7 11 1
Console.WriteLine();
// équivalent à Algos.FiltrerSi(lst, e => !new int[]{ 1,3 }.Contains(e));
foreach(int n in lst.Except(new int[] { 1, 3 }))
   Console.Write($"{n} "); // 2 5 7 11
Console.WriteLine();
// équivalent à Algos.Accumuler(lst, Math.Max, lst[0]);
Console.WriteLine(lst.Max()); // 11
// équivalent à Algos.Accumuler(lst, Math.Min, lst[0]);
Console.WriteLine(lst.Min()); // 1
// 
lst = lst.Order().ToList(); // trier en ordre croissant
foreach (int n in lst)
   Console.Write($"{n} "); // 1 2 3 5 7 11
Console.WriteLine();
// etc. (il y en a beaucoup, et on peut en ajouter!)

8 mai

s/o

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

13 mai

S28

Au menu :

  • PFI

15 mai

S29

Au menu :

  • 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 (note : ce tableau est celui de H2021; je ne l'ai pas mis à jour pour H2024).

Travail Nom Rôle

Labo 00

Le Cryptographe

Travail sur l'encapsulation et la conception de classes

Labo 01

À venir

Travail sur l'héritage et le polymorphisme

Labo 02

À venir

Travail sur les interfaces, l'encapsulation et le code générique

Labo 03

À venir

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

PFI

Production finale d'intégration

À venir

Solutionnaires et exemples

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

Programme de test – S04 Exercice 1a

L'exercice 1a de la séance S04 porte sur une classe Point générale dont chaque instance représente un point dans n'importe quel quadrant du plan cartésien. Un programme de test pour cet exercice suit.

//---------------------------------------------------------
// Programme de test de la classe Point
// 
// Ce programme vérifie que la classe Point fait 
// correctement son travail
//
// par Pierre Prud'homme, 2013 (retouché par Patrice Roy, 2020 puis 2023)
//---------------------------------------------------------
using System;

// test du constructeur par défaut
Point pDéfaut = new ();
AfficherPoint("Test du constructeur par défaut", pDéfaut);
AfficherSéparateur();

// test du constructeur paramétrique
Point pQuadrant1 = new (1.1f, 1.1f);
Point pQuadrant2 = new (-2.2f, 2.2f);
Point pQuadrant3 = new (-3.3f, -3.3f);
Point pQuadrant4 = new (4.4f, -4.4f);

AfficherPoint("Test du constructeur paramétrique pour Q1", pQuadrant1);
AfficherSéparateur();
AfficherPoint("Test du constructeur paramétrique pour Q2", pQuadrant2);
AfficherSéparateur();
AfficherPoint("Test du constructeur paramétrique pour Q3", pQuadrant3);
AfficherSéparateur();
AfficherPoint("Test du constructeur paramétrique pour Q4", pQuadrant4);
AfficherSéparateur();

// test du mutateur de x et de y
pQuadrant1.X = 10.10f;
pQuadrant1.Y = 10.10f;
AfficherPoint("Test du mutateur de Q1", pQuadrant1);
AfficherSéparateur();

TesterDistance();


// --------------------------
// fin du programme principal
// --------------------------


static bool AssezProches(float f0, float f1) => Math.Abs(f0 - f1) <= Math.Pow(10, -6);
static void TesterDistanceEx(Point p0, Point p1, float attendu)
{
   float dist = p0.Distance(p1);
   if (AssezProches(dist, attendu))
   {
      Console.WriteLine($"Distance entre [{p0.X},{p0.Y}] et [{p1.X},{p1.Y}] environ {attendu} comme prévu");
   }
   else
   {
      Console.WriteLine($"ERREUR : distance entre [{p0.X},{p0.Y}] et [{p1.X},{p1.Y}] == {dist}, mais {attendu} est attendu");
   }
}
static void TesterDistance()
{
   TesterDistanceEx(new Point(0,0), new Point(0,0), 0);
   TesterDistanceEx(new Point(0,0), new Point(1,0), 1);
   TesterDistanceEx(new Point(0,0), new Point(0,1), 1);
   TesterDistanceEx(new Point(0,0), new Point(1,1), (float) Math.Sqrt(2));
}

static void AfficherPoint(string message, Point p)
{
   Console.WriteLine($"{message} : le point vaut [{p.X}, {p.Y}]");
}
static string CréerSéparateur(int nbCar) => new string('-', nbCar);
static void AfficherSéparateur()
{
   const int NB_CAR_LIGNE = 72;
   Console.WriteLine($"\n{CréerSéparateur(NB_CAR_LIGNE)}\n");
}

Programme de test – S04 Exercice 1b

L'exercice 1b de la séance S04 porte sur une classe PointQuadrant1 générale dont chaque instance représente un point situé dans le premier quadrant du plan cartésien. Un programme de test pour cet exercice suit.

//---------------------------------------------------------
// Programme de test de la classe PointQuadrant1
// 
// Ce programme vérifie que la classe Point fait 
// correctement son travail
//
// par Pierre Prud'homme, 2013 (retouché par Patrice Roy, 2020 puis 2023)
//---------------------------------------------------------
using System;

TesterConstructeurParamétrique();
TesterMutateurs();


// --------------------------
// fin du programme principal
// --------------------------


static void AfficherPoint(string message, PointQuadrant1 p)
{
   Console.WriteLine($"{message} :");
   Console.WriteLine($"Le point vaut [{p.X}, {p.Y}]");
}
static string CréerSéparateur(int nbCar) => new string('-', nbCar);
static void AfficherSéparateur()
{
   const int NB_CAR_LIGNE = 72;
   Console.WriteLine($"\n{CréerSéparateur(NB_CAR_LIGNE)}\n");
}

static void TesterConstructeurParamétrique()
{
   // tests du constructeur paramétrique
   TesterPoint(1.1f, 1.1f);
   TesterPoint(-2.2f, 2.2f);
   TesterPoint(-3.3f, -3.3f);         
   TesterPoint(4.4f, -4.4f);
}

static void TesterPoint(float x, float y)
{
   Console.WriteLine($"Point reçu : [{x}, {y}]");
   try
   {
      PointQuadrant1 p = new (x, y);
      AfficherPoint("Test réussi du constructeur paramétrique pour PointQuadrant1", p);
   }
   catch (CoordonnéeXInvalideException)
   {
      Console.WriteLine("Point invalide en x lors de la construction");
   }
   catch (CoordonnéeYInvalideException)
   {
      Console.WriteLine("Point invalide en y lors de la construction");
   }
   AfficherSéparateur();
}

static void TesterMutateurs()
{
   PointQuadrant1 p = new (0, 0);

   ModifierPoint(p, 11.0f, p.Y);
   ModifierPoint(p, p.X, 11.0f);
   ModifierPoint(p, 111.0f, 111.0f);
   ModifierPoint(p, -22.2f, p.Y);
   ModifierPoint(p, p.X, -22.2f);
   ModifierPoint(p, -222.0f, -222.0f);
}

static void ModifierPoint(PointQuadrant1 p, float x, float y)
{
   Console.WriteLine($"Modification du point : [{x}, {y}]");
   try
   {
      p.X = x;
      p.Y = y;
      Console.WriteLine("Test réussi de la mutation du point");
   }
   catch (CoordonnéeXInvalideException)
   {
      Console.WriteLine("Point invalide en x lors de la modification");
   }
   catch (CoordonnéeYInvalideException)
   {
      Console.WriteLine("Point invalide en y lors de la modification");
   }
   AfficherPoint("Après mutation ", p);
   AfficherSéparateur();
}

Code vu en classe à la séance S05 pour l'exercice 3 de la séance S04

Le code vu en classe suit.

Classe Algos

Notez que cette classe est en évolution alors que la session progresse, et que nous la séparerons éventuellement en plusieurs classes.

using System;
namespace z
{
   class Algos
   {
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
      static char[] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      //
      // Ceci est un cas raisonnable de double point de sortie pour une fonction :
      // sortir dès qu'on a trouvé la réponse permet d'économiser du temps, peut-être
      // même _beaucoup_ de temps
      //
      public static bool EstDans(char c, char [] cars)
      {
         foreach(char ch in cars)
            if(ch == c)
               return true;
         return false;
      }
      public static bool ContientSeulementConsonnnes(string s)
      {
         foreach(char c in s)
            if(!EstConsonne(c))
               return false;
         return true;
      }
      public static bool EstVoyelle(char c) => EstDans(char.ToLower(c), voyelles);
      public static bool EstConsonne(char c) => char.IsAsciiLetter(c) && !EstVoyelle(c);
   }
}

Classe Personnage

using System;
namespace z
{
   class ForceInvalideException : Exception { }
   class NomInvalideException : Exception { }
   class Personnage
   {
      public int Vie { get; private set; }
      public bool EstMort => Vie <= 0;
      public bool EstVivant => !EstMort;
      public string Nom { get; private init; }
      public int Force { get; private init; }
      public Personnage(string nom, int vie, int force)
      {
         Nom = nom;
         Vie = vie;
         Force = force;
      }
   }
}

Classe Héros

using System;
namespace z
{
   class Héros : Personnage
   {
      public Héros(string nom, int force)
         : base(nom, GénérerVieInitiale(), ValiderForce(force))
      {
      }
      static int GénérerVieInitiale()
      {
         const int VIE_MIN = 50,
                   VIE_MAX = 100;
         return new Random().Next(VIE_MIN, VIE_MAX + 1); // bof
      }
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
   }
}

Classe Monstre

using System;
namespace z
{
   class Monstre : Personnage
   {
      public Monstre(string nom, int vie, int force)
         : base(ValiderNom(nom), vie, ValiderForce(force))
      {
      }
      const int LG_MAX_NOM = 5;
      static bool EstNomValide(string candidat) =>
         candidat.Length <= LG_MAX_NOM &&
         Algos.ContientSeulementConsonnes(candidat);
      static string ValiderNom(string candidat) =>
         EstNomValide(candidat) ? candidat : throw new NomInvalideException();
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
   }
}

Code (complet cette fois) vu en classe à la séance S06 pour l'exercice 3 de la séance S04

Le code vu en classe suit.

Classe Algos

Notez que cette classe est en évolution alors que la session progresse, et que nous la séparerons éventuellement en plusieurs classes. Depuis la version précédente, rien n'a changé.

using System;
namespace z
{
   class Algos
   {
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
      static char[] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      //
      // Ceci est un cas raisonnable de double point de sortie pour une fonction :
      // sortir dès qu'on a trouvé la réponse permet d'économiser du temps, peut-être
      // même _beaucoup_ de temps
      //
      public static bool EstDans(char c, char [] cars)
      {
         foreach(char ch in cars)
            if(ch == c)
               return true;
         return false;
      }
      public static bool ContientSeulementConsonnnes(string s)
      {
         foreach(char c in s)
            if(!EstConsonne(c))
               return false;
         return true;
      }
      public static bool EstVoyelle(char c) => EstDans(char.ToLower(c), voyelles);
      public static bool EstConsonne(char c) => char.IsAsciiLetter(c) && !EstVoyelle(c);
   }
}

Classe Personnage

Depuis la version précédente, nous avons ajouté la méthode Frapper (notez la qualification protected). Nous avons choisi d'offrir une fonction générale à deux paramètres qui pourra être appelée par d'autres de manière à préserver le côté privé du mutateur de Vie.

using System;
namespace z
{
   class ForceInvalideException : Exception { }
   class NomInvalideException : Exception { }
   class Personnage
   {
      public int Vie { get; private set; }
      public bool EstMort => Vie <= 0;
      public bool EstVivant => !EstMort;
      public string Nom { get; private init; }
      public int Force { get; private init; }
      public Personnage(string nom, int vie, int force)
      {
         Nom = nom;
         Vie = vie;
         Force = force;
      }
      protected void Frapper(Personnage autre, int dégâts)
      {
         autre.Vie -= dégâts;
      }
   }
}

Classe Héros

Depuis la version précédente, nous avons ajouté la méthode Frapper. Nous avons choisi d'offrir une fonction spécialisée à un paramètre qui déléguera le travail plus général vers la méthode à deux paramètres du parent.

using System;
namespace z
{
   class Héros : Personnage
   {
      public Héros(string nom, int force)
         : base(nom, GénérerVieInitiale(), ValiderForce(force))
      {
      }
      static int GénérerVieInitiale()
      {
         const int VIE_MIN = 50,
                   VIE_MAX = 100;
         return new Random().Next(VIE_MIN, VIE_MAX + 1); // bof
      }
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
      public void Frapper(Personnage autre)
      {
         const int PCT_MIN = 50,
                   PCT_MAX = 100;
         int dégâts = Force * new Random().Next(PCT_MIN, PCT_MAX + 1) / 100;
         Frapper(autre, dégâts);
      }
   }
}

Classe Monstre

Depuis la version précédente, nous avons ajouté la méthode Frapper. Nous avons choisi d'offrir une fonction spécialisée à un paramètre qui déléguera le travail plus général vers la méthode à deux paramètres du parent.

using System;
namespace z
{
   class Monstre : Personnage
   {
      public Monstre(string nom, int vie, int force)
         : base(ValiderNom(nom), vie, ValiderForce(force))
      {
      }
      const int LG_MAX_NOM = 5;
      static bool EstNomValide(string candidat) =>
         candidat.Length <= LG_MAX_NOM &&
         Algos.ContientSeulementConsonnes(candidat);
      static string ValiderNom(string candidat) =>
         EstNomValide(candidat) ? candidat : throw new NomInvalideException();
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
      public void Frapper(Personnage autre)
      {
         const int PCT_MIN = 50,
                   PCT_MAX = 75;
         int dégâts = Force * new Random().Next(PCT_MIN, PCT_MAX + 1) / 100;
         Frapper(autre, dégâts);
      }
   }
}

Programme de test

Le code de notre programme de test suit.

using System;
Héros héros = new ("Adam", 18);
Monstre monstre = new ("GRR", 75, 20);
Tour àQui = ChoisirProtagoniste();
while(héros.EstVivant && monstre.EstVivant)
{
   Console.WriteLine($"Avant le choc, {héros.Nom} a {héros.Vie} vie");
   Console.WriteLine($"Avant le choc, {monstre.Nom} a {monstre.Vie} vie");
   if(àQui == Tour.héros)
   {
      Console.WriteLine($"{héros.Nom} frappe {monstre.Nom}");
      héros.Frapper(monstre);
      àQui = Tour.monstre;
   }
   else
   {
      Console.WriteLine($"{monstre.Nom} frappe {héros.Nom}");
      monstre.Frapper(héros);
      àQui = Tour.héros;
   }
   Console.WriteLine($"Après le choc, {héros.Nom} a {héros.Vie} vie");
   Console.WriteLine($"Après le choc, {monstre.Nom} a {monstre.Vie} vie");
   Console.WriteLine(new string('-', 60));
}
if (héros.EstMort)
   Console.WriteLine($"{héros.Nom} est mort... snif!");
if (monstre.EstMort)
   Console.WriteLine($"{monstre.Nom} est mort... ouf!");

//////////////////////////

static Tour ChoisirProtagoniste() =>
   new Random().Next() % 2 == 0? Tour.héros : Tour.monstre;
enum Tour { héros, monstre }

Code vu en classe à la séance S06 pour l'exercice 4 de la séance S04

Le code vu en classe suit.

Classe Point

using System;
namespace S04ex04
{
   class Point
   {
      int x;
      int y;
      static bool EstXValide(int x) => x >= 0;
      static bool EstYValide(int y) => y >= 0;
      public int X
      {
         get => x;
         set
         {
            x = EstXValide(value) ? value : throw new ArgumentException(); 
         }
      }
      public int Y
      {
         get => y;
         set
         {
            y = EstYValide(value) ? value : throw new ArgumentException();
         }
      }

      public Point(int x, int y)
      {
         X = x;
         Y = y;
      }
      public Point() : this(0,0)
      {
      }
   }
}

Classe Carré

using System;
namespace S04ex04
{
   class Carré
   {
      public Point Position
      {
         get; private init;
      }
      public int Côté
      {
         get; private init;
      }
      public ConsoleColor Couleur
      {
         get; private init;
      }
      public Carré(Point pos, int côté, ConsoleColor couleur)
      {
         Position = pos;
         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 col = 0; col != Côté; ++col)
            {
               Console.SetCursorPosition(Position.X + col, Position.Y + ligne);
               Console.Write('#');
            }
         }
         Console.ForegroundColor = avant;
      }
   }
}

Code de test

using System;

Carré[] carrés = new Carré[]
{
   new Carré(new Point(2, 5), 5, ConsoleColor.Blue),
   new Carré(new Point(15, 8), 3, ConsoleColor.Green),
   new Carré(new Point(7, 12), 6, ConsoleColor.Yellow)
};
Random r = new ();
for(int i = 0; i != 10; ++i)
{
   Console.Clear();
   foreach (Carré c in carrés)
      c.Dessiner();
   System.Threading.Thread.Sleep(1000);
   foreach (Carré c in carrés)
   {
      try
      {
         c.Position.X += r.Next(0, 5) - 2;
         c.Position.Y += r.Next(0, 5) - 2;
      }
      catch(ArgumentException)
      {
      }
   }
}

Code vu en classe lors de la séance S15

Nous avons fait trois petites structures de données dans ce cours, soit :

Classe Tableau

Le code de la classe suit :

using System;
using System.Collections.Generic;
using System.Diagnostics.SymbolStore;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace z
{
   class Tableau
   {
      int[] Éléments { get; set; }
      public int Count { get; private set; } = 0;
      public int Capacity => Éléments.Length;
      public bool EstVide => Count == 0;
      public bool EstPlein => Count == Capacity;
      public Tableau()
      {
         Éléments = new int[0]; // ou Éléments = null
      }
      public void Add(int valeur)
      {
         if (EstPlein)
            Croître();
         Éléments[Count] = valeur;
         ++Count;
      }
      private void Croître()
      {
         int nouvelleCap = Capacity == 0? 16 : Capacity * 2;
         int[] nouvTab = new int[nouvelleCap];
         for (int i = 0; i != Count; ++i)
            nouvTab[i] = Éléments[i];
         Éléments = nouvTab;
      }
      public int Find(int valeur)
      {
         for (int i = 0; i != Count; ++i)
            if (valeur.Equals(Éléments[i]))
               return i;
         return -1;
      }
      public void RemoveAt(int indice)
      {
         if(indice >= Count)
            throw new IndexOutOfRangeException();
         for (; indice < Count - 1; ++indice)
            Éléments[indice] = Éléments[indice + 1];
         --Count;
      }
      public void Remove(int valeur)
      {
         int indice = Find(valeur);
         if(indice != -1)
            RemoveAt(indice);
      }
      public int At(int indice)
      {
         return Éléments[indice];
      }
   }
}

Classe Pile (version reposant sur un Tableau)

Le code de la classe suit (je ne répète pas la classe Tableau par souci d'économie) :

class PileVideException : Exception { }
class Pile
{
   Tableau Substrat { get; init; }
   public Pile()
   {
      Substrat = new();
   }
   public bool EstVide { get => Substrat.EstVide; }
   public void Push(int valeur) // en français : Empiler
   {
      Substrat.Add(valeur);
   }
   public int Peek()
   {
      if (EstVide)
         throw new PileVideException();
      return Substrat.At(Substrat.Count - 1);
   }
   public int Pop()
   {
      int valeur = Peek();
      Substrat.RemoveAt(Substrat.Count - 1);
      return valeur;
   }
}

Classe Pile (version reposant sur des noeuds)

Le code de la classe suit :

class PileVideException : Exception { }
class Pile
{
   class Noeud
   {
      public int Valeur { get; init; }
      public Noeud Prédécesseur { get; set; }
      public Noeud(int valeur)
      {
         Valeur = valeur;
         Prédécesseur = null;
      }
   }
   Noeud Tête { get; set; }
   public Pile()
   {
      Tête = null;
   }
   public bool EstVide { get => Tête == null; }
   public void Push(int valeur) // en français : Empiler
   {
      Noeud p = new(valeur);
      p.Prédécesseur = Tête;
      Tête = p;
   }
   public int Peek()
   {
      if (EstVide)
         throw new PileVideException();
      return Tête.Valeur;
   }
   public int Pop()
   {
      int valeur = Peek();
      Tête = Tête.Prédécesseur;
      return valeur;
   }
}

Activité de la séance S13

Un exemple de code implémentant une solution à ce problème suit.

Classe Algos

namespace z
{
   internal static class Algos
   {
      static char[] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      static bool EstDans(char c, char[] vals)
      {
         foreach(char ch in vals)
            if (ch == c)
               return true;
         return false;
      }
      public static bool EstSeulementConsonnes(string s)
      {
         foreach(char c in s)
            if(!EstConsonne(c))
               return false;
         return true;
      }
      public static bool EstVoyelle(char c) =>
         EstDans(char.ToLower(c), voyelles);
      public static bool EstConsonne(char c) =>
         char.IsAsciiLetter(c) && !EstVoyelle(c);
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
   }
}

Classe Arme

namespace ActivitéS13
{
   internal class Arme
   {
      public string Nom { get; init; }
      public int Effet { get; init; }
      public Arme(string nom, int effet)
      {
         Nom = nom;
         Effet = effet;
      }
      public int AppliquerEffet(int dégâts) => dégâts + Effet;
   }
}

Classe Personnage

namespace z
{
   class ForceInvalideException : Exception { }
   class NomInvalideException : Exception { }

   internal class Personnage
   {
      public string Nom { get; private init; }
      public int Vie { get; private set; }
      public int Force { get; private init; }
      public bool EstVivant => !EstMort;
      public bool EstMort => Vie <= 0;
      public Personnage(string nom, int vie, int force)
      {
         Nom = nom;
         Vie = vie;
         Force = force;
      }
      protected static void Frapper(Personnage autre, int dégâts)
      {
         autre.Vie -= dégâts;
      }
      public virtual void Frapper(Personnage autre) // ICI
      {
         Frapper(autre, 1); // bof (on fera mieux bientôt)
      }
   }
}

Classe Héros

using static z.Algos;

namespace z
{
   internal class Héros : Personnage
   {
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      static bool EstForceValide(int candidate) =>
         EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ?
            candidate : throw new ForceInvalideException();
      // bornes pour la valeur initiale seulement
      const int VIE_MIN = 50,
                VIE_MAX = 100;
      public Héros(string nom, int force)
         : this(nom, force, null)
      {
      }
      public Héros(string nom, int force, Arme arme)
         : base(nom, new Random().Next(VIE_MIN, VIE_MAX + 1),
          ValiderForce(force))
      {
         ZeArme = arme;
      }
      public override void Frapper(Personnage p) // ICI
      {
         const int PCT_DÉGÂTS_MIN = 50,
                   PCT_DÉGÂTS_MAX = 100;
         int dégâts =
            (int) (Force *
                   (new Random().Next(PCT_DÉGÂTS_MIN, PCT_DÉGÂTS_MAX + 1) /
                   100.0));
         Frapper
         (
            p, ZeArme == null? dégâts : ZeArme.AppliquerEffet(dégâts)
         );
      }
      public Arme ZeArme { get; init; }
   }
}

Classe Monstre

using static z.Algos;

namespace z
{
   internal class Monstre : Personnage
   {
      const int LG_NOM_MAX = 5;
      static bool EstNomValide(string candidat) =>
         candidat.Length <= LG_NOM_MAX &&
         Algos.EstSeulementConsonnes(candidat);
      static string ValiderNom(string candidat) =>
         EstNomValide(candidat) ?
            candidat : throw new NomInvalideException();
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstForceValide(int candidate) =>
         EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ?
            candidate : throw new ForceInvalideException();
      public Monstre(string nom, int force, int vie)
         : base(ValiderNom(nom), vie, ValiderForce(force))
      {
      }
      public override void Frapper(Personnage p) // ICI
      {
         const int PCT_DÉGÂTS_MIN = 50,
                   PCT_DÉGÂTS_MAX = 75;
         int dégâts =
            (int)(Force *
                   (new Random().Next(PCT_DÉGÂTS_MIN, PCT_DÉGÂTS_MAX + 1) /
                   100.0));
         Frapper(p, dégâts);
      }
   }
}

Classe GroupePersos

namespace ActivitéS13
{
   internal class GroupePersos
   {
      Random Dé { get; init; } = new();
      Personnage [] Persos { get; init; }
      protected GroupePersos(Personnage[] persos)
      {
         Persos = persos; // pas bon, mais on s'en reparle
      }
      protected int NbÉléments => Persos.Length;
      protected int CompterMorts()
      {
         int n = 0;
         foreach (Personnage p in Persos)
            if (p.EstMort)
               ++n;
         return n;
      }
      public void Attaquer(GroupePersos autres)
      {
         Personnage deQui = Persos[Dé.Next(NbÉléments)];
         while(deQui.EstMort) // peut être long...
            deQui = Persos[Dé.Next(NbÉléments)];
         Personnage versQui = autres.Persos[Dé.Next(autres.NbÉléments)];
         while(versQui.EstMort) // peut être long...
            versQui = autres.Persos[Dé.Next(autres.NbÉléments)];
         Console.WriteLine($"{deQui.Nom} frappe {versQui.Nom}");
         Console.WriteLine($"\tAvant le coup, {versQui.Nom} a {versQui.Vie} vie");
         deQui.Frapper(versQui);
         Console.WriteLine($"\tAprès le coup, {versQui.Nom} a {versQui.Vie} vie");

      }
      public bool EstDéfait =>
            CompterMorts() == NbÉléments;
      public void PrésenterVivants()
      {
         foreach (Personnage p in Persos)
            if (p.EstVivant)
               Console.Write($"{p.Nom} avec {p.Vie} vies; ");
      }
   }
}

Classe Armée

namespace ActivitéS13
{
   class ArméeMalforméeException : Exception { }
   internal class Armée : GroupePersos
   {
      static bool EstArméeValide(Héros[] h) =>
         h.Length != 0 && h.Length % 2 == 0;
      static Héros[] ValiderArmée(Héros[] h) =>
         EstArméeValide(h)? h : throw new ArméeMalforméeException();
      public Armée(Héros[] héros) : base(ValiderArmée(héros))
      {
      }
   }
}

Classe Horde

namespace ActivitéS13
{
   internal class Horde : GroupePersos
   {
      class HordeMalforméeException : Exception { }
      static bool EstHordeValide(Monstre[] m) => m.Length != 0;
      static Monstre[] ValiderHorde(Monstre[] m) =>
         EstHordeValide(m) ? m : throw new HordeMalforméeException();
      public Horde(Monstre[] monstres) : base(ValiderHorde(monstres))
      {
      }
   }
}

Classe Program

using ActivitéS13;
using z;

Armée armée = new(new Héros[]
{
   new Héros("Bill le petit", 15),
   new ("Valentin", 18),
   new ("Galahad", 17, new Arme("Halebarde", 8)),
   new ("Brute immonde", 20, new Arme("Gourdin", 3))
});
Horde horde = new(new Monstre[]
{
   new("GRRR", 24, 50),
   new("HSSSS", 22, 44),
   new("BRK", 20, 40),
   new("FFFT", 19, 15),
   new("prkkk", 21, 38)
});

bool tourHéros = new Random().Next() % 2 == 0;
while (!armée.EstDéfait && !horde.EstDéfait)
{
   if (tourHéros)
   {
      armée.Attaquer(horde);
   }
   else
   {
      horde.Attaquer(armée);
   }
   Console.WriteLine(new string('-', 70));
   tourHéros = !tourHéros;
}
//Console.WriteLine($"Victoire de {(héros.EstVivant? héros.Nom : monstre.Nom)}");
if (horde.EstDéfait)
{
   Console.Write("Victoire de l'armée. Encore vivants : ");
   armée.PrésenterVivants();
}
else
{
   Console.Write("Victoire de la horde. Encore vivants : ");
   horde.PrésenterVivants();
}

 


Valid XHTML 1.0 Transitional

CSS Valide !