|
|
Ligne 300 : |
Ligne 300 : |
| == [https://msdn.microsoft.com/en-us/library/bb348436(v=vs.110).aspx Distinct] == | | == [https://msdn.microsoft.com/en-us/library/bb348436(v=vs.110).aspx Distinct] == |
| === Spécifier une propriété de distinction === | | === Spécifier une propriété de distinction === |
| Distinct retourne un IEnumerable<T> sans doublons. La notion d'identique est vérifiée via Equals et GetHashCode.<br> | | {{info | MoreLINQ [[MoreLINQ#DistinctBy|DistinctBy]]}} |
| | {{boxx|Distinct}} retourne un {{boxx|IEnumerable<T>}} sans doublons. La notion d'identique est vérifiée via {{boxx|Equals}} et {{boxx|GetHashCode}}.<br> |
| Cette notion d'identique peut être étendue à une comparaison sur une propriété de l'objet (ex : objet.Nom). | | Cette notion d'identique peut être étendue à une comparaison sur une propriété de l'objet (ex : objet.Nom). |
| <kode lang=csharp> | | <kode lang=csharp> |
Version du 14 novembre 2020 à 22:57
Assemblage
|
// Ajoutez au projet une référence à l'assemblage System.Core
using System.Linq;
|
Enumerable
Générer un énumérable
|
var UnACent = Enumerable.Range(1, 100); // 1 2 ... 100
var DixFoisSept = Enumerable.Repeat(7, 10); // 7 7 ... 7
|
Retourner un énumérable vide
|
return Enumerable.Empty<string>();
|
|
// afficher le contenu de MyTable
MyTable.Dump();
|
|
LinqPad permet d'obtenir l'équivalent SQL d'une requête LINQ. |
Union – Intersect – Except
|
int[] numbersA = { 0, 1, 2, 3 };
int[] numbersB = { 0, 2, 4, 6 };
// liste des numéros des deux tableaux
IEnumerable<int> allNumbers = numbersA.Concat(numbersB); // 0 1 2 3 0 2 4 6
// liste des numéros des deux tableaux sans les doublons
IEnumerable<int> uniqueNumbers = numbersA.Union(numbersB); // 0 1 2 3 4 6
// liste des numéros présent dans les deux tableaux
IEnumerable<int> duplicateNumbers = numbersA.Intersect(numbersB); // 0 2
// équivalent Comprehension Query Syntax
IEnumerable<int> query = from itemA in numbersA
join itemB in numbersB on itemA equals itemB
select itemA;
// liste des numéros présent dans numbersA mais pas dans numbersB
IEnumerable<int> exceptNumbers = numbersA.Except(numbersB); // 1 3
|
Select
|
int[] numbers = { 1, 2, 3 };
var numbersPlusOne = from n in numbers
select n + 1;
var numbersPlusOne = numbers.Select(n => n + 1);
// numbersPlusOne : 2 3 4
|
Async
|
public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
{
return Task.WhenAll(source);
}
var result = await myEnumerable.Select(MyMethodAsync).WhenAll();
public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, Task<TResult>> func)
{
return await Task.WhenAll(source.Select(func));
}
var result = await myEnumerable.SelectAsync(MyMethodAsync);
|
SelectMany
Même chose que Select, mais aplatit les résultat en une seule énumération.
|
var numbers = new List<List<int>>() {
new List<int>() { 1, 2, 3 },
new List<int>() { 4, 5, 6 }
};
// List<List<int>> → IEnumerable<List<int>>
var numbersSelect = numbers.Select(li => li);
// List<ListInt> → IEnumerable<int>, le résultat est aplatit en une seule énumération: [1, 2, 3, 4, 5, 6]
var numbersSelectMany = numbers.SelectMany(li => li);
var numbersSelectMany = from li in numbers
from i in li
select i;
|
Async
|
public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
{
return Task.WhenAll(source);
}
var result = (await myEnumerable.Select(MyMethodAsync).WhenAll()).SelectMany(s => s);
public static async Task<IEnumerable<TResult>> SelectManyAsync<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, Task<IEnumerable<TResult>>> func)
{
return (await Task.WhenAll(source.Select(func))).SelectMany(s => s);
}
var result = await myEnumerable.SelectManyAsync(MyMethodAsync);
|
OrderBy
|
// tri croissant suivant la longueur des mots
string[] words = { "cherry", "apple", "blueberry" };
var sortedWords = words.OrderBy(w => w.Length);
var sortedWords = from w in words
orderby w.Length
select w;
// tri décroissant
double[] doubles = { 1.7, 2.3, 1.9, 4.1, 2.9 };
var sortedDoubles = doubles.OrderByDescending(d => d);
var sortedDoubles = from d in doubles
orderby d descending
select d;
// double tri suivant la taille des mots, et en cas d'égalité suivant l'ordre alphabétique
string[] digits = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
var sortedDigits = digits.OrderBy(d => d.Length).ThenBy(d => d);
var sortedDigits = from d in digits
orderby d.Length, d
select d;
// OrderBy extension method to sort by string property name and specifying the ascending or descending order
private static IEnumerable<T> OrderBy<T>(
this IEnumerable<T> source,
string propertyName,
bool ascendingOrder)
{
var propertyDescriptor = TypeDescriptor.GetProperties(typeof(T))
.Find(propertyName, false);
return ascendingOrder ?
source.OrderBy(x => propertyDescriptor.GetValue(x)) :
source.OrderByDescending(x => propertyDescriptor.GetValue(x));
}
|
GroupBy
Permet de regrouper suivant une clé. Retourne une liste de IGrouping contenant chacun la valeur de la clé et la liste des éléments correspondant à la clé.
IGrouping<out TKey, out TElement> hérite de IEnumerable<TElement>.
|
// groupement des mots suivant leur première lettre
string[] words = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" };
var wordGroups = from w in words
group w by w[0] into g;
var wordGroups = words.GroupBy(w => w[0]);
foreach (var g in wordGroups)
{
Console.WriteLine("Words that start with the letter '{0}':", g.Key);
// g implémente IEnumerable<string>
foreach (var w in g)
{
Console.WriteLine(w);
}
}
// Result
// Words that start with the letter 'b':
// blueberry
// banana
// Words that start with the letter 'c':
// chimpanzee
// cheese
// Words that start with the letter 'a':
// abacus
// apple
// Variante
var wordGroups = from w in words
group w by w[0] into g
select new { FirstLetter = g.Key, Words = g };
var wordGroups = words.GroupBy(w => w[0])
.Select(g => new { FirstLetter = g.Key, Words = g });
foreach (var g in wordGroups)
{
Console.WriteLine("Words that start with the letter '{0}':", g.FirstLetter);
foreach (var w in g.Words)
{
Console.WriteLine(w);
}
}
// Group by sur plusieurs éléments
var groupement= maListe.GroupBy(element => new {element.Propriété1, element.Propriété2});
|
Avec un delegate nommé
|
// avec un delagate anonyme
var wordGroups = words.GroupBy(w => w[0]);
// avec un delegate nommé
var wordGroups = words.GroupBy(GroupByDelegate);
private char GroupByDelegate(string word)
{
return word[0];
// le type de retour est object dans le cas où return renvoie un type anonyme
// return new { Prop1 = word[0], Prop2 = word[1] };
}
|
GroupBy + Sum
|
var expensesSumByAccountId = expenses.GroupBy(x => x.AccountId)
.ToDictionary(x => x.Key, x => x.Sum(y => y.Amount));
|
Lookup
Lookup est executé immediatement, à la différence de GroupBy qui est exécuté en différé. Il vaut mieux utiliser Lookup si l'on souhaite parcourir le résultat plus d'une fois.
ILookup<TKey, TElement> hérite de IEnumerable<IGrouping<TKey, TElement>> et se comporte comme un IDictionary<TKey, IEnumerable<TElement>>
|
// groupement des mots suivant leur première lettre
string[] words = { "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" };
// GroupBy
IEnumerable<IGrouping<char, string>> wordGroupsGroupBy = words.GroupBy(w => w[0]);
foreach (IGrouping<char, string> g in wordGroupsGroupBy)
{
Console.WriteLine("Words that start with the letter '{0}':", g.Key);
foreach (var w in g)
{
Console.WriteLine(w);
}
}
// Lookup
ILookup<char, string> wordGroupsLookup = words.ToLookup(w => w[0]);
foreach (IGrouping<char, string> g in wordGroupsLookup)
{
Console.WriteLine("Words that start with the letter '{0}':", g.Key);
foreach (var w in g)
{
Console.WriteLine(w);
}
}
// Même résultats avec GroupBy et Lookup
// Words that start with the letter 'b':
// blueberry
// banana
// Words that start with the letter 'c':
// chimpanzee
// cheese
// Words that start with the letter 'a':
// abacus
// apple
// possibilité d'utiliser ILookup comme un dictionnaire
foreach (string w in wordGroupsLookup['a'])
{
Console.WriteLine(w);
}
// Resultats
// abacus
// apple
|
Any – All
|
// Test si au moins un des éléments valide le critère.
string[] words = { "pomme", "fraise", "kiwi", };
bool isWordWithW = words.Any(w => w.Contains("w")); // true
// Test si tous les éléments valident le critère.
int[] numbers = { 1, 11, 3, 19, 41, 65, 19 };
bool onlyOdd = numbers.All(n => n % 2 == 1); // true, tous impair
// attention, si l'énumération est vide, All retourne true
var numbers = Enumerable.Empty<int>();
bool onlyOdd = numbers.All(n => n % 2 == 1); // true
bool onlyOdd = numbers.DefaultIfEmpty().All(n => n % 2 == 1); // false
// numbers.DefaultIfEmpty() retourne un enumerable avec un élément de valeur default, ici 0 pour int
// numbers.DefaultIfEmpty(-1) on peut aussi forcer la valeur
// Any permet aussi de tester si une énumération contient des éléments ou non
var numbers = new List<int> { 1, 2 };
bool hasElements = numbers.Any();
|
Spécifier une propriété de distinction
Distinct retourne un IEnumerable<T> sans doublons. La notion d'identique est vérifiée via Equals et GetHashCode.
Cette notion d'identique peut être étendue à une comparaison sur une propriété de l'objet (ex : objet.Nom).
|
Personne[] personnes = {
new Personne() { Nom = "Nicolas" },
new Personne() { Nom = "Audrey" },
new Personne() { Nom = "Nicolas" }
};
personnes.Distinct() // Nicolas Audrey Nicolas
personnes.DistinctBy(personne => personne.Nom) // Nicolas Audrey
|
Pour cela il faut ajouter une méthode d'extension à IEnumerable<T> :
|
using System.Linq;
public static class IEnumerableExtensions
{
public static IEnumerable<T> DistinctBy<T>(this IEnumerable<T> source, Func<T, object> uniqueCheckerMethod)
{
return source.Distinct(new GenericComparer<T>(uniqueCheckerMethod));
}
}
class GenericComparer<T> : IEqualityComparer<T>
{
private Func<T, object> _uniqueCheckerMethod;
public GenericComparer(Func<T, object> uniqueCheckerMethod)
{
this._uniqueCheckerMethod = uniqueCheckerMethod;
}
bool IEqualityComparer<T>.Equals(T x, T y)
{
return this._uniqueCheckerMethod(x).Equals(this._uniqueCheckerMethod(y));
}
int IEqualityComparer<T>.GetHashCode(T obj)
{
return this._uniqueCheckerMethod(obj).GetHashCode();
}
}
|
Aggregate
|
string sentence = "un deux trois quatre cinq";
string[] words = sentence.Split(' ');
string reversed = words.Aggregate((workingSentence, next) => next + " " + workingSentence);
// premier passage workingSentence = un, next = deux, retour "deux un"
// deuxième passage workingSentence = "deux un", next = trois, retour "trois deux un"
// sum with seed of 0 in case of intArray is empty to avoid "sequence contains no elements" exception
var sum = intArray.Aggregate(0, (current, next) => current + next);
|
|
int[] chiffres = { 1, 2, 3, 4 };
// saute les 2 premiers éléments et retourne le reste
chiffres.Skip(2); // 3 4
// prend seulement les 2 premiers éléments
chiffres.Take(2); // 1 2
|
- si la classe implémente IEquatable<T>, Equals(T) est utilisé
- sinon les surcharges de Equals et GetHashCode de l'objet sont utilisés
- sinon il est possible de spécifier un EqualityComparer<T>
|
var products1 = new Product[] { new Product { Name = "apple" }, new Product { Name = "orange" } };
var products2 = new Product[] { new Product { Name = "apple" }, new Product { Name = "lemon" } };
products1.Intersect(products2); // empty
// leur référence est utilisé pour les comparer, ils sont donc tous différents même s'ils ont le même Name
product1.Intersect(product2, new ProductEqualityComparer()); // apple
|
|
public class Product : IEquatable<Product>
{
public string Name { get; set; }
public bool Equals(Product other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return string.Equals(Name, other.Name);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Product) obj);
}
public override int GetHashCode()
{
return (Name != null ? Name.GetHashCode() : 0);
}
}
|
|
// GetHash code est appelé en premier
// Si les hash codes sont égaux alors Equals est appelé
public class ProductEqualityComparer : EqualityComparer<Product>
{
public override bool Equals(Product p1, Product p2)
{
if (ReferenceEquals(null, p1) || ReferenceEquals(null, p2))
{
return false;
}
if (ReferenceEquals(p1, p2))
{
return true;
}
return Equals(p1.Name, p2.Name); // && Equals(p1.Prop2, p2.Prop2);
}
public override int GetHashCode(Product p)
{
if (p == null)
{
return 0;
}
// Get hash code for the Name field if it is not null.
return (p.Name != null ? p.Name.GetHashCode() : 0);
// Calcul du hash code avec plusieurs propriétés
return p.Prop1.GetHashCode() ^ p.Prop2.GetHashCode();
// Calcul du hash code avec plusieurs propriétés
unchecked
{
var hashCode = (p.Prop1 != null ? p.Prop1.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (p.Prop2 != null ? p.Prop2.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (p.Prop3 != null ? p.Prop3.GetHashCode() : 0);
return hashCode;
}
}
}
|
Applique une fonction aux éléments de deux séquences pour produire une séquence de résultats.
|
int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };
IEnumerable<string> numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);
// 1 one
// 2 two
// 3 three
|
Met en corrélation les éléments de deux séquences en fonction des clés qui correspondent.
|
// join a list of users and a list of groups
// pair them arround the matching between user.GroupId and group.Id
var query = users.Join(groups,
user => user.GroupId,
group => group.Id,
(user, group) =>
new { User = user.Name, GroupName = group.Name });
|
|
DataTable clients = dataSet.Tables["client"];
DataTable commandes = dataSet.Tables["commande"];
var query =
from client in clients.AsEnumerable()
join commande in commandes.AsEnumerable()
on client.Field<int>("id") equals commande.Field<int>("clientId")
where client.Field<string>("name").EndsWith("2")
&& commande.Field<string>("details").EndsWith("2")
select new
{
ClientId = client.Field<int>("id"),
ClientName = client.Field<string>("name"),
CommandeId = commande.Field<int>("id"),
CommandeDetails = commande.Field<string>("details")
};
|
Affichage de la requête dans WPF
|
<DataGrid ItemsSource="{Binding Result}" />
|
|
private IEnumerable _result;
public IEnumerable Result
{
get
{
return _result;
}
set
{
_result = value;
OnPropertyChanged("Result");
}
}
/* ... */
Result = query;
|
- View → Server Explorer → Data Connections → Add Connection
- Add → Class → Data → LINQ to SQL Classes (fichier *.dbml)
- Glisser les tables dans le designer
|
var db = new DataClasses1DataContext();
var query =
from client in db.clients
select new
{
ClientId = client.id,
ClientName = client.name
};
// affichage du résultat dans un dataGridView WinForm
dataGridView1.DataSource = query;
|
Étend IEnumerable, et possède
- Provider: query provider associé au type de données (LINQ to SQL)
- Expression: arbre d'expression
- ElementType: type d'élément retourné
Group By
|
// groupement des clients par ville
var query =
from client in db.clients
group by client.city into group
select new
{
City = group.Key,
ClientNumber = group.Sum(c => c.city)
};
|
|
Expression<Func<Item, bool>> predicate = x => x.Date.Year > 2019;
// concat predicates
var predicate = PredicateBuilder.True<Item>();
predicate = predicate.And(x => x.Date.Year > 2019);
predicate = predicate.And(x => x.Date.Year < 2021);
|
PredicateBuilder.cs
|
public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T>() => f => true;
public static Expression<Func<T, bool>> False<T>() => f => false;
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
}
}
|
LINQ to XML
|
XDocument est disponible à partir du Framework 3.5 |
Lecture
Ne pas fait directement les requêtes sur XDocument car c'est un nœud virtuel qui ne contient que le nœud racine. Utiliser donc XDocument.Root.
|
// chargement du XML depuis un fichier
XDocument xDoc = XDocument.Load(xmlFilePath);
// ou depuis une chaîne de caratères
XDocument xDoc = XDocument.Parse("<?xml version=\"1.0\"?><root><noeud /></root>");
// Séléctionne tous les noeuds "noeud2" fils direct de "noeud1"
// qui lui-même est fils direct du noeud racine.
var query = xDoc.Root.Element("noeud1").Elements("noeud2");
var query = from n in xDoc.Root.Element("noeud1").Elements("noeud2")
select n;
// Séléctionne tous les noeuds "noeud2" fils direct ou non de la racine
var query = xDoc.Root.Descendants("noeud2");
// Récupère le contenu de noeud1, <noeud1>true</noeud1> ici "true" au format string
string content = xDoc.Root.Element("noeud1").Value;
// Convertion
bool b = XmlConvert.ToBoolean(content);
foreach (var noeud in query) // noeud est un XElement
{
string attribut = noeud.Attribute("attribut").Value;
string attribut = (string)noeud.Attribute("attribut"); // cast de XAttribut
}
|
XPath
|
using System.Xml.XPath;
var noeud2 = xDoc.XPathSelectElement("/noeud1/noeud2");
var noeud2innerText = noeud2.Value;
IEnumerable<XElement> elements = xDoc.XPathSelectElements("noeud1 | noeud2");
IEnumerable attributs = (IEnumerable)xDoc.XPathEvaluate("/noeud/@nom_attribut");
var attribut = attributs.Cast<XAttribute>().FirstOrDefault();
// gestion des namespaces
var xmlNamespaceManager = new XmlNamespaceManager(new NameTable());
xmlNamespaceManager.AddNamespace("prefix", "uri");
var noeud2 = xDoc.XPathSelectElement("/noeud1/prefix:noeud2", xmlNamespaceManager);
|
Doc XPath
Écriture
|
var xDoc = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XComment("Commentaire XML"),
new XElement("contacts",
new XElement("contact",
new XElement("name", "Nicolas"),
new XElement("phone",
new XAttribute("type", "mobile"),
"06-...")),
new XElement("contact",
new XElement("name", "Audrey"),
new XElement("phone", "01-..."))));
var xDoc = new XDocument();
xDoc.Declaration = new XDeclaration("1.0", "utf-8", "yes");
xDoc.Add(new XComment("Commentaire XML"));
var xContacts = new XElement("contacts");
xDoc.Root.Add(xContacts);
var xContactNicolas = new XElement("contact");
xContacts.Add(xContactNicolas);
xContactNicolas.SetElementValue("name", "Nicolas");
// équivalent à
xContactNicolas.Add(new XElement("name", "Nicolas"));
var xPhone = new XElement("phone");
xContactNicolas.Add(xPhone);
xPhone.SetAttributeValue("type", "mobile");
// équivalent à
xPhone.Add(new XAttribute("type", "mobile"));
xPhone.SetValue("06-...");
var xContactAudrey = new XElement("contact");
xContacts.Add(xContactAudrey);
xContactAudrey.Add(new XElement("name", "Audrey"));
xContactAudrey.Add(new XElement("phone", "01-..."));
|
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--Commentaire XML -->
<contacts>
<contact>
<name>Nicolas</name>
<phone type="mobile">06-...</phone>
</contact>
<contact>
<name>Audrey</name>
<phone>01-...</phone>
</contact>
</contacts>
|
XElement.Remove() et foreach
|
foreach (XElement xElt in xDoc.Root.Elements())
{
// PB : après l'appel de Remove, on sort du foreach
xElt.Remove();
}
// Solution 1 : utiliser LINQ
xDoc.Root.Elements().Remove();
// Solution 2 : parcourir à l’envers
foreach (XElement xElt in xDoc.Root.Elements().Reverse())
{
xElt.Remove();
}
// Solution 3 : créer une liste temporaire d'éléments à supprimer
|
XElement.SetAttributeValue
- Assigne la valeur à l'attribut spécifié.
- Créer l'attribut s'il n'existe pas.
- Si la valeur est null l'attribut est supprimé.
|
// code du Framework .NET
public void SetAttributeValue(XName name, object value)
{
XAttribute xAttribute = this.Attribute(name);
if (value == null)
{
if (xAttribute != null)
{
this.RemoveAttribute(xAttribute);
return;
}
}
else
{
if (xAttribute != null)
{
xAttribute.Value = XContainer.GetStringValue(value);
return;
}
this.AppendAttribute(new XAttribute(name, value));
}
}
|
Cast
Nativement les objets XAttribut et XElement peuvent être castés en :
- DateTime, DateTimeOffset, TimeSpan
- Decimal, Double, Int32, UInt32, Int64, UInt64
- Guid
- String
- Boolean
Remarque : pour les XElement c'est la valeur du noeud ou la concaténation des valeurs de ses sous-noeuds qui sera casté.
Jointure
|
var query = from n1 in xdoc.Root.Elements("noeud1")
join n2 in xdoc.Root.Elements("noeud2")
on (string)n1.Attribute("attribut") equals
(string)n2.Attribute("attribut")
select new
{
Propriété1 = n1.Attribute("p1"),
Propriété2 = n2.Attribute("p2")
};
|