Quelle (s) différence (s) entre .ToList (), .AsEnumerable (), AsQueryable ()?

182

Je connais certaines différences de LINQ to Entities et LINQ to Objects que le premier implémente IQueryableet le second implémente IEnumerableet la portée de ma question est dans EF 5.

Ma question est quelle (s) différence (s) technique (s) de ces 3 méthodes? Je vois que dans de nombreuses situations, tous fonctionnent. Je vois aussi utiliser des combinaisons d'entre eux comme .ToList().AsQueryable().

  1. Que signifient exactement ces méthodes?

  2. Y a-t-il un problème de performance ou quelque chose qui conduirait à l'utilisation de l'un sur l'autre?

  3. Pourquoi utiliserait-on, par exemple, .ToList().AsQueryable()au lieu de .AsQueryable()?

Amin Saqi
la source

Réponses:

354

Il y a beaucoup à dire à ce sujet. Permettez - moi de mettre l' accent sur AsEnumerableet AsQueryableet mentionner le ToList()long du chemin.

Que font ces méthodes?

AsEnumerableet AsQueryablecast ou convertir en IEnumerableou IQueryable, respectivement. Je dis lancer ou convertir avec une raison:

  • Lorsque l'objet source implémente déjà l'interface cible, l'objet source lui-même est renvoyé mais converti en interface cible. En d'autres termes: le type n'est pas modifié, mais le type à la compilation l'est.

  • Lorsque l'objet source n'implémente pas l'interface cible, l'objet source est converti en un objet qui implémente l'interface cible. Ainsi, le type et le type au moment de la compilation sont modifiés.

Laissez-moi vous montrer cela avec quelques exemples. J'ai cette petite méthode qui rapporte le type de compilation et le type réel d'un objet ( avec la permission 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);
}

Essayons un linq-to-sql arbitraire Table<T>, qui implémente IQueryable:

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

Le résultat:

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

Vous voyez que la classe de table elle-même est toujours renvoyée, mais sa représentation change.

Maintenant un objet qui implémente IEnumerable, pas IQueryable:

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

Les resultats:

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

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

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

Le voilà. AsQueryable()a converti le tableau en un EnumerableQuery, qui "représente unIEnumerable<T> collection en tant que IQueryable<T>source de données». (MSDN).

Quel en est l'usage?

AsEnumerableest fréquemment utilisé pour passer de toute IQueryableimplémentation à LINQ vers des objets (L2O), principalement parce que le premier ne prend pas en charge les fonctions de L2O. Pour plus de détails, voir Quel est l'effet de AsEnumerable () sur une entité LINQ? .

Par exemple, dans une requête Entity Framework, nous ne pouvons utiliser qu'un nombre limité de méthodes. Donc, si, par exemple, nous devons utiliser l'une de nos propres méthodes dans une requête, nous écrirons généralement quelque chose comme

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

ToList - qui convertit un IEnumerable<T>en a List<T>- est également souvent utilisé à cette fin. L'avantage d'utiliser AsEnumerablevs. ToListest qu'il AsEnumerablen'exécute pas la requête.AsEnumerablepréserve l'exécution différée et ne construit pas une liste intermédiaire souvent inutile.

D'autre part, lorsque l'exécution forcée d'une requête LINQ est souhaitée, ToList peut être un moyen de le faire.

AsQueryablepeut être utilisé pour qu'une collection énumérable accepte des expressions dans des instructions LINQ. Voir ici pour plus de détails: Ai-je vraiment besoin d'utiliser AsQueryable () sur la collecte? .

Remarque sur la toxicomanie!

AsEnumerablefonctionne comme une drogue. C'est une solution rapide, mais à un coût et ne résout pas le problème sous-jacent.

Dans de nombreuses réponses Stack Overflow, je vois des personnes postuler AsEnumerablepour résoudre à peu près n'importe quel problème avec des méthodes non prises en charge dans les expressions LINQ. Mais le prix n'est pas toujours clair. Par exemple, si vous faites ceci:

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

... tout est proprement traduit en une instruction SQL qui filtre ( Where) et projette ( Select). Autrement dit, la longueur et la largeur, respectivement, du jeu de résultats SQL sont réduites.

Supposons maintenant que les utilisateurs ne souhaitent voir que la partie date de CreateDate. Dans Entity Framework, vous découvrirez rapidement que ...

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

... n'est pas pris en charge (au moment de la rédaction). Ah, heureusement, il y a le AsEnumerablecorrectif:

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

Bien sûr, il fonctionne, probablement. Mais il tire la table entière en mémoire et applique ensuite le filtre et les projections. Eh bien, la plupart des gens sont assez intelligents pour faire le Wherepremier:

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

Cependant, toutes les colonnes sont récupérées en premier et la projection est effectuée en mémoire.

La vraie solution est:

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

(Mais cela nécessite juste un peu plus de connaissances ...)

Que ne font PAS ces méthodes?

Restaurer les capacités IQueryable

Maintenant une mise en garde importante. Quand tu fais

context.Observations.AsEnumerable()
                    .AsQueryable()

vous vous retrouverez avec l'objet source représenté par IQueryable. (Parce que les deux méthodes ne font que lancer et ne pas convertir)

Mais quand tu fais

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

quel sera le résultat?

Le Selectproduit un WhereSelectEnumerableIterator. Il s'agit d'une classe .Net interne qui implémente IEnumerable, nonIQueryable . Ainsi, une conversion vers un autre type a eu lieu et la suiteAsQueryable ne peut plus jamais retourner la source d'origine.

L'implication de cela est que l'utilisation AsQueryablen'est pas un moyen d'injecter par magie un fournisseur de requêtes avec ses fonctionnalités spécifiques dans un énumérable. Supposez que vous faites

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

La condition where ne sera jamais traduite en SQL. AsEnumerable()suivi d'instructions LINQ coupe définitivement la connexion avec le fournisseur de requêtes de structure d'entité.

Je montre délibérément cet exemple parce que j'ai vu des questions ici où les gens, par exemple, essaient d'injecter des Includecapacités dans une collection en appelant AsQueryable. Il se compile et s'exécute, mais il ne fait rien car l'objet sous-jacent n'a pas deInclude implémentation.

Exécuter

Les deux AsQueryableet AsEnumerablen'exécutent pas (ou n'énumèrent ) pas l'objet source. Ils changent uniquement de type ou de représentation. Les deux interfaces impliquées, IQueryableet IEnumerable, ne sont rien d'autre qu'une "énumération en attente de se produire". Ils ne sont pas exécutés avant d'être obligés de le faire, par exemple, comme mentionné ci-dessus, en appelant ToList().

Cela signifie que l'exécution d'un IEnumerableobtenu en appelant AsEnumerableun IQueryableobjet exécutera le sous-jacent IQueryable. Une exécution ultérieure du IEnumerableexécutera à nouveau le IQueryable. Ce qui peut être très coûteux.

Implémentations spécifiques

Jusqu'à présent, il ne s'agissait que des méthodes d'extension Queryable.AsQueryableet Enumerable.AsEnumerable. Mais bien sûr, n'importe qui peut écrire des méthodes d'instance ou des méthodes d'extension avec les mêmes noms (et fonctions).

En fait, un exemple courant de AsEnumerableméthode d'extension spécifique est DataTableExtensions.AsEnumerable. DataTablen'implémente pas IQueryableou IEnumerable, donc les méthodes d'extension habituelles ne s'appliquent pas.

Gert Arnold
la source
Merci pour la réponse, pouvez-vous s'il vous plaît partager votre réponse à la troisième question de l'OP - 3. Pourquoi utiliser, par exemple, .ToList (). AsQueryable () au lieu de .AsQueryable ()? ?
kgzdev
@ikram Je ne vois rien où cela serait utile. Comme je l'ai expliqué, l'application AsQueryable()est souvent basée sur une idée fausse. Mais je vais laisser mijoter dans le dos de ma tête pendant un moment et voir si je peux ajouter un peu plus de couverture sur cette question.
Gert Arnold
1
Très bonne réponse. Pourriez-vous préciser ce qui se passe si le IEnumerable obtenu en appelant AsEnumerable () sur un IQueryable est énuméré plusieurs fois? La requête sera-t-elle exécutée plusieurs fois ou les données déjà chargées de la base de données en mémoire seront-elles réutilisées?
antoninod le
@antoninod Bonne idée. Terminé.
Gert Arnold le
46

Lister()

  • Exécutez la requête immédiatement

AsEnumerable ()

  • paresseux (exécutez la requête plus tard)
  • Paramètre: Func<TSource, bool>
  • Chargez CHAQUE enregistrement dans la mémoire de l'application, puis traitez / filtrez-les. (par exemple Where / Take / Skip, il sélectionnera * de table1, dans la mémoire, puis sélectionnera les X premiers éléments) (Dans ce cas, ce qu'il a fait: Linq-to-SQL + Linq-to-Object)

AsQueryable ()

  • paresseux (exécutez la requête plus tard)
  • Paramètre: Expression<Func<TSource, bool>>
  • Convertissez Expression en T-SQL (avec le fournisseur spécifique), interrogez à distance et chargez le résultat dans la mémoire de votre application.
  • C'est pourquoi DbSet (dans Entity Framework) hérite également d'IQueryable pour obtenir la requête efficace.
  • Ne chargez pas tous les enregistrements, par exemple, si Take (5), il générera le top 5 * SQL sélectionné en arrière-plan. Cela signifie que ce type est plus convivial pour SQL Database, et c'est pourquoi ce type a généralement des performances plus élevées et est recommandé lorsqu'il s'agit d'une base de données.
  • Cela AsQueryable()fonctionne donc généralement beaucoup plus rapidement que AsEnumerable()lorsqu'il génère T-SQL au début, qui inclut toutes vos conditions where dans votre Linq.
Xin
la source
14

ToList () sera tout en mémoire et vous travaillerez dessus. donc, ToList (). où (appliquer un filtre) est exécuté localement. AsQueryable () exécutera tout à distance c'est-à-dire qu'un filtre sur celui-ci est envoyé à la base de données pour application. Queryable ne fait rien tant que vous ne l'exécutez pas. ToList, cependant, s'exécute immédiatement.

Regardez également cette réponse Pourquoi utiliser AsQueryable () au lieu de List ()? .

EDIT: De plus, dans votre cas, une fois que vous faites ToList (), toutes les opérations suivantes sont locales, y compris AsQueryable (). Vous ne pouvez pas passer à distant une fois que vous avez commencé à exécuter localement. J'espère que cela rend les choses un peu plus claires.

ashutosh raina
la source
2
"AsQueryable () exécutera tout à distance" Seulement si l'énumérable est déjà interrogeable. Sinon, ce n'est pas possible et tout fonctionne toujours localement. La question a ".... ToList (). AsQueryable ()", ce qui pourrait nécessiter des éclaircissements dans votre réponse, IMO.
2

Rencontré une mauvaise performance sur le code ci-dessous.

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

Fixé avec

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

}

Pour un IQueryable, restez dans IQueryable lorsque cela est possible, essayez de ne pas être utilisé comme IEnumerable.

Mettre à jour . Il peut être encore simplifié en une seule expression, merci Gert Arnold .

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