Entiendo lambdas y la Func
y Action
delegados. Pero las expresiones me sorprenden.
¿En qué circunstancias Expression<Func<T>>
usarías un viejo en lugar de uno viejo Func<T>
?
Entiendo lambdas y la Func
y Action
delegados. Pero las expresiones me sorprenden.
¿En qué circunstancias Expression<Func<T>>
usarías un viejo en lugar de uno viejo Func<T>
?
Respuestas:
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 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 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:
Si bien ambos se ven iguales en tiempo de compilación, lo que genera el compilador es totalmente diferente .
Expression
contiene la metainformación sobre cierto delegado.
Expression<Func<...>>
lugar de solo Func<...>
.
(isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }
tal expresión es un árbol de expresión, se crean ramas para la instrucción If.
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 Expression
lugar de Func
, terminando aquí.
Una expresión simplemente convierte a un delegado en datos sobre sí misma. Entonces se 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, 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 Func
no es adecuado. Func
no 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. Func
no 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 Expression
a 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.
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)
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;Func<T>
;ExpressionVisitor
;Func<T>
;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.
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 );
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.
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 Func
en Expression
empodera el controlador para analizar Func
y convertirlo en una consulta SQL / MongoDb / other.
Expression
pero cuando esté de vacaciones será Func/Action
;)
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
/ out
args 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 .
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.
La razón principal es cuando no desea ejecutar el código directamente, sino que desea inspeccionarlo. Esto puede ser por varias razones:
Expression
puede 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.
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 IEnumerable
cosas 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.