Qual é a diferença (ões) entre .ToList (), .AsEnumerable (), AsQueryable ()?

182

Conheço algumas diferenças de LINQ to Entities e LINQ to Objects que o primeiro implementa IQueryablee o segundo implementa IEnumerablee o escopo da minha pergunta está no EF 5.

Minha pergunta é: qual é a diferença técnica desses 3 métodos? Eu vejo que em muitas situações todos eles funcionam. Eu também vejo usando combinações deles como .ToList().AsQueryable().

  1. O que esses métodos significam exatamente?

  2. Existe algum problema de desempenho ou algo que levaria ao uso de um sobre o outro?

  3. Por que alguém usaria, por exemplo, em .ToList().AsQueryable()vez de .AsQueryable()?

Amin Saqi
fonte

Respostas:

354

Há muito a dizer sobre isso. Deixe-me focar AsEnumerablee AsQueryablee menção ToList()ao longo do caminho.

O que esses métodos fazem?

AsEnumerablee AsQueryableconverter ou converter para IEnumerableou IQueryable, respectivamente. Digo elenco ou converter com uma razão:

  • Quando o objeto de origem já implementa a interface de destino, o próprio objeto de origem é retornado, mas convertido na interface de destino. Em outras palavras: o tipo não é alterado, mas o tipo em tempo de compilação é.

  • Quando o objeto de origem não implementa a interface de destino, o objeto de origem é convertido em um objeto que implementa a interface de destino. Portanto, o tipo e o tipo de tempo de compilação são alterados.

Deixe-me mostrar isso com alguns exemplos. Eu tenho esse pequeno método que relata o tipo de tempo de compilação e o tipo real de um objeto ( cortesia de Jon Skeet ):

void ReportTypeProperties<T>(T obj)
{
    Console.WriteLine("Compile-time type: {0}", typeof(T).Name);
    Console.WriteLine("Actual type: {0}", obj.GetType().Name);
}

Vamos tentar um linq-to-sql arbitrário Table<T>, que implementa IQueryable:

ReportTypeProperties(context.Observations);
ReportTypeProperties(context.Observations.AsEnumerable());
ReportTypeProperties(context.Observations.AsQueryable());

O resultado:

Compile-time type: Table`1
Actual type: Table`1

Compile-time type: IEnumerable`1
Actual type: Table`1

Compile-time type: IQueryable`1
Actual type: Table`1

Você vê que a própria classe da tabela sempre é retornada, mas sua representação é alterada.

Agora, um objeto que implementa IEnumerable, não IQueryable:

var ints = new[] { 1, 2 };
ReportTypeProperties(ints);
ReportTypeProperties(ints.AsEnumerable());
ReportTypeProperties(ints.AsQueryable());

Os resultados:

Compile-time type: Int32[]
Actual type: Int32[]

Compile-time type: IEnumerable`1
Actual type: Int32[]

Compile-time type: IQueryable`1
Actual type: EnumerableQuery`1

Aí está. AsQueryable()converteu a matriz em um EnumerableQuery, que "representa uma IEnumerable<T>coleção como uma IQueryable<T>fonte de dados". (MSDN).

Qual o uso?

AsEnumerableé freqüentemente usado para alternar de qualquer IQueryableimplementação para LINQ para objetos (L2O), principalmente porque o primeiro não suporta funções que o L2O possui. Para obter mais detalhes, consulte Qual é o efeito de AsEnumerable () em uma entidade LINQ? .

Por exemplo, em uma consulta do Entity Framework, podemos usar apenas um número restrito de métodos. Portanto, se, por exemplo, precisarmos usar um de nossos próprios métodos em uma consulta, normalmente escreveremos algo como

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => MySuperSmartMethod(x))

ToList - que converte um IEnumerable<T>em List<T>- é também frequentemente usado para esse fim. A vantagem de usar AsEnumerablevs. ToListé que AsEnumerablenão executa a consulta. AsEnumerablepreserva a execução adiada e não cria uma lista intermediária geralmente inútil.

Por outro lado, quando a execução forçada de uma consulta LINQ é desejada, ToListpode ser uma maneira de fazer isso.

AsQueryablepode ser usado para fazer uma coleção enumerável aceitar expressões em instruções LINQ. Veja aqui para mais detalhes: Eu realmente preciso usar AsQueryable () na coleção? .

Nota sobre abuso de substâncias!

AsEnumerablefunciona como uma droga. É uma solução rápida, mas com um custo e não soluciona o problema subjacente.

Em muitas respostas de estouro de pilha, vejo pessoas se candidatando AsEnumerablepara corrigir praticamente qualquer problema com métodos não suportados nas expressões LINQ. Mas o preço nem sempre é claro. Por exemplo, se você fizer isso:

context.MyLongWideTable // A table with many records and columns
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate })

... tudo está perfeitamente traduzido em uma instrução SQL que filtra ( Where) e projeta ( Select). Ou seja, o comprimento e a largura, respectivamente, do conjunto de resultados SQL são reduzidos.

Agora, suponha que os usuários desejem apenas ver a parte da data CreateDate. No Entity Framework, você descobrirá rapidamente que ...

.Select(x => new { x.Name, x.CreateDate.Date })

... não é suportado (no momento da escrita). Felizmente, há a AsEnumerablesolução:

context.MyLongWideTable.AsEnumerable()
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate.Date })

Claro, corre, provavelmente. Mas ele puxa a tabela inteira para a memória e depois aplica o filtro e as projeções. Bem, a maioria das pessoas é inteligente o suficiente para fazer o Whereprimeiro:

context.MyLongWideTable
       .Where(x => x.Type == "type").AsEnumerable()
       .Select(x => new { x.Name, x.CreateDate.Date })

Mas ainda todas as colunas são buscadas primeiro e a projeção é feita na memória.

A correção real é:

context.MyLongWideTable
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, DbFunctions.TruncateTime(x.CreateDate) })

(Mas isso requer um pouco mais de conhecimento ...)

O que esses métodos NÃO fazem?

Restaurar recursos IQueryable

Agora, uma ressalva importante. Quando você faz

context.Observations.AsEnumerable()
                    .AsQueryable()

você terminará com o objeto de origem representado como IQueryable. (Porque os dois métodos apenas convertem e não convertem).

Mas quando você faz

context.Observations.AsEnumerable().Select(x => x)
                    .AsQueryable()

qual será o resultado?

O Selectproduz um WhereSelectEnumerableIterator. Esta é uma classe .Net interna que implementa IEnumerable, nãoIQueryable . Portanto, ocorreu uma conversão para outro tipo e o subsequente AsQueryablenão pode mais retornar a fonte original.

A implicação disso é que o uso nãoAsQueryable é uma maneira de injetar magicamente um provedor de consulta com seus recursos específicos em um enumerável. Suponha que você faça

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => x.ToString())
                   .AsQueryable()
                   .Where(...)

A condição where nunca será convertida em SQL. AsEnumerable()seguido por instruções LINQ corta definitivamente a conexão com o provedor de consulta de estrutura de entidade.

Eu deliberadamente mostro este exemplo, porque já vi perguntas aqui onde pessoas, por exemplo, tentam 'injetar' Includerecursos em uma coleção chamando AsQueryable. Ele compila e executa, mas não faz nada porque o objeto subjacente não possui mais uma Includeimplementação.

Executar

Ambos AsQueryablee AsEnumerablenão executam (ou enumeram ) o objeto de origem. Eles apenas alteram seu tipo ou representação. Ambas as interfaces envolvidas IQueryablee IEnumerablesão nada mais que "uma enumeração esperando para acontecer". Eles não são executados antes de serem forçados a fazê-lo, por exemplo, como mencionado acima, chamando ToList().

Isso significa que executar um IEnumerableobtido chamando AsEnumerableum IQueryableobjeto executará o subjacente IQueryable. Uma execução subsequente do IEnumerableexecutará novamente o IQueryable. O que pode ser muito caro.

Implementações específicas

Até agora, isso era apenas sobre os métodos de extensão Queryable.AsQueryablee Enumerable.AsEnumerable. Mas é claro que qualquer um pode escrever métodos de instância ou métodos de extensão com os mesmos nomes (e funções).

De fato, é um exemplo comum de um AsEnumerablemétodo de extensão específico DataTableExtensions.AsEnumerable. DataTablenão implementa IQueryableou IEnumerable, portanto, os métodos de extensão regulares não se aplicam.

Gert Arnold
fonte
Obrigado pela resposta, você pode compartilhar sua resposta com a terceira pergunta do OP- 3. Por que alguém usaria, por exemplo, .ToList (). AsQueryable () em vez de .AsQueryable ()? ?
kgzdev
@ikram Não consigo pensar em nada em que isso seria útil. Como expliquei, a inscrição AsQueryable()geralmente se baseia em conceitos errôneos. Mas vou deixar ferver na parte de trás da minha cabeça por um tempo e ver se posso adicionar mais cobertura sobre essa questão.
Gert Arnold
1
Ótima resposta. Você poderia precisar o que acontece se o IEnumerable obtido chamando AsEnumerable () em um IQueryable for enumerado várias vezes? A consulta será executada várias vezes ou os dados já carregados do banco de dados na memória serão reutilizados?
antoninod 9/04
@antoninod Good idea. Feito.
Gert Arnold
46

Listar()

  • Execute a consulta imediatamente

AsEnumerable ()

  • preguiçoso (execute a consulta posteriormente)
  • Parâmetro: Func<TSource, bool>
  • Carregue TODOS os registros na memória do aplicativo e manipule / filtre-os. (por exemplo, Where / Take / Skip, ele selecionará * da tabela1, na memória e depois selecionará os primeiros elementos X) (nesse caso, o que ele fez: Linq-to-SQL + Linq-to-Object)

AsQueryable ()

  • preguiçoso (execute a consulta posteriormente)
  • Parâmetro: Expression<Func<TSource, bool>>
  • Converta Expressão em T-SQL (com o provedor específico), consulte remotamente e carregue o resultado na memória do aplicativo.
  • É por isso que o DbSet (no Entity Framework) também herda IQueryable para obter a consulta eficiente.
  • Não carregue todos os registros, por exemplo, se o Take (5), ele gerará o top 5 * SQL selecionado em segundo plano. Isso significa que esse tipo é mais amigável ao banco de dados SQL e é por isso que esse tipo geralmente tem desempenho superior e é recomendado ao lidar com um banco de dados.
  • Por isso, AsQueryable()geralmente funciona muito mais rápido do que AsEnumerable()quando gera o T-SQL no início, o que inclui todas as suas condições where em seu Linq.
Xin
fonte
14

ToList () será tudo na memória e você estará trabalhando nisso. portanto, ToList (). where (aplicar algum filtro) é executado localmente. O AsQueryable () executará tudo remotamente, ou seja, um filtro será enviado ao banco de dados para aplicação. Queryable não faz nada até você executá-lo. ToList, no entanto, é executado imediatamente.

Além disso, veja esta resposta Por que usar AsQueryable () em vez de List ()? .

EDIT: Além disso, no seu caso, depois de fazer ToList (), todas as operações subseqüentes são locais, incluindo AsQueryable (). Você não pode mudar para remoto depois de começar a executar localmente. Espero que isso torne um pouco mais claro.

ashutosh raina
fonte
2
"AsQueryable () executará tudo remotamente" Somente se o enumerável já for consultável. Caso contrário, isso não é possível e tudo ainda será executado localmente. A pergunta tem ".... ToList (). AsQueryable ()", que poderia usar algum esclarecimento na sua resposta, IMO.
2

Foi encontrado um desempenho ruim no código abaixo.

void DoSomething<T>(IEnumerable<T> objects){
    var single = objects.First(); //load everything into memory before .First()
    ...
}

Corrigido com

void DoSomething<T>(IEnumerable<T> objects){
    T single;
    if (objects is IQueryable<T>)
        single = objects.AsQueryable().First(); // SELECT TOP (1) ... is used
    else
        single = objects.First();

}

Para um IQueryable, permaneça no IQueryable quando possível, tente não ser usado como IEnumerable.

Update . Pode ser ainda mais simplificado em uma expressão, graças a Gert Arnold .

T single =  objects is IQueryable<T> q? 
                    q.First(): 
                    objects.First();
Rm558
fonte