Árvores de expressão em C # usando um exemplo de localização de uma derivada (Expression Tree Visitor vs Pattern matching)

Dia bom. As árvores de expressão, especialmente quando combinadas com o padrão Visitor, sempre foram um tópico bastante confuso. Portanto, quanto mais informações diversas sobre esse tópico, mais exemplos, mais fácil será para aqueles que estão interessados ​​em encontrar algo que seja claro e útil para eles.



O artigo foi criado como de costume - começa com a estrutura conceitual e as definições e termina com exemplos e formas de uso. Sumário abaixo.

Noções básicas sobre árvores de expressão
Sintaxe das árvores de expressão
Tipos de expressão
Correspondência de
visitantes ingênuos Visitante
clássico

Bem, o objetivo não é impor uma solução específica ou dizer que uma é melhor que a outra. Proponho tirar conclusões, levando em conta todas as nuances do seu caso. Vou expressar minha opinião no meu exemplo.


Árvores de expressão


O básico


Primeiro você precisa lidar com árvores de expressão. Eles significam o tipo de expressão ou qualquer um de seus herdeiros (serão discutidos mais adiante). No cenário usual, a expressão / algoritmos são apresentados na forma de código / instruções executáveis ​​com as quais o usuário pode não ter muito o que fazer (principalmente executar). O tipo Expressão permite representar uma expressão / algoritmo (geralmente lambdas, mas não necessário) como dados organizados em uma estrutura de árvore à qual o usuário tem acesso. A maneira semelhante a uma árvore de organizar informações sobre o algoritmo e o nome da classe nos fornece "árvores de expressão".

Para maior clareza, analisaremos um exemplo simples. Suponha que tenhamos lambda

(x) => Console.WriteLine (x + 5)

Isso pode ser representado como a seguinte árvore


A raiz da árvore é a parte superior do MethodCall , os parâmetros do método também são expressões, portanto, pode ter qualquer número de filhos.

No nosso caso, existe apenas um descendente - o pico da " Operação Aritmética ". Ele contém informações sobre que tipo de operação é e os operandos esquerdo e direito também são expressões. Esse vértice sempre terá 2 descendentes.

Os operandos são representados por uma constante ( Constante ) e um parâmetro ( Parâmetro ). Tais expressões não têm descendentes.

Estes são exemplos muito simplificados, mas refletem completamente a essência.

A principal característica das árvores de expressão é que elas podem ser analisadas e ler todas as informações necessárias sobre o que o algoritmo deve fazer. De algum ponto de vista, isso é o oposto de atributos. Os atributos são um meio de descrição declarativa do comportamento (muito condicional, mas o objetivo final é aproximadamente o mesmo). Enquanto as árvores de expressão estão usando uma função / algoritmo para descrever dados.

Eles são usados, por exemplo, em provedores de estrutura de entidade. A aplicação é óbvia - analisar a árvore de expressões, entender o que deve ser executado lá e criar SQL a partir dessa descrição . Menos exemplos bem conhecidos são o MOQ biblioteca para moking . As árvores de expressão também são usadas no DLR.(tempo de execução dinâmico do idioma). Os desenvolvedores de compiladores os usam para garantir a compatibilidade entre a natureza dinâmica e o dotnet, em vez de gerar o MSIL .

Também vale ressaltar que as árvores de expressão são imutáveis.

Sintaxe


A próxima coisa que vale a pena discutir é a sintaxe. Existem 2 maneiras principais:

  • Criando árvores de expressão através de métodos estáticos da classe Expression
  • Usando expressões lambda compiladas em Expression

Métodos estáticos da classe Expression


Criar árvores de expressão através de métodos estáticos da classe Expression é menos usado (principalmente do ponto de vista do usuário). Isso é complicado, mas bastante simples, temos
muitos tijolos básicos à nossa disposição, dos quais é possível construir
coisas bastante complexas . A criação acontece através de métodos estáticos, pois construtores de expressão têm um modificador interno . E isso não significa que você precise descobrir a reflexão.

Como exemplo, vou criar uma expressão a partir do exemplo acima:

(x) => Console.WriteLine (x + 5)

ParameterExpression parameter = Expression.Parameter(typeof(double));
ConstantExpression constant = Expression.Constant(5d, typeof(double));
BinaryExpression add = Expression.Add(parameter, constant);
MethodInfo writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(double) });
MethodCallExpression methodCall = Expression.Call(null, writeLine, add);
Expression<Action<double>> expressionlambda = Expression.Lambda<Action<double>>(methodCall, parameter);
Action<double> delegateLambda = expressionlambda.Compile();
delegateLambda(123321);

Isso pode não ser uma maneira muito conveniente, mas reflete completamente a estrutura interna das árvores de expressão. Além disso, esse método fornece mais recursos e recursos que podem ser usados ​​em árvores de expressão: de loops, condições, try-catch, goto, atribuição, terminando com blocos de falha, informações de depuração para pontos de interrupção, dinâmico, etc.

Expressão lambda


Usar lambdas como expressões é uma maneira mais frequente. Funciona de maneira muito simples - o compilador inteligente no estágio de compilação examina para que o lambda é usado. E o compila em um delegado ou em uma expressão. Em um exemplo já dominado, tem a seguinte aparência

Expression<Action<double>> write =  => Console.WriteLine( + 5);

Vale a pena esclarecer isso - uma expressão é uma descrição exaustiva. E é o suficiente para
obter o resultado. Árvores de expressão como LambdaExpression ou seus descendentes podem ser
convertidas em uma IL executável. Os demais tipos não podem ser convertidos diretamente em código executável (mas isso não faz muito sentido).
A propósito, se alguém critica a compilação rápida de uma expressão, você pode dar uma olhada neste projeto de terceiros.

O inverso não é verdadeiro no caso geral. Um delegado não pode apenas buscá-lo e se apresentar como uma expressão (mas isso ainda é possível).

Nem todas as lambdas podem ser convertidas em árvores de expressão. Esses incluem:

  • Operador de atribuição contendo
  • Contribuindo dinâmico
  • Assíncrono
  • Com corpo (aparelho)

double variable;
dynamic dynamic;
Expression<Action> assignment = () => variable = 5; //Compiler error: An expression tree may not contain an assignment operator
Expression<Func<double>> dynamically = () => dynamic; //Compiler error: An expression tree may not contain a dynamic operation
Expression<Func<Task>> asynchon = async () => await Task.CompletedTask; //Compiler error: Async lambda cannot be converted to expresiion trees
Expression<Action> body = () => { }; //Compiler error: A lambda expression with a statement body cannot be converted to an expression tree


Tipos de expressões


Sugiro uma rápida olhada nos tipos disponíveis para representar quais oportunidades temos. Todos eles estão no espaço de nome System.Linq.Expressions.Eu

sugiro que você se familiarize primeiro com vários recursos realmente interessantes e incomuns. Os tipos mais simples de expressões que reuni em um tablet com uma breve descrição.

Dinâmico


Usando DynamicExpression, é possível usar dinâmico e todos os seus recursos em árvores de expressão. Há uma API bastante confusa, fiquei neste exemplo por mais tempo do que em todos os outros combinados. Toda a confusão é fornecida por várias bandeiras. E alguns deles são semelhantes aos que você está procurando, mas não são necessariamente. E ao trabalhar com dinâmico em árvores de expressão, é difícil obter um erro de fala. Exemplo:

var parameter1 = Expression.Parameter(typeof(object), "name1");
var parameter2 = Expression.Parameter(typeof(object), "name2"); 
var dynamicParam1 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
var dynamicParam2 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
CallSiteBinder csb = Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation(CSharpBinderFlags.None, ExpressionType.Add, typeof(Program), new[] { dynamicParam1, dynamicParam2 });
var dyno = Expression.Dynamic(csb, typeof(object), parameter1, parameter2);
Expression<Func<dynamic, dynamic, dynamic>> expr = Expression.Lambda<Func<dynamic, dynamic, dynamic>>(dyno, new[] { parameter1, parameter2 });
Func<dynamic, dynamic, dynamic> action = expr.Compile();
var res = action("1", "2");
Console.WriteLine(res); //12
res = action(1, 2);
Console.WriteLine(res); //3

Eu indiquei explicitamente de onde o Fichário vem para evitar confusão com o Fichário do System.Reflection. Entre as coisas interessantes, podemos fazer parâmetros ref e out, parâmetros nomeados, operações unárias e basicamente tudo o que pode ser feito através da dinâmica, mas isso exigirá alguma habilidade.

Blocos de captura de exceção


A segunda coisa na qual prestarei atenção é a funcionalidade try / catch / finalmente / fault, ou melhor, o fato de
termos acesso ao bloco de falhas. Ele não está disponível em C #, mas está em MSIL.Este é um tipo de
finalmente analógico que será executado em caso de qualquer exceção. No exemplo abaixo, uma exceção será lançada, após a qual “Hi” será exibido e o programa aguardará a entrada. Somente depois disso cairá completamente. Eu não recomendo esta prática para uso.

var throwSmth = Expression.Throw(Expression.Constant(new Exception(), typeof(Exception)));
var log = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }), Expression.Constant("Hi", typeof(string)));
var read = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.ReadLine)));
var fault = Expression.TryFault(throwSmth, Expression.Block(new[] { log, read }));
Expression<Action> expr = Expression.Lambda<Action>(fault);
Action compiledExpression = expr.Compile();
compiledExpression();

Breve descrição dos tipos de árvore de expressão disponíveis

Mesa


Um tipoPequena descrição
O principal
Expressão, . ,
Expression<TDelegate>
BinaryExpression(+, — )
UnaryExpression(+, -), throw
ConstantExpression
ParameterExpression
MethodCallExpression, MethodInfo
IndexExpression
BlockExpression, .
ConditionalExpression— if-else
LabelTargetgoto
LabelExpression, . LabelTarget. , GotoExpression, — . void, .
GotoExpression. . ( .. «break»)
LoopExpression, «break»
SwitchCaseSwitchExpression
SwitchExpressionswitch/case
TryExpressiontry/catch/finally/fault
CatchBlock, ,
ElementInitIEnumerable. ListInitExpression
ListInitExpression+
DefaultExpression
NewArrayExpression+
NewExpression
/
MemberAssignment
MemberBinding, , ,
MemberExpression/
MemberInitExpression
MemberListBinding/
MemberMemberBinding/ , /
LambdaExpression
InvocationExpression-
DebugInfoExpression.
SymbolDocumentInfo, .
DynamicExpression( )
RuntimeVariablesExpression/
TypeBinaryExpression, (is)



  • ExpressionVisitor — . .
  • DynamicExpressionVisitor — DynamicExpression ( VisitDynamic)


Esta informação é suficiente para começar a comparar os métodos de trabalho com árvores de expressão. Decidi analisar tudo isso pelo exemplo de encontrar a derivada. Não previ todas as opções possíveis - apenas básicas. Mas se, por algum motivo, alguém decidiu modificá-lo e usá-lo, ficarei feliz em compartilhar as melhorias através da solicitação ao meu repositório .


Correspondência de padrões


Portanto, a tarefa é fazer um cálculo de derivadas. Você pode estimar o seguinte: existem algumas regras para encontrar a derivada para diferentes tipos de operações - multiplicação, divisão etc. Dependendo da operação, você deve selecionar uma fórmula específica. Em uma formulação tão banal, a tarefa é idealmente colocada em switch / case . E na versão mais recente do idioma, fomos apresentados com switch / case 2.0 ou correspondência de padrões .

É difícil discutir algo aqui. Em um hub, essa quantidade de código parece complicada e mal lida, por isso sugiro olhar para o github . Para um exemplo de derivado, ficou assim:

Exemplo
    public class PatterntMatchingDerivative
    {
        private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
        private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
        private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));
		
        public Expression<Func<double, double>> ParseDerivative(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(ParseDerivative(function.Body), function.Parameters);
        }

        private Expression ParseDerivative(Expression function) => function switch
        {
            BinaryExpression binaryExpr => function.NodeType switch
            {
                ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),

                ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
		{	
	  	    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression _) => constant,
		    (ParameterExpression _, ConstantExpression constant) => constant,
		    _ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
		},

                ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
		{
		    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
		    (ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
		    _ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
	        },
            },
            MethodCallExpression methodCall when methodCall.Method == _pow => (methodCall.Arguments[0], methodCall.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(ParseDerivative(expression), methodCall), Expression.Call(null, _log, constant)),
             },
             _ => function.NodeType switch
            {
                ExpressionType.Constant => _zero,
                ExpressionType.Parameter => _one,
                _ => throw new OutOfMemoryException("Bitmap best practice")
             }
        };
    }


Parece um pouco incomum, mas interessante. Foi um prazer escrever isso - todas as condições se encaixam organicamente em uma linha.

O exemplo fala por si, você não pode descrevê-lo melhor com palavras.

Visitante ingênuo


Em tal tarefa, um visitante da árvore de expressão vem à mente imediatamente, o que faz muito barulho
e um pouco de pânico entre os fãs para discutir a agilidade na cozinha. “Não temas a ignorância, mas o falso conhecimento. É melhor não saber nada do que considerar a verdade aquilo que não é verdade. " Lembrando-se desta maravilhosa frase de Tolstoi, reconhecendo a ignorância e contando com o apoio do Google, você pode encontrar o seguinte guia .

Eu tenho esse link é o primeiro (depois da Sibéria em 1949) para a consulta "Visitante da árvore de expressões".
À primeira vista, é exatamente disso que precisamos. O título do artigo se encaixa no que queremos fazer, e as classes nos exemplos são nomeadas com o sufixo Visitor .

Após revisar o artigo e fazer, por analogia ao nosso exemplo com derivativos, obtemos:
Link para o github.

Exemplo
public class CustomDerivativeExpressionTreeVisitor
    {
        public Expression<Func<double, double>> Visit(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(Visitor.CreateFromExpression(function.Body).Visit(), function.Parameters);
        }
    }

    public abstract class Visitor
    {
        protected static readonly MethodInfo Pow = typeof(Math).GetMethod(nameof(Math.Pow));
        protected static readonly MethodInfo Log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        protected readonly ConstantExpression Zero = Expression.Constant(0d, typeof(double));
        protected readonly ConstantExpression One = Expression.Constant(1d, typeof(double));
        public abstract Expression Visit();
        public static Visitor CreateFromExpression(Expression node)
            => node switch
            {
                BinaryExpression be => new BinaryVisitor(be),
                MethodCallExpression mce when mce.Method == Pow => new PowMethodCallVisitor(mce),
                _ => new SimpleVisitor(node),
            };
        
    }

    public class BinaryVisitor : Visitor
    {
        private readonly BinaryExpression _node;
        
        public BinaryVisitor(BinaryExpression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => _node.NodeType switch
            {
                ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),

                ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
		{	
	  	    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression _) => constant,
		    (ParameterExpression _, ConstantExpression constant) => constant,
		    _ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
		},

                ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
		{
		    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
		    (ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
		    _ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
	        },
            };
    }

    public class PowMethodCallVisitor : Visitor
    {
        private readonly MethodCallExpression _node;

        public PowMethodCallVisitor(MethodCallExpression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => (_node.Arguments[0], _node.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(_node, Expression.Call(null, Log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, Pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(CreateFromExpression(expression).Visit(), _node), Expression.Call(null, Log, constant)),
            };
    }

    public class SimpleVisitor : Visitor
    {
        private readonly Expression _node;

        public SimpleVisitor(Expression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => _node.NodeType switch
            {
                ExpressionType.Constant => Zero,
                ExpressionType.Parameter => One,
            };
    }


De fato - espalhamos os casos de troca para diferentes classes. Não havia menos deles, a magia não apareceu. Nos mesmos casos, muito mais falas. E onde está o prometido despacho de expedição dupla?

Visitante clássico e expedição dupla


Aqui vale a pena falar sobre o próprio modelo do visitante , também é o Visitor , que é a base do visitante da árvore Expression . Vamos analisá-lo apenas no exemplo de árvores de expressão.

Por um segundo, suponha que projetemos árvores de expressão. Queremos oferecer aos usuários a capacidade de iterar pela árvore de expressões e, dependendo dos tipos de nós (tipos de expressão), executar determinadas ações.

A primeira opção é não fazer nada. Ou seja, force os usuários a usar switch / case. Esta não é uma opção tão ruim. Mas aqui há uma nuance: espalhamos a lógica responsável por um tipo específico. Simplificando, polimorfismo e desafios virtuais ( também conhecido como encadernação tardia)) tornam possível mudar a definição de tipo para o tempo de execução e remover essas verificações do nosso código. Basta que tenhamos uma lógica que crie uma instância do tipo desejado, então tudo será feito pelo tempo de execução para nós.

A segunda opçãoA solução óbvia é colocar a lógica em métodos virtuais. Substituindo o método virtual em cada sucessor, podemos esquecer o switch / case. O mecanismo das chamadas polimórficas vai decidir por nós. A tabela de métodos funcionará aqui, os métodos serão chamados pelo deslocamento nela. Mas este é um tópico para um artigo inteiro, então não vamos nos deixar levar. Métodos virtuais parecem resolver nosso problema. Mas, infelizmente, eles criam outro. Para nossa tarefa, poderíamos adicionar o método GetDeriviative (). Mas agora as próprias classes de expressão parecem estranhas. Poderíamos adicionar esses métodos para todas as ocasiões, mas eles não se encaixam na lógica geral da classe. E ainda não tivemos a oportunidade de fazer algo semelhante aos usuários (de maneira adequada, é claro). Precisamos permitir que o usuário defina a lógica para cada tipo específico,mas mantenha o polimorfismo (que está disponível para nós).

Somente os esforços do usuário para fazer isso não terão êxito.

É aqui que o verdadeiro visitante se encontra. No tipo básico de hierarquia (Expressão no nosso caso), definimos um método da forma

virtual Expression Accept(ExpressionVisitor visitor);

Nos herdeiros, esse método será substituído.

O próprio ExpressionVisitor é uma classe base que contém um método virtual com a mesma assinatura
para cada tipo de hierarquia. No exemplo da classe ExpressionVisitor - VisitBinary (...), VisitMethodCall (...), VisitConstant (...), VisitParameter (...).

Esses métodos são chamados na classe correspondente da nossa hierarquia.

Essa. O método Accept na classe BinaryExpression terá a seguinte aparência:

	
protected internal override Expression Accept(ExpressionVisitor visitor)
{
        return visitor.VisitBinary(this);
}

Como resultado, para definir um novo comportamento, o usuário precisa apenas criar um herdeiro da classe ExpressionVisitor, na qual os métodos correspondentes para resolver um problema serão redefinidos. No nosso caso, um DerivativeExpressionVisitor é criado.

Além disso, temos alguns objetos dos sucessores da Expressão, mas quais são desconhecidos, mas não necessários.
Chamamos o método Accept virtual com a implementação ExpressionVisitor de que precisamos, ou seja, com DerivativeExpressionVisitor. Graças ao envio dinâmico, uma implementação substituída do Accept é chamada, como um tempo de execução, digamos BinaryExpression. No corpo desse método, entendemos perfeitamente que estamos na BinaryExpression, mas não sabemos qual sucessor do ExpressionVisitor veio até nós. Mas desde O VisitBinary também é virtual, não precisamos saber. Novamente, simplesmente chamamos a referência para a classe base, a chamada é enviada dinamicamente (em tempo de execução) e uma implementação VisitBinary substituída do tipo de tempo de execução é chamada. Chega de expedição dupla - pingue-pongue no estilo de "você faz" - "não, você".

O que isso nos dá. De fato, isso permite “adicionar” métodos virtuais de fora, não de
mudando de classe. Parece ótimo, mas tem suas desvantagens:

  1. Alguns esquerdistas na forma do método Accept, responsável por tudo e por nada ao mesmo tempo
  2. O efeito cascata de uma boa função de hash é que, se você adicionar apenas um herdeiro à hierarquia, na pior das hipóteses, todos terão que terminar seus visitantes

Mas a natureza das árvores de expressão permite esses custos devido às especificidades do trabalho com expressões, porque esse tipo de solução alternativa é um dos principais recursos.
Aqui você pode ver todos os métodos disponíveis para sobrecarga.

Então, vamos ver como fica no final.
Link para o github.

Exemplo
public class BuildinExpressionTreeVisitor : ExpressionVisitor
    {
        private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
        private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
        private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));

        public Expression<Func<double, double>> GetDerivative(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(Visit(function.Body), function.Parameters);
        }

        protected override Expression VisitBinary(BinaryExpression binaryExpr)
            => binaryExpr.NodeType switch
            {
                ExpressionType.Add => Expression.Add(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),

                ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
                ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => binaryExpr.Left,
                ExpressionType.Multiply when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => binaryExpr.Right,
                ExpressionType.Multiply => Expression.Add(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))),

                ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
                ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => Expression.Divide(binaryExpr.Left, Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
                ExpressionType.Divide when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => Expression.Divide(_one, binaryExpr.Right),
                ExpressionType.Divide => Expression.Divide(Expression.Subtract(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
            };

        protected override Expression VisitMethodCall(MethodCallExpression methodCall)
            => (methodCall.Arguments[0], methodCall.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(Visit(expression), methodCall), Expression.Call(null, _log, constant)),
            };

        protected override Expression VisitConstant(ConstantExpression _) => _zero;

        protected override Expression VisitParameter(ParameterExpression b) => _one;
    }


achados


Talvez, como na maioria das tarefas de programação, uma resposta definitiva não possa ser dada. Tudo, como sempre, depende da situação específica. Eu gosto da correspondência usual de padrões para o meu exemplo, porque Não o desenvolvi na escala do desenvolvimento industrial. Se essa expressão aumentasse incontrolavelmente, valeria a pena pensar no visitante. E mesmo um visitante ingênuo tem direito à vida - afinal, essa é uma boa maneira de espalhar uma grande quantidade de código em classes, se a hierarquia não fornecer suporte de sua parte. E mesmo aqui há exceções.

Da mesma forma, o apoio do visitante da hierarquia é uma coisa muito controversa.

Mas espero que as informações fornecidas aqui sejam suficientes para fazer a escolha certa.

Source: https://habr.com/ru/post/undefined/


All Articles