Preferência de estilo LINQ [fechada]

21

Eu vim para usar o LINQ na minha programação diária todos os dias. De fato, raramente, se é que alguma vez, uso um loop explícito. Descobri, no entanto, que não uso mais o SQL como sintaxe. Eu apenas uso as funções de extensão. Então, ao invés de dizer:

from x in y select datatransform where filter 

Eu uso:

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

Qual estilo de LINQ você prefere e com que outras pessoas de sua equipe se sentem confortáveis?

Erin
fonte
5
Pode-se notar que a posição oficial do MS é que a sintaxe da consulta é preferível.
R0MANARMY
1
Em última análise, isso não importa. O que importa é que o código seja compreensível. Um formulário pode ser melhor em um caso, o outro em um caso diferente. Portanto, use o que for apropriado no momento.
ChrisF
Acredito que seu segundo exemplo é chamado sintaxe lambda, que eu uso 95% das vezes. Os outros 5% eu uso a sintaxe de consulta que é quando estou fazendo junções, estou tentando fazer a transição para junções de sintaxe lambda, mas, como outros apontaram, fica confuso.
O homem de muffin

Respostas:

26

Acho lamentável que a postura da Microsoft de acordo com a documentação do MSDN seja de que a sintaxe da consulta seja preferível, porque eu nunca a uso, mas uso a sintaxe do método LINQ o tempo todo. Adoro poder disparar consultas de uma linha para o conteúdo do meu coração. Comparar:

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

Para:

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

Mais rápido, menos linhas, e aos meus olhos parece mais limpo. A sintaxe da consulta também não suporta todos os operadores LINQ padrão. Um exemplo de consulta que fiz recentemente se parecia com isso:

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

Que eu saiba, para replicar essa consulta usando a sintaxe da consulta (na medida do possível), ficaria assim:

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

Não parece mais legível para mim e você precisaria saber como usar a sintaxe do método de qualquer maneira. Pessoalmente, estou realmente apaixonado pelo estilo declarativo que o LINQ torna possível e o uso em qualquer situação em que seja possível - talvez às vezes em meu prejuízo. Caso em questão, com a sintaxe do método, posso fazer algo assim:

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

Eu imagino que o código acima seria difícil de entender para alguém que entra no projeto sem uma boa documentação e, se não tiver um histórico sólido no LINQ, poderá não entender de qualquer maneira. Ainda assim, a sintaxe do método expõe uma capacidade bastante poderosa para projetar rapidamente (em termos de linhas de código) uma consulta para obter informações agregadas sobre várias coleções que, de outra forma, exigiriam muitos ciclos tediosos de foreach. Em um caso como esse, a sintaxe do método é ultra compacta para o que você obtém dela. Tentar fazer isso com a sintaxe da consulta pode ficar difícil de usar rapidamente.

klir2m
fonte
A transmissão que você pode fazer dentro do select, mas infelizmente você não pode especificar para obter os melhores registros X sem recorrer ao uso dos métodos LINQ. Isso é especialmente irritante em lugares onde você sabe que precisa apenas de um único registro e precisa colocar toda a consulta entre colchetes.
Ziv
2
Apenas para o registro, você pode fazer Select (x => x.ItemInfo) .OfType <GereralMerchInfo> () em vez de Where (). Select (). Cast <> (), que eu acredito que seja mais rápido (grande O de 2n em vez de n * 2m, eu acho). Mas você está totalmente certo, a sintaxe lambda é muito melhor do ponto de vista da legibilidade.
Ed James
16

Acho a sintaxe funcional mais agradável aos olhos. A única exceção é se eu precisar juntar mais de dois conjuntos. O Join () fica louco muito rapidamente.

John Kraft
fonte
Concordo ... Eu prefiro muito mais a aparência e a legibilidade dos métodos de extensão, exceto (como indicado) ao ingressar. Os fornecedores de componentes (por exemplo, Telerik) usam bastante os métodos de extensão. O exemplo em que estou pensando é sobre os controles Rad no ASP.NET MVC. Você precisa ser muito proficiente usando métodos de extensão para usar / ler esses.
Catchops
Veio dizer isso. Eu normalmente uso lambdas, a menos que haja uma associação envolvida. Depois que há uma associação, a sintaxe LINQ tende a ser mais legível.
Sean
10

É tarde demais para adicionar outra resposta?

Eu escrevi uma tonelada de código LINQ para objetos e afirmo que, pelo menos nesse domínio, é bom entender as duas sintaxes para usar o que for mais simples - o que nem sempre é uma sintaxe de pontos.

É claro que há momentos em que a sintaxe de pontos é o caminho a seguir - outros forneceram vários desses casos; no entanto, acho que as compreensões foram modificadas - dado um mau rap, se você quiser. Então, fornecerei uma amostra em que acredito que as compreensões são úteis.

Aqui está uma solução para um quebra-cabeça de substituição de dígitos: (solução escrita usando o LINQPad, mas pode ser autônoma em um aplicativo de 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);

... que produz:

N = 1, O = 6, K = 4

Não é tão ruim, a lógica flui linearmente e podemos ver que ela apresenta uma única solução correta. Esse quebra-cabeça é fácil de resolver manualmente: raciocinar que 3>> N0 e O> 4 * N implica 8> = O> = 4. Isso significa que há um máximo de 10 casos para testar manualmente (2 por N5 por 5) O) Já me desviei o suficiente - esse quebra-cabeça é oferecido para fins de ilustração do LINQ.

Transformações de compilador

O compilador faz muito para traduzir isso em sintaxe de ponto equivalente. Além da segunda efromSelectMany habitual cláusulas subseqüentes serem transformadas em chamadas , temos letcláusulas que se tornam Selectchamadas com projeções, as quais usam identificadores transparentes . Como estou prestes a mostrar, ter que nomear esses identificadores na sintaxe de pontos afasta a legibilidade dessa abordagem.

Eu tenho um truque para expor o que o compilador faz ao traduzir esse código para sintaxe de ponto. Se você descomentar as duas linhas comentadas acima e executá-las novamente, obterá o seguinte resultado:

N = 1, O = 6, K = 4

árvore de expressão da solução System.Linq.Enumerable + d_ b8.SelectMany (O => Intervalo (1, 8), (O, N) => novo <> 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))). Selecione (<> h_ TransparentIdentifier1 => novo <> 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))). Onde (<> 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_ TransparentIdentifier2 . product / 10) == <> h_ TransparentIdentifier3. <> h _TransparentIdentifier2. <> h_ TransparentIdentifier1. <> h_TransparentIdentifier0.O))). Selecione (<> h_ TransparentIdentifier3 => novo <> 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 cada operador LINQ em uma nova linha, traduzindo os identificadores "indizíveis" para aqueles que podemos "falar", alterando os tipos anônimos para sua forma familiar e alterando o AndAlsojargão da árvore de expressão para &&expor as transformações que o compilador faz para chegar a um equivalente na sintaxe de pontos:

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

Que, se você executar, poderá verificar se o resultado é novamente:

N = 1, O = 6, K = 4

... mas você escreveria um código como este?

Aposto que a resposta é NONBHN (não apenas não, mas não!) - porque é muito complexo. Claro que você pode criar nomes de identificadores mais significativos que "temp0" .. "temp3", mas o ponto é que eles não adicionam nada ao código - eles não fazem o código ter um desempenho melhor, eles não faça com que o código seja lido melhor, eles apenas o tornam feio e, se você o estivesse fazendo manualmente, sem dúvida você o estragaria uma ou três vezes antes de acertar. Além disso, jogar "o jogo do nome" já é difícil o suficiente para identificadores significativos, por isso congratulo-me com a pausa do jogo do nome que o compilador me fornece na compreensão de consultas.

Esse exemplo de quebra-cabeça pode não ser do mundo real o suficiente para você levar a sério; no entanto, existem outros cenários em que as compreensões da consulta brilham:

  • A complexidade Joine GroupJoin: o escopo das variáveis ​​de intervalo nas joincláusulas de compreensão de consulta transformam erros que de outra forma poderiam ser compilados na sintaxe de pontos em erros de tempo de compilação na sintaxe de compreensão.
  • Sempre que o compilador introduzir um identificador transparente na transformação da compreensão, as compreensões valem a pena. Isso inclui o uso de qualquer um dos seguintes itens: váriosfrom cláusulas, joine join..intocláusulas e letcláusulas.

Conheço mais de uma loja de engenharia em minha cidade natal que proibiu a sintaxe de compreensão. Eu acho que é uma pena, já que a sintaxe da compreensão é apenas uma ferramenta e uma ferramenta útil. Eu acho que é como dizer, "Há coisas que você pode fazer com uma chave de fenda e que não pode fazer com um cinzel. Como você pode usar uma chave de fenda como um cinzel, os formões são proibidos a partir de agora por decreto do rei".

devgeezer
fonte
-1: Uau. O OP estava procurando um pequeno conselho. Você produziu um romance! Você se importaria de apertar isso um pouco?
Jim G.
8

Meu conselho é usar a sintaxe de compreensão de consulta quando toda a expressão puder ser feita na sintaxe de compreensão. Ou seja, eu preferiria:

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

para

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

Mas eu preferiria

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

para

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

Eu gostaria que tivéssemos inventado alguma sintaxe que tornasse melhor misturar os dois. Algo como:

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

Mas, infelizmente, não o fizemos.

Mas, basicamente, é uma questão de preferência. Faça o que parecer melhor para você e seus colegas de trabalho.

Eric Lippert
fonte
3
Como alternativa, você pode considerar separar uma compreensão de outras chamadas do operador LINQ por meio de uma refatoração "introduzir variável de explicação". por exemplo,var londonCustomers = from c in ...; int count = londonCustomers.Count();
devgeezer 04/09/12
3

Como o SQL, é uma boa maneira de começar. Porém, como é limitado (suporta apenas as construções suportadas pelo seu idioma atual), os desenvolvedores acabam adotando o estilo dos métodos de extensão.

Gostaria de observar que existem alguns casos que podem ser facilmente implementados no estilo semelhante ao SQL.

Além disso, você pode combinar os dois lados em uma consulta.

SiberianGuy
fonte
2

Eu costumo usar a sintaxe que não é de consulta, a menos que precise definir uma variável no meio do caminho, embora a consulta como

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

mas escrevo a sintaxe sem consulta, como

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

fonte
2

Eu sempre uso as funções de extensão por causa do pedido. Pegue o seu exemplo simples - no SQL, você escreveu select primeiro - embora, na verdade, o local onde foi executado primeiro. Quando você escreve usando os métodos de extensão, sinto-me muito mais no controle. Recebo o Intellisense sobre o que está em oferta, escrevo as coisas na ordem em que elas acontecem.

DeadMG
fonte
Acho que você descobrirá que na sintaxe "compreensão de consulta" a ordem na página é igual à ordem em que as operações acontecem. O LINQ não coloca o "select" em primeiro lugar, ao contrário do SQL.
Eric Lippert
1

Também gosto da função de extensão.

Talvez porque seja menos um salto de sintaxe em minha mente.

Parece mais legível aos olhos também, especialmente se você estiver usando estruturas de terceiros que possuem linq api.

Erion
fonte
0

Aqui está a heurística que eu sigo:

Favorecer expressões LINQ sobre lambdas quando você tiver junções.

Eu acho que lambdas com junções parecem bagunçadas e difíceis de ler.

Jim G.
fonte