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
| Date |
Séance |
Détails |
|
22 janvier |
S00 |
Au menu :
- Présentation du cours et du plan
de cours
- Petit exercice de remise en forme
Notre exercice fut :
Écrivez un programme console C# qui :
- Lit une hauteur de rectangle, qui doit être entre 1
et 20 inclusivement
- Lit une largeur de rectangle, qui doit être entre 1
et 70 inclusivement
- Lit un symbole qui servira à dessiner le rectangle. Ce symbole sera un caractère non-blanc (voir char.IsWhiteSpace
pour valider ceci)
Ensuite, dessinez à l'écran le rectangle correspondant
Nous en avons profité pour faire une brève révision de
420SF1, puis
pour apporter des ajustements menant à un glissement vers la matière au
menu de notre cours.
Notre version initiale fut :
int hauteur = LireHauteur();
int largeur = LireLargeur();
char symbole = LireSymbole();
DessinerRectangle(hauteur, largeur, symbole);
static void DessinerRectangle(int hau, int lar, char sym)
{
for(int ligne = 0; ligne != hau; ++ligne)
{
for(int col= 0; col != lar; ++col)
{
Console.Write(sym);
}
Console.WriteLine();
}
}
static char LireSymbole()
{
Console.Write("Symbole? ");
char symbole = char.Parse(Console.ReadLine());
while(char.IsWhiteSpace(symbole))
{
Console.WriteLine($"Entrée erronnée. Symbole? ");
symbole = char.Parse(Console.ReadLine());
}
return symbole;
}
static int LireEntierBorné(string msg, int min, int max)
{
int valeur;
Console.Write($"{msg}? ");
valeur = int.Parse(Console.ReadLine());
while (!EstEntreInclusif(valeur, min, max))
{
Console.WriteLine($"Entrée erronnée. {msg}? ");
valeur = int.Parse(Console.ReadLine());
}
return valeur;
}
static int LireHauteur()
{
const int HAUTEUR_MIN = 1,
HAUTEUR_MAX = 20;
return LireEntierBorné("Hauteur", HAUTEUR_MIN, HAUTEUR_MAX);
}
static int LireLargeur()
{
const int LARGEUR_MIN = 1,
LARGEUR_MAX = 70;
return LireEntierBorné("Largeur", LARGEUR_MIN, LARGEUR_MAX);
}
static bool EstEntreInclusif(int val, int min, int max)
{
return min <= val && val <= max;
}
Nous avons ensuite introduit l'idée d'une classe Rectangle, pour que le code qui
dessine des rectangles ait bel et bien... des rectangles! Le code du programme principal
résultant fut :
using z;
Rectangle[] rects = new Rectangle[]
{
new (5, 12, 'L'), new(7, 20, '0'), new(17, 43, '*')
};
for (int i = 0; i != rects.Length; ++i)
rects[i].Dessiner();
... et celui de la classe Rectangle en tant que telle fut :
namespace z
{
internal class Rectangle
{
// variables membres : attributs
int hauteur;
int largeur;
char symbole;
// propriété : mécanisme de contrôle d'accès
// de fine granularité aux états d'un objet
public int Hauteur
{
// permet de consulter la variable en lecture
get { return hauteur; }
private init /*set*/ // permet de modifier la variable
{
hauteur = value;
}
}
public int Largeur
{
get { return largeur; }
private init /*set*/ { largeur = value; }
}
public char Symbole
{
get { return symbole; }
private set { symbole = value; }
}
// constructeur : méthode qui sert à placer
// l'objet nouvellement construit dans un état
// initial correct
public Rectangle(int hau, int lar, char sym)
{
Hauteur = hau;
Largeur = lar;
Symbole = sym;
}
// méthode d'instance Dessiner, qui a pour rôle
// d'afficher _ce_ Rectangle à la console
public void Dessiner()
{
for (int ligne = 0; ligne != hauteur; ++ligne)
{
for (int col = 0; col != largeur; ++col)
{
Console.Write(symbole);
}
Console.WriteLine();
}
}
}
}
Notez qu'en ce premier cours, nous investissons nos efforts sur deux
aspects, soit :
- Reprendre la forme intellectuelle après le congé des Fêtes, et
- Mettre en place le vocabulaire et les idées qui guideront nos
réflexions en ce début de session
Ça fait un gros cours pour démarrer la session, mais nous réinvestirons
le tout cours après cours, alors ça va entrer doucement dans notre tête,
et s'intégrer à notre praxis émergente 🙂.
- Mise en place du vocabulaire de base de la POO, mais sous forme d'un
survol seulement, et avec accent sur l'encapsulation
(sans la nommer, mais vous ne perdez rien pour attendre!). Ainsi, nous avons
identifié et situé les termes et idées suivant(e)s :
- une maxime importante : « un objet est responsable de son
intégrité, du début à la fin de sa vie »
- quelques mots clés qui aident à encadrer la capacité d'un objet à
assurer son encapsulation, soit les qualifications d'accès private
et public
- nous n'avons pas vraiment parlé de protected,
qui a une utilité réelle mais plus limitée que des deux autres, et
dont nous ne pouvons pas traiter pour le moment
- nous avons toutefois insisté sur le fait que private,
la qualification par défaut des membres dans une classe, est ce que
nous souhaitons utiliser le plus possible, et nous avons donné
quelques raisons pour soutenir cette prise de position
- nous avons démontré que public, du moins pour les états d'un objet,
devrait être évité dans la plupart des cas, cette qualification
empêchant de facto l'objet d'assurer son encapsulation
- Nous avons mis de l'avant quelques mots de vocabulaire plus techniques,
soit :
- les méthodes, qui sont les services (les
fonctions, les comportements) d'un objet
- les attributs, qui sont les états (variables,
constantes) d'un objet
- les constructeurs, qui sont les méthodes un peu
spéciales mais Ô combien importantes qui permettent de déterminer
l'état initial d'un objet – sans eux, pas d'état initial connu pour un
objet, donc pas d'encapsulation pour lui
- Nous avons discuté du glissement sémantique entre
F(obj), où on
applique une opération sur une entité , ce qui rejoint l'approche
procédurale mise en application dans le cours
420-SF1-RE,
et obj.F(), qui sollicite la méthode de l'objet
et rend ce dernier actif
- Nous avons discuté brièvement des accesseurs (méthodes de
consultation) et des mutateurs (méthodes de modification), qui sont des
familles de services typiques pour les objets
- La partie get d'une
propriété est un
exemple d'accesseur
- La partie set d'une
propriété est un
exemple de mutateur
- La partie init d'une
propriété est
aussi un
exemple de mutateur, mais qui ne fonctionne que durant la construction
d'un objet. Évidemment, une
propriété
ne peut pas avoir à la fois un set et un
init (c'est l'un ou l'autre)
- Il est évidemment possible d'écrire des méthodes qui ont on
comportement d'accesseur ou un comportement de mutateur
- Nous avons montré que
C#
permet une écriture concise pour ces deux familles importantes de
services, soit les
propriétés, et nous avons indiqué qu'il s'agit d'une
pratique idiomatique dans ce langage, donc d'une pratique que nous
allons mettre en application même si elle est plus « décorative » que
nécessaire
- Nous avons abordé très sommairement le mot static, qui en
C#
a le sens de
« membre de classe » alors qu'un membre qui n'est pas qualifié
static est un « membre d'instance »
// ...
Triangle t = new(5, 'A');
t.Dessiner();
// ...
... affichera à la console la chose suivante :
A
AAA
AAAAA
AAAAAAA
AAAAAAAAA
En fait, notre Triangle n'était pas centré lors de l'affichage, mais considérez cet ajout comme un bonbon!
À titre de « petit bonbon », nous avons vu en début de
séance que les fonctions qui se limitent à un seul return
peuvent être écrites
de manière simplifiée, et nous avons appris que votre chic prof, souhaitant
vous encourager à écrire de courtes fonctions qui font une et une seule chose
(et le font bien!), acceptera cette syntaxe si elle est bien utilisée.
Ainsi, ceci :
static int Carré(int n)
{
return n * n;
}
... peut s'écrire de manière équivalente sous la forme suivante :
static int Carré(int n) => n * n;
... alors que ceci :
class Nom
{
const string NOM_DÉFAUT = "Inconnu(e)";
string valeur;
public string Valeur
{
get
{
return valeur;
}
private set
{
if (value != null && value.Length > 0)
{
valeur = value;
}
else
{
valeur = NOM_DÉFAUT;
}
}
}
public string Crié
{
get
{
return Valeur.ToUpper();
}
}
public Nom(string valeur)
{
Valeur = valeur;
}
}
... pourra s'écrire comme suit
class Nom
{
const string NOM_DÉFAUT = "Inconnu(e)";
string valeur;
public string Valeur
{
get => valeur;
private set
{
if (value != null && value.Length > 0)
{
valeur = value;
}
else
{
valeur = NOM_DÉFAUT;
}
}
}
public string Crié => Valeur.ToUpper(); // note : un get seulement, pas de set
public Nom(string valeur)
{
Valeur = valeur;
}
}
D'autres simplifications d'écriture viendront plus tard dans la session.
Quelques nouveaux termes de vocabulaire utilisés aujourd'hui : classe, instance, attribut d'instance,
propriété d'instance (avec volets get, set
et init), constructeur par défaut, constructeur paramétrique, qualifications d'accès
private et public (il y en a d'autres),
encapsulation (à peine), membre d'instance (non-static), membre de classe :
static,
invariant,
précondition et
postcondition... Ouf!
|
|
26 janvier |
S01 |
Au menu :
- Retour sur l'idée de propriété
- Accesseur (volet get)
- Mutateur à la construction seulement (volet
init)
- Mutateur en tout temps (volet set)
- Pourquoi init est souvent préférable
à set (et comment choisir!)
- Membre d'instance (non-static) ou
membre de classe (static)?
- Poursuite de la mise en place du vocabulaire de base de la POO, mais sous forme d'un
survol seulement, et avec accent sur l'encapsulation. Ainsi, nous avons
identifié et situé les termes et idées suivant(e)s :
- Respect des
invariants,
entre chaque appel d'un service d'un objet
- Identification et garantie du respect des
préconditions
pour les services d'un objet
- Identification et garantie du respect des
postconditions
pour les services d'un objet
- Rappel d'une maxime importante : « un objet est responsable de son
intégrité, du début à la fin de sa vie »
- Petite séance de programmation collective. Écrivons ensemble un
programme qui :
- Lit un nombre de personnes. Ce nombre doit être un entier
strictement positif
- Pour chaque personne :
- lit son nom, qui devra être une chaîne de caractères non-vide
- lit son âge (un entier positif, donc supérieur ou égal à zéro)
- crée une Personne ayant ce nom et
cet âge
- l'ajoute dans un tableau
- Une fois toutes les personnes lues, ce programme :
- Trouvera et affichera le nom de la personne dont le nom est le
plus long
- Affichera l'âge moyen des personnes
- Pour y arriver, nous définirons une classe Personne
telle que :
- Une Personne aura un nom et un âge
- Le nom d'une Personne ne pourra être vide.
Par défaut, nous utiliserons
le nom "INCONNU(E)"
- L'âge d'une Personne devra se situer entre 0
et 140 inclusivement. Par défaut, nous considérerons qu'une Personne
est d'âge zéro
Quelques nouveaux termes de vocabulaire utilisés aujourd'hui :
invariant,
précondition
et
postcondition
Une solution possible est disponible ici.
Vous remarquerez que les mutateurs (les volets set
et init des propriétés) sont exprimés
un peu différemment sur cet exemple que ce que nous avons fait en classe,
mais je vous explique ce que ça signifie dès la séance
S02.
À titre de bonbons aujourd'hui, j'ai aussi
montré :
- Le côté optionnel des accolades dans certaines structures de
contrôle (if, for,
while) quand le corps se
limite à une seule instruction
- Le droit à placer plus d'un return par
fonction, mais seulement si c'est justifié
|
|
29 janvier |
S02 |
Au menu :
- Que faire pour signaler qu'une fonction ne pourra pas rencontrer ses
postconditions :
introduction (très brève) aux exceptions, un sujet important que nous
abordons aujourd'hui mais aussi sur lequel nous
reviendrons bientôt, et au mot clé throw
À titre de bonbon aujourd'hui, j'ai aussi
montré :
- Comment utiliser foreach dans le cas où
une répétitive doit itérer sur tous les éléments d'une séquence et n'a
pas besoin de la valeur des indices
- Le côté optionnel des accolades dans certaines structures de
contrôle (foreach, mais pas
try ni catch) quand le corps se
limite à une seule instruction
- Comment utiliser l'opérateur ternaire dans les cas où nous souhaitons
choisir l'une de deux expressions de même type sur la base d'une condition
- Présentation du labo 00 – Le
cryptographe
- Travail sur le labo 00 – Le
cryptographe
Quelques trucs pour vous aider...
Si vous cherchez un exemple simpliste de programme de test
interactif, vous pouvez utiliser celui-ci (vous pouvez aussi
vous en écrire un plus à votre goût; je ne ramasserai que la classe
Crypteur après tout) :
// ...
do
{
Console.Write("Message à chiffrer? ");
string texte = Console.ReadLine();
Console.Write("Clé de chiffrement? ");
int cléChiffrement = int.Parse(Console.ReadLine());
Crypteur crypteur = new (texte, cléChiffrement);
Console.Write("Clé à utiliser pour déchiffrer? ");
int cléDéchiffrement = int.Parse(Console.ReadLine());
Console.WriteLine($"Message \"déchiffré\" : {crypteur.Déchiffrer(cléDéchiffrement)}");
}
while (Poursuivre());
// ...
... notez que je suppose que vous pouvez écrire la fonction
Poursuivre puisque nous l'avons fait à quelques reprises cet
automne.
Si vous souhaitez transformer une string
en char[], examinez les
méthodes d'instance de cette string (portez
attention en particulier à celles dont le nom commence par
To...).
Si vous souhaitez transformer une string
en majuscules, examinez les méthodes d'instance de cette
string (portez attention en particulier à celles dont le nom
commence par To...).
Si vous souhaitez transformer un char[]
en string, sachez que string
expose un constructeur paramétrique acceptant un char[]
en paramètre. Par
exemple, comparez les deux exemples ci-dessous (l'un des deux est plus
pertinent que l'autre pour vos fins) :
// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = cs.ToString();
Console.Write(s);
// ...
|
// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = new string(cs);
Console.Write(s);
// ...
|
Si vous souhaitez modifier un char, alors... que diriez-vous de la
simple arithmétique? Par exemple :
// ...
char c = 'A';
Console.WriteLine(c); // A
c = (char)(c + 3);
Console.WriteLine(c); // D
// ...
Quelques trucs pour vous aider...
Si vous cherchez un exemple simpliste de programme de test
interactif, vous pouvez utiliser celui-ci (vous pouvez aussi
vous en écrire un plus à votre goût; je ne ramasserai que la classe
Crypteur après tout) :
// ...
do
{
Console.Write("Message à chiffrer? ");
string texte = Console.ReadLine();
Console.Write("Clé de chiffrement? ");
int cléChiffrement = int.Parse(Console.ReadLine());
Crypteur crypteur = new (texte, cléChiffrement);
Console.Write("Clé à utiliser pour déchiffrer? ");
int cléDéchiffrement = int.Parse(Console.ReadLine());
Console.WriteLine($"Message \"déchiffré\" : {crypteur.Déchiffrer(cléDéchiffrement)}");
}
while (Poursuivre());
// ...
... notez que je suppose que vous pouvez écrire la fonction
Poursuivre puisque nous l'avons fait à quelques reprises cet
automne.
Si vous souhaitez transformer une string
en char[], examinez les
méthodes d'instance de cette string (portez
attention en particulier à celles dont le nom commence par
To...).
Si vous souhaitez transformer une string
en majuscules, examinez les méthodes d'instance de cette
string (portez attention en particulier à celles dont le nom
commence par To...).
Si vous souhaitez transformer un char[]
en string, sachez que string
expose un constructeur paramétrique acceptant un char[]
en paramètre. Par
exemple, comparez les deux exemples ci-dessous (l'un des deux est plus
pertinent que l'autre pour vos fins) :
// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = cs.ToString();
Console.Write(s);
// ...
|
// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = new string(cs);
Console.Write(s);
// ...
|
Si vous souhaitez modifier un char, alors... que diriez-vous de la
simple arithmétique? Par exemple :
// ...
char c = 'A';
Console.WriteLine(c); // A
c = (char)(c + 3);
Console.WriteLine(c); // D
// ...
|
|
2 février |
S03 |
Au menu :
- Comment demander de l'aide
-
Comment convertir un objet en
string, et
pourquoi, si tabChar est un
char[], l'expression tabChar.ToString()
ne donne pas le même résultat que l'expression
new string(tabChar) (note : nous y reviendrons)
-
Bref rappel sur les exceptions,
qui permettent entre autres de découpler la détection d'une situation
atypique (une erreur, dans la grande majorité des cas) de son
traitement. Plus en détail :
- Pourquoi distinguer le moment / le lieu où un problème est détecté
du moment / du lieu où il est traité (le cas échéant)
- Trois mots clés : throw (signaler une
situation exceptionelle), try (exécuter du
code à risque, avec pour objectif de réagir si un problème survient),
catch (gérer la situation signalée)
- Un quatrième mot, finally, est très
important (plus que catch!), mais nous y
reviendrons
- Exceptions et
préconditions
- Exceptions et
postconditions
- Exceptions et respect des
invariants
- Exceptions et constructeurs
- Définir un type d'exception « maison »
- Introduction à la syntaxe (qui, en
C#, implique l'héritage
d'implémentation, sujet que nous ne ferons qu'effleurer aujourd'hui)
- Revisiter des exemples vus précédemment à la lueur de la matière
d'aujourd'hui
- De plus :
- jasette informelle sur le choix d'une université (c'est le
temps des portes ouvertes!)
- mesurer l'impact en temps de levées d'exceptions
- mesurer l'impact en temps d'ajouter un caractère à une
string
- pourquoi le type
string
est immuable en
C#
- pour celles et ceux qui ne l'ont pas vu la session
précédente : la méthode TryParse
- Quelques exemples concrets :
- division entière avec levée d'exception
- coût des exceptions
- coût de l'ajout de caractères à une string (version naïve)
- classe Cercle avec invariants
(voir la consigne ci-dessous)
- manipulation de
string
et passage de
string
à char[] (et inversement)
Rédigez la classe Cercle telle que :
- Un Cercle est représenté par un Centre
et un Rayon
- Le Centre est un Point
- Le Rayon est un nombre à virgule flottante supérieur à zéro
- Un Cercle est immuable
- Un Cercle offre une méthode Contient
acceptant en paramètre un Point et retournant true
seulement si ce Point est dans le Cercle
(bordure incluse)
À titre de bonbon aujourd'hui, j'ai aussi
montré :
- Comment faire des
propriétés automatiques, dans les cas où une classe
n'a pas d'invariant
à garantir
- Comment un constructeur peut déléguer une partie (ou la totalité) de son travail à un autre constructeur
class Point
{
public float X { get; private init; }
public float Y { get; private init; }
public Point() : this(0,0)
{
}
public Point(float x, float y)
{
X = x;
Y = y;
}
public float DistanceDe(Point autre) =>
(float) Math.Sqrt(Math.Pow(X - autre.X, 2) + Math.Pow(Y - autre.Y, 2));
}
class RayonInvalideException : Exception;
class Cercle
{
public Point Centre { get; init; }
private float rayon;
private static bool EstRayonValide(float candidat) => candidat > 0;
public float Rayon
{
get => rayon;
set
{
rayon = EstRayonValide(value)? value : throw new RayonInvalideException();
}
}
public Cercle() : this(1)
{
}
public Cercle(float rayon) : this(new(), rayon)
{
}
public Cercle(Point centre, float rayon)
{
Centre = centre;
Rayon = rayon;
}
public bool Contient(Point pt) => Centre.DistanceDe(pt) <= Rayon;
}
- Exemple (simpliste) de manipulation de chaînes de caractères :
string s = "J'aime mon prof";
Console.WriteLine(s);
s = s.ToUpper();
Console.WriteLine(s);
//for(int i = 0; i != s.Length; ++i)
// Console.Write($"{s[i]} ");
foreach (char c in s)
Console.Write($"{c} ");
Console.WriteLine();
char[] tab = s.ToArray();
for (int i = 0; i != tab.Length; ++i)
if (char.IsWhiteSpace(tab[i]))
tab[i] = '#';
// notez la différence entre les deux conversions ci-dessous...
s = new string(tab);
Console.WriteLine(s);
s = tab.ToString();
Console.WriteLine(s);
Le code proposé aujourd'hui était ce qui suit, ou une variante de ce
qui suit (avec quelques menus ajouts) :
- Exemple de division entière avec levée d'exception :
bool ok = false;
do
{
try
{
Console.Write("Numérateur ? ");
int num = int.Parse(Console.ReadLine());
Console.Write("Dénominateur? ");
int denom = int.Parse(Console.ReadLine());
Console.WriteLine($"{num} / {denom} == {DivisionEntière(num, denom)}");
ok = true;
}
catch(FormatException)
{
Console.WriteLine("Oups, je refuse (pas un entier); on réessaie");
}
catch (DivisionParZéroException)
{
Console.WriteLine("Oups, je refuse (dénominateur nul); on réessaie");
}
}
while (!ok);
// précondition : dénominateur != 0
// postcondition : retourne le quotient du numérateur par le dénominateur
// exception : la fonction ne peut pas satisfaire ses postconditions
// (en gros, elle ne peut pas faire son travail)
static int DivisionEntière(int numérateur, int dénominateur)
{
if(dénominateur == 0)
{
throw new DivisionParZéroException();
}
return numérateur / dénominateur;
}
class DivisionParZéroException : Exception { }
const int N = 300_000_000;
System.Diagnostics.Stopwatch sw = new();
int[] tab = CréerTableau(N);
sw.Start();
int combien = CompterImpairsPositifs(tab);
sw.Stop();
Console.WriteLine($"Compté {combien} impairs positifs et {sw.ElapsedMilliseconds} ms");
static bool EstImpairPositif(int n)
{
if (n < 0)
throw new ZutException();
return n % 2 != 0;
}
static int CompterImpairsPositifs(int[] tab)
{
int n = 0;
foreach (int nb in tab)
{
try
{
if (EstImpairPositif(nb))
++n;
}
catch (ZutException)
{
}
}
return n;
}
static int[] CréerTableau(int n)
{
int[] tab = new int[n];
for (int i = 0; i != n; ++i)
tab[i] = i % 10 == 0 ? -(2 * i + 1) : 2 * i + 1;
return tab;
}
class ZutException : Exception;
- Mesurer le coût en temps d'ajout naïf de caractères à une
string
(rappel : en
C#, les instances de
string
sont immuables, ce qui fait
que l'expression s += c est équivalente à s = new string(...texte de
s + caractère c...) ce qui crée à chaque itération une
string
plus
grande que la précédente... Ouf!
System.Diagnostics.Stopwatch sw = new();
const int N = 10_000_000;
for(int i = 1; i <= N; i *= 10)
{
sw.Start();
string s = CréerChaîne(i, 'S');
sw.Stop();
Console.WriteLine($"Créé string de {s.Length} caractères en {sw.ElapsedMilliseconds} ms");
}
static string CréerChaîne(int n, char c)
{
string s = "";
for(int i = 0; i != n; ++i)
s += c;
return s;
}
|
|
5 février |
S04 |
Au menu :
- Minitest Q00
- Dernière séance pour fignoler le labo 00. C'est le moment idéal
pour :
- Vous assurer de respecter les consignes (relisez-les avec
attention!)
- Tester les cas limites (p. ex. : notre
Crypteur rejette les chaînes vides, mais il reste que chiffrer une chaîne vide n'est pas
une erreur... Ça donne simplement une autre chaîne vide alors faudrait
que votre algorithme tienne la route!)
- Si vous ne l'avez pas encore fait, vous pouvez essayer les tests
proposés dans l'énoncé... Si vous ne
passez pas tous les tests, c'est qu'il vous reste encore au moins un
bogue (et si vous passez tous les tests, ça ne veut pas dire que le code
est propre! 🙂)
Vous pouvez aussi essayer le programme de test suivant (ajoutez le
using requis pour que votre Crypteur soit accessible) :
/// Programme client de tests pour le laboratoire 00
/// classe Crypteur - Hiver 2024
///
/// par Vincent Echelard, 2013
/// révisé et amélioré par Pierre Prud'homme, février 2022
/// Ajusté et mis à jour par Patrice Roy, février 2023
/// --------------------------------------------------------------------
using System;
Console.SetWindowSize(Console.LargestWindowWidth, Console.LargestWindowHeight);
Console.SetWindowPosition(0,0);
Console.BackgroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.Clear();
const int NB_TESTS = 10;
Console.Write("Nom de l'étudiant(e) : ");
string s = Console.ReadLine();
Console.WriteLine(new string('-', 72));
int résultatTests = 0;
résultatTests += Tester("ALLO", 2, "CNNQ", "Test simple (majuscules)");
résultatTests += Tester("Allo", 2, "CNNQ", "Test simple (majuscule/minuscules)");
résultatTests += Tester("allo", 2, "CNNQ", "Test simple (minuscules)");
résultatTests += Tester("allo", -2, "CNNQ", "Test clef négative (chiffrement)");
résultatTests += Tester("ALLO", 1328, "CNNQ", "Test clef très grande (chiffrement)");
résultatTests += Tester("Veux-tu 1 café?", 2, "XGWZ-VW 1 ECHÉ?", "Test caractères non-alphabétique (chiffrement)");
// oui, il y a une faute dans la phrase suivante, je sais
résultatTests += Tester("Zorglub veux 1 café!", -1332, "FUXMRAH BKAD 1 IGLÉ!", "Test complet (chiffrement)");
résultatTests += Tester("Élu par cette crapule", 27, "ÉMV QBS DFUUF DSBQVMF", "Test décalage +1");
résultatTests += Tester("Élu par cette crapule", 26, "ÉLU PAR CETTE CRAPULE", "Test de chiffrement sans effet");
résultatTests += Tester("élu par cette crapule", 25, "ÉKT OZQ BDSSD BQZOTKD", "Test décalage -1");
Console.Write($"A eu : {résultatTests} / {NB_TESTS}");
Console.ReadKey();
static int Tester(string messageÀCrypter, int clefDeChiffrement,
string messageCrypté, string nomDuTest)
{
string espacement = new string(' ', 13);
int résultat = 0;
Crypteur objTest = null;
try
{
objTest = new Crypteur(messageÀCrypter, clefDeChiffrement);
if (objTest.MessageSecret == messageCrypté)
{
résultat = 1;
Console.WriteLine($"Réussite --> {nomDuTest} " +
$"Msg à crypter: {messageÀCrypter} - " +
$"Clef: {clefDeChiffrement} " + Environment.NewLine +
$"{espacement}Prévu:{messageCrypté} - Obtenu:{objTest.MessageSecret} " + Environment.NewLine);
}
else
Console.WriteLine($"ÉCHEC --> {nomDuTest} " +
$"Msg à crypter: {messageÀCrypter} - " +
$"Clef: {clefDeChiffrement} " + Environment.NewLine +
$"{espacement}Prévu:{messageCrypté} - Obtenu:{objTest.MessageSecret} " + Environment.NewLine);
}
catch (Exception e)
{
Console.WriteLine($"Erreur lors du test " + nomDuTest + " : " + e.Message);
if (objTest != null)
Console.WriteLine($"Msg à crypter : {messageÀCrypter} - Clef : {clefDeChiffrement} -- Msg obtenu : {objTest.MessageSecret}");
}
return résultat;
}
Pour le reste : des exercices, des exercices, et des exercices!
Exercice 1a
Soit une classe représentant un point de l'espace 2D. Chaque instance de cette classe :
- Comporte deux attributs « réels » : un x
et un y
- Donne accès en lecture aux valeurs de x et y
par le moyen d'une propriété X et Y
respectivement
- Donne accès en écriture aux valeurs de x et de y
par le moyen des propriétés X et Y
sans les valider puisque toute valeur est acceptable
- Comporte un constructeur paramétrique qui s'assure que l'instance en création est dans un état valide
- Comporte un constructeur par défaut
- Offre une fonction pour calculer la distance entre deux points
Travail à réaliser :
- En précisant leur qualificateur d'accès, et en précisant s'il s'agit
de membres d'instance (non-static) ou de
membres de classe (static) :
- Dressez la liste des attributs de cette classe
- Dressez la liste des méthodes de cette classe
- Dressez la liste des propriétés de cette classe
- Rédigez cette classe et testez-la à l'aide du
code client disponible sur le site Web du cours.
Exercice 1b
Soit une classe représentant un point du quadrant
1 de l'espace 2D. Chaque instance de cette classe :
- Comporte deux attributs « réels » : un x
et un y
- Donne accès en lecture aux valeurs de x et y
par le moyen d'une propriété X et Y
respectivement
- Donne accès en écriture aux valeurs de x et de y
par le moyen des propriétés X et Y
en validant que la valeur est valide pour le quadrant
1
- Comporte un constructeur paramétrique qui s'assure que l'instance en création est dans un état valide
En cas d'erreur du programme client dans l'utilisation d'une instance de cette classe, la classe doit
lever une exception :
- Dans le cas où le code client tenterait de mettre une valeur négative en X, votre classe devra lever
une exception de type CoordonnéeXInvalideException
- Dans le cas où le code client tenterait de mettre une valeur négative en Y, votre classe devra lever
une exception de type CoordonnéeYInvalideException
Travail à réaliser :
- En précisant leur qualificateur d'accès, et en précisant s'il s'agit
de membres d'instance (non-static) ou de
membres de classe (static) :
- Dressez la liste des attributs de cette classe
- Dressez la liste des méthodes de cette classe
- Dressez la liste des propriétés de cette classe
- Rédigez cette classe et testez-la à l'aide du
code client disponible sur le site Web du cours.
Exercice 2
Soit une classe représentant un compte bancaire (très simple). Les instances de cette classe devront offrir les services suivants :
- Une propriété Solde permettant d'en connaître le solde
- Une méthode Déposer permettant d'effectuer un dépôt
- Une méthode Retirer permettant d'effectuer un retrait
- Un constructeur par défaut
- Un constructeur paramétrique permettant de préciser le solde initial du compte
Vous devez faire en sorte que par défaut, ce compte bancaire soit créé avec un solde à zéro; si le constructeur paramétrique est utilisé, la valeur précisée lors du processus d'instanciation doit être positive ou égale à 0.
Les méthodes permettant d'effectuer les dépôts et les retraits recevront en paramètre le montant à déposer ou à retirer selon le cas, qui doit être un entier positif strictement plus grand que 0, et ne retourneront rien.
Ce type de compte bancaire a un
invariant : il ne pourra à aucun moment avoir un solde négatif. En vertu de l'encapsulation, vous devez vous assurer du respect de cet
invariant, donc faire en sorte qu'en tout temps votre objet reste valide.
Dans le cas où le code client tenterait de créer une instance en y attribuant un solde négatif, votre classe devra lever
une exception
de type SoldeInvalideException.
Dans le cas où le code client tenterait de retirer un montant supérieur au solde du compte, ou encore de déposer un montant négatif ou nul, votre objet devra évidemment refuser l'opération et lever
une exception
de type OpérationInvalideException.
Travail à réaliser :
- En précisant leur qualificateur d'accès, et en précisant s'il s'agit
de membres d'instance (non-static) ou de
membres de classe (static) :
- Dressez la liste des attributs de cette classe
- Dressez la liste des méthodes de cette classe
- Dressez la liste des propriétés de cette classe
- Rédigez cette classe et testez-la.
Une solution possible serait ceci :
ClasseCompteBancaire.html
Exercice 3
Plus difficile, car moins directif. Vous développez
un jeu impliquant des héros et des monstres. Les règles sont les
suivantes :
- Tout héros a des points de vie, représentés par un nombre entier
- Tout héros a un nom
- Tout monstre a des points de vie, représentés par un nombre entier
- Tout monstre a un nom
- Le nom d'un monstre doit être d'une longueur maximale de cinq
caractères, et ne doit contenir que des consonnes
- À la construction, un héros a un nombre de points de vie choisi
aléatoirement entre 50 et 100
inclusivement
- À la construction, un monstre a un nombre de points de vie indiqué
par un paramètre passé au constructeur
- Tout héros a une force, un entier dont la valeur est indiquée par
un paramètre passé à la construction. Cette valeur doit être entre 10
et 20 inclusivement
- Un héros peut frapper un autre personnage. Ceci blessera ce
personnage en réduisant ses points de vie par une valeur
pseudoaléatoire entre et de la force du héros
- Tout monstre a une force, un entier dont la valeur est indiquée par
un paramètre passé à la construction. Cette valeur doit être entre 15
et 25 inclusivement
- Un monstre peut frapper un autre personnage. Ceci blessera ce
personnage en réduisant ses points de vie par une valeur
pseudoaléatoire entre et de la force du monstre
- Un héros est un personnage
- Un monstre est un personnage
- Si les points de vie d'un personnage sont inférieurs ou égaux à
zéro, alors ce personnage est mort, sinon il est vivant
Travail à réaliser :
- Rédigez les classes Héros et Monstre
- Identifiez ce qu'elles ont en commun et placez ces attributs,
propriétés et méthodes dans une classe Personnage
qui leur servira
toutes deux de parent
- Identifiez les attributs, propriétés, méthodes et constructeurs de
chacune des classes que vous envisagez
- Écrivez un petit programme de test représentant un combat entre un
Héros et un Monstre et faisant la démonstration que votre design
fonctionne, et respecte les consignes
Exercice 4
Sachant que la couleur de l'affichage de texte dans un écran console
est donnée par la propriété Console.ForegroundColor, et sachant qu'il
est possible de positionner le curseur où l'affichage de texte se fera à
l'aide de la méthode Console.SetCursorPosition, écrivez une classe
Carré telle que :
- Un Carré a une position décrite par un
point 2D
- Note : à l'écran console, le point est le coin en haut et à
gauche de l'écran, et les coordonnées en et en sont positives vers
la droite et vers le bas respectivement
- Un Carré a une longueur de côté
- Un Carré a une couleur, de type
ConsoleColor
- Les caractéristiques d'une instance de Carré
sont déterminées à la construction de cet objet
- Dessiner un Carré dessinera ce carré à
la position choisie, de la taille choisie, et à la couleur choisie
Travail à réaliser :
- Rédigez la classe Carré
- Identifiez les attributs, propriétés, méthodes et constructeurs de
cette classe
- Écrivez un programme de test dans lequel on trouvera un tableau de
références sur des Carré et qui, en itérant
à travers ce tableau, affichera ces divers objets à l'écran
|
|
9 février |
S05 |
Au menu :
- Retour sur le minitest
Q00
- Quelques exercices choisis de
S04
- Nous avons investi principalement du temps sur l'exercice
3 (une variante du code vu en classe est disponible
ici; notez qu'elle ne comprend pas la méthode
Frapper, pas plus que le code de test)
- L'héritage d'implémentation (dans le respect des limites de
C#), effleuré
seulement, par lequel une classe peut être une spécialisation d'une
autre
- Relation entre parent et enfant
- Impact des qualifications private et
public
- Héritage et construction
- Le mot clé base (introduction)
N'oubliez pas de remettre la version imprimée du labo 00 – Le
cryptographe au début du cours.
|
|
12 février |
S06 |
Au menu :
- Finir l'exercice 3 de
S04
- Le problème de la méthode Frapper
- Introduction des mots virtual,
override et abstract
- Qualification protected
- Petite parenthèse technique sur la différence entre la version
préfixée (++i) et la version suffixée (i++)
de l'opérateur ++, important quand on
utilise cet opérateur dans une expression plus complexe
- Faire l'exercice 4 de
S04
Pour vous faire pratiquer...
Vous trouverez un solutionnaire possible pour l'exercice 3
à cet endroit, mais j'ai fait les choses un peu différemment cette
fois (nous comparerons les approches
au prochain cours), alors voici grosso modo ce que
nous avons fait aujourd'hui...
Première étape
Pour la première version, quand Personnage
n'exposait pas de
méthode Frapper...
Classe Personnage
using System;
namespace z
{
internal abstract class Personnage
{
public bool EstVivant => Vie > 0;
public bool EstMort => !EstVivant;
//public bool EstVivant
//{
// get => Vie > 0;
//}
//public bool EstMort
//{
// get => !EstVivant;
//}
public int Vie { get; private set; }
public string Nom { get; private init; }
public Personnage(string nom, int vie)
{
Nom = nom;
Vie = vie;
}
public void Blesser(int dégâts)
{
Vie -= dégâts; // valider que dégâts >= 0 ? :)
}
}
}
Classe Héros
using System;
namespace z
{
class ForceInvalideException : Exception;
internal class Héros : Personnage
{
const int FORCE_MIN = 10,
FORCE_MAX = 20;
static bool EstForceValide(int candidate) =>
Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
int force;
public int Force
{
get => force;
private init
{
force = EstForceValide(value) ?
value : throw new ForceInvalideException();
}
}
static int GénérerVieInitiale() =>
new Random().Next(50, 100 + 1); // bof...
public Héros(string nom, int force)
: base(nom, GénérerVieInitiale())
{
Force = force;
}
public void Frapper(Personnage autre)
{
int dégâts = (int) (Force * (new Random().Next(50, 100 + 1) / 100.0));
autre.Blesser(dégâts);
}
}
}
Classe Monstre
using System;
namespace z
{
internal class Monstre : Personnage
{
static bool EstNomValide(string candidat) =>
candidat.Length <= 5 &&
Algos.ContientSeulementConsonnes(candidat);
static string ValiderNom(string candidat) =>
EstNomValide(candidat) ?
candidat : throw new NomInvalideException();
const int FORCE_MIN = 15,
FORCE_MAX = 25;
static bool EstForceValide(int candidate) =>
Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
int force;
public int Force
{
get => force;
private init
{
force = EstForceValide(value) ?
value : throw new ForceInvalideException();
}
}
public Monstre(string nom, int force, int vie)
: base(ValiderNom(nom), vie)
{
Force = force;
}
public void Frapper(Personnage autre)
{
int dégâts = (int)(Force * (new Random().Next(50, 75 + 1) / 100.0));
autre.Blesser(dégâts);
}
}
}
Classe Algos
using System;
namespace z
{
internal static class Algos
{
public static bool EstDans(char c, char[] tab)
{
foreach(char ch in tab)
if(ch == c)
return true;
return false;
}
public static bool EstVoyelle(char c)
{
char[] voyelles = { 'A', 'E', 'I', 'O', 'U', 'Y' };
return EstDans(char.ToUpper(c), voyelles);
}
public static bool EstEntreInclusif(int val, int min, int max) =>
min <= val && val <= max;
public static bool EstLettre(char c) =>
EstEntreInclusif(char.ToUpper(c), 'A', 'Z');
public static bool ContientSeulementConsonnes(string s)
{
foreach (char c in s)
if (!EstLettre(c) || EstVoyelle(c))
return false;
return true;
}
}
}
Programme principal
using System;
using z;
Héros héros = new("Robert", 11);
Monstre monstre = new("GRR", 20, 80);
bool tourAuHéros = new Random().Next() % 2 == 0;
while (héros.EstVivant && monstre.EstVivant)
{
Console.WriteLine($"Avant : {héros.Nom} a {héros.Vie} vie et {monstre.Nom} a {monstre.Vie} vie");
if (tourAuHéros)
héros.Frapper(monstre);
else
monstre.Frapper(héros);
Console.WriteLine($"Après : {héros.Nom} a {héros.Vie} vie et {monstre.Nom} a {monstre.Vie} vie");
Console.WriteLine(new string('-', 70));
tourAuHéros = !tourAuHéros;
}
if (héros.EstVivant)
Console.WriteLine($"Victoire de {héros.Nom}!");
else
Console.WriteLine($"Victoire de {monstre.Nom}!");
Deuxième étape
Pour la première version, quand Personnage
exposait une méthode abstraite Frapper...
(nous avons fait une étape intermédiaire avec une méthode virtuelle
Frapper aussi, mais elle était presque identique)
Classe Personnage
using System;
namespace z
{
internal abstract class Personnage
{
public bool EstVivant => Vie > 0;
public bool EstMort => !EstVivant;
//public bool EstVivant
//{
// get => Vie > 0;
//}
//public bool EstMort
//{
// get => !EstVivant;
//}
public int Vie { get; private set; }
public string Nom { get; private init; }
public Personnage(string nom, int vie)
{
Nom = nom;
Vie = vie;
}
public void Blesser(int dégâts)
{
Vie -= dégâts; // valider que dégâts >= 0 ? :)
}
// virtual : la méthode _peut_ être spécialisée par les enfants
// abstract : la méthode _doit_ être spécialisée par les enfants
public abstract void Frapper(Personnage autre);
//public virtual void Frapper(Personnage autre)
//{
// // quoi faire ici? Mystère...
//}
}
}
Classe Héros
using System;
namespace z
{
class ForceInvalideException : Exception;
internal class Héros : Personnage
{
const int FORCE_MIN = 10,
FORCE_MAX = 20;
static bool EstForceValide(int candidate) =>
Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
int force;
public int Force
{
get => force;
private init
{
force = EstForceValide(value) ?
value : throw new ForceInvalideException();
}
}
static int GénérerVieInitiale() =>
new Random().Next(50, 100 + 1); // bof...
public Héros(string nom, int force)
: base(nom, GénérerVieInitiale())
{
Force = force;
}
public override void Frapper(Personnage autre)
{
int dégâts = (int) (Force * (new Random().Next(50, 100 + 1) / 100.0));
autre.Blesser(dégâts);
}
}
}
Classe Monstre
using System;
namespace z
{
internal class Monstre : Personnage
{
static bool EstNomValide(string candidat) =>
candidat.Length <= 5 &&
Algos.ContientSeulementConsonnes(candidat);
static string ValiderNom(string candidat) =>
EstNomValide(candidat) ?
candidat : throw new NomInvalideException();
const int FORCE_MIN = 15,
FORCE_MAX = 25;
static bool EstForceValide(int candidate) =>
Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
int force;
public int Force
{
get => force;
private init
{
force = EstForceValide(value) ?
value : throw new ForceInvalideException();
}
}
public Monstre(string nom, int force, int vie)
: base(ValiderNom(nom), vie)
{
Force = force;
}
// override : je choisis de spécialiser cette méthode
public override void Frapper(Personnage autre)
{
int dégâts = (int)(Force * (new Random().Next(50, 75 + 1) / 100.0));
autre.Blesser(dégâts);
}
}
}
Classe Algos
using System;
namespace z
{
internal static class Algos
{
public static bool EstDans(char c, char[] tab)
{
foreach(char ch in tab)
if(ch == c)
return true;
return false;
}
public static bool EstVoyelle(char c)
{
char[] voyelles = { 'A', 'E', 'I', 'O', 'U', 'Y' };
return EstDans(char.ToUpper(c), voyelles);
}
public static bool EstEntreInclusif(int val, int min, int max) =>
min <= val && val <= max;
public static bool EstLettre(char c) =>
EstEntreInclusif(char.ToUpper(c), 'A', 'Z');
public static bool ContientSeulementConsonnes(string s)
{
foreach (char c in s)
if (!EstLettre(c) || EstVoyelle(c))
return false;
return true;
}
}
}
Programme principal
using System;
using z;
Personnage[] persos = new Personnage[]
{
new Héros("Robert", 11),
new Monstre("GRR", 20, 80),
new Héros("Roberta", 19)
// new Personnage("Joe le misérable", 100) // non car Personnage est abstraite
};
//Héros héros = new("Robert", 11);
//Monstre monstre = new("GRR", 20, 80);
int tour = 0;
while (CompterVivants(persos) > 1)
{
if (persos[tour].EstVivant)
{
Console.Write($"{persos[tour].Nom} va frapper sur... ");
int cible = TrouverCiblePour(tour, persos);
Console.WriteLine($" {persos[cible].Nom} qui a {persos[cible].Vie} vie");
persos[tour].Frapper(persos[cible]);
Console.WriteLine($"PAF! {persos[cible].Nom} a maintenant {persos[cible].Vie} vie");
Console.WriteLine(new string('-', 70));
}
tour = (tour + 1) % persos.Length;
}
Console.WriteLine("Combat terminé. État des forces :");
foreach (Personnage p in persos)
Console.WriteLine($"\t{p.Nom} a {p.Vie} vie");
// précondition : persos[agresseur].EstVivant
// précondition : CompterVivants(persos) > 1
static int TrouverCiblePour(int agresseur, Personnage[] persos)
{
int nbPossibilités = CompterVivants(persos) - 1;
int[] ciblesPotentielles = new int[nbPossibilités];
int n = 0;
for (int i = 0; i != persos.Length; ++i)
if (i != agresseur && persos[i].EstVivant)
ciblesPotentielles[n++] = i;
return ciblesPotentielles[new Random().Next(0, ciblesPotentielles.Length)];
}
static int CompterVivants(Personnage[] persos)
{
int n = 0;
foreach (Personnage p in persos)
if (p.EstVivant)
++n;
return n;
}
//
// nous avions examiné cette option initialement mais elle ne nous convenait pas;
// la fonction reste valide, alors gardons-la juste au cas
//
static bool SontTousVivants(Personnage[] persos)
{
foreach (Personnage p in persos)
if (p.EstMort)
return false;
return true;
}
Nous avons aussi fait autre chose, incluant cet exemple avec méthode
virtuelle...
Exemple avec Jim, Joe
et Bob
using System;
Perso[] persos = new Perso[]
{
new Jim("N'Guyen"),
new Joe("Hébert"),
new Bob("Stratford")
};
foreach (Perso p in persos)
p.Présenter();
class Perso
{
public string Nom { get; private init; }
public Perso(string nom)
{
Nom = nom;
}
public virtual void Présenter()
{
Console.WriteLine($"Bonjour, mon nom est {Nom}");
}
}
class Jim : Perso
{
public Jim(string nom) : base(nom)
{
}
public override void Présenter()
{
Console.WriteLine($"Mon nom est Jim {Nom}");
}
}
class Joe : Perso
{
public Joe(string nom) : base(nom)
{
}
public override void Présenter()
{
Console.WriteLine($"Ouain, chu Joe {Nom}");
}
}
class Bob : Perso
{
public Bob(string nom) : base(nom)
{
}
public override void Présenter()
{
Console.WriteLine($"Mon nom est {Nom}, BOB {Nom}");
}
}
Vous trouverez un solutionnaire possible pour l'exercice
4
à cet endroit, mais j'ai fait les choses un peu différemment aujourd'hui
car j'étais un peu pressé (nous comparerons les approches
au prochain cours), alors voici grosso modo ce que
nous avons fait pour le moment...
Classe Point2D
using System;
namespace z;
class Point2D
{
public int X { get; init; }
public int Y { get; init; }
public Point2D(int x, int y)
{
X = x;
Y = y;
}
public Point2D() : this(0, 0) { }
}
Classe Carré
using System;
namespace z;
class Carré
{
Point2D Position { get; init; }
ConsoleColor Couleur { get; init; }
int Côté { get; init; }
public Carré(Point2D pos, ConsoleColor couleur, int côté)
{
Position = pos;
Couleur = couleur;
Côté = côté;
}
public void Dessiner()
{
ConsoleColor avant = Console.ForegroundColor;
Console.ForegroundColor = Couleur;
for(int i = 0; i != Côté; ++i) // ligne
for(int j = 0; j != Côté; ++j) // colonne
{
Console.SetCursorPosition(j + Position.X, i + Position.Y);
Console.Write('#');
}
Console.ForegroundColor = avant;
}
}
Programme principal
using System;
using z;
Carré[] carrés =
{
new(new(5, 3), ConsoleColor.Cyan, 5),
new(new(47, 15), ConsoleColor.Red, 6),
new(new(40, 18), ConsoleColor.Green, 10),
};
foreach(Carré c in carrés)
c.Dessiner();
J'ai ensuite ajouté une classe Rectangle,
vous m'avez recommandé une classe parent Forme,
mais nous nous sommes arrêté(e)s avant d'aller au fond des choses.
Entre autres, nous avons soulevé la question de savoir si un
Carré est un Rectangle... On s'en
reparle!
- Petit quiz de vocabulaire. Soit le code ci-dessous :
using System;
// ... programme principal (omis par simplicité)
class ContientBlancsException : Exception;
class MotVideException : Exception;
class Mot
{
private string valeur;
public string Valeur
{
get => valeur;
private set
{
if (value == null || value.Length == 0)
throw new MotVideException();
if (Contient(value.ToCharArray(), ' '))
throw new ContientBlancsException();
valeur = value.ToLower();
}
}
public int Longueur => Valeur.Length;
public Mot(string valeur)
{
Valeur = valeur;
}
private static char[] ObtenirVoyelles() => new char[] { 'a', 'e', 'i', 'o', 'u', 'y' };
public int NbVoyelles => CompterOccurrences(Valeur, ObtenirVoyelles());
public int NbConsonnes => Longueur - NbVoyelles;
private static bool Contient(char [] cs, char c)
{
for(int i = 0; i != cs.Length; ++i)
if (cs[i] == c)
return true;
return false;
}
private static int CompterOccurrences(string chaîne, char [] caractères)
{
int n = 0;
foreach(char c in chaîne)
if (Contient(caractères, c))
++n;
return n;
}
private static int TrouverPremièreDifférence(string s0, string s1)
{
int plusPetit = Math.Min(s0.Length, s1.Length);
int i = 0;
while (i != plusPetit && s0[i] == s1[i])
{
++i;
}
return i;
}
public bool Précède(Mot autre)
{
int pos = TrouverPremièreDifférence(Valeur, autre.Valeur);
return pos == Math.Min(Longueur, autre.Longueur) ? Longueur < autre.Longueur : Valeur[pos] < autre.Valeur[pos];
}
}
Répondez aux questions suivantes :
- Que représente une instance de la classe Mot?
Soyez aussi précise ou précis que possible
- Quelles sont les règles qui assurent la validité d'un
Mot? Soyez aussi précise ou précis que possible
- Quelles sont les méthodes d'instance de la classe
Mot? (listez leurs noms seulement)
- Quelles sont les méthodes de classe de la classe
Mot? (listez leurs noms seulement)
- Quelles sont les propriétés d'une instance de la classe
Mot? (listez leurs noms seulement)
- Quels sont les attributs d'une instance de la classe
Mot? (listez leurs noms seulement)
- Dans un programme principal de votre cru, créez deux instances
m0 et m1 de Mot
avec des chaînes de caractères valides et distinctes l'une de
l'autre, puis appelez la méthode Précède
correctement pour ensuite afficher laquelle de ces deux instances de
Mot apparaîtrait en premier dans un dictionnaire
- Est-ce que NbConsonnes donnerait la bonne
valeur pour un Mot créé de la manière
suivante : new Mot("plate-forme")? Expliquez
votre réponse
- Est-ce que NbVoyelles donnerait la bonne
valeur pour un Mot créé de la manière
suivante : new Mot("Yogourt")? Expliquez
votre réponse
|
|
16 février |
S07 |
Au menu :
- On fait le laboratoire 00 ensemble
- Retour sur la (très chargée) séance
S06
- Examen du solutionnaire « officiel » de l'exercice
3 et discussion des différences avec ce que nous avons fait
- Examen du solutionnaire « officiel » de l'exercice
4 et discussion des différences avec ce que nous avons fait
- On complète notre réflexion sur l'exercice 4
de S04,
incluant l'exploration d'une hiérarchie de formes et la question qui
vous préoccupe assurément : est-ce qu'un Carré
est un Rectangle? (ou : le
principe de Liskov)
Petits éléments de vocabulaire
Héritage d'implémentation (enfant est un cas particulier du parent). Bon,
c'est pas nouveau d'aujourd'hui, mais...
Polymorphisme :
- Par une abstraction (p.ex. : le parent), on appelle le service
le plus spécialisé de l'objet pointé (p.ex. : enfant)
- virtual (l'enfant peut spécialiser la méthode)
- abstract (l'enfant doit spécialiser la
méthode sinon l'enfant est aussi abstrait). Note : une classe qui
expose au moins un service abstract doit
être qualifiée abstract
- override (l'enfant choisit de spécialiser le service)
Ensuite, sujets plus généraux :
- Parler de soi : le mot clé this
- Constructeurs de délégation
- Accès explicite aux membres d'instance
- Discussion de quelques relations entre classes
- Héritage d'implémentation
- Composition
- Agrégation
- Association
- Sens de chacun
- Quelle relation privilégier quand plusieurs sont possibles
- Idée de couplage
- Idée de cohésion
- Introduction à la
surcharge d'opérateurs
- Ébauche de classe Entier
- Ébauche (incomplète) de classe Rationnel
Pour les éléments de vocabulaire quant aux relations, informellement, ce que nous avons relevé est :
- Héritage d'implémentation : verbe être
- Un Héros est un Personnage
- Les membres publics (et protégés) du parent (Personnage)
sont accessibles pour l'enfant (Héros)
- En
C#, un enfant parle de sa partie parent avec le mot base
- On peut traiter un enfant comme un cas particulier de son parent (ici : si une fonction prend un Personnage
en paramètre, je peux lui passer un Héros)
- Quand on construit un enfant, il faut d'abord construire
la partie parent
- Composition : verbe avoir
- Un objet contient d'autres objets
- Une voiture contient un moteur (mettons)
- Un objet est composé d'autres objets
- L'objet contenu a une vie délimitée par l'objet qui le contient
- Agrégation : verbe avoir / utiliser
- Un peu comme la composition
- L'objet « contenu » a une vie qui peut chevaucher celle de
l'objet qui le « contient »
- Pensez à des pneus dans une voiture
- Association :
- Deux objets se connaissent, et peuvent se parler
- On veut : faible couplage, forte cohésion
- Quand on a des choix, on vise les relations au plus faible
couplage possible
- Couplage des relations ci-dessus, du plus fort au plus faible :
- Héritage d'implémentation
- Agrégation / composition
- Association
- Couplage
- Essayer d'isoler les changements pour qu'ils aient un impact le plus local possible
- Plus les trucs sont locaux, mieux c'est (p.ex. : variable locale)
- Plus les trucs sont privés, mieux c'est
- Cohésion
- « Les trucs qui vont ensemble... vont ensemble »
|
|
19 février |
S08 |
Au menu :
- Minitest Q01
- À l'aide de notre ébauche de la classe
Rationnel :
- Surcharge des opérateurs de conversion (implicite ou
explicite)
- Surcharge de operator== et
traitement des références null (choisir entre
is ou ==)
- Petit tour d'horizon de la classe
string
- Comparatif de vitesse avec
StringBuilder
- Quand utiliser
string et
quand utiliser
StringBuilder
- Présentation du Labo 01
Algorithme du PGCD :
namespace z
{
static class Algos
{
// ...
// https://fr.wikipedia.org/wiki/Algorithme_d%27Euclide
public static int PGCD(int a, int b)
{
while(b != 0)
{
int temp = b;
b = a % b;
a = temp;
}
return a;
}
// ...
}
}
Classe Rationnel vue en classe :
class Rationnel
{
public int Numérateur { get; private init; }
private int dénominateur;
public int Dénominateur
{
get => dénominateur;
private init
{
if (value == 0)
throw new ArgumentOutOfRangeException("Dénominateur nul");
dénominateur = value;
}
}
public Rationnel() : this(0) { }
public Rationnel(int entier)
{
Numérateur = entier;
Dénominateur = 1;
}
public Rationnel(int num, int dénom)
{
// On inverse les signes du numérateur et dénominateur si
// le dénominateur est négatif
if (dénom < 0)
{
num = -num;
dénom = -dénom;
}
int pgcd = Math.Abs(Algos.PGCD(num, dénom)); // vu en classe
Numérateur = num / pgcd;
Dénominateur = dénom / pgcd;
}
public override string ToString()
{
string résultat;
if (Numérateur == 0)
résultat = "0";
else if (Dénominateur == 1)
résultat = Numérateur.ToString(); // ou $"{Numérateur}"
else
résultat = $"{Numérateur}/{Dénominateur}";
return résultat;
}
public Rationnel AppliquerPuissance(int puissance) =>
new((int)Math.Pow(Numérateur, puissance),
(int)Math.Pow(Dénominateur, puissance));
public static Rationnel operator +(Rationnel gauche, Rationnel droite) =>
new(gauche.Numérateur * droite.Dénominateur +
gauche.Dénominateur * droite.Numérateur,
gauche.Dénominateur * droite.Dénominateur);
public static Rationnel operator -(Rationnel r) =>
new(-r.Numérateur, r.Dénominateur);
public static Rationnel operator -(Rationnel gauche, Rationnel droite) =>
gauche + -droite;
public static Rationnel operator *(Rationnel gauche, Rationnel droite) =>
new(gauche.Numérateur * droite.Numérateur,
gauche.Dénominateur * droite.Dénominateur);
public static Rationnel operator /(Rationnel gauche, Rationnel droite)
{
// Remarquez qu'une exception serait quand même levée à la
// construction de l'objet si on omettait celle-ci. Lever une
// exception plus précise ici clarifie toutefois le message
if (droite.Numérateur == 0)
throw new DivideByZeroException();
return new(gauche.Numérateur * droite.Dénominateur,
gauche.Dénominateur * gauche.Numérateur);
}
public static bool operator ==(Rationnel gauche, Rationnel droite) =>
(object) gauche == null && (object) droite == null ||
((object) gauche != null && (object) droite != null &&
gauche.Numérateur == droite.Numérateur &&
gauche.Dénominateur == droite.Dénominateur);
//
// Alternative :
//
// public static bool operator ==(Rationnel gauche, Rationnel droite) =>
// gauche is null && droite is null ||
// (!(gauche is null) && !(droite is null) &&
// gauche.Numérateur == droite.Numérateur &&
// gauche.Dénominateur == droite.Dénominateur);
//
public static bool operator !=(Rationnel gauche, Rationnel droite) =>
!(gauche == droite);
public static implicit operator Rationnel(int i) => new(i);
public static explicit operator double(Rationnel r) =>
r.Numérateur / (double)r.Dénominateur;
}
Code de test proposé pour le labo 01 :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Labo01
{
static class Tests
{
static Rationnel[] coefsVide = new Rationnel[0];
static Rationnel[] coefsNulls = new Rationnel[3];
static Rationnel[] coefJuste1 = { new(1)};
static Rationnel[] coefs0 = { new(1, 2), new(0), new(2,3) };
static Rationnel[] coefs1 = { new(1, 2), new(2), new(-1, 3) };
static Rationnel[] coefs2 = { new(2, 3), new(5, 2), new(-2, 10), new(2, 7) };
static Rationnel[] coefs3 = { new(2), new(5), new(-2), new(3) };
public static void Tests1()
{
Console.WriteLine("============= Tests1 =============");
Polynome pVide = new ();
Console.WriteLine(pVide); // Affiche 0
Console.WriteLine(pVide.ToStringÉpurée()); // Affiche 0
Polynome p0 = new (coefs0);
Console.WriteLine(p0); // Affiche 1/2 + 0x + 2/3x^2
Console.WriteLine(p0.ToStringÉpurée()); // Affiche 1/2 + 2/3x^2
Console.WriteLine(pVide.ÉvaluerAvec(2.5)); // Affiche 0
Console.WriteLine(p0.ÉvaluerAvec(2.5)); // Affiche 4,66666666
Console.WriteLine(pVide.ÉvaluerAvec(new Rationnel(1, 2))); // Affiche 0
Console.WriteLine(p0.ÉvaluerAvec(new Rationnel(1, 2))); // Affiche 2/3
//Polynome pException = new(coefsVide); // lève exception
//Polynome pException2 = new(coefsNulls); // lève exception
// N'oubliez pas d'ajouter des tests de votre cru.
}
public static void Tests2()
{
Console.WriteLine("============= Tests2 =============");
Polynome p0 = new(coefs0);
Console.WriteLine((p0 + p0).ToStringÉpurée()); // Affiche 1 + 4/3x^2
Console.WriteLine((-p0).ToStringÉpurée()); // Affiche -1/2 + -2/3x^2
Console.WriteLine((p0 - p0).ToStringÉpurée()); // Affiche 0
Polynome p2 = new(coefs2);
// La surcharge de operator+ devra fonctionner pour
// des polynomes de degrées différents pour que cette
// addition fonctionne
Console.WriteLine((p0 + p2).ToStringÉpurée()); // Affiche 7/6 + 5/2x + 7/15x^2 + 2/7x^3
Console.WriteLine(p0 == new Polynome(coefs0)); // Affiche True
Console.WriteLine(p0 == new Polynome(coefs0) + new Polynome(coefJuste1)); // Affiche False
Console.WriteLine(p0 == p2); // Affiche False
// N'oubliez pas d'ajouter des tests de votre cru.
}
public static void Tests3()
{
Console.WriteLine("============= Tests3 =============");
Polynome p3 = new(coefs3);
Console.WriteLine(p3.ToStringÉpurée()); // Affiche 2 + 5x + -2x^2 + 3x^3
Polynome p3Prime = p3.Dériver();
Console.WriteLine(p3Prime.ToStringÉpurée()); // Affiche 5 + -4x + 9x^2
Console.WriteLine(p3Prime.Dériver().ToStringÉpurée()); // Affiche -4 + 18x
Console.WriteLine(p3.Dériver(2).ToStringÉpurée()); // Le même que la ligne plus haut
// N'oubliez pas d'ajouter des tests de votre cru.
}
}
}
Le code en exemple ce matin la classe Entier
était :
Entier e0 = new(2);
Entier e1 = new(2);
Console.WriteLine($"e0 vaut {e0.Valeur}");
Console.WriteLine($"e1 vaut {e1.Valeur}");
Console.WriteLine($"e0 + e1 == {(e0 + e1).Valeur}");
Console.WriteLine($"e0 - e1 == {(e0 - e1).Valeur}");
Console.WriteLine($"-e0 == {-e0.Valeur}");
if (e0 < e1)
Console.WriteLine($"{e0.Valeur} < {e1.Valeur}");
if (e0 > e1)
Console.WriteLine($"{e0.Valeur} > {e1.Valeur}");
if (e0 <= e1)
Console.WriteLine($"{e0.Valeur} <= {e1.Valeur}");
if (e0 >= e1)
Console.WriteLine($"{e0.Valeur} >= {e1.Valeur}");
if (e0 == e1)
Console.WriteLine($"{e0.Valeur} == {e1.Valeur}");
if (e0 != e1)
Console.WriteLine($"{e0.Valeur} != {e1.Valeur}");
class Entier
{
public int Valeur { get; private init; }
public Entier() : this(0) { }
public Entier(int val)
{
Valeur = val;
}
public static Entier operator +(Entier e0, Entier e1) =>
new(e0.Valeur + e1.Valeur);
// opérateur - binaire (deux opérandes) : a - b
public static Entier operator -(Entier e0, Entier e1) =>
new(e0.Valeur - e1.Valeur);
// *, / et % laissés en exercice :)
// opérateur - unaire (un opérande) : -a
public static Entier operator -(Entier e) => new(-e.Valeur);
// relationnels d'inégalité
public static bool operator <(Entier gauche, Entier droite) =>
gauche.Valeur < droite.Valeur;
public static bool operator >(Entier gauche, Entier droite) =>
droite < gauche;
public static bool operator <=(Entier gauche, Entier droite) =>
!(droite < gauche);
public static bool operator >=(Entier gauche, Entier droite) =>
!(gauche < droite);
// opérateur d'équivalence
public static bool operator ==(Entier e0, Entier e1) =>
e0.Valeur == e1.Valeur;
public static bool operator !=(Entier e0, Entier e1) =>
!(e0 == e1);
}
L'ébauche de classe Rationnel ce matin
était comme suit (rappel : ce code est incomplet) :
class Rationnel
{
public int Numérateur { get; private init; }
int dénominateur;
public int Dénominateur
{
get => dénominateur;
private init
{
dénominateur = value == 0? throw new ArgumentException("Dénominateur nul") : value;
}
}
public Rationnel()
{
Numérateur = 0;
Dénominateur = 1;
}
public Rationnel(int num, int dénom)
{
// normalisation
if(dénom < 0)
{
num = -num;
dénom = -dénom;
}
Numérateur = num;
Dénominateur = dénom;
}
public string ConvertirEnString() => $"{Numérateur}/{Dénominateur}";
public static Rationnel operator+(Rationnel gauche, Rationnel droite)
{
if (gauche.Dénominateur == droite.Dénominateur)
return new(gauche.Numérateur + droite.Numérateur,
gauche.Dénominateur);
int dénomCommun = gauche.Dénominateur * droite.Dénominateur;
int numGauche = gauche.Numérateur * droite.Dénominateur;
int numDroite = droite.Numérateur * gauche.Dénominateur;
return new (numGauche + numDroite, dénomCommun);
}
public static Rationnel operator -(Rationnel r) =>
new(-r.Numérateur, r.Dénominateur);
public static Rationnel operator -(Rationnel r0, Rationnel r1) =>
r0 + -r1;
public static explicit operator double(Rationnel r) =>
(double) r.Numérateur / r.Dénominateur;
public static bool operator==(Rationnel gauche, Rationnel droite)
{
int dénomCommun = gauche.Dénominateur * droite.Dénominateur;
int numGauche = gauche.Numérateur * droite.Dénominateur;
int numDroite = droite.Numérateur * gauche.Dénominateur;
return numGauche == numDroite;
}
public static bool operator !=(Rationnel r0, Rationnel r1) =>
!(r0 == r1);
}
|
|
23 février |
S09 |
Au menu : je dois m'absenter car je suis hyper malade (bronchite + sinusite + fièvre). C'est dé-gueu-lasse
|
|
26 février |
S10 |
Je devrai m'absenter aujourd'hui parce que je dois passer la journée à
l'hôpital avec un des enfants. Profitez-en pour faire avancer vos travaux
dans les autres cours!
|
|
2 mars |
|
Jour de mise à niveau (cours suspendus)
|
|
5 mars |
|
Jour de mise à niveau (cours suspendus)
|
|
9 mars |
S11 |
Au menu, on se revoit enfin! Je me suis ennuyé de vous! Nous allons
faire :
- Retour sur le minitest 01
- Questions sur le labo 01 – Polynômes et leur dérivation
- Classes statiques et leur rôle
- Comprendre un petit allègement syntaxique sympathique (ou : ce
qu'on fait parfois mécaniquement sans le comprendre...)
- Premier coup d'oeil sur une classe très utile : la classe List<T>
qui
modélise un tableau dynamique
- Petit exemple avec un List<T> où
T est int :
- Créer une List<int> vide
- Ajouter des valeurs dans une List<int>
- Créer une List<int> avec des valeurs
initiales
- Parcourir une List<int> (for, foreach)
- Trier une List<int>
- Petite introduction à l'héritage d'interfaces
- Petite introduction à l'idiome NVI
Quelques exemples couverts dans ce cours :
- Petite introduction au polymorphisme par voie d'héritage
d'interfaces (version simplifiée) :
OiseauVolant fred = new();
Décoller(fred);
static void Décoller(IVolant v)
{
v.Voler();
}
interface IVolant
{
void Voler();
}
class Animal
{
// ...
}
class Oiseau : Animal
{
// ...
}
class Insecte : Animal
{
// ...
}
class OiseauVolant : Oiseau, IVolant
{
public void Voler()
{
Console.WriteLine("Cui cui je vole");
}
// ...
}
class OiseauNonVolant : Oiseau
{
// ...
}
class InsecteVolant : Insecte, IVolant
{
public void Voler()
{
Console.WriteLine("Bzzz bzzz je vole");
}
// ...
}
class InsecteNonVolant : Insecte
{
// ...
}
- Introduction aux classes abstraites sur la base d'une version
retouché de la hiérarchie de classes prenant Forme
pour racine (avec
introduction à l'idiome NVI, en prime)
et qui dessine de chics formes colorées. C'est pas tout à fait ce
qu'on a fait en classe mais ça y ressemble :
// Forme frm = new(ConsoleColor.Magenta); // <-- serait illégal
// frm.Dessiner(); // ... car ceci est abstrait
List<Forme> formes = new()
{
new Rectangle(ConsoleColor.Red, 10, 5, '/'),
new Triangle(ConsoleColor.Yellow, 6, 'A'),
new Carré(ConsoleColor.Blue, 7, '#')
};
foreach (Forme f in formes)
f.Dessiner();
abstract class Forme
{
public ConsoleColor Couleur { get; init; }
public Forme(ConsoleColor couleur)
{
Couleur = couleur;
}
// idiome NVI : non-virtual interface
public void Dessiner()
{
// ce qui doit être fait avant le dessin
ConsoleColor avant = Console.ForegroundColor;
Console.ForegroundColor = Couleur;
// le dessin en soi
DessinerImpl();
// ce qui doit être fait après le dessin
Console.ForegroundColor = avant;
}
// abstract : l'enfant _doit_ spécialiser
protected abstract void DessinerImpl();
}
class Rectangle : Forme
{
public int Hauteur { get; private init; }
public int Largeur { get; private init; }
public char Symbole { get; private init; }
public int Périmètre => 2 * Hauteur + 2 * Largeur;
public int Aire => Hauteur * Largeur;
public Rectangle(ConsoleColor couleur, int hau, int lar, char sym)
: base(couleur)
{
Hauteur = hau;
Largeur = lar;
Symbole = sym;
}
protected override void DessinerImpl()
{
for (int ligne = 0; ligne != hauteur; ++ligne)
{
for (int col = 0; col != largeur; ++col)
Console.Write(symbole);
Console.WriteLine();
}
}
}
class Carré : Forme
{
Rectangle Impl { get; init; }
public Carré(ConsoleColor couleur, int côté, char symbole)
: base(couleur)
{
Impl = new(couleur, côté, côté, symbole);
}
public int Largeur => Impl.Largeur;
public int Hauteur => Impl.Hauteur;
public int Périmètre => Impl.Périmètre;
public int Aire => Impl.Aire;
protected override void DessinerImpl() => Impl.Dessiner();
}
class SymboleInvalideException : Exception { }
class HauteurInvalideException : Exception { }
class Triangle : Forme
{
int hauteur;
char symbole;
static bool EstSymboleValide(char symbole) =>
!char.IsWhiteSpace(symbole);
static bool EstHauteurValide(int val) => val > 0;
public int Hauteur
{
get => hauteur;
private init
{
hauteur = EstHauteurValide(value) ? value : throw new HauteurInvalideException();
}
}
public char Symbole
{
get => symbole;
private init
{
symbole = EstSymboleValide(value) ? value : throw new SymboleInvalideException();
}
}
public Triangle(ConsoleColor couleur, int hauteur,char symbole)
: base(couleur)
{
Hauteur = hauteur;
Symbole = symbole;
}
protected override void DessinerImpl()
{
for (int ligne = 0; ligne != hauteur; ++ligne)
{
for (int col = 0; col <= ligne; ++col)
Console.Write(symbole);
Console.WriteLine();
}
}
}
N'oubliez pas de remettre votre laboratoire 01 en version imprimée au début du cours
|
|
12 mars |
S12 |
Au menu :
-
Tri avec comparateurs
(ou : comment rendre le comparateur plus charmant)
- Qu'est-ce qu'un three-way-compare, ou fonction de comparaison
trilatérale?
- Comment l'implémenter efficacement?
- Implémenter une interface standard
- Exemple de IEquatable<T>
- Exemple de IComparable<T>
- Impact sur List<T>.Sort
- Implémenter un operator== correct
- Attention, c'est pas joli
- Enjeux de la hiérarchie imposée de .NET,
en particulier de la classe object
(alias pour System.Object)
- Présentation du labo 02 – Le
conjugueur
- Travail sur le labo 02 – Le
conjugueur
|
|
16 mars |
S13 |
Au menu :
- Introduction à la conception de
structures de
données
- Concevoir un
tableau
dynamique de int (sorte de
List<int> simplifiée)
- Si le temps le permet, concevoir une
pile de
int à l'aide de ce tableau dynamique
- Les exemples ont été mis à votre disposition :
- S'il reste du temps, travail sur le labo 02 – Le
conjugueur
|
|
19 mars |
S14 |
Au menu :
- Minitest Q02
- Un mot sur le
Turing Award 2025 (site officiel :
https://amturing.acm.org/)
- Concevoir une liste simplement chaînée d'entiers nommée
ListeEntiers
- Services
implémentés : EstVide, AjouterDébut, AjouterFin,
ValeurDébut, ValeurFin, SupprimerDébut
et – de manière temporaire – Afficher
- Implémenter une PileEntiers avec
notre ListeEntiers comme substrat
La classe ListeEntiers faite en classe ressemblait à :
using System;
using System.Collections.Generic;
namespace z
{
class ListeVideException : Exception;
internal class ListeEntiers
{
class Noeud
{
public int Valeur { get; init; }
public Noeud Succ { get; set; } = null;
public Noeud(int val)
{
Valeur = val;
}
}
Noeud Tête { get; set; } = null;
Noeud Queue { get; set; } = null;
// O(1) : complexité constante
public bool EstVide => Tête == null;
// O(1)
public void AjouterDébut(int val)
{
Noeud p = new(val);
if (EstVide)
Queue = p;
p.Succ = Tête;
Tête = p;
++Count;
}
// O(1) :)
public void AjouterFin(int val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
Queue.Succ = p;
Queue = p;
}
++Count;
}
// O(1)
public void SupprimerDébut()
{
if (EstVide)
throw new ListeVideException();
Tête = Tête.Succ;
if (EstVide)
Queue = null;
--Count;
}
public int Count { get; private set; } = 0;
// O(1)
public int ValeurPremier =>
EstVide? throw new ListeVideException() :
Tête.Valeur;
// O(1) :)
public int ValeurDernier =>
EstVide? throw new ListeVideException() :
Queue.Valeur;
// O(n) : yesss!
public static ListeEntiers Dupliquer(ListeEntiers src)
{
ListeEntiers dest = new();
for (Noeud p = src.Tête; p != null; p = p.Succ)
dest.AjouterFin(p.Valeur);
return dest;
}
// ARK ARK ARK
public void Afficher()
{
for (Noeud p = Tête; p != null; p = p.Succ)
Console.Write($"{p.Valeur} ");
Console.WriteLine();
}
}
}
La version de PileEntiers utilisant une ListeEntiers
comme substrat était :
using System;
using System.Collections.Generic;
namespace zg
{
class PileVideException : Exception;
// LIFO : Last in, First out
internal class PileEntiers
{
ListeEntiers Substrat { get; init; } = new();
public bool EstVide => Substrat.EstVide;
public void Push(int val)
{
Substrat.AjouterDébut(val);
}
public int Pop()
{
int val = Peek();
Substrat.SupprimerDébut();
return val;
}
public int Peek()
{
if(EstVide)
throw new PileVideException();
return Substrat.ValeurPremier;
}
//class Noeud
//{
// public int Valeur { get; init; }
// public Noeud Pred { get; set; } = null;
// public Noeud(int val)
// {
// Valeur = val;
// }
//}
//Noeud Tête { get; set; } = null;
//public bool EstVide => Tête == null;
//public void Push(int val)
//{
// Noeud p = new(val);
// p.Pred = Tête;
// Tête = p;
//}
//public int Pop()
//{
// int val = Peek();
// Tête = Tête.Pred;
// return val;
//}
//public int Peek()
//{
// if (EstVide)
// throw new PileVideException();
// return Tête.Valeur;
//}
//TableauEntiers Substrat { get; init; } = new();
//public bool EstVide => Substrat.EstVide;
//public void Push(int val) // empiler : ajouter un élément sur le dessus
//{
// Substrat.Add(val);
//}
//public int Pop() // dépiler : retirer l'élément sur le dessus et le retourner
//{
// int val = Peek();
// Substrat.RemoveAt(Substrat.Count - 1);
// return val;
//}
//public int Peek() // examiner l'élément sur le dessus sans le retirer
//{
// if (EstVide)
// throw new PileVideException();
// return Substrat[Substrat.Count - 1];
//}
}
}
Le code de la classe ListeSimple conçue en classe était :
namespace z
{
class ListeVideException : Exception;
internal class ListeSimple
{
class Noeud
{
public int Val { get; init; }
public Noeud Succ { get; set; } = null;
public Noeud (int val)
{
Val = val;
}
}
Noeud Tête { get; set; } = null;
Noeud Queue { get; set; } = null;
public bool EstVide => Tête == null;
// complexité : O(1)
public void AjouterDébut(int val)
{
Noeud p = new(val);
if (EstVide)
Queue = p;
p.Succ = Tête;
Tête = p;
++Count;
}
// note : privée
// précondition : !EstVide
// complexité : O(n)
Noeud TrouverDernier()
{
Noeud p = Tête;
for (; p.Succ != null; p = p.Succ)
;
return p;
}
// complexité : O(1) :)
public void AjouterFin(int val)
{
if(EstVide)
AjouterDébut (val);
else
{
Noeud p = new(val);
Noeud q = Queue;
q.Succ = p;
Queue = p;
++Count;
}
}
// complexité : O(1)
public void SupprimerDébut()
{
if (EstVide)
throw new ListeVideException();
Tête = Tête.Succ;
if (EstVide)
Queue = null;
--Count;
}
public int Count { get; private set; } = 0;
// complexité : O(1)
public int PeekDébut()
{
if (EstVide)
throw new ListeVideException();
return Tête.Val;
}
// complexité : O(n)
public int PeekFin()
{
if (EstVide)
throw new ListeVideException();
return TrouverDernier().Val;
}
// TEMPORAIRE ARK ARK ARK
public void Afficher()
{
for (Noeud p = Tête; p != null; p = p.Succ)
Console.Write($"{p.Val} ");
Console.WriteLine();
}
// duplication (version méthode d'instance)
// complexité : O(n)... :)
public ListeSimple Dupliquer()
{
ListeSimple lst = new();
for(Noeud p = Tête; p != null; p = p.Succ)
lst.AjouterFin(p.Val);
return lst;
}
// duplication (version méthode de classe)
//public static ListeSimple Dupliquer(ListeSimple src)
//{
// ListeSimple lst = new();
// for (Noeud p = src.Tête; p != null; p = p.Succ)
// lst.AjouterFin(p.Val);
// return lst;
//}
}
}
Le code de la classe ListeDouble conçue en classe était :
namespace z
{
class ListeVideException : Exception;
internal class ListeDouble
{
class Noeud
{
public int Val { get; init; }
public Noeud Succ { get; set; } = null;
public Noeud Pred { get; set; } = null;
public Noeud(int val)
{
Val = val;
}
}
public int Count { get; private set; } = 0;
Noeud Tête { get; set; } = null;
Noeud Queue { get; set; } = null;
public bool EstVide => Count == 0;
// AjouterDébut
public void AjouterDébut(int val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
Tête.Pred = p;
p.Succ = Tête;
Tête = p;
}
++Count;
}
// AjouterFin
public void AjouterFin(int val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
Queue.Succ = p;
p.Pred = Queue;
Queue = p;
}
++Count;
}
// SupprimerDébut
public void SupprimerDébut()
{
if (EstVide)
throw new ListeVideException();
Tête = Tête.Succ;
if (Tête == null)
Queue = null;
else
Tête.Pred = null;
--Count;
}
// SupprimerFin
public void SupprimerFin()
{
if (EstVide)
throw new ListeVideException();
Queue = Queue.Pred;
if (Queue == null)
Tête = null;
else
Queue.Succ = null;
--Count;
}
// PeekDébut
public int PeekDébut()
{
if (EstVide)
throw new ListeVideException();
return Tête.Val;
}
// PeekFin
public int PeekFin()
{
if (EstVide)
throw new ListeVideException();
return Queue.Val;
}
// Dupliquer
public ListeDouble Dupliquer()
{
ListeDouble lst = new();
for (Noeud p = Tête; p != null; p = p.Succ)
lst.AjouterFin(p.Val);
return lst;
}
}
}
Le code de la classe File construite
en classe sur les fondations du substrat ListeEntiers
était :
namespace z
{
class FileVideException : Exception;
class File
{
ListeDouble Substrat { get; init; } = new();
public bool EstVide => Substrat.EstVide;
public void Enfiler(int valeur)
{
Substrat.AjouterDébut(valeur);
}
public int Peek() =>
EstVide? throw new FileVideException() : Substrat.PeekFin();
public int Défiler()
{
int val = Peek();
Substrat.SupprimerFin();
return val;
}
}
}
Rappel : avec Visual Studio, vos fichiers font quelques
using implicites, dont un using de
System.IO qui contient une classe nommée...
File. Si vous souhaitez utiliser notre classe
File, il se peut que vous deviez écrire le nom du
namespace explicitement (p. ex. : z.File
au lieu de File).
En espérant que le tout vous ait diverti!
|
|
23 mars |
S15 |
Au menu :
- Concevoir une liste doublement chaînée d'entiers nommée
ListeDouble
- Services
implémentés : EstVide, AjouterDébut, AjouterFin,
ValeurDébut,
ValeurFin, SupprimerDébut et – de manière temporaire – Afficher
- Concevoir une
file d'entiers nommée FileEntiers en,utilisant notre liste doublement chaînée d'entiers à
titre de substrat
Le code de la classe ListeDouble conçue en classe était :
namespace z
{
class ListeVideException : Exception;
internal class ListeDouble
{
class Noeud
{
public int Valeur { get; init; }
public Noeud Succ { get; set; } = null;
public Noeud Pred { get; set; } = null;
public Noeud(int val)
{
Valeur = val;
}
}
public int Count { get; private set; } = 0;
Noeud Tête { get; set; } = null;
Noeud Queue { get; set; } = null;
public bool EstVide => Count == 0;
// AjouterDébut
public void AjouterDébut(int val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
Tête.Pred = p;
p.Succ = Tête;
Tête = p;
}
++Count;
}
// AjouterFin
public void AjouterFin(int val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
Queue.Succ = p;
p.Pred = Queue;
Queue = p;
}
++Count;
}
// SupprimerDébut
public void SupprimerDébut()
{
if (EstVide)
throw new ListeVideException();
Tête = Tête.Succ;
if (Tête == null)
Queue = null;
else
Tête.Pred = null;
--Count;
}
// SupprimerFin
public void SupprimerFin()
{
if (EstVide)
throw new ListeVideException();
Queue = Queue.Pred;
if (Queue == null)
Tête = null;
else
Queue.Succ = null;
--Count;
}
public int ValeurPremier =>
EstVide ? throw new ListeVideException() : Tête.Valeur;
public int ValeurDernier =>
EstVide ? throw new ListeVideException() : Queue.Valeur;
}
// Dupliquer
public ListeDouble Dupliquer()
{
ListeDouble lst = new();
for (Noeud p = Tête; p != null; p = p.Succ)
lst.AjouterFin(p.Valeur);
return lst;
}
}
}
Le code de la classe FileEntiers construite
en classe sur les fondations du substrat ListeDouble
était :
namespace z
{
class FileVideException : Exception;
class FileEntiers
{
ListeDouble Substrat { get; init; } = new();
public bool EstVide => Substrat.EstVide;
public void Enfiler(int valeur)
{
Substrat.AjouterDébut(valeur);
}
public int Peek() =>
EstVide? throw new FileVideException() : Substrat.ValeurDernier;
public int Défiler()
{
int val = Peek();
Substrat.SupprimerFin();
return val;
}
}
}
Si le temps le permet :
- Enrichir les services de ListeDouble
- Trouver
- Contient
- Compter
- Supprimer
En espérant que le tout vous ait diverti!
N'oubliez pas de remettre Labo 02 en version imprimée au début de la
séance!
|
|
26 mars |
S16 |
Ce cours n'a pas lieu parce que vous êtes devant le Collège avec des
pancartes en chantant des slogans contre l'austérité. Dans l'intérêt de
(a) respecter votre mouvement de grève et (b) assurer ma propre intégrité
physique, on oublie ça pour aujourd'hui.
|
|
30 mars |
S17 |
Au menu :
- Ma semaine au WG21 :
../../../Sujets/Orthogonal/wg21-2026-Croydon.html
- Retour sur Q02
- confusion sur certains mots clés
- lire avant d'écrire!
- l'importance de se pratiquer
- Retour sur le labo 02
- mérites de l'encapsulation et risques des collections mutables
dans un langage sans objets
- importance de l'indentation (et choix de police!)
- Introduction (brève) au code générique
- de ListeDouble à
ListeDouble<T>
- conséquences de cette transformation
- quelques mots sur les fonctions génériques
- quelques mots sur les expressions lambda
La classe ListeDouble<T> à laquelle nous sommes parvenus était :
internal class ListeDouble<T> where T : IEquatable<T>
{
class Noeud
{
public T Val { get; init; }
public Noeud Succ { get; set; } = null;
public Noeud Pred { get; set; } = null;
public Noeud(T val)
{
Val = val;
}
}
public int Count { get; private set; } = 0;
Noeud Tête { get; set; } = null;
Noeud Queue { get; set; } = null;
public bool EstVide => Count == 0;
public void AjouterDébut(T val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
Tête.Pred = p;
p.Succ = Tête;
Tête = p;
}
++Count;
}
public void AjouterFin(T val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
Queue.Succ = p;
p.Pred = Queue;
Queue = p;
}
++Count;
}
public void SupprimerDébut()
{
if (EstVide)
throw new ListeVideException();
Tête = Tête.Succ;
if (Tête == null)
Queue = null;
else
Tête.Pred = null;
--Count;
}
public void SupprimerFin()
{
if (EstVide)
throw new ListeVideException();
Queue = Queue.Pred;
if (Queue == null)
Tête = null;
else
Queue.Succ = null;
--Count;
}
public T ValeurPremier =>
EstVide? throw new ListeVideException() : Tête.Val;
public T ValeurDernier =>
EstVide? throw new ListeVideException() : Queue.Val;
public ListeDouble<T> Dupliquer()
{
ListeDouble<T> lst = new();
for (Noeud p = Tête; p != null; p = p.Succ)
lst.AjouterFin(p.Val);
return lst;
}
// ARK ARK ARK
public void Afficher()
{
for (Noeud p = Tête; p != null; p = p.Succ)
Console.Write($"{p.Val} ");
Console.WriteLine();
}
// Contient : retourne true seulement si la liste
// contient une valeur donnée
// complexité : O(n)
public bool Contient(T val) => Trouver(val) != null;
// Compter : retourne le nombre d'occurrences d'une
// valeur donnée dans la liste
// complexité : O(n)
public int Compter(T val)
{
int n = 0;
for (Noeud p = Tête; p != null; p = p.Succ)
if (p.Val.Equals(val)/*p.Val == val*/)
++n;
return n;
}
// Supprimer : supprimer la première occurrence
// d'une valeur dans la liste
public void Supprimer(T val)
{
Noeud p = Trouver(val);
// ici, soit p est null, soit p.Val vaut val
if(p != null)
{
if (p.Succ == null) // dernier noeud
Queue = p.Pred;
else
p.Succ.Pred = p.Pred;
if (p.Pred == null) // premier noeud
Tête = p.Succ;
else
p.Pred.Succ = p.Succ;
--Count;
}
}
// (privée) Trouver : retourne une référence sur
// la première occurrence d'un noeud d'une
// valeur donnée, null sinon
Noeud Trouver(T val)
{
for (Noeud p = Tête; p != null; p = p.Succ)
if (p.Val.Equals(val)/*p.Val == val*/)
return p;
return null;
}
}
La fonction Compter<T> quant à elle était :
static int Compter<T>(T [] vals, T val) where T : IEquatable<T>
{
int nb = 0;
foreach(T e in vals)
if (e.Equals(val))
++nb;
return nb;
}
Exercice 00
Écrivez une classe FilePrioritaire<T> ayant les
caractéristiques suivantes :
- Elle utilisera un List<T> (le type List
de
C#) comme substrat
- Une FilePrioritaire<T> nouvellement
construite sera vide
- Elle exposera une propriété EstVide qui vaudra true
seulement si elle
est vide
- Elle inclura un critère de tri déterminé à la construction
- Note : un critère
de tri sera une fonction de comparaison trilatérale (voir
S12), prenant en paramètre
deux T retournant un int
- Elle exposera une méthode Add acceptant en paramètre un T
et l'ajoutant
au substrat
- Elle exposera une méthode Prochain retournant le premier T
dans le
substrat et l'en extrayant
- Invariant : les éléments d'une FilePrioritaire<T>
seront triés en
fonction de son critère de tri
- Note : utilisez la fonction Trier
dans l'exemple ci-dessous pour réaliser ce tri
Code de test possible :
using System;
using System.Collections.Generic;
FilePrioritaire<int> file = new (TriDécroissant);
foreach(int n in new []{ 2, 3, 5, 7, 11 })
file.Add(n);
while(!file.EstVide)
Console.WriteLine(file.Prochain()); // 11 7 5 3 2
// ...
static void Trier<T>(List<T> lst, Func<T,T,int> critère) // tri à bulles, pour fins de simplicité
{
for(int i = 0; i < lst.Count - 1; ++i)
for(int j = i + 1; j < lst.Count; ++j)
if(critère(lst[i], lst[j]) > 0)
PermuterÉléments(lst, i, j);
}
static void PermuterÉléments<T>(List<T> lst, int i, int j)
{
T elem = lst[i];
lst[i] = lst[j];
lst[j] = elem;
}
// ...
static int TriDécroissant(int x, int y) => y - x;
// ...
Affichage attendu :
Pour le plaisir : pouvez-vous faire une FilePrioritaire<Client>
pour une classe Client où chaque
Client a un nom, un prénom et une dette, et où les clients les plus
endettés sont prioritaires sur les moins endettés? Si deux clients sont aussi endettés l'un que l'autre, alors la priorité doit respecter l'ordre lexicographique (ordre croissant de
nom, puis de prénom si deux noms sont équivalents).
Solution possible :
class FilePrioritaire<T>
{
Func<T,T,int> Critère;
List<T> Elems{ get;init; } = new();
public FilePrioritaire(Func<T,T,int> critère)
{
Critère = critère;
}
public void Add(T elem)
{
Elems.Add(elem);
Trier(Elems, Critère);
}
public T Prochain()
{
T elem = Elems[0];
Elems.RemoveAt(0);
return elem;
}
public bool EstVide => Elems.Count == 0;
}
Question : est-ce que le substrat est bien choisi? Expliquez votre
réponse.
Exercice 01
Ajoutons quelques
algorithmes génériques à notre banque d'outils :
EX01.0 – Écrivez l'algorithme
Permuter<T>(ref T a, ref T b) qui permutera les valeurs de
a et de b, de telle sorte que l'exécution du programme
suivant :
int i0 = 2, i1 = 3;
Console.WriteLine($"Avant permutation : {i0}, {i1}");
Permuter(ref i0, ref i1);
Console.WriteLine($"Après permutation : {i0}, {i1}");
double d0 = 2.5, d1 = 3.5;
Console.WriteLine($"Avant permutation : {d0}, {d1}");
Permuter(ref d0, ref d1);
Console.WriteLine($"Après permutation : {d0}, {d1}");
string s0 = "allo", s1 = "toi";
Console.WriteLine($"Avant permutation : \"{s0}\", \"{s1}\"");
Permuter(ref s0, ref s1);
Console.WriteLine($"Après permutation : \"{s0}\", \"{s1}\"");
... affiche ce qui suit :
Avant permutation : 2,3
Après permutation : 3,2
Avant permutation : 2.5,3.5
Après permutation : 3.5,2.5
Avant permutation : "allo","toi"
Après permutation : "toi","allo"
EX01.1 – Écrivez l'algorithme
RotaterGauche<T>(List<T> src) qui retourne une
List<T> contenant un équivalent des éléments de la List<T>
reçue en paramètre, mais où les éléments de src
ont été décalés à gauche d'une position de manière cyclique (l'élément à
la position 0 dans src
est placée à la position Count-1 dans la
List<T> résultante), de telle sorte que le programme suivant :
using System;
using System.Collections.Generic;
Afficher(RotaterGauche(new List<int>()));
Afficher(RotaterGauche(new List<int>(){ 2,3,5,7,11 }));
static void Afficher<T>(List<T> lst)
{
foreach(T obj in lst)
Console.Write($"{obj} ");
Console.WriteLine();
}
... affiche ce qui suit (notez la première ligne qui est vide, car nous avons
opéré sur... une séquence vide!) :
Note : ici comme en général, évitez de modifier
src (le code client ne vous aimerait pas si vous le faisiez!)
Note : ceci peut se faire élégamment avec des
permutations. Vous ne pourrez pas utiliser Permuter
directement sur src, malheureusement,
mais c'est une tare de
C#
et de l'implémentation de List<T>, pas un
problème avec votre algorithme (utilisez le classique « temp = a; a = b; b
= temp; » tout simplement)
EX01.2 – Écrivez l'algorithme
RotaterDroite<T>(List<T> src) qui retourne une
List<T> contenant un équivalent des éléments de la List<T>
reçue en paramètre, mais où les éléments de src
ont été décalés à droite d'une position de manière cyclique (l'élément à
la position Count-1 dans src
est placée à la position 0 dans la
List<T> résultante), de telle sorte que le programme suivant :
using System;
using System.Collections.Generic;
Afficher(RotaterDroite(new List<int>()));
Afficher(RotaterDroite(new List<int>(){ 2,3,5,7,11 }));
static void Afficher<T>(List<T> lst)
{
foreach(T obj in lst)
Console.Write($"{obj} ");
Console.WriteLine();
}
... affiche ce qui suit (notez la première ligne qui est vide, car nous avons
opéré sur... une séquence vide!) :
EX01.3 – Écrivez la fonction
Concaténer<T>(List<T> lst0, List<T> lst1) qui retournera une
List<T> contenant les mêmes éléments que lst0,
dans l'ordre, suivis des éléments de lst1, dans
l'ordre.
EX01.4 – Écrivez l'algorithme
Transformer<T,U>(List<T> src, Func<T,U> fct) qui retourne une
List<U> contenant un équivalent des éléments de la List<T>
reçue en paramètre,
mais transformés par application de la fonction fct, de telle sorte que le programme suivant :
using System;
using System.Collections.Generic;
List<int> lst0 = new List<int>(){ 2,3,5,7,11 };
List<double> lst1 = Transformer(lst0, CarréNégatif);
foreach(double x in lst1)
Console.Write($"{x} ");
Console.WriteLine();
foreach(string s in Transformer(new List<string>(){ "j'aime", "mon", "prof" }, Exclamer))
Console.Write($"{s} ");
Console.WriteLine();
static double CarréNégatif(int x) => -1.0 * x * x; // nom pas terrible, je sais
static string Exclamer(string s) => s.ToUpper() + "!";
... affiche ce qui suit :
-4 -9 -25 -49 -121
J'AIME! MON! PROF!
EX01.5 – Écrivez l'algorithme
Cumuler<T,U>(List<T> src, Func<U,T,U> accum, U init) qui reçoit en paramètre
une List<T>, une fonction applicable à un
U et à un T et retournant un
U, de même qu'une valeur initiale de type U,
et retourne l'accumulation, de telle sorte que le programme suivant :
using System;
using System.Collections.Generic;
Console.WriteLine($"1+2+3+4+5 == {Cumuler(new List<int>(){ 1,2,3,4,5 }, Somme, 0)}");
Console.WriteLine($"1*2*3*4*5 == {Cumuler(new List<int>(){ 1,2,3,4,5 }, Produit, 1.0)}");
Console.WriteLine($"min(2,-3,5,-7,11) == {Cumuler(new List<int>(){ 2,-3,5,-7,11 }, Minimum, int.MaxValue)}");
var mots = new List<string>() { "yo", "man", "genre" };
Console.Write("La somme des longueurs des mots (");
foreach (string s in mots)
Console.Write($"{s} ");
Console.WriteLine($"\b) est : {Cumuler(mots, CumulLongueur, 0)}");
static int Somme(int x, int y) => x + y;
static double Produit(double x, int y) => x * y;
static int Minimum(int x, int y) => Math.Min(x, y);
static int CumulLongueur(int lg, string s) => lg + s.Length;
... affiche ce qui suit :
1+2+3+4+5 == 15
1*2*3*4*5 == 120
min(2,-3,5,-7,11) == -7
La somme des longueurs des mots (yo man genre) est : 10
EX01.6 – Écrivez la fonction
Trouver<T>(List<T> src, T val) qui retournera l'indice de la première
occurrence de val dans src,
ou -1 si aucune occurrence n'est trouvée.
EX01.7 – Écrivez la fonction
TrouverSi<T>(List<T> src, Func<T,bool> pred) qui retournera l'indice du
premier élément de src satisfaisant le prédicat
pred, ou -1 si aucun élément ne satisfaisant
pred n'est trouvé.
EX01.8 – Écrivez la fonction
Filtrer<T>(List<T> src, T val) qui retournera une
List<T> contenant les mêmes éléments que src,
dans le même ordre, à ceci près que toutes les occurrences de la valeur
val en auront été supprimées.
EX01.9 – Écrivez la fonction
FiltrerSi<T>(List<T> src, Func<T,bool> pred) qui retournera une
List<T> contenant les mêmes éléments que src,
dans le même ordre, à ceci près que tous les éléments respectant le prédicat
pred auront été supprimés.
EX01.10 – Écrivez la fonction
Remplacer<T>(List<T> src, T pré, T post) qui retournera une
List<T> contenant les mêmes éléments que src,
dans l'ordre, mais dont chaque occurrence de pré
aura été remplacée par post.
EX01.11 – Écrivez la fonction
RemplacerSi<T>(List<T> src, Func<T,bool> pred, T post) qui retournera une
List<T> contenant les mêmes éléments que src,
dans l'ordre, mais dont chaque élément satisfaisant le prédicat
pred aura été remplacé par post.
|
|
2 avril |
S18 |
Au menu :
- Retour sur les exercices de
S17
- On les fait tous ensemble!
- Note : si vous voulez vraiment profiter de la séance
d'aujourd'hui, assurez-vous d'avoir vous-mêmes faits ces exercices
au préalable, ou du moins d'avoir sincèrement essayé de les faire!
Certaines et certains d'entre vous ont exprimé le
souhait de faire d'autres exercices, alors pour votre bon plaisir... Si
vous l'estimez pertinent, vous pouvez réutiliser d'autres algorithmes
de votre cru dans vos implémentations (vous ne pouvez pas utiliser des
fonctions déjà implémentées dans la bibliothèque standard du langage,
le but étant de vous pratiquer et d'apprendre!).
EX02.0 – Écrivez l'algorithme
SupprimerDoublons<T>(List<T> src) qui reçoit en paramètre
une List<T> préalablement triée (il s'agit d'une
précondition de la fonction), et retourne une List<T>
contenant les mêmes valeurs que la List<T>
originale, mais exempte de doublons, de telle sorte que le programme suivant :
Afficher(SupprimerDoublons(new List<int>()));
Afficher(SupprimerDoublons(new List<int>(){ 2 }));
Afficher(SupprimerDoublons(new List<int>(){ 2, 2 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,3,3,3,5,7,11 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,2,3,5,5,7,11 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,3,5,7,7,11 }));
Afficher(SupprimerDoublons(new List<int>(){ 2,3,5,7,11,11 }));
static void Afficher<T>(List<T> lst)
{
foreach(T obj in lst)
Console.Write($"{obj} ");
Console.WriteLine();
}
... affiche ce qui suit (notez la première ligne qui est vide, car nous avons
supprimé les doublons... d'une séquence vide!) :
2
2
2 3 5 7 11
2 3 5 7 11
2 3 5 7 11
2 3 5 7 11
EX02.1 – Écrivez l'algorithme
Inverser<T>(List<T> lst) qui modifie lst
et ne retourne rien. Cette fonction doit inverser l'ordre des
éléments de lst, de telle sorte que le
programme suivant :
List<int> lst = new();
Inverser(lst);
Afficher(lst);
lst = new(){ 1 };
Inverser(lst);
Afficher(lst);
lst = new(){ 1, 2 };
Inverser(lst);
Afficher(lst);
lst = new(){ 2,3,5,7,11 };
Inverser(lst);
Afficher(lst);
static void Afficher<T>(List<T> lst)
{
foreach(T obj in lst)
Console.Write($"{obj} ");
Console.WriteLine();
}
... affiche ce qui suit (notez la première ligne qui est vide, car nous avons
inversé les éléments... d'une séquence vide!) :
EX02.2 – Écrivez le prédicat
EstPalindrome<T>(List<T> lst) qui retourne
true seulement si lst est un
palindrome. Un palindrome est une séquence qui a la même forme si on
l'observe de gauche à droite ou de droite à gauche, comme par exemple
un tableau contenant 1,3,3,1 ou le texte
"laval". Implémentez cette fonction sans allouer de mémoire.
EX02.3 – Écrivez le prédicat
SontTous<T> prenant en paramètre une
List<T> et un prédicat applicable à un T,
et retournant true seulement si tous les
éléments de la List<T> satisfont le
prédicat.
EX02.4 – Écrivez le prédicat
AuMoinsUn<T> prenant en paramètre une
List<T> et un prédicat applicable à un T,
et retournant true seulement si au moins
un élément de la List<T> satisfait le
prédicat.
EX02.5 – Écrivez le prédicat
Aucun<T> prenant en paramètre une List<T>
et un prédicat applicable à un T,
et retournant true seulement si aucun
élément de la List<T> ne satisfait le
prédicat.
Une implémentation possible de FilePrioritaire<T>
serait la suivante (note : Trier
est dans Algos, plus bas) :
class FileVideException : Exception;
internal class FilePrioritaire<T>
{
List<T> Substrat { get; init; } = new();
public bool EstVide => Substrat.Count == 0;
Func<T,T,int> Critère { get; init; } // fonction de comparaison trilatérale
public FilePrioritaire(Func<T,T,int> critère)
{
Critère = critère;
}
public void Add(T elem)
{
Substrat.Add(elem);
Algos.Trier(Substrat, Critère);
}
public T Prochain()
{
if(EstVide)
throw new FileVideException();
T elem = Substrat[0];
Substrat.RemoveAt(0);
return elem;
}
}
Une implémentation possible de des algorithmes proposés suit. Notez que
pour Concaténer<T> j'ai utilisé le mot clé
params et
je me suis permis de l'implémenter à partir de
Cumuler (vous remarquerez que j'ai une lambda de plus d'une
instruction dans cette implémentation; voyez-vous pourquoi?)
internal static class Algos
{
//
// ... autres algos du passé (omis par souci d'économie) ...
//
// Trier et PermuterÉléments servent à l'implémentation de FilePrioritaire<T>, mais
// PermuterÉléments peut aussi servir à d'autres algorithmes comme RotaterGauche<T>
// et RotaterDroite<T> à titre d'exemple
//
public static void Trier<T>(List<T> lst, Func<T, T, int> critère) // tri à bulles, pour fins de simplicité
{
for (int i = 0; i < lst.Count - 1; ++i)
for (int j = i + 1; j < lst.Count; ++j)
if (critère(lst[i], lst[j]) > 0)
PermuterÉléments(lst, i, j);
}
public static void PermuterÉléments<T>(List<T> lst, int i, int j)
{
T elem = lst[i];
lst[i] = lst[j];
lst[j] = elem;
}
//
// algorithmes demandés à titre d'exercices
//
public static void Permuter<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
//
// note : j'ai fait plus d'une implémentation de RotaterGauche<T>, ce qui
// explique celle(s) laissée(s) en commentaire
//
//public static T[] RotaterGauche<T>(T[] src)
//{
// T[] dest = new T[src.Length];
// for(int i = 0; i != src.Length; ++i)
// dest[i] = src[i];
// for (int i = 1; i < dest.Length; ++i)
// Permuter(ref dest[i], ref dest[i - 1]);
// return dest;
//}
//public static List<T> RotaterGauche<T>(List<T> src) =>
// RotaterGauche(src.ToArray()).ToList();
public static List<T> RotaterGauche<T>(List<T> src)
{
List<T> dest = new(src);
for (int i = 1; i < src.Count; ++i)
PermuterÉléments(dest, i, i - 1); // voir plus haut pour celle-ci
return dest;
}
public static List<T> RotaterDroite<T>(List<T> src)
{
List<T> dest = new(src);
for (int i = src.Count - 1; i > 0; --i)
PermuterÉléments(dest, i, i - 1); // voir plus haut pour celle-ci
return dest;
}
public static List<T> Concaténer<T>(params List<T>[] src) =>
Cumuler
(
src.ToList(),
(a, b) => { a.AddRange(b); return a; },
new List<T>()
);
// public static List<T> Concaténer<T>(params List<T>[] lsts)
// {
// List<T> dest = new();
// foreach (List<T> lst in lsts)
// dest.AddRange(lst);
// return dest;
// }
public static List<U> Transformer<T, U>(List<T> src, Func<T, U> f)
{
List<U> dest = new();
foreach (T e in src)
dest.Add(f(e));
return dest;
}
public static U Cumuler<T, U>(List<T> src, Func<U, T, U> f, U init)
{
foreach (T e in src)
init = f(init, e);
return init;
}
public static int Trouver<T>(List<T> src, T val)
where T : IEquatable<T>
=> TrouverSi(src, e => e.Equals(val));
public static int TrouverSi<T>(List<T> src, Func<T, bool> pred)
{
for(int i = 0; i != src.Count; ++i)
if(pred(src[i]))
return i;
return -1;
}
public static List<T> Filtrer<T>(List<T> src, T val)
where T : IEquatable<T>
=> FiltrerSi(src, e => e.Equals(val));
public static List<T> FiltrerSi<T>(List<T> src, Func<T,bool> pred)
{
List<T> dest = new();
foreach(T e in src)
if(!pred(e))
dest.Add(e);
return dest;
}
public static List<T> Remplacer<T>(List<T> src, T pre, T post)
where T : IEquatable<T>
=> RemplacerSi(src, e => e.Equals(pre), post);
public static List<T> RemplacerSi<T>(List<T> src, Func<T, bool> pred, T post)
=> Transformer(src, e => pred(e) ? post : e);
}
|
|
6 avril
|
|
Jour férié (lundi de Pâques)
|
|
9 avril |
S19 |
Au menu, sujets divers :
- Où se trouve l'exécutable généré quand on compile un programme
- Comment passer des paramètres à un programme : le paramètre
implicite args
- À la ligne de commande
- Dans Visual Studio pour fins de débogage
- Scinder une string à partir d'un ou
plusieurs caractères délimiteurs : méthode d'instance
Split
- Épurer une string des blancs aux
extrémités :
- Générer un code de hashage « simplement » pour la méthode
GetHashCode avec HashCode.Combine
- Surtout : entrées et sorties avec fichiers texte
- Classe StreamReader
- Classe StreamWriter
- Gestion des erreurs et try...finally
- Gestion des erreurs et blocs using
string texte = "";
using (StreamReader sr = new(args.Length > 0 ?args[0] : "../../../Program.cs"))
{
for (string s = sr.ReadLine(); s != null; s = sr.ReadLine())
texte += $"{s}\n" ; // PRÉFÉREZ UN STRINGBUILDER!!!
}
using (StreamWriter sw = new("sortie.txt"))
{
sw.Write(texte.ToUpper());
}
|
|
13 avril |
S20 |
Au menu :
- Minitest Q03
- Du code générique encore plus utile : les itérateurs (en
C#,
les énumérateurs) à travers les interfaces
IEnumerable<T> et IEnumerator<T>
- Comment foreach fonctionne
- Exemple avec ListeDouble<T>
- Exemple avec Tableau (qu'on
transforme en Tableau<T>, parce que
pourquoi pas?)
- Impact sur nos algorithmes
- Ça pourrait vous aider dans votre labo 🙂
La classe ListeDouble<T> énumérable était :
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace z
{
internal class ListeDouble<T>
: IEnumerable<T>
where T : IEquatable<T>
{
class Noeud
{
public Noeud Pred { get; set; } = null;
public Noeud Succ { get; set; } = null;
public T Valeur { get; init; }
public Noeud(T val)
{
Valeur = val;
}
}
public int Count { get; private set; } = 0;
Noeud Tête { get; set; } = null;
Noeud Queue { get; set; } = null;
public bool EstVide => Count == 0;
public void AjouterDébut(T val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
p.Succ = Tête;
Tête.Pred = p;
Tête = p;
}
++Count;
}
public void AjouterFin(T val)
{
Noeud p = new(val);
if (EstVide)
Tête = Queue = p;
else
{
p.Pred = Queue;
Queue.Succ = p;
Queue = p;
}
++Count;
}
public void SupprimerDébut()
{
if (EstVide)
throw new ListeVideException();
Tête = Tête.Succ;
if (Tête != null)
Tête.Pred = null;
else
Queue = null;
--Count;
}
public void SupprimerFin()
{
if(EstVide)
throw new ListeVideException();
Queue = Queue.Pred;
if (Queue != null)
Queue.Succ = null;
else
Tête = null;
--Count;
}
public T ValeurPremier =>
EstVide ? throw new ListeVideException() : Tête.Valeur;
public T ValeurDernier =>
EstVide ? throw new ListeVideException() : Queue.Valeur;
// Trouver (un Noeud par sa valeur)
Noeud Trouver(T val)
{
for (Noeud p = Tête; p != null; p = p.Succ)
if(p.Valeur.Equals(val)) // if (p.Valeur == val)
return p;
return null;
}
public bool Contient(T val) => Trouver(val) != null;
public int Compter(T val)
{
int n = 0;
for (Noeud p = Tête; p != null; p = p.Succ)
if(p.Valeur.Equals(val)) // if (p.Valeur == val)
++n;
return n;
}
// Supprimer (la première occurrence d'une valeur)
public void Supprimer(T val)
{
Noeud p = Trouver(val);
if(p != null)
{
if (p == Tête)
SupprimerDébut();
else if (p == Queue)
SupprimerFin();
else
{
p.Pred.Succ = p.Succ;
p.Succ.Pred = p.Pred;
--Count;
}
}
}
public IEnumerator<T> GetEnumerator() => new Énumérateur(this);
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
class Énumérateur : IEnumerator<T>
{
public Énumérateur(ListeDouble<T> src)
{
Cur = new(default);
Cur.Succ = src.Tête;
}
Noeud Cur { get; set; }
public T Current => Cur.Valeur;
object IEnumerator.Current => Current;
public bool MoveNext()
{
if(Cur.Succ == null)
return false;
Cur = Cur.Succ;
return true;
}
public void Reset() { }
public void Dispose() { }
}
}
}
La classe Tableau<T> énumérable était :
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace z
{
// invariant : Count <= Capacity
internal class Tableau<T>
: IEnumerable<T>
where T : IEquatable<T>
{
T[] Substrat { get; set; }
public int Count { get; private set; } = 0;
public int Capacity => Substrat.Length;
public bool EstVide => Count == 0;
public bool EstPlein => Count == Capacity;
public Tableau()
{
Substrat = new T[0];
}
public void Add(T val)
{
if (EstPlein)
Croître();
Substrat[Count] = val;
++Count;
}
void Croître()
{
int nouvelleCapacité = Capacity == 0?
4 : Capacity * 2;
var tab = new T[nouvelleCapacité];
for (int i = 0; i != Count; ++i)
tab[i] = Substrat[i];
Substrat = tab;
}
public void RemoveAt(int indice)
{
if (indice < 0 || Count <= indice)
throw new IndexOutOfRangeException();
for (int i = indice + 1; i < Count; ++i)
Substrat[i - 1] = Substrat[i];
--Count;
}
// complexité : O(2n) donc O(n) : linéaire
public void Remove(T val)
{
int indice = Trouver(val);
if(indice != -1)
RemoveAt(indice);
}
// complexité : linéaire O(n)
int Trouver(T val)
{
for (int i = 0; i != Count; ++i)
if (Substrat[i].Equals(val))
return i;
return -1;
}
public void Insert(int indice, T val)
{
if (indice < 0 || Count <= indice)
throw new IndexOutOfRangeException();
if (EstPlein)
Croître();
for (int i = Count - 1; i >= indice; --i)
Substrat[i + 1] = Substrat[i];
Substrat[indice] = val;
++Count;
}
public IEnumerator<T> GetEnumerator() => new Énumérateur(this);
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
class Énumérateur : IEnumerator<T>
{
Tableau<T> Source { get; init; }
int Indice { get; set; } = -1;
public Énumérateur(Tableau<T> src)
{
Source = src;
}
public T Current => Source[Indice];
object IEnumerator.Current => Current;
public bool MoveNext()
{
if(Indice == Source.Count - 1)
return false;
++Indice;
return true;
}
public void Reset() { }
public void Dispose() { }
}
// indexeur
public T this[int indice]
{
get => Substrat[indice];
// si on souhaite permettre de modifier un élément
// par son indice
set { Substrat[indice] = value; }
}
}
}
- S'il reste du temps : travail sur le labo 03
|
|
16 avril |
S21 |
Au menu :
- Retour sur Q03
- Sélectives (switch) :
- Forme « énoncé »
- Forme « expression »
- Introduction aux dictionnaires
- Solution aux exercices de
S18
- Travail sur le labo 03
|
|
20 avril |
S22 |
Au menu :
- Un dernier petit laboratoire
N'oubliez pas de remettre la version imprimée du labo 03 au début du cours!
|
|
23 avril |
S23 |
Au menu :
- Un « dernier » minitest (quoi que...)
|
|
27 avril |
S24 |
Au menu :
|
|
30 avril |
S25 |
Au menu :
|
|
4 mai |
S26 |
Pas de séance en classe aujourd'hui (votre prof est à
https://ndctoronto.com/ toute la
semaine)
|
|
7 mai |
s/o
|
Journée d'examen de français / formation générale (cours suspendus)
|
|
11 mai |
S27 |
Au menu :
|
|
14 mai |
S28 |
Au menu :
|
|
18 mai |
s/o
|
Jour férié (Journée nationale des Patriotes)
|
|
19 mai |
S29 |
Au menu :
Attention : mardi selon l'horaire du lundi
|
Vous trouverez ici quelques documents, la plupart petits, qui peuvent vous
donner un petit coup de pouce occasionnel.
Si vous cherchez les énoncés des travaux pratiques, vous pourrez entre autres
les trouver ci-dessous (note : ce tableau est celui de
H2021; je ne l'ai pas mis à jour pour H2024).
Quelques solutionnaires suivent. En espérant que ça vous aide à organiser vos idées!