Il s'agit d'un piège connu pour les personnes qui se mouillent les pieds avec 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;
}
}
}
Cela affichera "False", car pour chaque nom fourni pour créer la collection d'origine, la fonction de sélection continue d'être réévaluée et l' Record
objet résultant est à nouveau créé. Pour résoudre ce problème, un simple appel à ToList
pourrait être ajouté à la fin de GenerateRecords
.
Quel avantage Microsoft espérait-il gagner en l'implémentant de cette façon?
Pourquoi l'implémentation ne mettrait-elle pas simplement en cache les résultats d'un tableau interne? Une partie spécifique de ce qui se passe peut être l'exécution différée, mais cela pourrait toujours être implémenté sans ce comportement.
Une fois qu'un membre donné d'une collection retourné par LINQ a été évalué, quel avantage offre de ne pas conserver une référence / copie interne, mais de recalculer le même résultat, comme comportement par défaut?
Dans les situations où il existe un besoin particulier dans la logique pour le même membre d'une collection recalculé encore et encore, il semble que cela pourrait être spécifié via un paramètre facultatif et que le comportement par défaut pourrait faire autrement. De plus, l'avantage de vitesse qui est gagné par l'exécution différée est finalement réduit par le temps qu'il faut pour recalculer continuellement les mêmes résultats. Enfin, c'est un bloc déroutant pour ceux qui sont nouveaux dans LINQ, et cela pourrait conduire à des bugs subtils dans le programme de n'importe qui.
Quel avantage y a-t-il et pourquoi Microsoft a-t-il pris cette décision apparemment très délibérée?
la source
return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList();
Cela vous donne votre "copie en cache". Problème résolu.Réponses:
La mise en cache des résultats ne fonctionnerait tout simplement pas pour tout le monde. Tant que vous avez de petites quantités de données, tant mieux. Bien pour vous. Mais que se passe-t-il si vos données sont plus grandes que votre RAM?
Cela n'a rien à voir avec LINQ, mais avec l'
IEnumerable<T>
interface en général.C'est la différence entre File.ReadAllLines et File.ReadLines . L'un lira le fichier entier dans la RAM et l'autre vous le donnera ligne par ligne, afin que vous puissiez travailler avec des fichiers volumineux (tant qu'ils ont des sauts de ligne).
Vous pouvez facilement tout cache que vous voulez cache en matérialisant votre séquence d' appel soit
.ToList()
ou.ToArray()
sur elle. Mais ceux d' entre nous qui ne pas veulent le mettre en cache, nous avons une chance de ne pas le faire.Et sur une note connexe: comment mettez-vous en cache les éléments suivants?
Vous ne pouvez pas. C'est pourquoi
IEnumerable<T>
existe comme ça.la source
int i=1; while(true) { i++; yield fib(i); }
Enumerable.Range(1,int.MaxValue)
auquel je pensais était : il est très facile de déterminer une limite inférieure pour la quantité de mémoire qui va être utilisée.while (true) return ...
étaitwhile (true) return _random.Next();
de générer un flux infini de nombres aléatoires.Exactitude? Je veux dire, le noyau énumérable peut changer entre les appels. La mise en cache produirait des résultats incorrects et ouvrirait l'ensemble «quand / comment puis-je invalider ce cache?» Boîte de vers.
Et si l' on considère LINQ a été conçu à l' origine comme un moyen de faire LINQ aux sources de données (comme Entity Framework, SQL ou directement), le dénombrable a été va changer puisque c'est ce que les bases de données font .
En plus de cela, il y a des préoccupations concernant le principe de responsabilité unique. Il est beaucoup plus facile de créer du code de requête qui fonctionne et de créer une mise en cache par-dessus que de créer du code qui interroge et met en cache, puis de supprimer la mise en cache.
la source
ICollection
existe, et se comporte probablement de la façon dont OP s'attendIEnumerable
à se comporterParce que LINQ est, et était destiné depuis le début à être, une implémentation générique du modèle Monad populaire dans les langages de programmation fonctionnels , et un Monad n'est pas contraint de toujours donner les mêmes valeurs étant donné la même séquence d'appels (en fait, son utilisation en programmation fonctionnelle est populaire précisément à cause de cette propriété, qui permet d'échapper au comportement déterministe des fonctions pures).
la source
Une autre raison qui n'a pas été mentionnée est la possibilité de concaténer différents filtres et transformations sans créer de résultats intermédiaires.
Prenez ceci par exemple:
Si les méthodes LINQ calculaient les résultats immédiatement, nous aurions 3 collections:
Dont nous ne nous soucions que du dernier. Il ne sert à rien de sauvegarder les résultats intermédiaires parce que nous n'y avons pas accès, et nous voulons seulement connaître les voitures déjà filtrées et regroupées par année.
S'il était nécessaire de sauvegarder l'un de ces résultats, la solution est simple: séparez les appels et appelez-
.ToList()
les et enregistrez-les dans une variable.Tout comme une remarque, en JavaScript, les méthodes Array retournent en fait les résultats immédiatement, ce qui peut entraîner une consommation de mémoire plus importante si l'on ne fait pas attention.
la source
Fondamentalement, ce code - mettant
Guid.NewGuid ()
uneSelect
déclaration à l' intérieur - est très suspect. C'est sûrement une sorte d'odeur de code!En théorie, nous ne nous attendrions pas nécessairement à ce qu'une
Select
instruction crée de nouvelles données mais récupère des données existantes. Bien qu'il soit raisonnable que Select joigne les données de plusieurs sources pour produire un contenu joint de forme différente ou même calculer des colonnes supplémentaires, nous pouvons toujours nous attendre à ce qu'il soit fonctionnel et pur. Mettre l'NewGuid ()
intérieur le rend non fonctionnel et non pur.La création des données pourrait être taquinée en dehors de la sélection et placée dans une opération de création d'une sorte, de sorte que la sélection puisse rester pure et réutilisable, ou bien la sélection ne devrait être effectuée qu'une seule fois et enveloppée / protégée - cela est la
.ToList ()
suggestion.Cependant, pour être clair, le problème me semble être le mélange de la création à l'intérieur de la sélection plutôt que le manque de mise en cache. Mettre l'
NewGuid()
intérieur à l' intérieur de la sélection me semble être un mélange inapproprié de modèles de programmation.la source
L'exécution différée permet à ceux qui écrivent du code LINQ (pour être précis, en utilisant
IEnumerable<T>
) de choisir explicitement si le résultat est immédiatement calculé et stocké en mémoire, ou non. En d'autres termes, il permet aux programmeurs de choisir le temps de calcul ou le compromis d'espace de stockage le plus approprié à leur application.On pourrait faire valoir que la majorité des applications souhaitent les résultats immédiatement, ce qui aurait dû être le comportement par défaut de LINQ. Mais il existe de nombreuses autres API (par exemple
List<T>.ConvertAll
) qui offrent ce comportement et l'ont fait depuis la création du Framework, alors que jusqu'à l'introduction de LINQ, il n'y avait aucun moyen d'avoir une exécution différée. Ce qui, comme d'autres réponses l'ont démontré, est une condition préalable à l'activation de certains types de calculs qui seraient autrement impossibles (en épuisant tout le stockage disponible) lors de l'exécution immédiate.la source