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 Record
objeto resultante é criado novamente. Para corrigir isso, uma simples chamada para ToList
poderia 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?
fonte
return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList();
Isso fornece sua "cópia em cache". Problema resolvido.Respostas:
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?
Você não pode. É por isso que
IEnumerable<T>
existe como existe.fonte
int i=1; while(true) { i++; yield fib(i); }
Enumerable.Range(1,int.MaxValue)
- é muito fácil calcular um limite inferior para a quantidade de memória que será usada.while (true) return ...
foiwhile (true) return _random.Next();
gerar um fluxo infinito de números aleatórios.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.
fonte
ICollection
existe, e provavelmente se comporta da maneira OP está esperandoIEnumerable
para se comportarComo 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).
fonte
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:
Se os métodos LINQ calculassem os resultados imediatamente, teríamos três coleções:
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.
fonte
Fundamentalmente, esse código - colocando
Guid.NewGuid ()
umaSelect
declaração interna - é altamente suspeito. Certamente é algum tipo de cheiro de código!Em teoria, não esperaríamos necessariamente que uma
Select
declaraçã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 oNewGuid ()
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.fonte
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.fonte