Préférence de style LINQ [fermé]

21

J'en suis venu à utiliser beaucoup LINQ dans ma programmation quotidienne. En fait, j'utilise rarement, voire jamais, une boucle explicite. J'ai cependant constaté que je n'utilise plus la syntaxe SQL. J'utilise juste les fonctions d'extension. Alors plutôt dire:

from x in y select datatransform where filter 

J'utilise:

x.Where(c => filter).Select(c => datatransform)

Quel style de LINQ préférez-vous et avec quoi les autres membres de votre équipe sont-ils à l'aise?

Erin
la source
5
Il peut être intéressant de noter que la position officielle de MS est que la syntaxe de requête est préférable.
R0MANARMY
1
En fin de compte, cela n'a pas d'importance. Ce qui compte, c'est que le code soit compréhensible. Une forme peut être meilleure dans un cas, l'autre dans un cas différent. Donc, utilisez ce qui est approprié à l'époque.
ChrisF
Je crois que votre deuxième exemple s'appelle la syntaxe lambda, que j'utilise 95% du temps. Les 5% restants que j'utilise la syntaxe de requête, c'est-à-dire lorsque je fais des jointures, j'essaie de passer aux jointures de syntaxe lambda, mais comme d'autres l'ont souligné, cela devient compliqué.
The Muffin Man

Réponses:

26

Je trouve regrettable que la position de Microsoft selon la documentation MSDN soit que la syntaxe de requête soit préférable, car je ne l'utilise jamais, mais j'utilise tout le temps la syntaxe de la méthode LINQ. J'adore pouvoir lancer des requêtes à une ligne au contenu de mon cœur. Comparer:

var products = from p in Products
               where p.StockOnHand == 0
               select p;

À:

var products = Products.Where(p => p.StockOnHand == 0);

Plus rapide, moins de lignes et à mes yeux semble plus propre. La syntaxe de requête ne prend pas non plus en charge tous les opérateurs LINQ standard. Un exemple de requête que j'ai récemment faite ressemblait à ceci:

var itemInfo = InventoryItems
    .Where(r => r.ItemInfo is GeneralMerchInfo)
    .Select(r => r.ItemInfo)
    .Cast<GeneralMerchInfo>()
    .FirstOrDefault(r => r.Xref == xref);

À ma connaissance, pour reproduire cette requête en utilisant la syntaxe de requête (dans la mesure du possible), cela ressemblerait à ceci:

var itemInfo = (from r in InventoryItems
                where r.ItemInfo is GeneralMerchInfo
                select r.ItemInfo)
                .Cast<GeneralMerchInfo>()
                .FirstOrDefault(r => r.Xref == xref);

Cela ne me semble pas plus lisible et vous devez de toute façon savoir utiliser la syntaxe des méthodes. Personnellement, je suis vraiment amoureux du style déclaratif que LINQ rend possible et je l'utilise dans toutes les situations où cela est possible - peut-être parfois à mon détriment. Par exemple, avec la syntaxe de la méthode, je peux faire quelque chose comme ceci:

// projects an InventoryItem collection with total stock on hand for each GSItem
inventoryItems = repository.GSItems
    .Select(gsItem => new InventoryItem() {
        GSItem = gsItem,
        StockOnHand = repository.InventoryItems
            .Where(inventoryItem => inventoryItem.GSItem.GSNumber == gsItem.GSNumber)
            .Sum(r => r.StockOnHand)
     });

J'imagine que le code ci-dessus serait difficile à comprendre pour quelqu'un entrant dans le projet sans une bonne documentation, et s'il n'a pas une solide expérience en LINQ, il pourrait ne pas le comprendre de toute façon. Pourtant, la syntaxe de la méthode expose certaines capacités assez puissantes pour projeter rapidement (en termes de lignes de code) une requête pour obtenir des informations agrégées sur plusieurs collections qui autrement prendraient beaucoup de temps pour chaque boucle fastidieuse. Dans un cas comme celui-ci, la syntaxe de la méthode est ultra compacte pour ce que vous en retirez. Essayer de le faire avec la syntaxe de la requête peut devenir difficile à gérer assez rapidement.

klir2m
la source
Le cast que vous pouvez faire à l'intérieur de la sélection, mais malheureusement, vous ne pouvez pas spécifier de prendre les enregistrements X supérieurs sans recourir aux méthodes LINQ. C'est particulièrement ennuyeux dans les endroits où vous savez que vous n'avez besoin que d'un seul enregistrement et que vous devez mettre toutes les requêtes entre crochets.
Ziv
2
Juste pour l'enregistrement, vous pouvez faire Select (x => x.ItemInfo) .OfType <GeneralMerchInfo> () au lieu de Where (). Select (). Cast <> (), qui je crois est plus rapide (grand O de 2n au lieu de n * 2m je pense). Mais vous avez tout à fait raison, la syntaxe lambda est bien meilleure du point de vue de la lisibilité.
Ed James
16

Je trouve la syntaxe fonctionnelle plus agréable à l'œil. La seule exception est si je dois rejoindre plus de deux ensembles. Le Join () devient fou très rapidement.

John Kraft
la source
D'accord ... Je préfère de beaucoup l'aspect et la lisibilité des méthodes d'extension sauf (comme indiqué) lors de la jonction. Les fournisseurs de composants (par exemple Telerik) utilisent beaucoup les méthodes d'extension. L'exemple auquel je pense est leur Rad Controls dans ASP.NET MVC. Vous devez être très compétent en utilisant des méthodes d'extension pour les utiliser / lire.
Catchops
Je suis venu dire ça. J'utilise habituellement des lambdas, sauf si une jointure est impliquée. Une fois qu'il y a une jointure, la syntaxe LINQ a tendance à être plus lisible.
Sean
10

Est-il trop tard pour ajouter une autre réponse?

J'ai écrit une tonne de code LINQ-to-objects et je soutiens qu'au moins dans ce domaine, il est bon de comprendre les deux syntaxes afin d'utiliser ce qui rend le code plus simple - ce qui n'est pas toujours la syntaxe à points.

Bien sûr, il y a des moments où la syntaxe à points EST la voie à suivre - d'autres ont fourni plusieurs de ces cas; cependant, je pense que les compréhensions ont été brièvement modifiées - étant donné un mauvais rap, si vous voulez. Je vais donc fournir un exemple où je pense que les compréhensions sont utiles.

Voici une solution à un casse-tête de substitution de chiffres: (solution écrite à l'aide de LINQPad, mais peut être autonome dans une application console)

// NO
// NO
// NO
//+NO
//===
// OK

var solutions =
    from O in Enumerable.Range(1, 8) // 1-9
                    //.AsQueryable()
    from N in Enumerable.Range(1, 8) // 1-9
    where O != N
    let NO = 10 * N + O
    let product = 4 * NO
    where product < 100
    let K = product % 10
    where K != O && K != N && product / 10 == O
    select new { N, O, K };

foreach(var i in solutions)
{
    Console.WriteLine("N = {0}, O = {1}, K = {2}", i.N, i.O, i.K);
}

//Console.WriteLine("\nsolution expression tree\n" + solutions.Expression);

... qui produit:

N = 1, O = 6, K = 4

Pas trop mal, la logique s'écoule linéairement et nous pouvons voir qu'elle propose une seule solution correcte. Ce casse-tête est assez facile à résoudre à la main: raisonner que 3>> N0 et O> 4 * N implique 8> = O> = 4. Cela signifie qu'il y a un maximum de 10 cas à tester à la main (2 pour N-par- 5 pour O). J'ai assez erré - ce puzzle est proposé à des fins d'illustration LINQ.

Transformations du compilateur

Le compilateur fait beaucoup pour traduire cela en syntaxe point équivalente. Outre la seconde fromclauseSelectMany habituelle et les clauses suivantes qui sont transformées en appels, nous avons des letclauses qui deviennent des Selectappels avec des projections, qui utilisent tous deux des identificateurs transparents . Comme je vais le montrer, le fait de devoir nommer ces identificateurs dans la syntaxe à points enlève à la lisibilité de cette approche.

J'ai une astuce pour exposer ce que le compilateur fait en traduisant ce code en syntaxe à points. Si vous décommentez les deux lignes commentées ci-dessus et l'exécutez à nouveau, vous obtiendrez la sortie suivante:

N = 1, O = 6, K = 4

arborescence d'expression de solution System.Linq.Enumerable + d_ b8.SelectMany (O => Range (1, 8), (O, N) => new <> f _AnonymousType0 2(O = O, N = N)).Where(<>h__TransparentIdentifier0 => (<>h__TransparentIdentifier0.O != <>h__TransparentIdentifier0.N)).Select(<>h__TransparentIdentifier0 => new <>f__AnonymousType12 (<> h_ TransparentIdentifier0 = <> h _TransparentIdentifier0, NO = ((10 * <> h_ TransparentIdentifier0.N) + <> h _TransparentIdentifier0.O))). Sélectionnez (<> h_ TransparentIdentifier1 => new <> f _AnonymousType2 2(<>h__TransparentIdentifier1 = <>h__TransparentIdentifier1, product = (4 * <>h__TransparentIdentifier1.NO))).Where(<>h__TransparentIdentifier2 => (<>h__TransparentIdentifier2.product < 100)).Select(<>h__TransparentIdentifier2 => new <>f__AnonymousType32 (<> h_ TransparentIdentifier2 = <> h _TransparentIdentifier2, K = ( <> h_ TransparentIdentifier2.product% 10))). Où (<> h _TransparentIdentifier3 => (((<> h_ TransparentIdentifier3.K! = <> h _TransparentIdentifier3. <> h_ TransparentIdentifier2. <>h _TransparentIdentifier1. <> h_TransparentIdentifier0.O) AndAlso (<> h _TransparentIdentifier3.K! = <> H_ TransparentIdentifier3. <> H _TransparentIdentifier2. <> H_ TransparentIdentifier1. <> H _TransparentIdentifier0.N)) AndAlso ((<> h_ TransparentIdentifier3. < Indentifier2. produit / 10) == <> h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.O)))). Sélectionnez (<> h_ TransparentIdentifier3 => nouveau <> f _AnonymousType4`3 (N = < > h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.N,O = <> h_ TransparentIdentifier3. <> H_TransparentIdentifier2. <> H_ TransparentIdentifier1. <> H _TransparentIdentifier0.O, K = <> h__TransparentIdentifier3.K))

Mettre chaque opérateur LINQ sur une nouvelle ligne, traduire les identificateurs "indescriptibles" en ceux que nous pouvons "parler", changer les types anonymes en leur forme familière et changer le AndAlsojargon de l'arborescence d'expressions pour &&exposer les transformations que fait le compilateur pour arriver à un équivalent en syntaxe à points:

var solutions = 
    Enumerable.Range(1,8) // from O in Enumerable.Range(1,8)
        .SelectMany(O => Enumerable.Range(1, 8), (O, N) => new { O = O, N = N }) // from N in Enumerable.Range(1,8)
        .Where(temp0 => temp0.O != temp0.N) // where O != N
        .Select(temp0 => new { temp0 = temp0, NO = 10 * temp0.N + temp0.O }) // let NO = 10 * N + O
        .Select(temp1 => new { temp1 = temp1, product = 4 * temp1.NO }) // let product = 4 * NO
        .Where(temp2 => temp2.product < 100) // where product < 100
        .Select(temp2 => new { temp2 = temp2, K = temp2.product % 10 }) // let K = product % 10
        .Where(temp3 => temp3.K != temp3.temp2.temp1.temp0.O && temp3.K != temp3.temp2.temp1.temp0.N && temp3.temp2.product / 10 == temp3.temp2.temp1.temp0.O)
        // where K != O && K != N && product / 10 == O
        .Select(temp3 => new { N = temp3.temp2.temp1.temp0.N, O = temp3.temp2.temp1.temp0.O, K = temp3.K });
        // select new { N, O, K };

foreach(var i in solutions)
{
    Console.WriteLine("N = {0}, O = {1}, K = {2}", i.N, i.O, i.K);
}

Si vous exécutez, vous pouvez vérifier qu'il renvoie à nouveau:

N = 1, O = 6, K = 4

... mais écririez-vous jamais du code comme celui-ci?

Je parierais que la réponse est NONBHN (non seulement non, mais enfer non!) - parce que c'est tout simplement trop complexe. Bien sûr, vous pouvez trouver des noms d'identificateurs plus significatifs que "temp0" .. "temp3", mais le fait est qu'ils n'ajoutent rien au code - ils n'améliorent pas le code, ils ne le font pas mieux rendre le code plus lisible, ils ne font que vider le code, et si vous le faisiez à la main, vous le gâcheriez sans doute une ou trois fois avant de le faire correctement. De plus, jouer au "jeu de noms" est assez difficile pour des identifiants significatifs, donc je me réjouis de la rupture avec le jeu de noms que le compilateur me fournit dans les compréhensions de requêtes.

Cet exemple de puzzle peut ne pas être assez réel pour que vous le preniez au sérieux; cependant, d'autres scénarios existent où les compréhensions de requête brillent:

  • La complexité de Joinet GroupJoin: l'étendue des variables de plage dans les joinclauses de compréhension des requêtes transforme les erreurs qui pourraient autrement se compiler en syntaxe à points en erreurs au moment de la compilation dans la syntaxe de compréhension.
  • Chaque fois que le compilateur introduirait un identificateur transparent dans la transformation de compréhension, les compréhensions en valent la peine. Cela comprend l'utilisation de l'un des éléments suivants: plusieurs fromclauses, join& join..intoclauses et letclauses.

Je connais plus d'un atelier d'ingénierie dans ma ville natale qui a interdit la syntaxe de compréhension. Je pense que c'est dommage car la syntaxe de compréhension n'est qu'un outil et un outil utile à cela. Je pense que c'est un peu comme dire: "Il y a des choses que vous pouvez faire avec un tournevis que vous ne pouvez pas faire avec un burin. Parce que vous pouvez utiliser un tournevis comme burin, les burins sont désormais interdits par décret du roi."

devgeezer
la source
-1: Ouah. L'OP cherchait un petit conseil. Vous avez sorti un roman! Pourriez-vous resserrer un peu cela?
Jim G.
8

Mon conseil est d'utiliser la syntaxe de compréhension des requêtes lorsque l'expression entière peut être effectuée dans la syntaxe de compréhension. Autrement dit, je préférerais:

var query = from c in customers orderby c.Name select c.Address;

à

var query = customers.OrderBy(c=>c.Name).Select(c=>c.Address);

Mais je préfère

int count = customers.Where(c=>c.City == "London").Count();

à

int count = (from c in customers where c.City == "London" select c).Count();

Je souhaite que nous ayons trouvé une syntaxe qui a rendu plus agréable de mélanger les deux. Quelque chose comme:

int count = from c in customers 
            where c.City == "London" 
            select c 
            continue with Count();

Mais malheureusement, nous ne l'avons pas fait.

Mais en gros, c'est une question de préférence. Faites celui qui vous convient le mieux ainsi qu'à vos collègues.

Eric Lippert
la source
3
Alternativement, vous pouvez envisager de séparer une compréhension des autres appels d'opérateur LINQ via un refactoring "introduire une variable explicative". par exemple,var londonCustomers = from c in ...; int count = londonCustomers.Count();
devgeezer
3

Le SQL-like est une bonne façon de commencer. Mais comme il est limité (il ne prend en charge que les constructions prises en charge par votre langage actuel), les développeurs finissent par adopter le style des méthodes d'extension.

Je voudrais noter qu'il existe certains cas qui peuvent être facilement mis en œuvre par un style similaire à SQL.

Vous pouvez également combiner les deux façons dans une même requête.

SiberianGuy
la source
2

J'ai tendance à utiliser la syntaxe sans requête, sauf si je dois définir une variable à mi-chemin si la requête comme

from x in list
let y = x.DoExpensiveCalulation()
where y > 42
select y

mais j'écris la syntaxe non-requête comme

x.Where(c => filter)
 .Select(c => datatransform)

la source
2

J'utilise toujours les fonctions d'extension à cause de la commande. Prenez votre exemple simple - dans le SQL, vous avez écrit select first - même si en fait, le où a été exécuté en premier. Lorsque vous écrivez en utilisant les méthodes d'extension, je me sens beaucoup plus en contrôle. Je reçois Intellisense sur ce qui est proposé, j'écris les choses dans l'ordre où elles se produisent.

DeadMG
la source
Je pense que vous constaterez que dans la syntaxe de "compréhension des requêtes", l'ordre sur la page est le même que l'ordre dans lequel les opérations se déroulent. LINQ ne place pas la "sélection" en premier, contrairement à SQL.
Eric Lippert
1

J'aime aussi la fonction d'extension.

Peut-être parce que c'est moins un saut de syntaxe dans mon esprit.

Cela semble plus lisible à l'œil aussi, surtout si vous utilisez des frameworks tiers qui ont linq api.

Erion
la source
0

Voici l'heuristique que je suis:

Privilégiez les expressions LINQ aux lambdas lorsque vous avez des jointures.

Je pense que les lambdas avec jointures semblent désordonnés et difficiles à lire.

Jim G.
la source