Prise en main
Hello World
Les programmes C# commencent dans Main(). using System; importe le namespace System (Console, Math, etc.). C# 9+ supporte les top-level statements : un fichier avec juste Console.WriteLine("Hello"); est un programme valide. Le compilateur génère la classe et Main automatiquement.
using System;
class Program {
static void Main() {
Console.WriteLine("Hello, World!");
}
}Variables & types
C# est typé statiquement. var laisse le compilateur inférer le type (toujours de type sûr à la compilation). decimal (suffixe m) est pour les calculs financiers avec une précision exacte. const est une constante à la compilation ; readonly est une constante à l'exécution définie dans le constructeur.
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42; // implicitly typed (int)
const double TAX = 0.08;Interpolation de chaînes
L'interpolation de chaîne ($"...") intègre des expressions entre accolades. Les spécificateurs de format après : contrôlent la sortie (F2 pour 2 décimales, yyyy-MM-dd pour les dates). Les chaînes verbatim (@"") traitent les barres obliques inverses littéralement — utiles pour les chemins de fichiers et les regex. Combinez les deux : $@"...".
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");
// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}"); // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");
// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";Entrée & sortie
Console.ReadLine() lit une ligne complète en tant que chaîne. int.Parse convertit mais lance une exception sur une entrée invalide ; int.TryParse est plus sûr — il renvoie un booléen et utilise un paramètre out. Utilisez toujours TryParse pour l'entrée utilisateur afin d'éviter les exceptions dues à de mauvaises données.
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());
Console.WriteLine($"Hi {name}, you are {age}");
// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
Console.WriteLine($"Valid: {num}");
} else {
Console.WriteLine("Invalid number");
}Types nullables
int? est un type valeur nullable (Nullable<int>). L'opérateur ?. (null-conditional) accède en sécurité aux membres — renvoie null au lieu de lancer. L'opérateur ?? (null-coalescing) fournit une valeur par défaut pour null. Les types référence nullables de C# 8+ avertissent des nulls potentiels à la compilation.
int? maybeAge = null; // nullable int
if (maybeAge.HasValue) {
Console.WriteLine(maybeAge.Value);
}
string? maybeName = null; // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0; // null-conditional + null-coalescing
// ValueOrDefault
int age = maybeAge ?? 0; // 0 if nullChaînes
Méthodes de chaîne
Les chaînes C# sont immuables — les méthodes renvoient de nouvelles chaînes plutôt que de modifier l'originale. Méthodes courantes : Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. Pour une manipulation lourde de chaînes, utilisez StringBuilder pour éviter de créer de nombreuses chaînes intermédiaires.
string s = "Hello, World!";
Console.WriteLine(s.Length); // 13
Console.WriteLine(s.ToUpper()); // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", ")); // ["Hello", "World!"]StringBuilder
StringBuilder est mutable et efficace pour construire des chaînes dans des boucles. La concaténation de chaînes (+) crée une nouvelle chaîne à chaque fois, donc la concaténation répétée est O(n^2). StringBuilder s'amortit en O(n). Utilisez StringBuilder lorsque vous avez plus de quelques concaténations.
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();
// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;Formatage et analyse
string.Format utilise des espaces réservés {0}, {1}. Spécificateurs de format : F (fixe), N (nombre avec séparateurs), P (pourcentage), X (hex). L'analyse est sensible à la culture — les séparateurs décimaux diffèrent selon la locale. Utilisez CultureInfo pour un contrôle explicite, ou TryParse pour la sécurité.
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}"; // "3.14"
string s3 = $"{1234567:N0}"; // "1,234,567" (thousands separator)
// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");
// Culture-aware parsing
double euro = double.Parse("3,14",
System.Globalization.CultureInfo.GetCultureInfo("de-DE"));Comparaison de chaînes
Utilisez StringComparison.OrdinalIgnoreCase pour une comparaison insensible à la casse — c'est plus clair et plus rapide que ToLower() puis ==. Évitez == pour les comparaisons sensibles à la culture ; utilisez string.Compare avec un CultureInfo. Pour les clés de dictionnaire, utilisez StringComparer.OrdinalIgnoreCase.
string a = "Hello", b = "hello";
// Case-sensitive
bool equal = a == b; // false
bool equal2 = a.Equals(b); // false
// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase); // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
// Best practice for comparison
bool same = string.Equals(a, b,
StringComparison.OrdinalIgnoreCase);Expressions régulières
Regex (de System.Text.RegularExpressions) fournit la correspondance de patterns. Utilisez des chaînes verbatim @"" pour que les barres obliques inverses n'aient pas besoin de double échappement. Pour un usage répété, compilez une instance Regex une fois et réutilisez-la (RegexOptions.Compiled) pour de meilleures performances.
using System.Text.RegularExpressions;
string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value); // 123-456-7890
// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"
// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");Nombres & mathématiques
Types numériques
C# a des types à taille fixe : int (32 bits), long (64 bits), double (64 bits flottant), decimal (128 bits, pour le financier). Les tirets bas (9_000_000) améliorent la lisibilité. Utilisez decimal pour l'argent — il évite les erreurs d'arrondi en virgule flottante. Suffixes : L (long), f (float), m (decimal).
int i = 42;
long big = 9_000_000_000L; // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m; // exact decimal for money
byte b = 255;
Console.WriteLine(sizeof(int)); // 4
Console.WriteLine(int.MaxValue); // 2147483647Classe Math
Math fournit des méthodes statiques pour les opérations courantes. Math.Round utilise l'arrondi bancaire (arrondit au pair) par défaut — utilisez MidpointRounding.AwayFromZero pour l'arrondi scolaire. Math.BigMul gère la multiplication longue pour éviter le débordement.
double x = 2.5;
Math.Pow(x, 3); // 15.625
Math.Sqrt(x); // 1.581
Math.Abs(-5); // 5
Math.Floor(3.7); // 3
Math.Ceiling(3.2); // 4
Math.Round(3.5); // 4 (banker's rounding)
Math.Max(3, 7); // 7
Math.Min(3, 7); // 3
Math.PI; // 3.14159...Nombres aléatoires
Random génère des nombres pseudo-aléatoires. Créez une instance et réutilisez-la (en créer beaucoup dans une boucle peut produire des doublons en raison du seed basé sur le temps). Pour un aléatoire cryptographique, utilisez System.Security.Cryptography.RandomNumberGenerator. C# 8+ fournit Random.Shared pour la sécurité des threads.
var random = new Random();
int r = random.Next(1, 101); // 1-100
double d = random.NextDouble(); // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);
// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);
// Array of random items
int[] nums = Enumerable.Range(0, 5)
.Select(_ => random.Next(1, 100)).ToArray();Conversion de type
Les conversions implicites se produisent automatiquement lorsqu'il n'y a pas de perte de données (int vers double). Les casts explicites (type) sont nécessaires lorsque la précision peut être perdue. Convert.ToInt32 gère de nombreux types et arrondit (contrairement au cast qui tronque). Préférez toujours TryParse pour la conversion chaîne-vers-nombre afin d'éviter les exceptions.
// Implicit (safe, no data loss)
int i = 42;
double d = i; // int -> double
// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi; // 3
// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);
// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result); // falseDébordement d'entier et checked
Par défaut, le débordement d'entier s'enroule silencieusement (unchecked). Le bloc checked lance OverflowException en cas de débordement — utilisez-le pour le code critique pour la sécurité. Pour les très grands nombres, BigInteger (System.Numerics) gère les entiers à précision arbitraire sans débordement.
int a = int.MaxValue; // 2147483647
// a++; // overflow: wraps to -2147483648 (unchecked by default)
checked {
a++; // throws OverflowException!
}
// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big); // 1267650...376 (31 digits)Flux de contrôle
If / Else
if/else if/else est standard. L'opérateur ternaire (condition ? a : b) est une expression if/else concise. C# requiert des conditions booléennes — contrairement au C, les entiers ne sont pas implicitement convertis en bool. Utilisez des accolades même pour les instructions simples pour prévenir les bugs de maintenance.
int score = 85;
if (score >= 90) {
Console.WriteLine("A");
} else if (score >= 80) {
Console.WriteLine("B");
} else {
Console.WriteLine("C");
}
// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";Switch & pattern matching
Les expressions switch de C# 8+ (=>) sont concises et renvoient des valeurs. Le pattern _ est le cas par défaut. Le pattern matching (is, switch) supporte les patterns de type, les patterns de propriété et les patterns relationnels (> 18). C'est plus puissant que le switch traditionnel et réduit le boilerplate.
// Classic switch
int day = 3;
string name = day switch {
1 => "Mon", 2 => "Tue", 3 => "Wed",
6 or 7 => "Weekend",
_ => "Invalid" // default
};
// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
Console.WriteLine(s); // "hello"
}
// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
// matches if Age > 18 and Name == "Alice"
}Boucles
for est pour les itérations comptées ; foreach itère les collections (tableaux, listes, IEnumerable) ; while boucle jusqu'à ce qu'une condition soit fausse. foreach est en lecture seule — vous ne pouvez pas modifier la collection pendant l'itération. Utilisez for si vous avez besoin de l'index ou de modifier les éléments.
// for loop
for (int i = 0; i < 5; i++) {
Console.WriteLine(i);
}
// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
Console.WriteLine(fruit);
}
// while
int n = 5;
while (n > 0) Console.WriteLine(n--);Break, continue & goto
break sort de la boucle ou du switch le plus proche ; continue passe à l'itération suivante. C# n'a pas de break étiqueté pour les boucles imbriquées — utilisez un drapeau, extrayez vers une méthode avec return, ou utilisez LINQ. goto est valide dans switch pour les retombées mais est autrement déconseillé.
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // skip 3
if (i == 7) break; // stop at 7
Console.WriteLine(i); // 0 1 2 4 5 6
}
// goto in switch (rare but valid)
switch (code) {
case 1:
DoSomething();
goto case 2; // fall through
case 2:
DoMore();
break;
}Itérateurs & yield
yield return transforme une méthode en itérateur qui produit des valeurs paresseusement — une à la fois selon les besoins. C'est efficace en mémoire pour les grandes séquences ou infinies. Le compilateur génère une machine à états. yield break termine l'itération tôt. LINQ utilise largement les itérateurs.
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
for (int i = 0; i < 5; i++) {
yield return i;
}
}
foreach (int n in GetNumbers()) {
Console.WriteLine(n); // 0 1 2 3 4
}
// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
int n = 0;
while (true) yield return n++;
}Méthodes & délégués
Définition de méthode
Les méthodes peuvent être expression-bodied (=>) pour les one-liners. Les paramètres par défaut rendent les arguments optionnels. Les arguments nommés (name:) améliorent la lisibilité pour les appels avec beaucoup de paramètres et permettent de sauter les optionnels. C# ne supporte pas la surcharge de méthode avec juste des types de retour différents.
static int Add(int a, int b) {
return a + b;
}
// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;
// Default parameters
static void Greet(string name = "Guest", int times = 1) {
for (int i = 0; i < times; i++)
Console.WriteLine($"Hi {name}!");
}
Greet(); // Hi Guest!
Greet("Alice", 3); // Hi Alice! x3
Greet(times: 2); // named argumentout, ref & params
Les paramètres out doivent être assignés par la méthode (l'appelant n'a pas besoin d'initialiser). Les paramètres ref doivent être initialisés par l'appelant et peuvent être modifiés. params permet un nombre variable d'arguments. C# 7+ permet la déclaration out var en ligne : TryParse(s, out var result).
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
result = 0;
return int.TryParse(s, out result);
}
// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x); // x is now 6
// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3); // 6
Sum(); // 0Délégués & événements
Les délégués sont des pointeurs de fonction de type sûr. Func<T,TResult> prend des entrées et renvoie une valeur ; Action<T> renvoie void ; Predicate<T> renvoie bool. Les événements sont des délégués avec abonnement +=/-= et ?.Invoke pour un déclenchement sûr (vérification null). Utilisez les événements pour le pattern observer (pub/sub).
// Delegate type
delegate int MathOp(int a, int b);
MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4)); // 7
// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;
// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");Expressions lambda
Les lambdas sont des méthodes anonymes utilisant =>. Elles capturent les variables du périmètre englobant (fermetures). Les variables capturées sont évaluées lorsque le lambda s'exécute, pas lors de sa création — méfiez-vous de cela dans les boucles. Utilisez les lambdas largement avec LINQ, les événements et les rappels.
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;
// Type inference
Func<int, int, int> add = (a, b) => a + b;
// Statement lambda
Action<string> log = msg => {
Console.WriteLine($"[{DateTime.Now}] {msg}");
};
// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5)); // 15Méthodes d'extension
Les méthodes d'extension ajoutent des méthodes à des types existants sans les modifier. Le mot-clé this avant le premier paramètre le marque comme méthode d'extension. Elles doivent être dans une classe statique. LINQ est entièrement implémenté comme méthodes d'extension sur IEnumerable. C'est un moyen puissant d'ajouter des méthodes utilitaires.
static class StringExtensions {
public static int WordCount(this string s) {
return s.Split(new[] { ' ' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
string text = "Hello World C#";
Console.WriteLine(text.WordCount()); // 3
// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);Collections
List<T>
List<T> est un tableau dynamique (comme vector en C++ ou ArrayList en Java). Add/Remove sont O(1) amorti / O(n). Count donne le nombre d'éléments (pas la capacité). Find/FindAll utilisent des prédicats. Pour des insertions/suppressions fréquentes au milieu, LinkedList<T> peut être meilleur.
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count); // 4
Console.WriteLine(nums.Contains(4)); // true
// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);Dictionary<TKey, TValue>
Dictionary est une table de hachage avec recherche O(1) en moyenne. TryGetValue est plus sûr que l'indexeur (qui lance KeyNotFoundException). Pour les clés de chaîne insensibles à la casse, passez StringComparer.OrdinalIgnoreCase au constructeur. Dictionary ne préserve pas l'ordre d'insertion (utilisez OrderedDictionary si nécessaire).
var ages = new Dictionary<string, int> {
["Alice"] = 30,
["Bob"] = 25
};
ages["Charlie"] = 35;
// Safe access
if (ages.TryGetValue("Alice", out int age)) {
Console.WriteLine($"Alice: {age}");
}
// Iterate
foreach (var kv in ages) {
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
// Case-insensitive keys
var dict = new Dictionary<string, int>(
StringComparer.OrdinalIgnoreCase);HashSet<T> & SortedSet<T>
HashSet<T> stocke des éléments uniques avec recherche O(1) — utilisez pour la déduplication et le test d'appartenance. Il supporte les opérations ensemblistes (Union, Intersect, Except). SortedSet<T> (arbre rouge-noir) garde les éléments triés avec des opérations O(log n). Utilisez HashSet lorsque l'ordre n'a pas d'importance, SortedSet lorsqu'il compte.
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2); // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count); // 4
Console.WriteLine(set.Contains(3)); // true
// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other); // set: {3, 4}
set.UnionWith(other); // set: {3, 4, 5}
// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 }; // {1, 2, 3}Queue<T> & Stack<T>
Queue<T> est FIFO (Enqueue/Dequeue) — utilisez pour la planification de tâches, BFS. Stack<T> est LIFO (Push/Pop) — utilisez pour undo/redo, DFS, l'évaluation d'expressions. Les deux sont O(1) pour leurs opérations principales. Pour les scénarios concurrents, utilisez ConcurrentQueue et ConcurrentStack de System.Collections.Concurrent.
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue(); // "first"
string peek = queue.Peek(); // "second"
// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop(); // 2
int peek = stack.Peek(); // 1Tableaux
Les tableaux sont de taille fixe. Array.Sort trie sur place. Les tableaux multidimensionnels ([,]) sont rectangulaires (colonnes uniformes). Les tableaux en escalier ([][]) sont des tableaux de tableaux (les lignes peuvent avoir des longueurs différentes). Utilisez List<T> pour le dimensionnement dynamique ; utilisez des tableaux pour les données de taille fixe critiques pour la performance.
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length); // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);
// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]); // 3
// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };Classes & POO
Classe & propriétés
Les auto-propriétés ({ get; set; }) génèrent automatiquement les champs de support. Les propriétés expression-bodied (=>) calculent à l'accès. Les propriétés init-only de C# 9+ ({ get; init; }) ne sont définissables que pendant la construction, permettant l'immuabilité. Initialiseurs d'objet : new Person { Name = "X" }.
class Person {
// Auto-properties (C# 3+)
public string Name { get; set; }
public int Age { get; set; }
// Read-only property
public bool IsAdult => Age >= 18;
// Constructor
public Person(string name, int age) {
Name = name;
Age = age;
}
}
var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult); // trueHéritage & virtual
virtual marque une méthode pour la redéfinition ; override dans les sous-classes fournit la nouvelle implémentation. C# requiert un override explicite (contrairement à Java). sealed empêche une classe d'être héritée ou une méthode d'être redéfinie. Toutes les classes héritent implicitement de object (System.Object).
class Animal {
public virtual void Speak() {
Console.WriteLine("...");
}
}
class Dog : Animal {
public override void Speak() {
Console.WriteLine("Woof");
}
}
Animal a = new Dog();
a.Speak(); // Woof (polymorphism)
// sealed prevents further inheritance
sealed class Puppy : Dog { }Interfaces
Les interfaces définissent des contrats sans implémentation. Une classe peut implémenter plusieurs interfaces (contrairement à l'héritage unique de classe). C# 8+ permet les méthodes d'interface par défaut. Utilisez les interfaces pour le polymorphisme et l'injection de dépendances. Convention de nommage : préfixe avec I (IShape, ILogger).
interface IShape {
double Area(); // interface member
double Perimeter { get; }
}
class Circle : IShape {
public double Radius { get; set; }
public double Area() => Math.PI * Radius * Radius;
public double Perimeter => 2 * Math.PI * Radius;
}
// Default interface methods (C# 8+)
interface ILogger {
void Log(string msg);
void Error(string msg) => Log($"ERROR: {msg}"); // default
}Classes abstraites
Les classes abstraites ne peuvent pas être instanciées et peuvent avoir des membres abstraits (à redéfinir) et concrets (virtuels). Utilisez les classes abstraites lorsqu'il y a une implémentation partagée ; utilisez les interfaces pour des contrats purs. Une classe peut hériter d'une seule classe abstraite mais implémenter plusieurs interfaces.
abstract class Shape {
public abstract double Area(); // must be overridden
public virtual void Describe() {
Console.WriteLine($"Area: {Area()}");
}
}
class Rectangle : Shape {
public double Width { get; set; }
public double Height { get; set; }
public override double Area() => Width * Height;
}
// Cannot instantiate abstract class
// var s = new Shape(); // ERROR
var r = new Rectangle { Width = 3, Height = 4 };Records & structs
Les records (C# 9+) fournissent une égalité basée sur la valeur, l'immuabilité et une syntaxe concise — idéaux pour les DTOs et modèles de données. L'expression with crée une copie avec des propriétés modifiées. Les structs sont des types valeur (copiés à l'affectation, alloués sur la pile) — utilisez-les pour de petites données légères. Les classes sont des types référence (allouées sur le tas).
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);
var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2); // true (value equality)
// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };
// Struct: value type (stack-allocated)
struct Vector {
public double X, Y;
public Vector(double x, double y) { X = x; Y = y; }
}LINQ
Where & Select
LINQ (Language Integrated Query) fournit des requêtes de type SQL sur les collections. Where filtre, Select transforme (map). LINQ est paresseux — les requêtes s'exécutent uniquement lors de l'énumération (par ex., via ToList()). Cela permet un chaînage efficace sans collections intermédiaires. La syntaxe de méthode (ci-dessus) est la plus courante ; la syntaxe de requête est aussi disponible.
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };
// Filter
var evens = nums.Where(n => n % 2 == 0); // 2, 4, 6
// Map/Transform
var squares = nums.Select(n => n * n); // 1, 4, 9, 16, 25, 36
// Chain operations
var result = nums
.Where(n => n > 2)
.Select(n => n * 10)
.ToList(); // 30, 40, 50, 60Tri et regroupement
OrderBy/OrderByDescending trient ; ThenBy ajoute des critères de tri secondaires. GroupBy regroupe les éléments par clé, renvoyant des groupes IGrouping<key, element>. Chaque groupe a une Key et est lui-même un IEnumerable de ses membres. Cela remplace les boucles manuelles et les dictionnaires pour le regroupement.
var people = new[] {
new { Name = "Alice", Age = 30 },
new { Name = "Bob", Age = 25 },
new { Name = "Charlie", Age = 30 },
};
// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);
// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 peopleAgrégation
Les méthodes d'agrégation LINQ (Sum, Average, Min, Max, Count) calculent des valeurs uniques à partir de collections. Aggregate est la plus générale — c'est un fold/reduce qui applique une fonction cumulativement. Elles lancent une exception sur les séquences vides ; utilisez les variantes *OrDefault ou vérifiez d'abord la vacuité.
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Sum()); // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min()); // 1
Console.WriteLine(nums.Max()); // 5
Console.WriteLine(nums.Count()); // 5
// Count with predicate
int evens = nums.Count(n => n % 2 == 0); // 2
// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b); // 120
string joined = nums.Aggregate("", (s, n) => s + n); // "12345"First, Single & ElementAt
First renvoie le premier élément (lance si vide) ; FirstOrDefault renvoie la valeur par défaut (0 pour int, null pour les types référence) si non trouvé. Single requiert exactement un élément (lance sinon) — utilisez pour la validation. Any/All renvoient un bool sans énumérer toute la collection (court-circuit).
int[] nums = { 1, 2, 3, 4, 5 };
int first = nums.First(); // 1
int firstEven = nums.First(n => n % 2 == 0); // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10); // 0 (default)
int single = new[] { 42 }.Single(); // 42
// nums.Single(); // throws! more than one element
int third = nums.ElementAt(2); // 3 (0-indexed)
bool any = nums.Any(n => n > 3); // true
bool all = nums.All(n => n > 0); // trueJoin & Zip
Join effectue une jointure interne (comme SQL) en faisant correspondre les éléments par clé. Zip apparie les éléments de deux séquences par position. Les deux produisent de nouvelles séquences. LINQ supporte aussi GroupJoin (jointure gauche avec regroupement). Ce sont des outils puissants pour combiner des données liées de plusieurs sources.
var users = new[] {
new { Id = 1, Name = "Alice" },
new { Id = 2, Name = "Bob" },
};
var orders = new[] {
new { UserId = 1, Item = "Book" },
new { Id = 2, Item = "Pen" },
};
// Join (inner join)
var joined = users.Join(orders,
u => u.Id, o => o.UserId,
(u, o) => new { u.Name, o.Item });
// { Alice, Book }
// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"Gestion des erreurs
Try / Catch / Finally
Attrapez d'abord les exceptions spécifiques, puis l'Exception générale en dernier (du plus spécifique au moins). finally s'exécute toujours — utilisez-le pour le nettoyage (fermeture de fichiers, libération de ressources). Évitez catch (Exception) pour le flux de contrôle ; attrapez uniquement ce que vous pouvez gérer. Utilisez throw; (pas throw e;) pour préserver la trace de la pile.
try {
int x = 10;
int y = 0;
int z = x / y; // throws DivideByZeroException
} catch (DivideByZeroException e) {
Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
Console.WriteLine($"Unexpected: {e.Message}");
} finally {
// always runs, even with return/throw
Console.WriteLine("Cleanup");
}Exceptions personnalisées
Dérivez les exceptions personnalisées de Exception (ou d'une base plus spécifique). Ajoutez des champs de contexte qui aident au débogage. Appelez toujours le constructeur de base avec le message. Par convention, les noms de classe d'exception se terminent par Exception. Utilisez des exceptions personnalisées pour des conditions d'erreur spécifiques au domaine que les appelants peuvent gérer.
class ValidationError : Exception {
public string Field { get; }
public ValidationError(string field, string msg)
: base(msg) {
Field = field;
}
}
throw new ValidationError("email", "Invalid format");
try {
// ...
} catch (ValidationError e) {
Console.WriteLine($"{e.Field}: {e.Message}");
}using & IDisposable
using garantit que Dispose() est appelé même si une exception se produit — c'est l'équivalent C# de RAII pour la gestion des ressources. La déclaration using (C# 8+) est plus propre — Dispose est appelé en fin de périmètre. Implémentez IDisposable pour les classes qui détiennent des ressources non managées (handles de fichiers, connexions de base de données).
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
string content = file.ReadToEnd();
} // file.Dispose() called automatically
// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope
// Implementing IDisposable
class Resource : IDisposable {
public void Dispose() {
// release unmanaged resources
GC.SuppressFinalize(this);
}
}Gestion de null
Les types référence nullables de C# 8+ (string?) permettent la sécurité null à la compilation. ?. renvoie null au lieu de lancer NullReferenceException. ?? fournit une valeur de repli. L'expression throw (?? throw) est concise pour la validation. Activez <Nullable>enable</Nullable> dans .csproj pour la vérification null à l'échelle du projet.
string? name = null;
// Null-conditional operator
int? length = name?.Length; // null (no exception)
// Null-coalescing operator
string display = name ?? "Unknown"; // "Unknown"
// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));
// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }Filtres d'exception
Les filtres d'exception (when) ajoutent des conditions aux blocs catch — l'exception n'est attrapée que si le filtre est vrai. C'est plus puissant qu'attraper et relancer car cela préserve la trace de la pile. Le pattern de journalisation (when renvoie false) vous permet d'observer les exceptions sans les gérer.
try {
// ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
Console.WriteLine("Server error");
} catch (HttpRequestException e) {
Console.WriteLine($"HTTP error: {e.Message}");
}
// Log without catching
catch (Exception e) when (LogError(e)) {
// never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }E/S fichiers & async
Classe File (E/S rapide)
File fournit des méthodes statiques pour des opérations rapides de fichiers en une fois — simples mais chargent tout le fichier en mémoire. Pour les grands fichiers, utilisez StreamReader/StreamWriter pour traiter ligne par ligne. File.Exists vérifie l'existence mais il y a une race TOCTOU — gérez toujours les exceptions des opérations de fichiers.
using System.IO;
// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");
// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });
// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");
// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length); // bytesFlux (StreamReader/Writer)
StreamReader/StreamWriter traitent les fichiers ligne par ligne sans tout charger en mémoire — utilisez-les pour les grands fichiers. L'instruction using garantit que le fichier est fermé même si une exception se produit. StreamWriter met en mémoire tampon les données ; appelez Flush() pour écrire immédiatement, ou laissez Dispose s'en charger.
using System.IO;
// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
Console.WriteLine(line);
}
// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush(); // optional, using disposes and flushesasync / await
async/await permet des opérations asynchrones non bloquantes. La méthode renvoie Task<T> (ou Task pour void). await suspend la méthode sans bloquer le thread — le thread est libéré pour d'autres travaux. C'est essentiel pour les opérations liées aux E/S (fichiers, réseau, base de données) dans les applications UI et web.
async Task<string> ReadFileAsync(string path) {
using var reader = new StreamReader(path);
return await reader.ReadToEndAsync();
}
// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);
// Async doesn't block the calling thread
// The thread is freed while waiting for I/OTask & Parallel
Task.Run planifie du travail sur le pool de threads. Task.WhenAll attend plusieurs tâches concurremment (n'utilisez pas async/await pour le travail CPU — utilisez Task.Run). Parallel.For est pour les boucles parallèles CPU. PLINQ (AsParallel) parallélise les requêtes LINQ. Utilisez async pour les E/S, Parallel/PLINQ pour le CPU.
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;
// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2); // wait for both
// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
Process(i); // runs on multiple threads
});
// PLINQ
var results = nums.AsParallel()
.Where(n => IsPrime(n))
.ToArray();Sérialisation JSON
System.Text.Json est le sérialiseur JSON moderne et haute performance (remplace Newtonsoft.Json dans la plupart des cas). Serialize/Deserialize gèrent objet<->JSON. Utilisez JsonSerializerOptions pour le formatage (indenté, camelCase). Pour l'async, utilisez les méthodes basées sur les flux pour éviter de charger de grands JSON en mémoire.
using System.Text.Json;
var person = new { Name = "Alice", Age = 30 };
// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}
// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);
// With options
var options = new JsonSerializerOptions {
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);
// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);LINQ approfondi
Syntaxe de requête vs méthode
LINQ a deux syntaxes équivalentes. La syntaxe de requête est de type SQL et plus lisible pour les jointures/regroupements complexes. La syntaxe de méthode (fluent) est plus courante, supporte tous les opérateurs et s'enchaîne naturellement. Elles compilent vers le même IL. Utilisez la syntaxe de requête pour les requêtes complexes multi-clauses ; la syntaxe de méthode pour les chaînes simples. Les deux sont évaluées paresseusement (exécution différée).
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Query syntax (SQL-like)
var evens = from n in nums
where n % 2 == 0
orderby n descending
select n * 2;
// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
.OrderByDescending(n => n)
.Select(n => n * 2);
// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus moreExécution différée vs immédiate
LINQ utilise l'exécution différée — Where/Select/OrderBy construisent juste une requête ; elle s'exécute lors de l'énumération. Cela signifie que les résultats reflètent l'état de la source au moment de l'énumération, pas à la création de la requête. ToList/ToArray/Count/etc. forcent l'exécution immédiate, capturant un instantané. Attention : énumérer une requête différée deux fois l'exécute deux fois. Mettez en cache avec ToList si vous avez besoin de résultats stables.
var nums = new List<int> { 1, 2, 3 };
// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4); // query sees this!
Console.WriteLine(query.Count()); // 3 (2, 3, 4)
// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList(); // runs now
nums.Add(5);
Console.WriteLine(list.Count); // still 3 (snapshot)
// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, AllRegroupement et jointure
GroupBy regroupe les éléments par clé, renvoyant des séquences IGrouping<key, element>. Join effectue des jointures internes (clés correspondantes). La jointure de groupe (join...into) crée des résultats hiérarchiques et permet les jointures externes gauches via DefaultIfEmpty(). Ces opérations sont puissantes pour l'analyse de données. Pour les grands jeux de données, envisagez ToLookup pour les recherches répétées (c'est un regroupement pré-calculé).
var students = new[] {
new { Name = "Alice", Grade = "A", Dept = "CS" },
new { Name = "Bob", Grade = "B", Dept = "Math" },
new { Name = "Carol", Grade = "A", Dept = "CS" },
};
// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
Console.WriteLine(`{g.Key}: {g.Count()}`);
// CS: 2, Math: 1
}
// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
join c in courses on s.Dept equals c.Dept
select new { s.Name, c.Course };
// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
join c in courses on s.Dept equals c.Dept into sc
from c in sc.DefaultIfEmpty()
select new { s.Name, Course = c?.Course };Agrégation et quantificateurs
LINQ fournit les agrégations standard (Sum, Min, Max, Average, Count) et un Aggregate générique pour les réductions personnalisées (comme reduce de JavaScript). Les quantificateurs (Any, All, Contains) renvoient des booléens et court-circuitent — Any s'arrête à la première correspondance, All s'arrête au premier non-correspondant. Utilisez Any() (pas Count() > 0) pour vérifier l'existence — c'est plus efficace et lisible.
var nums = new[] { 1, 2, 3, 4, 5 };
// Aggregations
int sum = nums.Sum(); // 15
double avg = nums.Average(); // 3.0
int min = nums.Min(); // 1
int max = nums.Max(); // 5
int count = nums.Count(); // 5
// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b); // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b); // 25
// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0); // true
bool allPos = nums.All(n => n > 0); // true
bool contains3 = nums.Contains(3); // true
// LongCount for > int.MaxValue elements
long big = largeList.LongCount();IEnumerable et IQueryable
IEnumerable<T> est pour les collections en mémoire (LINQ to Objects) — utilise des délégués, s'exécute localement. IQueryable<T> est pour les sources distantes (Entity Framework, LINQ to SQL) — utilise des arbres d'expression, traduit vers le langage de requête de la source (SQL). IQueryable compose des requêtes côté serveur efficacement. Basculez vers IEnumerable avec AsEnumerable() lorsque vous avez besoin d'une logique côté client qui ne peut pas être traduite.
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1); // executes in memory
// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18); // generates SQL
// SELECT * FROM Users WHERE Age > 18
// IQueryable composes queries that execute server-side
var query = db.Users
.Where(u => u.Active)
.OrderBy(u => u.Name)
.Take(10);
// All translated to one SQL query
// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
.Where(u => ExpensiveCheck(u));Async/Await approfondi
Task et Task<T>
Task représente une opération async sans résultat ; Task<T> renvoie T. ValueTask<T> (C# 7) évite l'allocation tas lorsque le résultat est souvent disponible synchronement (scénarios de cache) — mais ne peut être attendu qu'une fois. Utilisez Task.FromResult pour des résultats synchrones dans des APIs async. Préférez Task à void pour les méthodes async (void est uniquement pour les gestionnaires d'événements).
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
await Task.Delay(1000);
Console.WriteLine("Done");
}
// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
await Task.Delay(500);
return 42;
}
// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
if (_cache != null) return new ValueTask<int>(_cache);
return new ValueTask<int>(LoadFromDbAsync());
}
// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42); // already complete
Task completed = Task.CompletedTask;CancellationToken
CancellationToken permet l'annulation coopérative. Passez-le aux méthodes async et vérifiez ThrowIfCancellationRequested() dans les boucles. L'appelant annule via CancellationTokenSource.Cancel() ; la méthode décide quand/comment répondre. Acceptez toujours un paramètre CancellationToken dans les APIs async (surtout le code de bibliothèque). Utilisez CancellationTokenSource avec un TimeSpan pour les délais. L'annulation lance OperationCanceledException.
async Task DownloadAsync(string url, CancellationToken ct) {
using var client = new HttpClient();
// Pass token to async operations
var data = await client.GetByteArrayAsync(url, ct);
// Check manually in loops
foreach (var chunk in data) {
ct.ThrowIfCancellationRequested();
Process(chunk);
}
}
// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Timed out or cancelled");
}
// Cooperative cancellation: caller cancels, method checks
cts.Cancel(); // request cancellationTask.WhenAll vs WhenAny
Task.WhenAll attend plusieurs tâches concurremment et continue lorsque toutes sont terminées (parallélisme). Task.WhenAny continue lorsque la première tâche se termine (course/redondance). WhenAll ne lance que la première exception par défaut ; inspectez IsFaulted/Exception de chaque tâche pour voir tous les échecs. Pour fire-and-forget, utilisez Task.Run et gérez les exceptions à l'intérieur pour éviter les exceptions non observées.
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
FetchAsync("url1"),
FetchAsync("url2"),
FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails
// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;
// WhenAll with exception handling
try {
await Task.WhenAll(tasks);
} catch (Exception ex) {
// Only first exception is thrown; access all via Task.Exception
foreach (var inner in tasks.Where(t => t.IsFaulted))
Console.WriteLine(inner.Exception);
}Flux async (IAsyncEnumerable)
IAsyncEnumerable<T> (C# 8) est un flux async — produit des éléments au fur et à mesure qu'ils deviennent disponibles, avec des await entre les productions. Consommé avec await foreach. Parfait pour le streaming de grands jeux de données ou de données en temps réel sans tout bufferiser en mémoire. L'attribut [EnumeratorCancellation] garantit que le jeton d'annulation circule correctement lorsque le flux est consommé avec WithCancellation.
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 0; i < 10; i++) {
await Task.Delay(100, ct);
yield return i; // async yield
}
}
// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
Console.WriteLine(n);
}
// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
yield return MapRecord(reader);
}Pièges async (interblocages)
Le piège async n°1 : appeler .Result ou .Wait() sur une Task peut interbloquer lorsqu'un SynchronizationContext est présent (apps UI, ancien ASP.NET). await capture le contexte ; .Result bloque le thread dont la continuation a besoin. Solution : rendez les méthodes async jusqu'en haut, ou utilisez ConfigureAwait(false) dans le code de bibliothèque. async void est dangereux — les exceptions ne peuvent pas être attrapées et il n'est pas awaitable. Utilisez async Task à la place.
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
string data = GetDataAsync().Result; // DEADLOCK risk!
return data;
}
// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
string data = await GetDataAsync();
return data;
}
// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.
// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result; // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);
// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!Événements & délégués
Bases des délégués
Les délégués sont des pointeurs de fonction de type sûr. Les délégués personnalisés (delegate int MathOp(int,int)) sont largement remplacés par les Action (retour void) et Func<T> (renvoie T) intégrés. Les délégués sont multicast — ils peuvent contenir plusieurs méthodes (combinées avec +). Lorsqu'invoqués, toutes les méthodes s'exécutent. La valeur de retour d'un délégué multicast est le résultat de la dernière méthode. Utilisez Action/Func pour la plupart du code moderne.
// Declare a delegate type
public delegate int MathOp(int a, int b);
// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;
MathOp op = Add;
Console.WriteLine(op(2, 3)); // 5
op = Mul;
Console.WriteLine(op(2, 3)); // 6
// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)
// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0; // returns boolÉvénements (Publisher/Subscriber)
Les événements sont une forme restreinte de délégués — seule la classe déclarante peut les invoquer, mais le code externe peut s'abonner/se désabonner (+=/-=). Utilisez EventHandler<TArgs> pour le pattern standard. Utilisez toujours l'opérateur null-conditionnel (Click?.Invoke) car un événement sans abonnés est null. Désabonnez-vous toujours pour prévenir les fuites de mémoire (l'événement détient une référence forte vers le gestionnaire).
public class Button {
// Event: restricted multicast delegate
public event EventHandler<EventArgs> Click;
public void SimulateClick() {
// Null-conditional invoke pattern (thread-safe)
Click?.Invoke(this, EventArgs.Empty);
}
}
// Custom event args
public class SearchEventArgs : EventArgs {
public string Query { get; init; }
}
// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod; // method group
// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;
void HandlerMethod(object sender, EventArgs e) { /* ... */ }INotifyPropertyChanged (liaison de données)
INotifyPropertyChanged est le pattern standard pour la liaison de données dans WPF, MAUI et WinForms. Lorsqu'une propriété change, déclenchez PropertyChanged pour que l'UI liée se mette à jour. [CallerMemberName] (C# 5) passe automatiquement le nom de la propriété appelante — pas de chaînes magiques. La vérification (if != value) prévient les notifications inutiles. Les ViewModels l'utilisent largement ; les générateurs de source (CommunityToolkit.Mvvm) peuvent auto-générer ce boilerplate.
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class ViewModel : INotifyPropertyChanged {
private string _name;
public string Name {
get => _name;
set {
if (_name != value) {
_name = value;
OnPropertyChanged(); // raises event
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
// CallerMemberName auto-fills the property name
protected void OnPropertyChanged([CallerMemberName] string prop = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
// WPF/MAUI bindings automatically update UI when Name changesÉvénement vs délégué
La différence clé : un champ délégué public peut être invoqué, réaffecté ou effacé par n'importe qui. Un événement restreint l'accès externe à uniquement += (s'abonner) et -= (se désabonner) — seule la classe déclarante peut l'invoquer ou l'effacer. Cette encapsulation est pourquoi les événements sont le standard pour le pattern observer. Les événements génèrent aussi des accesseurs add/remove thread-safe et s'intègrent aux outils de conception.
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
public Notify OnDone; // field-like, public
}
// External code can do:
// bad.OnDone(); // invoke (BAD: not the owner)
// bad.OnDone = null; // clear all subscribers (BAD)
// Event: only owner can invoke/clear
public class Good {
public event Notify OnDone; // restricted
public void Complete() {
OnDone?.Invoke(); // OK: owner invokes
}
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside
// Event = delegate + access restriction
// Under the hood: event generates add/remove accessorsÉvénements faibles (prévention des fuites de mémoire)
Les événements détiennent des références fortes vers les abonnés, causant des fuites de mémoire si l'abonné devrait être garbage collected mais que le publisher vit. Solutions : se désabonner explicitement (mieux), utiliser WeakEventManager (WPF), ou implémenter un pattern d'événement faible avec WeakReference. C'est une source courante de fuites dans les apps longues (serveurs, apps desktop). Désabonnez-vous toujours dans Dispose ou lorsque la durée de vie de l'abonné se termine.
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long
// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);
// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
public void Subscribe(EventHandler<TArgs> h) =>
_handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
public void Raise(object sender, TArgs args) {
_handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
foreach (var wr in _handlers) {
if (wr.TryGetTarget(out var h)) h(sender, args);
}
}
}
// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishersRéflexion
Informations de type
La réflexion inspecte les métadonnées de type à l'exécution. typeof(T) obtient un Type à la compilation ; obj.GetType() l'obtient à l'exécution. Type fournit Name, IsClass, IsValueType, et des méthodes pour énumérer les membres (GetMethods, GetProperties, GetFields). BindingFlags contrôlent ce qui est renvoyé (NonPublic, Instance, Static, Public). La réflexion est puissante mais lente — mettez en cache les résultats lorsque possible.
using System;
Type t = typeof(string);
// Or: Type t = "hello".GetType();
Console.WriteLine(t.Name); // String
Console.WriteLine(t.FullName); // System.String
Console.WriteLine(t.IsClass); // true
Console.WriteLine(t.IsValueType); // false
Console.WriteLine(t.IsAbstract); // false
// Get methods
foreach (var m in t.GetMethods())
Console.WriteLine(m.Name);
// Get properties
var props = t.GetProperties();
// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t)); // falseInstanciation et invocation
Activator.CreateInstance crée des objets dynamiquement. MethodInfo.Invoke appelle des méthodes via réflexion (lent en raison du boxing d'arguments et des vérifications de sécurité). Pour des appels répétés, CreateDelegate est beaucoup plus rapide — crée un délégué fortement typé qui contourne le surcoût de réflexion. La réflexion casse la sécurité de type à la compilation et est plus lente que les appels directs ; utilisez-la pour les frameworks (sérialisation, DI, ORMs) et les scénarios de cache.
using System.Reflection;
// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")
// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });
// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);
// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);
// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));Attributs
Les attributs attachent des métadonnées aux éléments de code (classes, méthodes, propriétés). Définissez avec [AttributeUsage(...)] spécifiant les cibles valides. Récupérez via GetCustomAttributes<T>() (générique, moderne) ou Attribute.GetCustomAttribute. Attributs intégrés : [Obsolete], [Serializable], [DllImport], [Conditional]. Les frameworks utilisent largement les attributs : [HttpGet] (ASP.NET), [JsonProperty] (JSON), [Table] (EF Core).
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
public string Name { get; }
public AuthorAttribute(string name) => Name = name;
}
// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
[Author("Charlie")]
public void DoWork() { }
}
// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name); // Alice, Bob
// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));Emit et génération de code dynamique
Reflection.Emit génère du IL à l'exécution pour une performance maximale dans les scénarios dynamiques (compilation d'expressions, sérialiseurs, frameworks mock). Vous construisez des types/méthodes et émettez des opcodes IL directement. C'est avancé — utilisez les arbres d'expression (Expression.Compile) pour du code dynamique plus simple. Emit est utilisé par DLR (langages dynamiques), les ORMs et les bibliothèques de sérialisation pour générer du code typé rapide à l'exécution.
using System.Reflection;
using System.Reflection.Emit;
// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);
// Add a method
var method = type.DefineMethod("Add",
MethodAttributes.Public | MethodAttributes.Static,
typeof(int), new[] { typeof(int), typeof(int) });
// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); // load first arg
il.Emit(OpCodes.Ldarg_1); // load second arg
il.Emit(OpCodes.Add); // add
il.Emit(OpCodes.Ret); // return
Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 }); // 7Arbres d'expression
Les arbres d'expression représentent le code comme données (un arbre de nœuds Expression). Ils sont le fondement d'IQueryable (les providers LINQ les traduisent en SQL, etc.). Compile() transforme une expression en un délégué exécutable. Vous pouvez construire des expressions manuellement pour la génération de code dynamique (plus lisible que Reflection.Emit). Les arbres d'expression permettent les ORMs, les moteurs de règles et le LINQ dynamique.
using System.Linq.Expressions;
// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;
// Inspect the tree
Console.WriteLine(expr.Body.NodeType); // Add
Console.WriteLine(expr.Body.Type); // int
// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3); // 6
// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18
// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();Méthodes d'extension & patterns
Méthodes d'extension
Les méthodes d'extension vous permettent d''ajouter' des méthodes à des types existants sans les modifier. Définies dans une classe statique avec 'this' sur le premier paramètre. Ce sont du sucre syntaxique — le compilateur réécrit email.IsEmail() en StringExtensions.IsEmail(email). LINQ est entièrement des méthodes d'extension sur IEnumerable. Utilisez les extensions pour ajouter des méthodes utilitaires à des classes sealed, des interfaces ou des tiers. Amenez-les dans le périmètre avec using.
public static class StringExtensions {
// Must be static, in static class, first param with 'this'
public static bool IsEmail(this string s) =>
s.Contains('@') && s.Contains('.');
public static string Repeat(this string s, int n) =>
string.Concat(Enumerable.Repeat(s, n));
// Chainable
public static string Truncate(this string s, int max) =>
s.Length <= max ? s : s[..max];
}
// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3); // "ababab"
string short_ = "hello world".Truncate(5); // "hello"
// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0); // extensionInterfaces fluentes
Les interfaces fluentes renvoient 'this' pour permettre le chaînage de méthodes, produisant du code lisible qui se lit comme une phrase. Courant dans les builders (QueryBuilder), la configuration (ASP.NET, EF) et les DSL. Les méthodes d'extension peuvent ajouter des méthodes fluentes aux types primitifs (5.Seconds()). Gardez les méthodes pures (renvoient une nouvelle instance) pour les APIs fluentes immuables, ou mutez et renvoyez this pour les builders.
public class QueryBuilder {
private string _table = "";
private string _where = "";
private int _limit;
public QueryBuilder From(string table) { _table = table; return this; }
public QueryBuilder Where(string clause) { _where = clause; return this; }
public QueryBuilder Limit(int n) { _limit = n; return this; }
public string Build() =>
$"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}
// Fluent chaining
var sql = new QueryBuilder()
.From("users")
.Where("age > 18")
.Limit(10)
.Build();
// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds(); // TimeSpan
var timeout = 2.Minutes();Méthodes itératrices (yield)
yield return transforme une méthode en itérateur — le compilateur génère une machine à états. L'exécution est paresseuse : le code ne s'exécute qu'au fur et à mesure que les éléments sont tirés. Cela permet les séquences infinies et le streaming efficace (traiter un élément à la fois sans bufferiser). yield break termine l'itération. La méthode doit renvoyer IEnumerable<T> ou IEnumerator<T>. Les opérateurs LINQ sont construits sur yield. Réénumérer réexécute la méthode.
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
for (int i = 0; i <= max; i += 2)
yield return i;
}
// Lazy: nothing runs until enumerated
var evens = Evens(100); // no execution yet
foreach (var e in evens) { /* runs here */ }
// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
foreach (var item in src) {
if (!pred(item)) yield break;
yield return item;
}
}
// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
int n = 0;
while (true) yield return n++;
}
var first10 = Naturals().Take(10); // 0..9Injection de dépendances
L'injection de dépendances (DI) passe les dépendances via les constructeurs plutôt que de les créer en interne. Avantages : testabilité (échanger les implémentations dans les tests), couplage lâche, responsabilité unique. Le conteneur DI intégré de .NET (Microsoft.Extensions.DependencyInjection) gère l'enregistrement et la durée de vie (AddTransient, AddScoped, AddSingleton). Dépendez toujours des interfaces, pas des classes concrètes, pour la flexibilité.
// Interface for the dependency
public interface IEmailSender {
Task SendAsync(string to, string body);
}
// Implementation
public class SmtpEmailSender : IEmailSender {
public async Task SendAsync(string to, string body) { /* SMTP */ }
}
// Consumer: depends on abstraction, not concretion
public class UserService {
private readonly IEmailSender _email;
public UserService(IEmailSender email) => _email = email; // inject
public async Task RegisterAsync(string email) {
// ... save user ...
await _email.SendAsync(email, "Welcome!");
}
}
// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();
// Resolve
var userService = app.Services.GetRequiredService<UserService>();Patterns Factory et Builder
Le pattern Factory centralise la création d'objets, cachant les classes concrètes aux appelants (utile lorsque la création est complexe ou le type choisi à l'exécution). Le pattern Builder construit des objets complexes étape par étape avec des méthodes fluentes, évitant les 'constructeurs télescopiques' (beaucoup de paramètres). Les deux améliorent la lisibilité et la maintenabilité. Les records C# avec les expressions 'with' remplacent souvent les builders pour les données immuables.
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }
public static class ShapeFactory {
public static IShape Create(string type) => type switch {
"circle" => new Circle(),
"square" => new Square(),
_ => throw new ArgumentException()
};
}
// Builder: step-by-step construction of complex objects
public class Pizza {
public string Size { get; set; }
public bool Cheese { get; set; }
public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
private readonly Pizza _p = new();
public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
public Pizza Build() => _p;
}
var pizza = new PizzaBuilder()
.WithSize("large").AddCheese().AddPepperoni().Build();Pattern matching (C# 7-10+)
Patterns de type et is
Le pattern matching (C# 7+) combine la vérification de type et la liaison de variable en une étape : 'o is string s' vérifie et assigne. Les patterns de propriété ({ Length: > 3 }) correspondent aux propriétés d'objet. C# 9 ajoute les combinateurs 'not', 'and', 'or'. Les expressions switch (C# 8) renvoient des valeurs et utilisent des patterns — beaucoup plus propre que les chaînes if-else. Le _ est le pattern discard (cas par défaut).
object o = "hello";
// Type pattern (C# 7)
if (o is string s) {
Console.WriteLine(s.Length); // s is string here
}
// Combined with null check
if (o is string { Length: > 3 } longStr) {
Console.WriteLine(longStr); // string with length > 3
}
// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }
// Switch on type
string Describe(object o) => o switch {
int i => $"Integer: {i}",
string s => $"String: {s}",
double d => $"Double: {d}",
null => "null",
_ => "Unknown"
};Patterns de propriété et positionnels
Les patterns de propriété correspondent aux propriétés d'objet : { Prop: pattern }. Les patterns positionnels déconstruisent via la méthode Deconstruct (les records l'ont automatiquement). Les patterns se composent : vous pouvez les imbriquer et les combiner avec des opérateurs relationnels (<, >, etc.). C# 10 permet le raccourci de propriété imbriquée (Name.Length au lieu de Name: { Length: }). Le pattern matching rend la logique conditionnelle complexe déclarative et lisible.
public record Point(int X, int Y);
public record Person(string Name, int Age);
// Property pattern
string Describe(Person p) => p switch {
{ Age: < 18 } => "Minor",
{ Age: >= 65 } => "Senior",
{ Name.Length: > 10 } => "Long name",
{ } => "Adult" // non-null Person
};
// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
(0, 0) => "Origin",
( > 0, > 0) => "Q1",
( < 0, > 0) => "Q2",
( < 0, < 0) => "Q3",
( > 0, < 0) => "Q4",
_ => "On axis"
};
// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };Patterns relationnels et logiques (C# 9)
C# 9 a introduit les patterns relationnels (<, >, <=, >=) et les combinateurs logiques (and, or, not) pour les patterns. Ceux-ci rendent les expressions switch expressives pour les vérifications de plage et la logique de catégorie. 'or' combine les patterns (correspond si l'un ou l'autre) ; 'and' requiert les deux ; 'not' nie. Les parenthèses groupent pour la précédence. Cela remplace les chaînes if-else verbeuses par une logique déclarative, exhaustive (vérifiée par le compilateur).
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
< 0 => "Negative",
0 => "Zero",
> 0 and < 10 => "Small positive",
>= 10 and < 100 => "Medium",
>= 100 => "Large"
};
// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;
// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
Student => 0.2m,
Senior or Veteran => 0.15m,
not null => 0.0m,
null => throw new ArgumentNullException()
};
// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);Patterns de liste (C# 11)
Les patterns de liste (C# 11) correspondent à la forme des tableaux et listes : vide [], un seul élément [x], exact [a, b], ou avec des tranches [first, .., last]. Le pattern slice (..) capture zéro ou plusieurs éléments du milieu dans une variable. Combiné avec var et les gardes (when), cela permet une correspondance expressive sur les séquences. Utile pour l'analyse, la validation et l'implémentation d'algorithmes.
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };
string Describe(int[] arr) => arr switch {
[] => "Empty",
[single] => $"Single: {single}",
[first, second] => $"Pair: {first}, {second}",
[first, .., last] => $"First: {first}, Last: {last}", // slice
[.., var last] => $"Last: {last}",
_ => "Other"
};
// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}
// With guards
string Classify(int[] arr) => arr switch {
[var a, var b] when a == b => "Equal pair",
[var a, var b] => "Unequal pair",
_ => "Other"
};Expressions switch
Les expressions switch (C# 8) sont des expressions qui renvoient des valeurs, contrairement aux instructions switch. Elles utilisent le pattern matching (=>) et sont plus concises. Le switch de tuple ((a, b) switch) gère plusieurs valeurs. Le compilateur vérifie l'exhaustivité pour les enums (avertit si un cas manque). Incluez toujours un défaut (_) sauf si vous voulez une exception à l'exécution. Les expressions switch sont préférées aux instructions pour la logique renvoyant une valeur.
// Traditional switch statement (verbose)
string Old(int n) {
switch (n) {
case 1: return "one";
case 2: return "two";
default: return "many";
}
}
// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
1 => "one",
2 => "two",
_ => "many"
};
// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
(6 or 7, _) => "Weekend",
(_, true) => "Holiday",
(1, false) => "Monday",
_ => "Weekday"
};
// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)Records & immuabilité
Bases des records (C# 9)
Les records (C# 9) sont des types référence avec une égalité basée sur la valeur — deux records sont égaux si leurs données sont égales (contrairement aux classes, qui utilisent l'égalité de référence). Le compilateur génère Equals, GetHashCode, ToString et Deconstruct automatiquement. L'expression 'with' crée une copie avec des propriétés modifiées (mutation non destructive). Les records sont idéaux pour les DTOs, les objets de valeur et les modèles de données immuables.
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);
// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
// Value equality (auto-generated)
Console.WriteLine(p1 == p2); // true (compares properties)
Console.WriteLine(p1.Equals(p2)); // true
// ToString (auto-generated, readable)
Console.WriteLine(p1); // Person { Name = Alice, Age = 30 }
// Deconstruction
var (name, age) = p1;
// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31Record structs (C# 10)
C# 10 a ajouté les record structs (types valeur avec fonctionnalités de record) et les readonly record structs (types valeur immuables). Choisissez : record class (type référence, pour les données plus grandes/partagées), record struct (type valeur, pour les petites données, évite l'allocation tas), readonly record struct (type valeur immuable, le plus sûr). Les record structs ont l'égalité de valeur et le support 'with' comme les record classes. Utilisez readonly record struct pour les petites valeurs immuables comme Point, Money.
// record class (default, reference type)
public record PersonRef(string Name, int Age);
// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);
// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);
// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
public int Value { get; set; } = Value;
}
var p = new Point(1, 2);
var p2 = p with { X = 5 }; // copy with X=5
Console.WriteLine(p == p2); // false
// Records can have additional members
public record Person(string Name) {
public int Age { get; init; } // extra init-only prop
public string DisplayName => Name.ToUpper();
}Setters init-only
Les setters init-only (C# 9) permettent la définition pendant l'initialisation de l'objet mais pas après — immuabilité après construction sans boilerplate de constructeur. 'required' (C# 11) force l'appelant à définir une propriété dans l'initialiseur (vérification à la compilation). Les records utilisent les init setters par défaut. Cela permet les patterns d'objets immuables avec la syntaxe d'initialiseur d'objet, qui est plus lisible que les paramètres de constructeur.
public class Config {
// init: settable only during construction (object initializer)
public string Name { get; init; } = "";
public int Timeout { get; init; } = 30;
}
var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other"; // ERROR: init-only after construction
// Required properties (C# 11): must be set in initializer
public class User {
public required string Email { get; init; }
public string? DisplayName { get; init; }
}
// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };
// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
// public int Y { get; init; }Héritage de records
Les records supportent l'héritage : un record dérivé inclut les paramètres du record de base dans son constructeur. L'expression 'with' préserve le type à l'exécution (créant un Dog à partir d'un Dog, pas un Animal). L'égalité vérifie le type à l'exécution — deux records sont égaux uniquement s'ils sont du même type avec des données égales. Cela fait que les records fonctionnent correctement dans les collections polymorphiques, contrairement à l'égalité de valeur naïve.
// Base record
public record Animal(string Name);
// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);
// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);
var d = new Dog("Rex", "Labrador");
Console.WriteLine(d); // Dog { Name = Rex, Breed = Labrador }
// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name); // Dog (not Animal)
// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b); // true
// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c); // false (different runtime types)Records vs classes vs structs
Les classes utilisent l'égalité de référence (== compare les références) ; les records utilisent l'égalité de valeur (compare les données) ; les structs utilisent l'égalité de valeur par défaut mais sont des types valeur (copiés à l'affectation). Utilisez les records pour les données immuables avec sémantique de valeur (DTOs, objets de valeur, messages). Utilisez les classes pour les entités muables avec identité (User, Order) et les hiérarchies d'héritage. Utilisez les structs pour les petites valeurs (≤16 octets) qui devraient être copiées (Point, Color, Money).
// Class: reference type, reference equality
public class PersonClass {
public string Name { get; set; }
public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2); // false (different references)
// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2); // true (value equality)
// Struct: value type, value equality (manual)
public struct PointStruct {
public int X, Y;
public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2)); // true (default struct equality)
// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)Pattern matching approfondi
Patterns de propriété
Les patterns de propriété correspondent aux propriétés d'objet en ligne. L'expression switch renvoie une valeur directement. Les patterns combinés (and/or) créent des conditions flexibles. null et _ (discard) gèrent les cas limites. Cela remplace les chaînes if-else verbeuses par du code déclaratif et lisible.
static decimal CalcDiscount(Customer c) => c switch
{
{ IsPremium: true, Orders.Count: > 100 } => 0.20m,
{ IsPremium: true } => 0.10m,
{ Orders.Count: > 50 } => 0.05m,
{ Country: "US" or "CA" } => 0.03m,
null => 0m,
_ => 0m
};Patterns de liste
Les patterns de liste (C# 11) correspondent aux tableaux et séquences indexables. [] correspond à vide, [single] à un élément, [first, .. rest] utilise le pattern slice pour capturer les éléments restants. Utile pour analyser des commandes, valider des séquences et les algorithmes récursifs.
int Sum(int[] n) => n switch
{
[] => 0,
[int single] => single,
[int first, int second] => first + second,
[int first, .. int[] rest] => first + Sum(rest),
};
// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4Patterns de tuple
Les patterns de tuple correspondent plusieurs valeurs simultanément. Chaque cas teste un tuple de valeurs. Combinés avec les patterns relationnels (>, <, >=) et les patterns logiques (and, or, not), cela crée une correspondance multidimensionnelle puissante sans instructions if imbriquées.
static string Classify(int t, bool rain) => (t, rain) switch
{
(< 0, _) => "Freezing",
(>= 0 and < 15, true) => "Cold and wet",
(>= 0 and < 15, false) => "Cold",
(>= 15 and < 25, _) => "Mild",
(>= 25, true) => "Hot and wet",
(>= 25, false) => "Hot",
};Patterns de type & gardes
Les patterns de type correspondent les types à l'exécution. La clause when ajoute des gardes supplémentaires. var correspond à tout (y compris null). L'ordre compte : les patterns plus spécifiques doivent venir avant les généraux. C'est la façon idiomatique de faire la distribution basée sur le type en C# moderne.
object Process(object input) => input switch
{
int i when i > 0 => $"Positive: {i}",
int i => $"Non-positive: {i}",
string s when s.Length > 10 => s.Substring(0, 10),
string s => s,
IList list => $"List with {list.Count} items",
var x => x?.ToString() ?? "null"
};Patterns imbriqués
Les patterns s'imbriquent arbitrairement profondément. var à l'intérieur d'un pattern capture la valeur correspondante pour l'utiliser dans les gardes ou le corps de l'expression. Cet exemple correspond aux propriétés de Point à l'intérieur des records Line. Les patterns imbriqués sont puissants pour valider des graphes d'objets complexes de manière concise.
record Point(int X, int Y);
record Line(Point Start, Point End);
string Describe(Line line) => line switch
{
Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
when x1 == x2 && y1 == y2 => "Degenerate line",
Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
_ => "Diagonal line"
};Records & expressions with
Égalité de valeur des records
Les records fournissent l'égalité basée sur la valeur par défaut : deux instances avec les mêmes données sont égales. Le compilateur génère Equals, GetHashCode, ToString et l'opérateur ==. Les records sont des types référence mais conçus pour l'immuabilité. Utilisez-les pour les DTOs, les objets de valeur et les données qui devraient être comparées par contenu.
public record Person(string Name, int Age);
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2); // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1); // Person { Name = Alice, Age = 30 }Expressions with
L'expression with crée une copie d'un record avec des propriétés modifiées. L'original reste inchangé (mutation non destructive). C'est la façon idiomatique de mettre à jour des données immuables. Sous le capot, le compilateur utilise un constructeur de copie protégé et des setters init-only.
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };
// Original is unchanged (immutable)
Console.WriteLine(alice.Age); // 30
Console.WriteLine(alice31.Age); // 31
Console.WriteLine(aliceMarried.Name); // Alice SmithPositionnel vs init-only
Les records positionnels utilisent la syntaxe de constructeur primaire et génèrent des méthodes deconstruct. Les records init-only utilisent la syntaxe d'initialiseur d'objet, permettant les valeurs par défaut et les modificateurs required. Choisissez positionnel pour les types de valeur simples ; init-only pour les records complexes avec des propriétés optionnelles ou calculées.
// Positional record (primary constructor)
public record Point(int X, int Y);
// Init-only record (more flexible properties)
public record Employee
{
public required string Name { get; init; }
public decimal Salary { get; init; }
public string Dept { get; init; } = "General";
}
var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };Record structs & héritage
Les record structs sont des types valeur (copiés à l'affectation) et ne peuvent pas hériter d'autres record structs. Les records de type référence supportent l'héritage. readonly record struct empêche la mutation. Utilisez record struct pour les petites valeurs immuables ; record class pour les plus grands objets qui bénéficient de la sémantique de référence et de l'héritage.
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);
// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal); // True
var copied = dog with { Breed = "Poodle" };Deconstruct & matching
Les records positionnels auto-génèrent une méthode Deconstruct, permettant la déconstruction en tuple. Cela s'intègre parfaitement avec le pattern matching : vous pouvez correspondre aux propriétés du record positionnellement ou par nom. La déconstruction fonctionne dans les patterns var, les patterns de tuple et les expressions switch.
var person = new Person("Alice", 30);
var (name, age) = person; // Deconstruct
Console.WriteLine($"{name} is {age}");
// Works with pattern matching
string Greet(Person p) => p switch
{
("Alice", _) => "Hi Alice!",
(_, < 18) => "Hello young one",
var (n, a) => $"Hello {n}, age {a}"
};Générateurs de source
Générateur de source de base
Les générateurs de source s'exécutent à la compilation et ajoutent des fichiers source C# à la compilation. IIncrementalGenerator est l'API moderne (C# 9+). RegisterPostInitializationOutput ajoute du code statique sans analyser le code utilisateur. Les générateurs sont en lecture seule : ils peuvent ajouter des fichiers mais ne peuvent pas modifier le code existant.
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("Hello.g.cs", """
namespace Generated;
public static class Hello
{
public static string World => "Hello!";
}
""");
});
}
}Génération depuis des attributs
ForAttributeWithMetadataName trouve les types marqués avec un attribut spécifique. Le pipeline : le provider de syntaxe filtre les nœuds, la transformation extrait les données, Collect regroupe les résultats, RegisterSourceOutput émet le code. Ce pipeline incrémental met en cache les résultats et ne régénère que lorsque les entrées changent, gardant les builds rapides.
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
"MyApp.AutoToStringAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
.Collect()
.RegisterSourceOutput((spc, classes) =>
{
foreach (var cls in classes)
GenerateToString(spc, cls);
});Sortie StringBuilder
Les générateurs produisent du source sous forme de chaînes. StringBuilder est efficace pour la sortie multi-lignes. La classe cible doit être partial pour que le générateur ajoute des membres. Les fichiers générés apparaissent dans l'IDE sous Dependencies > Analyzers. Utilisez le suffixe .g.cs par convention pour distinguer les fichiers générés.
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
var sb = new StringBuilder();
sb.AppendLine($"namespace {cls.ContainingNamespace};");
sb.AppendLine($"public partial class {cls.Identifier}");
sb.AppendLine("{");
sb.AppendLine(" public override string ToString()");
sb.AppendLine(" {");
var props = cls.Members.OfType<PropertyDeclarationSyntax>();
sb.AppendLine($" return $"{cls.Identifier}: " +
string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
sb.AppendLine(" }");
sb.AppendLine("}");
ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}JsonSerializerContext
System.Text.Json inclut un générateur de source intégré qui pré-compile la logique de sérialisation, éliminant la réflexion à l'exécution. Cela améliore les performances et supporte la compilation AOT (Native AOT). Déclarez les types avec JsonSerializable, puis utilisez le contexte généré pour les appels serialize/deserialize.
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }
// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);Cas d'usage courants
Cas d'usage populaires des générateurs : génération de DTO, enregistrement DI, configuration fortement typée, profils AutoMapper, modèles de journalisation et sérialisation JSON. Des bibliothèques comme MediatR, AutoMapper et Microsoft.Extensions.Logging utilisent des générateurs pour réduire le boilerplate et améliorer la compatibilité AOT.
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }
// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();
// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)Span & Memory
Bases de Span
Span<T> fournit une vue de type sûr et mémoire-sûre sur de la mémoire contiguë sans copie. Il peut envelopper des tableaux, de la mémoire stackalloc, de la mémoire non managée ou des chaînes. Span est un ref struct (pile uniquement), donc il ne peut pas être boxed, stocké dans des champs ou capturé par des lambdas. Utilisez-le pour le découpage et l'analyse haute performance.
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };
// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3); // [20, 30, 40]
slice[0] = 99; // Modifies original array
// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5); // "Hello"Memory pour async
Memory<T> est la contrepartie tas-sûr de Span<T>. Il peut être stocké dans des champs, capturé par des lambdas et utilisé à travers les frontières await. Convertissez en Span<T> via .Span lorsque vous faites le travail réel. Utilisez Memory pour les APIs async et Span pour les chemins chauds synchrones.
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
await Task.Delay(100);
Span<byte> span = buffer.Span;
for (int i = 0; i < span.Length; i++)
span[i] = (byte)(span[i] * 2);
}
byte[] data = new byte[1024];
await ProcessAsync(data); // Implicit conversion to Memory<byte>Éviter les allocations
Span élimine les allocations de chaînes intermédiaires lors de l'analyse et du découpage. Pour les chemins chauds traitant de grandes entrées (analyse HTTP, JSON, fichiers de log), cela peut réduire la pression GC considérablement. int.TryParse et autres primitives ont des surcharges Span. Appelez ToString() uniquement lorsque vous avez réellement besoin d'une chaîne.
// Traditional (allocates new string)
string Extract(string input, int start, int len)
=> input.Substring(start, len);
// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
=> input.Slice(start, len);
// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
=> int.TryParse(span, out result);stackalloc & non managé
stackalloc alloue sur la pile d'appels (auto-libéré, pas de pression GC) mais est limité aux petites tailles. Pour les grands tampons, utilisez NativeMemory.Alloc/Free (C# 7+) ou Marshal.AllocHGlobal. Enveloppez toujours la mémoire non managée dans un Span et libérez-la dans un bloc finally pour prévenir les fuites.
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = i;
// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
try
{
Span<int> span = new Span<int>(ptr, 1024);
span.Fill(42);
}
finally
{
NativeMemory.Free(ptr);
}
}APIs compatibles Span
Concevez les APIs pour accepter Span<T> ou ReadOnlySpan<T> pour les tampons, évitant les allocations de tableau. L'appelant décide d'utiliser stackalloc, un tableau ou de la mémoire non managée. Span.TryWrite (C# 10+) formate directement dans un span sans allouer de chaîne, utile pour la journalisation et la sérialisation.
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
int count = 0;
foreach (var v in new[] { 1, 2, 3 })
{
if (count >= dest.Length) break;
dest[count++] = v;
}
return count;
}
// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());Chaînes interpolées approfondies
Spécificateurs de format
L'interpolation de chaîne ($"...") intègre des expressions entre accolades. Les spécificateurs de format après un deux-points contrôlent la sortie : C pour la devise, F pour les décimales fixes, X pour l'hex, D pour les chiffres avec remplissage, P pour le pourcentage, yyyy-MM-dd pour les dates. Le compilateur traduit cela en string.Format ou FormattableString.
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;
Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}"); // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}"); // FF
Console.WriteLine($"Padded: {age:D5}"); // 00030
Console.WriteLine($"Percent: {0.85:P0}"); // 85%Interpolation de chaîne brute
Les chaînes interpolées verbatim ($@) préservent les barres obliques inverses littéralement et permettent le contenu multi-lignes. L'interpolation de chaîne brute (C# 11) utilise des triples guillemets et élimine tout échappement : pas besoin d'échapper les accolades ou les guillemets. Le nombre de $ contrôle le nombre d'accolades nécessaires pour l'interpolation.
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
""name"": ""{name}"",
""age"": {age}
}}";
// Raw string interpolation (C# 11)
string query = $$"""
SELECT * FROM Users
WHERE Name = '{{name}}'
AND Age > {{age - 5}}
""";
// $$ means {{ }} for interpolation, no escaping neededFormattableString & culture
FormattableString préserve la structure d'interpolation, permettant un formatage différé avec des cultures spécifiques. C'est critique pour les bibliothèques qui ne devraient pas dépendre de la culture courante. FormattableString.Invariant force la culture invariante, prévenant les problèmes spécifiques à la locale dans la sérialisation et la journalisation.
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";
// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);
// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");Gestionnaires de chaînes interpolées
Les gestionnaires de chaînes interpolées (C# 10) laissent le compilateur passer les composants d'interpolation directement à une méthode, évitant l'allocation de chaîne lorsque le résultat n'est pas nécessaire. C'est ainsi que fonctionne la journalisation haute performance : si le niveau de log est désactivé, aucune chaîne n'est construite. Le gestionnaire reçoit les littéraux et les valeurs formatées séparément.
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
{
enabled = logger.IsEnabled;
// Skip allocation if logging is disabled
}
public void AppendLiteral(string s) { /* ... */ }
public void AppendFormatted<T>(T value) { /* ... */ }
}
// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");Formatage basé sur Span
Span.TryWrite formate les chaînes interpolées directement dans un tampon alloué sur la pile, évitant entièrement l'allocation tas. string.Create avec une chaîne interpolée et une culture vous donne un formatage sensible à la culture sans allocations intermédiaires. Ce sont des techniques essentielles pour les chemins chauds dans les applications à haut débit.
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];
if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
var result = buffer[..chars];
Console.WriteLine(result);
}
// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");Namespaces à portée de fichier
Syntaxe à portée de fichier
Les namespaces à portée de fichier (C# 10) déclarent le namespace pour tout le fichier avec un point-virgule au lieu d'accolades. Cela supprime un niveau d'indentation, rendant le code plus plat et plus facile à lire. Un seul namespace à portée de fichier par fichier est autorisé. Tous les types du fichier appartiennent à ce namespace.
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
public class UserService
{
public void Process() { }
}
}
// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;
public class UserService
{
public void Process() { }
}Usings globaux
Les usings globaux (C# 10) déclarés dans un fichier s'appliquent à tous les fichiers du projet. Combinez avec les namespaces à portée de fichier pour un boilerplate propre et minimal. Les projets .NET SDK auto-génèrent les usings globaux pour les namespaces courants (ImplicitUsings). Ordre : usings globaux, puis usings de fichier, puis namespace.
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;
// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;
namespace MyApp.Api;
public class UserController
{
// Can use List, LINQ, Json, Models without per-file usings
}Namespaces imbriqués
Les namespaces à portée de fichier utilisent des noms en pointillé pour la hiérarchie : MyApp.Services.Authentication est équivalent à des blocs de namespace imbriqués. Plusieurs fichiers peuvent partager le même namespace. Vous ne pouvez pas imbriquer des types dans un namespace différent au sein du même fichier ; utilisez des fichiers s éparés à la place.
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;
public class TokenService { }
// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services; // Same namespaceConfiguration de projet
ImplicitUsings dans le fichier de projet auto-génère les usings globaux pour les namespaces System courants. Combiné avec les namespaces à portée de fichier, cela élimine la plupart du boilerplate. Nullable active les types référence nullables. Ce sont les valeurs par défaut pour les nouveaux projets .NET 6+.
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
<!-- ImplicitUsings generates:
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
-->Migration & bonnes pratiques
Utilisez .editorconfig pour imposer les namespaces à portée de fichier (IDE0161). L'IDE fournit une migration en un clic du traditionnel vers la portée de fichier. Bonne pratique : un type public par fichier, avec le nom de fichier correspondant au nom du type. Cela garde les fichiers petits et la navigation facile.
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error // Prefer file-scoped
// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace
// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }Membres requis
Propriétés requises
Le modificateur required (C# 11) force les appelants à initialiser les propriétés lors de la création d'objet. Le compilateur émet CS9035 si un membre requis n'est pas défini. Cela remplace la validation de constructeur pour les propriétés obligatoires. Fonctionne avec les setters init-only et les initialiseurs d'objet, fournissant des garanties à la compilation.
public class User
{
public required Guid Id { get; init; }
public required string Name { get; init; }
public required string Email { get; init; }
public string? Phone { get; init; } // Optional
}
// Compiler enforces initialization
var user = new User
{
Id = Guid.NewGuid(),
Name = "Alice",
Email = "[email protected]"
};
// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };SetsRequiredMembers
L'attribut SetsRequiredMembers indique au compilateur qu'un constructeur initialise tous les membres requis, permettant aux appelants d'utiliser le constructeur sans initialiseur d'objet. Cela fait le pont entre les propriétés requises et les constructeurs traditionnels. L'attribut ne vérifie pas à l'exécution ; c'est une promesse à la compilation.
public class Configuration
{
public required string ConnectionString { get; init; }
public required int Port { get; init; }
[SetsRequiredMembers]
public Configuration(string conn, int port)
{
ConnectionString = conn;
Port = port;
}
}
// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
ConnectionString = "localhost",
Port = 5432
};Required avec records
Les membres requis fonctionnent avec les records. Pour les records positionnels, les propriétés requises sont déclarées dans le corps, pas le constructeur primaire. L'expression with préserve automatiquement les valeurs requises. Les valeurs par défaut (comme OpenedAt) rendent les propriétés optionnelles sans le modificateur required.
public record Account
{
public required string AccountNumber { get; init; }
public required decimal Balance { get; init; }
public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}
// Positional record with required
public record Customer(string Name)
{
public required string Email { get; init; }
}
var cust = new Customer("Alice") { Email = "[email protected]" };Init vs required vs set
init rend une propriété définissable uniquement pendant la construction (immuable après). required force l'initialisation. Ensemble (required + init) ils créent des propriétés immuables obligatoires. required + set permet la modification post-construction. Choisissez selon si la valeur devrait changer après la création.
// Init-only: can be set in initializer, but not required
public class Settings
{
public string Name { get; init; } = "Default";
}
// Required + init: must be set, immutable after
public class SecureSettings
{
public required string ApiKey { get; init; }
}
// Required + set: must be set, but can be modified later
public class MutableConfig
{
public required string Environment { get; set; }
}Sérialisation JSON
System.Text.Json respecte le modificateur required : la désérialisation lance JsonException si une propriété requise manque du JSON. Cela fournit une validation à l'exécution correspondant au contrat à la compilation. Combinez avec init pour des DTOs immuables garantis d'avoir tous les champs obligatoires peuplés.
using System.Text.Json;
public class ApiResponse
{
public required string Status { get; init; }
public required int Code { get; init; }
public string? Message { get; init; }
}
// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);
// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");Expressions de collection
Syntaxe d'expression de collection
Les expressions de collection (C# 12) fournissent une syntaxe uniforme [..] pour initialiser tout type de collection. Le compilateur infère le type et utilise le builder approprié. Cela remplace la syntaxe verbeuse new List<int> { 1, 2, 3 }. Fonctionne avec les tableaux, List<T>, Span<T>, IEnumerable<T> et les collections personnalisées avec un attribut CollectionBuilder.
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];
// Empty collections
int[] empty = [];
List<int> emptyList = [];
// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];Opérateur spread
L'opérateur spread (..) aplatit une collection dans l'expression de collection englobante. Cela remplace Concat, AddRange et les boucles manuelles. Vous pouvez mélanger les spreads avec des éléments individuels. Le spread fonctionne avec tout IEnumerable<T>, facilitant la combinaison de plusieurs sources de données de manière concise.
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];
// Spread elements with ..
int[] combined = [..a, ..b, 7, 8]; // [1,2,3,4,5,6,7,8]
// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];
// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];Patterns de retour et de paramètre
Les expressions de collection fonctionnent dans les instructions return, les arguments et les affectations. Le compilateur convertit vers le type cible automatiquement. Les spreads conditionnels (ternaire avec ..) vous permettent d'inclure optionnellement des éléments. Cela rend la construction de collections à partir de plusieurs sources optionnelles propre et lisible.
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];
// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);
// Conditional spread
bool includeExtra = true;
var items = [
1, 2, 3,
.. includeExtra ? [4, 5] : []
];Builders de collection personnalisés
L'attribut CollectionBuilder connecte un type de collection à une méthode factory, permettant le support des expressions de collection. La méthode Create reçoit un ReadOnlySpan<T> d'éléments. Cela permet aux collections tierces de participer à la syntaxe uniforme [..]. Le pattern builder garde l'initialisation efficace.
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
private readonly List<T> _items = new();
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
public static MyCollection<T> Create(ReadOnlySpan<T> items)
{
var col = new MyCollection<T>();
foreach (var item in items) col._items.Add(item);
return col;
}
}
// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];Collections params
C# 12 permet params avec tout type de collection, pas seulement les tableaux. Les expressions de collection peuvent être passées aux méthodes params. Lorsque la cible est Span<T>, le compilateur peut utiliser l'allocation sur pile pour les petites collections, évitant entièrement l'allocation tas. Cela rend les méthodes variadiques à la fois ergonomiques et efficaces.
// C# 12: params with collection expressions
void Log(params string[] messages)
{
foreach (var m in messages) Console.WriteLine(m);
}
// Traditional params call
Log("Error", "Warning", "Info");
// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);
// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]); // May use stackalloc internallySnippets C# associés
Copy-paste ready code for common tasks.
LINQ Query
Filter, project, sort, group, and aggregate sequences with LINQ.
Async and Await
Run I/O concurrently with async methods, await, and Task.WhenAll.
Properties
Encapsulate state with auto, computed, validated, and init-only properties.
Generics
Write type-parameterized methods and classes with constraints.
Delegates and Events
Define delegate types and publish events with safe subscription.
Reflection
Inspect type metadata and invoke members at runtime via System.Reflection.
Collections
Use List, Dictionary, HashSet, Queue, Stack, and read-only views.
File I/O
Read, write, append, copy, and JSON-serialize files with File and streams.
Was this helpful?