LINQORNE 🦄 ​
La librairie LINQ est limitée sur certains aspects (et trop standard 😉) et il est temps de réaliser une version unique et enrichie 💵
Base ​
Créer un projet librairie nommé Linqorne.
ForEach ➿ ​
Commençons par définir une fonction Loop qui mimique le ForEach qui n'est que disponible sur les List et pas directement sur un IEnumerable.
Aide si nécessaire
public static class Linqorne
{
public static void Loop<TSource>(this IEnumerable<TSource> subject,Action<TSource> action)
{
foreach (var item in subject)
{
action(item);
}
}
}Quelques explications ​
Généricité ​
Comme Linq, pour que la librairie fonctionne sur tous les types possibles (Collections de n'importe quel élément), on a besoin de la généricité, dont voici quelques explications en lien avec le code présenté :
TSource :
TSourceest un paramètre de type générique. Cela signifie que cette méthode ne fonctionne pas avec un type particulier, mais avec un type qui sera déterminé au moment de l'utilisation de la méthode.- Le type
TSourceest spécifié avec la syntaxe générique dans la déclaration de la méthode :Loop<TSource>. Cela rend la méthode générique, permettant d'utiliser différents types d'éléments sans réécrire la méthode pour chaque type. - Par exemple,
TSourcepourrait être unint,string,Person, ou n'importe quel autre type d'objet.
Paramètres ​
IEnumerable<TSource>:- Le paramètre
subjectest de typeIEnumerable<TSource>. Cela signifie qu'il s'agit d'une collection de plusieurs éléments de typeTSource. IEnumerableest une interface qui représente une collection qui peut être énumérée (ou itérée) sur n'importe quel type d'objet.
- Le paramètre
Action<TSource>:- Le deuxième paramètre,
action, est de typeAction<TSource>. Cela signifie que c'est une déléguée qui prend un paramètre de typeTSourceet ne renvoie rien (void). - En d'autres termes,
Action<TSource>représente une méthode ou un bloc de code qui prend un élément de typeTSourceet fait quelque chose avec cet élément.
- Le deuxième paramètre,
Exécution du code : ​
Lorsque la méthode Loop est appelée, elle itère sur chaque élément de la collection subject, et pour chaque élément, elle exécute l'action spécifiée. Étant donné que la méthode est générique, elle peut être utilisée pour tout type de collection et toute action qui agit sur ce type.
Exemple d'utilisation : ​
List<int> numbers = new List<int> { 1, 2, 3, 4 };
numbers.Loop(n => Console.WriteLine(n)); // Affichera chaque nombre dans la console
numbers.Loop(Console.WriteLine); // Version écourtée du lambdaDans cet exemple :
TSourceestint.subjectest une liste d'entiers.actionest une expression lambda qui affiche chaque nombre.
Find 🔎 ​
La méthode Where de Linq donne l'impression de faire du SQL et il serait plus humain d'avoir une fonction Find à qui on passe la condition.
Compléter le code suivant :
public static IEnumerable<TSource> Find<TSource>(
this IEnumerable<TSource> subject,
Func<TSource,bool> isWanted)
{
var wanteds = new List<TSource>();
//TODO
return wanteds.ToImmutableArray();
}Map 🗺 ​
Pour converger vers la nomenclature standard, il serait judicieux de redéfinir le Select en Map.
Cette fois-ci, pas d'aide hormis la feuille presque blanche suivante (... et TODO à compléter) :
public static IEnumerable<...> Map<TSource,TTarget> (this ..., Func<TSourc,TTarget> convert)
{
//TODO
}Reduce 🥿 ​
Pas très original et toutefois intéressant pour le drill, il est temps de refaire un Aggregate nommé Reduce et ceci, bien sûr, sans réutiliser le Aggregate de Linq...
public static ... Reduce ...Statistiques ​
Linq offre Min, Max et Average mais il serait pertinent d'avoir d'autres outils statistiques comme la médiane et la covariance.
Min,Max,Average ​
Écrire ces fonctions pour Linqorne pour les types int,double et decimal.
Suppléments ​
Pour définir des méthodes qui calculent la médiane (Median) et la covariance (Covariance), nous devons comprendre les opérations mathématiques impliquées dans chacune de ces statistiques.
1. Calcul de la Médiane (Median) ​
La médiane est la valeur qui sépare la moitié inférieure des données de la moitié supérieure. Pour calculer la médiane, on peut :
- Trier la collection.
- Si le nombre d'éléments est impair, la médiane est le milieu de la collection.
- Si le nombre d'éléments est pair, la médiane est la moyenne des deux valeurs centrales.
Sans utiliser
Linqmais en utilisant tout ce qui est disponible dansLinqorne(par exemple Map...)
Exemple d'implémentation de la méthode `Median`
public static double Median<TSource>(this IEnumerable<TSource> source, Func<TSource, double> converter)
{
var sortedList = source.Map(converter).OrderBy(n => n).ToList();
int count = sortedList.Count;
if (count == 0)
{
throw new InvalidOperationException("La collection ne peut pas être vide");
}
if (count % 2 == 1) // Si le nombre d'éléments est impair
{
return sortedList[count / 2];
}
else // Si le nombre d'éléments est pair
{
double middle1 = sortedList[(count / 2) - 1];
double middle2 = sortedList[count / 2];
return (middle1 + middle2) / 2.0;
}
}2. Calcul de la Covariance (Covariance) ​
La covariance mesure la manière dont deux ensembles de données varient ensemble. Voici la formule de base pour la covariance entre deux séries de valeurs
Où :
et sont les valeurs individuelles des séries et , et sont les moyennes des séries et , est le nombre d'éléments dans les séries.
Exemple d'implémentation de la méthode `Covariance`
public static double Covariance<TSource>(this IEnumerable<TSource> source, Func<TSource, double> converterX, Func<TSource, double> converterY)
{
var valuesX = source.Map(converterX);
var valuesY = source.Map(converterY);
if (valuesX.Count != valuesY.Count || valuesX.Count == 0)
{
throw new InvalidOperationException("Les collections doivent avoir la même taille et ne pas être vides.");
}
double avgX = valuesX.Average();
double avgY = valuesY.Average();
double covariance = 0.0;
int count = valuesX.Count;
for (int i = 0; i < count; i++)
{
covariance += (valuesX[i] - avgX) * (valuesY[i] - avgY);
}
return covariance / (count - 1);
}Exemple d'utilisation : ​
List<(double X, double Y)> data = new List<(double, double)>
{
(2.1, 8.0),
(2.5, 12.0),
(3.6, 14.0),
(4.0, 10.0),
(4.4, 12.0),
};
double medianX = data.Median(d => d.X); // Calcule la médiane des X
double covariance = data.Covariance(d => d.X, d => d.Y); // Calcule la covariance entre X et Y
Console.WriteLine($"Médiane de X: {medianX}");
Console.WriteLine($"Covariance entre X et Y: {covariance}");Nombre et généricité ​
Ces méthodes génériques permettent de calculer des statistiques directement sur des collections de n'importe quel type d'objets, tout en utilisant des expressions pour sélectionner/convertir les données dans des valeurs utilisables...
Pour faire mieux, à l'image de Linq, il faudrait redéfinir ces méthodes pour tous les types de nombres :
...
public static double Average(this IEnumerable<long> source) => Average<long, long, double>(source);
public static float Average(this IEnumerable<float> source) => (float)Average<float, double, double>(source);
public static double Average(this IEnumerable<double> source) => Average<double, double, double>(source);
public static decimal Average(this IEnumerable<decimal> source) => Average<decimal, decimal, decimal>(source);
...Suite et fin 🔚 ​
Voici encore quelques idées d'extension pour Linqorne
1. ChunkBy : Diviser une collection en sous-groupes de taille fixe ​
Il peut être utile de diviser une collection en plusieurs sous-listes (ou chunks) de taille fixe. Cela pourrait être utile lorsque vous traitez des données par lots ou lorsque vous voulez manipuler des sous-ensembles d'éléments.
Utilisation : ​
var numbers = Enumerable.Range(1, 10);
var chunks = numbers.ChunkBy(3); // Divise la collection en sous-listes de taille 3Solution avec un bonus
public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
if (chunkSize <= 0)
throw new ArgumentException("Le chunkSize doit être supérieur à zéro.");
List<TSource> chunk = new List<TSource>(chunkSize);
foreach (var element in source)
{
chunk.Add(element);
if (chunk.Count == chunkSize)
{
yield return chunk;
chunk = new List<TSource>(chunkSize);
}
}
if (chunk.Any())
yield return chunk;
}Le mot-clé
yielden C# est utilisé pour produire des valeurs de manière paresseuse (ou lazy evaluation) dans une méthode itérative, généralement utilisée avec des méthodes qui retournent des types commeIEnumerableouIEnumerator. Il permet de suspendre l'exécution de la méthode et de retourner une valeur unique à chaque appel de l'itérateur, tout en maintenant l'état de la méthode entre chaque appel.
Fonctionnement : ​
yield return: retourne une valeur au niveau de l'itérateur et conserve l'état de la méthode pour continuer l'exécution lors du prochain appel de l'itérateur.yield break: arrête l'itération avant que tous les éléments ne soient produits.
Exemple basique : ​
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}Dans cet exemple, chaque fois que l'itérateur avance (par exemple, dans une boucle foreach), une nouvelle valeur est renvoyée (1, 2, puis 3), et l'exécution de la méthode reprend là où elle s'était arrêtée.
Avantages : ​
- Paresseux : Les éléments sont générés un par un, uniquement lorsqu'ils sont nécessaires, ce qui économise de la mémoire et permet de traiter des collections potentiellement infinies.
- État préservé : Pas besoin d'une collection intermédiaire, la méthode conserve son état entre les itérations.
En résumé, yield simplifie la création d'itérateurs sans avoir à gérer manuellement des collections ou des objets de gestion de l'énumération.
2. Mode : Trouver l'élément le plus fréquent ​
Cette extension renverrait l'élément le plus fréquent d'une collection. C'est utile dans des scénarios de statistiques simples.
Utilisation : ​
var numbers = new[] { 1, 2, 2, 3, 3, 3, 4 };
var mode = numbers.Mode(); // Renvoie 3Proposition de solution à base de Linq
Exemple : ​
public static TSource Mode<TSource>(this IEnumerable<TSource> source)
{
return source.GroupBy(x => x)
.OrderByDescending(g => g.Count())
.First()
.Key;
}3. ToDictionarySafely : Convertir en dictionnaire avec gestion des clés en double ​
La méthode ToDictionary lève une exception si des doublons sont détectés dans les clés. On pourrait avoir besoin d'une méthode qui ignore ou gère les doublons proprement, soit en choisissant la première ou la dernière occurrence.
Signature ​
public static Dictionary<TKey, TValue> ToDictionarySafely<TSource, TKey, TValue>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TValue> valueSelector,
bool preferLast = true)Utilisation : ​
var items = new[]
{
new { Key = "a", Value = 1 },
new { Key = "b", Value = 2 },
new { Key = "a", Value = 3 }
};
var dict = items.ToDictionarySafely(x => x.Key, x => x.Value); // Choisit la dernière occurrenceExemple de solution
public static Dictionary<TKey, TValue> ToDictionarySafely<TSource, TKey, TValue>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TValue> valueSelector,
bool preferLast = true)
{
Dictionary<TKey, TValue> dictionary = new Dictionary<TKey, TValue>();
foreach (var element in source)
{
TKey key = keySelector(element);
TValue value = valueSelector(element);
if (dictionary.ContainsKey(key))
{
// Remplace la valeur existante selon la préférence
if (preferLast)
{
dictionary[key] = value;
}
}
else
{
dictionary.Add(key, value);
}
}
return dictionary;
}