Expression<Func<T>> vs Func<T>: el secreto de la optimización en C#

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Expression\n\n

Imagina: estás escribiendo código C#. Necesitas pasar una condición de filtrado a un método. ¿Qué elegir: Func<User, bool> o Expression<Func<User, bool>>? Parece que la diferencia es mínima: ambos reciben un usuario y devuelven un bool. Pero entre bastidores se esconde todo un universo de diferencias.

\n\n

En Stack Overflow, esta pregunta ha acumulado miles de visitas, y con razón. Hoy entenderemos por qué Expression<Func<T>> puede ser más potente, rápido y seguro que un Func<T> común. Y lo más importante: cuándo usar cada uno para no dispararte en el pie.

\n\n

Mini curso intensivo: ¿qué son Func y Expression?

\n\n

Func<T> es simplemente un delegado. Una función lista para ejecutar, código máquina compilado. Le pasas datos y los procesa. Rápido, eficiente, pero... opaco.

\n\n
Func<User, bool> isAdult = user => user.Age >= 18;\nbool result = isAdult(someUser); // se ejecuta al instante
\n\n

Expression<Func<T>> es un árbol de expresión. No es código, sino su descripción. No puedes simplemente invocarlo: necesitas compilarlo en un delegado (con .Compile()) o analizar su estructura.

\n\n
Expression<Func<User, bool>> expr = user => user.Age >= 18;\n// No se puede invocar directamente: expr(someUser) — error de compilación\nvar compiled = expr.Compile();\nbool result = compiled(someUser); // ahora sí se puede
\n\n

Podría parecer un rodeo innecesario, pero es precisamente este carácter "descriptivo" el que abre la puerta a la magia.

\n\n

Escenario 1: LINQ to Objects (trabajo en memoria)

\n\n

Cuando trabajas con colecciones en memoria (por ejemplo, List<T>), LINQ to Objects usa Func<T>. Aquí no se necesitan árboles de expresión: todo se ejecuta en el mismo proceso.

\n\n
var users = new List<User>();\nvar adults = users.Where(u => u.Age >= 18).ToList(); // Func<User, bool>
\n\n

El rendimiento es excelente: sin sobrecarga por analizar el árbol. Si solo trabajas con datos locales, usa Func sin dudar. Es más rápido y sencillo.

\n\n

Escenario 2: LINQ to SQL / Entity Framework (trabajo con BD)

\n\n

Y aquí es donde comienza la magia. Cuando escribes:

\n\n
var adults = dbContext.Users.Where(u => u.Age >= 18).ToList();
\n\n

Entity Framework no ejecuta tu código en C#. En su lugar, analiza el árbol de expresión pasado y lo transforma en una consulta SQL:

\n\n
SELECT * FROM Users WHERE Age >= 18
\n\n

Si hubieras pasado un Func, EF no podría "mirar dentro": simplemente descargaría TODOS los usuarios de la tabla y realizaría el filtrado en memoria. Imagina: una tabla de 10 millones de registros viajando por la red a tu aplicación para filtrarse en el cliente.

\n\n

Expression<Func<T>> permite a EF (o a cualquier proveedor IQueryable) leer tu condición, traducirla al lenguaje de la base de datos (SQL, consulta MongoDB, OData, etc.) y ejecutar el filtrado en el servidor.

\n\n

Rendimiento: benchmarks y cifras reales

\n\n

Veamos las cifras. Tomemos un ejemplo simple: filtrar 100 000 usuarios por edad.

\n\n
// Opción 1: Func — cargamos todos, filtramos en memoria\nvar allUsers = dbContext.Users.ToList(); // SELECT * FROM Users\nvar adults1 = allUsers.Where(u => u.Age >= 18).ToList();\n\n// Opción 2: Expression — filtramos en el servidor\nvar adults2 = dbContext.Users.Where(u => u.Age >= 18).ToList(); // SELECT * FROM Users WHERE Age >= 18
\n\n

Resultados (aproximados, pero ilustrativos):

\n
    \n

Blogs

Book Recommendations