Preferenza stile LINQ [chiuso]


21

Sono venuto a usare LINQ nella mia programmazione quotidiana molto. In effetti, raramente, se mai, utilizzo un ciclo esplicito. Tuttavia, ho scoperto che non uso più la sintassi simile a SQL. Uso solo le funzioni di estensione. Quindi piuttosto dicendo:

from x in y select datatransform where filter 

Io uso:

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

Quale stile di LINQ preferisci e con quali altri la tua squadra è a suo agio?


5
Vale la pena notare che la posizione ufficiale della SM è che la sintassi della query è preferibile.
R0MANARMY,

1
Alla fine non importa. Ciò che conta è che il codice è comprensibile. Una forma può essere migliore in un caso, l'altra in un caso diverso. Quindi usa quello che è sempre appropriato al momento.
ChrisF

Credo che il tuo secondo esempio sia chiamato sintassi lambda, che uso il 95% delle volte. L'altro 5% utilizzo la sintassi della query che è quando eseguo i join, sto tentando di passare ai join della sintassi lambda, ma come altri hanno sottolineato, diventa disordinato.
The Muffin Man,

Risposte:


26

Trovo sfortunato che la posizione di Microsoft per la documentazione MSDN sia che la sintassi della query è preferibile, perché non la uso mai, ma utilizzo sempre la sintassi del metodo LINQ. Adoro essere in grado di rispondere a domande a risposta libera al contenuto del mio cuore. Confrontare:

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

A:

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

Più veloce, meno linee e ai miei occhi sembra più pulito. La sintassi della query non supporta neanche tutti gli operatori LINQ standard. Una query di esempio che ho fatto di recente è simile a questa:

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

Per quanto ne so, replicare questa query usando la sintassi della query (per quanto possibile) sarebbe simile al seguente:

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

Non mi sembra più leggibile e dovresti comunque sapere come usare la sintassi del metodo. Personalmente sono davvero innamorato dello stile dichiarativo che LINQ rende possibile e lo uso in qualsiasi situazione in cui sia possibile - forse a volte a mio danno. Caso in questione, con la sintassi del metodo posso fare qualcosa del genere:

// 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)
     });

Immagino che il codice di cui sopra sarebbe difficile da capire per qualcuno che entra nel progetto senza una buona documentazione, e se non hanno un solido background in LINQ potrebbero non capirlo comunque. Tuttavia, la sintassi del metodo espone alcune capacità piuttosto potenti, per proiettare rapidamente (in termini di righe di codice) una query per ottenere informazioni aggregate su più raccolte che altrimenti richiederebbero un noioso ciclo foreach. In un caso come questo, la sintassi del metodo è ultra compatta per ciò che ne esce. Provare a farlo con la sintassi della query potrebbe diventare ingombrante piuttosto rapidamente.


Il cast che puoi fare all'interno della selezione, ma sfortunatamente non puoi specificare di prendere i primi X record senza ricorrere all'uso dei metodi LINQ. Ciò è particolarmente fastidioso in luoghi in cui sai che hai solo bisogno di un singolo record e devi mettere tutta la query tra parentesi.
Ziv,

2
Solo per la cronaca puoi fare Select (x => x.ItemInfo) .OfType <GeneralMerchInfo> () invece di Where (). Select (). Cast <> (), che credo sia più veloce (grande O di 2n invece di n * 2m penso). Ma hai perfettamente ragione, la sintassi lambda è molto meglio dal punto di vista della leggibilità.
Ed James,

16

Trovo la sintassi funzionale più piacevole alla vista. L'unica eccezione è se devo unire più di due set. Join () diventa pazzo molto rapidamente.


D'accordo ... Preferisco di più l'aspetto e la leggibilità dai metodi di estensione, tranne (come sottolineato) quando mi unisco. I fornitori di componenti (ad es. Telerik) utilizzano molto i metodi di estensione. L'esempio a cui sto pensando sono i loro controlli Rad in ASP.NET MVC. Devi essere molto abile usando i metodi di estensione per usare / leggere quelli.
Catchops

Sono venuto a dire questo. Di solito uso lambdas a meno che non sia coinvolto un join. Una volta che c'è un join, la sintassi LINQ tende ad essere più leggibile.
Sean,

10

È mai troppo tardi per aggiungere un'altra risposta?

Ho scritto un sacco di codice LINQ-to-object e sostengo che almeno in quel dominio è bene capire entrambe le sintassi per usare qualunque sia il codice più semplice, che non è sempre una sintassi punto.

Naturalmente ci sono momenti in cui la sintassi del punto È la strada da percorrere - altri hanno fornito molti di questi casi; tuttavia, penso che le comprensioni siano state modificate a breve - dato un brutto colpo, se vuoi. Quindi fornirò un esempio in cui credo che le comprensioni siano utili.

Ecco una soluzione a un puzzle di sostituzione delle cifre: (soluzione scritta usando LINQPad, ma può essere autonoma in un'app 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);

... che genera:

N = 1, O = 6, K = 4

Non male, la logica scorre in modo lineare e possiamo vedere che viene fuori con un'unica soluzione corretta. Questo puzzle è abbastanza facile da risolvere a mano: ragionando che 3>> N0 e O> 4 * N implica 8> = O> = 4. Ciò significa che ci sono un massimo di 10 casi da testare a mano (2 perN -by- 5 per O). Ho smarrito abbastanza: questo puzzle è offerto a scopo illustrativo LINQ.

Trasformazioni del compilatore

C'è molto che il compilatore fa per tradurre questo in sintassi dot equivalente. Oltre alle solite seconde e successive fromclausole che vengono trasformate in SelectManychiamate, abbiamo letclausole che diventano Selectchiamate con proiezioni, entrambe le quali usano identificatori trasparenti . Come sto per mostrare, dover nominare questi identificatori nella sintassi del punto toglie la leggibilità di quell'approccio.

Ho un trucco per esporre ciò che fa il compilatore nel tradurre questo codice in sintassi punto. Se si decommenta le due righe commentate sopra e lo si esegue di nuovo, si otterrà il seguente output:

N = 1, O = 6, K = 4

albero delle espressioni della soluzione 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))). Selezionare (<> 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))). Where (<> h _TransparentIdentifier3 => ((((<> h_ TransparentIdentifier3.K! = <> h _TransparentIdentifier3. <> h_ TransparentIdentifier2. <>h _TransparentIdentifier1. <> h_TransparentIdentifier0.O) AndAlso (<> h h _TransparentIdentifier3.K! = <> h_TransparentIdentifier3. <> H _TransparentIdentifier2. <> H_ TransparentIdentifier1. <> H _TransparentIdentifier0.N)) AndAlso ((<> h_ TransparentIdentifier3. <> H _TransparentIdentifier > <> h ) h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.O))). Selezionare (<> h_ TransparentIdentifier3 => nuova <> f _AnonymousType4`3 (N = <> h_ TransparentIdentifier3. <> h _TransparentIdentifier2 . <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.N, O = <> h_ TransparentIdentifier3. <_TransparentIdentifier2. <> H_TransparentIdentifier1. <> H _TransparentIdentifier0.O, K = <> h__TransparentIdentifier3.K))

Mettendo ogni operatore LINQ su una nuova riga, traducendo gli identificatori "indicibili" in quelli che possiamo "parlare", cambiando i tipi anonimi nella loro forma familiare e cambiando il AndAlsogergo dell'albero delle espressioni per &&esporre le trasformazioni che il compilatore fa per arrivare ad un equivalente in sintassi punto:

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);
}

Che se esegui puoi verificare che restituisca di nuovo:

N = 1, O = 6, K = 4

... ma scriveresti mai un codice come questo?

Scommetto che la risposta è NONBHN (non solo no, ma l'inferno no!) - perché è troppo complessa. Sicuramente puoi trovare alcuni nomi di identificatori più significativi di "temp0" .. "temp3", ma il punto è che non aggiungono nulla al codice - non fanno funzionare meglio il codice, non lo fanno rendere il codice migliore, rendono il brutto codice e se lo facessi a mano, senza dubbio lo sbaglieresti una o tre volte prima di farlo bene. Inoltre, giocare a "il gioco del nome" è abbastanza difficile per identificatori significativi, quindi accolgo con favore l'interruzione del gioco del nome che il compilatore mi fornisce nella comprensione delle query.

Questo esempio di puzzle potrebbe non essere abbastanza reale da poter essere preso sul serio; tuttavia, esistono altri scenari in cui brillano le comprensioni:

  • La complessità di Joine GroupJoin: l'ambito delle variabili di intervallo nelle joinclausole di comprensione delle query trasforma gli errori che altrimenti potrebbero essere compilati in sintassi punto in errori di tempo di compilazione nella sintassi di comprensione.
  • Ogni volta che il compilatore introduce un identificatore trasparente nella trasformazione della comprensione, le comprensioni diventano utili. Ciò include l'uso di uno dei seguenti: più fromclausole, joine join..intoclausole e letclausole.

Conosco più di un negozio di ingegneria nella mia città che ha messo fuorilegge la sintassi della comprensione. Penso che sia un peccato dato che la sintassi di comprensione non è che uno strumento e utile a questo. Penso che sia molto come dire: "Ci sono cose che puoi fare con un cacciavite che non puoi fare con uno scalpello. Poiché puoi usare un cacciavite come scalpello, gli scalpelli sono banditi d'ora in poi con il decreto del re."


-1: Wow. L'OP stava cercando un piccolo consiglio. Hai sfornato un romanzo! Ti dispiacerebbe stringere un po 'questo?
Jim G.

8

Il mio consiglio è di usare la sintassi della comprensione della query quando l'intera espressione può essere fatta nella sintassi della comprensione. Cioè, preferirei:

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

a

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

Ma preferirei

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

a

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

Vorrei che avessimo escogitato una sintassi che rendesse più piacevole mescolare i due. Qualcosa di simile a:

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

Ma purtroppo no.

Ma sostanzialmente è una questione di preferenza. Fai quello che sembra migliore a te e ai tuoi colleghi.


3
In alternativa, potresti prendere in considerazione la possibilità di separare una comprensione dalle altre chiamate dell'operatore LINQ tramite un refactoring "introduci spiegando la variabile". per esempio,var londonCustomers = from c in ...; int count = londonCustomers.Count();
devgeezer,

3

SQL-like è un buon modo per iniziare. Ma poiché è limitato (supporta solo quelle costruzioni che la tua lingua corrente supporta) alla fine gli sviluppatori passano allo stile dei metodi di estensione.

Vorrei sottolineare che ci sono alcuni casi che possono essere facilmente implementati da uno stile simile a SQL.

Inoltre puoi combinare entrambi i modi in una query.


2

Tendo a usare la sintassi non di query a meno che non sia necessario definire una variabile a metà strada sebbene la query sia simile

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

ma scrivo la sintassi non query come

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

2

Uso sempre le funzioni di estensione a causa dell'ordinamento. Prendi il tuo semplice esempio - nell'SQL, hai scritto select first- anche se in realtà, dove è stato eseguito per primo. Quando scrivi usando i metodi di estensione, mi sento molto più in controllo. Ottengo Intellisense su ciò che viene offerto, scrivo le cose nell'ordine in cui accadono.


Penso che scoprirai che nella sintassi della "comprensione delle query" l'ordinamento nella pagina è lo stesso dell'ordine in cui si verificano le operazioni. LINQ non inserisce prima la "selezione", a differenza di SQL.
Eric Lippert,

1

Mi piace anche la funzione di estensione.

Forse perché è meno un salto di sintassi nella mia mente.

Sembra anche più leggibile alla vista, soprattutto se si utilizzano framework di terze parti che hanno linq api.


0

Ecco l'euristica che seguo:

Favorisci le espressioni LINQ rispetto a lambdas quando hai unioni.

Penso che i lambda con i join sembrino disordinati e difficili da leggere.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.