LINQ Preferencia de estilo [cerrado]

21

He venido a usar LINQ en mi programación diaria. De hecho, rara vez, si alguna vez, uso un ciclo explícito. Sin embargo, descubrí que ya no uso la sintaxis de SQL. Solo uso las funciones de extensión. Entonces, en lugar de decir:

from x in y select datatransform where filter 

Yo suelo:

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

¿Qué estilo de LINQ prefiere y con qué otros miembros de su equipo se sienten cómodos?

Irlanda
fuente
55
Vale la pena señalar que la postura oficial de MS es que la sintaxis de consulta es preferible.
R0MANARMY
1
En definitiva no importa. Lo que importa es que el código sea comprensible. Una forma puede ser mejor en un caso, la otra en un caso diferente. Así que use lo que sea apropiado en ese momento.
ChrisF
Creo que su segundo ejemplo se llama sintaxis lambda, que uso el 95% del tiempo. El otro 5% utilizo la sintaxis de consulta, que es cuando estoy haciendo uniones, intento hacer la transición a las uniones de sintaxis lambda, pero como otros han señalado, se vuelve desordenado.
The Muffin Man

Respuestas:

26

Me parece lamentable que la postura de Microsoft según la documentación de MSDN sea que la sintaxis de consulta es preferible, porque nunca la uso, pero uso la sintaxis del método LINQ todo el tiempo. Me encanta poder hacer consultas de una sola línea al contenido de mi corazón. Comparar:

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

A:

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

Más rápido, menos líneas, y para mis ojos se ve más limpio. La sintaxis de consulta tampoco admite todos los operadores estándar de LINQ. Una consulta de ejemplo que hice hace poco se parecía a esto:

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

Que yo sepa, para replicar esta consulta utilizando la sintaxis de consulta (en la medida de lo posible) se vería así:

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

No me parece más legible, y de todos modos necesitarías saber cómo usar la sintaxis del método. Personalmente, estoy realmente enamorado del estilo declarativo que LINQ hace posible y lo uso en cualquier situación en la que sea posible, tal vez a veces en mi detrimento. Caso en cuestión, con la sintaxis del método puedo hacer algo como esto:

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

Me imagino que el código anterior sería difícil de entender para alguien que entra en el proyecto sin una buena documentación, y si no tienen una sólida formación en LINQ, es posible que no lo entiendan de todos modos. Aún así, la sintaxis del método expone una capacidad bastante poderosa para proyectar rápidamente (en términos de líneas de código) una consulta para obtener información agregada sobre múltiples colecciones que de otro modo requerirían una gran cantidad de bucles foreach tediosos. En un caso como este, la sintaxis del método es ultra compacta para lo que obtienes de ella. Intentar hacer esto con la sintaxis de consulta puede volverse difícil de manejar bastante rápido.

klir2m
fuente
El reparto lo puede hacer dentro de la selección, pero desafortunadamente no puede especificar tomar los registros X principales sin recurrir al uso de los métodos LINQ. Esto es especialmente molesto en lugares donde sabe que solo necesita un único registro y tiene que poner toda la consulta entre paréntesis.
Ziv
2
Solo para el registro puede hacer Select (x => x.ItemInfo) .OfType <GeneralMerchInfo> () en lugar de Where (). Select (). Cast <> (), que creo que es más rápido (gran O de 2n en lugar de n * 2m, creo). Pero tiene toda la razón, la sintaxis lambda es mucho mejor desde el punto de vista de la legibilidad.
Ed James
16

La sintaxis funcional me parece más agradable a la vista. La única excepción es si necesito unir más de dos conjuntos. The Join () se vuelve loco muy rápido.

John Kraft
fuente
De acuerdo ... prefiero mucho más el aspecto y la legibilidad de los métodos de extensión, excepto (como se señaló) al unirme. Los proveedores de componentes (por ejemplo, Telerik) utilizan mucho los métodos de extensión. El ejemplo en el que estoy pensando es en sus Rad Controls en ASP.NET MVC. Debes ser muy competente usando métodos de extensión para usarlos / leerlos.
Catchops
Vino a decir esto. Usualmente uso lambdas a menos que haya una unión involucrada. Una vez que hay una unión, la sintaxis de LINQ tiende a ser más legible.
Sean
10

¿Es demasiado tarde para agregar otra respuesta?

He escrito un montón de código LINQ-to-objects y afirmo que, al menos en ese dominio, es bueno comprender ambas sintaxis para usar el código que sea más simple, que no siempre es sintaxis de puntos.

Por supuesto, hay momentos en que la sintaxis de puntos ES el camino a seguir; otros han proporcionado varios de estos casos; sin embargo, creo que las comprensiones se han modificado brevemente, dado un mal golpe, por así decirlo. Así que proporcionaré una muestra donde creo que las comprensiones son útiles.

Aquí hay una solución para un rompecabezas de sustitución de dígitos: (solución escrita usando LINQPad, pero puede ser independiente en una aplicación de consola)

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

... que produce:

N = 1, O = 6, K = 4

No está mal, la lógica fluye linealmente y podemos ver que surge una única solución correcta. Este rompecabezas es bastante fácil de resolver a mano: razonando que 3>> N0 y O> 4 * N implica 8> = O> = 4. Eso significa que hay un máximo de 10 casos para probar a mano (2 para N-por- 5 para O) Me he desviado lo suficiente: este rompecabezas se ofrece con fines ilustrativos de LINQ.

Transformaciones del compilador

El compilador hace mucho para traducir esto en sintaxis de puntos equivalente. Además de las segundas fromcláusulasSelectMany habituales y posteriores que se convierten en llamadas , tenemos letcláusulas que se convierten en Selectllamadas con proyecciones, las cuales usan identificadores transparentes . Como estoy a punto de mostrar, tener que nombrar estos identificadores en la sintaxis de puntos elimina la legibilidad de ese enfoque.

Tengo un truco para exponer lo que hace el compilador al traducir este código a la sintaxis de puntos. Si descomenta las dos líneas comentadas arriba y lo ejecuta nuevamente, obtendrá el siguiente resultado:

N = 1, O = 6, K = 4

árbol de expresión de solución 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))). Seleccione (<> 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))). Donde (<> 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. <> H_ product / 10) == <> h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.O))). Seleccione (<> h_ TransparentIdentifier3 => new <> f _AnonymousType4`3 (N = < > h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h _TransparentIdentifier0.N,O = <> h_Identificador transparente3. <> H_TransparentIdentifier2. <> H_ TransparentIdentifier1. <> H _TransparentIdentifier0.O, K = <> h__TransparentIdentifier3.K))

Colocando a cada operador LINQ en una nueva línea, traduciendo los identificadores "indescriptibles" a los que podemos "hablar", cambiando los tipos anónimos a su forma familiar y cambiando la AndAlsojerga del árbol de expresiones para &&exponer las transformaciones que hace el compilador para llegar a un equivalente en sintaxis de puntos:

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

Lo que si ejecuta puede verificar que salga nuevamente:

N = 1, O = 6, K = 4

... pero ¿escribirías código como este?

Apuesto a que la respuesta es NONBHN (¡No solo no, sino que no!), Porque es demasiado complejo. Claro que puede encontrar algunos nombres de identificadores más significativos que "temp0" .. "temp3", pero el punto es que no agregan nada al código, no hacen que el código funcione mejor, no lo hacen haga que el código se lea mejor, solo lo desagradable, y si lo hiciera a mano, sin duda lo estropearía una o tres veces antes de hacerlo bien. Además, jugar "el juego de los nombres" es lo suficientemente difícil como para identificar identificadores significativos, por lo que agradezco el descanso del juego de nombres que el compilador me proporciona en la comprensión de consultas.

Es posible que esta muestra de rompecabezas no sea lo suficientemente real como para que la tome en serio; sin embargo, existen otros escenarios donde brillan las comprensiones de consultas:

  • La complejidad de Joiny GroupJoin: el alcance de las variables de rango en las joincláusulas de comprensión de consultas convierten los errores que de otro modo podrían compilarse en sintaxis de puntos en errores de tiempo de compilación en la sintaxis de comprensión.
  • Cada vez que el compilador introduce un identificador transparente en la transformación de comprensión, las comprensiones valen la pena. Esto incluye el uso de cualquiera de los siguientes: múltiples fromcláusulas, joiny join..intocláusulas y letcláusulas.

Sé de más de un taller de ingeniería en mi ciudad natal que ha prohibido la sintaxis de comprensión. Creo que es una pena, ya que la sintaxis de comprensión no es más que una herramienta y una herramienta útil. Creo que es muy parecido a decir: "Hay cosas que puedes hacer con un destornillador que no puedes hacer con un cincel. Debido a que puedes usar un destornillador como cincel, los cinceles están prohibidos en adelante por decreto del rey".

devgeezer
fuente
-1: Wow. El OP estaba buscando un pequeño consejo. ¡Produjiste una novela! ¿Te importaría endurecer esto un poco?
Jim G.
8

Mi consejo es usar la sintaxis de comprensión de consultas cuando toda la expresión se puede hacer en la sintaxis de comprensión. Es decir, preferiría:

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

a

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

Pero preferiría

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

a

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

Ojalá hubiéramos creado una sintaxis que hiciera más agradable mezclar los dos. Algo como:

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

Pero lamentablemente no lo hicimos.

Pero básicamente, es una cuestión de preferencia. Haz el que te parezca mejor a ti y a tus compañeros de trabajo.

Eric Lippert
fuente
3
Alternativamente, podría considerar separar una comprensión de otras llamadas de operador LINQ a través de una refactorización de "introducción de variables explicativas". por ejemplo,var londonCustomers = from c in ...; int count = londonCustomers.Count();
devgeezer
3

SQL-like es una buena manera de comenzar. Pero como es limitado (solo es compatible con las construcciones que admite su lenguaje actual), eventualmente los desarrolladores van al estilo de métodos de extensión.

Me gustaría señalar que hay algunos casos que pueden implementarse fácilmente con un estilo similar a SQL.

También puede combinar ambas formas en una consulta.

SiberianGuy
fuente
2

Tiendo a usar la sintaxis sin consulta a menos que necesite definir una variable a mitad de camino aunque la consulta como

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

pero escribo la sintaxis sin consulta como

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

fuente
2

Siempre uso las funciones de extensión debido a los pedidos. Tome su ejemplo simple, en el SQL, escribió select primero, aunque en realidad, donde se ejecutó primero. Cuando escribes usando los métodos de extensión, me siento mucho más en control. Recibo Intellisense sobre lo que se ofrece, escribo las cosas en el orden en que suceden.

DeadMG
fuente
Creo que encontrará que en la sintaxis de "comprensión de consultas", el orden en la página es el mismo que el orden en que ocurren las operaciones. LINQ no pone primero la "selección", a diferencia de SQL.
Eric Lippert
1

También me gusta la función de extensión.

Tal vez porque es menos un salto de sintaxis en mi mente.

También se siente más legible a la vista, especialmente si está utilizando marcos de terceros que tienen api linq.

Erion
fuente
0

Aquí está la heurística que sigo:

Favorezca las expresiones LINQ sobre lambdas cuando tenga uniones.

Creo que las lambdas con combinaciones se ven desordenadas y son difíciles de leer.

Jim G.
fuente