[Réponse] GQ08 II: combinaisons

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.