← C#: linguagem e decisões de aplicação
06 / 10 · 50 MIN

LINQ, trabalho diferido e materialização

Distingue definir uma consulta de obter resultados, fixa valores para relatórios e diagnostica cardinalidade, ordenação e falhas diferidas.

Fonte, consulta e consumo

Nesta aula, LINQ-to-Objects significa operações de Enumerable sobre arrays, listas e outras sequências locais. Where escolhe elementos por um predicado e Select transforma cada elemento; nenhuma destas operações implica alterar a coleção original.

Escrever var q = readings.Where(r => r.Accepted) define uma consulta. Para obter resultados é necessário consumi-la, por exemplo com foreach ou ToList. Se a origem local estiver vazia, um filtro válido produz uma sequência vazia, o que pode ou não satisfazer a regra de negócio.

Começa pela pergunta: preciso de todos os registos elegíveis, de um total ou de exatamente um registo? Essa decisão precede a escolha do operador. Uma expressão semelhante sobre IQueryable pode ser interpretada por um fornecedor remoto.

Não transfiras pressupostos de execução local para SQL, custos de rede ou tradução de funções sem consultar esse fornecedor.

O momento da leitura faz parte do contrato

Uma consulta diferida pode observar alterações feitas antes da enumeração. Se uma lista contém 1 e 4, a consulta Where(x => x > 2) é criada e depois se acrescenta 6, a enumeração posterior devolve 4 e 6. Isto difere de alterar uma List enquanto um enumerador já está ativo, situação que pode invalidá-lo.

Também os parâmetros capturados merecem atenção: uma lambda que usa a variável local limit lê essa variável quando é chamada. Alterar limit entre a definição e o consumo pode mudar a seleção. Para conservar um limite, guarda o valor num local dedicado que não seja alterado; isto conserva o parâmetro, mas não congela a origem.

Documenta separadamente quando ficam fixos os critérios e quando são recolhidos os dados. Um nome como acceptedQuery não demonstra que a aceitação já foi avaliada.

Materializar fixa resultados, com limites de cópia

ToList consome uma consulta e guarda os elementos obtidos numa nova lista. Voltar a ler essa lista evita repetir a consulta original. Acrescentar depois um elemento à lista de origem não o acrescenta à lista resultante, mas ambas podem conter referências aos mesmos objetos.

Se Reading é uma classe mutável, alterar Amount num objeto continua visível por qualquer referência a esse objeto. Para um relatório estável, projeta os campos necessários para valores próprios antes da materialização: por exemplo, um novo record com ID string e Amount decimal.

Não coloques nesse record uma lista mutável partilhada e depois pressuponhas uma cópia profunda. A recolha deve ainda ocorrer sob uma política que impeça alterações durante a captura; ToList não fornece uma transação nem sincronização. Decide também o limite de volume: guardar todos os resultados exige memória proporcional ao conjunto retido.

A cardinalidade deve expressar a regra de negócio

Single expressa a exigência de exatamente um elemento elegível. Dois elementos com o mesmo valor continuam a ser dois: Single sobre 7, 7 falha, apesar de só existir um valor distinto. Esta verificação é útil quando qualquer desvio de cardinalidade deve interromper a operação.

Se o consumidor tem de distinguir ausência de conflito, não convertas indistintamente a exceção de Single num estado de ausência. Num lote pequeno e estável, materializar as correspondências e classificar Count em zero, um ou vários torna os três resultados explícitos.

Já FirstOrDefault sobre int pode devolver 0 por a origem estar vazia ou por o primeiro valor ser realmente zero. Quando zero é válido, mantém informação de presença separada. Escolher uma API porque evita uma exceção, sem preservar estes estados, pode transformar um erro de dados numa aceitação silenciosa.

Ordenação principal e desempate

Para Priority crescente e Age crescente dentro de cada prioridade, usa OrderBy(x => x.Priority).ThenBy(x => x.Age). Um segundo OrderBy inicia outra ordenação principal; não significa acrescentar um desempate à primeira. Nos registos A=(1,9), B=(2,1), C=(1,3), a ordenação pedida é C,A,B, enquanto dar precedência a Age produz B,C,A.

A estabilidade da ordenação conserva a ordem relativa de elementos com chaves iguais, mas não cria uma identidade única. Se a origem pode chegar noutra ordem e o relatório exige resultados reprodutíveis, acrescenta uma chave de desempate adequada, como um ID único.

Para IDs textuais de protocolo, especifica o comparador previsto, por exemplo StringComparer.Ordinal. A ordenação LINQ produz uma sequência ordenada; não é uma chamada que reorganiza a List original no local.

Diagnosticar a fase onde ocorre a falha

Em new[] { 2, 0 }.Select(x => 10 / x), criar a consulta não executa as divisões. ToList consome a projeção, encontra zero e a chamada não devolve normalmente uma lista parcial. Um try que terminou depois da definição da consulta não abrange a falha posterior.

Identifica no diagnóstico a origem, o seletor e o ponto de consumo. Isto não significa que todos os erros sejam diferidos: argumentos inválidos podem ser rejeitados ao chamar o operador. Mantém os predicados e seletores sem efeitos externos sempre que possível.

Duas enumerações podem voltar a avaliar a consulta; contagens de chamadas inferidas de um operador terminal também podem depender de otimizações. Uma transformação de relatório não deve cobrar um pagamento nem atualizar um estado de negócio como efeito escondido de Select. Se há efeitos, torna a sua execução e recuperação explícitas.

Leitura guiada: uma captura para dois consumidores

O exemplo recebe uma lista local finita e não nula, sem elementos nulos e com IDs ASCII únicos e não nulos, sem alterações durante a chamada. Capture aceita um limite decimal, filtra Accepted e Amount, projeta cada correspondência para um novo ReportRow, ordena por Amount decrescente e desempata por ID ordinal.

ToList termina a recolha antes de devolver o resultado. Para B/12/true, A/12/true e C/5/false, com limite 10, a sequência é A/12, B/12. Se o chamador alterar depois Amount de A na lista de origem, os valores do relatório mantêm-se porque o decimal já foi copiado.

A lista devolvida continua mutável e deve ser partilhada pelos dois consumidores sem alterações; a função não impõe essa disciplina. Analisa também os limites: duplicação de IDs, origem concorrente ou volume sem limite exigem contratos adicionais.

As perguntas desta aula pedem interpretar decisões e excertos, sem executar código.

using System;
using System.Collections.Generic;
using System.Linq;

public sealed class Reading
{
    public required string Id { get; init; }
    public decimal Amount { get; set; }
    public bool Accepted { get; set; }
}

public sealed record ReportRow(string Id, decimal Amount);

public static class Reporting
{
    public static List<ReportRow> Capture(List<Reading> source, decimal minimum)
    {
        return source
            .Where(r => r.Accepted && r.Amount >= minimum)
            .Select(r => new ReportRow(r.Id, r.Amount))
            .OrderByDescending(r => r.Amount)
            .ThenBy(r => r.Id, StringComparer.Ordinal)
            .ToList();
    }
}
NA PRÁTICA

Com IDs únicos e sem alterações durante Capture, B/12/true, A/12/true e C/5/false, com limite 10, produzem A/12 e B/12. Ler o excerto basta para responder.

Armadilhas comuns

Confundir consulta com resultados; tomar ToList por cópia profunda; capturar um limite alterável; ocultar duplicados; substituir ThenBy por OrderBy; tratar exceções apenas na definição.

Tópicos relacionados: Coleções, generics e igualdade · Objetos, interfaces e records · Exceções, recursos e ficheiros

Leva esta ideia contigo

Explicita o momento da leitura, os valores conservados e a cardinalidade aceite antes de escolher a cadeia LINQ.

Criar conta

Referência: Introduction to LINQ queries · C# 14 / .NET 10; no preview features