Après les bases, après la POO, vient l'artisanat.
Les outils qui font la différence entre un amateur et un professionnel.
Les collections qui organisent, LINQ qui interroge, les exceptions qui gèrent l'échec.
Le genre de chapitre où on apprend à se salir les mains proprement.
9. Collections
List<T>, Dictionary<TKey, TValue>
Les basiques. Ceux qu'on utilise tant qu'on n'a pas besoin de mieux.
// Une liste - l'ordre compte
List<string> cibles = new List<string>
{
"Le Baron",
"La Taupe",
"Le Traitre"
};
cibles.Add("L'Informateur"); // À la fin
cibles.Insert(1, "Le Corbeau"); // À une position précise
foreach (var cible in cibles)
{
Console.WriteLine($"Cible : {cible}");
// L'ordre est préservé
}
// Un dictionnaire - la clé est tout
Dictionary<string, string> dossiers = new Dictionary<string, string>
{
["A7"] = "Affaire Serpent",
["B2"] = "Dossier Ombre",
["X9"] = "Projet Fantôme"
};
// Accès rapide
if (dossiers.ContainsKey("A7"))
{
Console.WriteLine($"Dossier A7 : {dossiers["A7"]}");
}
// Attention : les clés doivent être uniques
// dossiers.Add("A7", "Doublon"); // ERREUR : KeyAlreadyExists
HashSet, Queue, Stack
Les spécialistes. Quand la liste ou le dictionnaire ne suffisent pas.
// HashSet - pour l'unicité, pas l'ordre
HashSet<string> temoinsInterroges = new HashSet<string>
{
"Alice",
"Bob",
"Charlie"
};
temoinsInterroges.Add("Alice"); // Ne fait rien - déjà présent
Console.WriteLine($"Témoins uniques : {temoinsInterroges.Count}"); // 3
// Queue - Premier entré, premier sorti (FIFO)
Queue<string> missionsEnAttente = new Queue<string>();
missionsEnAttente.Enqueue("Surveillance banque");
missionsEnAttente.Enqueue("Récupération données");
missionsEnAttente.Enqueue("Extraction cible");
string prochaineMission = missionsEnAttente.Dequeue(); // "Surveillance banque"
// Comme une file d'attente
// Stack - Dernier entré, premier sorti (LIFO)
Stack<string> pistesExplorees = new Stack<string>();
pistesExplorees.Push("Piste financière");
pistesExplorees.Push("Piste politique");
pistesExplorees.Push("Piste personnelle");
string dernierePiste = pistesExplorees.Pop(); // "Piste personnelle"
// Comme une pile d'assiettes
Parcours et manipulation
Apprendre à bouger dans les collections sans se faire remarquer.
List<Agent> agents = new List<Agent>
{
new Agent { Nom = "Bond", Niveau = 9 },
new Agent { Nom = "Bourne", Niveau = 8 },
new Agent { Nom = "Salt", Niveau = 7 }
};
// Suppression conditionnelle
agents.RemoveAll(a => a.Niveau < 8); // Adieu Salt
// La méthode agit directement sur la liste
// Trouver un élément
Agent? agentElite = agents.Find(a => a.Niveau >= 9); // Bond
// Retourne null si pas trouvé
// Convertir
string[] noms = agents.ConvertAll(a => a.Nom.ToUpper()).ToArray();
// ["BOND", "BOURNE"]
// Attention aux références
List<Agent> copieSuperficielle = new List<Agent>(agents);
// Copie la liste, mais les objets sont les mêmes références
// Modifier Bond dans une liste affecte l'autre
10. LINQ
Syntaxe LINQ
Language Integrated Query. Interroger des collections comme une base de données.
Deux syntaxes : la méthode et la requête.
List<Operation> operations = new List<Operation>
{
new Operation { Code = "NuitNoire", Risque = 9, Reussie = true },
new Operation { Code = "AubeRouge", Risque = 7, Reussie = false },
new Operation { Code = "Crépuscule", Risque = 8, Reussie = true }
};
// Syntaxe méthode (la plus utilisée)
var operationsReussies = operations
.Where(op => op.Reussie)
.OrderByDescending(op => op.Risque)
.Select(op => new { op.Code, op.Risque });
// Syntaxe requête (style SQL)
var operationsRisquees = from op in operations
where op.Risque > 7
orderby op.Code
select op;
Where, Select, OrderBy
Les trois piliers.
// Where - filtrer
var missionsCritiques = operations.Where(op => op.Risque >= 8);
// Comme un if dans une boucle
// Select - transformer
var rapports = operations.Select(op =>
$"Opération {op.Code} : {(op.Reussie ? "SUCCÈS" : "ÉCHEC")}"
);
// Change la forme, garde l'essence
// OrderBy - trier
var parRisqueCroissant = operations.OrderBy(op => op.Risque);
var parRisqueDecroissant = operations.OrderByDescending(op => op.Risque);
// On peut chaîner
var topOperations = operations
.Where(op => op.Reussie)
.OrderByDescending(op => op.Risque)
.Take(3); // Prendre les 3 premiers
Any, All, First, Single
Les questions qu'on pose aux collections.
// Any - est-ce qu'il existe au moins un... ?
bool existeEchec = operations.Any(op => !op.Reussie);
// Plus rapide que Count() > 0
// All - est-ce que tous... ?
bool tousRisqueux = operations.All(op => op.Risque > 5);
// First - le premier qui correspond
Operation premiereReussie = operations.First(op => op.Reussie);
// Lance une exception si aucun ne correspond
Operation? premiereReussieOuNull = operations.FirstOrDefault(op => op.Reussie);
// Retourne null si pas trouvé
// Single - l'unique qui correspond
Operation operationUnique = operations.Single(op => op.Code == "NuitNoire");
// Lance une exception s'il y en a 0 ou plus d'un
// À utiliser quand l'unicité est garantie
LINQ vs boucles classiques
Le choix entre l'élégance et le contrôle.
// Avec LINQ - déclaratif
var codesReussis = operations
.Where(op => op.Reussie)
.Select(op => op.Code)
.ToList();
// Avec boucle - impératif
List<string> codesReussis2 = new List<string>();
foreach (var op in operations)
{
if (op.Reussie)
{
codesReussis2.Add(op.Code);
}
}
// LINQ est souvent plus lisible
// Mais la boucle donne plus de contrôle
// LINQ est lazy (sauf avec ToList, ToArray...)
// La boucle est immédiate
11. Exceptions
Gestion des erreurs (try / catch / finally)
Quand tout se passe mal, il faut savoir réagir. Ou disparaître proprement.
public class Vault
{
public decimal Open(string code, decimal montantDemande)
{
try
{
ValidateCode(code);
ValidateAmount(montantDemande);
// Le code dangereux
return ProcessWithdrawal(montantDemande);
}
catch (InvalidCodeException ex)
{
Console.WriteLine($"Code invalide : {ex.Message}");
LogSecurityBreach(code);
throw; // Relance l'exception - on ne laisse pas passer
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"Fonds insuffisants : {ex.Message}");
return 0; // On gère et on continue
}
catch (Exception ex)
{
// Catch-all - dangereux mais parfois nécessaire
Console.WriteLine($"Erreur inattendue : {ex.Message}");
LogToBlackBox(ex);
throw new VaultException("Échec de l'opération", ex);
}
finally
{
// Toujours exécuté, erreur ou pas
CloseConnection();
Console.WriteLine("Coffre refermé.");
}
}
}
Exceptions personnalisées
Quand les exceptions système ne suffisent pas.
public class MissionFailedException : Exception
{
public string MissionCode { get; }
public DateTime FailureTime { get; }
public MissionFailedException(string missionCode, string message)
: base($"Mission {missionCode} échouée : {message}")
{
MissionCode = missionCode;
FailureTime = DateTime.Now;
}
// Constructeur avec exception interne
public MissionFailedException(string missionCode, string message, Exception inner)
: base($"Mission {missionCode} échouée : {message}", inner)
{
MissionCode = missionCode;
FailureTime = DateTime.Now;
}
}
// Utilisation
throw new MissionFailedException("NuitNoire", "Cible a échappé à la surveillance");
Bonnes pratiques
Les erreurs sont inévitables. Les gérer mal est un choix.
// MAUVAIS
try
{
RiskyOperation();
}
catch (Exception)
{
// Avaler l'exception - le pire crime
// Personne ne saura que quelque chose s'est mal passé
// Jusqu'à ce que tout explose ailleurs
}
// MOINS MAUVAIS
try
{
RiskyOperation();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // Au moins on loggue
// Mais on avale quand même
}
// BIEN
try
{
RiskyOperation();
}
catch (SpecificException ex)
{
// On gère ce qu'on peut gérer
RecoveryProcedure();
LogError(ex);
// On ne relance pas si on a géré
}
catch (Exception ex)
{
// Pour le reste, on loggue et on relance
LogCriticalError(ex);
throw; // On prévient l'appelant
}
// Le finally pour le nettoyage
FileStream? file = null;
try
{
file = new FileStream("data.bin", FileMode.Open);
// Traitement
}
finally
{
file?.Close(); // Toujours fermer
}
// Ou mieux : using
using (var file2 = new FileStream("data.bin", FileMode.Open))
{
// Auto-fermeture même en cas d'exception
}
12. Enums et structs
Enums typés
Les listes de choix fermées. Ce qui ne peut prendre que certaines valeurs.
public enum MissionStatus
{
Planned, // 0
InProgress, // 1
Completed, // 2
Failed, // 3
Aborted // 4
}
// Avec valeurs personnalisées
public enum SecurityLevel : byte // Typé byte au lieu de int
{
Public = 1,
Confidential = 50,
Secret = 100,
TopSecret = 255
}
// Utilisation
MissionStatus status = MissionStatus.InProgress;
// Vérification
if (status == MissionStatus.Failed)
{
InitiateCleanup();
}
// Parcours de toutes les valeurs
foreach (SecurityLevel level in Enum.GetValues(typeof(SecurityLevel)))
{
Console.WriteLine($"Level: {level} ({(byte)level})");
}
// Conversion
string levelStr = "Secret";
if (Enum.TryParse(levelStr, out SecurityLevel parsedLevel))
{
// parsedLevel vaut SecurityLevel.Secret
}
Structs vs classes
Le choix entre la pile et le tas, entre la valeur et la référence.
// STRUCT - type valeur
public struct Coordinate
{
public int X { get; }
public int Y { get; }
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
// Méthodes autorisées
public Coordinate Move(int dx, int dy) => new Coordinate(X + dx, Y + dy);
}
// CLASSE - type référence
public class AgentPosition
{
public int X { get; set; }
public int Y { get; set; }
public string AgentName { get; set; } // Référence !
public AgentPosition(int x, int y, string name)
{
X = x;
Y = y;
AgentName = name;
}
}
// Différences cruciales
Coordinate coord1 = new Coordinate(10, 20);
Coordinate coord2 = coord1; // COPIE
coord2 = coord2.Move(5, 5); // coord1 reste (10, 20)
AgentPosition pos1 = new AgentPosition(10, 20, "Bond");
AgentPosition pos2 = pos1; // MÊME RÉFÉRENCE
pos2.X = 30; // pos1.X vaut maintenant 30 aussi !
Cas d'usage
Quand utiliser quoi.
// Utilise un struct quand :
// - L'objet est petit (moins de 16 octets idéalement)
// - Il est immuable
// - Tu veux éviter l'allocation dans le tas
// - La sémantique de copie est voulue
public struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// Utilise une classe quand :
// - L'objet est gros
// - Il a besoin d'être partagé (référence)
// - Il a une identité propre
// - Tu as besoin d'héritage
public class BankAccount
{
public string AccountNumber { get; } // Identité
public Money Balance { get; private set; } // Struct dans une classe
public List<Transaction> Transactions { get; } = new(); // Référence
public void Deposit(Money amount)
{
// Logique complexe
}
}
Fin du chapitre intermédiaire.
Tu connais maintenant les outils qui séparent le script kiddie du professionnel.
Les collections pour organiser, LINQ pour questionner, les exceptions pour survivre aux échecs.
Mais attention :
Un LINQ mal écrit peut être plus lent qu'une boucle.
Une exception attrapée et avalée est une bombe à retardement.
Un struct mal utilisé peut tuer les performances.
Le prochain chapitre parlera d'asynchrone.
De tâches qui tournent en parallèle, de async et await.
De comment faire plusieurs choses à la fois, sans tout faire planter.
Dans le monde moderne, tout est parallèle.
Apprends à jongler avec les threads.
Ou prépare-toi à attendre très, très longtemps.