Que vantagem foi obtida com a implementação do LINQ de uma maneira que não armazena em cache os resultados?


20

Essa é uma armadilha conhecida para as pessoas que estão molhando os pés usando o LINQ:

public class Program
{
    public static void Main()
    {
        IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
        var newCollection = new List<Record>(originalCollection);

        Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
    }

    private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
    {
        return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
    }

    private static bool ContainTheSameSingleObject(IEnumerable<Record>
            originalCollection, List<Record> newCollection)
    {
        return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
                originalCollection.Single().Id == newCollection.Single().Id;
    }

    private class Record
    {
        public Guid Id { get; }
        public string SomeValue { get; }

        public Record(Guid id, string someValue)
        {
            Id = id;
            SomeValue = someValue;
        }
    }
}

Isso imprimirá "False", porque para cada nome fornecido para criar a coleção original, a função de seleção continua sendo reavaliada e o Recordobjeto resultante é criado novamente. Para corrigir isso, uma simples chamada para ToListpoderia ser adicionada no final de GenerateRecords.

Que vantagem a Microsoft esperava obter ao implementá-la dessa maneira?

Por que a implementação não armazenaria em cache os resultados em uma matriz interna? Uma parte específica do que está acontecendo pode ser a execução adiada, mas isso ainda pode ser implementado sem esse comportamento.

Depois que um determinado membro de uma coleção retornada pelo LINQ é avaliado, qual é a vantagem de não manter uma referência / cópia interna, mas recalcular o mesmo resultado, como um comportamento padrão?

Nas situações em que há uma necessidade específica na lógica de o mesmo membro de uma coleção ser recalculado repetidamente, parece que isso pode ser especificado por meio de um parâmetro opcional e que o comportamento padrão pode fazer o contrário. Além disso, a vantagem de velocidade obtida com a execução adiada é reduzida no tempo necessário para recalcular continuamente os mesmos resultados. Finalmente, este é um bloco confuso para aqueles que são novos no LINQ e pode levar a erros sutis no programa de qualquer um.

Que vantagem há para isso e por que a Microsoft tomou essa decisão aparentemente muito deliberada?


1
Basta chamar ToList () no seu método GenerateRecords (). return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); Isso fornece sua "cópia em cache". Problema resolvido.
Robert Harvey

1
Eu sei, mas estava me perguntando por que eles teriam feito isso necessário em primeiro lugar.
Panzercrisis 23/03

11
Como a avaliação preguiçosa tem benefícios significativos, entre os quais "ah, a propósito, esse registro mudou desde a última vez que você solicitou; aqui está a nova versão", que é exatamente o que o seu exemplo de código ilustra.
Robert Harvey

Eu poderia jurar que li uma pergunta quase idêntica aqui nos últimos 6 meses, mas não estou encontrando agora. O mais próximo que eu consegui encontrar foi de 2016 no stackoverflow: stackoverflow.com/q/37437893/391656 #
31718 Mr.Mindor

29
Temos um nome para um cache sem uma política de expiração: "vazamento de memória". Temos um nome para um cache sem uma política de invalidação: "bug farm". Se você não vai propor uma política de expiração e invalidação sempre correta, que funcione para todas as consultas LINQ possíveis , sua pergunta meio que se responde.
Eric Lippert

Respostas:


51

Que vantagem foi obtida com a implementação do LINQ de uma maneira que não armazena em cache os resultados?

Armazenar em cache os resultados simplesmente não funcionaria para todos. Contanto que você tenha pequenas quantidades de dados, ótimo. Bom para você. Mas e se seus dados forem maiores que sua RAM?

Não tem nada a ver com LINQ, mas com a IEnumerable<T>interface em geral.

É a diferença entre File.ReadAllLines e File.ReadLines . Um lerá o arquivo inteiro na RAM e o outro o fornecerá linha por linha, para que você possa trabalhar com arquivos grandes (desde que tenham quebras de linha).

Você pode armazenar em cache facilmente tudo o que deseja armazenar em cache, materializando sua chamada de sequência .ToList()ou .ToArray()nela. Mas aqueles de nós que não querem armazená-lo em cache, temos a chance de não fazê-lo.

E em uma nota relacionada: como você armazena em cache o seguinte?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

Você não pode. É por isso que IEnumerable<T>existe como existe.


2
Seu último exemplo seria mais atraente se fosse uma série infinita real (como Fibonnaci), e não apenas uma sequência interminável de zeros, o que não é particularmente interessante.
Robert Harvey

23
@RobertHarvey Isso é verdade, eu apenas achei mais fácil perceber que é um fluxo interminável de zeros quando não há lógica alguma para entender.
Nvoigt 23/03/19

2
int i=1; while(true) { i++; yield fib(i); }
Robert Harvey

2
O exemplo em que eu estava pensando era Enumerable.Range(1,int.MaxValue)- é muito fácil calcular um limite inferior para a quantidade de memória que será usada.
23418 Chris

4
A outra coisa que eu vi ao longo das linhas while (true) return ...foi while (true) return _random.Next();gerar um fluxo infinito de números aleatórios.
23418 Chris

24

Que vantagem a Microsoft esperava obter ao implementá-la dessa maneira?

Correção? Quero dizer, o núcleo enumerável pode mudar entre as chamadas. Armazená-lo em cache produziria resultados incorretos e abriria todo "quando / como invalido esse cache?" Lata de worms.

E se você considerar que o LINQ foi originalmente projetado como um meio de fazer LINQ para fontes de dados (como estrutura de entidade ou SQL diretamente), o enumerável era indo para mudança desde que é o que os bancos de dados fazer .

Além disso, há preocupações com o princípio de responsabilidade única. É muito mais fácil criar um código de consulta que funcione e criar cache sobre ele do que criar código que consulta e armazena em cache, mas depois remove o cache.


3
Pode valer a pena mencionar que ICollectionexiste, e provavelmente se comporta da maneira OP está esperando IEnumerablepara se comportar
Caleth

Se você estiver usando IEnumerable <T> para ler um cursor de banco de dados aberto, seus resultados não serão alterados se você estiver usando um banco de dados com transações ACID.
Doug

4

Como o LINQ é, e foi planejado desde o início, uma implementação genérica do padrão Monad popular em linguagens de programação funcional , e um Monad não é restrito a sempre produzir os mesmos valores, dada a mesma sequência de chamadas (na verdade, seu uso na programação funcional é popular justamente por causa dessa propriedade, que permite escapar do comportamento determinístico das funções puras).


4

Outro motivo que não foi mencionado é a possibilidade de concatenar diferentes filtros e transformações sem criar resultados médios de lixo.

Veja isso por exemplo:

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

Se os métodos LINQ calculassem os resultados imediatamente, teríamos três coleções:

  • Onde resultado
  • Selecionar resultado
  • Resultado GroupBy

Dos quais nos preocupamos apenas com o último. Não faz sentido salvar os resultados intermediários porque não temos acesso a eles e queremos apenas saber sobre os carros já filtrados e agrupados por ano.

Se houver necessidade de salvar qualquer um desses resultados, a solução é simples: separar as chamadas e chamá .ToList()-las e salvá-las em uma variável.


Como observação, em JavaScript, os métodos Array retornam os resultados imediatamente, o que pode levar a mais consumo de memória se não for necessário.


3

Fundamentalmente, esse código - colocando Guid.NewGuid ()uma Selectdeclaração interna - é altamente suspeito. Certamente é algum tipo de cheiro de código!

Em teoria, não esperaríamos necessariamente que uma Selectdeclaração criasse novos dados, mas recuperasse dados existentes. Embora seja razoável que o Select junte dados de várias fontes para produzir conteúdo associado de forma diferente ou até mesmo calcular colunas adicionais, ainda podemos esperar que seja funcional e puro. Colocar o NewGuid ()interior torna-o não funcional e não puro.

A criação dos dados pode ser provocada além da seleção e colocada em uma operação de criação de algum tipo, para que a seleção possa permanecer pura e reutilizável, ou então a seleção deve ser feita apenas uma vez e encapsulada / protegida - isso é o .ToList () sugestão.

No entanto, para ficar claro, a questão me parece a mistura da criação dentro da seleção, em vez da falta de armazenamento em cache. Colocar o NewGuid()interior do select me parece uma mistura inadequada de modelos de programação.


0

A execução adiada permite que aqueles que escrevem código LINQ (para ser preciso, usando IEnumerable<T>) escolham explicitamente se o resultado é imediatamente calculado e armazenado na memória ou não. Em outras palavras, permite que os programadores escolham o tempo de cálculo versus a troca de espaço de armazenamento mais apropriada para sua aplicação.

Pode-se argumentar que a maioria dos aplicativos deseja os resultados imediatamente, portanto esse deveria ter sido o comportamento padrão do LINQ. Porém, existem inúmeras outras APIs (por exemplo List<T>.ConvertAll) que oferecem esse comportamento e o fazem desde que o Framework foi criado, enquanto até o LINQ ser introduzido, não havia como adiar a execução. O que, como outras respostas demonstraram, é um pré-requisito para permitir certos tipos de cálculos que seriam impossíveis (esgotando todo o armazenamento disponível) ao usar a execução imediata.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.