# Daf.Linq — Secuencias Perezosas para Delphi
🌍 Idioma: English | Español
Biblioteca autónoma de transformación de colecciones: perezosa y con conteo de referencias — en el espíritu de LINQ-to-Objects, las secuencias de Kotlin o los iteradores de Rust. No sabe nada de SQL y tiene valor por sí sola.
Conceptos
| Concepto | Descripción |
|---|---|
TQuery<T> |
La superficie de operadores fluida: un tipo valor que envuelve un System.IEnumerable<T> diferido y convierte implícitamente hacia y desde él |
IGrouping<TKey, T> |
Un grupo producido por GroupBy: una Key y sus Members |
Linq.From<T> |
Punto de entrada — construye un TQuery<T> sobre un array, un TEnumerable<T>, un IEnumerable<T> o un TSmartArray<T> |
Linq.FromOwned<T> |
Punto de entrada sobre una lista temporal que Linq libera con la consulta — ver Propiedad |
Las interfaces de Delphi no pueden declarar métodos genéricos: por eso los operadores genéricos (Select, Map, Zip, GroupBy) viven en el record TQuery<T> y no en la interfaz. Pasa IEnumerable<T>; encadena sobre TQuery<T>.
Inicio Rápido
uses Daf.Linq;
var Adultos := Linq.From<TPersona>(Personas) // Personas: TList<TPersona>
.Where(function(P: TPersona): Boolean begin Result := P.Edad >= 18 end)
.OrderBy(TComparer<TPersona>.Construct(
function(const A, B: TPersona): Integer begin Result := CompareStr(A.Nombre, B.Nombre) end))
.Take(10);
for var P in Adultos do // nada se ejecutó hasta esta línea
Writeln(P.Nombre);Adultos es un IEnumerable<TPersona> — diferido, con conteo de referencias, sin Free.
Operadores
| Operador | Tipo | Comportamiento |
|---|---|---|
Where / Filter |
perezoso | Conserva los elementos que cumplen un TFilterSpec<T> (un closure pelado convierte implícitamente) |
Select<TResult> / Map<TResult> |
perezoso | Proyecta cada elemento con un TFunc<T, TResult> |
Take(N) |
perezoso | Produce como mucho N y deja de consumir la fuente |
Skip(N) |
perezoso | Descarta los primeros N |
Zip<TOther, TResult> |
perezoso | Empareja con otra secuencia elemento a elemento hasta que una termina |
Distinct(Comparer) |
con buffer | Elimina duplicados por valor; pasa un comparador consciente de NULL para semántica SQL |
OrderBy(Comparer) |
con buffer | Ordena mediante un IComparer<T>, y es estable, como en .NET: los elementos que el comparador considera iguales conservan el orden de la fuente. Descendente es un comparador invertido: no hay OrderByDescending |
GroupBy<TKey>(KeySelector) |
con buffer | Devuelve IEnumerable<IGrouping<TKey, T>>, preservando el orden de las claves |
IsEmpty |
terminal | True si no produce nada; se detiene en el primer elemento |
Any(Spec) |
terminal | True si algún elemento cumple; se detiene en la primera coincidencia |
FirstOrDefault / LastOrDefault |
terminal | El primer / último elemento, o Default(T) (o el fallback indicado) |
TryFirst(out Value) |
terminal | El primer elemento y además un resultado encontrado/no encontrado |
Single / SingleOrDefault / TrySingle |
terminal | El único elemento — ver más abajo |
ToArray / ToList / ToList(List) / ToSmartArray |
terminal | Materializa la secuencia |
MoveToList(List) / MoveToList / MoveToArray |
terminal | Mueve los elementos: los saca de la lista fuente al entregarlos — ver Propiedad |
Diferido por defecto. Where/Select/Take/Skip/Zip se ejecutan elemento a elemento durante la iteración. OrderBy/Distinct/GroupBy necesitan todos los elementos, así que materializan una vez al comenzar la enumeración — pero tampoco antes de ella.
Los terminales no son destructivos. Enumeran sin consumir la query, así que el mismo valor sigue usable y un terminal puede llamarse dos veces:
var Q := Linq.From<TPersona>(Personas).Where(EsAdulto);
if not Q.IsEmpty then
for var P in Q do ... // sigue produciendo todoLa contrapartida es que cada llamada a un terminal vuelve a recorrer la fuente. Con una fuente costosa o de una sola pasada, materializa una vez (ToArray / ToList) y consulta eso.
Las excepciones están en Propiedad: sobre una fuente que Linq posee, un elemento que un terminal entrega a la aplicación no vuelve a salir, y los terminales MoveTo… sacan de la lista fuente lo que entregan.
¿First… o Single…?
Single afirma una forma: lanza ELinqError si la secuencia está vacía o tiene más de un elemento. SingleOrDefault tolera vacío pero sigue lanzando con más de uno. Úsalos cuando “más de uno” signifique que tus datos están mal y quieras enterarte:
Usuarios.Where(EmailEs(X)).SingleOrDefault; // un email duplicado es un bug → lanza
Entradas.OrderBy(MasRecientePrimero).FirstOrDefault; // se esperan muchos → coge el más recienteTryFirst / TrySingle son las formas que no lanzan. TryFirst es además la única manera de distinguir una secuencia vacía de una cuyo primer elemento vale justo Default(T) — algo que FirstOrDefault no puede expresar en tipos valor.
Propiedad
Linq maneja referencias, nunca objetos: qué lista libera cada objeto lo deciden cinco reglas.
Linq.From(X)— X es de la aplicación. Linq nunca lo libera, ni a él ni a sus objetos.Linq.FromOwned(X)— X es una lista temporal que nadie más guarda. Linq la libera cuando se libera la última consulta construida sobre ella.- Los operadores (
Where,Select,Take…) no crean nada: mantienen la referencia a la fuente. - Lo que devuelve un terminal es de la aplicación. El contenedor que crea, siempre. Los objetos, con
From, siguen siendo de quien los poseía; conFromOwned, pasan a la aplicación. Un objeto que crea una lambda es de la aplicación en los dos casos. MoveToList/MoveToArraysacan de la lista fuente lo que entregan, y no liberan nada. Son los únicos terminales que cambian la fuente.
Una fuente que Linq posee
FromOwned recibe una TObjectList<T> —o una lista que descienda de ella— de una clase T. Es la entrada para una lista que devuelve una llamada y nadie guarda en una variable:
Pedidos := TObjectList<TPedido>.Create; // posee sus objetos
Linq.FromOwned<TPedido>(CargarPedidos(Cliente)) // una lista temporal
.Where(EstaPendiente)
.ToList(Pedidos);
// los pedidos pendientes son ahora de Pedidos; la lista temporal y los
// demás pedidos se liberan con la consultaLo que hace cada terminal sobre una fuente poseída:
| Terminal | Sobre una fuente poseída |
|---|---|
ToList(List) / ToList / ToArray / ToSmartArray |
Cada elemento que pone en el contenedor pasa a la aplicación |
FirstOrDefault / LastOrDefault / TryFirst / Single / SingleOrDefault / TrySingle |
El elemento que devuelve pasa a la aplicación; un valor por defecto que se le pase no es de la fuente y no se toca |
IsEmpty / Any |
Ningún elemento cambia de dueño |
for..in |
Presta los elementos mientras vive la consulta |
- Un elemento que pasó a la aplicación no vuelve a salir en ningún terminal posterior sobre la misma fuente.
- Si la lista no posee sus objetos (
OwnsObjects = False), Linq libera solo la lista, y ningún elemento cambia de dueño. - Si añadir un elemento a la lista de destino lanza, el elemento se queda donde acabó —en el destino si lo guardó, en la fuente si no— y la excepción llega al llamante.
- Para mirar un objeto sin quedártelo, proyecta antes del terminal:
Linq.FromOwned<TPedido>(CargarPedidos(Cliente)).Select<string>(NumeroDePedido).FirstOrDefaultdevuelve una cadena, y todos los pedidos siguen siendo de la fuente. - Los temporales de la consulta viven hasta el final de la rutina que la construye, o dentro de un bucle hasta la vuelta siguiente, y la fuente vive con ellos. Para soltarla antes, guarda la consulta en una variable y reiníciala:
Q := Default(TQuery<TPedido>).
Límites:
- Convertir la consulta a
IEnumerable<T>—de forma implícita o conAsEnumerable— yGroupByprestan los elementos: lo que se lee por ahí vale mientras vive la consulta, y ningún terminal al que se llegue así entrega nada. - Una proyección que devuelve un objeto que posee un elemento —un
Selectque devuelvePedido.Cliente— no cambia de dueño: vive lo que vive el elemento. - Una lista que la aplicación sigue usando se libera con la consulta:
FromOwnedes solo para una lista que nadie más guarda.
Mover elementos a otra lista
Cuando la aplicación se queda la fuente y quiere parte de ella en otra lista, MoveToList(List) saca de la lista fuente cada elemento seleccionado —con Extract, que no lo libera— y lo añade a List. MoveToList sin argumentos hace lo mismo en una lista nueva, y MoveToArray en un array nuevo.
Linq.From<TPedido>(Pedidos) // Pedidos sigue siendo de la aplicación
.Where(EstaArchivado)
.MoveToList(Archivados);
// los pedidos archivados salieron de Pedidos y están en Archivados: un dueño cada uno- Mover necesita una consulta que llegue a una lista:
Linq.FromoLinq.FromOwnedsobre unTList<T>, con operadores que conservan el tipo del elemento (Where,Take,Skip,Distinct,OrderBy). Sobre un array o unIEnumerable<T>, o después deSelectoZip, los tres lanzanELinqError(MoveToList: the query does not reach a source list…) y la fuente no cambia. - La selección se recoge entera antes de mover nada, así que el orden de la cadena es el orden en el destino.
- Si añadir un elemento al destino lanza, el elemento se queda donde acabó, como con una fuente poseída.
- Sobre una fuente que Linq posee, lo que se mueve sale de la lista y no se libera con ella.
Un vocabulario, dos estrategias de ejecución
Commons trae una colección ansiosa (TSmartArray<T>) anterior a este módulo. Ambas hablan los mismos tipos de filtro y de mapper, así que un valor escrito una vez sirve para las dos:
| Concepto | Tipo compartido | Ansioso (TSmartArray<T>) |
Perezoso (TQuery<T>) |
|---|---|---|---|
| filtro | TFilterSpec<T> |
Where, Any |
Where / Filter, Any |
| proyección | TFunc<T, U> |
Select<U> |
Select<U> / Map<U> |
| ansioso → perezoso | — | Linq.From<T>(SmartArray) |
|
| perezoso → ansioso | — | ToSmartArray |
var Adultos: TFilterSpec<TPersona> := ...;
var Nombres: TFunc<TPersona, string> := ...;
Ansioso.Where(Adultos).Select<string>(Nombres); // ansioso, Commons
Linq.From<TPersona>(Personas).Where(Adultos).Select<string>(Nombres); // perezoso, aquíDeliberadamente distinto en cada lado:
Sortfrente aOrderBy— elSortansioso muta en sitio;OrderBydevuelve una secuencia diferida nueva. Semánticas distintas, nombres distintos.- Solo perezoso —
Take,Skip,Zip,GroupBy,Distincty los terminales de arriba. - Solo ansioso —
Concat,BinarySearch,FreeItems, propiedadesFirst/Last: son asuntos de contenedor, no de secuencia.
Por qué existen los dos. El propio Daf.Rtti de Commons encadena Where(...).Select(...) sobre un TSmartArray y está por debajo de este módulo en el grafo de dependencias, así que no puede usar la API perezosa sin invertir ese grafo. La duplicación de implementación es por tanto estructural, no un descuido — pero el vocabulario no está duplicado.
Patrones Comunes
Agrupar y resumir
for var G in Linq.From<TPersona>(Personas).GroupBy<string>(
function(P: TPersona): string begin Result := P.Pais end) do
Writeln(Format('%s: %d', [G.Key, Length(G.Members)]));Puente con el TSmartArray<T> ansioso
Commons ya trae una colección ansiosa respaldada por array. Daf.Linq reutiliza su vocabulario en lugar de inventar un segundo, de modo que ansioso y perezoso son dos estrategias de ejecución sobre una misma API:
uses Daf.Arrays, Daf.Linq;
var Perezoso := Linq.From<TPersona>(Ansioso); // TSmartArray<T> → perezoso
var Ansioso2 := Perezoso.Where(EsAdulto).ToSmartArray; // perezoso → TSmartArray<T>Cortocircuito sobre una fuente grande
Linq.From<Integer>(Millon).Where(EsPrimo).Take(5).ToArray;
// deja de recorrer en cuanto produce el quinto primoNotas
Resetsobre los enumeradores subyacentes no está soportado — las secuencias son de una sola pasada.- Se admiten tipos valor como
T(records,TNullable<T>): los adaptadores deDaf.Enumerableresuelven el lastre delGetCurrent: TObjectno genérico mediante una cláusula de resolución de métodos. - Para ordenación y deduplicación conscientes de NULL, usa los comparadores de
Daf.Comparersen Commons (TValueComparers,TNullableComparers).
Documentación
| Recurso | Descripción |
|---|---|
| SQLCute In-Memory | Ejecuta un IQuery de SQLCute sobre colecciones en memoria apoyándose en esta biblioteca |
| Guía de uso In-Memory | Los dos carriles, semántica NULL, alcance de DISTINCT |
| SQLCute | El constructor fluido de consultas SQL |