Setembro de 2019

Volume 34 – Número 9

[Desenvolvimento .NET]

Árvores de Expressão no Visual Basic e em C#

Por Zev Spitz

Imagine se você tivesse objetos que representassem partes de um programa. Seu código poderia combinar essas partes de várias maneiras, criando um objeto que representasse um novo programa. Você poderia compilar esse objeto em um novo programa e executá-lo. Seu programa poderia até mesmo reescrever a própria lógica. Você poderia passar esse objeto para um ambiente diferente, em que as partes individuais seriam analisadas como um conjunto de instruções a serem executadas nesse outro ambiente.

Bem-vindo às árvores de expressão. Este artigo busca fornecer uma visão geral de alto nível das árvores de expressão, do uso delas na modelagem de operações de código e na compilação de código de runtime e de como criá-las e transformá-las.

Uma observação sobre a linguagem Para muitos recursos de linguagem em Visual Basic e C#, a sintaxe é, na verdade, apenas uma fina camada sobre a parte principal, que são os métodos e tipos do .NET independentes de linguagem. Por exemplo, tanto o loop foreach do C# quanto o constructo For Each do Visual Basic chamam o método GetEnumerator do tipo que implementa IEnumerable. A sintaxe de palavra-chave do LINQ é resolvida em métodos como Select e Where, com a assinatura apropriada. Uma chamada para IDisposable.Dispose encapsulada no tratamento de erro é gerada toda vez que você escreve Using no Visual Basic ou um bloco Using no C#.

Isso também é verdadeiro em árvores de expressão. O Visual Basic e o C# têm suporte sintático para criar árvores de expressão e podem usar a infraestrutura do .NET (no namespace System.Linq.Expressions) para trabalhar com elas. Assim, os conceitos descritos aqui se aplicam igualmente bem às duas linguagens. Independentemente de sua linguagem de desenvolvimento principal ser C# ou Visual Basic, acredito que este artigo terá algum valor para você.

Expressões, árvores de expressão e os tipos em System.Linq.Expressions

Uma expressão no Visual Basic e no C# é um trecho de código que retorna um valor quando avaliado, por exemplo:

42
"abcd"
True
n

As expressões podem ser compostas por outras expressões, assim como:

x + y
"abcd".Length < 5 * 2

Elas formam uma árvore de expressões ou uma árvore de expressão. Considere n + 42 = 27 em Visual Basic ou n + 42 == 27 em C#, o que pode ser separado em partes, conforme mostrado na Figura 1.

Breakdown of an Expression Tree
Figura 1 Detalhamento de uma árvore de expressão

O .NET fornece um conjunto de tipos no namespace System.Linq.Expressions para construir estruturas de dados que representam árvores de expressão. Por exemplo, n + 42 = 27 pode ser representado com objetos e propriedades, conforme mostrado na Figura 2. (Observe que esse código não será compilado – esses tipos não têm construtores públicos e são imutáveis, portanto, não há inicializadores de objeto.)

Figura 2 Objetos de árvore de expressão usando notação de objeto

New BinaryExpression With {
  .NodeType = ExpressionType.Equal,
  .Left = New BinaryExpression With {
    .NodeType = ExpressionType.Add,
    .Left = New ParameterExpression With {
      .Name = "n"
    },
    .Right = New ConstantExpression With {
      .Value = 42
    }
  },
  .Right = New ConstantExpression With {
    .Value = 27
  }
}
new BinaryExpression {
  NodeType = ExpressionType.Equal,
  Left = new BinaryExpression {
    NodeType = ExpressionType.Add,
    Left = new ParameterExpression {
      Name = "n"
    },
    Right = new ConstantExpression {
      Value = 42
    }
  },
  Right = new ConstantExpression {
    Value = 27
  }
}

O termo árvore de expressão no .NET é usado para árvores de expressão sintática (x + y) e para objetos de árvore de expressão (uma instância de BinaryExpression).

Para entender o que um determinado nó na árvore de expressão representa, o tipo do objeto de nó – BinaryExpression, ConstantExpression, ParameterExpression – descreve apenas a forma da expressão. Por exemplo, o tipo BinaryExpression me informa que a expressão representada tem dois operandos, mas não informa qual operação combina os operandos. Os operandos são adicionados juntos, multiplicados juntos ou comparados numericamente? Essa informação está contida na propriedade NodeType, definida na classe base Expression. (Observe que alguns tipos de nó não representam operações de código – confira "Tipos de nó meta".)

Tipos de nó meta

A maioria dos tipos de nó representa operações de código. No entanto, há três tipos de nó “meta” – tipos de nó que fornecem informações sobre a árvore, mas não são mapeados diretamente para o código.

ExpressionType.Quote Esse tipo de nó sempre encapsula uma LambdaExpression e especifica que a LambdaExpression define uma nova árvore de expressão, não um delegado. Por exemplo, a árvore gerada com base no seguinte código do Visual Basic:

Dim expr As Expression(Of Func(Of Func(Of Boolean))) = Function() Function() True

ou o seguinte código C#:

Expression<Func<Func<bool>>> expr = () => () => true;

representa um delegado que produz outro delegado. Se deseja representar um delegado que produz outra árvore de expressão, você precisa encapsular o interno em um Quotenode. O compilador faz isso automaticamente com o seguinte código do Visual Basic:

Dim expr As Expression(Of Func(Of Expression(Of Func(Of Boolean)))) =
  Function() Function() True

Ou o seguinte código C#:

 

Expression<Func<Expression<Func<bool>>>> expr = () => () => true;

ExpressionType.DebugInfo Esse nó emite informações de depuração, então quando você depura expressões compiladas, a IL em um ponto específico pode ser mapeada para o lugar certo em seu código-fonte.

ExpressionType.RuntimeVariablesExpression Considere o objeto arguments em ES3. Ele está disponível em uma função sem ter sido declarado como uma variável explícita. O Python expõe uma função local, que retorna um dicionário de variáveis definidas no namespace local. Essas variáveis "virtuais" são descritas dentro de uma árvore de expressão pelo uso da RuntimeVariablesExpression.

Uma propriedade adicional digna de menção em Expression é a propriedade Type. Tanto no Visual Basic quanto no C#, cada expressão tem um tipo – a adição de dois valores Integer produzirá um Integer, ao passo que a adição de dois Double produzirá um Double. A propriedade Type retorna o System.Type da expressão.

Ao visualizar a estrutura de uma árvore de expressão, é útil se concentrar nessas duas propriedades, conforme ilustrado na Figura 3.

NodeType, Type, Value and Name Properties
Figura 3 Propriedades NodeType, Type, Value e Name

Se não há nenhum construtor público para esses tipos, como criá-los? Há duas opções. O compilador poderá gerá-los para você se você escrever a sintaxe lambda em que um tipo de expressão é esperado. Ou você pode usar os métodos de fábrica compartilhados (estáticos em C#) na classe Expression.

Como construir objetos de árvore de expressão I: Como usar o compilador

A maneira simples de construir esses objetos é fazer com que o compilador os gere para você, conforme mencionado anteriormente, usando a sintaxe lambda em qualquer lugar em que uma Expression(Of TDelegate) (Expression<TDelegate> em C#) é esperada: seja por atribuição a uma variável tipada ou como um argumento para um parâmetro tipado. A sintaxe lambda é introduzida no Visual Basic usando as palavras-chave Function ou Sub, seguidas por uma lista de parâmetros e um corpo de método. No C#, o operador => indica a sintaxe lambda.

A sintaxe lambda que define uma árvore de expressão é chamada de expressão lambda (em oposição à instrução lambda) e tem aparência semelhante a esta em C#:

Expression<Func<Integer, String>> expr = i => i.ToString();
IQueryable<Person> personSource = ...
var qry = qry.Select(x => x.LastName);

E a esta em Visual Basic:

Dim expr As Expression(Of Func(Of Integer, String))  = Function(i) i.ToString
Dim personSource As IQueryable(Of Person) = ...
Dim qry = qry.Select(Function(x) x.LastName)

Produzir uma árvore de expressão desse modo tem algumas limitações:

  • Limitado pela sintaxe lambda
  • É compatível apenas com expressões lambda de linha única, sem lambdas de várias linhas
  • Lambdas de expressão só podem conter expressões, não instruções como If...Then ou Try...Catch
  • Inexistência de associação tardia (ou dinâmica em C#)
  • No C#, nenhum argumento nomeado nem argumentos opcionais omitidos
  • Nenhum operador de propagação de nulo

A estrutura completa da árvore de expressão construída – ou seja, tipos de nós, tipos de subexpressão e resolução de sobrecarga de métodos – é determinada no tempo de compilação. Já que as árvores de expressão são imutáveis, a estrutura não pode ser modificada depois de criada.

Observe também que as árvores geradas pelo compilador imitam o comportamento dele, de modo que a árvore gerada pode ser diferente do que você esperaria, dado o código original (Figura 4). Alguns exemplos:

Compiler-Generated Trees vs. Source Code
Figura 4 Árvores geradas pelo compilador vs. código-fonte

Variáveis envolvidas Para referenciar o valor atual de variáveis ao entrar e sair de expressões lambda, o compilador cria uma classe oculta cujos membros correspondem a cada uma das variáveis referenciadas. A função da expressão lambda torna-se um método da classe (confira "Variáveis envolvidas"). A árvore de expressão correspondente renderizará variáveis envolvidas como nós MemberAccess na instância oculta. No Visual Basic, o prefixo $VB$Local_ também será acrescentado ao nome da variável.

Variáveis envolvidas

Quando uma expressão lambda referencia uma variável definida fora dela, dizemos que a expressão lambda "envolve" a variável. O compilador tem que dar uma atenção especial a essa variável, pois a expressão lambda poderá ser usada depois que o valor da variável tiver sido alterado e será esperado que a expressão lambda referencie o novo valor. Ou vice-versa, a expressão lambda pode alterar o valor e essa alteração deve ser visível fora da expressão lambda. Por exemplo, no código C# a seguir:

var i = 5;
Action lmbd = () => Console.WriteLine(i);
i = 6;
lmbd();

ou no código do Visual Basic a seguir:

Dim i = 5
Dim lmbd = Sub() Console.WriteLine(i)
i = 6
lmbd()

a saída esperada seria 6, não 5, porque a expressão lambda deve usar o valor de i no ponto em que a expressão lambda é invocada, depois de ter sido definida como 6.

O compilador C# realiza isso criando uma classe oculta com as variáveis necessárias como campos da classe e as expressões lambda como métodos na classe. Em seguida, o compilador substitui todas as referências a essa variável com acesso de membro na instância de classe. A instância de classe não é alterada, mas o valor dos campos dela pode ser. A IL resultante tem aparência semelhante à seguinte em C#:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0 {
  public int i;
  internal void <Main>b__0() => Console.WriteLine(i);
}
var @object = new <>c__DisplayClass0_0();
@object.i = 5;
Action lmbd = @object.<Main>b__0;
@object.i = 6;
lmbd();

O compilador do Visual Basic faz algo semelhante, com uma diferença: $VB$Local é anexado ao nome da propriedade, desta forma:

 

<CompilerGenerated> Friend NotInheritable Class _Closure$__0-0
  Public $VB$Local_i As Integer
  Sub _Lambda$__0()
    Console.WriteLine($VB$Local_i)
  End Sub
End Class
Dim targetObject = New _Closure$__0-0 targetObject With { .$VB$Local_i = 5 }
Dim lmbd = AddressOf targetObject. _Lambda$__0
targetObject.i = 6
lmbd()

Operador NameOf O resultado do operador NameOf é renderizado como um valor de cadeia de caracteres constante.

Interpolação de cadeia de caracteres e conversão boxing Isso é resolvido em uma chamada para String.Format e uma cadeia de caracteres de formato constante. Já que a expressão de interpolação é digitada como um tipo de valor – Date (o alias do Visual Basic para DateTime) – enquanto o parâmetro correspondente de String.Format está esperando um objeto, o compilador também encapsula a expressão de interpolação com um nó Converter para Object.

Chamadas de método de extensão Elas são, na verdade, chamadas para métodos no nível do módulo (métodos estáticos em C#) e são renderizadas como tal em árvores de expressão. Os métodos no nível do módulo e compartilhados não têm uma instância, portanto, a propriedade Object do MethodCallExpression correspondente retornará Nothing (ou nulo). Se MethodCallExpression representar uma chamada de método de instância, a propriedade Object não será Nothing.

A maneira como os métodos de extensão são representados em árvores de expressão realça uma diferença importante entre árvores de expressão e árvores de sintaxe de compilador Roslyn, que preservam a sintaxe como está. As árvores de expressão se concentram menos na sintaxe precisa e mais nas operações subjacentes. A árvore de sintaxe Roslyn para uma chamada de método de extensão teria a mesma aparência de uma chamada de método de instância padrão, não uma chamada de método compartilhada ou estática.

Conversões Quando o tipo de uma expressão não corresponde ao tipo esperado e há uma conversão implícita do tipo da expressão, o compilador encapsula a expressão interna em um nó Converter para o tipo esperado. O compilador do Visual Basic fará o mesmo ao gerar árvores de expressão com uma expressão cujo tipo implementa ou herda do tipo esperado. Por exemplo, na Figura 4, o método de extensão Count espera um IEnumerable(Of Char), mas o tipo real da expressão é String.

Como construir objetos de árvore de expressão II: Como usar os métodos de fábrica

Você também pode construir árvores de expressão usando os métodos de fábrica compartilhados (estáticos em C#) em System.Linq.Expressions.Expression. Por exemplo, para construir os objetos de árvore de expressão para i.ToString, em que i é um inteiro no Visual Basic (ou um int em C#), use código semelhante ao seguinte:

' Imports System.Linq.Expressions.Expression
Dim prm As ParameterExpression = Parameter(GetType(Integer), "i")
Dim expr As Expression = [Call](
  prm,
  GetType(Integer).GetMethods("ToString", {})
)

Em C#, o código deverá ter uma aparência semelhante a esta:

// Using static System.Linq.Expressions.Expression
ParameterExpression prm = Parameter(typeof(int), "i");
Expression expr = Call(
  prm,
  typeof(int).GetMethods("ToString", new [] {})
);

Enquanto a criação de árvores de expressão desta maneira normalmente exige uma boa dose de reflexão e várias chamadas aos métodos de fábrica, você tem mais flexibilidade para personalizar a árvore de expressão necessária, exatamente do modo que precisa. Com a sintaxe do compilador, você precisaria escrever todas as variações possíveis da árvore de expressão que seu programa poderia eventualmente vir a precisar.

Além disso, a API do método de fábrica permite que você crie alguns tipos de expressões que atualmente não são compatíveis com as expressões geradas pelo compilador. Alguns exemplos:

Instruções como um System.Void retornando ConditionalExpression, que corresponde a If..Then e If..Then..Else..End If (conhecido em C# como if (...) { ...} else { ... }); ou então uma TryCatchExpression representando um bloco Try..Catch ou try { ... } catch (...) { ... }.

Assignments Dim x As Integer: x = 17.

Blocos para agrupar várias instruções juntas.

Por exemplo, considere o seguinte código do Visual Basic:

Dim msg As String = "Hello!"
If DateTime.Now.Hour > 18 Then msg = "Good night"
Console.WriteLine(msg)

ou o seguinte código equivalente em C#:

string msg = "Hello";
if (DateTime.Now.Hour > 18) {
  msg = "Good night";
}
Console.WriteLine(msg);

Você pode construir uma árvore de expressão correspondente no Visual Basic ou C# usando os métodos de fábrica, conforme mostra a Figura 5.

Figura 5 Blocos, atribuições e instruções em árvores de expressão

' Imports System.Linq.Expressions.Expression
Dim msg = Parameter(GetType(String), "msg")
Dim body = Block(
  Assign(msg, Constant("Hello")),
  IfThen(
    GreaterThan(
      MakeMemberAccess(
        MakeMemberAccess(
          Nothing,
          GetType(DateTime).GetMember("Now").Single
        ),
        GetType(DateTime).GetMember("Hour").Single
      ),
      Constant(18)
    ),
    Assign(msg, Constant("Good night"))
  ),
  [Call](
    GetType(Console).GetMethod("WriteLine", { GetType(string) }),
    msg
  )
)
// Using static System.Linq.Expressions.Expression
var msg = Parameter(typeof(string), "msg");
var expr = Lambda(
  Block(
    Assign(msg, Constant("Hello")),
    IfThen(
      GreaterThan(
        MakeMemberAccess(
          MakeMemberAccess(
            null,
            typeof(DateTime).GetMember("Now").Single()
          ),
          typeof(DateTime).GetMember("Hour").Single()
        ),
        Constant(18)
      ),
      Assign(msg, Constant("Good night"))
    ),
    Call(
      typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }),
      msg
    )
  )
);

Como usar árvores de expressão I: Como mapear constructos de código para APIs externas

As árvores de expressão foram originalmente projetadas para habilitar o mapeamento de sintaxe de C# ou Visual Basic em uma API diferente. O caso de uso clássico é a geração de uma instrução SQL, assim como esta:

SELECT * FROM Persons WHERE Persons.LastName LIKE N'D%'

Essa instrução pode ser derivada de código como o snippet a seguir, que usa acesso de membro (operador .), chamadas de método dentro da expressão e o método Queryable.Where. Aqui está o código no Visual Basic:

Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(Function(x) x.LastName.StartsWith("D"))

e aqui está o código em C#:

IQueryable<Person> personSource = ...
var qry = personSource.Where(x => x.LastName.StartsWith("D");

Como isso funciona? Há duas sobrecargas que poderiam ser usadas com a expressão lambda – Enumerable.Where e Queryable.Where. No entanto, a resolução de sobrecarga prefere a sobrecarga na qual a expressão lambda é uma expressão lambda – ou seja, Queryable.Where – em detrimento da sobrecarga que usa um delegado. Em seguida, o compilador substitui a sintaxe lambda por chamadas para os métodos de fábrica apropriados.

No runtime, o método Queryable.Where encapsula a árvore de expressão passada com um nó de chamada cuja propriedade Method referencia Queryable.Where e usa dois parâmetros – personSource e a árvore de expressão da sintaxe lambda (Figura 6). (O nó Quote indica que a árvore de expressão interna está sendo passada para Queryable.Where como uma árvore de expressão e não como um delegado.)

Visualization of Final Expression Tree
Figura 6 Visualização da árvore de expressão final

Um provedor de banco de dados LINQ (tal como Entity Framework, LINQ2SQL ou NHibernate) pode usar essa árvore de expressão e mapear as diferentes partes para a instrução SQL no início desta seção. Veja como:

  • ExpresssionType.Call para Queryable.Where é analisado como uma cláusula WHERE do SQL
  • ExpressionType.MemberAccess de LastName em uma instância de Person passa a ler o campo LastName na tabela Persons – Persons.LastName
  • ExpressionType.Call para o método StartsWith com um argumento constante é convertido no operador LIKE do SQL, mediante um padrão que corresponde ao início de uma cadeia de caracteres constante:
LIKE N'D%'

Portanto, é possível controlar APIs externas usando convenções e constructos de código, com todos os benefícios do compilador – segurança de tipos e sintaxe correta nas várias partes da árvore de expressão, bem como preenchimento automático do IDE. Alguns outros exemplos:

Como criar solicitações da Web Usando a biblioteca Simple.OData.Client (bit.ly/2YyDrsx), você pode criar solicitações OData ao passar as árvores de expressão para os diversos métodos. A biblioteca produzirá a solicitação correta (a Figura 7 mostra o código para Visual Basic e para C#).

Figura 7 Solicitações usando a biblioteca Simple.OData.Client e árvores de expressão

Dim client = New ODataClient("https://services.odata.org/v4/TripPinServiceRW/")
Dim people = Await client.For(Of People)
  .Filter(Function(x) x.Trips.Any(Function(y) y.Budget > 3000))
  .Top(2)
  .Select(Function(x) New With { x.FirstName, x.LastName})
  .FindEntriesAsync
var client = new ODataClient("https://services.odata.org/v4/TripPinServiceRW/");
var people = await client.For<People>()
  .Filter(x => x.Trips.Any(y => y.Budget > 3000))
  .Top(2)
  .Select(x => new {x.FirstName, x.LastName})
  .FindEntriesAsync();

Solicitação de saída:

 

> https://services.odata.org/v4/TripPinServiceRW/People?$top=2 &amp;
  $select=FirstName, LastName &amp; $filter=Trips/any(d:d/Budget gt 3000)

Reflexão por exemplo Em vez de usar a reflexão para obter um MethodInfo, você pode escrever uma chamada de método dentro de uma expressão e passar essa expressão para uma função que extraia a sobrecarga específica usada na chamada. Aqui está o código de reflexão, primeiro no Visual Basic:

Dim writeLine as MethodInfo = GetType(Console).GetMethod(
  "WriteLine", { GetType(String) })

E o mesmo código em C#:

MethodInfo writeLine = typeof(Console).GetMethod(
  "WriteLine", new [] { typeof(string) });

Agora, veja como essa função e o respectivo uso podem ser vistos no Visual Basic:

Function GetMethod(expr As Expression(Of Action)) As MethodInfo
  Return CType(expr.Body, MethodCallExpression).Method
End Function
Dim mi As MethodInfo = GetMethod(Sub() Console.WriteLine(""))

e veja que aparência ela teria em C#:

public static MethodInfo GetMethod(Expression<Action> expr) =>
  (expr.Body as MethodCallExpression).Method;
MethodInfo mi = GetMethod(() => Console.WriteLine(""));

Essa abordagem também simplifica a obtenção de métodos de extensão e a construção de métodos genéricos fechados, conforme mostrado aqui no Visual Basic:

Dim wherePerson As MethodInfo = GetMethod(Sub() CType(Nothing, IQueryable(Of
  Person)).Where(Function(x) True)))

Ela também fornece uma garantia de tempo de compilação de que o método e a sobrecarga existem, conforme mostrado aqui em C#:

 

// Won’t compile, because GetMethod expects Expression<Action>, not Expression<Func<..>>
MethodInfo getMethod = GetMethod(() => GetMethod(() => null));

Configuração de coluna de grade Se você tiver algum tipo de interface do usuário de grade e desejar permitir a definição das colunas de modo declarativo, poderá ter um método que cria as colunas com base em subexpressões em um literal de matriz (usando Visual Basic):

grid.SetColumns(Function(x As Person) {x.LastName, x.FirstName, x.DateOfBirth})

Ou nas subexpressões em um tipo anônimo (em C#):

grid.SetColumns((Person x) => new {x.LastName, x.FirstName, DOB = x.DateOfBirth});

Como usar árvores de expressão II: Como compilar código invocável em runtime

O segundo caso de uso principal para árvores de expressão é gerar código executável em runtime. Lembra-se da variável body vista anteriormente? É possível encapsulá-la em uma LambdaExpression, compilá-la em um delegado e invocar o delegado, tudo em runtime. O código teria esta aparência no Visual Basic:

Dim lambdaExpression = Lambda(Of Action)(body)
Dim compiled = lambdaExpression.Compile
compiled.Invoke
' prints either "Hello" or "Good night"

e esta em C#:

var lambdaExpression = Lambda<Action>(body);
var compiled = lambdaExpression.Compile();
compiled.Invoke();
// Prints either "Hello" or "Good night"

A compilação de uma árvore de expressão para código executável é muito útil para implementar outras linguagens sobre o CLR, pois é muito mais fácil trabalhar com árvores de expressão por manipulação direta de IL. Mas se você estiver programando em C# ou em Visual Basic e souber a lógica do programa no tempo de compilação, por que não inserir essa lógica em seu método ou assembly existente no tempo de compilação, em vez de compilar no runtime?

No entanto, a compilação em runtime realmente se destaca quando você não conhece o melhor caminho ou o algoritmo certo no tempo de design. Ao usar árvores de expressão e compilação em runtime, é relativamente fácil reescrever de modo iterativo e refinar a lógica do programa em runtime em resposta a condições reais ou dados do campo.

Código que se reescreve sozinho: Cache de site de chamada em linguagens tipadas dinamicamente Como um exemplo, considere o cache de um site de chamada no DLR (Dynamic Language Runtime), que permite a tipagem dinâmica em implementações de linguagem destinadas ao CLR. Ele usa árvores de expressão para fornecer uma otimização avançada, reescrevendo iterativamente o delegado atribuído a um site de chamada específico quando necessário.

Tanto o C# quanto o Visual Basic são (na maior parte das vezes) linguagens tipadas estaticamente – para cada expressão na linguagem, podemos resolver um tipo fixo que não é alterado durante o tempo de vida do programa. Em outras palavras, se as variáveis x e y tiverem sido declaradas como um inteiro (ou um int em C#) e o programa contiver uma linha de código x + y, a resolução do valor dessa expressão sempre usará a instrução "add" para dois inteiros.

No entanto, linguagens tipadas dinamicamente não têm tal garantia. Geralmente, x e y não têm nenhum tipo inerente, então a avaliação de x + y deve levar em consideração que x e y podem ser de qualquer tipo, por exemplo, String, e resolver x + y nesse caso significaria usar String.Concat. Por outro lado, se na primeira vez em que o programa atingir a expressão x e y forem inteiros, será altamente provável que as ocorrências sucessivas também tenham os mesmos tipos de x e y. O DLR aproveita esse fato com o cache de site de chamada, que usa árvores de expressão para reescrever o delegado do site de chamada sempre que um novo tipo é encontrado.

Cada site de chamada obtém uma instância de CallSite(Of T) (ou, em C#, CallSite<T>) com uma propriedade Target que aponta para um delegado compilado. Cada site também obtém um conjunto de testes, juntamente com as ações que devem ser executadas quando cada teste é executado com sucesso. Inicialmente, o delegado Target tem apenas o código para atualizar a si mesmo, da seguinte forma:

‘ Visual Basic code representation of Target delegate
Return site.Update(site, x, y)

Na primeira iteração, o método Update recuperará um teste e uma ação aplicáveis da implementação da linguagem (por exemplo, "se ambos os argumentos forem inteiros, usar a instrução 'add'"). Em seguida, ele gerará uma árvore de expressão que executará a ação somente se o teste tiver sucesso. Um equivalente de código da árvore de expressão resultante teria aparência semelhante a esta:

‘ Visual Basic code representation of expression tree
If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y)
Return site.Update(site, x, y)

A árvore de expressão será então compilada em um novo delegado e armazenada na propriedade Target, enquanto o teste e a ação serão armazenados no objeto do site de chamada.

Em iterações posteriores, o site de chamada usará o novo delegado para resolver x + y. No novo delegado, se o teste tiver êxito, a operação do CLR resolvida será usada. Somente se os testes falharem (nesse caso, se x ou y não for um número inteiro), o método Update precisará voltar à implementação da linguagem. Mas quando o método Update for chamado, ele adicionará o novo teste e a ação e recompilará o delegado Target para que eles sejam contabilizados. Nesse ponto, o delegado Target conterá testes para todos os pares de tipos encontrados anteriormente e a estratégia de resolução de valor para cada tipo, conforme mostrado no código a seguir:

If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y)
If TypeOf x Is String Then Return String.Concat(x, y)
Return site.Update(site, x, y)

Fazer com que esse código de runtime se reescrevesse em resposta a fatos reais seria muito difícil – ou até impossível – sem a capacidade de compilar código de uma árvore de expressão em runtime.

Compilação dinâmica com árvores de expressão versus com Roslyn Usando o Roslyn, também é possível compilar dinamicamente o código com certa facilidade com base em cadeias de caracteres simples, em vez de uma árvore de expressão. Na verdade, essa abordagem é até preferida se você tem pouca experiência em sintaxe de Visual Basic ou C# ou se é importante preservar a sintaxe do Visual Basic ou do C# que você gerou. Como observado anteriormente, as árvores de sintaxe do Roslyn modelam a sintaxe, enquanto as árvores de expressão apenas representam operações de código sem considerar a sintaxe.

Além disso, se você tentar construir uma árvore de expressão inválida, obterá apenas uma única exceção. Ao analisar e compilar cadeias de caracteres para código executável usando o Roslyn, você pode obter várias partes de informações de diagnóstico em diferentes partes da compilação, assim como faria ao escrever em C# ou no Visual Basic no Visual Studio.

Por outro lado, o Roslyn é uma dependência grande e complicada para ser adicionada ao seu projeto. Talvez você já tenha um conjunto de operações de código provenientes de uma origem que não seja código-fonte Visual Basic ou C#, então pode ser desnecessário reescrever no modelo semântico do Roslyn. Além disso, lembre-se que o Roslyn requer multithreading e não poderá ser usado se novos threads não forem permitidos (assim como em um visualizador de depuração do Visual Studio).

Como reescrever árvores de expressão: Como implementar o operador Like do Visual Basic

Mencionei que as árvores de expressão são imutáveis. No entanto, você pode criar uma nova árvore de expressão que reutiliza partes da original. Vamos imaginar que você queira consultar um banco de dados em busca de pessoas cujo nome contenha um "e" e um "i" subsequente. O Visual Basic tem um operador Like, que retornará True se uma cadeia de caracteres corresponder a um padrão, conforme mostrado aqui:

Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(Function(x) x.FirstName Like "*e*i*")
For Each person In qry
  Console.WriteLine($"LastName: {person.LastName}, FirstName: {person.FirstName}")
Next

Mas se você tentar fazer isso em um Entity Framework 6 DbContext, receberá uma exceção com a seguinte mensagem:

'O LINQ to Entities não reconhece o método 'Boolean LikeString(System.String, System.String, Microsoft.VisualBasic.CompareMethod)', e esse método não pode ser convertido em uma expressão de repositório.'

O operador Like do Visual Basic resolve para o método LikeOperator.Like-String (no namespace Microsoft.VisualBasic.CompilerServices), que o EF6 não é capaz de converter em uma expressão LIKE do SQL. Daí o erro.

Agora, o EF6 dá suporte a uma funcionalidade semelhante por meio do método DbFunctions.Like, que o EF6 pode mapear para o LIKE correspondente. É necessário substituir a árvore de expressão original, que usa o Like do Visual Basic, por uma que usa DbFunctions.Like, mas sem alterar nenhuma outra parte da árvore. O modo mais comum de fazer isso é herdar da classe ExpressionVisitor do .NET e substituir os métodos base Visit* de interesse. No meu caso, já que desejo substituir uma chamada de método, substituirei VisitMethodCall, conforme mostrado na Figura 8.

Figura 8 ExpressionTreeVisitor substituindo o Like do Visual Basic por DbFunctions.Like

Class LikeVisitor
  Inherits ExpressionVisitor
  Shared LikeString As MethodInfo =
    GetType(CompilerServices.LikeOperator).GetMethod("LikeString")
  Shared DbFunctionsLike As MethodInfo = GetType(DbFunctions).GetMethod(
    "Like", {GetType(String), GetType(String)})
  Protected Overrides Function VisitMethodCall(
    node As MethodCallExpression) As Expression
    ' Is this node using the LikeString method? If not, leave it alone.
    If node.Method <> LikeString Then Return MyBase.VisitMethodCall(node)
    Dim patternExpression = node.Arguments(1)
    If patternExpression.NodeType = ExpressionType.Constant Then
      Dim oldPattern =
        CType(CType(patternExpression, ConstantExpression).Value, String)
      ' partial mapping of Visual Basic's Like syntax to SQL LIKE syntax
      Dim newPattern = oldPattern.Replace("*", "%")
      patternExpression = Constant(newPattern)
    End If
    Return [Call](DbFunctionsLike,
      node.Arguments(0),
      patternExpression
    )
  End Function
End Class

A sintaxe do padrão do operador Like é diferente daquela do LIKE do SQL, portanto, substituirei os caracteres especiais usados no Like do Visual Basic por aqueles correspondentes usados pelo LIKE do SQL. (Este mapeamento está incompleto – ele não mapeia toda a sintaxe de padrão do Like do Visual Basic e não coloca os caracteres especiais do LIKE do SQL entre caracteres de escape, nem remove os caracteres de escape dos caracteres especiais do Like do Visual Basic. Uma implementação completa pode ser encontrada no GitHub em bit.ly/2yku7tx, junto com a versão em C#.)

Observe que só posso substituir esses caracteres se o padrão fizer parte da árvore de expressão e se o nó de expressão for uma constante. Se o padrão for outro tipo de expressão – tal como o resultado de uma chamada de método ou o resultado de uma BinaryExpression que concatena duas outras cadeias de caracteres – o valor do padrão não existirá até que a expressão tenha sido avaliada.

Agora, posso substituir a expressão por aquela reescrita e usar a nova expressão em minha consulta, desta forma:

Dim expr As Expression(Of Func(Of Person, Boolean)) =
  Function(x) x.FirstName Like "*e*i*"
Dim visitor As New LikeVisitor
expr = CType(visitor.Visit(expr), Expression(Of Func(Of Person, Boolean)))
Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(expr)
For Each person In qry
  Console.WriteLine($"LastName: {person.LastName}, FirstName: {person.FirstName}")
Next

O ideal é que esse tipo de transformação seja feito dentro do provedor LINQ to Entities, em que toda a árvore de expressão – que pode incluir outras árvores de expressão e chamadas de método Queryable – possa ser reescrita de uma só vez, em vez de ter que reescrever cada expressão antes de passá-la para os métodos Queryable. Mas a transformação essencialmente seria a mesma: alguma classe ou função que visita todos os nós e conecta um nó de substituição quando necessário.

Conclusão

As árvores de expressão modelam várias operações de código e podem ser usadas para expor APIs sem exigir que os desenvolvedores aprendam uma nova linguagem ou vocabulário – o desenvolvedor pode aproveitar o Visual Basic ou o C# para direcionar essas APIs, enquanto o compilador fornece verificação de tipo e a correção de sintaxe e o IDE fornece o IntelliSense. Uma cópia modificada de uma árvore de expressão pode ser criada, com nós adicionados, removidos ou substituídos. As árvores de expressão também podem ser usadas para compilar código dinamicamente em runtime e até mesmo para código que se reescreve sozinho, mesmo que o Roslyn seja o caminho preferencial para a compilação dinâmica.

Exemplos de código para este artigo podem ser encontrados em bit.ly/2yku7tx.

Mais informações

  • Árvores de expressão nos guias de programação para Visual Basic (bit.ly/2Msocef) e C# (bit.ly/2Y9q5nj)
  • Expressões lambda nos guias de programação para Visual Basic (bit.ly/2YsZFs3) e C# (bit.ly/331ZWp5)
  • Projeto de futuros da árvore de expressão do Bart De Smet (bit.ly/2OrUsRw)
  • O projeto DLR no GitHub (bit.ly/2yssz0x) tem documentos que descrevem o design de árvores de expressão no .NET
  • Renderizar árvores de expressão como cadeias de caracteres e depurar visualizadores para árvores de expressão – bit.ly/2MsoXnB e bit.ly/2GAp5ha
  • "O que a Expression.Quote() faz que a Expression.Constant() já não é capaz de fazer?" em StackOverflow (bit.ly/30YT6Pi)

Zev Spitz escreveu uma biblioteca para renderizar árvores de expressão como cadeias de caracteres em vários formatos – C#, Visual Basic e chamadas de método de fábrica – e um visualizador de depuração do Visual Studio para árvores de expressão.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Kathleen Dollard


Discuta esse artigo no fórum do MSDN Magazine