Esta es una trampa conocida para las personas que se están mojando los pies con 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;
}
}
}
Esto imprimirá "Falso", porque para cada nombre proporcionado para crear la colección original, la función de selección se vuelve a evaluar y el Record
objeto resultante se crea de nuevo. Para solucionar esto, ToList
se puede agregar una simple llamada a al final de GenerateRecords
.
¿Qué ventaja esperaba obtener Microsoft al implementarlo de esta manera?
¿Por qué la implementación simplemente no almacena en caché los resultados en una matriz interna? Una parte específica de lo que está sucediendo puede ser la ejecución diferida, pero eso podría implementarse sin este comportamiento.
Una vez que se ha evaluado un miembro dado de una colección devuelta por LINQ, ¿qué ventaja se proporciona al no mantener una referencia / copia interna, sino que se vuelve a calcular el mismo resultado, como un comportamiento predeterminado?
En situaciones donde hay una necesidad particular en la lógica para el mismo miembro de una colección recalculada una y otra vez, parece que eso podría especificarse a través de un parámetro opcional y que el comportamiento predeterminado podría hacerlo de otra manera. Además, la ventaja de velocidad que se obtiene con la ejecución diferida se reduce en última instancia por el tiempo que lleva recalcular continuamente los mismos resultados. Finalmente, este es un bloque confuso para aquellos que son nuevos en LINQ, y podría conducir a errores sutiles en el programa de cualquier persona.
¿Qué ventaja tiene esto y por qué Microsoft tomó esta decisión aparentemente muy deliberada?
fuente
return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList();
Eso te da tu "copia en caché". Problema resuelto.Respuestas:
El almacenamiento en caché de los resultados simplemente no funcionaría para todos. Mientras tengas pequeñas cantidades de datos, genial. Bien por usted. Pero, ¿qué pasa si sus datos son más grandes que su RAM?
No tiene nada que ver con LINQ, sino con la
IEnumerable<T>
interfaz en general.Es la diferencia entre File.ReadAllLines y File.ReadLines . Uno leerá el archivo completo en la RAM y el otro se lo dará línea por línea, para que pueda trabajar con archivos grandes (siempre que tengan saltos de línea).
Puede almacenar en caché fácilmente todo lo que desea almacenar en la memoria materializando su secuencia llamando
.ToList()
o.ToArray()
en él. Pero aquellos de nosotros que no queremos almacenarlo en caché, tenemos la oportunidad de no hacerlo.Y en una nota relacionada: ¿cómo almacena en caché lo siguiente?
No puedes. Por eso
IEnumerable<T>
existe como existe.fuente
int i=1; while(true) { i++; yield fib(i); }
Enumerable.Range(1,int.MaxValue)
: es muy fácil calcular un límite inferior para la cantidad de memoria que se va a usar.while (true) return ...
erawhile (true) return _random.Next();
generar una corriente infinita de números aleatorios.¿Exactitud? Quiero decir, el núcleo enumerable puede cambiar entre llamadas. El almacenamiento en caché produciría resultados incorrectos y abriría todo el “cuándo / cómo invalidar ese caché”. Lata de gusanos.
Y si se tiene en cuenta LINQ fue diseñado originalmente como un medio para hacer LINQ a las fuentes de datos (como el marco de la entidad, o SQL directamente), la enumerables se va a cambiar ya que eso es lo que las bases de datos hacen .
Además de eso, hay preocupaciones sobre el Principio de Responsabilidad Única. Es mucho más fácil crear algún código de consulta que funcione y generar caché encima que generar código que consulte y almacene en caché, pero luego elimine el almacenamiento en caché.
fuente
ICollection
existe, y probablemente se comporta de la manera en que OP esperaIEnumerable
comportarseDebido a que LINQ es, y fue pensado desde el principio, una implementación genérica del patrón Monad popular en lenguajes de programación funcionales , y un Monad no está obligado a producir siempre los mismos valores dada la misma secuencia de llamadas (de hecho, su uso en programación funcional es popular precisamente por esta propiedad, que permite escapar del comportamiento determinista de las funciones puras).
fuente
Otra razón que no se ha mencionado es la posibilidad de concatenar diferentes filtros y transformaciones sin crear resultados intermedios basura.
Toma esto por ejemplo:
Si los métodos LINQ calcularan los resultados inmediatamente, tendríamos 3 colecciones:
De los cuales solo nos importa el último. No tiene sentido guardar los resultados intermedios porque no tenemos acceso a ellos, y solo queremos saber acerca de los autos que ya están filtrados y agrupados por año.
Si era necesario guardar alguno de estos resultados, la solución es simple: separe las llamadas y recurra
.ToList()
a ellas y guárdelas en una variable.Como nota al margen, en JavaScript, los métodos de matriz en realidad devuelven los resultados de inmediato, lo que puede conducir a un mayor consumo de memoria si no se tiene cuidado.
fuente
Básicamente, este código, poner
Guid.NewGuid ()
unaSelect
declaración dentro , es muy sospechoso. ¡Este es seguramente un código de olor de algún tipo!En teoría, no necesariamente esperaríamos que una
Select
declaración creara datos nuevos sino que recuperara datos existentes. Si bien es razonable que Select combine datos de múltiples fuentes para producir contenido unido de diferente forma o incluso calcular columnas adicionales, aún podríamos esperar que sea funcional y puro. Poner elNewGuid ()
interior lo hace no funcional y no puro.La creación de los datos se puede separar de la selección y ponerla en una operación de creación de algún tipo, para que la selección pueda permanecer pura y reutilizable, o de lo contrario, la selección debe hacerse solo una vez y envolverse / protegerse, esto Es la
.ToList ()
sugerencia.Sin embargo, para ser claros, el problema me parece la mezcla de la creación dentro de la selección en lugar de la falta de almacenamiento en caché. Poner el
NewGuid()
interior de la selección me parece una mezcla inapropiada de modelos de programación.fuente
La ejecución diferida permite a aquellos que escriben código LINQ (para ser precisos, usar
IEnumerable<T>
) elegir explícitamente si el resultado se calcula de inmediato y se almacena en la memoria, o no. En otras palabras, permite a los programadores elegir el tiempo de cálculo versus el intercambio de espacio de almacenamiento que sea más apropiado para su aplicación.Se podría argumentar que la mayoría de las aplicaciones desean los resultados de inmediato, por lo que debería haber sido el comportamiento predeterminado de LINQ. Pero hay muchas otras API (por ejemplo
List<T>.ConvertAll
) que ofrecen este comportamiento y lo han hecho desde que se creó el Framework, mientras que hasta que se introdujo LINQ, no había forma de tener una ejecución diferida. Lo cual, como han demostrado otras respuestas, es un requisito previo para habilitar ciertos tipos de cálculos que de otra manera serían imposibles (agotando todo el almacenamiento disponible) cuando se utiliza la ejecución inmediata.fuente