Quelques raccourcis :

420-KBB-LG – Programmation orientée objet avancée

Ceci est un petit site de support pour le cours 420-KBB-LG – Programmation orientée objet avancée.

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/. Portez une attention particulière à ../../../Sujets/Divers--cdiese/index.html.

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.

Plan de cours

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 les normes appliquées dans ce cours, en ce qui a trait au pseudocode

Quelques trucs pour demander de l'aide plus efficacement

Détail des séances en classe

Puisque nous serons en quelque sorte laboratoire à la fois pour les séances théoriques et les séances de laboratoire, j'ai fait le choix de construire le cours sous forme de 30 séances (de S00 à S29, ou plutôt à S28 car les jours fériés introduisent du chaos dans ma planification) plutôt que sous forme de 15 séances théoriques et 15 séances de laboratoire. Le dosage prévu de temps en théorie et de temps en laboratoire (soit environ moitié-moitié) devrait être respecté.

Date Séance Détails

25 et 26 août

S00

Au menu :

  • Présentation du cours et du plan de cours
  • Les outils dont nous aurons besoin :
    • Nous utiliserons C# 12.0
    • Nous ferons des projets .NET 9
    • Assurez-vous que votre version de Visual Studio soit à jour!
  • Échange sur le contenu du cours, les modalités, les attentes
    • Il est possible que nous tenions une séance « en ligne » pour « roder la mécanique » au cas où la pandémie mettrait du sable dans l'engrenage (personne ne le souhaite, mais mieux vaut être prudentes et prudents)
  • Réponses aux questions de la classe :
  • Présentation d'une petite activité formative : 420KBB--Consignes-activite-revision.pdf

Si vous souhaitez le code du programme principal à partir duquel vous devrez démarrer, vous pouvez le prendre du fichier PDF ou encore le prendre ci-dessous (parfois, copier / coller d'un PDF, ça donne des résultats suspects) :

// ...
List<Orque> orques = new();
try
{
   for(string s = Console.ReadLine(); "" != s; s = Console.ReadLine())
   {
      orques.Add(new Orque(s));
      Console.WriteLine($"Orque créé : {orques[orques.Count - 1].Nom}");
   }
}
catch(NomInvalideException nie)
{
   Console.WriteLine(nie.Message);
}
if(Trier(ref orques, out int nbPermutations))
   Console.WriteLine("Les orques ont été entrés en ordre alphabétique");
else
   Console.WriteLine($"Trier les orques a nécessité {nbPermutations} permutations");
Console.Write("La tribu d'orques est :");
foreach (Orque orque in orques)
   Console.Write($" {orque.Nom}");

Après avoir pris un peu de temps pour se « chamailler » avec ce petit défi de remise en forme, je vous ai proposé un peu de code pour vous aider à redémarrer vos instincts de programmeuse et de programmeur. L'accent a été mis sur l'écriture de code simple :

  • Écrire des fonctions
  • Viser « une vocation par fonction »
  • Essayer d'écrire des fonctions qui se limitent à une instruction quand cela s'avère possible
  • ... et se récompenser quand on y parvient, en se donnant le droit d'utiliser la notation => qui est concise et élégante

Le code produit en classe suit (note : il y a quelques ajouts et ajustements dans ce code en comparaison avec ce que nous avons fait aujourd'hui, mais nous en parlerons à la séance S01) :

Program.cs
//
// code produit pour vous aider et pour amorcer une réflexion avec vous
//
using MonNamespace;
List<Orque> orques = new();
try
{
   for (string s = Console.ReadLine(); "" != s; s = Console.ReadLine())
   {
      orques.Add(new Orque(s));
      Console.WriteLine($"Orque créé : {orques[orques.Count - 1].Nom}");
   }
}
catch (NomInvalideException nie)
{
   Console.WriteLine(nie.Message);
}
if (Trier(ref orques, out int nbPermutations))
   Console.WriteLine("Les orques ont été entrés en ordre alphabétique");
else
   Console.WriteLine($"Trier les orques a nécessité {nbPermutations} permutations");
Console.Write("La tribu d'orques est :");
foreach (Orque orque in orques)
   Console.Write($" {orque.Nom}");

//
//
//
static void Permuter(ref Orque a, ref Orque b)
{
   Orque temp = a;
   a = b;
   b = temp;
}
static bool Trier(ref List<Orque> orques, out int nbPermutations)
{
   nbPermutations = 0;
   Orque[] orq = orques.ToArray(); 
   for (int i = 0; i < orq.Length - 1; ++i)
      for (int j = i + 1; j < orq.Length; ++j)
         if (!(orq[i].Nom.CompareTo(orq[j].Nom) < 0)) // désordre
         {
            Permuter(ref orq[i], ref orq[j]);
            ++nbPermutations;
         }
   orques = orq.ToList();
   return nbPermutations == 0;
}
Algos.cs
// using...
namespace MonNamespace
{
   static class Algos
   {
      static char [] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      public static bool Contient(char [] tab, char c)
      {
         foreach (char ch in tab)
            if (ch == c)
               return true;
         return false;
      }
      public static bool EstVoyelle(char c) =>
         Contient(voyelles, char.ToLower(c));
      public static int CompterVoyelles(string s)
      {
         int n = 0;
         foreach (char c in s)
            if (EstVoyelle(c))
               ++n;
         return n;
      }
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
   }
}
Orque.cs
using static MonNamespace.Algos;
// ...
class NomInvalideException : Exception;

class Orque
{
   static bool EstNomValide(string nom) =>
      nom != null &&
      EstEntreInclusif(nom.Length, 1, 4) &&
      Algos.CompterVoyelles(nom) <= 1;

   string nom;
   public string Nom
   {
      get => nom;
      private init
      {
         nom = EstNomValide(value) ?
            value : throw new NomInvalideException();
         //if (!EstNomValide(value))
         //   throw new NomInvalideException();
         //nom = value;
      }
   }
   public Orque(string nom)
   {
      Nom = nom;
   }
}

Suggestions de lecture :

28 et 29 août

S01

Au menu :

  • Quelques améliorations générales à notre solution embryonnaire de S00 pour l'activité formative
  • Accélérer l'exécution du code en réduisant le nombre d'initialisations d'états immuables à travers une variable static
  • Quelques mots sur l'idée de classe static, qui permet de pallier en partie à un manque de certains langages comme C# ou Java
    • Léger allègement syntaxique rendu possible par ce mécanisme

L'idée est que les classes static en C# servent entre autres de palliatif pour l'absence de vraies fonctions dans le langage. En effet, tout comme Java, C# ne supporte que les méthodes, qui sont des fonctions membres d'une classe ou d'une instance, mais ne supporte pas les fonctions en tant que telles, hors d'une classe).

Ainsi, le programme suivant (https://dotnetfiddle.net/OWcfrC) :

using System;
Point p0 = new(),
      p1 = new(1,1);
Console.WriteLine("Distance({0},{1}) == {2}", p0, p1, Distance(p0, p1));
// Note : nous sommes (implicitement) dans la classe Program, donc
// Distance est en fait Program.Distance
static double Distance(Point p0, Point p1) =>
   Math.Sqrt(Math.Pow(p0.X - p1.X, 2) + Math.Pow(p0.Y - p1.Y, 2)); // Notez «Math.» trois fois... et pourquoi?
class Point
{
    public double X { get; init; }
    public double Y { get; init; }
    public Point() : this(0, 0) {}
    public Point(double x, double y)
    {
       X = x;
       Y = y;
    }
}

... offre une méthode Distance qui est un peu verbeuse, mais qui peut s'écrire plus simplement (https://dotnetfiddle.net/CFsrjf) :

using System;
using static System.Math;
Point p0 = new(),
      p1 = new(1,1);
Console.WriteLine("Distance({0},{1}) == {2}", p0, p1, Distance(p0, p1));
// Note : nous sommes (implicitement) dans la classe Program, donc
// Distance est en fait Program.Distance
static double Distance(Point p0, Point p1) =>
   Sqrt(Pow(p0.X - p1.X, 2) + Pow(p0.Y - p1.Y, 2)); // On va à l'essentiel!
class Point
{
    public double X { get; init; }
    public double Y { get; init; }
    public Point() : this(0, 0) {}
    public Point(double x, double y)
    {
       X = x;
       Y = y;
    }
}

... qui est un peu plus léger pour les yeux.

  • Retour sur la petite activité formative proposée à S00
  • Discussion de divers aspects techniques et architecturaux associés à cette activité
  • Avenues de raffinement ou d'optimisation, incluant les questions de réflexion proposées dans l'énoncé
  • Quelques explorations qui nous mèneront vers notre premier travail pratique, le TP00

À titre de référence, le code d'aujourd'hui est à peu près le suivant. Ce code est perfectible; nous ferons bien mieux plus tard dans la session. Je vous laisse le soin d'écrire le programme principal :

Soldat.cs
// ...
class NomInvalideException : Exception { }
abstract class Soldat
{
   public string Nom { get; private init; }
   public Soldat(string nom)
   {
      Nom = nom;
   }
   public abstract void Saluer();
}
Orque.cs
// ...
class Orque : Soldat
{
   const int LG_NOM_MIN = 1,
             LG_NOM_MAX = 4;
   static bool EstNomValide(string s) =>
      EstEntreInclusif(s.Length, LG_NOM_MIN, LG_NOM_MAX) &&
      CompterVoyelles(s) <= 1;
   static string ValiderNom(string s) =>
      EstNomValide(s) ? s : throw new NomInvalideException();
      public Orque(string nom)
         : base(ValiderNom(nom))
      {
      }
      public override void Saluer()
      {
         Console.WriteLine($"MOI {Nom.ToUpper()}, MOI PUE");
      }
   }
}

En espérant que cela vous soit utile!

Nous avons aussi discuté sommairement du clonage et de l'idiome NVI.

À titre de référence, le code produit lors de cette séance pour démontrer le clonage était :

Image[] images = new Image[]
{
   new Jpeg(ConsoleColor.Magenta),
   new Png(ConsoleColor.Green),
   new Bmp(ConsoleColor.Blue)
};
// non, pas le droit!
//foreach (Image img in images)
//{
//   img.Dessiner();
//   img = ModifierPeutÊtre(img); // <-- ceci serait illégal
//   img.Dessiner();
//}

for (int i = 0; i != images.Length; ++i)
{
   images[i].Dessiner();
   images[i] = ModifierPeutÊtre(images[i]);
   images[i].Dessiner();
}


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

static Image ModifierPeutÊtre(Image img)
{
   // 0 : créer un backup
   Image backup = img.Cloner();

   // 1 : modifier img
   img.Teinte = ConsoleColor.Red;

   // 2 : demander si on veut conserver les modifs
   Console.WriteLine("Conserver les modifs? ");
   // 2a : si oui, on retourne img
   // 2b : sinon, on retourne le backup
   if (Console.ReadKey(true).Key == ConsoleKey.O)
      return img;
   return backup;
}


// il existe une interface ICloneable, qui expose une méthode Clone
// ... mais ne l'utilisez pas :) Pour des détails, voir :
// https://codeql.github.com/codeql-query-help/csharp/cs-class-implements-icloneable/
abstract class Image
{
   public ConsoleColor Teinte { get; set; }
   protected Image(ConsoleColor teinte)
   {
      Teinte = teinte;
   }
   // Idiome NVI : non-virtual interface
   public void Dessiner()
   {
      ConsoleColor pre = Console.ForegroundColor;
      Console.ForegroundColor = Teinte;
      DessinerImpl(); // varie selon les enfants
      Console.ForegroundColor = pre;
   }
   protected abstract void DessinerImpl();
   public abstract Image Cloner();
}
class Jpeg : Image
{
   public Jpeg(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Jpeg {Teinte}");
   }
   protected Jpeg(Jpeg autre) : base(autre.Teinte)
   {
   }
   // spécialisation covariante
   public override Jpeg Cloner() => new (this);
}
class Bmp : Image
{
   public Bmp(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Bmp {Teinte}");
   }
   protected Bmp(Bmp autre) : base(autre.Teinte)
   {
   }
   public override Bmp Cloner() => new (this);
}
class Png : Image
{
   public Png(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Png {Teinte}");
   }
   protected Png(Png autre) : base(autre.Teinte)
   {
   }
   public override Png Cloner() => new (this);
}

En fin de séance, j'ai fait une petite activité dirigée d'un système à deux assemblages, soit le code client (qui était une application console, donc un exécutable), et le code serveur (qui était une bibliothèque de classes – une DLL).

Distribution du TP00

Le code auquel nous en sommes arrivés pour le client était le suivant (j'ai pris quelques libertés pour vous divertir) :

using Arsenal;
FabriqueArmes fab = new ();
IArme p = fab.CréerArme(Arsenal.Gravité.violent);
p.Frapper();
p = fab.CréerArme(Arsenal.Gravité.délicat);
p.Frapper();

Le code auquel nous en sommes arrivés pour le serveur était le suivant (encore une fois avec quelques libertés) : 

namespace Arsenal
{
   public interface IArme
   {
      void Frapper(); // rappel : les membres d'une interface sont implicitement publics
   }
   class Masse : IArme // note : par défaut, les membres d'un namespace sont internal
   {
      public void Frapper()
      {
         Console.WriteLine("POURRRRH");
      }
   }
   class Chainsaw : IArme
   {
      public void Frapper()
      {
         Console.WriteLine("FVRRRRRRRR!");
      }
   }
   public enum Gravité { violent, délicat }
   public class FabriqueArmes
   {
      public IArme CréerArme(Gravité grav) =>
         grav == Gravité.délicat ? new Masse() : new Chainsaw();
   }
}

Nous avons ensuite survolé les consignes du TP00.

Si votre serveur pour le TP00 fonctionne correctement, le programme de test suivant...

// ... code de test (note : le namespace se nomme Consommateur)
using GénérateurId;
using static Consommateur.Tests;

var fab = new FabriqueGénérateurs();
Test(fab, "Séquentiel", "ID", TypeGénérateur.Séquentiel);
Test(fab, "Recycleur", "ID", TypeGénérateur.Recycleur);
Test(fab, "Aléatoire", "ID", TypeGénérateur.Aléatoire, 3);
Test(fab, "Partagé", "ID", TypeGénérateur.Partagé, 3);
Test(fab, "Recycleur", "ID", TypeGénérateur.Recycleur);
Test(fab, "Partagé", "ID", TypeGénérateur.Partagé);
foreach (var (clé, valeur) in fab.ObtenirStatistiques())
   Console.WriteLine($"{clé} a été instancié {valeur} fois");

// ... placer ce qui suit dans une classe «static» nommée Tests
public static void Test(FabriqueGénérateurs fab, string nom, string préfixe, TypeGénérateur type)
{
   IGénérateurId p = fab.Créer(type, préfixe);
   var lst = new List<Identifiant>();
   Console.Write($"{nom}, pige initiale :\n\t");
   for (int i = 0; i != 10; ++i)
   {
      lst.Add(p.Prendre());
      Console.Write($"{lst[lst.Count - 1]} ");
   }
   Console.WriteLine();
   foreach (var n in lst)
      p.Rendre(n);
   Console.Write($"{nom}, pige post-remise :\n\t");
   for (int i = 0; i != 10; ++i)
      Console.Write($"{p.Prendre()} ");
   Console.WriteLine();
}

public static void Test(FabriqueGénérateurs fab, string nom, string préfixe, TypeGénérateur type, int germe)
{
   IGénérateurId p = fab.Créer(type, préfixe, germe);
   var lst = new List<Identifiant>();
   Console.Write($"{nom}, pige initiale :\n\t");
   for (int i = 0; i != 10; ++i)
   {
      lst.Add(p.Prendre());
      Console.Write($"{lst[lst.Count - 1]} ");
   }
   Console.WriteLine();
   foreach (var n in lst)
      p.Rendre(n);
   Console.Write($"{nom}, pige post-remise :\n\t");
   for (int i = 0; i != 10; ++i)
      Console.Write($"{p.Prendre()} ");
   Console.WriteLine();
}
// ...

... devrait donner un affichage comme le suivant (il peut y avoir certaines différences dans les cas « partagé » et « aléatoire », mais il y a des limites à ces différences – le test utilise un germe choisi – alors consultez votre chic prof si vous avez des doutes) :

Séquentiel, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Séquentiel, pige post-remise :
        ID00010 ID00011 ID00012 ID00013 ID00014 ID00015 ID00016 ID00017 ID00018 ID00019
Recycleur, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Recycleur, pige post-remise :
        ID00009 ID00008 ID00007 ID00006 ID00005 ID00004 ID00003 ID00002 ID00001 ID00000
Aléatoire, pige initiale :
        ID19236 ID45716 ID56688 ID13007 ID36732 ID11833 ID16397 ID62078 ID22853 ID24902
Aléatoire, pige post-remise :
        ID32911 ID53056 ID45554 ID01984 ID05379 ID59249 ID08091 ID56063 ID49708 ID31213
Partagé, pige initiale :
        ID19236 ID45716 ID56688 ID13007 ID36732 ID11833 ID16397 ID62078 ID22853 ID24902
Partagé, pige post-remise :
        ID32911 ID53056 ID45554 ID01984 ID05379 ID59249 ID08091 ID56063 ID49708 ID31213
Recycleur, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Recycleur, pige post-remise :
        ID00009 ID00008 ID00007 ID00006 ID00005 ID00004 ID00003 ID00002 ID00001 ID00000
Partagé, pige initiale :
        ID14127 ID38283 ID32702 ID05286 ID65150 ID29877 ID33647 ID35818 ID24609 ID32588
Partagé, pige post-remise :
        ID19316 ID15965 ID33013 ID04178 ID45250 ID58540 ID33793 ID27893 ID56175 ID43324
Séquentiel a été instancié 1 fois
Recycleur a été instancié 2 fois
Aléatoire a été instancié 1 fois
Partagé a été instancié 1 fois

1 sept.

s/o

Fête du travail (jour férié)

2 sept.

?

La Fête du travail crée du chaos dans mon horaire alors on se limitera à travailler sur le TP00 pour éviter que la synchronisation ne se brise entre mes trois groupes.

4 et 5 sept.

S02

Au menu :

  • Petit truc pour vous aider à écrire DomaineIdentifiants.Formater : https://dotnetfiddle.net/GVpa1L
  • Propriétés : get, set et init
  • Retour sur le clonage
  • Retour sur l'idiome NVI, survolé à S01
  • Retour sur l'exercice de créer une bibliothèque à liens dynamiques, et d'une création d'un petit système client / serveur
    • schéma de conception Interface et son implémentation en C#
    • implémentation(s) de cette interface
    • schéma de conception Fabrique
    • écriture d'un client pour ce service
    • utilité de ce type d'architecture
  • Petit rappel sur les générateurs de nombres pseudoaléatoires et sur leur bon usage
const int N = 10_000_000;
const int NB_FACES = 6;
Random dé = new();
int[] lancers = new int[12]; // 0 .. 11, et la 0 sera à 0
for (int i = 0; i != N; ++i)
{
   int a = dé.Next(1, NB_FACES + 1),
       b = dé.Next(1, NB_FACES + 1);
   // Console.WriteLine($"{a} + {b} == {a+b}");
   lancers[(a + b) - 1]++;
}
Console.WriteLine($"Après {N} lancers de deux dés à {NB_FACES} faces...");
for (int i = 1; i != lancers.Length; ++i)
   Console.WriteLine($"{i + 1} : {lancers[i]}");
string texte = "..."; // utilisez le texte de votre choix

Dictionary<string, int> fréquence = new();
foreach(string s in texte.Split(new char[] { ' ', '\t', '\r', '\n' }))
   if(s.Trim().Length > 0)
      if(fréquence.ContainsKey(s))
         fréquence[s]++;
      else
         fréquence.Add(s, 1);

foreach (var (mot, n) in fréquence)
   Console.WriteLine($"Le mot {mot} apparaît {n} fois");

8 et 9 sept.

S03

Au menu :

  • Nombres pseudoaléatoires et germe
  • Propriétés en lecture seule ou calculées
    • Propriétés synthétiques (propriétés de deuxième ordre)

Pour un résumé des syntaxes examinées pour les propriétés

using System;
Carré c = new(3); // 3x3

class CôtéInvalideException : Exception;
class Carré
{
   public static int LongueurMax => 1 // note : pas « = »
   // on aurait aussi pu écrire :
   // public static int LongueurMax { get => 1; }
   // ... ou encore :
   // public static int LongueurMax { get; } = 1;
   public Carré(int côté, ConsoleColor couleur)
   {
      Côté = côté;
      Couleur = couleur;
   }
   public ConsoleColor Couleur // propriété de premier ordre sans validation
   {
      get; private init;
   }
   int côté;
   public int Côté // propriété de premier ordre avec validation
   {
      get => côté;
      private init
      {
         côté = value > 0 ? value : throw new CôtéInvalideException();
      }
   }
   public int Aire { get => Côté * Côté; } // propriété calculée, version « longue »
   public int Surface => 4 * Côté; // propriété calculée, version « courte »
}

Si votre TP00 semble bien fonctionner, voici quelques tests que vous pouvez envisager.

  • Un test validant que vous donnez bel et bien tous les identifiants avec un générateur séquentiel (vous pouvez faire quelque chose de semblable avec un générateur recycleur) :
// ...
IGénérateurId p = new FabriqueGénérateurs().Créer(TypeGénérateur.Séquentiel);
ushort val = p.Prendre().Valeur;
Console.WriteLine($"Premier identifiant pris : {val}"); // devrait être 0
try
{
   for(;;) // boucle infinie (« for ever »)
      val = p.Prendre().Valeur;
}
catch(BanqueVideException)
{
}
Console.WriteLine($"Dernier identifiant pris : {val}"); // devrait être 65535
  • Un test validant que vous donnez bel et bien tous les identifiants avec un générateur aléatoire (ou avec le générateur partagé), et que l'algorithme choisi est raisonnablement rapide :
// ...
IGénérateurId p = new FabriqueGénérateurs().Créer(TypeGénérateur.Aléatoire);
int n = 0;
ushort val = p.Prendre().Valeur;
++n;
try
{
   for(;; ++n) // boucle infinie (« for ever »)
      val = p.Prendre().Valeur;
}
catch(BanqueVideException)
{
}
Console.WriteLine($"Nombre d'identifiants pris : {n}"); // devrait être 65536

11 et 12 sept.

S04

Au menu :

  • Q00
  • Les entiers (int, uint, short, ushort, etc.) et ce qui se passe aux frontières de ces types
  • Démystifier les qualifications d'accès public, protected, private, internal, et pourquoi static n'a rien à voir avec ces mots 🙂
  • Pourquoi diverses sortes d'exceptions?
  • Blocs finally
  • Blocs using
  • Travail sur le TP00

15 et 16 sept.

S05

Au menu :

  • Remise « papier » du TP00
  • On fait Q00

Ensuite :

Pour une implémentation des schémas de conception Singleton et Observateur, les deux à travers un gestionnaire de clavier, voir ceci (qui ressemble à ce que nous avons fait en classe) :

var ges = GesClavier.GetInstance();
var croc = new CroqueMort();
ges.Abonner(new Terminateur(croc));
ges.Abonner(new ÀDroite(ConsoleKey.D));
ges.Abonner(new EnHaut(ConsoleKey.W));
ges.Abonner(new ÀGauche(ConsoleKey.A));
ges.Abonner(new EnBas(ConsoleKey.S));
// ges.Abonner(new Afficheur());
while (!croc.Terminé)
   ges.Exécuter();

//
// Deux schémas de conception (Design Patterns)
// ce matin : Singleton, Observateur
//
interface IRéactionClavier
{
   void Réagir(ConsoleKeyInfo clé);
}
class Afficheur : IRéactionClavier
{
   public void Réagir(ConsoleKeyInfo clé)
   {
      Console.Write(clé.KeyChar);
   }
}
interface ISignalFin
{
   void Signaler();
   bool Terminé();
}
class CroqueMort : ISignalFin
{
   bool Fin { get; set; } = false;
   public void Signaler() => Fin = true;
   public bool Terminé => Fin;
}
class Terminateur : IRéactionClavier
{
   ISignalFin Signaleur { get; init; }
   public Terminateur(ISignalFin p)
   {
      Signaleur = p;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == ConsoleKey.Q /*ConsoleKey.Escape*/)
         Signaleur.Signaler();
   }
}
class GesClavier
{
   List<IRéactionClavier> Abonnés { get; } = new();
   public void Abonner(IRéactionClavier p)
   {
      Abonnés.Add(p);
   }
   GesClavier() // note : privé
   {
   }
   public static GesClavier Get { get; } = new();
   public void Exécuter()
   {
      var clé = Console.ReadKey(true);
      foreach (var p in Abonnés)
         p.Réagir(clé);
   }
}
//
// diantre, beaucoup de répétition de code n'est-ce
// pas? mais on va dormir là-dessus pour le moment :)
//
class ÀDroite : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public ÀDroite(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Est");
   }
}
class EnHaut : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public EnHaut(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Nord");
   }
}
class ÀGauche : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public ÀGauche(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Ouest");
   }
}
class EnBas : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public EnBas(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Sud");
   }
}

Pour un exemple connexe avec délégués :

GesClavier ges = GesClavier.Get;
SignalArrêt signal = new();
ges.Abonner(new Magie().Wow);
ges.Abonner(new Assassin(signal).Réagir);
ges.Abonner(new Afficheur().Réagir);
ges.Abonner(new Est().Réagir);
ges.Abonner(new Nord().Réagir);
ges.Abonner(new Ouest().Réagir);
ges.Abonner(new Sud().Réagir);
while (!signal.Valeur)
{
   ges.Exécuter();
}
class SignalArrêt
{
   public bool Valeur { get; set; } = false;
}
delegate void RéactionTouche(char c);
class Magie
{
   public void Wow(char c)
   {
      // ...
   }
}
class Assassin
{
   SignalArrêt Signal { get; init; }
   public Assassin(SignalArrêt signal)
   {
      Signal = signal;
   }
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'q')
         Signal.Valeur = true; //Environment.Exit(0);
   }
}
class Afficheur
{
   public void Réagir(char c)
   {
      Console.WriteLine(c);
   }
}
class Est
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'd')
         Console.WriteLine("Vers l'Est");
   }
}
class Nord
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'w')
         Console.WriteLine("Vers le Nord");
   }
}
class Ouest
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'a')
         Console.WriteLine("Vers l'Ouest");
   }
}
class Sud
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 's')
         Console.WriteLine("Vers le Sud");
   }
}

class GesClavier
{
   List<RéactionTouche> Abonnés { get; } = new();
   public void Abonner(RéactionTouche /*IRéactionTouche*/ p)
   {
      Abonnés.Add(p);
   }
   public void Exécuter()
   {
      char c = Console.ReadKey(true).KeyChar;
      foreach (RéactionTouche f in Abonnés)
         f(c);
   }
   public static GesClavier Get { get; } = new();
   GesClavier() // note : privé
   {
   }
}

18 et 19 sept.

S06

Au menu :

  • On fait le TP00
  • Introduction à la sérialisation (avec format JSON, qui est le format en vogue en ce moment)
  • On présente le TP01a
  • Travail sur le TP01a

Le programme principal imposé pour le TP01a est le suivant :

using TP01_Wallyd;
using TP01a_Wallyd_Affichage;

Console.ReadKey(true);

ConfigInfo config = Config.LireConfig("../../../config_tp1.json");

char symboleDéchet = config.CatégorieDéchet.Symbole;

// associer les couleurs connues au catalogue de couleurs
CatalogueCouleurs.Get.Associer(config.Robot.Symbole, config.Robot.Couleur);

// préparer la surface d'affichage
Surface surf = FabriqueSurface.Créer(config);

// préparer la zone de messagerie
Messagerie messagerie = new(new(0, surf.Hauteur));

// positionner Wallyd
Robot wallyd = FabriqueSurface.CréerRobot(config.Robot, surf);

// tant qu'il reste des déchets à ramasser
while (surf.TrouverSi(c => c == symboleDéchet).Count > 0)
{
   bool trouvé = false;
   // Trouver et ramasser le déchet
   do
   {
      PipelineAffichage pipeline = new();
      // Ajouter une fonction de transformation au pipeLine
      pipeline.Ajouter(pipeline.Appliquer(surf.Cadre));

      // Appliquer le pipeline de transformation au mutable
      // (la surface avec son halo) l'afficher à la position pos(0,0)
      pipeline.Appliquer
      (
         GénérerHalo(wallyd.Zone, surf.Dupliquer())
      );
      var pts = wallyd.Détecter(surf, Catégorie.Métal);
      if (pts.Count > 0)
      {
         trouvé = true;
         if (pts[0] == wallyd.Pos)
         {
            messagerie.Effacer();
            messagerie.Afficher
            (
               $"Déchet collecté à la position {pts[0]}"
            );
         }
         else
         {
            messagerie.Afficher
            (
               $"Trouvé {pts.Count} déchet(s)",
               $"Déplacement vers {pts[0]}"
            );
            wallyd.DéplacerVers(pts[0], surf);
         }
      }
      else
      {
         wallyd.AugmenterPuissance();
         messagerie.Effacer();
      }
      Thread.Sleep(25);
   }
   while (!trouvé);
   wallyd.RéinitialiserPuissance();
}
//
// dernier affichage une fois les déchets collectés
//
{
   PipelineAffichage pipeline = new();
   pipeline.Ajouter(pipeline.Appliquer(surf.Cadre));
   pipeline.Appliquer
   (
      GénérerHalo(wallyd.Zone, surf.Dupliquer())
   );
}
static Mutable GénérerHalo(Cercle c, Mutable p)
{
   Mutable res = p.Dupliquer();
   for (int ligne = 0; ligne != res.Hauteur; ++ligne)
      for (int col = 0; col != res.Largeur; ++col)
      {
         Point2D pt = new(col, ligne);
         if (c.Centre.Distance(pt) <= c.Rayon)
            res[pt] = new(res[pt].Symbole, res[pt].Avant, ConsoleColor.Green);
      }
   return res;
}

L'exemple de consommation d'un fichier JSON vu en classe était :

using System.Text.Json;
using System.Text.Json.Serialization;

string s = "";
using (StreamReader sr = new("../../../fichier.json"))
{
   for (string str = sr.ReadLine(); str != null; str = sr.ReadLine())
      s += $"{str}\n";
}
Caractères cars = JsonSerializer.Deserialize<Caractères>(s);
Console.WriteLine($"Nom : {cars.NomSurface}");
foreach (Caractère c in cars.Cars)
{
   ConsoleColor avant = Console.ForegroundColor;
   Console.ForegroundColor = c.Couleur;
   Console.WriteLine($"Symbole {c.Symbole}");
   Console.ForegroundColor= avant;
}


//
// classes représentant les types de données lus
//
class Caractère
{
   public char Symbole { get; set; }
   [JsonConverter(typeof(JsonStringEnumConverter))]
   public ConsoleColor Couleur { get; set; }
}
class Caractères
{
   public string NomSurface { get; set; }
   public List Cars { get; set; }
}

... et le fichier JSON lui-même était :

{
  "NomSurface": "Surf",
  "Cars": [
    {
      "Symbole": "$",
      "Couleur": "Red"
    },
    {
      "Symbole": "*",
      "Couleur": "Blue"
    }
  ]
}

22 et 23 sept.

S07

Au menu :

25 et 26 sept.

S08

Au menu :

  • Premiers pas vers une manière plus intelligente de programmer : paramétrer un algorithme de recherche
  • Introduction à la programmation générique
    • Exemple de Afficher<T>, et pourquoi une version non-générique fonctionnerait tout autant dans ce cas
    • Exemple de TriBulles<T>, accompagné de Permuter<T>, avec une classe X qui est IComparable<X> en comparaison avec une classe Y qui ne l'est pas
    • Exemple de Pile d'entiers avec une List<int> à titre de substrat
    • Exemple de Pile<T> avec une List<T> à titre de substrat
    • Exemple de Pile<T> avec des noeuds à titre de substrat
    • Exemple de Chercher<T> T doit être IEquatable<T>
  • Exercices de programmation à l'aide d'algorithmes génériques et d'expressions λ : exercice-apprivoiser-genericite-lambda.html
  • Travail sur le TP01a

Pour une Pile<T> reposant sur une List<T> à titre de substrat :

using Sytem.Collections.Generic;
class PileVideException : Exception;
class Pile<T>
{
   List<T> Substrat{ get; } = new();
   public bool EstVide => Substrat.Count == 0;
   public void Push(T val)
   {
      Substrat.Add(val);
   }
   public T Pop()
   {
      T val = Peek();
      Substrat.RemoveAt(Substrat.Count - 1);
      return val;
   }
   public T Peek()
   {
      if (EstVide)
         throw new PileVideException();
      return Substrat[Substrat.Count - 1];
   }
}

Pour une Pile<T> dont le substrat est fait de noeuds :

using Sytem.Collections.Generic;
class PileVideException : Exception;
class Pile<T>
{
   class Noeud
   {
      public T Valeur{ get; init; }
      public Noeud Prédécesseur{ get; set; } = null;
      public Noeud(T val)
      {
         Valeur = val;
      }
   }
   Noeud Tête { get; set; } = null;
   public bool EstVide => Tête == null;
   public void Push(T val)
   {
      Noeud p = new(val);
      p.Précédesseur = Tête;
      Tête = p;
   }
   public T Pop()
   {
      T val = Examiner();
      Tête = Tête.Précédesseur;
      return val;
   }
   public T Peek()
   {
      if (EstVide)
         throw new PileVideException();
      return Tête.Valeur`;
   }
}

29 et 30 sept.

S09

Au menu :

2 et 3 oct.

S10

Au menu :

  • Q01
  • On se permet quelques exercices supplémentaires :

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 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.1 – É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.2 – É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.3 – É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.4 – É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.

EX02.5 – Écrivez le prédicat EstTrié<T> prenant en paramètre une List<T> et retournant true seulement si les éléments sont en ordre. Exigez que T implémente IComparable<T>.

EX02.6 – Écrivez le prédicat EstTrié<T> prenant en paramètre une List<T> et une fonction de comparaison trilatérale, et retournant true seulement si les éléments sont en ordre sur la base de la fonction de comparaison. Note : une fonction de comparaison trilatérale est une fonction acceptant en paramètre deux T et retournant un int; le int retourné sera inférieur à zéro si le premier T précède le second, égal à zéro si les deux T sont équivalents, et supérieur à zéro sur le premier T suit le second.

EX02.7 – Écrivez la fonction UnionEnsembliste<T> prenant en paramètre deux List<T> et retournant l'union ensembliste de ces deux List<T>, donc une List<T> triée qui contiendra une occurrence de chaque élément se trouvant dans l'une ou l'autre des deux List<T> passées en paramètre. Précondition : chacune des deux List<T> passées en paramètre est triée avant l'appel et les deux sont triées selon les mêmes critères. Par exemple, le programme suivant :

List<int> a = new(){ 2,3,5,7,11 };
List<int> b = new(){ 1,2,3,4,5,6,7 };
Afficher(UnionEnsembliste(a, b));

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

... affichera ce qui suit :

1 2 3 4 5 6 7 11 

EX02.8 – Écrivez la fonction IntersectionEnsembliste<T> prenant en paramètre deux List<T> et retournant l'intersection ensembliste de ces deux List<T>, donc une List<T> triée qui contiendra une occurrence de chaque élément se trouvant à la fois dans les deux List<T> passées en paramètre Précondition : chacune des deux List<T> passées en paramètre est triée avant l'appel et les deux sont triées selon les mêmes critères. Par exemple, le programme suivant :

List<int> a = new(){ 2,3,5,7,11 };
List<int> b = new(){ 1,2,3,4,5,6,7 };
Afficher(IntersectionEnsembliste(a, b));

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

... affichera ce qui suit :

2 3 5 7 

Pour l'implémentation de ListeSimple<T> dans sa déclinaison énumérable :

class ListVideException : Exception { }
class ListeSimple<T> : IEnumerable<T>
{
   class Noeud
   {
      public T Valeur { get; init; }
      public Noeud Succ { get; set; } = null;
      public Noeud(T val)
      {
         Valeur = val;
      }
   }
   Noeud Tête { get; set; } = null;
   Noeud Queue { get; set; } = null;
   public bool EstVide => Tête == null;
   public int Count { get; private set; } = 0;
   public void AjouterDébut(T val)
   {
      Noeud p = new(val);
      if (EstVide)
         Queue = p;
      p.Succ = Tête;
      Tête = p;
      ++Count;
   }
   public void AjouterFin(T val)
   {
      Noeud p = new(val);
      if (EstVide)
         Tête = p;
      else
         Queue.Succ = p;
      Queue = p;
      ++Count;
   }
   public void SupprimerDébut()
   {
      if (EstVide)
         throw new ListVideException();
      Tête = Tête.Succ;
      if (EstVide)
         Queue = null;
      --Count;
   }
   class Énumérateur : IEnumerator<T>
   {
      Noeud Cur { get; set; }
      public Énumérateur(ListeSimple<T> src)
      {
         Cur = new(default);
         Cur.Succ = src.Tête;
      }
      public T Current => Cur.Valeur;
      object IEnumerator.Current => Cur.Valeur;
      public void Dispose() { }
      public bool MoveNext()
      {
         if (Cur.Succ == null)
            return false;
         Cur = Cur.Succ;
         return true;
      }
      public void Reset() { }
   }
   public IEnumerator<T> GetEnumerator() =>
      new Énumérateur(this);
   IEnumerator IEnumerable.GetEnumerator() =>
      new Énumérateur(this);
   // SupprimerFin ... ark ark ark je refuse
   public T First =>
      !EstVide ? Tête.Valeur : throw new ListVideException();
   public T Last =>
      !EstVide ? Queue.Valeur : throw new ListVideException();
}

6 et 7 oct.

S11

Au menu :

  • On résout Q01
  • On résout les exercices proposés à la séance S10
  • On examine un tout petit changement qu'il est possible de faire à certains de nos algorithmes génériques pour les rendre encore plus utiles

EX02.0 – Pour Inverser, une solution possible, accompagnée d'un code de test, serait (https://dotnetfiddle.net/opb3Ap) :

using System;
using System.Collections.Generic;

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

static void Inverser<T>(List<T> lst)
{
   int mid = lst.Count / 2;
   for(int i = 0; i < mid; ++i)
      (lst[i], lst[lst.Count - i - 1]) = (lst[lst.Count - i - 1], lst[i]);
}
static void Afficher<T>(List<T> lst)
{
   foreach(T e in lst) Console.Write($"{e} ");
   Console.WriteLine();
}

Notez que les tests incluent une liste vide, une liste d'un seul élément, de même qu'une liste plus générale contenant un nombre impair d'éléments et une autre contenant un nombre pair d'éléments. On veut se donner les moyens de trouver nos bogues si on en a!

Une autre solution possible serait la suivante (https://dotnetfiddle.net/G52nuD). Notez que je ne répète pas le code de test :

static void Inverser<T>(List<T> lst)
{
   for(int gauche = 0, droite = lst.Count - 1; gauche < droite; ++gauche, --droite)
      (lst[gauche], lst[droite]) = (lst[droite], lst[gauche]);
}

EX02.1 – Pour EstPalindrome<T>(List<T> lst), une solution possible serait (https://dotnetfiddle.net/7vaUIA) :

static bool EstPalindrome<T>(List<T> lst) where T : IEquatable<T>
{
   int mid = lst.Count / 2;
   for(int i = 0; i < mid; ++i)
      if(!lst[i].Equals(lst[lst.Count - i - 1]))
         return false;
   return true;
}

Une autre solution possible serait (https://dotnetfiddle.net/0K0krv) :

static bool EstPalindrome<T>(List<T> lst) where T : IEquatable<T>
{
   for(int gauche = 0, droite = lst.Count - 1; gauche < droite; ++gauche, --droite)
      if(!lst[gauche].Equals(lst[droite]))
         return false;
   return true;
}

EX02.2 – Pour SontTous<T>, une solution possible serait (https://dotnetfiddle.net/TUkEqK) :

static bool SontTous<T>(List<T> lst, Func<T, bool> pred)
{
   foreach(T e in lst)
      if(!pred(e))
         return false;
   return true;
}

EX02.3 – Pour AuMoinsUn<T>, une solution possible serait (https://dotnetfiddle.net/pUa6vT) :

static bool AuMoinsUn<T>(List<T> lst, Func<T, bool> pred)
{
   foreach(T e in lst)
      if(pred(e))
         return true;
   return false;
}

 ... ou encore :

static bool AuMoinsUn<T>(List<T> lst, Func<T, bool> pred) => !SontTous(lst, e => !pred(e));

 EX02.4 – Pour  Aucun<T>, une solution possible serait (https://dotnetfiddle.net/5RqnjF) :

static bool Aucun<T>(List<T> lst, Func<T, bool> pred)
{
   foreach(T e in lst)
      if(pred(e))
         return true;
   return false;
}

... ou encore :

static bool Aucun<T>(List<T> lst, Func<T, bool> pred) => !AuMoinsUn(lst, pred);

EX02.5 – Pour EstTrié<T> sans comparateur, donc se limitant aux services de IComparable<T>, une solution possible serait (https://dotnetfiddle.net/8vcOU3) :

static bool EstTrié<T>(List<T> lst) where T : IComparable<T>
{
   if(lst.Count == 0) return true;
   for(int i = 1; i < lst.Count; ++i)
      if(lst[i - 1].CompareTo(lst[i]) > 0)
         return false;
   return true;
}

EX02.6 – Pour EstTrié<T> avec comparateur, une solution possible serait (https://dotnetfiddle.net/2nY2ZC) :

static bool EstTrié<T>(List<T> lst, Func<T,T,int> comp)
{
   if(lst.Count == 0) return true;
   for(int i = 1; i < lst.Count; ++i)
      if(comp(lst[i - 1], lst[i]) > 0)
         return false;
   return true;
}

EX02.7 – Pour UnionEnsembliste<T>, une solution possible serait ( xx ) :

static List<T> UnionEnsembliste<T>(List<T> a, List<T> b) where T : IComparable<T>
{
   List<T> dest = new();
   int ia = 0, ib = 0;
   while(ia != a.Count && ib != b.Count)
   {
      if(a[ia].CompareTo(b[ib]) < 0)
      {
         if(dest.Count == 0 || dest[dest.Count - 1].CompareTo(a[ia]) != 0)
            dest.Add(a[ia]);
         ++ia;
      }
      else
      {
        if(dest.Count == 0 || dest[dest.Count - 1].CompareTo(b[ib]) != 0)
           dest.Add(b[ib]);
        ++ib;
      }
   }
   // on a épuisé l'une des deux List
   if(ia == a.Count) // reste b
   {
      for(; ib != b.Count; ++ib)
         if(dest.Count == 0 || dest[dest.Count - 1].CompareTo(b[ib]) != 0)
            dest.Add(b[ib]);
   }
   else // reste a
   {
      for(; ia != a.Count; ++ia)
         if(dest.Count == 0 || dest[dest.Count - 1].CompareTo(a[ia]) != 0)
            dest.Add(a[ia]);
   }
   return dest;
}

Ouf, c'est lourd! Une autre solution possible serait (https://dotnetfiddle.net/kIUatj) :

static List<T> UnionEnsembliste<T>(List<T> a, List<T> b) where T : IComparable<T>
{
   static void AjouterProchain(ref int i, List<T> src, List<T> dest) // oui, fonction locale :)
   {
      if(dest.Count == 0 || dest[dest.Count - 1].CompareTo(src[i]) != 0)
         dest.Add(src[i]);
      ++i;
   }
   List<T> dest = new();
   int ia = 0, ib = 0;
   while(ia != a.Count && ib != b.Count)
      if(a[ia].CompareTo(b[ib]) < 0)
         AjouterProchain(ref ia, a, dest);
      else
         AjouterProchain(ref ib, b, dest);
   // on a épuisé l'une des deux List
   var(i, src) = ia == a.Count? (ib, b) : (ia, a);
   for(; i != src.Count; ++i)
      if(dest.Count == 0 || dest[dest.Count - 1].CompareTo(src[i]) != 0)
         dest.Add(src[i]);
   return dest;
}

On peut encore simplifier un peu (https://dotnetfiddle.net/amN7Zy) :

static List<T> UnionEnsembliste<T>(List<T> a, List<T> b) where T : IComparable<T>
{
   static void AjouterProchain(ref int i, List<T> src, List<T> dest) // oui, fonction locale :)
   {
      if(dest.Count == 0 || dest[dest.Count - 1].CompareTo(src[i]) != 0)
         dest.Add(src[i]);
      ++i;
   }
   List<T> dest = new();
   int ia = 0, ib = 0;
   while(ia != a.Count && ib != b.Count)
      if(a[ia].CompareTo(b[ib]) < 0)
         AjouterProchain(ref ia, a, dest);
      else
         AjouterProchain(ref ib, b, dest);
   // on a épuisé l'une des deux List
   var(i, src) = ia == a.Count? (ib, b) : (ia, a);
   while(i != src.Count)
      AjouterProchain(ref i, src, dest);
   return dest;
}

Évidemment, si la vitesse et la consommation de ressources n'est pas un enjeu, on peut faire plus simple (https://dotnetfiddle.net/sI0To6) :

static List<T> UnionEnsembliste<T>(List<T> a, List<T> b) where T : IComparable<T>
{
   static void AjouterProchain(ref int i, List src, List dest) // oui, fonction locale :)
   {
      dest.Add(src[i]);
      ++i;
   }
   List<T> dest = new();
   int ia = 0, ib = 0;
   while(ia != a.Count && ib != b.Count)
      if(a[ia].CompareTo(b[ib]) < 0)
         AjouterProchain(ref ia, a, dest);
      else
         AjouterProchain(ref ib, b, dest);
   // on a épuisé l'une des deux List
   var(i, src) = ia == a.Count? (ib, b) : (ia, a);
   while(i != src.Count)
      AjouterProchain(ref i, src, dest);
   return SupprimerDoublons(dest);
}

EX02.8 – Pour IntersectionEnsembliste<T>, une solution possible serait (https://dotnetfiddle.net/VoucFm) :

static List<T> IntersectionEnsembliste<T>(List<T> a, List<T> b) where T : IComparable<T>
{
   List dest = new();
   int ia = 0, ib = 0;
   while(ia != a.Count && ib != b.Count)
   {
      if(a[ia].CompareTo(b[ib]) < 0)
         ++ia;
      else if(b[ib].CompareTo(a[ia]) < 0)
         ++ib;
      else // équivalents
      {
         dest.Add(a[ia]); // ou b[ib], au choix
         ++ia;
         ++ib;
      }
   }
   return dest;
}

9 et 10 oct.

s/o

Journée pédagogique (9 oct.) et journées de mise à niveau (10 oct.). Cours suspendus

13 oct.

s/o

Action de grâce (jour férié)

14 oct.

s/o

Journée de mise à niveau

16 et 17 oct.

S12

Au menu :

  • On se fait une petite collection simpliste :
    • Nous avons fait une liste simplement chaînée de T
    • Nous l'avons ensuite raffinée car certaines de ses fonctions étaient beaucoup trop coûteuses
    • Nous avons ajusté cette collection pour qu'il soit possible de la traverser avec foreach
    • Je me suis permis de revenir sur le mot clé default pour initialiser un objet avec sa valeur par défaut (0 pour un int, 0.0 pour un double, null pour une string, etc.)
  • On rend cette ListeSimple<T> « parcourable » avec foreach en implémentant IEnumerable<T> pour la collection et IEnumerator<T> pour l'objet capable de la parcourir
  • On ajoute une classe Tableau<T>, elle aussi IEnumerable<T>, pour voir un deuxième cas (différent du premier) d'implémentation de cette interface
  • Survol des algorithmes génériques implémentés aux séances S08 et S10 pour voir lesquels gagneraient en utilité si on élargissait leur signature pour accepter en paramètre des IEnumerable<T>

Pour ListeSimple<T>, nous en sommes arrivés à :

// ...
   class ListeVideException : Exception;
   internal class ListeSimple<T> : IEnumerable<T>
   {
      class Noeud
      {
         public T Valeur { get; init; }
         public Noeud Succ { get; set; } = null;
         public Noeud (T val)
         {
            Valeur = val;
         }
      }
      Noeud Tête { get; set; } = null;
      Noeud Queue { get; set; } = null;
      public int Count { get; private set; } = 0;
      public bool EstVide => Tête == null;
      // Complexité : O(1) (constante)
      public void AjouterDébut(T val)
      {
         Noeud p = new(val);
         if (EstVide)
            Queue = p;
         p.Succ = Tête;
         Tête = p;
         ++Count;
      }
      // Complexité : O(1) constante
      public void AjouterFin(T val)
      {
         if (EstVide)
            AjouterDébut(val);
         else
         {
            Noeud p = Queue;
            p.Succ = new(val);
            Queue = Queue.Succ;
            ++Count;
         }
      }
      // Complexité : O(1) (constante)
      public void SupprimerDébut()
      {
         if (EstVide)
            throw new ListeVideException();
         Tête = Tête.Succ;
         --Count;
         if (EstVide)
            Queue = null;
      }
      public T Premier => !EstVide ?
         Tête.Valeur : throw new ListeVideException();
      public T Dernier => !EstVide ?
         Queue.Valeur : throw new ListeVideException();
      // Complexité : O(n) (linéaire... Ouf!)
      public ListeSimple<T> Dupliquer()
      {
         ListeSimple<T> autre = new();
         for (Noeud p = Tête; p != null; p = p.Succ)
            autre.AjouterFin(p.Valeur);
         return autre;
      }

      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(ListeSimple<T> source)
         {
            Cur = new(default);
            Cur.Succ = source.Tête;
         }
         public bool MoveNext()
         {
            if (Cur.Succ == null)
               return false;
            Cur = Cur.Succ;
            return true;
         }
         public void Reset() { }
         public void Dispose() { }
         public T Current => Cur.Valeur;
         object IEnumerator.Current => Cur.Valeur;
      }
   }
// ...

Pour Tableau<T>, nous en sommes arrivés à :

// ...
   // List<T> « des pauvres »
   internal class Tableau<T> : IEnumerable<T>
   {
      T[] Vals { get; set; }
      public int Count { get; private set; } = 0;
      public int Capacity { get; private set; } = 0;
      public bool EstVide => Count == 0;
      public bool EstPlein => Count == Capacity;
      public Tableau()
      {
         Vals = new T[0];
      }
      public void Add(T val)
      {
         if (EstPlein)
            Croître();
         Vals[Count] = val;
         ++Count;
      }
      // rôle : accroître la capacité du Tableau
      private void Croître()
      {
         int nouvelleCapacité = Capacity != 0 ?
            Capacity * 2 : 8; // arbitraire
         T[] nouveauTab = new T[nouvelleCapacité];
         for(int i = 0; i != Count; ++i)
            nouveauTab[i] = Vals[i];
         Vals = nouveauTab;
         Capacity = nouvelleCapacité;
      }
      public IEnumerator<T> GetEnumerator() =>
         new Énumérateur(this);
      IEnumerator IEnumerable.GetEnumerator() =>
         new Énumérateur(this);
      class Énumérateur : IEnumerator<T>
      {
         public Énumérateur(Tableau<T> source)
         {
            Source = source;
         }
         Tableau<T> Source { get; init; }
         int Indice { get; set; } = -1;
         public bool MoveNext()
         {
            if (Indice == Source.Count - 1)
               return false;
            ++Indice;
            return true;
         }
         public void Reset() { }
         public void Dispose() { }
         public T Current => Source[Indice];
         object IEnumerator.Current => Source[Indice];
      }
      public T this[int indice]
      {
         get => Vals[indice];
         set => Vals[indice] = value;
      }
   }
`// ...

20 et 21 oct.

S13

Au menu :

23 et 24 oct.

S14

Au menu :

L'exemple utilisé en classe pour illustrer le faux-partage ressemblait à :

using System;
using System.Threading;
using System.Diagnostics;

const int N = 25_000;
var tab = CréerTableau(N * N);
for(int i = 1; i <= 16; ++i)
{
 var (r0, dt0) = Tester(() => CompterSiMT(tab, n => n % 2 != 0, i));
 if(i < 10) // bof, mais je ne me souviens plus du code de formatage...
    Console.WriteLine($"Compté {r0} impairs avec  {i} fils en {dt0} ms");
 else
    Console.WriteLine($"Compté {r0} impairs avec {i} fils en {dt0} ms");
}
      
static short[] CréerTableau(int n)
{
   short[] tab = new short[n];
   for (int i = 0; i != tab.Length; ++i)
      tab[i] = (short)(i * 2 + 1);
   return tab;
}
static (T rés, long dt) Tester<T>(Func<T> f)
{
   var sw = new Stopwatch();
   sw.Start();
   T rés = f();
   sw.Stop();
   return (rés, sw.ElapsedMilliseconds);
}

static int CompterSiMT(short[] tab, Func<short, bool> pred, int nthrs)
{
   var thrs = new Thread[nthrs-1]; // initialisés à null en C#
   var nimpairs = new int[nthrs]; // initialisés à 0 en C#
   int tailleBloc = tab.Length / nthrs;

   for(int i = 0; i < thrs.Length; ++i)
   {
      int monIndice = i;
      int début = i * tailleBloc; // inclus
      int fin = (i + 1) * tailleBloc; // exclue
      thrs[i] = new Thread(() =>
      {
         int m = 0;
         for (; début != fin; ++début)
            if (pred(tab[début]))
               ++m;
         nimpairs[monIndice] = m;
         //for (; début != fin; ++début)
         //   if (pred(tab[début]))
         //      ++nimpairs[monIndice];
      });
   }
   foreach (var th in thrs)
      th.Start();
   {
      int début = (nthrs - 1) * tailleBloc; // inclus
      int fin = tab.Length; // exclue
      int m = 0;
      for (; début != fin; ++début)
         if (pred(tab[début]))
            ++m;
      nimpairs[nthrs - 1] = m;
      //for (; début != fin; ++début)
      //   if (pred(tab[début]))
      //      ++nimpairs[nthrs - 1];
   }
   foreach (var th in thrs)
      th.Join();
   int cumul = 0;
   foreach (int n in nimpairs)
      cumul += n;
   return cumul;
}

Petit exemple inspiré de celui donné en classe (voir https://dotnetfiddle.net/hbR0Iv pour une version en-ligne) :

const int N = 1_000_000;
var (r0,dt0) = Test(() =>
{
   int n = 0;
   var th0 = new Thread(() =>
   {
      for (int i = 0; i != N; ++i)
         ++n;
   });
   var th1 = new Thread(() =>
   {
      for (int i = 0; i != N; ++i)
         ++n;
   });
   th0.Start();
   th1.Start();
   th1.Join();
   th0.Join();
   return n;
});
var (r1, dt1) = Test(() =>
{
   int n = 0;
   var mutex = new object();
   var th0 = new Thread(() =>
   {
      for (int i = 0; i != N; ++i)
         lock (mutex)
         {
            ++n;
         }
   });
   var th1 = new Thread(() =>
   {
      for (int i = 0; i != N; ++i)
         lock (mutex)
         {
            ++n;
         }
   });
   th0.Start();
   th1.Start();
   th1.Join();
   th0.Join();
   return n;
});
var (r2, dt2) = Test(() =>
{
   int n = 0;
   var mutex = new object();
   var th0 = new Thread(() =>
   {
      int m = 0;
      for (int i = 0; i != N; ++i)
        ++m;
      lock (mutex)
      {
         n += m;
      }
   });
   var th1 = new Thread(() =>
   {
      int m = 0;
      for (int i = 0; i != N; ++i)
         ++m;
      lock (mutex)
      {
         n += m;
      }
   });
   th0.Start();
   th1.Start();
   th1.Join();
   th0.Join();
   return n;
});
Console.WriteLine($"Sans synchro : {r0} obtenu en {dt0} tics");
Console.WriteLine($"Avec synchro : {r1} obtenu en {dt1} tics");
Console.WriteLine($"Avec synchro : {r2} obtenu en {dt2} tics");

static (T,long) Test<T>(Func<T> f)
{
   var sw = new System.Diagnostics.Stopwatch();
   sw.Start();
   T res = f();
   sw.Stop();
   return (res, sw.ElapsedTicks);
}

Petit exemple de code qui devrait être rapide mais ne l'est pas... même s'il donne la bonne réponse! (voir https://dotnetfiddle.net/UL4JLB pour une version en ligne mais qui est moins gourmande en mémoire car il y a des limites à ce site) :

const int N = 25_000;
var tab = CréerTableau(N * N);

var (r0, dt0) = Tester(() => CompterMT(1, tab));
Console.WriteLine($"1 fil  : compté {r0} impairs en {dt0} ms");
var (r1, dt1) = Tester(() => CompterMT(2, tab));
Console.WriteLine($"2 fils : compté {r1} impairs en {dt1} ms");
var (r2, dt2) = Tester(() => CompterMT(4, tab));
Console.WriteLine($"4 fils : compté {r2} impairs en {dt2} ms");
var (r3, dt3) = Tester(() => CompterMT(8, tab));
Console.WriteLine($"8 fils : compté {r3} impairs en {dt3} ms");

static short[] CréerTableau(int n)
{
   short[] tab = new short[n];
   for (int i = 0; i != tab.Length; ++i)
      tab[i] = (short)(i * 2 + 1);
   return tab;
}
static int CompterSi<T>(T[] tab, Func<T, bool> pred, int début, int fin) // début inclus, fin exclue
{
   int n = 0;
   for (int i = début; i != fin; ++i)
      if (pred(tab[i]))
         ++n;
   return n;
}
static (T rés, long dt) Tester<T>(Func<T> f)
{
   var sw = new Stopwatch();
   sw.Start();
   T rés = f();
   sw.Stop();
   return (rés, sw.ElapsedMilliseconds);
}
static int CompterMT(int nbThreads, short [] tab)
{
   int[] nbImpairs = new int[nbThreads]; // initialisé à 0 en C#
   int tailleBloc = tab.Length / nbThreads;
   Thread[] thrs = new Thread[nbThreads - 1];
   for(int i = 0; i != thrs.Length; ++i)
   {
      int index = i;
      int début = index * tailleBloc;
      int fin = début + tailleBloc;
      thrs[index] = new Thread(() =>
      {
         for (int j = début; j != fin; ++j)
            if (tab[j] % 2 != 0)
               ++nbImpairs[index];
      });
   }
   foreach (var th in thrs) th.Start();
   ////
   {
      int début = (nbThreads - 1) * tailleBloc;
      int fin = tab.Length;
      for (int j = début; j != fin; ++j)
         if (tab[j] % 2 != 0)
            ++nbImpairs[nbThreads - 1];
   }
   ////
   foreach (var th in thrs) th.Join();
   int somme = 0;
   foreach (int n in nbImpairs)
      somme += n;
   return somme;
}

Petit exemple de code qui devrait être rapide et l'est... avec un tout petit changement! (voir https://dotnetfiddle.net/Jxerel pour une version en ligne, mais qui est moins gourmande en mémoire) :

const int N = 25_000;
var tab = CréerTableau(N * N);

for(int i = 1; i <= 16; ++i)
{
   var (r, dt) = Tester(() => CompterMT(i, tab));
   Console.WriteLine($"{i} fil(s)  : compté {r} impairs en {dt} ms");
}
    
static short[] CréerTableau(int n)
{
   short[] tab = new short[n];
   for (int i = 0; i != tab.Length; ++i)
      tab[i] = (short)(i * 2 + 1);
   return tab;
}
static int CompterSi<T>(T[] tab, Func<T, bool> pred, int début, int fin) // début inclus, fin exclue
{
   int n = 0;
   for (int i = début; i != fin; ++i)
      if (pred(tab[i]))
         ++n;
   return n;
}
static (T rés, long dt) Tester<T>(Func<T> f)
{
   var sw = new Stopwatch();
   sw.Start();
   T rés = f();
   sw.Stop();
   return (rés, sw.ElapsedMilliseconds);
}
static int CompterMT(int nbThreads, short [] tab)
{
   int[] nbImpairs = new int[nbThreads]; // initialisé à 0 en C#
   int tailleBloc = tab.Length / nbThreads;
   Thread[] thrs = new Thread[nbThreads - 1];
   for(int i = 0; i != thrs.Length; ++i)
   {
      int index = i;
      int début = index * tailleBloc;
      int fin = début + tailleBloc;
      thrs[index] = new Thread(() =>
      {
         int nb = 0;
         for (int j = début; j != fin; ++j)
            if (tab[j] % 2 != 0)
               nb++;
         nbImpairs[index] = nb;
      });
   }
   foreach (var th in thrs) th.Start();
   ////
   {
      int début = (nbThreads - 1) * tailleBloc;
      int fin = tab.Length;
      int nb = 0;
      for (int j = début; j != fin; ++j)
         if (tab[j] % 2 != 0)
            ++nb;
      nbImpairs[nbThreads - 1] = nb;
   }
   ////
   foreach (var th in thrs) th.Join();
   int somme = 0;
   foreach (int n in nbImpairs)
      somme += n;
   return somme;
}

27 et 28 oct.

S15

Au menu :

Le code du pipeline que nous avons implémenté est, pour l'essentiel :

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
foreach(string nomFichier in args)
{
   string s = LireFichier(nomFichier);
   s = TransformerMajuscules(s);
   s = Censurer(s);
   ÉcrireFichier(nomFichier, s);
}

const int NB_ÉTAPES = 4;
ZoneTransit<(string nom, string s)>[] zt =
   new ZoneTransit<(string, string)>[NB_ÉTAPES - 1]
{
   new(), new(), new()
};
bool[] fini = new bool[NB_ÉTAPES - 1]; // tous à false initialement
Thread[] fils = new Thread[NB_ÉTAPES]
{
   // lire fichier
   new(() =>
   {
      foreach(string nomFichier in args)
      {
         string s = LireFichier(nomFichier);
         zt[0].Ajouter((nomFichier, s));
      }
      fini[0] = true;
   }),
   // transformer en majuscules
   new(() =>
   {
      while(!fini[0])
      {
         var data = zt[0].Extraire();
         foreach(var (nom, s) in data)
         {
            string str = TransformerMajuscules(s);
            zt[1].Ajouter((nom, str));
         }
      }
      { // traitement résiduel
         var data = zt[0].Extraire();
         foreach(var (nom, s) in data)
         {
            string str = TransformerMajuscules(s);
            zt[1].Ajouter((nom, str));
         }
      }
      fini[1] = true;
   }),
   // censurer
   new(() =>
   {
      while(!fini[1])
      {
         var data = zt[1].Extraire();
         foreach(var(nom, s) in data)
         {
            string str = Censurer(s);
            zt[2].Ajouter((nom, str));
         }
      }
      { // traitement résiduel
         var data = zt[1].Extraire();
         foreach(var(nom, s) in data)
         {
            string str = Censurer(s);
            zt[2].Ajouter((nom, str));
         }
      }
      fini[2] = true;
   }),
   // écrire fichier
   new(() =>
   {
      while(!fini[2])
      {
         var data = zt[2].Extraire();
         foreach(var (nom, s) in data)
         {
            ÉcrireFichier(nom, s);
         }
      }
      { // traitement résiduel
         var data = zt[2].Extraire();
         foreach(var (nom, s) in data)
         {
            ÉcrireFichier(nom, s);
         }
      }
   })
};

foreach (Thread th in fils) th.Start();
foreach (Thread th in fils) th.Join();

static string LireFichier(string nomFichier)
{
   using (StreamReader sr = new(nomFichier))
      return sr.ReadToEnd();
}
static string TransformerMajuscules(string s) =>
   s.ToUpper();
static string Censurer(string s)
{
   (string avant, string après)[] àCensurer = new (string, string)[]
   {
      ("STREAMREADER", "CENSURÉ"),
      ("STRING", "CENSURÉ"),
      ("FLOAT", "CENSURÉ"),
      ("TOUPPER", "CENSURÉ"),
      ("PROTECTED", "CENSURÉ")
   };
   foreach (var (avant, après) in àCensurer)
      s = s.Replace(avant, après);
   return s;
}
static void ÉcrireFichier(string nom, string s)
{
   using (StreamWriter sw = new(nom + ".résultat"))
      sw.Write(s);
}

class ZoneTransit<T>
{
   List<T> data = new();
   object mutex = new();
   public void Ajouter(T elem)
   {
      lock(mutex)
         data.Add(elem);
   }
   public List<T> Extraire()
   {
      lock (mutex)
      {
         List<T> lst = data;
         data = new();
         return lst;
      }
   }
}

30 et 31 oct.

S16

Au menu :

  • Q02
  • Exercice en deux temps. Première étape :
    • Écrivez un programme qui ouvre un fichier .cs et, à l'aide d'un dictionnaire, compte le nombre d'occurrences de chaque mot. Pour fins de simplicité, un mot sera une séquence de caractères séparé par un ou plusieurs blancs, Ainsi, le texte « if(a < b) a= b; » contient les mots "if(a", "<", "b)", "a=" et "b;"
    • Ensuite, affichez à la console les mots et leur nombre d'occurrences en ordre lexicographique (ordre du dictionnaire... On parle d'un dictionnaire comme le Robert, le Larousse ou le Multi ici, pas du type Dictionary<K,V> de C#)
    • Ensuite, affichez à la console les mots et leur nombre d'occurrences en ordre décroissant de nombre d'occurrences
  • Puis, deuxième étape :
    • Transformez ce programme pour qu'il traite un tableau de fichiers
    • Séparez ce tableau en deux et donnez à un fil d'exécution la première moitié des fichiers à traiter, puis à l'autre fil d'exécution la deuxième moitié des fichiers à traiter
    • Quand les deux fils d'exécution ont terminé, fusionnez leurs dictionnaires respectifs avant d'afficher les résultats
  • S'il vous reste du temps, il y a une étape additionnelle à faire (venez me voir, je vous expliquerai!)

Joyeuse Halloween!

Halloween

3 et 4 nov.

S17

Au menu :

  • Retour sur Q02
  • Retour sur l'exercice de la séance S16
  • Les struct
  • La classe Interlocked et ses services
  • Les classes CancellationTokenSource et CancellationToken
  • Présentation du TP02
  • Travail sur le TP02

Le « solutionnaire » de l'exercice proposé à la séance S16 était :

Dictionary<string, int> d0 = null,
                        d1 = null;
Thread[] fils = new Thread[]
{
   new(() => d0 = CompterOccurrencesPar(args, 0, args.Length / 2)),
   new(() => d1 = CompterOccurrencesPar(args, args.Length / 2, args.Length))
};
foreach (Thread fil in fils) fil.Start();
foreach (Thread fil in fils) fil.Join();
var occurrences = Fusionner(d0, d1);

var tab = occurrences.ToArray();
Console.WriteLine("ORDRE LEXICOGRAPHIQUE");
Array.Sort(tab, (p0, p1) => p0.Key.CompareTo(p1.Key));
foreach (var (mot, nb) in tab)
   Console.WriteLine($"{mot} apparaît {nb} fois");
Console.WriteLine(new string('-', 70));
Console.WriteLine("ORDRE DÉCROISSANT DE NB D'OCCURRENCES");
Array.Sort(tab, (p0, p1) => p1.Value.CompareTo(p0.Value));
foreach (var (mot, nb) in tab)
   Console.WriteLine($"{mot} apparaît {nb} fois");

Console.ReadLine();


static Dictionary<string, int>
   CompterOccurrencesPar(string[] fich, int début, int fin)
{
   DictionaryDictionary<string, int> occurrences = new();
   for (; début != fin; ++début)
   {
      string texte = LireFichier(fich[début]);
      var mots = Décomposer(texte);
      Dictionary<string, int> occ = CompterOccurrences(mots);
      occurrences = Fusionner(occurrences, occ);
   }
   return occurrences;
}

static Dictionary<string, int>
    Fusionner(Dictionary<string, int> d0, Dictionary<string, int> d1)
{
   Dictionary<string, int> d = new();
   foreach (var (k, v) in d0)
      d.Add(k, v);
   foreach (var (k, v) in d1)
      if (d.ContainsKey(k))
         d[k] += v;
      else
         d.Add(k, v);
   return d;
}

static Dictionary<string, int> CompterOccurrences(List<string> mots)
{
   Dictionary<string, int> occ = new();
   foreach (string s in mots)
      if (occ.ContainsKey(s))
         occ[s]++;
      else
         occ.Add(s, 1);
   return occ;
}

static string LireFichier(string nom)
{
   using (StreamReader sr = new(nom))
      return sr.ReadToEnd();
}

static List<string> Décomposer(string s)
{
   List<string> mots = new();
   string mot = "";
   bool dansMot = false;
   for (int i = 0; i != s.Length; ++i)
   {
      if (!dansMot)
      {
         if (!char.IsWhiteSpace(s[i]))
         {
            dansMot = true;
            mot += s[i];
         }
      }
      else
      {
         if (char.IsWhiteSpace(s[i]))
         {
            dansMot = false;
            mots.Add(mot);
            mot = "";
         }
         else
            mot += s[i];
      }
   }
   if (mot.Length != 0) // cas résiduel
      mots.Add(mot);
   return mots;
}

L'exemple utilisant Interlocked.Increment était (cas AvecSynchroAllégé) :

const int N = 1_000_000;
var (r0, dt0) = Test(SansSynchro);
var (r1, dt1) = Test(AvecSynchro);
var (r2, dt2) = Test(AvecSynchroMieux);
var (r3, dt3) = Test(AvecSynchroAllégé);
Console.WriteLine($"Sans synchro          : {r0} en {dt0} tics");
Console.WriteLine($"Avec synchro          : {r1} en {dt1} tics");
Console.WriteLine($"Avec synchro (mieux)  : {r2} en {dt2} tics");
Console.WriteLine($"Avec synchro (allégé) : {r3} en {dt3} tics");
static int SansSynchro()
{
   int n = 0;
   Thread th0 = new(() =>
   {
      for (int i = 0; i != N; ++i)
         ++n;
   });
   Thread th1 = new(() =>
   {
      for (int i = 0; i != N; ++i)
         ++n;
   });
   th0.Start(); th1.Start();
   th1.Join(); th0.Join();
   return n;
}
static int AvecSynchro()
{
   int n = 0;
   object mutex = new();
   Thread th0 = new(() =>
   {
      for (int i = 0; i != N; ++i)
         lock (mutex)
            ++n;
   });
   Thread th1 = new(() =>
   {
      for (int i = 0; i != N; ++i)
         lock (mutex)
            ++n;
   });
   th0.Start(); th1.Start();
   th1.Join(); th0.Join();
   return n;
}
static int AvecSynchroAllégé()
{
   int n = 0;
   object mutex = new();
   Thread th0 = new(() =>
   {
      for (int i = 0; i != N; ++i)
         Interlocked.Increment(ref n);
      //lock (mutex)
      //   ++n;
   });
   Thread th1 = new(() =>
   {
      for (int i = 0; i != N; ++i)
         Interlocked.Increment(ref n);
      //lock (mutex)
      //   ++n;
   });
   th0.Start(); th1.Start();
   th1.Join(); th0.Join();
   return n;
}
static int AvecSynchroMieux()
{
   int n = 0;
   object mutex = new();
   Thread th0 = new(() =>
   {
      int m = 0;
      for (int i = 0; i != N; ++i)
         ++m;
      lock (mutex)
         n += m;
   });
   Thread th1 = new(() =>
   {
      int m = 0;
      for (int i = 0; i != N; ++i)
         ++m;
      lock (mutex)
         n += m;
   });
   th0.Start(); th1.Start();
   th1.Join(); th0.Join();
   return n;
}
static (T, long) Test<T>(Func<T> f)
{
   System.Diagnostics.Stopwatch sw = new();
   sw.Start();
   T res = f();
   sw.Stop();
   return (res, sw.ElapsedTicks);
}

L'exemple de CancellationToken était :

CancellationTokenSource source = new();
CancellationToken jeton = source.Token;
Thread lireTouche = new(() =>
{
   Console.ReadKey(true);
   source.Cancel(); // demande l'arrêt des travaux
});
lireTouche.Start();
while(!jeton.IsCancellationRequested) // constater demande d'arrêt
{
   Console.Write('.');
   Thread.Sleep(1000);
}
lireTouche.Join();

6 et 7 nov.

S18

Au menu :

  • J'ai dû suspendre les cours pour des raisons familiales. J'espère que vous avez profité du moment pour faire progresser TP02!

10 et 11 nov.

S19

Au menu :

  • Travail sur le TP02
  • Si vous trouvez la marche un peu haute entre TP01b et TP02, et si vous souhaitez une activité pratique préparatoire, en voici une petite. Soit le code client suivant :
using System;
using System.Threading;

var bb = new Ardoise();
bb.Abonner(new Caméra());
var noms = new[] { "Bill", "Bob", "Frieda", "Wanda" };
var pub = new int[noms.Length];
for(int i = 0; i != noms.Length; ++i)
   pub[i] = bb.AjouterPublieur(noms[i]);
int fini = 0;
var th = new Thread[pub.Length];
for(int i = 0; i != th.Length; ++i)
{
   int indice = i;
   th[i] = new (() =>
   {
      var dé = new Random();
      for (int n = 0; fini == 0; )
      {
         if (dé.Next(0, pub.Length) == indice)
            bb.Publier(pub[indice], $" : \" message {n++} de {noms[indice]}\"");
         Thread.Sleep(dé.Next(400, 500));
      }
   });
}
foreach (var thr in th) thr.Start();
Console.ReadKey(true);
Interlocked.Exchange(ref fini, 1);
foreach (var thr in th) thr.Join();

class Caméra : IObservateurArdoise
{
   public void Nouveauté(string qui, string quoi)
   {
      Console.WriteLine($"{qui} a dit {quoi}");
   }
}
  • Votre objectif est d'offrir une classe Ardoise qui exposera au minimum les services suivants :
    • Abonner(IObservateurArdoise) pour accepter un nouvel observateur
    • AjouterPublieur(string nom) qui ajoute un publieur nommé nom et retourne le id (un int) qui lui sera associé (levez une exception s'il existe déjà un publieur de ce nom). C'est à l'Ardoise de trouver une stratégie pour donner un id différent à chaque publieur
    • Publier(int id, string message) qui publiera un message sur l'Ardoise
    • RetirerPublieur(int id) qui retire un publieur ayant l'identifiant id de la liste des publieurs autorisés de l'Ardoise (levez une exception s'il n'existe pas de publieur avec cet identifiant)
    • Quand une publication sera faite sur l'Ardoise, elle doit informer ses abonnés (de type IObservateurArdoise) en appelant leur méthode Nouveauté(string qui, string quoi)

Pour voir si ça tient la route, assurez-vous de tester votre code. Des exemples possibles de tests (rien d'exhaustif) :

static void StressTestA(int n)
{
   var bb = new Ardoise();
   var noms = new[] { "Bill", "Bob" };
   var pub = new int[noms.Length];
   var th = new Thread[pub.Length];
   for(int i = 0; i != th.Length; ++i)
   {
      int indice = i;
      th[i] = new (() =>
      {
         for(int j = 0; j != n; ++j)
         {
            int id = bb.AjouterPublieur(noms[indice]);
            bb.Publier(id, $"Coucou #{j}");
            bb.RetirerPublieur(id);
         }
      });
   }
   foreach (var thr in th) thr.Start();
   foreach (var thr in th) thr.Join();
}

static void StressTestB(int n)
{
   Compteur compteur = new();
   var bb = new Ardoise();
   bb.Abonner(compteur);
   var noms = new[] { "Bill", "Bob" };
   var pub = new int[noms.Length];
   var th = new Thread[pub.Length];
   for (int i = 0; i != th.Length; ++i)
   {
      int indice = i;
      th[i] = new (() =>
      {
         for (int j = 0; j != n; ++j)
         {
            int id = bb.AjouterPublieur(noms[indice]);
            bb.Publier(id, $"Coucou #{j}");
            bb.RetirerPublieur(id);
         }
      });
   }
   foreach (var thr in th) thr.Start();
   foreach (var thr in th) thr.Join();
   compteur.Rapport();
}

class Compteur : IObservateurArdoise
{
   Dictionary<string, int> Compte { get; } = new();
   public void Nouveauté(string qui, string quoi)
   {
      lock(this)
      {
         if (Compte.ContainsKey(qui))
            Compte[qui]++;
         else
            Compte.Add(qui, 1);
         }
      }
      public void Rapport()
      {
         foreach (var (k, v) in Compte)
            Console.WriteLine($"{k} a publié {v} fois");
      }
   }

Si vous y parvenez, voici un petit défi supplémentaire.

  • Soit le code client suivant (note : il y a peu de changements en comparaison avec le précédent), produisez un affichage coloré par colonnes (demandez à votre chic prof pour une démonstration) :
using System;
using System.Threading;
using System.Collections.Generic;

var bb = new Ardoise();
Messagerie msg = new(10);
bb.Abonner(msg);
var noms = new[] { "Bill", "Bob", "Frieda" };
var pub = new int[noms.Length];
for(int i = 0; i != noms.Length; ++i)
   pub[i] = bb.AjouterPublieur(noms[i]);
int fini = 0; // <--
var th = new Thread[pub.Length];
for(int i = 0; i != th.Length; ++i)
{
   int indice = i;
   th[i] = new Thread(() =>
   {
      var dé = new Random();
      for (int n = 0; fini == 0;) // <--
      {
         if (dé.Next() % pub.Length == indice)
            bb.Publier(pub[indice], $"message {n++} de {noms[indice]}");
         Thread.Sleep(dé.Next(400, 500));
      }
   });
}
foreach (var thr in th) thr.Start();
Thread affichage = new(() =>
{
   while(fini == 0)
   {
      msg.Afficher();
      Thread.Sleep(500);
   }
});
affichage.Start();
Console.ReadKey(true);
Interlocked.Exchange(ref fini, 1); // <--
foreach (var thr in th) thr.Join();
affichage.Join();

13 et 14 nov.

S20

Au menu :

  • Q03
  • Q04
  • Travail sur TP02

17 et 18 nov.

S21

Au menu :

  • On revient sur Q03
  • On revient sur Q04, en particulier sur le bonus
  • Travail sur TP02
  • Pour celles et ceux qui ont terminé TP02, quelques exercices.

EX00 – Écrivez la classe Monitor<T> telle que :

  • Toute instance de Monitor<T> encapsule un T connu à la construction
  • Toute instance de Monitor<T> expose une méthode Utiliser acceptant en paramètre une fonction f applicable à un T et retournant un U. Cette méthode appliquera f à l'objet de type T que le Monitor<T> encapsule mais de manière synchronisée, et retournera la valeur retournée par cet appel à f
  • Toute instance de Monitor<T> expose une méthode Utiliser acceptant en paramètre une fonction f applicable à un T et retournant void (le type Action<T> en C# représente une telle fonction). Cette méthode appliquera f à l'objet de type T que le Monitor<T> encapsule mais de manière synchronisée
  • Assurez-vous que même si le code client utilise les deux déclinaisons de Utiliser concurremment sur un même Monitor<T>, les accès au T encapsulé par ce Monitor<T> demeurent synchronisés

Un exemple de code de test (essentiellement, Q03 – code : https://dotnetfiddle.net/2Ob3kC – mais avec un Monitor<List<int>> au lieu d'une simple List<int>) serait le suivant. Notez que le code de Q03 plantait de manière pseudoaléatoire alors que le code de EX00 ne devrait pas planter :

using System;
using System.Collections.Generic;
const int NESSAIS = 1000, NELEMS = 100;
Monitor<List<int>> canal = new(new());
List<int> données = CréerDonnées(NELEMS);
bool fini = false;
var remplisseur = new Thread(() =>
{
   for (int i = 0; i != NESSAIS; ++i)
      canal.Utiliser(lst => lst.AddRange(données));
   fini = true;
});
var videur = new Thread(() =>
{
   while (!fini)
   {
      var lst = canal.Utiliser(lst =>
      {
         var temp = new List<int>();
         temp.AddRange(lst);
         lst.Clear();
         return temp;
      });
      foreach (var i in lst)
         Console.Write($"{i} ");
   }
   {
      var lst = canal.Utiliser(lst =>
      {
         var temp = new List<int>();
         temp.AddRange(lst);
         lst.Clear();
         return temp;
      });
      foreach (var i in lst)
         Console.Write($"{i} ");
   }
});
videur.Start(); remplisseur.Start();
remplisseur.Join(); videur.Join();


static List<int> CréerDonnées(int combien)
{
   var lst = new List<int>();
   for (int i = 0; i < combien; ++i)
      lst.Add(i + 1);
   return lst;
}

Possible solution :

class Monitor<T>
{
   T Obj { get; init; }
   public Monitor(T obj)
   {
      Obj = obj;
   }
   public U Utiliser<U>(Func<T, U> f)
   {
      lock (this) return f(Obj);
   }
   public void Utiliser(Action<T> f)
   {
      lock (this) f(Obj);
   }
}

EX01a – Écrivez la classe ColorSwitcher exposant la méthode d'instance Exécuter qui accepte en paramètre une Action et un ConsoleColor, puis de manière synchronisée (avec tous les clients du même ColorSwitcher) (a) copiera la couleur du texte à la console (propriété ForegroundColor de Console), (b) changera cette couleur pour la couleur passée en paramètre, (c) exécutera l'Action passée en paramètre et (d) remettra en place la couleur du texte copiée en (a).

Voir EX01b pour un exemple de code client.

Possible solution :

class ColorSwitcher
{
   public void Execute(Action f, ConsoleColor coul)
   {
      lock(this)
      {
         ConsoleColor avant = Console.ForegroundColor;
         Console.ForegroundColor = coul;
         f();
         Console.ForegroundColor = avant;
      }
   }
}

EX01b – Écrivez la classe RegroupementFils qui :

  • À la construction, créera un nombre de fils d'exécution concurrents (le nombre sera un paramètre passé au constructeur) prenant chacun en charge une méthode d'instance Travailleur et démarrera ces fils d'exécution
  • Tant qu'aucune demande d'arrêt n'aura été reçue, chaque Travailleur prendra une tâche (une Action) d'une file de tâches logée dans le RegroupementFils et l'exécutera
  • Il sera possible d'ajouter (méthode Ajouter) une Action aux tâches devant être prises en charge par le regroupement de fils
  • Il sera possible de demander l'arrêt (méthode Terminer) des travailleurs. Une fois cette demande soumise, attendez que tous les travailleurs aient terminé les tâches qu'ils auront entreprises. Assurez-vous bien entendu que, si certaines tâches n'ont pas encore été traitées par les travailleurs lorsque tous les travailleurs seront arrêtés, ces tâches soient toutes traitées avant la fin de Terminer

Note : EX01b est plus difficile que ses prédécesseurs, et il y a plusieurs enjeux de synchronisation à considérer (pas nécessairement beaucoup de code à écrire cependant; on parle d'une cinquantaine de lignes de code pour la classe entière). Aussi, les consignes ci-dessus sont délibérément moins directives pour vous donner une occasion de réfléchir et d'explorer. Choisissez vos méthodes, atttributs, propriétés, etc. avec soin et profitez de l'occasion!

Un exemple de code client serait le suivant :

const int N = 20;
ConsoleColor[] couleurs = new[]
{
   ConsoleColor.Red,
   ConsoleColor.Green,
   ConsoleColor.Blue,
   ConsoleColor.Yellow,
   ConsoleColor.Magenta,
   ConsoleColor.Cyan
};
RegroupementFils pool = new(10);
ColorSwitcher colSwitch = new();
Random dé = new();
for (int i = 0; i != N; ++i)
{
   int indice = i;
   int délai = dé.Next(200, 300 + 1);
   pool.Ajouter(() =>
   {
      colSwitch.Execute
      (
         () => Console.WriteLine($"DÉBUT du fil {indice}"),
         couleurs[indice % couleurs.Length]
      );
      Thread.Sleep(délai);
      colSwitch.Execute
      (
         () => Console.WriteLine($"FIN  du fil {indice}"),
         couleurs[indice % couleurs.Length]
      );
   });
}
colSwitch.Execute
(
   () => Console.WriteLine("Pressez une touche pour terminer..."),
   ConsoleColor.White
);
Console.ReadKey(true);
pool.Terminer();

Possible solution :

class RegroupementFils
{
   Thread[] Workers { get; init; }
   int fini = 0;
   Queue<Action> Tâches { get; } = new();
   CancellationTokenSource Src { get; } = new();
   bool Fini
   {
      get => Src.Token.IsCancellationRequested;
    }
   bool Prendre(out Action act)
   {
      lock(Tâches)
         if(Tâches.Count > 0)
         {
            act = Tâches.Dequeue();
            return true;
         }
      act = null;
      return false;
   }
   void Travailleur()
   {
      while (!Fini)
      {
         if (Prendre(out Action tâche))
            tâche();
      }
   }
   public void Ajouter(Action act)
   {
      lock(Tâches)
         Tâches.Enqueue(act);
   }
   public RegroupementFils(int n)
   {
      Workers = new Thread[n];
      for (int i = 0; i != n; ++i)
         Workers[i] = new(Travailleur);
      foreach (Thread th in Workers)
         th.Start();
   }
   public void Terminer()
   {
      Src.Cancel(); // Fini = true;
      foreach(Thread th in Workers)
         th.Join();
      // tâches résiduelles
      while (Prendre(out Action tâche))
         tâche();
   }
}

20 et 21 nov.

S22

Au menu :

  • Travail sur TP02
  • Pour celles et ceux qui ont terminé TP02, en plus des exercices à S21, voici un exercice supplémentaire :

EX01c – Écrivez la classe RegroupementFils<T> qui :

  • À la construction, créera un nombre de fils d'exécution concurrents (le nombre sera un paramètre passé au constructeur) prenant chacun en charge une méthode d'instance Travailleur et démarrera ces fils d'exécution
  • Tant qu'aucune demande d'arrêt n'aura été reçue, chaque Travailleur prendra une tâche (une Func<T>) d'une file de tâches logée dans le RegroupementFils et l'exécutera
  • Il sera possible d'ajouter (méthode Ajouter) une Func<T> aux tâches devant être prises en charge par le regroupement de fils. Ceci retournera un RegroupementFils<T>.IFuture permettant au code client de savoir (a) si la tâche a été traitée (propriété Prêt de type bool) et (b) quel en fut le résultat (propriété Valeur de type T)
  • Il sera possible de demander l'arrêt (méthode Terminer) des travailleurs. Une fois cette demande soumise, attendez que tous les travailleurs aient terminé les tâches qu'ils auront entreprises. Assurez-vous bien entendu que, si certaines tâches n'ont pas encore été traitées par les travailleurs lorsque tous les travailleurs seront arrêtés, ces tâches soient toutes traitées avant la fin de Terminer. Cette fonction doit retourner une List des RegroupementFils<T>.IFuture associées aux tâches résiduelles traitées après l'arrêt des travailleurs (pour que le code client obtienne le résultat des calculs!)

Note : EX01c, comme son prédécesseur EX01b, est un peu croustillant. Il y a plusieurs enjeux de synchronisation à considérer (pas nécessairement beaucoup de code à écrire cependant; on parle d'une soixantaine de lignes de code pour la classe entière). Aussi, les consignes ci-dessus sont délibérément moins directives pour vous donner une occasion de réfléchir et d'explorer. Choisissez vos méthodes, atttributs, propriétés, etc. avec soin et profitez de l'occasion!

Un exemple de code client serait le suivant :

const int N = 20;
ConsoleColor[] couleurs = new[]
{
   ConsoleColor.Red,
   ConsoleColor.Green,
   ConsoleColor.Blue,
   ConsoleColor.Yellow,
   ConsoleColor.Magenta,
   ConsoleColor.Cyan
};
RegroupementFils<(int, int)> pool = new(10);
ColorSwitcher colSwitch = new();
Random dé = new();
List<RegroupementFils<(int,int)>.IFuture> résultats = new();
for (int i = 0; i != N; ++i)
{
   int indice = i;
   int délai = dé.Next(200, 300 + 1);
   résultats.Add(pool.Ajouter(() =>
   {
      colSwitch.Execute
      (
         () => Console.WriteLine($"DÉBUT du fil {indice}"),
         couleurs[indice % couleurs.Length]
      );
      Thread.Sleep(délai);
      colSwitch.Execute
      (
         () => Console.WriteLine($"FIN  du fil {indice}"),
         couleurs[indice % couleurs.Length]
      );
      return (indice, délai);
   }));
}
Thread lireTouche = new(() =>
{
   colSwitch.Execute
   (
      () => Console.WriteLine("Pressez une touche pour terminer..."),
      ConsoleColor.White
   );
   Console.ReadKey(true);
});
lireTouche.Start();
while(résultats.Count != 0)
{
   for (int i = 0; i != résultats.Count; ++i)
      if (résultats[i].Prêt)
      {
         colSwitch.Execute(() =>
         {
            var (indice, délai) = résultats[i].Valeur;
            Console.WriteLine($"Tâche {indice} terminée en {délai} ms");
         }, ConsoleColor.DarkRed);
         résultats.RemoveAt(i);
         break;
      }
}
lireTouche.Join();
var résidus = pool.Terminer();
foreach(var fut in résidus)
   colSwitch.Execute(() =>
   {
      var (indice, délai) = fut.Valeur;
      Console.WriteLine($"Tâche {indice} terminée en {délai} ms");
   }, ConsoleColor.DarkRed);

24 et 25 nov.

S23

Au menu :

Ma famille et moi avons vécu une journée terrible le 24 novembre, et j'ai dû m'absenter en conséquence le 25 novembre. Pour les groupes 01 et 03, les séances S23 et S24 seront un peu enchevêtrées. Je suis désolé des conséquences pour vous toutes et tous.

27 et 28 nov.

S24

1 et 2 déc.

S25

Au menu :

  • 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.
  • Les types StringBuilder et string
  • Présentation du TP03
  • Travail sur le TP03

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!)

4 et 5 déc.

S26

Au menu :

  • Q05
  • Q06
  • Travail sur le TP03

8 et 9 déc.

S27

Au menu :

  • Rapportez votre grille dûment remplie de Q06
  • On fait la PFI (soyez prêtes, soyez prêts!)
    • C'est pratique alors vous avez droit à Visual Studio mais pas d'IA générative, pas d'ami(e)s (c'est un travail individuel)

10 et 11 déc.

S28

Au menu :

  • Chic examen final
    • Vous avez droit à toute documentation écrite (pas d'ordinateur, pas d'IA générative, pas d'ami(e)s, pas d'Internet)

Note : techniquement, le dernier jour de classe est le lundi 15 décembre, mais dû à l'ajustement fait pour compenser le désordre causé par la Fête du travail, je ne tiendrai pas de séance en classe ce jour-là. Je serai toutefois disponible pour vous (modalités à définir entre vous et moi)

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.

Consignes des travaux pratiques

Les consignes des travaux pratiques suivent.

Consignes Détails supplémentaires À remettre...

Activité de révision

Voir S00 et S01

s/o

TP00

Code de test proposé à la séance S01

Vendredi le 12 septembre 2025 à 23 h 59

Important : la copie électronique livrée par Colnet doit être votre version finale, mais je ne corrigerai que la version papier que vous me remettrez au plus tard au début du cours suivant

TP01a

Bibliothèque de classes pour l'affichage : TP01a-Wallyd-Affichage.dll

Fichier source JSON de base : config_tp1.json

Pour des diagrammes susceptibles de vous aider à mettre les classes de ce travail pratique en contexte et de voir les relations entre elles (merci à Marc Beaulne pour ceci!) :

Vendredi le 10 octobre 2025 à 23 h 59

Important : la copie électronique livrée par Colnet doit être votre version finale, mais je ne corrigerai que la version papier que vous me remettrez au plus tard au début du cours suivant

TP01b

La DLL d'affichage pour ce travail est TP01b_Affichage.dll

Le fichier JSON à utiliser est config_tp1b.json

Le code du programme principal sera :

// ...
ConfigInfo config = Config.LireConfig("../../../config_tp1b.json");
// préparer la surface d'affichage
Surface surf = FabriqueSurface.Créer(config);

// préparer la zone de messagerie
Messagerie messagerie = new(new(surf.Largeur, 0));  // mb::

// positionner Wallyd
List<Robot> robots = FabriqueSurface.CréerRobots(config.Robot, surf);
Robot wallyd = robots[0];

PipelineAffichage pipeline = new PipelineAffichage(surf);

// tant qu'il reste des déchets à ramasser
while (surf.TrouverSi(c => wallyd.PeutDétecter(c)).Count > 0)
{
   bool trouvé = false;
   // Trouver et ramasser le déchet
   do
   {
      // Appliquer le pipeline de transformation au mutable
      // (la surface avec son halo) l'afficher à la position pos(0,0)
      pipeline.Appliquer
      (
         GénérerHalo(wallyd.Zone,
                     CatalogueCouleurs.Get.ObtenirCouleur(wallyd.Symbole, ConsoleColor.Green),
                     surf.Dupliquer())
      );
      var pts = wallyd.Détecter(surf, Catégorie.Métal);
      if (pts.Count > 0)
      {
         trouvé = true;
         Point2D nouvellePosi = wallyd.CalculerDéplacementVers(pts[0]); // mb::
         if (pts[0] == nouvellePosi)
         {
            messagerie.Effacer();
            messagerie.Afficher
            (
               $"Déchet collecté à la position {pts[0]}"
            );
            surf.Retirer(pts[0]);
         }
         else
         {
            if (surf[nouvellePosi].EstVide())
               messagerie.Afficher
               (
                  $"Trouvé {pts.Count} déchet(s)",
                  $"Déplacement vers {nouvellePosi}"
               );
            else
            {
               nouvellePosi = wallyd.TrouverPassage(surf);
               messagerie.Effacer();
               messagerie.Afficher
               (
                  $"Déplacement impossible vers {pts[0]}",
                  $"Contournement vers {nouvellePosi}"
               );
            }
         }
         wallyd.DéplacerVers(nouvellePosi, surf);
      }
      else
      {
         wallyd.AugmenterPuissance();
      }
      Thread.Sleep(500);
   }
   while (!trouvé);
   wallyd.RéinitialiserPuissance();
}
//
// dernier affichage une fois les déchets collectés
//
{
   pipeline.Appliquer
   (
       GénérerHalo(wallyd.Zone,
                   CatalogueCouleurs.Get.ObtenirCouleur(wallyd.Symbole, ConsoleColor.Green),
                   surf.Dupliquer())
   );
}

La classe PipelineAffichage sera (outre ce qui manque) :

class PipelineAffichage
{
   List<Func<Mutable, Mutable>> Transfos { get; } = new();
   // NOTE : IL MANQUE UN CONSTRUCTEUR ICI
   public void Ajouter(Func transfo) =>
      Transfos.Add(transfo);
   public IProjetable Appliquer(Point2D pos, Mutable p)
   {
      foreach (var transfo in Transfos)
         p = transfo(p);
      return Afficher(pos, p);
   }
   public IProjetable Appliquer(Mutable p) =>
      Appliquer(new(), p);
   public IProjetable Afficher(Point2D pos, Mutable p)
   {
      for (int ligne = 0; ligne != p.Hauteur; ++ligne)
         for (int col = 0; col != p.Largeur; ++col)
         {
            Console.SetCursorPosition(col + pos.X, ligne + pos.Y);
            var préF = Console.ForegroundColor;
            var préB = Console.BackgroundColor;
            Console.ForegroundColor = p[ligne, col].Avant;
            Console.BackgroundColor = p[ligne, col].Arrière;
            Console.Write
            (
               p[ligne, col].Symbole == default ?
                  ' ' : p[ligne, col].Symbole
            );
            Console.BackgroundColor = préB;
            Console.ForegroundColor = préF;
         }
      return p;
   }
   // retourne une fonction qui transforme un mutable pour 
   // y ajouter un cadre
   public Func<Mutable, Mutable> Appliquer(Cadre p)
   {
      return m =>
      {
         Mutable res = m.Dupliquer();
         for (int i = 0; i != p.Hauteur; ++i)
            for (int j = 0; j != p.Largeur; ++j)
            {
               var c = p[i, j];
               if (c.Symbole != default)
                  res[i, j] = c;
            }
         return res;
      };
   }
}

TP02

La DLL d'affichage du TP02 est TP02_Affichage.dll

Le fichier JSON de configuration est config_tp2.json

Le programme principal imposé est :

ConfigInfo config = Config.LireConfig("../../../config_tp2.json");

// préparer la surface d'affichage
Surface surf = FabriqueSurface.Créer(config);

// préparer la zone de messagerie
Messagerie messagerie = new(new(surf.Largeur, 0), surf.Hauteur);
// préparer la zone d'informations
Messagerie information = new(new(0, surf.Hauteur), 2);

List<Robot> robots = FabriqueSurface.CréerRobots(config.Robot, surf);
CancellationTokenSource src = new();
Thread saboteur = CréerSaboteur(robots, surf, src.Token);
saboteur.Start();

CanalComm canal = new(robots.Count);
Afficheur pipeline = new(surf, canal, messagerie, information);

pipeline.Démarrer();
Thread[] threads = CréerFils
(
   robots, surf, canal, messagerie, information, src.Token
);

Thread lireTouche = new(() =>
{
   Console.ReadKey(true);
   src.Cancel();
});
lireTouche.Start();
foreach (var c in threads) c.Start();
foreach (var c in threads) c.Join();
lireTouche.Join();
pipeline.Arrêter();

Le code imposé pour CréerFils est :

static Thread[] CréerFils
(
   List<Robot> robots, Surface surf, CanalComm canal,
   Messagerie messagerie, Messagerie information, CancellationToken jeton
)
{
   Dictionary<char, int> collectés = new();
   Thread[] fils = new Thread[robots.Count];
   for (int r = 0; r < robots.Count; r++)
   {
      var robot = robots[r];
      int portNo = r;
      fils[r] = new(() =>
      {
         bool décédé = false;
         // tant qu'il reste des déchets à ramasser
         while (!décédé && !jeton.IsCancellationRequested &&
               surf.TrouverSi(c => robot.PeutDétecter(c)).Count > 0)
         {
            bool trouvé = false;
            // Trouver et ramasser le déchet
            do
            {
               canal.PublierSur(
                  portNo,
                  new(robot.Zone,
                      CatalogueCouleurs.Get.ObtenirCouleur(robot.Symbole,
                                                           ConsoleColor.Green),
                      robot.Symbole));
               var pts = robot.Détecter(surf);
               if (pts.Count > 0)
               {
                  trouvé = true;
                  Point2D nouvellePosi = robot.CalculerDéplacementVers(pts[0]);
                  if (pts[0] == nouvellePosi)
                  {
                     char c = surf[pts[0]].Symbole;
                     lock (collectés)
                        if (collectés.ContainsKey(c))
                           collectés[c]++;
                        else
                           collectés.Add(c, 1);
                     messagerie.Ajouter
                     (
                        CatalogueCouleurs.Get.ObtenirCouleur(robot.Symbole,
                                                             ConsoleColor.Green),
                        $"Déchet collecté à la position {nouvellePosi}"
                     );
                     surf.Retirer(pts[0]);
                  }
                  else
                  {
                     if (surf[nouvellePosi].EstVide)
                        messagerie.Ajouter
                        (
                           CatalogueCouleurs.Get.ObtenirCouleur(robot.Symbole,
                                                                ConsoleColor.Green),
                           $"Trouvé {pts.Count} déchet(s)",
                           $"Déplacement vers {pts[0]}"
                        );
                     else
                     {
                        nouvellePosi = robot.TrouverPassage(surf);
                        messagerie.Ajouter
                        (
                          CatalogueCouleurs.Get.ObtenirCouleur(robot.Symbole,
                                                               ConsoleColor.Green),
                          $"Déplacement impossible vers {pts[0]}",
                          $"Contournement par {nouvellePosi}"
                        );
                     }
                     Thread.Sleep(100);
                  }
                  try
                  {
                     robot.DéplacerVers(nouvellePosi, surf);
                  }
                  catch (DéplacementFatalException ex)
                  {
                     messagerie.Ajouter(ConsoleColor.White, $"{robot.Nom} : {ex.Message}");
                     décédé = true;
                  }
                  string s = "Collectés : ";
                  lock (collectés)
                  {
                     foreach (var (sym, n) in collectés)
                        s += $"{sym} x {n}; ";
                     information.Ajouter(ConsoleColor.White, s,
                                         "Pressez une touche pour terminer");
                  }
               }
               else
               {
                  robot.AugmenterPuissance();
               }
               Thread.Sleep(400);
            }
            while (!trouvé && !décédé);
            robot.RéinitialiserPuissance();
         }
         if (!décédé && surf.TrouverSi(c => robot.PeutDétecter(c)).Count == 0)
            messagerie.Ajouter(ConsoleColor.White,
                               $"{robot.Nom} : nettoyage complété, mise au repos");
      });
   }
   return fils;
}

Le code imposé pour CréerSaboteur est :

static Thread CréerSaboteur
   (List<Robot> robots, Surface surf, CancellationToken jeton)
{
   Thread fils = new(() =>
   {
      Random rnd = new();
      bool terminé = false;
      Bombe[] zeBombes;
      List<char> symRobots = new();
      foreach (var r in robots)
         symRobots.Add(r.Symbole);
      while (!terminé)
      {
         lock (surf)
         {
            // Sélectionner un robot au hasard
            int idxRobot = rnd.Next(robots.Count);
            var posiLibres = surf.TrouverSi
            (
               c => c == default || c == ' ',
               surf.Cadre.Exclure
            );
            List<Bombe> bombes = new();
            foreach (var pl in posiLibres)
               if (pl.Distance(robots[idxRobot].Pos) < 2)
                  bombes.Add(new('.', pl));

            zeBombes = bombes.ToArray();
            surf.Ajouter(zeBombes);
         }
         // Laisser les bombes un temps aléatoire
         Thread.Sleep(rnd.Next(100, 201));
         lock (surf)
            surf.Retirer(zeBombes);
         if (jeton.IsCancellationRequested)
            terminé = true;
         else
            Thread.Sleep(2000); // Répéter à toutes les 2 secondes
      }
   });
   return fils;
}

TP03

Notez la date et les détails de la remise (c'est un peu différent des travaux pratiques précédents!)

PFI

   

Solutionnaires

Solutionnaire de la classe Messagerie proposée à la séance S17 :

using System;
using System.Collections.Generic;

namespace ActivitéArdoise
{
   class HauteurInvalideException : Exception { }
   class Messagerie : IObservateurArdoise
   {
      Dictionary<string, List<string>> Messages { get; } = new();
      Dictionary<string, ConsoleColor> Couleurs { get; } = new();
      static readonly ConsoleColor[] couleurs =
      {
         ConsoleColor.Red, ConsoleColor.Green, ConsoleColor.Blue,
         ConsoleColor.Yellow, ConsoleColor.Cyan, ConsoleColor.Magenta
      };
      int Hauteur { get; init; }
      public Messagerie(int hauteur)
      {
         if (hauteur <= 1) throw new HauteurInvalideException();
         Hauteur = hauteur;
      }
      public void Nouveauté(string qui, string quoi)
      {
         lock (this)
         {
            if (Messages.ContainsKey(qui))
            {
               Messages[qui].Add(quoi);
               if (Messages[qui].Count > Hauteur)
                  Messages[qui].RemoveAt(0);
            }
            else
            {
               Messages.Add(qui, new());
               Couleurs.Add(qui, couleurs[Messages.Count % couleurs.Length]);
            }
         }
      }
      public void Afficher()
      {
         int[] largeurs;
         string[] noms;
         List<string> [] messages;
         lock (this)
         {
            largeurs = new int[Messages.Count];
            noms = new string[Messages.Count];
            messages = new List<string>[Messages.Count];
            int i = 0;
            foreach (var (qui, msgs) in Messages)
            {
               noms[i] = qui;
               largeurs[i] = Algos.Cumuler(
                  msgs, (cur, s) => Math.Max(cur, s.Length), noms[i].Length
               );
               messages[i] = new(msgs);
               ++i;
            }
         }
         // titres
         Console.Clear();
         for (int i = 0; i != largeurs.Length; ++i)
            Console.Write(noms[i].PadRight(largeurs[i]));
         Console.WriteLine();
         // messages
         for (int j = 0; j != Hauteur; ++j)
         {
            for (int i = 0; i != largeurs.Length; ++i)
            {
               string nom = noms[i];
               if (j < messages[i].Count)
               {
                  ConsoleColor pré = Console.ForegroundColor;
                  Console.ForegroundColor = Couleurs[nom];
                  Console.Write(messages[i][j].PadRight(largeurs[i]));
                  Console.ForegroundColor = pré;
               }
               else
                  Console.Write(new string(' ', largeurs[i]));
            }
            Console.WriteLine();
         }
      }
   }
}

En espérant que ça vous aide à organiser vos idées!


Valid XHTML 1.0 Transitional

CSS Valide !