1. Intro
Quand tu bosses juste avec une liste "plate" d’objets (genre une liste de nos produits ou d’utilisateurs des confs précédentes), c’est easy : tu filtres, tu transformes, tu tries. Mais dans les vraies applis business, en entretien (et même dans les devoirs à la maison !), tu tombes souvent sur des collections qui contiennent d’autres collections à l’intérieur.
Par exemple, t’as une classe User qui a une liste de commandes. Ou, genre, une classe Product qui a une liste d’avis. Et maintenant, le défi : comment choper la liste de toutes les commandes de tous les users ? Ou récupérer tous les avis de tous les produits ? Et si tu veux la liste de tous les produits dans toutes les commandes ?
Dans ces cas-là, un simple Select et même la magie de C# ne suffisent pas toujours. C’est là que débarque le héros — l’opérateur SelectMany, qui transforme une "liste de listes" en juste une "liste".
Une analogie de cuisine
Imagine que t’as une boîte, dans laquelle y’a encore des boîtes, et dans ces boîtes, des cookies. Ta mission — tout verser dans un grand saladier. Tu vas pas prendre chaque boîte et sortir les cookies un par un, tu vas tout balancer d’un coup dans le saladier.
C’est exactement ce que fait SelectMany : il "aplatit" la collection, transformant une "boîte de boîtes" en un seul "saladier" avec tout dedans.
2. Petit rappel d’appli
Dans les confs précédentes, on a codé une appli simple pour gérer des users et des produits. Pour cette conf, on va ajouter des collections "imbriquées".
Imaginons qu’on a ces classes :
public class User
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public List<Product> Products { get; set; }
}
public class Product
{
public string Name { get; set; }
}
Maintenant, on a une liste d’utilisateurs, chacun avec une liste de commandes, et chaque commande — une liste de produits.
Comment choper tous les produits de toutes les commandes de tous les users ?
Disons qu’on veut faire une liste générale de tous les produits — sans imbrication, juste tout dans un "saladier". Si on tente un simple Select, ça donne quoi ?
List<User> users = ...; // imaginons qu’on a déjà les données quelque part
var allOrders = users.Select(u => u.Orders);
Le type de la variable allOrders sera... IEnumerable<List<Order>>. On a donc une "liste de listes", c’est-à-dire plein de collections de commandes. Mais nous, on veut juste une seule liste plate de toutes les commandes.
On peut tenter encore :
var allProducts = users.Select(u => u.Orders)
.Select(oList => oList.Select(o => o.Products));
Et là ? Maintenant, on a une "liste de listes de listes" ! En fait, on complique la structure au lieu de la simplifier.
3. La solution : l’opérateur SelectMany
C’est là que toute la puissance de SelectMany entre en jeu. Son taf — "aplatir" (flatten) les collections imbriquées en une seule.
La syntaxe générale :
collection.SelectMany(item => item.CollectionInterne)
Dans notre cas :
var allOrders = users.SelectMany(u => u.Orders);
// Type : IEnumerable<Order>
Maintenant, on a juste une séquence de toutes les commandes de tous les users.
On va encore plus loin ! Tous les produits de toutes les commandes de tous les users :
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
// Type : IEnumerable<Product>
Et en une seule ligne ?
Ouais, c’est possible aussi :
var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));
Lis cette chaîne étape par étape, sauf si t’es un robot.
4. Un gros exemple pour piger
On va créer des données de test et voir concrètement comment SelectMany marche.
var users = new List<User>
{
new User
{
Name = "Alice",
Orders = new List<Order>
{
new Order
{
Id = 1,
Products = new List<Product>
{
new Product { Name = "Biscuit" },
new Product { Name = "Lait" },
}
},
new Order
{
Id = 2,
Products = new List<Product>
{
new Product { Name = "Chocolat" }
}
}
}
},
new User
{
Name = "Bob",
Orders = new List<Order>
{
new Order
{
Id = 3,
Products = new List<Product>
{
new Product { Name = "Café" },
new Product { Name = "Thé" }
}
}
}
}
};
On "verse" tous les produits dans un saladier
var allProducts = users
.SelectMany(u => u.Orders)
.SelectMany(o => o.Products);
foreach (var product in allProducts)
{
Console.WriteLine(product.Name);
}
Résultat du programme :
Biscuit
Lait
Chocolat
Café
Thé
Qu’est-ce qui se passe à chaque étape ?
- users.SelectMany(u => u.Orders) — on prend tous les users et on "déplie" leurs commandes dans une seule collection de commandes (là, on a 3 commandes).
- .SelectMany(o => o.Products) — on déplie la liste des produits de toutes les commandes.
Attention : Si on utilisait un simple Select, on aurait une collection de collections (genre IEnumerable<List<Product>>). Mais avec SelectMany, on a une collection "plate" de produits : IEnumerable<Product>.
5. À quoi ça ressemble en schéma
Avant SelectMany
users (List<User>)
└── User #1
│ └── Orders (List<Order>)
│ ├── Order #1 -> Products (List<Product>)
│ └── Order #2 -> Products (List<Product>)
└── User #2
└── Orders (List<Order>)
└── Order #3 -> Products (List<Product>)
Après le premier SelectMany(u => u.Orders)
IEnumerable<Order>: [Order #1, Order #2, Order #3]
Après le second SelectMany(o => o.Products)
IEnumerable<Product>: [Biscuit, Lait, Chocolat, Café, Thé]
6. Comparaison : Select vs SelectMany
| Select | SelectMany | |
|---|---|---|
| Retourne | Une collection de "collections" | Une collection plate |
| Exemple | → |
→ |
| À utiliser | Si tu veux garder la structure imbriquée | Si tu veux juste une séquence plate |
Tableau : quand utiliser quel opérateur
| Ce que tu veux faire | À utiliser | Résultat |
|---|---|---|
| Obtenir une liste de listes (sans "aplatir") | Select | |
| Obtenir une séquence "plate" | SelectMany | |
| Obtenir des paires "parent-enfant" | SelectMany avec un sélecteur de résultat | |
| Juste transformer les éléments d’une collection "plate" | Select | |
7. Tips utiles
Syntaxe Query (pour les fans du style "SQL")
Pour ceux qui aiment l’expressivité — LINQ permet d’écrire les requêtes dans une syntaxe qui ressemble à SQL :
var allProducts = from user in users
from order in user.Orders
from product in order.Products
select product;
Ce code fait la même chose que la chaîne de SelectMany, mais ça ressemble à des "from" imbriqués.
"Aplatir" une matrice
Le cas classique pour SelectMany — bosser avec un tableau à deux dimensions ou une liste de listes.
List<List<int>> matrix = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5 },
new List<int> { 6 }
};
Comment avoir une seule liste de tous les nombres ?
var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
Console.Write(value + " "); // 1 2 3 4 5 6
}
8. Syntaxe avancée
Parfois, en utilisant la surcharge de SelectMany, tu peux retourner non seulement l’élément de la sous-collection, mais aussi des infos de l’extérieur.
Exemple : tu veux obtenir des paires "Nom d’utilisateur — Nom du produit" pour chaque produit dans chaque commande.
var userProductPairs = users.SelectMany(
user => user.Orders.SelectMany(
order => order.Products,
(order, product) => new { UserName = user.Name, ProductName = product.Name }
)
);
foreach (var pair in userProductPairs)
{
Console.WriteLine($"{pair.UserName} a commandé {pair.ProductName}");
}
Ici, le deuxième paramètre du lambda — c’est l’élément externe. Super utile pour ne pas "perdre" l’info des niveaux supérieurs.
9. Scénarios pratiques
Plein de commandes, plein de produits
T’es dev d’un e-shop. Tu veux compter combien de produits uniques ont été achetés par tous les clients ? Voilà :
var uniqueProductNames = users
.SelectMany(user => user.Orders)
.SelectMany(order => order.Products)
.Select(product => product.Name)
.Distinct();
foreach (var name in uniqueProductNames)
{
Console.WriteLine(name);
}
Créer une collection plate à partir d’une hiérarchie
T’as une liste de départements dans une boîte, chaque département — des employés. Tu veux la liste de tous les employés de la boîte :
var allEmployees = departments.SelectMany(d => d.Employees);
Bosser avec des strings : "aplatir" un tableau de mots en caractères
string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
Console.Write(c + " "); // h e l l o w o r l d
}
10. Pièges classiques et subtilités
Maintenant que t’es chaud avec la puissance de SelectMany, c’est le moment de te prévenir sur quelques pièges fréquents.
Le plus classique — croire que Select va te donner une collection "plate". En vrai, il retourne toujours une collection de collections si la fonction interne retourne une collection. Résultat : tu dois faire des doubles boucles ou tu te perds dans les types.
Deuxième piège — oublier que "l’aplatissement" fait perdre l’info sur l’élément externe : genre, si tu bosses juste avec les produits, tu sais plus dans quelle commande ou chez quel user il était, sauf si tu utilises la surcharge avec sélecteur de résultat.
Troisième subtilité — si tes listes internes peuvent être null, pense à protéger avant d’utiliser SelectMany : filtre ou remplace par une liste vide, sinon tu vas te manger un NullReferenceException plus vite que tu tapes F5.
GO TO FULL VERSION