Bonjour à tous,
Quelques news afin de reprendre la rentrée du bon pied :
- Les quizz C# de l'été: comme au mois d'août dernier, j'ai posté un ensemble de quizz afin d'animer un peu l'été de ceux qui ne sont pas partis ou du moins de sont qui sont restés connectés. Au total, 10 quizz et un peu plus d'une centaine de réponses.
Pour ceux qui les auraient ratés ou qui voudraient les relire, tout est ici : http://blogs.msdn.com/mitsufu/archive/tags/Quizz08/default.aspx
- la création du groupe Facebook "French Visual C# users": http://www.new.facebook.com/group.php?gid=35617573520
- la création du groupe Facebook "French WPF & Silverlight community" : http://www.new.facebook.com/group.php?gid=22681754853
les groupes Facebook nous permettront d'avoir un mini-portail communautaire afin que chacun puisse ajouter son contenu, ses questions, ses commentaires.
N'hésitez pas à ajouter du contenu ou lancer de nouvelles discussions !
- l'évènement technique parisien de la rentrée: la « Winwise Solutions Conference » est un évènement destiné aux professionnels du développement ainsi qu'aux enseignants et étudiants en informatique. http://wsc08.winwise.fr/AccueilPrincipal
RDV le 10 septembre 2008
En voici un un peu plus dur. J'ai un ensemble de villes 'cities' et une liste de groupes de villes. J'aimerai afficher l'ensemble du contenu de 'cities' mais en faisant apparaître les groupes à la place des villes si ceux-ci y sont présents. Les villes isolées apparaissent seules.
var cities = new string[] { "Paris", "Londres", "Berlin", "Madrid", "Bruxelles", "New York", "Seattle", "Tokyo" };
var groups = CreateList(
new { Id = "Europe", Cities = new string[] { "Paris", "Londres", "Berlin", "Madrid", "Bruxelles" } },
new { Id = "US", Cities = new string[] { "New York", "Seattle" } }
);
var result = new List<string>();
?
foreach (var s in result)
Console.WriteLine(s);
Le code de CreateList est ici: http://blogs.msdn.com/mitsufu/archive/2008/08/26/gq08-viii-initialisation-de-collections.aspx
Bien évidemment si on enlève "Madrid" de 'cities' l'ensemble 'Europe' disparaît et les villes sont affichées seules.
Personnellement je n'ai pas résolu la question en une seule requête Linq.
A vous de jouer.
Commençons par une réponse 'fonctionnelle'.
if (query.Count() == 0)
{
}
nous recherchons ici à déterminer si query est une séquence non nulle, autrement dit, si query renvoie des éléments. Techniquement '.Count() == 0' répond à la question mais est assez coûteux. En effet, .Count() parcourt l'ensemble de la séquence et dans notre cas le nombre total d'éléments nous importe peu. Du coup, initialiser la séquence et tester la présence du premier élément suffit bien évidemment à résoudre la question beaucoup plus efficacement.
query.GetEnumerator().MoveNext()
'true' signifie que la séquence n'est pas nulle et rien ne sert d'aller plus rien. 'false', la séquence est nulle.
Une méthode Linq fait exactement ce test: query.Any()
Pour répondre au commentaires:
.First() peut résoudre la question dans le sens où il soulevera une exception s'il n'y a aucun élément mais en effet la gestion de l'exception est un peu coûteuse pour un test si simple.
Par contre en aucun cas .FirstOrDefault() ne peut répondre à la question. Attardons nous un temps sur ce cas là. Contrairement à .First(), .FirstOrDefault() ne plante pas dans le cas où aucun élément n'est présent dans la séquence mais renvoie la valeur par défaut du type. Pour tous les types référence, la valeur par défaut est 'null'.
Mais Attention !
Récupérer 'null' depuis .FirstOrDefault() est ambigu du point de vu de notre problématique.
En effet, on a tout à fait le droit de renvoyer un élément null depuis une séquence. Imaginons donc une séquence qui renvoit un unique élément null:
private static IEnumerable<string> Test()
{
yield return null;
}
Dans le cas ci-dessus 'Test().FirstOrDefault()' renvoie null mais 'Test().Count()' renvoie 1.
private static IEnumerable<string> Test()
{
return new string[] { };
}
Dans ce dernier cas 'Test().FirstOrDefault()' renvoie également null mais 'Test().Count()' renvoie 0...
Dernière petite information non négligeable:
.Count() est une méthode qui renvoie le nombre d'éléments dans une séquence. Cette méthode d'extension fonctionne avec n'importe quel IEnumerable.
La logique de base de .Count() est d'itérer sur l'ensemble de la séquence en incrémentant un compteur. Cette implémentation fonctionne de manière universelle avec n'importe quel IEnumerable. MAIS ! parfois (voire souvent :-) ), la source de la requête est une collection.
Si la source est une pure énumération, ok, il n'y a pas d'autre choix que de compter un à un les éléments, mais si c'est une collection, il est bien entendu plus efficace d'utiliser ICollection.Count (ou .Length dans le cas d'un tableau).
Sachez donc que si votre source est une collection, par exemple List<string>, la méthode .Count() de Linq est optimisée car elle fait le test en interne afin d'utiliser ICollection.Count sans avoir à itérer, sinon, elle itère.
Allez, une rapide pour la fin de journée.
Le truc est simple mais il est important de toujours l'avoir en tête lorsque l'on fait du Linq.
La requête suivante est correcte mais peut-être optimisée. Comment ?
if (query.Count() == 0)
{
//...
}
Dans le même genre:
if ((from c in customers
where c.City == "Paris"
select c).Count() != 0)
{
}
Depuis .Net 3.5, C# (3.0 donc) propose une syntaxe facilitant l'initialisation de collections.
On peut ainsi écrire:
var list = new List<string> { "Paris", "Londres" };
Imaginons maintenant que l'on veuille initialiser une liste d'un type anonyme.
Ex d'élément: 'new { FirstName="Guillaume", LastName="Renaud" }'
J'attends vos propositions.
Linq to Sql utilise des informations de mapping pour générer les requêtes Sql.
Comment récupérer ces informations de mapping pour notre propre usage ?
Imaginons un scénario simple pour générer dynamiquement les colonnes d'une grille (en mettant des combos pour les relations par exemple).
var db = new NorthwindDataContext();
var q =
from c in db.Customers
select c;
grid.Columns.Add(...);
grid.Columns.Add(...);
...
grid.ItemsSource = q.ToList();
Linq to Sql est capable de générer le 'in' Sql. Cela se traduit en Linq par l'usage de la méthode .Contains(). L'exemple ci-dessous compile mais pourtant plante à l'exécution.
Pourquoi donc ?
var db = new NorthwindDataContext();
var cq =
CompiledQuery.Compile((NorthwindDataContext ctx, string[] cities) =>
from c in ctx.Customers
where cities.Contains(c.City)
select c);
var list = cq(db, new string[] {"Paris", "London"}).ToList();
Rappelons un point important qu'il est toujours bon d'avoir en tête lorsque l'on fait du
Linq to Object.
Une majorité des opérations de Linq déroulent l'énumération source et exploitent chaque élément un par un pour effectuer leur traitement. Aucune collection intermédiaire interne n'est utilisée. Nous appelons ces opérations "streamed". Comme le nom l'indique, ces opération travaillent directement sur le flux de données sans avoir besoin d'une connaissance globale des données.
Prenons des cas simples:
.Where() est entièrement autonome sur l'élément en cours. Aucune autre donnée du flux ne lui est nécessaire pour savoir si oui ou non, un élément doit être filtré ou pas.
De même, Select, Disctinct, Any, etc, sont des méthodes "streamed".
Par opposition,
.Count() de toute évidence doit balayer l'intégralité du flux pour renvoyer son résultat.
C'est également vrai pour OrderBy, GroupBy, All, etc.
Il faut être conscient de l'impact de ces méthodes sur la performance.
Voici un exemple simple:
var q =
from n in names
where n.StartsWith("A")
select n;
foreach (var n in q)
{
//traitement
}
int count = q.Count();
le code suivant va balayer deux fois de suite l'intégralité des données renvoyées par 'q'. Une fois de manière explicite dans le foreach, puis une fois dans le .Count().
Dans ce cas précis nous savons pertinemment que nous allons parcourir l'ensemble des éléments dans le foreach. Afin de ne pas parcourir l'énumération une seconde fois, il est préférable de:
- soit ajouter un compteur dans le foreach (count++).
- soit, de projeter l'ensemble des données dans un tableau si d'après votre contexte sa taille est raisonnable.
var q =
from n in names
where n.StartsWith("A")
select n;
var a = q.ToArray();
foreach (var n in a)
{
//traitement
}
int count = a.Lenght;
Si la source de vos données est une structure mémoire (List<>, Array, etc) de taille moyenne, l'impact au niveau de la performance restera relativement faible mais toujours intéressant. Par contre si votre source est un flux, imaginons un reader sur un fichier texte sous forme d'énumérateur ou pire encore un reader sur un flux réseau type socket par exemple, il faut bien être conscient qu'une requête Linq avec un simple OrderBy ne renverra le premier élément qu'après avoir lu l'intégralité du flux...
Réponse au quizz précédent.
Ce quizz va me permettre de rappeler plusieurs points intéressants:
- Jouer avec les chaînes de caractères est toujours un jeu dangereux d'un point de vue de la performance. N'oublions pas qu'en .Net une chaîne est une collection de caractères en lecture seule (immuable). Les syntaxes utilisant les surcharges d'opérateur (ex: "A" + "B") nous laisse croire à une opération peu coûteuse mais il n'en est rien. Même si beaucoup de gens le savent, rappelons que lorsqu'une concaténation de chaines dépasse 2 arguments, il est largement préférable d'utiliser StringBuilder, string.Format ou autres string.Join.
- Les chaînes de caractères sont des IEnumerable<char> ! Cela parait évident mais pourtant le doute survient lorsque l'on s'aperçoit que l'intellisense de Visual Studio ne présente pas les extensions de Linq (Select, Where, Count, Distinct, etc). Ceci est un choix délibéré afin de ne pas complexifier la vue du type string déjà riche en fonctions et de ne pas apporter la confusion sur un type autant utilisé.
Utilisez s.Cast<string>() pour forcer le typage et accéder aux fonctions de Linq via l'intellisense. voir une ancienne discussion sur le sujet
Venons en à la solution. Vous avez tous proposé de très bonnes idées. Examinons les d'un peu plus près.
Pour la première question (union de tous les caractères distints utilisés dans les différentes chaines), Yoann, comme les suivants d'ailleurs, a donné la solution la plus simple et la plus efficace.
//All dictincts chars
var dc = (from name in names
from ch in name
where ch != ' '
select char.ToUpper(ch)).Distinct();
Pour information, cette requête résoud le problème en une seule passe d'où l'efficacité.
La seconde requête est plus complexe.
Yoann recherche pour chaque caractère s'il est contenu dans l'ensemble des mots.
Cela fonctionne évidemment mais c'est relativement coûteux.
En effet, si on accumule un ensemble de caractères communs pour chaque chaîne de caractères, celui-ci ne peut que diminuer puisque l'on ne garde que l'intersection. Du coup, on cherche à chaque itération de moins en moins de caractères. Cela dépend évidemment énormément du jeu de données.
Commençons par du classique:
IEnumerable<char> result = null;
foreach (var n in names)
{
if (result == null)
result = n;
else
result = result.Intersect(n);
}
En bouclant sur l'ensemble des chaînes, on peut cumuler les intersections un peu comme un calculerait une somme. Il ne restera à la fin que la liste des caractères présents uniquement dans l'ensemble des chaînes.
Linq définit une méthode d'extension .Sum() qui calcule la somme d'une énumération de nombres. Bien évidemment cette méthode ne fonctionne pas avec des non numériques. Linq fournit également une méthode .Aggregate() qui fait exactement le même parcours que .Sum() mais qui vous demande de fournir une expression définissant l'opération à effectuer à chaque itération.
Nous pouvons ainsi écrire:
var q2 =
(from n in names
select n.Cast<Char>()).Aggregate((acc, n) => acc.Intersect(n))
qui donnera exactement le même résultat.
Afin de répondre exactement au quizz, j'ajoute le test excluant les espaces:
var q2 =
from c in
(from n in names
select n.Cast<Char>()).Aggregate((acc, n) => acc.Intersect(n))
where c != ' '
select c;
Pour le mot de la fin, bravo à Miitch pour sa requête certes un peu couteuse mais très originale.
Voici un cinquième quizz pour le week-end. Soupçonnant une semaine de 15 août un peu désertée je reprendrai plus activement les quizzs la semaine prochaine.
Je vous propose ici de rechercher la liste distincte des caractères présents dans l'ensemble des chaînes de caractères.
Puis dans une second temps, l'ensemble des caractères présents dans toutes les chaînes de caractères.
Petite option: exclure les espaces.
var names = new string[] { "Mitsuru FURUTA",
"Dick LANTIM", "Pierre LAGARDE" };
?
//All dictincts chars
foreach (var c in?)
Console.WriteLine(c);
//Chars common to all strings
foreach (var c in?)
Console.WriteLine(c);

Dans le langage Microsoft, les services pack offrent généralement un ensemble de correctifs à un produit. Ce service pack est bien plus riche, ne vous y trompez pas !
Vous y trouverez des améliorations mais également de nouveaux produits complets. Quelques extraits:
- Clients riches :
- Amélioration des performances de 20 à 45% pour les applications Windows Presentation Foundation (WPF) sans modification du code.
- Déploiement aisé d'applications clientes .Net pour les machines sans .Net framework installé.
- ADO.Net Entity Framework
- ADO .Net Data Services (projet Astoria)
- De nombreuses améliorations pour le travail en équipe (Visual Studio Team System 2008 Team Foundation)
Tous les détails sur le site officiel: http://msdn.microsoft.com/en-us/vstudio/products/cc533447.aspx
Mitsu
Un peu de Linq to Sql !
Le fonctionnel est simple, j'aimerais depuis toutes les catégories récupérer une liste d'éléments regroupant le nom de la catégorie ainsi qu'un dictionnaire des produits appartenants à cette catégorie. Ceci afin de pouvoir retrouver rapidement un produit appartenant à une catégorie.
Le code suivant est syntaxiquement correct, il compile même !
Pourtant son exécution est impossible. Quelle est l'erreur ?
var db = new NorthwindDataContext();
var q =
from c in db.Categories
select new { Name = c.CategoryName,
Products = c.Products.ToDictionary(cat => cat.CategoryID) };
foreach (var c in q)
{
if (c.Products.ContainsKey(123))
Console.WriteLine(c.Name + " contient le produit d'id 123");
}
PS: c'est vendredi !! Prenez le temps de répondre et ne laissez pas tout à Matthieu ! :p
Imaginons un tableau quelconque à deux dimensions. J'aimerais tout simplement lister l'ensemble des données sous forme d'un simple vecteur en éliminant la seconde dimension.
var values = new int[][] {
new int[] {1, 2, 3},
new int[] {4, 5, 6},
new int[] {7, 8, 9}
};
var q = ?
foreach (var i in q)
Console.WriteLine(i);
donnerait donc :

Voici la réponse au précédent Quizz.
Pour la version simple:
var q =
from v1 in values1
from v2 in values2
select v1 + v2;
En effet, comme un serveur de base de données, Linq to object fait un produit cartésien lorsque l'on définit plusieurs source de données. Dans cette première écriture relativement simple et lisible, nous remarquerons juste que v1 et v2 sont accessibles au niveau du 'select'.
Voici maintenant la syntaxe équivalente en C# 'classique':
var q = values1.SelectMany(v1 =>
values2.Select(v2 => v1 + v2));
Pourquoi est-elle aussi complexe ?
Je vais redéfinir ma question: quelle est la portée d'un paramètre d'expression lambda ? En plus clair, quand j'écris 'source.Where(s => ...).Select(s2 => ...)' d'où puis-je appeler 's' ?
's' est un paramètre de méthode (certes anonyme dans notre cas) et n'est donc utilisable qu'à l'intérieur du 'Where'.
Beaucoup de méthodes d'extension de Linq renvoient leur paramètre d'entrée tel quel (OrderBy, Where) et nous font croire que le paramètre est utilisable tout au long de la requête alors qu'il est passé de méthode en méthode.
var q =
from c in customers
where c.City == "Paris"
select c.CustomerID;
Dans cette simple requête, 'c' semble être utilisable un peu partout. Pourtant on peut très bien écrire la requête équivalente:
var q =
customers.Where(c => c.City == "Paris").Select(c2 => c2.CustomerID);
J'ai volontairement nommé les paramètres 'c' et 'c2' pour montrer que ce sont deux variables qui finissent par véhiculer la même valeur et non pas un paramètre qui serait accessible un peu partout.
D'autres méthodes d'extension de Linq ne renvoient pas le paramètre d'entrée (Select, GroupBy) et cassent ainsi la visibilité des paramètres précédemment définis.
Un problème apparait donc : comment définir deux sources (v1 et v2 dans notre exemple) en rendant chacune d'elle accessible dans les méthodes suivantes ?
Je rappelle la déclaration précédente: un paramètre de méthode a une portée limité à la méthode elle-même. Le seul moyen est donc de créer un second 'Select' à l'intérieur d'un premier et non en enchaînant une séquence de méthodes.
var q = values1.SelectMany(v1 =>
values2.Select(v2 => v1 + v2));
Dans cette syntaxe, nous voyons bien que 'values2.Select()' est à l'intérieur de la méthode 'SelectMany'. Les méthodes anonymes partageant les variables de la méthode qui les porte, v1 est accessible dans le second 'Select' !
La syntaxe Linq est évidemment beaucoup plus simple à lire en nous faisant croire que les 'from' sont en séquence alors qu'ils s'imbriquent. On pourrait symboliquement ajouter les parenthèses suivantes:
var q =
from v1 in values1
( from v2 in values2
select v1 + v2 );
Pour répondre au commentaire de KooKiz du premier quizz, le 'let' a exactement la même problématique de rendre un paramètre accessible dans toute la suite de la requête. La compilation d'un let utilise une autre technique qui crée de manière transparente un type anonyme portant le paramètre. Un premier 'select' insère ce type anonyme portant le paramètre en début de requête alors qu'un dernier 'select' retire le type anonyme en ne sélectionnant que le paramètre. Ce dernier select terminant toujours la requête Linq, peut masquer un éventuel OrderBy ou ThenBy. C'est donc toujours un IEnumerable qui est renvoyé.
Cette compréhension ne vous est bien évidemment pas nécessaire pour utiliser Linq mais risque de le devenir si vous voulez écrire vos propres méthodes d'extension.
France Televisions vient de sortir un player Silverlight qui permet d’avoir 12 chaines TV et de suivre en direct sa discipline préférée mais aussi ce qui passera sur les chaines publiques France 2, France 3 et France 4.
http://sport.francetv.fr/jeux-olympiques-2008/
Toutes les informations sur le blog de Pierre Lagarde qui bien sûr n'est pas étranger à ce projet...
http://blogs.msdn.com/pierlag/archive/2008/08/07/france-t-l-visions-met-disposition-un-player-silverlight-pour-suivre-les-jo-de-p-kin-en-direct.aspx