Árboles de expresión en C # usando un ejemplo de búsqueda de una derivada (Expression Tree Visitor vs Pattern match)

Buen día. Los árboles de expresión, especialmente cuando se combinan con el patrón Visitante, siempre han sido un tema bastante confuso. Por lo tanto, mientras más diversa información sobre este tema, más ejemplos, más fácil será para aquellos interesados ​​en encontrar algo que sea claro y útil para ellos.



El artículo se construye como de costumbre: comienza con el marco conceptual y las definiciones y termina con ejemplos y formas de uso. Tabla de contenido a continuación.

Conceptos básicos de los árboles de expresión
Sintaxis de los árboles de expresión
Tipos de expresión
Coincidencia de
visitante Visitante ingenuo Visitante
clásico

Bueno, el objetivo no es imponer una solución específica o decir que una es mejor que la otra. Propongo sacar conclusiones nosotros mismos, teniendo en cuenta todos los matices de su caso. Expresaré mi opinión sobre mi ejemplo.


Árboles de expresión


Los basicos


Primero debes lidiar con los árboles de expresión. Significan el tipo de expresión o cualquiera de sus herederos (se discutirán más adelante). En el escenario habitual, las expresiones / algoritmos se presentan en forma de código / instrucciones ejecutables con las que el usuario puede no tener mucho que hacer (principalmente ejecutar). El tipo de expresión le permite representar una expresión / algoritmo (generalmente lambdas, pero no necesario) como datos organizados en una estructura de árbol a la que el usuario tiene acceso. La forma de árbol de organizar la información sobre el algoritmo y el nombre de la clase nos da "árboles de expresión".

Para mayor claridad, analizaremos un ejemplo simple. Supongamos que tenemos lambda

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

Esto se puede representar como el siguiente árbol


La raíz del árbol es la parte superior de MethodCall , los parámetros del método también son expresiones, por lo tanto, puede tener cualquier número de hijos.

En nuestro caso, solo hay un descendiente: el pico de " ArithmeticOperation ". Contiene información sobre qué tipo de operación es y los operandos izquierdo y derecho también son expresiones. Tal vértice siempre tendrá 2 descendientes.

Los operandos están representados por una constante ( Constante ) y un parámetro ( Parámetro ). Tales expresiones no tienen descendientes.

Estos son ejemplos muy simplificados, pero reflejan completamente la esencia.

La característica principal de los árboles de expresión es que se pueden analizar y leer toda la información necesaria sobre lo que debe hacer el algoritmo. Desde algún punto de vista, esto es lo contrario de los atributos. Los atributos son un medio de descripción declarativa del comportamiento (muy condicional, pero el objetivo final es aproximadamente el mismo). Mientras que los árboles de expresión están utilizando una función / algoritmo para describir datos.

Se utilizan, por ejemplo, en proveedores de marcos de entidades. La aplicación es obvia: analizar el árbol de expresiones, comprender qué se debe ejecutar allí y crear SQL a partir de esta descripción . Ejemplos menos conocidos son la biblioteca moq para moking . Los árboles de expresión también se usan en DLR.(tiempo de ejecución dinámico del lenguaje). Los desarrolladores de compiladores los usan para garantizar la compatibilidad entre la naturaleza dinámica y dotnet, en lugar de generar MSIL .

También vale la pena mencionar que los árboles de expresión son inmutables.

Sintaxis


Lo siguiente que vale la pena discutir es la sintaxis. Hay 2 formas principales:

  • Crear árboles de expresión a través de métodos estáticos de la clase Expresión
  • Usar expresiones lambda compiladas en Expression

Métodos Estáticos de la Clase de Expresión


La creación de árboles de expresión a través de métodos estáticos de la clase Expresión se usa con menos frecuencia (especialmente desde el punto de vista del usuario). Esto es engorroso, pero bastante simple, tenemos
muchos ladrillos básicos a nuestra disposición, desde los cuales puede construir
cosas bastante complejas . La creación ocurre a través de métodos estáticos desde Los constructores de expresiones tienen un modificador interno . Y esto no significa que deba descubrir la reflexión.

Como ejemplo, crearé una expresión a partir del ejemplo anterior:

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

Puede que esta no sea una forma muy conveniente, pero refleja completamente la estructura interna de los árboles de expresión. Además, este método proporciona más características y características que se pueden usar en los árboles de expresión: desde bucles, condiciones, try-catch, goto, asignación, terminando con bloques de falla, información de depuración para puntos de interrupción, dinámica, etc.

Expresión lambda


Usar lambdas como expresiones es una forma más frecuente. Funciona de manera muy simple: el compilador inteligente en la etapa de compilación analiza para qué se utiliza lambda. Y lo compila en un delegado o en una expresión. En un ejemplo ya dominado, se ve de la siguiente manera

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

Vale la pena aclarar tal cosa: una expresión es una descripción exhaustiva. Y es suficiente para
obtener el resultado. Los árboles de expresión como LambdaExpression o sus descendientes se pueden
convertir en un IL ejecutable. Los tipos restantes no se pueden convertir directamente a código ejecutable (pero esto no tiene mucho sentido).
Por cierto, si alguien es crítico con la compilación rápida de una expresión, puede echar un vistazo a este proyecto de terceros.

Lo contrario no es cierto en el caso general. Un delegado no puede simplemente recogerlo y presentarse como una expresión (pero esto todavía es posible).

No todas las lambdas se pueden convertir en árboles de expresión. Éstas incluyen:

  • Operador de asignación que contiene
  • Dinámica contribuyente
  • Asincrónico
  • Con cuerpo (frenillos)

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 expresiones


Sugiero un vistazo rápido a los tipos disponibles para representar las oportunidades que tenemos. Todos ellos están en el espacio de nombres System.Linq.Expressions .

Sugiero que primero se familiarice con varias características realmente interesantes e inusuales. Los tipos más simples de expresiones que reúno en una tableta con una breve descripción.

Dinámica


Usando DynamicExpression es posible usar Dynamic y todas sus características en los árboles de expresión. Hay una API bastante confusa, me senté en este ejemplo más tiempo que en todos los demás combinados. Toda la confusión es proporcionada por un grupo de varias banderas. Y algunos de ellos son similares a los que está buscando, pero no necesariamente. Y cuando se trabaja con árboles dinámicos en expresión, es difícil obtener un error de conversación. Ejemplo:

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

Indiqué explícitamente de dónde viene el Binder para evitar confusiones con el Binder del System.Reflection. De cosas interesantes, podemos hacer parámetros de ref y out, parámetros con nombre, operaciones unitarias y, en principio, todo lo que se puede hacer a través de la dinámica, pero esto requerirá algo de habilidad.

Bloques de captura de excepción


La segunda cosa a la que prestaré atención es la funcionalidad de prueba / captura / finalmente / falla, o más bien, el hecho de
que tenemos acceso al bloque de falla. No está disponible en C #, pero está en MSIL. Este es un tipo de
finalmente analógico que se ejecutará en caso de cualquier excepción. En el ejemplo a continuación, se lanzará una excepción, después de lo cual se mostrará "Hola" y el programa esperará la entrada. Solo después de eso caerá por completo. No recomiendo esta práctica para su 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 descripción de los tipos de árbol de expresión disponibles

Mesa


Un tipoBreve descripción
El principal
Expresión, . ,
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 información es suficiente para comenzar a comparar los métodos de trabajo con árboles de expresión. Decidí analizar todo esto con el ejemplo de encontrar la derivada. No preveía todas las opciones posibles, solo las básicas. Pero si por alguna razón alguien decide modificarlo y usarlo, estaré encantado de compartir las mejoras a través de la solicitud en mi repositorio .


La coincidencia de patrones


Entonces, la tarea es hacer un cálculo de derivados. Puede estimar lo siguiente: hay un par de reglas para encontrar la derivada para diferentes tipos de operaciones: multiplicación, división, etc. Dependiendo de la operación, debe seleccionar una fórmula específica. En una formulación tan banal, la tarea se coloca idealmente en el interruptor / caja . Y en la última versión del lenguaje, se nos presentó el switch / case 2.0 o la coincidencia de patrones .

Es difícil discutir algo aquí. En un concentrador, tal cantidad de código parece engorroso y mal leído, por lo que sugiero mirar github . Para un ejemplo de una derivada, resultó así:

Ejemplo
    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 un poco inusual, pero interesante. Fue un placer escribir esto: todas las condiciones se ajustan orgánicamente en una línea.

El ejemplo habla por sí mismo, no puedes describirlo mejor con palabras.

Visitante ingenuo


En tal tarea, un visitante del árbol de expresiones viene inmediatamente a la mente, lo que hace mucho ruido
y un poco de pánico entre los aficionados para hablar ágilmente en la cocina. “No temas la ignorancia, sino el falso conocimiento. Es mejor no saber nada que considerar la verdad como algo que no es verdad ". Recordando esta maravillosa frase de Tolstoi, reconociendo la ignorancia y contando con el apoyo de Google, puede encontrar la siguiente guía .

Tengo este enlace es el primero (después de Siberia en 1949) para la consulta "Visitante del árbol de expresión".
A primera vista, esto es exactamente lo que necesitamos. El título del artículo se ajusta a lo que queremos hacer, y las clases en los ejemplos se nombran con el sufijo Visitor .

Después de revisar el artículo y hacer, por analogía para nuestro ejemplo con derivados, obtenemos:
Enlace a github.

Ejemplo
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 hecho, difundimos los casos de cambio para diferentes clases. No había menos de ellos, la magia no apareció. Todos los mismos casos, muchas más líneas. ¿Y dónde está el prometido despacho de despacho doble?

Visitante clásico y doble despacho


Aquí vale la pena contar acerca de la plantilla Visitante en, también es Visitante , que es la base del visitante del árbol de Expresión . Analicemos solo en el ejemplo de los árboles de expresión.

Por un segundo, supongamos que diseñamos árboles de expresión. Queremos ofrecer a los usuarios la capacidad de iterar a través del árbol de expresión y, según los tipos de nodos (tipos de expresión), realizar ciertas acciones.

La primera opción es no hacer nada. Es decir, obligar a los usuarios a usar switch / case. Esta no es una mala opción. Pero aquí hay tal matiz: difundimos la lógica responsable de un tipo particular. En pocas palabras, polimorfismo y desafíos virtuales ( también conocido como enlace tardío) hacen posible cambiar la definición de tipo al tiempo de ejecución y eliminar estas comprobaciones de nuestro código. Es suficiente para nosotros tener una lógica que cree una instancia del tipo deseado, luego todo lo hará el tiempo de ejecución por nosotros.

La segunda opción.La solución obvia es llevar la lógica a métodos virtuales. Al anular el método virtual en cada sucesor, podemos olvidarnos de cambiar / caso. El mecanismo de las llamadas polimórficas decidirá por nosotros. La tabla de métodos funcionará aquí, los métodos serán llamados por el desplazamiento en ella. Pero este es un tema para un artículo completo, así que no nos dejemos llevar. Los métodos virtuales parecen resolver nuestro problema. Pero desafortunadamente, crean otro. Para nuestra tarea, podríamos agregar el método GetDeriviative (). Pero ahora las clases de expresión se ven raras. Podríamos agregar tales métodos para todas las ocasiones, pero no se ajustan a la lógica general de la clase. Y todavía no brindamos la oportunidad de hacer algo similar a los usuarios (de manera adecuada, por supuesto). Necesitamos dejar que el usuario defina la lógica para cada tipo particular,pero mantén el polimorfismo (que está disponible para nosotros).

Solo los esfuerzos del usuario para hacer esto no tendrán éxito.

Aquí es donde se encuentra el verdadero visitante. En el tipo básico de jerarquía (Expresión en nuestro caso), definimos un método de la forma

virtual Expression Accept(ExpressionVisitor visitor);

En los herederos, este método será anulado.

ExpressionVisitor es una clase base que contiene un método virtual con la misma firma
para cada tipo de jerarquía. Usando la clase ExpressionVisitor como ejemplo, VisitBinary (...), VisitMethodCall (...), VisitConstant (...), VisitParameter (...).

Estos métodos se llaman en la clase correspondiente de nuestra jerarquía.

Aquellos. El método Aceptar en la clase BinaryExpression se verá así:

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

Como resultado, para definir un nuevo comportamiento, el usuario solo necesita crear un heredero para la clase ExpressionVisitor, en el que se redefinirán los métodos correspondientes para resolver un problema. En nuestro caso, se crea un DerivativeExpressionVisitor.

Además, tenemos algunos objetos de los sucesores de Expression, pero cuáles son desconocidos, pero no necesarios.
Llamamos al método de aceptación virtual con la implementación de ExpressionVisitor que necesitamos, es decir, con DerivativeExpressionVisitor. Gracias al despacho dinámico, se llama a una implementación anulada de Aceptar, como un tiempo de ejecución, por ejemplo, BinaryExpression. En el cuerpo de este método, entendemos perfectamente que estamos en BinaryExpression, pero no sabemos qué sucesor de ExpressionVisitor vino a nosotros. Pero desde VisitBinary también es virtual, no necesitamos saberlo. Nuevamente, simplemente llamamos por referencia a la clase base, la llamada se despacha dinámicamente (en tiempo de ejecución) y se llama a una implementación de VisitBinary anulada del tipo de tiempo de ejecución. Esto en cuanto al doble despacho: ping-pong al estilo de "lo haces", "no, tú".

Que nos da De hecho, esto hace posible "agregar" métodos virtuales desde afuera, no
cambiando la clase Suena genial, pero tiene sus desventajas:

  1. Algunos izquierdistas en la forma del método Aceptar, que es responsable de todo y de nada al mismo tiempo.
  2. El efecto dominó de una buena función hash es que cuando agrega solo un heredero a la jerarquía, en el peor de los casos, todos tendrán que finalizar sus visitas

Pero la naturaleza de los árboles de expresión permite estos costos debido a los detalles de trabajar con expresiones, ya que este tipo de soluciones es una de sus principales características.
Aquí puede ver todos los métodos disponibles para sobrecargar.

Entonces, veamos cómo se ve al final.
Enlace a github.

Ejemplo
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;
    }


recomendaciones


Quizás, como en la mayoría de las tareas de programación, no se puede dar una respuesta definitiva. Todo, como siempre, depende de la situación específica. Me gusta la coincidencia de patrones habitual para mi ejemplo, porque No lo desarrollé a la escala del desarrollo industrial. Si esta expresión aumentara sin control, valdría la pena pensar en el visitante. E incluso un visitante ingenuo tiene derecho a la vida; después de todo, esta es una buena manera de dispersar una gran cantidad de código en las clases si la jerarquía no ha brindado apoyo por su parte. E incluso aquí hay excepciones.

Del mismo modo, el apoyo del visitante desde la jerarquía es algo muy controvertido.

Pero espero que la información proporcionada aquí sea suficiente para tomar la decisión correcta.

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


All Articles