Por que você usaria a expressão <Func <T>> em vez de Func <T>?

949

Eu entendo lambdas e o Funce Actiondelegados. Mas expressões me surpreendem.

Em que circunstâncias você usaria um Expression<Func<T>>velho, e não um velho comum Func<T>?

Richard Nagle
fonte
14
Func <> será convertido em um método no nível compilador c #, Expression <Func <>> será executado no nível MSIL depois de compilar o código diretamente, que é a razão pela qual é mais rápido
Waleed AK
1
além das respostas, a especificação da linguagem csharp "tipos de árvores 4,6 de expressão" é útil para fazer referência cruzada
djeikyb

Respostas:

1133

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 delegateque é 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:

Expressão vs Func Ampliar imagem

Enquanto os dois parecem iguais em tempo de compilação, o que o compilador gera é totalmente diferente .

Mehrdad Afshari
fonte
96
Portanto, em outras palavras, um Expressioncontém as meta-informações sobre um determinado delegado.
BERTL
40
@bertl Na verdade, não. O delegado não está envolvido. O motivo pelo qual existe alguma associação com um delegado é que você pode compilar a expressão para um delegado - ou, para ser mais preciso, compilá-lo em um método e levar o delegado a esse método como um valor de retorno. Mas a própria árvore de expressão é apenas dados. O delegado não existe quando você usa, em Expression<Func<...>>vez de apenas Func<...>.
Luaan 16/06/2015
5
@Kyle Delaney, (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }essa expressão é uma ExpressionTree, ramificações são criadas para a instrução If.
Matteo Marciano - MSCP 20/01
3
@bertl Delegate é o que a CPU vê (código executável de uma arquitetura), Expressão é o que o compilador vê (apenas outro formato de código-fonte, mas ainda assim o código-fonte).
Codewarrior 5/05
5
@bertl: Pode ser resumido com mais precisão dizendo que uma expressão é para um func o que um construtor de string é para uma string. Não é uma string / func, mas contém os dados necessários para criar um quando solicitado.
Flater
337

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 Expressionvez de Funcacabar aqui.

Uma expressão simplesmente transforma um delegado em um dado sobre si mesmo. Isso a => a + 1se 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 Expressione Funcnão é adequado. Funcnã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. Funcnã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 Expressionpara 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.

Chad Hedgcock
fonte
2
Chade; Por favor, explique um pouco mais esse comentário: "O Func não funcionou porque meu DbContext estava cego para o que realmente estava na expressão lambda para transformá-lo em SQL, então ele fez a melhor coisa e iterou essa condição através de cada linha da minha tabela . "
31616 John Peters
2
>> Func ... Tudo o que você pode fazer é executá-lo. Não é exatamente verdade, mas acho que esse é o ponto que deve ser enfatizado. Funções / ações devem ser executadas, expressões devem ser analisadas (antes da execução ou mesmo em vez de execução).
27917 Konstantin
@Chad O problema aqui foi esse ?: db.Set <T> consultou toda a tabela do banco de dados e depois, porque .Where (conditionLambda) usou o método de extensão Where (IEnumerable), que é enumerado em toda a tabela na memória . Acho que você obtém OutOfMemoryException porque, esse código tentou carregar a tabela inteira na memória (e, é claro, criou os objetos). Estou certo? Obrigado :)
Bence Végert
104

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)

LSpencer777
fonte
+ l para explicação. No entanto, recebo 'O tipo de nó de expressão LINQ' Invoke 'não é suportado no LINQ to Entities.' e teve que usar o ForEach depois de buscar os resultados.
usar o seguinte comando
77

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;
  • a árvore de expressão pode ser construída através da sintaxe de expressão lambda ou através da sintaxe da API;
  • a árvore de expressão pode ser compilada para um delegado Func<T>;
  • a conversão inversa é teoricamente possível, mas é uma espécie de descompilação, não há funcionalidade interna para isso, pois não é um processo direto;
  • árvore de expressão pode ser observada / traduzida / modificada através do ExpressionVisitor;
  • os métodos de extensão para IEnumerable operam com Func<T>;
  • os métodos de extensão para IQueryable operam com 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.

Olexander Ivanitskyi
fonte
Boa lista, uma pequena nota é que você mencionou que a conversão inversa é possível, no entanto, uma inversa exata não é. Alguns metadados são perdidos durante o processo de conversão. No entanto, você pode descompilar para uma árvore de Expressão que produz o mesmo resultado quando compilado novamente.
Aidiakapi 13/03/2015
76

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 );

Rico Mariani

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.

Oğuzhan Soykan
fonte
10
Bem colocado. ie Você precisa de expressão quando espera que o seu Func seja convertido em algum tipo de consulta. Ou seja. você precisa 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 Funcno Expressioncapacita o driver para analisar Funce transformá-lo em uma consulta Sql / MongoDb / other.
Chad Hedgcock 26/03
Então, quando eu estou planejando para umas férias, eu usaria Expression, mas quando estou de férias vai ser Func/Action;)
GoldBishop
1
@ChadHedgcock Esta foi a peça final que eu precisava. Obrigado. Eu estive olhando isso por um tempo, e seu comentário aqui fez todo o estudo clicar.
johnny
37

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/ outargs 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 .

Marc Gravell
fonte
20

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.

Andrew Hare
fonte
19

O principal motivo é quando você não deseja executar o código diretamente, mas deseja inspecioná-lo. Isso pode ser por vários motivos:

  • Mapeando o código para um ambiente diferente (ou seja, código C # para SQL no Entity Framework)
  • Substituindo partes do código em tempo de execução (programação dinâmica ou mesmo técnicas DRY simples)
  • Validação de código (muito útil ao emular scripts ou ao fazer análises)
  • Serialização - as expressões podem ser serializadas com bastante facilidade e segurança, os delegados não podem
  • Segurança fortemente tipada em coisas que não são inerentemente tipicamente fortes e exploração de verificações do compilador mesmo que você esteja fazendo chamadas dinâmicas em tempo de execução (o ASP.NET MVC 5 com Razor é um bom exemplo)
Luaan
fonte
você pode elaborar um pouco mais sobre no.5 #
uowzd01
@ uowzd01 Basta olhar para o Razor - ele usa essa abordagem extensivamente.
Luaan 26/10/2015
@Luaan Estou procurando serializações de expressão, mas não consigo encontrar nada sem um uso limitado de terceiros. O .Net 4.5 suporta serialização de árvore de expressão?
vabii
@ vabii Não que eu saiba - e não seria realmente uma boa ideia para o caso geral. Meu argumento era mais sobre você ser capaz de escrever serializações bastante simples para os casos específicos que você deseja suportar, em relação às interfaces projetadas com antecedência - já fiz isso algumas vezes. No caso geral, Expressionpode 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.
Luaan 13/12/19
15

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 IEnumerablematerial 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.

mhenry1384
fonte
Isso também se aplica à consulta na memória?
stt106
@ stt106 Provavelmente não.
precisa saber é o seguinte
Isso só é verdade se você enumerar a lista. Se você usar GetEnumerator ou foreach, não carregará o ienumerable totalmente na memória.
Nelsontruran
1
@ stt106 Quando passada para a cláusula .Where () de uma List <>, a Expressão <Func <>> recebe .Compile (), então Func <> é quase certamente mais rápido. Veja referencesource.microsoft.com/#System.Core/System/Linq/...
NStuke