visual consultas consulta c# linq

c# - consultas - consulta sql en visual basic



¿Cómo funciona realmente la ejecución de la consulta LINQ diferida? (5)

Recientemente me enfrenté a esa pregunta: What numbers will be printed considering the following code:

class Program { static void Main(string[] args) { int[] numbers = { 1, 3, 5, 7, 9 }; int threshold = 6; var query = from value in numbers where value >= threshold select value; threshold = 3; var result = query.ToList(); result.ForEach(Console.WriteLine); Console.ReadLine(); } }

Respuesta: 3, 5, 7, 9

Lo que fue bastante sorprendente para mí. Pensé que ese valor de threshold se colocará en la pila en la construcción de la consulta y más tarde en el tiempo de ejecución, ese número se retirará y se usará en la condición ... lo que no ocurrió.

Otro caso (los numbers se establecen en null justo antes de la ejecución):

static void Main(string[] args) { int[] numbers = { 1, 3, 5, 7, 9 }; int threshold = 6; var query = from value in numbers where value >= threshold select value; threshold = 3; numbers = null; var result = query.ToList(); ... }

Parece no tener efecto en la consulta. Imprime exactamente la misma respuesta que en el ejemplo anterior.

¿Podría alguien ayudarme a entender lo que realmente está pasando detrás de la escena? ¿Por qué cambiar el threshold tiene el impacto en la ejecución de la consulta mientras que cambiar los numbers no?


Creo que la forma más fácil de entenderlo es simplemente mirar su línea por línea y pensar en qué y cuándo se ejecuta, como se opone solo a la memoria.

//this line declares numbers array int[] numbers = { 1, 3, 5, 7, 9 }; //that one declares value of threshold and sets it to 6 int threshold = 6; //that line declares the query which is not of the type int[] but probably IQueryable<int>, but never executes it at this point //To create IQueryable it still iterates through numbers variable, and kind of assign lambda function to each of the items. var query = from value in numbers where value >= threshold select value; //that line changes threshold value to 6 threshold = 3; //that line executes the query defined easier, and uses current value value of threshold, as it is only reference var result = query.ToList(); result.ForEach(Console.WriteLine); Console.ReadLine();

Ese mecanismo le brinda algunas características interesantes como la creación de consultas en múltiples lugares y su ejecución una vez que todo está listo para funcionar.

Establecer el valor de la variable de numbers en null no cambiará el resultado como se invocó de inmediato, para la enumeración.


La variable "números" es aquella en la que se ha creado una instancia de la consulta y funciona en ella. Conserva el valor que tenía cuando se estableció la consulta. Mientras que el "umbral" valiable se usa en el predicado cuando se ejecuta la consulta, que está en la Lista de To (). En ese punto, los predicados encuentran el valor en trashhold.

De todos modos no es un código claro ...


Lo más fácil es ver qué generará el compilador. Puede utilizar este sitio: https://sharplab.io

using System.Linq; public class MyClass { public void MyMethod() { int[] numbers = { 1, 3, 5, 7, 9 }; int threshold = 6; var query = from value in numbers where value >= threshold select value; threshold = 3; numbers = null; var result = query.ToList(); } }

Y aquí está la salida:

using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; [assembly: AssemblyVersion("0.0.0.0")] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [module: UnverifiableCode] public class MyClass { [CompilerGenerated] private sealed class <>c__DisplayClass0_0 { public int threshold; internal bool <MyMethod>b__0(int value) { return value >= this.threshold; } } public void MyMethod() { MyClass.<>c__DisplayClass0_0 <>c__DisplayClass0_ = new MyClass.<>c__DisplayClass0_0(); int[] expr_0D = new int[5]; RuntimeHelpers.InitializeArray(expr_0D, fieldof(<PrivateImplementationDetails>.D603F5B3D40E40D770E3887027E5A6617058C433).FieldHandle); int[] source = expr_0D; <>c__DisplayClass0_.threshold = 6; IEnumerable<int> source2 = source.Where(new Func<int, bool>(<>c__DisplayClass0_.<MyMethod>b__0)); <>c__DisplayClass0_.threshold = 3; List<int> list = source2.ToList<int>(); } } [CompilerGenerated] internal sealed class <PrivateImplementationDetails> { [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 20)] private struct __StaticArrayInitTypeSize=20 { } internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=20 D603F5B3D40E40D770E3887027E5A6617058C433 = bytearray(1, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0, 9, 0, 0, 0); }

Como puede ver, si cambia la variable de threshold , realmente cambia el campo en auto-generated clase auto-generated . Debido a que puede ejecutar la consulta en cualquier momento, no es posible tener una referencia al campo que vive en la pila, porque al salir del método, el threshold se eliminará de la pila, por lo que el compilador cambia este campo a una clase generada automáticamente con el field del mismo tipo.

Y segundo problema: por qué funciona nulo (no es visible en este código)

Cuando usas: source.Where . source.Where llama a este método de extensión:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { if (source == null) throw Error.ArgumentNull("source"); if (predicate == null) throw Error.ArgumentNull("predicate"); if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Where(predicate); if (source is TSource[]) return new WhereArrayIterator<TSource>((TSource[])source, predicate); if (source is List<TSource>) return new WhereListIterator<TSource>((List<TSource>)source, predicate); return new WhereEnumerableIterator<TSource>(source, predicate); }

Como puedes ver, pasa referencia a:

WhereEnumerableIterator<TSource>(source, predicate);

Y aquí está el código fuente de where iterator :

class WhereEnumerableIterator<TSource> : Iterator<TSource> { IEnumerable<TSource> source; Func<TSource, bool> predicate; IEnumerator<TSource> enumerator; public WhereEnumerableIterator(IEnumerable<TSource> source, Func<TSource, bool> predicate) { this.source = source; this.predicate = predicate; } public override Iterator<TSource> Clone() { return new WhereEnumerableIterator<TSource>(source, predicate); } public override void Dispose() { if (enumerator is IDisposable) ((IDisposable)enumerator).Dispose(); enumerator = null; base.Dispose(); } public override bool MoveNext() { switch (state) { case 1: enumerator = source.GetEnumerator(); state = 2; goto case 2; case 2: while (enumerator.MoveNext()) { TSource item = enumerator.Current; if (predicate(item)) { current = item; return true; } } Dispose(); break; } return false; } public override IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) { return new WhereSelectEnumerableIterator<TSource, TResult>(source, predicate, selector); } public override IEnumerable<TSource> Where(Func<TSource, bool> predicate) { return new WhereEnumerableIterator<TSource>(source, CombinePredicates(this.predicate, predicate)); } }

Así que simplemente mantiene la referencia a nuestro objeto fuente en un campo privado.


Su consulta puede escribirse así en la sintaxis del método:

var query = numbers.Where(value => value >= threshold);

O:

Func<int, bool> predicate = delegate(value) { return value >= threshold; } IEnumerable<int> query = numbers.Where(predicate);

Estas piezas de código (incluida su propia consulta en la sintaxis de consulta) son todas equivalentes.

Cuando desenrolla la consulta de esa manera, verá que el predicate es un método anónimo y el threshold es un closure en ese método. Eso significa que asumirá el valor en el momento de la ejecución. El compilador generará un método real (no anónimo) que se encargará de eso. El método no se ejecutará cuando se declare, sino para cada elemento cuando la query se enumera (la ejecución se aplaza ). Dado que la enumeración ocurre después de que se cambia el valor del threshold (y el threshold es un cierre), se usa el nuevo valor.

Cuando establece numbers en null , no establece la referencia en ninguna parte, pero el objeto todavía existe. El IEnumerable devuelto por Where (y referenciado en la query ) todavía hace referencia a él y no importa que la referencia inicial sea null ahora.

Eso explica el comportamiento: los numbers y el threshold desempeñan diferentes roles en la ejecución diferida. numbers es una referencia a la matriz que se enumera, mientras que threshold es una variable local, cuyo alcance se "reenvía" al método anónimo.

Extensión, parte 1: Modificación del cierre durante la enumeración.

Puede llevar su ejemplo un paso más allá cuando reemplace la línea ...

var result = query.ToList();

...con:

List<int> result = new List<int>(); foreach(int value in query) { threshold = 8; result.Add(value); }

Lo que está haciendo es cambiar el valor del threshold durante la iteración de su matriz. Cuando golpea el cuerpo del bucle la primera vez (cuando el value es 3), cambia el umbral a 8, lo que significa que los valores 5 y 7 se omitirán y el siguiente valor que se agregará a la lista es 9. La razón es que el valor del threshold se evaluará nuevamente en cada iteración y se utilizará el valor válido de ese momento . Y como el umbral ha cambiado a 8, los números 5 y 7 ya no se evalúan como mayores o iguales.

Extensión, parte 2: Entity Framework es diferente

Para hacer las cosas más complicadas, cuando utiliza proveedores de LINQ que crean una consulta diferente de su original y luego la ejecutan, las cosas son ligeramente diferentes. Los ejemplos más comunes son Entity Framework (EF) y LINQ2SQL (ahora ampliamente reemplazado por EF). Estos proveedores crean una consulta SQL a partir de la consulta original antes de la enumeración . Dado que en este momento el valor del cierre se evalúa solo una vez (en realidad no es un cierre, porque el compilador genera un árbol de expresiones y no un método anónimo), los cambios en el threshold durante la enumeración no tienen efecto en el resultado . Estos cambios suceden después de que la consulta se envía a la base de datos.

La lección de esto es que debe estar siempre consciente de qué sabor de LINQ está utilizando y que una cierta comprensión de su funcionamiento interno es una ventaja.


Su consulta LINQ no devuelve los datos solicitados, sino la posibilidad de obtener algo que puede acceder a los elementos de sus datos uno por uno.

En términos de software: el valor de su declaración LINQ es un IEnumerable<T> (o IQueryable<T> no se trata más a fondo aquí). Este objeto no guarda tus datos. De hecho, no se puede hacer mucho con un IEnumerable<T> . Lo único que puede hacer es producir otro objeto que implemente IEnumerator<T> . (note la diferencia: IEnumerable vs IEnumerator). Esta función `GetEnumerator () ''es la parte" obtener algo que puede acceder a ... "en mi primera oración.

El objeto que obtuvo de IEnumerable<T>.GetEnumerator() , implementa IEnumerator. Este objeto tampoco tiene que contener sus datos. Solo sabe cómo producir el primer elemento de sus datos (si hay uno), y si tiene un elemento, sabe cómo obtener el siguiente elemento (si existe). Este es el " que puede acceder a los elementos de sus datos uno por uno " de mi primera oración.

Por lo tanto, tanto el IEnumerable<T> como el Enumerator<T> no (tienen que) retener sus datos. Solo son objetos que le ayudan a acceder a sus datos en un orden definido.

En los primeros días, cuando no teníamos List<T> o clases de colección comparables que implementaran IEnumerable<T> , era bastante molesto implementar IEnumerable<T> y las IEnumerator<T> Reset , Current y MoveNext . De hecho, hoy en día es difícil encontrar ejemplos de implementación de IEnumerator<T> que no utilicen una clase que también implemente IEnumerator<T> . Example

La introducción de la palabra clave Yield facilitó mucho la implementación de IEnumerable<T> e IEnumerator<T> . Si una función contiene un Yield return , devuelve un IEnumerable<T> :

IEnumerable<double> GetMySpecialNumbers() { // returns the sequence: 0, 1, pi and e yield return 0.0; yield return 1.0; yield return 4.0 * Math.Atan(1.0); yield return Math.Log(1.0) }

Tenga en cuenta que uso el término secuencia. No es una Lista, no es un Diccionario, solo puede acceder a los elementos solicitando el primero y solicitando repetidamente el siguiente.

Puede acceder a los elementos de la secuencia utilizando IEnumerable<T>.GetEnumerator() y las tres funciones de IEnumerator<T> . Este método ya no se usa más:

IEnumerable<double> myNumbers = GetMySpecialNumbers(); IEnumerator<double> enumerator = myNumbers.GetEnumerator(); enumerator.Reset(); // while there are numbers, write the next one while(enumerator.MoveNext()) { // there is still an element in the sequence double valueToWrite = enumerator.Current(); Console.WriteLine(valueToWrite); }

Con la introducción de foreach esto se ha vuelto mucho más fácil:

foreach (double valueToWrite in GetMySpecialNumbers()) Console.WriteLine(valueToWrite);

Internamente, esto hará GetNumerator() y Reset() / MoveNext() / Current()

Todas las clases de colecciones genéricas como Lista, Array, Diccionario, HashTable, etc., implementan IEnumerable. La mayoría de las veces que una función devuelve un IEnumerable, encontrará que internamente usa una de estas clases de colección.

Otro gran invento después del yield y la foreach fue la introducción de métodos de extensión. Ver los métodos de extensión desmitificados .

Los métodos de extensión le permiten tomar una clase que no puede cambiar, como List<T> y escribir nuevas funciones para ella, usando solo las funciones a las que tiene acceso.

Este fue el impulso para LINQ. Nos permitió escribir nuevas funciones para todo lo que decía: "hey, soy una secuencia, puedes solicitar mi primer elemento y mi siguiente elemento" (= Implemento IEnumerable).

Si observa el código fuente de LINQ, encontrará que las funciones de LINQ como Where / Select / First / Reverse / ... etc, están escritas como funciones de extensión de IEnumerable. La mayoría de ellos usan clases de colección genéricas (tabla hash, diccionario), algunas de ellas usan rendimiento, y algunas veces incluso verá las funciones básicas de IEnumerator como Restablecer / MoverNext

Muy a menudo escribirá nuevas funciones al concatenar funciones LINQ. Sin embargo, tenga en cuenta que a veces el yield hace que su función sea mucho más fácil de entender y, por lo tanto, más fácil de reutilizar, depurar y mantener.

Ejemplo: supongamos que tiene una secuencia de Products producidos. Cada Product tiene una propiedad DateTime ProductCompletedTime que representa cuando se completó su producción del producto.

Supongamos que desea saber cuánto tiempo hay entre dos productos terminados. Problema: esto no se puede calcular para el primer producto.

Con un rendimiento esto es fácil:

public static IEnumerable<TimeSpan> ToProductionTimes<Product> (this IEnumerable<Product> products) { var orderedProducts = product.OrderBy(product => product.ProductionTime; Product previousProduct = orderedProducts.FirstOrDefault(); foreach (Product product in orderedProducts.Skip(1)) { yield return product.ProductCompletedTime - previouseProduct.ProductCompletedTime; previousProduct = product; } }

Trate de hacer esto en Linq, será mucho más difícil entender lo que sucede.

Conclusión Un IEnumerable no guarda sus datos, solo tiene el potencial de acceder a sus datos uno por uno.

Los métodos más utilizados para acceder a los datos son foreach, ToList (), ToDictionary, First, etc.

Siempre que necesite escribir una función que devuelva un IEnumerable<T> difícil, al menos considere escribir una función de yield return .