Eu entendo lambdas e o Func
e Action
delegados. Mas expressões me surpreendem.
Em que circunstâncias você usaria um Expression<Func<T>>
velho, e não um velho comum Func<T>
?
Eu entendo lambdas e o Func
e Action
delegados. Mas expressões me surpreendem.
Em que circunstâncias você usaria um Expression<Func<T>>
velho, e não um velho comum Func<T>
?
Respostas:
Quando você deseja tratar expressões lambda como árvores de expressão e olhar dentro delas, em vez de executá-las. Por exemplo, LINQ to SQL obtém a expressão e a converte na instrução SQL equivalente e a envia ao servidor (em vez de executar o lambda).
Conceitualmente, Expression<Func<T>>
é completamente diferente de Func<T>
. Func<T>
denota um delegate
que é praticamente um ponteiro para um método e Expression<Func<T>>
indica uma estrutura de dados em árvore para uma expressão lambda. Essa estrutura em árvore descreve o que uma expressão lambda faz em vez de fazer a coisa real. Ele basicamente contém dados sobre a composição de expressões, variáveis, chamadas de métodos, ... (por exemplo, ele contém informações como essa lambda é uma constante + algum parâmetro). Você pode usar esta descrição para convertê-la em um método real (com Expression.Compile
) ou fazer outras coisas (como o exemplo LINQ to SQL) com ele. O ato de tratar lambdas como métodos anônimos e árvores de expressão é puramente uma coisa de tempo de compilação.
Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }
efetivamente será compilado para um método IL que não obtém nada e retorna 10.
Expression<Func<int>> myExpression = () => 10;
será convertido em uma estrutura de dados que descreve uma expressão que não obtém parâmetros e retorna o valor 10:
Enquanto os dois parecem iguais em tempo de compilação, o que o compilador gera é totalmente diferente .
Expression
contém as meta-informações sobre um determinado delegado.
Expression<Func<...>>
vez de apenas Func<...>
.
(isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }
essa expressão é uma ExpressionTree, ramificações são criadas para a instrução If.
Estou adicionando uma resposta para noobs, porque essas respostas pareceram exageradas, até que percebi como é simples. Às vezes, é sua expectativa de que é complicado que o torna incapaz de 'enrolar a cabeça'.
Não precisei entender a diferença até encontrar um 'bug' realmente irritante, tentando usar o LINQ-to-SQL genericamente:
public IEnumerable<T> Get(Func<T, bool> conditionLambda){
using(var db = new DbContext()){
return db.Set<T>.Where(conditionLambda);
}
}
Isso funcionou muito bem até eu começar a obter OutofMemoryExceptions em conjuntos de dados maiores. Definir pontos de interrupção dentro do lambda me fez perceber que ele estava iterando através de cada linha da minha tabela, um por um, procurando correspondências com minha condição de lambda. Isso me surpreendeu por um tempo, porque por que diabos está tratando minha tabela de dados como um IEnumerable gigante em vez de fazer LINQ-to-SQL como deveria? Ele também estava fazendo exatamente a mesma coisa no meu equivalente de LINQ para MongoDb.
A correção foi simplesmente transformar Func<T, bool>
-se Expression<Func<T, bool>>
, então eu pesquisei por que ele precisa de um, em Expression
vez de Func
acabar aqui.
Uma expressão simplesmente transforma um delegado em um dado sobre si mesmo. Isso a => a + 1
se torna algo como "No lado esquerdo, há um int a
. No lado direito, você adiciona 1 a ele". É isso aí. Você pode ir para casa agora. É obviamente mais estruturado do que isso, mas isso é basicamente tudo o que uma árvore de expressão realmente é - nada para se entender.
Entendendo isso, fica claro por que o LINQ-to-SQL precisa de Expression
e Func
não é adequado. Func
não carrega consigo uma maneira de entrar em si mesmo, de ver o âmago da questão de como convertê-lo em uma consulta SQL / MongoDb / other. Você não pode ver se está fazendo adição, multiplicação ou subtração. Tudo o que você pode fazer é executá-lo. Expression
, por outro lado, permite que você olhe dentro do delegado e veja tudo o que ele deseja fazer. Isso permite que você traduza o delegado para o que quiser, como uma consulta SQL. Func
não funcionou porque meu DbContext estava cego para o conteúdo da expressão lambda. Por isso, não foi possível transformar a expressão lambda em SQL; no entanto, ele fez a melhor coisa seguinte e iterou essa condição através de cada linha da minha tabela.
Edit: expondo minha última frase a pedido de John Peter:
IQueryable estende IEnumerable, então os métodos de IEnumerable, como Where()
obter sobrecargas que aceitam Expression
. Quando você passa um Expression
para isso, mantém um IQueryable como resultado, mas quando passa um Func
, você volta ao IEnumerable base e obtém um IEnumerable como resultado. Em outras palavras, sem perceber, você transformou seu conjunto de dados em uma lista para iteração, em vez de algo a ser consultado. É difícil notar uma diferença até que você realmente veja as assinaturas.
Uma consideração extremamente importante na escolha do Expression vs Func é que os provedores IQueryable, como LINQ to Entities, podem 'digerir' o que você passa em uma Expressão, mas ignoram o que você passa em um Func. Eu tenho duas postagens de blog sobre o assunto:
Mais sobre Expressão vs Func com Entity Framework e Apaixonar-se pelo LINQ - Parte 7: Expressões e Funcs (a última seção)
Gostaria de adicionar algumas notas sobre as diferenças entre Func<T>
e Expression<Func<T>>
:
Func<T>
é apenas um MulticastDelegate à moda antiga;Expression<Func<T>>
é uma representação da expressão lambda na forma de árvore de expressão;Func<T>
;ExpressionVisitor
;Func<T>
;Expression<Func<T>>
.Há um artigo que descreve os detalhes com exemplos de código:
LINQ: Func <T> vs. Expressão <Func <T>> .
Espero que seja útil.
Há uma explicação mais filosófica sobre o assunto no livro de Krzysztof Cwalina ( Diretrizes de design de estrutura: convenções, expressões idiomáticas e padrões para bibliotecas .NET reutilizáveis );
Edite para versão sem imagem:
Na maioria das vezes, você vai querer Func ou Action se tudo o que precisa acontecer é executar algum código. Você precisa do Expression quando o código precisar ser analisado, serializado ou otimizado antes de ser executado. Expressão é para pensar em código, Func / Action é para executá-lo.
database.data.Where(i => i.Id > 0)
ser executado como SELECT FROM [data] WHERE [id] > 0
. Se você acabou de passar em um Func, você colocou antolhos do seu driver e tudo o que pode fazer é SELECT *
e, em seguida, uma vez que é carregado todos os dados na memória, iterate através de cada e filtrar tudo com id> 0. Envolvendo o seu Func
no Expression
capacita o driver para analisar Func
e transformá-lo em uma consulta Sql / MongoDb / other.
Expression
, mas quando estou de férias vai ser Func/Action
;)
LINQ é o exemplo canônico (por exemplo, conversando com um banco de dados), mas, na verdade, sempre que você se preocupa mais em expressar o que fazer, em vez de realmente fazê-lo. Por exemplo, eu uso essa abordagem na pilha RPC do protobuf-net (para evitar a geração de código etc.) - então você chama um método com:
string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));
Isso desconstrói a árvore de expressão a ser resolvida SomeMethod
(e o valor de cada argumento), executa a chamada RPC, atualiza qualquer ref
/ out
args e retorna o resultado da chamada remota. Isso só é possível através da árvore de expressão. Eu cubro isso mais aqui .
Outro exemplo é quando você está construindo as árvores de expressão manualmente com o objetivo de compilar em um lambda, conforme feito pelo código genérico de operadores .
Você usaria uma expressão quando quiser tratar sua função como dados e não como código. Você pode fazer isso se desejar manipular o código (como dados). Na maioria das vezes, se você não vê necessidade de expressões, provavelmente não precisa usar uma.
O principal motivo é quando você não deseja executar o código diretamente, mas deseja inspecioná-lo. Isso pode ser por vários motivos:
Expression
pode ser tão impossível serializar quanto um delegado, pois qualquer expressão pode conter uma invocação de uma referência arbitrária de delegado / método. "Fácil" é relativo, é claro.
Ainda não vejo respostas que mencionem desempenho. Passar Func<>
s para Where()
ou Count()
é ruim. Muito ruim. Se você usar um Func<>
, em seguida, ele chama o IEnumerable
material LINQ em vez de IQueryable
, o que significa que as tabelas inteiras são atraídas e , em seguida, filtrada. Expression<Func<>>
é significativamente mais rápido, especialmente se você estiver consultando um banco de dados que mora em outro servidor.