Polar Code 🎭

Command Palette

Search for a command to run...

05
Pièce N°05

Chapitre 4 — Le Code Qui Ne Dort Pas

Le monde a accéléré.
Les applications ne peuvent plus se permettre d'attendre.
Tout doit être asynchrone, parallèle, réactif.
C# moderne, c'est le langage qui a appris à courir sans trébucher.


13. Asynchronisme

async / await
La révolution silencieuse. La promesse de ne jamais bloquer le thread principal.

// L'ancien monde - synchrone (bloquant)
public class AncienSurveillant
{
    public void SurveillerCible(string cible)
    {
        Thread.Sleep(5000);  // Bloque tout pendant 5 secondes
        Console.WriteLine($"{cible} surveillée.");
        // Pendant ce temps, l'interface utilisateur ne répond plus
    }
}

// Le nouveau monde - asynchrone (non-bloquant)
public class NouveauSurveillant
{
    public async Task SurveillerCibleAsync(string cible)
    {
        await Task.Delay(5000);  // Attente sans bloquer
        Console.WriteLine($"{cible} surveillée.");
        // Pendant ce temps, l'interface utilisateur reste réactive
    }
}

Task
La monnaie d'échange de l'asynchrone. Une promesse de résultat futur.

public async Task<string> RecupererRapportAsync(int id)
{
    // Task<string> - promet de retourner un string un jour
    
    // Simule un appel réseau long
    await Task.Delay(2000);
    
    // Simule un appel HTTP
    using var client = new HttpClient();
    string rapport = await client.GetStringAsync($"https://api.ops.com/rapports/{id}");
    
    return rapport;
}

// Tâche sans retour
public async Task EnvoyerAlerteAsync(string message)
{
    await Task.Delay(1000);
    Console.WriteLine($"Alerte : {message}");
    // Task (pas Task<T>) - fait quelque chose, ne retourne rien
}

// Tâche qui échoue
public async Task OperationDangereuseAsync()
{
    await Task.Delay(500);
    throw new InvalidOperationException("Tout a mal tourné");
    // Une Task peut aussi contenir une exception
}

Appels concurrents
Faire plusieurs choses en même temps, sans se tirer dans le pied.

// MAUVAIS - séquentiel (trop lent)
public async Task<string> TelechargerToutMauvaisAsync()
{
    var donnees1 = await TelechargerAsync("source1");
    var donnees2 = await TelechargerAsync("source2");  // Attend que source1 finisse
    var donnees3 = await TelechargerAsync("source3");  // Attend que source2 finisse
    
    return donnees1 + donnees2 + donnees3;  // 3x plus long que nécessaire
}

// BON - parallèle
public async Task<string> TelechargerToutBonAsync()
{
    // Lance toutes les tâches en même temps
    var tache1 = TelechargerAsync("source1");
    var tache2 = TelechargerAsync("source2");
    var tache3 = TelechargerAsync("source3");
    
    // Attend qu'elles finissent toutes
    await Task.WhenAll(tache1, tache2, tache3);
    
    // Récupère les résultats
    return await tache1 + await tache2 + await tache3;
}

// Attente du premier qui répond
public async Task<string> ObtenirPremiereReponseAsync()
{
    var sourceRapide = "https://api.rapide.com/data";
    var sourceSecours = "https://backup.secours.com/data";
    
    var tacheRapide = client.GetStringAsync(sourceRapide);
    var tacheSecours = client.GetStringAsync(sourceSecours);
    
    // Retourne dès qu'une des deux répond
    var premiereReponse = await Task.WhenAny(tacheRapide, tacheSecours);
    
    // Annule l'autre
    // (en vrai, c'est plus complexe avec CancellationToken)
    
    return await premiereReponse;
}

Bonnes pratiques async
L'asynchrone est puissant. Et dangereux. Quelques règles pour survivre.

// 1. Toujours suffixer avec Async
public Task FaireQqcAsync() { }  // OUI
public Task FaireQqc() { }        // NON (sauf méthodes sync)

// 2. Propager async/await dans toute la chaîne
public async Task<Niveau1> Methode1Async()
{
    return await Methode2Async();
}

public async Task<Niveau2> Methode2Async()
{
    return await Methode3Async();
}
// "async all the way down"

// 3. Éviter async void (sauf pour les event handlers)
public async void MauvaiseIdee()  // Les exceptions ne peuvent être catchées
{
    await Task.Delay(1000);
    throw new Exception("Perdue dans le néant");
}

public async Task BonneIdee()  // Les exceptions sont préservées
{
    await Task.Delay(1000);
    throw new Exception("Peut être catchée");
}

// 4. Utiliser ConfigureAwait(false) dans les librairies
public async Task<string> LibrairieAsync()
{
    var data = await client.GetStringAsync(url)
        .ConfigureAwait(false);  // Ne pas capturer le contexte de synchronisation
    
    // Important pour les performances
    // Mais pas dans les applications UI (WinForms, WPF, ASP.NET Core)
    
    return Process(data);
}

// 5. Gérer l'annulation
public async Task OperationLongueAsync(
    CancellationToken cancellationToken = default)
{
    // Vérifier régulièrement
    for (int i = 0; i < 100; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        
        await Task.Delay(100, cancellationToken);  // Delay aussi annulable
        // Faire du travail
    }
}

// 6. Attention aux deadlocks avec .Result ou .Wait()
public string Mauvaise()
{
    return MaMethodeAsync().Result;  // PEUT CRÉER UN DEADLOCK
}

public async Task<string> Bonne()
{
    return await MaMethodeAsync();  // Toujours utiliser await
}

14. Records

record
Les classes immuables faites simples. Introduits en C# 9.

// Avant : une classe immuable, c'était lourd
public class AgentAncien
{
    public string Nom { get; }
    public string Code { get; }
    public int Niveau { get; }
    
    public AgentAncien(string nom, string code, int niveau)
    {
        Nom = nom;
        Code = code;
        Niveau = niveau;
    }
    
    // Pour l'égalité, fallait redéfinir Equals, GetHashCode...
    // Beaucoup de code boilerplate
}

// Après : un record, c'est élégant
public record Agent(string Nom, string Code, int Niveau);
// 1 ligne. Immutabilité garantie. Égalité par valeur.

// Utilisation
var bond = new Agent("James Bond", "007", 9);
Console.WriteLine(bond.Nom);  // Propriétés en lecture seule

// Égalité par valeur (pas par référence)
var bond2 = new Agent("James Bond", "007", 9);
Console.WriteLine(bond == bond2);  // TRUE (mêmes valeurs)
Console.WriteLine(ReferenceEquals(bond, bond2));  // FALSE (objets différents)

// "with" expressions - créer une copie modifiée
var bondPromu = bond with { Niveau = 10 };
// bond reste inchangé (Niveau = 9)
// bondPromu est un nouvel objet (Niveau = 10)

Différences avec les classes
Records ne remplacent pas les classes. Ils complètent.

public record AgentRecord(string Nom, string Code, int Niveau);
// Positional record - le plus commun

public class AgentClass
{
    public string Nom { get; init; }  // init-only
    public string Code { get; init; }
    public int Niveau { get; init; }
    
    // On peut aussi avoir init-only dans les classes
    // Mais pas l'égalité par valeur automatique
}

// Les records peuvent avoir des méthodes
public record Operation(string Code, DateTime Date)
{
    public bool EstExpiree => Date < DateTime.Now.AddDays(-30);
    
    public string Rapport()
    {
        return $"Opération {Code} du {Date:d}";
    }
}

// Héritage possible
public record Personne(string Nom, DateTime Naissance);
public record AgentRecord(string Nom, DateTime Naissance, string Code)
    : Personne(Nom, Naissance);

Immutabilité
Le secret des données fiables.

// Un record est immuable par défaut
public record Contrat(string Client, decimal Montant, DateTime Echeance);
// Toutes les propriétés sont init-only

// Mais on peut rendre mutable (à éviter)
public record ContratMutable(string Client, decimal Montant, DateTime Echeance)
{
    public decimal Montant { get; set; } = Montant;  // Mutable
}

// L'immuabilité protège contre les modifications accidentelles
var contrat = new Contrat("Le Baron", 100000, DateTime.Now.AddDays(7));

List<Contrat> contrats = new() { contrat };

// Le contrat dans la liste ne peut pas être modifié
// contrats[0].Montant = 200000;  // ERREUR

// On doit en créer un nouveau
var contratMaj = contrat with { Montant = 200000 };
contrats[0] = contratMaj;  // OK

Cas d'usage (DTO)
Parfait pour les objets de transfert de données.

// API retournant un DTO
public record OperationDto(
    int Id,
    string Code,
    string Statut,
    DateTime DateDebut,
    DateTime? DateFin);

// Utilisation dans ASP.NET Core
[HttpGet("{id}")]
public async Task<ActionResult<OperationDto>> GetOperation(int id)
{
    var operation = await _repository.GetAsync(id);
    
    if (operation == null)
        return NotFound();
    
    return new OperationDto(
        operation.Id,
        operation.Code,
        operation.Statut,
        operation.DateDebut,
        operation.DateFin);
}

// Moins de code, plus de clarté
// Égalité par valeur utile pour les tests
// Immutabilité pour la sécurité

15. Pattern matching

switch avancé
Le switch a grandi. Il a appris à reconnaître plus que des entiers et des strings.

// Ancien switch
public string AncienneDescription(object obj)
{
    switch (obj)
    {
        case string s:
            return $"Chaîne : {s}";
        case int i:
            return $"Entier : {i}";
        case null:
            return "Null";
        default:
            return "Inconnu";
    }
}

// Nouveau switch (C# 8+)
public string NouvelleDescription(object obj) => obj switch
{
    string s => $"Chaîne de longueur {s.Length}",
    int i when i > 0 => $"Entier positif : {i}",
    int i when i < 0 => $"Entier négatif : {i}",
    int => "Zéro",
    Operation op => $"Opération {op.Code}",
    null => "Null",
    _ => "Inconnu"  // discard pattern
};

when
Les conditions dans les patterns.

public string EvaluerRisque(Operation op) => op switch
{
    { Risque: >= 9, Reussie: false } => "CRITIQUE - échec à haut risque",
    { Risque: >= 7 } when op.Date < DateTime.Now.AddDays(-30) 
        => "HAUT - ancienne opération",
    { Code: "NuitNoire" or "AubeRouge" } => "SPÉCIAL - code sensible",
    { Agents: { Count: > 5 } } => "ÉQUIPE LOURDE",
    { } => "STANDARD",  // Tout objet Operation non null
    null => "INCONNUE"
};

// when permet des conditions complexes
// Les propriétés sont extraites et typées automatiquement

Matching sur types
Reconnaître et déconstruire en une opération.

public string TraiterMessage(object message)
{
    // Pattern matching avec déconstruction
    return message switch
    {
        // Déconstruction de tuple
        (string code, int risque) => $"Code: {code}, Risque: {risque}",
        
        // Pattern positionnel sur record
        Operation(var code, var date) => $"Opération {code} le {date:d}",
        
        // Pattern avec conditions sur propriétés
        Agent { Niveau: >= 8, Code: not null } agent 
            => $"Élite: {agent.Nom} ({agent.Code})",
            
        // Pattern logique
        not null => "Message non null",
        null => "Message null"
    };
}

// Utile pour les hierarchies
public string ParlerAvec(Personne p) => p switch
{
    Agent a => $"Bonjour agent {a.Code}",
    Civil c when c.EstTemoin => "Monsieur, avez-vous vu quelque chose?",
    Civil c => $"Bonjour {c.Nom}",
    _ => "Bonjour"
};

16. Delegates, Func, Action

Délégués
Des pointeurs sur fonctions. Des promesses d'exécution future.

// Déclaration d'un type délégué
public delegate string TraitementRapport(string rapport);

// Méthode qui correspond à la signature
public string ChiffrerRapport(string rapport)
{
    return $"CHIFFRÉ: {rapport}";
}

public string NettoyerRapport(string rapport)
{
    return rapport.Replace("secret", "[REDACTED]");
}

// Utilisation
TraitementRapport traitement = ChiffrerRapport;
string resultat = traitement("Le message secret");  // Appelle ChiffrerRapport

// On peut changer
traitement = NettoyerRapport;
resultat = traitement("Le message secret");  // Appelle NettoyerRapport

// Multicast (plusieurs méthodes)
traitement += ChiffrerRapport;
resultat = traitement("secret");  
// Appelle NettoyerRapport, puis ChiffrerRapport
// Retourne le résultat de la dernière (ChiffrerRapport)

Func<> / Action<>
Les délégués génériques prédéfinis. Plus besoin de déclarer ses propres types.

// Func<TResult> - prend 0 à 16 paramètres, retourne TResult
Func<string> generateurCode = () => Guid.NewGuid().ToString()[..8].ToUpper();

// Func<T, TResult> - prend T, retourne TResult  
Func<string, string> chiffreur = s => $"🔐 {s}";

// Func<T1, T2, TResult> - prend T1 et T2, retourne TResult
Func<int, int, int> addition = (a, b) => a + b;

// Action - fait quelque chose, ne retourne rien
Action<string> logger = message => Console.WriteLine($"[LOG] {DateTime.Now}: {message}");

// Action<T1, T2> - prend deux paramètres
Action<string, int> alerter = (nom, niveau) => 
    Console.WriteLine($"ALERTE: {nom} niveau {niveau}");

// Utilisation
logger("Démarrage système");
string code = generateurCode();  // "A3F8B2C9"
string secret = chiffreur("message");
logger(secret);  // "[LOG] ...: 🔐 message"

Lambda expressions
Les fonctions anonymes. Le pouvoir de définir une logique sur place.

// Syntaxe lambda
Func<int, int, int> multiplicateur = (x, y) => x * y;

// Lambda avec plusieurs lignes
Func<string, string> processeur = input =>
{
    var trimmed = input.Trim();
    var upper = trimmed.ToUpper();
    return $"[{upper}]";
};

// Lambda capturant des variables locales
int seuil = 5;
Func<int, bool> estDangereux = risque => risque > seuil;

seuil = 8;  // La lambda voit le changement!
Console.WriteLine(estDangereux(7));  // FALSE (car 7 > 8? Non, 7 < 8)

// Attention aux closures
List<Func<int>> fonctions = new();
for (int i = 0; i < 3; i++)
{
    // MAUVAIS - toutes capturent la même variable i
    fonctions.Add(() => i * 2);
}

foreach (var f in fonctions)
    Console.WriteLine(f());  // Affiche 6, 6, 6 (pas 0, 2, 4!)

// BON - copie locale
for (int i = 0; i < 3; i++)
{
    int copie = i;  // Copie à chaque itération
    fonctions.Add(() => copie * 2);  // 0, 2, 4
}

// Expression lambda vs statement lambda
Func<int, int> carre = x => x * x;  // Expression (une ligne)
Func<int, int> carreAvecLog = x => 
{
    Console.WriteLine($"Calcul du carré de {x}");
    return x * x;
};  // Statement (bloc de code)

Fin du chapitre moderne.
Tu as maintenant les armes du C# contemporain.
Asynchrone pour la réactivité, records pour l'immuabilité, pattern matching pour l'expressivité, lambdas pour la flexibilité.

Mais souviens-toi :
Avec async/await vient la responsabilité de ne pas créer de deadlocks.
Avec les records, la tentation de tout immuabiliser (même quand c'est inutile).
Avec les lambdas, le risque de closures piégeuses.

Le prochain chapitre parlera des outils.
Des tests unitaires, du debugger, du profiling.
De comment s'assurer que ton code fait ce qu'il dit, et qu'il le fait vite.

Dans le monde réel, le code est jugé à deux choses :
Est-ce qu'il fonctionne ?
Et est-ce qu'il peut évoluer sans tout casser ?

Apprends à tester.
Apprends à mesurer.
Ou prépare-toi à déboguer à 3h du matin.