1. Introduction
On a sûrement entendu à l'école : « C'est une fonction d'une fonction ». Les fonctions d'ordre supérieur (Higher-Order Functions, HOF) — ce sont des fonctions qui prennent des fonctions comme arguments, ou qui retournent des fonctions comme résultat, ou les deux à la fois.
En termes simples : si ta méthode peut recevoir une autre fonction en paramètre (par ex. un delegate ou une lambda), ou la renvoyer — félicitations, tu as une fonction d'ordre supérieur !
Une fonction d'ordre supérieur, c'est comme une clé passe-partout : elle peut non seulement ouvrir une porte elle-même, mais aussi te donner une autre clé pour ouvrir une porte plus tard.
Applications dans la vraie vie
Tout ça sonne bien et un peu abstrait. Mais à quoi ça sert vraiment dans le quotidien d'un dev C# ? La réponse courte :
Les fonctions d'ordre supérieur rendent le code flexible, réutilisable et concis.
C'est la base de trucs comme LINQ, le filtrage et le tri de collections, la construction de pipelines de traitement, la configuration de callbacks, d'événements, et même l'injection de dépendances.
Quelques scénarios concrets :
- Limiter le comportement d'une méthode en lui passant la logique (par ex. filtrage, tri, transformation).
- Écrire un handler de données générique dans lequel tu "visses" l'opération voulue via un delegate.
- Construire des chaînes de traitement ("pipelines") où chaque fonction modifie les données selon sa recette.
- Créer des abstractions cross-platform : que faire sous Windows, que faire sous Linux ? Il suffit de passer la bonne fonction.
2. Exemples simples de fonctions d'ordre supérieur
Fonction qui prend une autre fonction
Le classique — une méthode qui prend un delegate ou une lambda en paramètre.
// Fonction d'ordre supérieur : prend la fonction process en paramètre
void ForEach<T>(IEnumerable<T> collection, Action<T> process)
{
foreach (var item in collection)
{
process(item); // On appelle la fonction passée en argument
}
}
// Utilisation :
var numbers = new List<int> { 1, 2, 3 };
ForEach(numbers, n => Console.WriteLine($"Élément : {n}"));
Que se passe-t-il ici ? La méthode ForEach ne sait pas quoi faire avec chaque élément. Tout ce qu'elle fait — c'est appeler le processeur passé (process). Ce processeur peut faire n'importe quoi — afficher, sauvegarder en base, dessiner, etc.
Oui, c'est comme ça que marche la méthode ForEach sur les collections en C#, et presque toutes les méthodes LINQ — ce sont des fonctions d'ordre supérieur !
Fonction qui retourne une fonction
Maintenant un exemple un peu plus costaud — une méthode qui retourne une fonction.
// Fonction d'ordre supérieur : retourne une autre fonction
Func<int, int> CreateMultiplier(int factor)
{
// On retourne une lambda utilisant la variable factor
return x => x * factor;
}
// Utilisation :
var multiplyBy10 = CreateMultiplier(10);
Console.WriteLine(multiplyBy10(7)); // 70
Ici CreateMultiplier renvoie une fonction qui multiplie son argument par un facteur donné. C'est une sorte de "factory" de fonctions.
Fonction qui prend et retourne une fonction
Func<int, int> Compose(Func<int, int> f, Func<int, int> g)
{
// Retourne une fonction qui applique g puis f : f(g(x))
return x => f(g(x));
}
// Utilisation :
Func<int, int> increment = x => x + 1;
Func<int, int> doubleIt = x => x * 2;
var incrementThenDouble = Compose(doubleIt, increment);
Console.WriteLine(incrementThenDouble(5)); // (5 + 1) * 2 = 12
Ce sont exactement ces compositions qui sont au cœur du traitement de données en chaîne — par exemple quand tu enchaînes Select, Where, OrderBy, etc.
3. Comment C# supporte les fonctions d'ordre supérieur
Dans les langages fonctionnels (Haskell, F#) toutes les fonctions sont d'ordre supérieur par défaut. Mais C# (depuis la version 2.0) supporte aussi ce style grâce aux delegates et aux lambdas.
- Delegates (Func, Action, Predicate) — types de fonctions.
- Expressions lambda — syntaxe pour créer des fonctions sur place.
- Les méthodes peuvent accepter et retourner des delegates — donc les HOFs sont supportées "out of the box".
Schéma visuel
flowchart LR
A[Données] --> B[Fonction 1]
B --> C[Fonction 2]
C --> D[Résultat]
subgraph "Pipeline de traitement (fonctions d'ordre supérieur)"
B
C
end
4. On enrichit notre appli
Continuons à faire évoluer notre demo pas à pas — imaginons un "Mini-transformateur de chaînes".
Ajout d'une méthode HOF simple
Supposons qu'on a une liste de noms d'utilisateurs et qu'on veut les modifier arbitrairement avec des fonctions.
// Méthode qui prend une liste de strings et une fonction de transformation
List<string> TransformNames(List<string> names, Func<string, string> transformer)
{
var result = new List<string>();
foreach (var name in names)
{
result.Add(transformer(name));
}
return result;
}
Comment utiliser cette méthode ?
var names = new List<string> { "Anna", "Boris", "Sergey" };
// Transformer en majuscules
var upperNames = TransformNames(names, n => n.ToUpper());
// Préfixer par "Cher/Chère " pour chaque nom
var politeNames = TransformNames(names, n => "Cher/Chère " + n);
foreach (var n in upperNames)
Console.WriteLine(n); // ANNA, BORIS, SERGEY
foreach (var n in politeNames)
Console.WriteLine(n); // Cher/Chère Anna, ...
La méthode TransformNames est générique : elle délègue la logique de transformation au paramètre (transformer), que ce soit un appel à ToUpper ou n'importe quelle autre recette.
Adaptation aux types
Notre exemple s'adapte facilement à n'importe quel type de données.
// Méthode générique type - HOF qui fonctionne pour n'importe quel T
List<TResult> Map<T, TResult>(List<T> items, Func<T, TResult> transformer)
{
var result = new List<TResult>();
foreach (var item in items)
{
result.Add(transformer(item));
}
return result;
}
Usage :
var numbers = new List<int> { 1, 2, 3 };
var doubled = Map(numbers, x => x * 2); // [2, 4, 6]
var strings = Map(numbers, x => $"Nombre : {x}"); // ["Nombre : 1", ...]
5. Filtrage et agrégation via des fonctions d'ordre supérieur
La logique de filtrage et de recherche s'exprime naturellement via des HOF.
// Filtrage : fonction d'ordre supérieur
List<T> Filter<T>(List<T> items, Predicate<T> criteria)
{
var result = new List<T>();
foreach (var item in items)
{
if (criteria(item)) // On appelle la fonction-critère
{
result.Add(item);
}
}
return result;
}
Utilisation :
var names = new List<string> { "Anna", "Boris", "Andrey" };
var aNames = Filter(names, n => n.StartsWith("A"));
// Résultat : "Anna", "Andrey"
6. Concept de composition de fonctions (function composition)
Les fonctions d'ordre supérieur permettent non seulement d'utiliser des fonctions individuelles, mais aussi de les enchaîner — les fameuses compositions. En C#, on peut implémenter ça avec une fonction qui prend deux fonctions et rend leur combinaison.
// Compositeur de fonctions : retourne une fonction qui applique d'abord g puis f
Func<T, TResult> Compose<T, TIntermediate, TResult>(
Func<TIntermediate, TResult> f,
Func<T, TIntermediate> g)
{
return x => f(g(x));
}
// Exemple :
Func<int, int> plusOne = n => n + 1;
Func<int, int> timesTwo = n => n * 2;
var plusOneThenDouble = Compose(timesTwo, plusOne);
Console.WriteLine(plusOneThenDouble(3)); // (3 + 1) * 2 = 8
7. Petites astuces utiles
Contexte historique : pourquoi c'était plus dur avant ?
Avant les delegates et les lambdas, les devs écrivaient plein de boucles similaires, copiaient des bouts de code pour "filtrer", "transformer" ou "grouper" des données. Avec les fonctions d'ordre supérieur on peut extraire la partie variable du comportement en fonctions-paramètres — ce qui réduit drastiquement la duplication et rend le code plus expressif.
Un peu de sucre syntaxique : fonctions en expression
Les HOF sont souvent écrites avec des méthodes expression-bodied — des méthodes concises sur une ligne :
List<string> FilterNames(Predicate<string> pred) =>
Names.Where(name => pred(name)).ToList();
List<TResult> MapNames<TResult>(Func<string, TResult> transformer) =>
Names.Select(transformer).ToList();
Comparaison avec les mécanismes LINQ
Voyons comment LINQ utilise des fonctions d'ordre supérieur :
| Méthode LINQ | Type de delegate accepté | Rôle |
|---|---|---|
|
|
Filtre les éléments |
|
|
Transforme les éléments |
|
|
Trie par clé |
|
|
Agrège (réduit) la collection |
|
|
Vérifie la présence d'un élément selon une condition |
Toutes ces méthodes reposent sur l'idée des fonctions d'ordre supérieur : tu écris les règles, la librairie standard fournit l'infrastructure.
8. Erreurs possibles et pièges
Confusion sur les types de delegates.
Au début il est facile de se perdre entre Action, Func et Predicate.
Astuce : si la fonction retourne bool — c'est souvent un Predicate. Si elle retourne une valeur — prends Func, si elle ne retourne rien — Action.
Captures de variables (closures).
Si la fonction retournée utilise des variables de l'environnement externe, veille à ce que les valeurs soient celles attendues au moment de l'appel. Les variables ne sont pas copiées, elles sont "capturées".
Déboguer des chaînes complexes.
Quand les fonctions sont composées en longs pipelines, il devient difficile de savoir quel maillon a transformé les données de façon incorrecte. Ajoute des affichages temporaires et des commentaires :
n => {
Console.WriteLine("Avant opération : " + n);
var res = n * 2;
Console.WriteLine("Après : " + res);
return res;
}
GO TO FULL VERSION