Module : Programmation .Net C#
Niveau : 4ème Génie Informatique (Spécialité Data Science)
Prérequis : TP1 validé & TP2 validé (Records, LINQ de base)
Après avoir maîtrisé les fondamentaux de la syntaxe C# dans le Chapitre 1, le TP 2 vous a
plongé dans le
paradigme fonctionnel. Vous avez remplacé les boucles foreach
manuelles par
des requêtes LINQ (Language Integrated Query) fluides et expressives. Ce chapitre
théorique
consolide ces acquis et explore les mécanismes internes qui font de LINQ l'outil de prédilection
pour la
Data Science en .NET.
Dans l'Activité 1 du TP 2, vous avez utilisé un record pour modéliser une
transaction. Pourquoi
?
En Data Science, une donnée est définie par son contenu, pas par son emplacement en mémoire. Si deux transactions ont le même ID, le même montant et la même date, elles sont identiques.
| Class (Référence) | Record (Valeur) |
|---|---|
| L'égalité vérifie si c'est le même objet en mémoire (pointeur). | L'égalité vérifie si toutes les propriétés sont identiques. |
| Mutable par défaut. | Immuable par défaut (Idéal pour le parallélisme). |
Travailler avec des données immuables évite les effets de bord indésirables. Si vous
devez modifier une
transaction, vous en créez une copie modifiée grâce au mot-clé with :
public record Transaction(int Id, double Montant);
var t1 = new Transaction(1, 100);
// t1.Montant = 200; // ERREUR ! Impossible de modifier.
// On crée une nouvelle version :
var t2 = t1 with { Montant = 200 };
// t1 existe toujours et vaut 100. t2 vaut 200.
LINQ (Language Integrated Query) n'est pas juste une librairie, c'est une syntaxe unifiée pour interroger n'importe quelle source de données (Objets mémoire, Bases de données SQL, XML, JSON...).
Contrairement à l'approche impérative (boucles imbriquées), LINQ permet de décrire un pipeline de transformation. Il est crucial de comprendre que ce pipeline est différé (Lazy).
Figure 1 : Le Pipeline de transformation LINQ et Agrégation
Le filtre correspond à la clause WHERE du SQL. Il utilise un prédicat (une
fonction qui renvoie
vrai ou faux).
// Syntaxe Lambda : x => condition
var franceOnly = transactions.Where(t => t.Pays == "France");
Pour ordonner une liste, on utilise OrderBy (Ascendant) ou OrderByDescending
(Descendant). On peut enchaîner avec ThenBy pour des tris multi-niveaux.
// Tri simple (Montant croissant)
var petitPrix = transactions.OrderBy(t => t.Montant);
// Tri complexe : Par Pays (A-Z), puis par Montant décroissant
var complexe = transactions.OrderBy(t => t.Pays)
.ThenByDescending(t => t.Montant);
La projection transforme la forme des données (SQL SELECT). Vous pouvez transformer un
objet
riche en une simple valeur, ou en un nouvel objet (Type Anonyme).
// On ne garde que les montants (List<double>)
var montants = transactions.Select(t => t.Montant);
// On crée un objet simplifié à la volée (Type Anonyme)
var rapport = transactions.Select(t => new {
Nom = t.Categorie,
PrixTTC = t.Montant * 1.2
});
C'est l'outil d'analyse par excellence. Il transforme une liste plate en une liste de "Groupes". Chaque groupe possède une Clé (`Key`) et une liste d'éléments.
var parPays = transactions.GroupBy(t => t.Pays);
foreach (var groupe in parPays) {
Console.WriteLine($"Pays : {groupe.Key}");
Console.WriteLine($"CA Total : {groupe.Sum(t => t.Montant)}"); // Affichage
}
Souvent, on ne veut pas une liste, mais un seul élément précis.
// Exception si videvarprem = transactions.First(t => t.Montant > 1000);// Renvoie null (ou 0) si vide (Plus sûr)varsafePrem = transactions.FirstOrDefault(t => t.Montant > 1000);
// Exception si aucun OU s'il y en a plusieursvarunique = transactions.Single(t => t.Id == 1);
Au-delà des bases, voici des méthodes très utiles pour le nettoyage de données :
boolaDesGeants = transactions.Any(t => t.Montant > 10000);booltoutEstPositif = transactions.All(t => t.Montant > 0);
varpage1 = resultats.Take(10); // Les 10 premiersvarpage2 = resultats.Skip(10).Take(10); // Les 10 suivants
var categoriesUniques = transactions.Select(t => t.Categorie).Distinct();
Comme en SQL, vous pouvez croiser deux listes (ex: Transactions et Clients) via une clé commune. Voici un exemple complet avec définition des données :
// 1. Définition des structures
public record Client(int Id, string Nom);
public record Transaction(int Id, int ClientId, double Amount);
// 2. Alimentation des listes
var clients = new List<Client> {
new(1, "Alice"),
new(2, "Bob")
};
var transactions = new List<Transaction> {
new(101, 1, 500), // Achat d'Alice
new(102, 2, 1200), // Achat de Bob
new(103, 1, 300) // Autre achat d'Alice
};
// 3. Requête Join
var historique = transactions.Join(clients,
t => t.ClientId, // Clé table 1 (FK)
c => c.Id, // Clé table 2 (PK)
(t, c) => new { // Projection du résultat
Client = c.Nom,
Montant = t.Amount,
Description = $"{c.Nom} a payé {t.Amount}€"
}
);
// 4. Affichage
foreach (var h in historique) {
Console.WriteLine(h.Description);
}
LINQ propose des méthodes performantes pour calculer des valeurs scalaires à partir d'une collection :
C'est le point fort de LINQ. Lorsque vous écrivez
var q = data.Where(...).Select(...),
rien ne se passe. Aucune donnée n'est filtrée. LINQ construit
simplement la requête en
mémoire.
L'exécution réelle se produit uniquement au moment de la consommation :
foreach..ToList() ou .ToArray()..Sum(), .Count() ou
.First().Cela permet d'optimiser les performances en ne traitant que les données strictement nécessaires.
Si vous venez du monde des bases de données, LINQ est très intuitif. C'est littéralement du "SQL pour le code".
| Concept SQL | Opérateur LINQ | Description |
|---|---|---|
| SELECT | .Select() | Projection / Transformation |
| WHERE | .Where() | Filtrage |
| ORDER BY | .OrderBy() / .OrderByDescending() | Tri |
| GROUP BY | .GroupBy() | Regroupement |
| JOIN | .Join() | Jointure interne |
| TOP / LIMIT | .Take() | Restriction du nombre de résultats |
| DISTINCT | .Distinct() | Dédoublonnage |
| COUNT() | .Count() | Comptage |
| MIN / MAX / AVG | .Min() / .Max() / .Average() | Agrégation |
Différence majeure : SQL est exécuté par le moteur de base de données (SGBD), tandis que LINQ to Objects est exécuté en mémoire par le CLR .NET.
LINQ est puissant, mais peut être lent si mal utilisé. Voici les règles d'or pour un ingénieur Data :
Module: .Net C# Programming
Level: 4th Year Computer Engineering (Data Science Specialty)
Prerequisite: TP1 Validated & TP2 Validated (Records, Basic LINQ)
After mastering the fundamentals of C# syntax in Chapter 1, Lab 2 (LAB 2) immersed you in the
functional paradigm. You replaced manual foreach loops with fluid and
expressive LINQ (Language Integrated Query) queries. This theoretical chapter
consolidates
these skills and explores the internal mechanisms that make LINQ the preferred tool for Data Science in
.NET.
In Activity 1 of LAB 2, you used a record to model a transaction. Why?
In Data Science, a piece of data is defined by its content, not its location in memory. If two transactions have the same ID, same amount, and same date, they are identical.
| Class (Reference) | Record (Value) |
|---|---|
| Equality checks if it is the same object in memory (pointer). | Equality checks if all properties are identical. |
| Mutable by default. | Immutable by default (Ideal for parallelism). |
Working with immutable data avoids unwanted side effects. If you need to modify a transaction, you
create a
modified copy using the with keyword:
public record Transaction(int Id, double Amount);
var t1 = new Transaction(1, 100);
// t1.Amount = 200; // ERROR! Cannot modify.
// Create a new version:
var t2 = t1 with { Amount = 200 };
// t1 still exists and is 100. t2 is 200.
LINQ (Language Integrated Query) isn't just a library; it's a unified syntax for querying any data source (In-memory objects, SQL databases, XML, JSON...).
Unlike the imperative approach (nested loops), LINQ allows you to describe a transformation pipeline. It is crucial to understand that this pipeline is deferred (Lazy).
Figure 1: The LINQ Pipeline: Definition vs Execution
The filter corresponds to the SQL WHERE clause. It uses a predicate (a function that
returns
true or false).
// Lambda Syntax: x => condition
var franceOnly = transactions.Where(t => t.Country == "France");
To sort a list, use OrderBy (Ascending) or OrderByDescending (Descending). You
can
chain with ThenBy for multi-level sorting.
// Simple Sort (Amount ascending)
var lowPrice = transactions.OrderBy(t => t.Amount);
// Complex Sort: By Country (A-Z), then by Amount descending
var complex = transactions.OrderBy(t => t.Country)
.ThenByDescending(t => t.Amount);
Projection transforms the shape of data (SQL SELECT). You can transform a rich object into
a
simple value, or into a new object (Anonymous Type).
// Keep only amounts (List<double>)
var amounts = transactions.Select(t => t.Amount);
// Create a simplified object on the fly (Anonymous Type)
var report = transactions.Select(t => new {
Name = t.Category,
PriceWithTax = t.Amount * 1.2
});
This is the ultimate analysis tool. It transforms a flat list into a list of "Groups". Each group has a Key and a list of elements.
var byCountry = transactions.GroupBy(t => t.Country);
foreach (var group in byCountry) {
Console.WriteLine($"Country : {group.Key}");
Console.WriteLine($"Total Revenue : {group.Sum(t => t.Amount)}"); // Display
}
Often, we don't want a list, but a single specific element.
// Exception if emptyvarfirst = transactions.First(t => t.Amount > 1000);// Returns null (or 0) if empty (Safer)varsafeFirst = transactions.FirstOrDefault(t => t.Amount > 1000);
// Exception if none OR if more than one existvarunique = transactions.Single(t => t.Id == 1);
Beyond the basics, here are very useful methods for data cleaning:
boolhasBigWhales = transactions.Any(t => t.Amount > 10000);boolallPositive = transactions.All(t => t.Amount > 0);
varpage1 = results.Take(10); // The first 10varpage2 = results.Skip(10).Take(10); // The next 10
var uniqueCategories = transactions.Select(t => t.Category).Distinct();
Like in SQL, you can merge two lists (e.g., Transactions and Customers) via a common key. Here is a complete example with data definition:
// 1. Structure Definitions
public record Client(int Id, string Name);
public record Transaction(int Id, int ClientId, double Amount);
// 2. List Population
var clients = new List<Client> {
new(1, "Alice"),
new(2, "Bob")
};
var transactions = new List<Transaction> {
new(101, 1, 500), // Alice's purchase
new(102, 2, 1200), // Bob's purchase
new(103, 1, 300) // Another purchase by Alice
};
// 3. Join Query
var history = transactions.Join(clients,
t => t.ClientId, // Key table 1 (FK)
c => c.Id, // Key table 2 (PK)
(t, c) => new { // Result Projection
Client = c.Name,
Amount = t.Amount,
Description = $"{c.Name} paid {t.Amount}€"
}
);
// 4. Display
foreach (var h in history) {
Console.WriteLine(h.Description);
}
LINQ offers performant methods to calculate scalar values from a collection:
This is LINQ's greatest strength. When you write
var q = data.Where(...).Select(...),
nothing happens. No data is filtered. LINQ simply constructs the query in
memory.
Actual execution happens only upon consumption:
foreach loop..ToList() or .ToArray()..Sum(), .Count(), or
.First().This optimizes performance by processing only strictly necessary data.
If you come from the database world, LINQ is very intuitive. It is literally "SQL for code".
| SQL Concept | LINQ Operator | Description |
|---|---|---|
| SELECT | .Select() | Projection / Transformation |
| WHERE | .Where() | Filtering |
| ORDER BY | .OrderBy() / .OrderByDescending() | Sorting |
| GROUP BY | .GroupBy() | Grouping |
| JOIN | .Join() | Inner Join |
| TOP / LIMIT | .Take() | Restrict result count |
| DISTINCT | .Distinct() | Deduplication |
| COUNT() | .Count() | Counting |
| MIN / MAX / AVG | .Min() / .Max() / .Average() | Aggregation |
Major Difference: SQL is executed by the Database Management System (DBMS), whereas LINQ to Objects is executed in memory by the .NET CLR.
LINQ is powerful but can be slow if misused. Here are golden rules for a Data Engineer: