¿Por qué usaría Expression <Func <T>> en lugar de Func <T>?

949

Entiendo lambdas y la Funcy Actiondelegados. Pero las expresiones me sorprenden.

¿En qué circunstancias Expression<Func<T>>usarías un viejo en lugar de uno viejo Func<T>?

Richard Nagle
fuente
14
Func <> se convertirá en un método en el nivel del compilador c #, Expression <Func <>> se ejecutará en el nivel MSIL después de compilar el código directamente, esa es la razón por la que es más rápido
Waleed AK
1
además de las respuestas, la especificación del lenguaje CSharp "tipos de árboles 4,6 de expresión" es útil como referencia cruzada
djeikyb

Respuestas:

1133

Cuando desee tratar las expresiones lambda como árboles de expresión y mirar dentro de ellas en lugar de ejecutarlas. Por ejemplo, LINQ to SQL obtiene la expresión y la convierte a la instrucción SQL equivalente y la envía al servidor (en lugar de ejecutar el lambda).

Conceptualmente, Expression<Func<T>>es completamente diferente de Func<T>. Func<T>denota un delegateque 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 alguna constante + algún parámetro). Puede usar esta descripción para convertirla a un método real (con Expression.Compile) o hacer otras cosas (como el ejemplo LINQ to SQL) con ella. El acto de tratar 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; }

efectivamente compilará a un método IL que no obtiene nada y devuelve 10.

Expression<Func<int>> myExpression = () => 10;

se convertirá en una estructura de datos que describe una expresión que no obtiene parámetros y devuelve el valor 10:

Expression vs Func imagen más grande

Si bien ambos se ven iguales en tiempo de compilación, lo que genera el compilador es totalmente diferente .

Mehrdad Afshari
fuente
96
En otras palabras, un Expressioncontiene la metainformación sobre cierto delegado.
bertl
40
@bertl En realidad, no. El delegado no está involucrado en absoluto. La razón por la que hay alguna asociación con un delegado es que puede compilar la expresión en un delegado, o para ser más precisos, compilarlo en un método y obtener el delegado en ese método como un valor de retorno. Pero el árbol de expresión en sí mismo son solo datos. El delegado no existe cuando se usa en Expression<Func<...>>lugar de solo Func<...>.
Luaan
55
@Kyle Delaney, (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }tal expresión es un árbol de expresión, se crean ramas para la instrucción If.
Matteo Marciano - MSCP
3
@bertl Delegate es lo que ve la CPU (código ejecutable de una arquitectura), Expression es lo que ve el compilador (simplemente otro formato de código fuente, pero aún así el código fuente).
codewarrior
55
@bertl: Podría resumirse con mayor precisión al decir que una expresión es para una función lo que un constructor de cadenas es para una cadena. No es una cadena / función, pero contiene los datos necesarios para crear uno cuando se le pide que lo haga.
Flater
337

Estoy agregando una respuesta para noobs porque estas respuestas parecían pasar por alto, hasta que me di cuenta de lo simple que es. A veces es su expectativa que es complicado lo que le hace incapaz de 'entenderlo'.

No necesitaba entender la diferencia hasta que me encontré con un 'error' realmente molesto que intentaba usar LINQ-to-SQL genéricamente:

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 del lambda me hizo darme cuenta de que estaba iterando a través de cada fila en mi tabla uno por uno buscando coincidencias con mi condición lambda. Esto me dejó perplejo por un tiempo, porque ¿por qué diablos trata a mi tabla de datos como un IEnumerable gigante en lugar de hacer LINQ-to-SQL como se supone que debe hacerlo? También estaba haciendo exactamente lo mismo en mi homólogo de LINQ-to-MongoDb.

La solución era simplemente convertir Func<T, bool>en Expression<Func<T, bool>>, por lo busqué en Google por lo que necesita un Expressionlugar de Func, terminando aquí.

Una expresión simplemente convierte a un delegado en datos sobre sí misma. Entonces se a => a + 1convierte 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, está más estructurado que eso, pero eso es esencialmente todo un árbol de expresión, nada que pueda comprender.

Entendiendo eso, queda claro por qué LINQ-to-SQL necesita un Expression, y un Funcno es adecuado. Funcno lleva consigo una forma de meterse en sí mismo, para ver lo esencial de cómo traducirlo a una consulta SQL / MongoDb / other. No puedes ver si se trata de sumar, multiplicar o restar. Todo lo que puedes hacer es ejecutarlo. Expression, por otro lado, le permite mirar dentro del delegado y ver todo lo que quiere hacer. Esto le permite traducir el delegado a lo que desee, como una consulta SQL. Funcno funcionó porque mi DbContext era ciego al contenido de la expresión lambda. Debido a esto, no pudo convertir la expresión lambda en SQL; sin embargo, hizo lo siguiente mejor e iteró eso condicional a través de cada fila en mi tabla.

Editar: exponiendo mi última oración a petición de John Peter:

IQueryable extiende IEnumerable, por lo que los métodos de IEnumerable como Where()obtener sobrecargas que aceptan Expression. Cuando pasas un Expressiona eso, mantienes un IQueryable como resultado, pero cuando pasas un Func, estás volviendo a la base IEnumerable y obtendrás un IEnumerable como resultado. En otras palabras, sin darse cuenta, ha convertido su conjunto de datos en una lista para ser iterada en lugar de algo para consultar. Es difícil notar una diferencia hasta que realmente miras bajo el capó las firmas.

Chad Hedgcock
fuente
2
Chad; Explique este comentario un poco más: "Func no funcionó porque mi DbContext estaba ciego a lo que realmente estaba en la expresión lambda para convertirlo en SQL, por lo que hizo la siguiente mejor opción e iteró ese condicional en cada fila de mi tabla ".
John Peters el
2
>> Func ... Todo lo que puedes hacer es ejecutarlo. No es exactamente cierto, pero creo que ese es el punto que debe enfatizarse. Las funciones / acciones deben ejecutarse, las expresiones deben analizarse (antes de ejecutarse o incluso en lugar de ejecutarse).
Konstantin
@Chad ¿El problema aquí fue eso ?: db.Set <T> consultó toda la tabla de la base de datos, y después, porque .Where (conditionLambda) usó el método de extensión Where (IEnumerable), que se enumera en toda la tabla en la memoria . Creo que obtienes OutOfMemoryException porque, este código intentó cargar toda la tabla en la memoria (y, por supuesto, creó los objetos). Estoy en lo cierto? Gracias :)
Bence Végert
104

Una consideración extremadamente importante en la elección de Expression vs Func es que los proveedores IQueryable como LINQ to Entities pueden 'digerir' lo que pasa en una Expresión, pero ignorarán lo que pasa en un Func. Tengo dos publicaciones de blog sobre el tema:

Más sobre Expresión vs Func con Entity Framework y caer en el amor con LINQ - Parte 7: Expresiones y Funcs (la última sección)

LSpencer777
fuente
+ l para explicación. Sin embargo, obtengo 'El tipo de nodo de expresión LINQ' Invocar 'no es compatible con LINQ to Entities'. y tuve que usar ForEach después de buscar los resultados.
tymtam
77

Me gustaría agregar algunas notas sobre las diferencias entre Func<T>y Expression<Func<T>>:

  • Func<T> es simplemente un Delegado de multidifusión de la vieja escuela normal;
  • Expression<Func<T>> es una representación de la expresión lambda en forma de árbol de expresión;
  • el árbol de expresión se puede construir mediante la sintaxis de expresión lambda o mediante la sintaxis API;
  • el árbol de expresión se puede compilar a un delegado Func<T>;
  • la conversión inversa es teóricamente posible, pero es una especie de descompilación, no hay funcionalidad integrada para eso, ya que no es un proceso sencillo;
  • el árbol de expresión se puede observar / traducir / modificar a través de ExpressionVisitor;
  • los métodos de extensión para IEnumerable funcionan con Func<T>;
  • Los métodos de extensión para IQueryable funcionan con Expression<Func<T>>.

Hay un artículo que describe los detalles con ejemplos de código:
LINQ: Func <T> vs. Expression <Func <T>> .

Espero que sea de ayuda.

Olexander Ivanitskyi
fuente
Buena lista, una pequeña nota es que mencionas que la conversión inversa es posible, sin embargo, un inverso exacto no lo es. Algunos metadatos se pierden durante el proceso de conversión. Sin embargo, podría descompilarlo en un árbol de Expresión que produce el mismo resultado cuando se compila nuevamente.
Aidiakapi
76

Hay una explicación más filosófica al respecto del libro de Krzysztof Cwalina ( Directrices de diseño del marco: convenciones, modismos y patrones para bibliotecas .NET reutilizables );

Rico Mariani

Editar para la versión sin imagen:

La mayoría de las veces vas a querer Func o Action si todo lo que necesita suceder es ejecutar algún código. Necesita Expresión cuando el código debe analizarse, serializarse u optimizarse antes de ejecutarse. Expression es para pensar en código, Func / Action es para ejecutarlo.

Oğuzhan Soykan
fuente
10
Así poner. es decir. Necesita expresión cuando espera que su Func se convierta en algún tipo de consulta. Es decir. necesitas database.data.Where(i => i.Id > 0)ser ejecutado como SELECT FROM [data] WHERE [id] > 0. Si sólo tiene que pasar en un Func, usted ha puesto anteojeras de su conductor y todo lo que puede hacer es SELECT *y luego una vez que se carga todos los datos en la memoria, iterar a través de cada uno y filtrar todo con id> 0. Envolviendo su Funcen Expressionempodera el controlador para analizar Funcy convertirlo en una consulta SQL / MongoDb / other.
Chad Hedgcock
Entonces, cuando estoy planeando unas vacaciones, lo usaría, Expressionpero cuando esté de vacaciones será Func/Action;)
GoldBishop
1
@ChadHedgcock Esta fue la pieza final que necesitaba. Gracias. He estado mirando esto por un tiempo, y su comentario aquí hizo que todo el estudio haga clic.
Johnny
37

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 expresión para resolver SomeMethod(y el valor de cada argumento), realiza la llamada RPC, actualiza any ref/ outargs y devuelve el resultado de la llamada remota. Esto solo es posible a través del árbol de expresión. Cubro esto más aquí .

Otro ejemplo es cuando está construyendo los árboles de expresión manualmente con el propósito de compilar en una lambda, como lo hace el código de operadores genéricos .

Marc Gravell
fuente
20

Usaría una expresión cuando desee 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, probablemente no necesite usar una.

Andrew Hare
fuente
19

La razón principal es cuando no desea ejecutar el código directamente, sino que desea inspeccionarlo. Esto puede ser por varias razones:

  • Asignación del código a un entorno diferente (es decir, código C # a SQL en Entity Framework)
  • Reemplazar partes del código en tiempo de ejecución (programación dinámica o incluso técnicas DRY simples)
  • Validación de código (muy útil al emular scripts o al hacer análisis)
  • Serialización: las expresiones se pueden serializar con bastante facilidad y seguridad, los delegados no pueden
  • Seguridad fuertemente tipada en cosas que no son intrínsecamente tipadas y explotando las comprobaciones del compilador aunque esté haciendo llamadas dinámicas en tiempo de ejecución (ASP.NET MVC 5 con Razor es un buen ejemplo)
Luaan
fuente
se puede elaborar un poco más en No.5
uowzd01
@ uowzd01 Basta con mirar a Razor: utiliza este enfoque ampliamente.
Luaan
@Luaan Estoy buscando serializaciones de expresiones pero no puedo encontrar nada sin un uso limitado de terceros. ¿Soporta .Net 4.5 la serialización del árbol de expresión?
vabii
@vabii No que yo sepa, y realmente no sería una buena idea para el caso general. Mi punto era más acerca de que pudieras escribir una serialización bastante simple para los casos específicos que deseas admitir, contra interfaces diseñadas con anticipación, lo he hecho varias veces. En el caso general, un Expressionpuede ser tan imposible de serializar como un delegado, ya que cualquier expresión puede contener una invocación de una referencia arbitraria de delegado / método. "Fácil" es relativo, por supuesto.
Luaan
15

Todavía no veo ninguna respuesta que mencione el rendimiento. Pasando Func<>s en Where()o Count()es malo. Realmente malo. Si usa un, Func<>entonces llama a las IEnumerablecosas LINQ en lugar de IQueryable, lo que significa que las tablas completas se extraen y luego se filtran. Expression<Func<>>es significativamente más rápido, especialmente si está consultando una base de datos que vive en otro servidor.

mhenry1384
fuente
¿Esto se aplica también a la consulta en memoria?
stt106
@ stt106 Probablemente no.
mhenry1384
Esto solo es cierto si enumera la lista. Si usa GetEnumerator o foreach, no cargará la cantidad total en la memoria.
nelsontruran
1
@ stt106 Cuando se pasa a la cláusula .Where () de una Lista <>, Expression <Func <>> obtiene .Compile (), así que Func <> es casi seguro que es más rápido. Ver referencesource.microsoft.com/#System.Core/System/Linq/…
NStuke