c# - expresiones - ¿Por qué usarías Expression<Func<T>> en lugar de Func<T>?
if lambda c# (9)
Entiendo lambdas y los delegados de Func
y Action
. Pero las expresiones me tocan. ¿En qué circunstancias usaría una Expression<Func<T>>
lugar de una antigua Func<T>
?
Cuando quiera tratar las expresiones lambda como árboles de expresión, mire dentro de ellas en lugar de ejecutarlas. Por ejemplo, LINQ to SQL obtiene la expresión y la convierte en la instrucción SQL equivalente y la envía al servidor (en lugar de ejecutar la lambda).
Conceptualmente, la Expression<Func<T>>
es completamente diferente de Func<T>
. Func<T>
denota un delegate
que es más o menos un puntero a un método y Expression<Func<T>>
denota una estructura de datos de árbol para una expresión lambda. Esta estructura de árbol describe lo que hace una expresión lambda en lugar de hacer lo real. Básicamente, contiene datos sobre la composición de expresiones, variables, llamadas a métodos, ... (por ejemplo, contiene información como esta lambda es una constante + algún parámetro). Puede usar esta descripción para convertirla en un método real (con Expression.Compile
) o hacer otras cosas (como el ejemplo de LINQ to SQL) con él. El hecho de tratar a las lambdas como métodos anónimos y árboles de expresión es puramente una cuestión de tiempo de compilación.
Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }
se compilará de manera efectiva a un método IL que no obtiene nada y devuelve 10.
Expression<Func<int>> myExpression = () => 10;
se convertirá a una estructura de datos que describe una expresión que no obtiene parámetros y devuelve el valor 10:
Si bien ambos se ven iguales en tiempo de compilación, lo que genera el compilador es totalmente diferente .
Estoy agregando respuesta por noobs porque estas respuestas parecieron pasadas por alto, hasta que me di cuenta de lo simple que es. A veces, su expectativa de que es complicado es lo que lo hace incapaz de "envolver su cabeza en eso".
No tuve que entender la diferencia hasta que entré en un ''error'' realmente molesto al intentar usar LINQ-to-SQL de forma genérica:
public IEnumerable<T> Get(Func<T, bool> conditionLambda){
using(var db = new DbContext()){
return db.Set<T>.Where(conditionLambda);
}
}
Esto funcionó muy bien hasta que comencé a obtener OutofMemoryExceptions en conjuntos de datos más grandes. Establecer puntos de interrupción dentro de la lambda me hizo darme cuenta de que estaba iterando a través de cada fila de mi mesa, uno por uno, buscando coincidencias con mi condición de lambda. Esto me dejó perplejo por un tiempo, porque ¿por qué diablos está tratando a mi tabla de datos como un IEnumerable gigante en lugar de hacer LINQ-a-SQL como se supone que lo haga? También estaba haciendo exactamente lo mismo en mi contraparte LINQ-to-MongoDb.
La solución fue simplemente convertir Func<T, bool>
en Expression<Func<T, bool>>
, así que busqué en Google por qué necesita una Expression
lugar de Func
, terminando aquí.
Una expresión simplemente convierte a un delegado en un dato sobre sí mismo. Así que a => a + 1
convierte en algo así como "En el lado izquierdo hay un int a
. En el lado derecho le agregas 1". Eso es. Puedes irte a casa ahora. Obviamente, es más estructurado que eso, pero eso es esencialmente todo lo que realmente es un árbol de expresiones, nada que envuelva la cabeza.
Al comprender esto, queda claro por qué LINQ-to-SQL necesita una Expression
y un Func
no es adecuado. Func
no lleva consigo una forma de meterse en sí mismo, de ver el meollo de la cuestión de cómo traducirlo en una consulta SQL / MongoDb / other. No puedes ver si está haciendo sumas o multiplicaciones en la resta. Todo lo que puedes hacer es ejecutarlo. Expression
, por otro lado, te permite mirar dentro del delegado y ver todo lo que quiere hacer, lo que te permite traducirlo a lo que quieras, como una consulta SQL. Func
no funcionó porque mi DbContext era ciego a lo que realmente estaba en la expresión lambda para convertirlo en SQL, por lo que hizo la mejor cosa siguiente e iteró ese condicional en cada fila de mi tabla.
Edición: explicando mi última oración a petición de John Peter:
IQueryable extiende IEnumerable, por lo que los métodos de IEnumerable como Where()
obtienen sobrecargas que aceptan Expression
. Cuando pasas una Expression
a eso, mantienes un IQueryable como resultado, pero cuando pasas un Func
, estás retrocediendo en la base IEnumerable y obtendrás un IEnumerable como resultado. En otras palabras, sin darte cuenta, has convertido tu conjunto de datos en una lista para ser iterado en lugar de algo para consultar. Es difícil notar una diferencia hasta que realmente miras bajo el capó las firmas.
Hay una explicación más filosófica al respecto en el libro de Krzysztof Cwalina ( Pautas de diseño de marcos: convenciones, modismos y patrones para bibliotecas .NET reutilizables );
Editar para la versión sin imagen:
La mayoría de las veces querrá Func o Action si todo lo que tiene que suceder es ejecutar algún código. Necesita Expression cuando el código necesita ser analizado, serializado u optimizado antes de ejecutarse. La expresión es para pensar en el código, Func / Action es para ejecutarlo.
LINQ es el ejemplo canónico (por ejemplo, hablar con una base de datos), pero en verdad, cada vez que te importa más expresar qué hacer, en lugar de hacerlo realmente. Por ejemplo, uso este enfoque en la pila RPC de protobuf-net (para evitar la generación de código, etc.), por lo que llama a un método con:
string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));
Esto deconstruye el árbol de expresiones para resolver SomeMethod
(y el valor de cada argumento), realiza la llamada RPC, actualiza los argumentos ref
/ out
y devuelve el resultado de la llamada remota. Esto solo es posible a través del árbol de expresiones. Cubro esto más here .
Otro ejemplo es cuando se construyen los árboles de expresiones manualmente con el propósito de compilarlos en un lambda, como lo hace el código de los operadores genéricos .
La razón principal es cuando no desea ejecutar el código directamente, sino que desea inspeccionarlo. Esto puede ser por cualquier número de razones:
- Asignación del código a un entorno diferente (es decir, código C # a SQL en Entity Framework)
- Reemplazo de partes del código en tiempo de ejecución (programación dinámica o incluso técnicas de SECADO)
- Validación de código (muy útil al emular scripts o al hacer análisis)
- Serialización: las expresiones se pueden serializar de manera fácil y segura, los delegados no pueden
- Seguridad fuertemente tipada en cosas que no son intrínsecamente tipificadas, y explotar las comprobaciones del compilador aunque esté realizando llamadas dinámicas en tiempo de ejecución (ASP.NET MVC 5 con Razor es un buen ejemplo)
Me gustaría agregar algunas notas sobre las diferencias entre Func<T>
y Expression<Func<T>>
:
-
Func<T>
es simplemente un MulticastDelegate normal de la vieja escuela; -
Expression<Func<T>>
es una representación de la expresión lambda en forma de árbol de expresión; - el árbol de expresiones se puede construir a través de la sintaxis de la expresión lambda oa través de la sintaxis de la API;
- el árbol de expresiones se puede compilar a un delegado
Func<T>
; - la conversión inversa es teóricamente posible, pero es un tipo de descompilación, no hay una funcionalidad integrada para eso ya que no es un proceso sencillo;
- el árbol de expresiones se puede observar / traducir / modificar a través de
ExpressionVisitor
; - los métodos de extensión para IEnumerable operan con
Func<T>
; - los métodos de extensión para IQueryable operan con
Expression<Func<T>>
.
Hay un artículo que describe los detalles con ejemplos de código:
LINQ: Func <T> vs. Expresión <Func <T>> .
Espero que sea de ayuda.
Todavía no veo ninguna respuesta que mencione el rendimiento. Pasar Func<>
s a Where()
o Count()
es malo. Realmente malo. Si usas un Func<>
entonces llama a las cosas de IQueryable
LINQ en lugar de a IQueryable
, lo que significa que todas las tablas se introducen y luego se filtran. Expression<Func<>>
es significativamente más rápida, especialmente si está consultando una base de datos que vive en otro servidor.
Una consideración extremadamente importante en la elección de Expression vs Func es que los proveedores de IQueryable como LINQ to Entities pueden "digerir" lo que pasa en una Expresión, pero ignorarán lo que pasa en una Func. Tengo dos publicaciones en el blog sobre el tema:
Más sobre Expression vs Func con Entity Framework y Falling in Love con LINQ - Parte 7: Expressions and Funcs (la última sección)
Usaría una expresión cuando quiera tratar su función como datos y no como código. Puede hacer esto si desea manipular el código (como datos). La mayoría de las veces, si no ve la necesidad de expresiones, es probable que no necesite usar una.