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.
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) 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 |
22
août
|
S00 |
Au menu :
- Présentation du cours et du plan
de cours
- Les outils dont nous aurons besoin :
- Nous utiliserons
C# 10.0
- Nous ferons des projets .NET 6
- 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...) :
// ...
List<Orque> orques = new List<Orque>();
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 ex)
{
Console.WriteLine(ex.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 :
//
// code produit pour vous aider ce matin et pour amorcer une réflexion avec vous
//
List<Orque> orques = new List<Orque>();
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 ex)
{
Console.WriteLine(ex.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}");
//
// squelette de fonction seulement (tout reste à faire!)
//
static bool Trier(ref List<Orque> orques, out int nbPermutations)
{
nbPermutations = 0;
return false;
}
static class OutilsTexte
{
public static bool EstVoyelle(char c) =>
new[] { 'a', 'e', 'i', 'o', 'u', 'y' }.Contains(char.ToLower(c));
public static int CompterVoyelles(string s)
{
int n = 0;
foreach (char c in s)
if (EstVoyelle(c))
++n;
return n;
}
}
class NomInvalideException : Exception { }
class Orque
{
// Pas terrible comme nom, EstLongueurNomValide, considérant
// que le paramètre n'est pas une longueur, mais j'ai changé
// d'idée en chemin et j'ai ce que je mérite...
static bool EstLongueurNomValide(string nom) =>
1 <= nom.Length && nom.Length <= 4;
static bool EstNomValide(string nom) =>
EstLongueurNomValide(nom) &&
OutilsTexte.CompterVoyelles(nom) <= 1;
string nom;
public string Nom
{
get => nom;
private init
{
if (!EstNomValide(value))
throw new NomInvalideException();
nom = value;
}
}
public Orque(string nom)
{
Nom = nom;
}
}
|
25 août
|
S01 |
Note importante : il est probable que votre chix prof arrive avec un
peu de retard ce matin (rentrée scolaire du plus jeune!), mais il arrivera
éventuellement, alors profitez du moment d'attente pour faire progresser
votre travail et vos réflexions.
Au menu :
- 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
- 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 (voir
https://dotnetfiddle.net/mcNYFe
pour une version en ligne). Ce code est perfectible; nous ferons bien mieux plus tard dans la session :
List<Orque> orques = new List<Orque>();
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 ex)
{
Console.WriteLine(ex.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}");
Console.WriteLine();
Soldat[] soldats = new Soldat[]
{
new Orque("Bill"), new Troll("Grr"), new Kobold("Piarkkk")
};
foreach (Soldat s in soldats)
s.Saluer(); // chaque Soldat s se présente à sa façon
////////////////////////
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[] tab = orques.ToArray();
for (int i = 0; i < tab.Length - 1; ++i)
for (int j = i + 1; j < tab.Length; ++j)
if (tab[i].Nom.CompareTo(tab[j].Nom) > 0)
{
Permuter(ref tab[i], ref tab[j]);
++nbPermutations;
}
orques = tab.ToList();
return nbPermutations == 0;
}
static class OutilsTexte
{
static readonly char[] voyelles = new[] { 'a', 'e', 'i', 'o', 'u', 'y' };
// static char[] Voyelles => new[] { 'a', 'e', 'i', 'o', 'u', 'y' };
public static bool EstVoyelle(char c) =>
voyelles.Contains(char.ToLower(c));
public static int CompterVoyelles(string s)
{
int n = 0;
foreach (char c in s)
if (EstVoyelle(c))
++n;
return n;
}
}
class NomInvalideException : Exception { }
static class Algos
{
public static bool EstEntreInclusif(int val, int min, int max) =>
min <= val && val <= max;
}
class Soldat
{
public virtual void Saluer()
{
Console.WriteLine("Je suis un soldat");
}
}
class Orque : Soldat
{
const int LG_NOM_MIN = 1,
LG_NOM_MAX = 4;
const int NB_VOYELLES_MAX = 1;
static bool EstLongueurNomValide(string nom) =>
Algos.EstEntreInclusif(nom.Length, LG_NOM_MIN, LG_NOM_MAX);
static bool EstNomValide(string nom) =>
EstLongueurNomValide(nom) &&
OutilsTexte.CompterVoyelles(nom) <= NB_VOYELLES_MAX;
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;
}
//public override void Saluer()
//{
// Console.WriteLine($"MOI ORQUE {Nom}");
//}
}
class Troll : Soldat
{
public Troll(string nom)
{
// ...
}
public override void Saluer()
{
Console.WriteLine($"Moi TROLLLLL");
}
}
class Kobold : Soldat
{
public Kobold(string nom)
{
// ...
}
public override void Saluer()
{
Console.WriteLine($"Kobold, moi Kobold!");
}
}
En espérant que cela vous soit utile!
Nous avons aussi discuté sommairement du
clonage.
À 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.Red),
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.Cyan;
// 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 :)
abstract class Image
{
public ConsoleColor Teinte { get; set; }
public 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 Jpeg(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 Bmp(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 Png(this);
}
En fin de séance, nous avons survolé l'énoncé du
TP00 et, puisque j'aurai un petit retard au début de
S02, j'ai fait une petite
activité dirigée d'un système à deux composants (code client, qui était une application console, et code serveur, qui était une
bibliothèque de classes – une DLL).
Le code auquel nous en sommes arrivés pour le client était le suivant
Arsenal.FabriqueArmes fab = new Arsenal.FabriqueArmes();
Arsenal.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
namespace Arsenal
{
public interface IArme
{
void Frapper();
}
class AK47 : IArme
{
public void Frapper()
{
Console.WriteLine("POW");
}
}
class WilliWaller : IArme
{
public void Frapper()
{
Console.WriteLine("schlish!");
}
}
public enum Gravité { violent, délicat }
public class FabriqueArmes
{
public IArme CréerArme(Gravité grav)
{
return grav == Gravité.délicat ? new WilliWaller() : new AK47();
}
}
}
|
29 août
|
S02 |
Note importante : il est probable que votre chix prof arrive avec un
peu de retard ce matin (rentrée scolaire du plus jeune!), mais il arrivera
éventuellement, alors profitez du moment d'attente pour faire progresser
votre travail et vos réflexions.
Au menu :
-
Propriétés : get,
set et init
- 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
- Présentation officielle du TP00
- Petit bonbon : introduction aux
uplets
- Travail sur le TP00
|
1 sept.
|
S03 |
Au menu :
|
5 sept.
|
s/o
|
Fête du travail
(jour férié)
|
8 sept.
|
S04 |
Au menu :
- Petite introduction à la complexité
algorithmique et à la consommation de ressources :
- pour un générateur séquentiel :
- pour un générateur recycleur :
- pour un générateur aléatoire naïf :
- raffiner le générateur aléatoire
- Examen du problème de la génération des statistiques :
- conceptualiser les paires
- introduction aux
dictionnaires
- traduction des paires clés / valeurs en
uplets
- Travail sur le TP00
N'oubliez pas de remettre votre TP00
avant 23 h 59 le 9 septembre
|
12 sept.
|
S05 |
Je serai absent cette semaine car je donne des conférences à
CppCon. Vous pourrez suivre mes
aventures sur :
../../../Sujets/Orthogonal/cppcon2022.html
|
15 sept.
|
S06 |
Je serai absent cette semaine car je donne des conférences à
CppCon. Vous pourrez suivre mes
aventures sur :
../../../Sujets/Orthogonal/cppcon2022.html
|
19 sept.
|
S07 |
Au menu :
À la demande générale, le code (incomplet mais fonctionnel) de
l'observateur de clavier avec interfaces est semblable à ceci :
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
{
static GesClavier singleton = null;
List<IRéactionClavier> Abonnés { get; } = new();
public void Abonner(IRéactionClavier p)
{
Abonnés.Add(p);
}
GesClavier()
{
}
public static GesClavier GetInstance()
{
//
// note : dangereux si multithreading
//
if (singleton == null)
singleton = new GesClavier();
return singleton;
}
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");
}
}
À la demande générale, le code (incomplet mais fonctionnel) de
l'observateur de clavier avec délégués est semblable à ceci :
var ges = GesClavier.GetInstance();
var croc = new CroqueMort();
// ges.Abonner(new Afficheur().Afficher);
ges.Abonner(new Terminateur(croc).Réagir);
ges.Abonner(new ÀDroite(ConsoleKey.D).Réagir);
ges.Abonner(new EnHaut(ConsoleKey.W).Réagir);
ges.Abonner(new ÀGauche(ConsoleKey.A).Réagir);
ges.Abonner(new EnBas(ConsoleKey.S).Réagir);
// 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é);
//}
delegate void RéactionClavier(ConsoleKeyInfo clé);
class Afficheur // : IRéactionClavier
{
public void Afficher(ConsoleKeyInfo clé) // note: nom :)
{
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
{
static GesClavier singleton = null;
List<RéactionClavier> Abonnés { get; } = new();
public void Abonner(RéactionClavier p)
{
Abonnés.Add(p);
}
GesClavier()
{
}
public static GesClavier GetInstance()
{
// note : dangereux si multithreading
if (singleton == null)
singleton = new GesClavier();
return singleton;
}
public void Exécuter()
{
var clé = Console.ReadKey(true);
foreach (var réac in Abonnés)
réac(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");
}
}
|
22 sept.
|
S08 |
Au menu :
|
26 sept.
|
S09 |
Au menu :
- Retour sur Q00
- Retour sur le TP00
- Mes pratiques de correction
et mes codes de correction
- Comment j'ai ajusté la pondération à votre avantage (pour
MOD, NRC et
NFP en particulier)
- Sources de confusion :
- Exceptions (où mettre un try? Type ou
message?)
- Qualifications d'accès (protected,
public, internal...)
- Sens du mot static
- Rôle d'un constructeur (ou : que se passe-t-il quand un attribut
d'instance – en particulier une string
– n'est pas initialisé en
C#)
- Même si (dû aux circonstanctes entourant mon absence lors de la remise) cette remise fut
faite sous forme électronique, quelques mots sur une question plus important qu'il n'y paraît,
soit : quelle
police utiliser quand un imprime du code?
- Travail sur le TP01
|
29 sept.
|
S10 |
Au menu :
Notez que je sais qu'il est parfois tentant de ne pas profiter du temps
offert à titre de soutien en classe, mais ce travail est plus costaud que
les précédents et je vous invite à profiter de l'occasion pour en assurer
la saine progression... et pour poser des questions!
|
3 oct.
|
s/o |
Élections provinciales (cours suspendus)
|
5 oct.
|
S11 |
Votre chic prof doit s'absenter en ce mercredi qui est un lundi au
calendrier... Pour compenser, la date de
remise du
TP01
est déplacée du 7 octobre au 14 octobre
|
6 oct.
|
S12 |
Au menu :
- 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<T> avec une capacité
fixe
- Exemple de Chercher<T> où
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 TP01
Les exemples de ce cours suivent. Pour la fonction
Afficher générique :
using System;
using System.Collections.Generic;
class Program
{
static void Afficher<T>(T n)
{
Console.Write($"{n} ");
}
static void Main()
{
Afficher(3);
Afficher(3.14159f);
}
}
// ...
... pour la fonction TriBulles générique,
de même que Permuter (j'ai ajouté la classe X
sous sa forme IComparable<X> pour
fins d'illustration; si vous la remplacez par une version qui n'est pas
IComparable<X>, on ne pourra plus trier un X[]) :
using System;
using System.Collections.Generic;
class Program
{
static void Permuter<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
static void TriBulles<T>(T [] tab) where T : IComparable<T>
{
for (int i = 0; i < tab.Length - 1; ++i)
for (int j = i + 1; j < tab.Length; ++j)
if (tab[i].CompareTo(tab[j]) > 0)
Permuter(ref tab[i], ref tab[j]);
}
class X : IComparable<X>
{
public int CompareTo(X x) => 0;
}
class Y
{
}
static void Main()
{
int[] tab = new[] { 3, 7, 11, 2, 5 };
TriBulles(tab);
foreach (int n in tab)
Console.Write($"{n} ");
Console.WriteLine();
string[] strs = new[] { "man", "allo", "genre" };
TriBulles(strs);
foreach (var s in strs)
Console.Write($"{s} ");
TriBulles(new X[] { new X(), new X(), new X() });
// TriBulles(new Y[] { new Y(), new Y(), new Y() });
}
}
// ...
... pour la classe
Pile<T> :
using System;
using System.Collections.Generic;
class Program
{
class Pile<T>
{
const int TAILLE_MAX = 10;
T[] Éléments { get; } = new T[TAILLE_MAX];
public int Count { get; private set; } = 0;
public bool EstVide { get => Count == 0; }
public bool EstPleine { get => Count == TAILLE_MAX; }
public void Push(T élem)
{
if (EstPleine) throw new Exception(); // bof
Éléments[Count] = élem;
++Count;
}
public void Pop()
{
if (EstVide) throw new Exception(); // bof
--Count;
}
public T Top()
{
if (EstVide) throw new Exception(); // bof
return Éléments[Count - 1];
}
}
static void Main()
{
var pile = new Pile<string>();
pile.Push("prof");
pile.Push("mon");
pile.Push("J'aime");
while(!pile.EstVide)
{
Console.WriteLine(pile.Top());
pile.Pop();
}
}
}
// ...
... Pour la fonction
Chercher<T> :
using System;
using System.Collections.Generic;
class Program
{
static int Chercher<T>(T[] tab, T val) where T : IEquatable<T>
{
for (int i = 0; i != tab.Length; ++i)
if (tab[i].Equals(val))
return i;
return -1;
}
static void Main()
{
string[] strs = new string[] { "Tomate", "Concombre", "Radis" };
int n = Chercher(strs, "Radis");
if (n == -1)
Console.WriteLine("Pas trouvé...");
else
Console.WriteLine($"Trouvé à l'indice {n}");
}
}
// ...
|
10 oct.
|
s/o
|
Action
de grâce
(jour férié)
|
13 oct.
|
S13 |
Au menu :
Attention, jeudi selon l'horaire du lundi |
17 oct.
|
S14 |
Au menu :
- Retour sur les exercices sur la programmation générique
(solutionnaire possible :
exercice-apprivoiser-genericite-lambda--solutionnaire.html)
- On se fait une petite collection simpliste :
- On commence par une liste simplement
chaînée de int
- On la transforme ensuite en une petite collection générique, soit une liste
simplement chaînée de T
- On ajuste cette collection pour qu'il soit
possible de la traverser avec foreach
- Je me suis permis de mentionner 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.)
Le code de notre Liste<T> (liste simplement chaînée générique) était
comme suit (j'ai ajouté une levée d'exception dans le cas où on essaierait
de supprimer un élément d'une liste vide; en classe, je m'étais limité à
un commentaire). Voir
https://dotnetfiddle.net/fUqGAq pour une version en ligne :
Liste<int> lst = new();
for (int i = 0; i != 10; ++i)
lst.AjouterFin(i + 1);
foreach (var e in lst)
Console.WriteLine(e);
class ListeVideException : Exception {}
class Liste<T> : IEnumerable<T>
{
class Énumérateur : IEnumerator<T>
{
Noeud Cur { get; set; }
public bool MoveNext()
{
if (Cur.Succ == null) return false;
Cur = Cur.Succ;
return true;
}
public void Reset() => throw new NotImplementedException();
public void Dispose() { }
public T Current => Cur.Valeur;
object IEnumerator.Current => Cur.Valeur;
public Énumérateur(Noeud tête)
{
Cur = new Noeud(); // sorte de "pré-tête"
Cur.Succ = tête;
}
}
// IEnumerator<T> :
// MoveNext() ==> essaie d'avancer au prochain élément, retourne true si ça a fonctionné
// Current ==> valeur pointée par l'énumérateur
// foreach(var e in lst)
// f(e)
// Intervalle à demi-ouvert : début exclus, fin incluse
// for(IEnumerator<T> e = lst.GetEnumerator(); e.MoveNext(); )
// f(e.Current);
//
public IEnumerator<T> GetEnumerator() => new Énumérateur(Tête);
class Noeud
{
public T Valeur { get; init; } = default;
public Noeud Succ { get; set; } = null;
public Noeud()
{
}
public Noeud(T val)
{
Valeur = val;
}
}
Noeud Tête { get; set; } = null;
Noeud Queue { get; set; } = null;
public bool EstVide => Count == 0;
public int Count { get; private set; } = 0;
public void AjouterDébut(T val)
{
var p = new Noeud(val);
p.Succ = Tête;
Tête = p;
if (EstVide) Queue = p;
++Count;
}
//Noeud Trouver(T val) // en haut : Liste<T> where T : IEquatable<T>
//{
// for (var p = Tête; p != null; p = p.Succ)
// if (p.Valeur.Equals(val))
// return p;
// return null;
//}
//void InsérerAprès(Noeud p, T val)
//{
// var q = new Noeud(val);
// q.Succ = p.Succ;
// p.Succ = q;
//}
public void AjouterFin(T val)
{
if (EstVide)
AjouterDébut(val);
else
{
var p = new Noeud(val);
Queue.Succ = p;
Queue = p;
++Count;
}
}
public void SupprimerDébut()
{
if(EstVide) throw new ListeVideException();
Tête = Tête.Succ;
--Count;
if (EstVide) Queue = null;
}
IEnumerator IEnumerable.GetEnumerator() => new Énumérateur(Tête);
//public void SupprimerFin()
//{
// // piarkkk....
//}
//public void Afficher()
//{
// for (var p = Tête; p != null; p = p.Succ)
// Console.WriteLine(p.Valeur);
//}
}
|
20 oct.
|
S15 |
Au menu :
- Q02
- Retravailler les algorithmes génériques écrits à la séance
S14 pour les rendre plus généraux
- Premier contact avec la programmation parallèle et concurrente en
C#
- Exercices sur la multiprogrammation :
exercice-apprivoiser-multiprog.html
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;
}
|
24 oct.
|
S16 |
Au menu :
- Discussion sur la
Cache
- Discussion sur le
faux-partage
- Quelques exemples amusants pour apprivoiser les enjeux de la
synchronisation
- Présentation des
zones de
transit
- Exemple avec un pipeline de traitement
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;
}
Le code du pipeline que nous avons implémenté est :
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.IO;
using System.Diagnostics;
var fini = new bool[3];
var zt = new ZoneTransit<(string, string)>[3]
{
new(), new(), new()
};
var thrs = new Thread[]
{
// lecteur
new Thread(() =>
{
foreach(var nom in args)
zt[0].Ajouter((LireFichier(nom), nom));
fini[0] = true; // dès maintenant, je ne produis plus
}),
// majusculeur
new Thread(() =>
{
while(!fini[0])
{
var lst = zt[0].Extraire();
foreach(var (s, nom) in lst)
zt[1].Ajouter(Majusculer(s, nom));
}
{
var lst = zt[0].Extraire();
foreach(var (s, nom) in lst)
zt[1].Ajouter(Majusculer(s, nom));
}
fini[1] = true; // dès maintenant, je ne produis plus
}),
// censeur
new Thread(() =>
{
while(!fini[1])
{
var lst = zt[1].Extraire();
foreach(var (s, nom) in lst)
zt[2].Ajouter(Censurer(s, nom));
}
{
var lst = zt[1].Extraire();
foreach(var (s, nom) in lst)
zt[2].Ajouter(Censurer(s, nom));
}
fini[2] = true;
}),
// scripteur
new Thread(() =>
{
while(!fini[2])
{
var lst = zt[2].Extraire();
foreach(var (s, nom) in lst)
Écrire(s, nom);
}
{
var lst = zt[2].Extraire();
foreach(var (s, nom) in lst)
Écrire(s, nom);
}
})
};
foreach (var th in thrs) th.Start();
foreach (var th in thrs) th.Join();
static string LireFichier(string nom)
{
using (var fich = new StreamReader(nom))
return fich.ReadToEnd();
}
static (string, string) Majusculer(string s, string nom)
=> (s.ToUpper(), nom);
static (string,string) Censurer(string s, string nom)
{
string résultat = "";
int début = 0;
string àCensurer = "IF";
int pos = s.IndexOf(àCensurer, début);
while(pos != -1)
{
résultat += s.Substring(début, pos - début);
résultat += "[CENSURÉ]";
début = pos + àCensurer.Length;
pos = s.IndexOf(àCensurer, début);
}
résultat += s.Substring(début);
return (résultat, nom);
}
static void Écrire(string s, string nom)
{
using (var sw = new StreamWriter(nom + ".out"))
sw.Write(s);
}
static class Algos
{
public static void Permuter<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
class ZoneTransit<T>
{
object mutex = new object();
List<T> data = new();
public void Ajouter(T elem) // à rediscuter (API suspecte)
{
lock (mutex)
data.Add(elem);
}
public List<T> Extraire()
{
List<T> lst = new();
lock (mutex)
Algos.Permuter(ref lst, ref data);
return lst;
}
}
|
27 oct.
|
S17 |
Au menu :
Activité préparatoire au TP02, que je devrais vous passer plus tard
cette semaine. Soit le code client suivant :
using System;
using System.Threading;
var bb = new Ardoise();
bb.Abonner(new Caméra());
var noms = new[] { "Bill", "Bob" };
var pub = new int[noms.Length];
for(int i = 0; i != noms.Length; ++i)
pub[i] = bb.AjouterPublieur(noms[i]);
bool fini = false;
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; )
{
if (dé.Next(0, pub.Length) == indice)
bb.Publier(pub[indice], $"{noms[indice]} dit : \"ceci est mon message {n++}\"");
Thread.Sleep(1000);
}
});
}
foreach (var thr in th) thr.Start();
Console.ReadKey(true);
fini = true;
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)
Dans le TP02, il y aura une
Ardoise différente mais selon le même modèle, alors vous ne perdez
pas votre temps ce matin
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 Thread(() =>
{
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 Thread(() =>
{
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 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");
}
}
}
|
31 oct.
|
S18 |
Au menu :
- Retour sur TP01
- Activité préparatoire au TP02 :
- Créez un type PanneauAffichage, qui aura entre autres comme états une
position (coin en haut et à gauche) de même qu'une hauteur et une largeur
- Il sera possible d'écrire (méthode Write(string, ConsoleColor)) sur un
PanneauAffichage. Écrire sur un panneau n'écrira pas à l'écran (truc :
gardez dans un PanneauAffichage un tableau 2D
de paires
(char,ConsoleColor)). Si une string contient des sauts de ligne ('\n'),
alors elle sera représentée sur plusieurs lignes. Écrire dans un
PanneauAffichage remplace la totalité du texte colorié qui s'y trouvait
précédemment
- Créez un type Écran, qui aura entre autres comme
états une List<PanneauAffichage>. Afficher un Écran
signifie afficher ses
panneaux du premier au dernier (du plus profond au moins profond); les
instances de PanneauAffichage peuvent se chevaucher, dans quel cas le plus
« haut » cache (en tout ou en partie) le plus « bas »
- Lancez des fils d'exécution (des Thread)
qui écriront des messages sur vos PanneauAffichage
à un rythme de votre choix (chaque Thread
peut avoir son propre rythme). Faites en sorte qu'un des fils
d'exécution lise du clavier le texte à écrire dans l'un des
PanneauAffichage
- Lancez un fil d'exécution qui sera responsable d'afficher votre
Écran. L'affichage devra se faire à rythme fixe (p. ex. : une
fois par 500ms ou une fois par seconde), et
devra ne remplacer que les cases à l'écran qui ont changé. Note : vous
voudrez utiliser Console.SetCursorPosition et Console.ForegroundColor
pour arriver à
vos fins
- L'enjeu : l'affichage doit se faire sans corruption 🙂
|
3 nov.
|
S19 |
Au menu : votre chic prof doit s'absenter, mais voici une démarche de
solution du problème proposé lors de
S18 : PanneauAffichage.html
|
7 nov.
|
s/o |
Journée de mise à niveau (cours suspendus)
|
10 nov.
|
S20 |
Au menu :
|
14 nov.
|
S21 |
Au menu :
|
17 nov.
|
S22 |
Au menu :
- Q04
- Q05
- Travail sur le TP02
|
21 nov.
|
S23 |
Cours suspendus dû aux événements quelque peu traumatisants du vendredi
18 novembre 2022
|
24 nov.
|
S24 |
Au menu :
|
28 nov.
|
S25 |
Au menu :
|
1 déc.
|
S26 |
Au menu :
- Q06
- Travail sur le TP02
- Présentation de la
PFI
|
5 déc.
|
S27 |
Au menu :
|
8 déc.
|
S28 |
Au menu :
- Q07
- Q08
- Travail sur la PFI
|
12 déc.
|
S29 |
Au menu :
À remettre :
- La
PFI de la session
A2022 (si elle est prête, sinon vous avez jusqu'au 16 décembre à 23 h 59 par courriel Colnet,
mais dans ce cas glissez une copie imprimée sous ma porte de bureau!)
|
Vous trouverez ici quelques documents, la plupart petits, qui peuvent vous
donner un petit coup de pouce occasionnel.
Les consignes des travaux pratiques suivent.
Quelques solutionnaires suivent.