Condividi tramite


Creare alberi delle espressioni

Il compilatore C# ha creato tutti gli alberi delle espressioni finora visualizzati. È stata creata un'espressione lambda assegnata a una variabile tipizzata come Expression<Func<T>> o un tipo simile. Per molti scenari si compila un'espressione in memoria in fase di esecuzione.

Gli alberi delle espressioni non sono modificabili. Ciò significa che l'albero deve essere compilato dalle foglie alla radice. Ciò si riflette nelle API usate per compilare gli alberi delle espressioni: i metodi usati per compilare un nodo accettano tutti gli elementi figlio come argomenti. I seguenti esempi illustrano le tecniche.

Creare nodi

Si inizia con l'espressione di addizione usata in queste sezioni:

Expression<Func<int>> sum = () => 1 + 2;

Per costruire l'albero delle espressioni, creare prima di tutto i nodi foglia. I nodi foglia sono costanti. Usare il metodo Constant per creare i nodi:

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));

Compilare quindi l'espressione di addizione:

var addition = Expression.Add(one, two);

Dopo aver compilato l'espressione di addizione, si crea l'espressione lambda:

var lambda = Expression.Lambda(addition);

Questa espressione lambda non contiene argomenti. Più avanti in questa sezione viene illustrato come eseguire il mapping degli argomenti ai parametri e compilare espressioni più complesse.

Per le espressioni come questa, è possibile combinare tutte le chiamate in una singola istruzione:

var lambda2 = Expression.Lambda(
    Expression.Add(
        Expression.Constant(1, typeof(int)),
        Expression.Constant(2, typeof(int))
    )
);

Creare un albero

Nella sezione precedente sono stati illustrati i concetti di base per la creazione di un albero delle espressioni in memoria. Strutture ad albero più complesse implicano in genere più tipi di nodo e più nodi dell'albero. L'esempio seguente mostra due tipi di nodo che vengono in genere compilati quando si creano alberi delle espressioni: i nodi argomento e i nodi di chiamata al metodo. Di seguito viene compilato un albero delle espressioni per creare questa espressione:

Expression<Func<double, double, double>> distanceCalc =
    (x, y) => Math.Sqrt(x * x + y * y);

Si inizia creando espressioni per i parametri per x e y:

var xParameter = Expression.Parameter(typeof(double), "x");
var yParameter = Expression.Parameter(typeof(double), "y");

Per creare le espressioni di addizione e moltiplicazione seguire il modello illustrato in precedenza:

var xSquared = Expression.Multiply(xParameter, xParameter);
var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

Quindi è necessario creare un'espressione di chiamata al metodo per la chiamata a Math.Sqrt.

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) }) ?? throw new InvalidOperationException("Math.Sqrt not found!");
var distance = Expression.Call(sqrtMethod, sum);

La chiamata GetMethod potrebbe restituire null se il metodo non viene trovato. Molto probabilmente questo è dovuto al fatto che il nome del metodo è stato digitato in modo non corretto. In caso contrario, potrebbe significare che l'assembly richiesto non viene caricato. Inserire infine la chiamata al metodo in un'espressione lambda e verificare che gli argomenti nell'espressione lambda siano stati definiti:

var distanceLambda = Expression.Lambda(
    distance,
    xParameter,
    yParameter);

In questo esempio più complesso, vengono illustrate altre due tecniche spesso necessarie per creare alberi delle espressioni.

Innanzitutto è necessario creare gli oggetti che rappresentano i parametri o le variabili locali prima di usarli. Dopo aver creato questi oggetti, è possibile inserirli nell'albero delle espressioni secondo necessità.

È quindi necessario usare un sottoinsieme dell'API Reflection per creare un oggetto System.Reflection.MethodInfo, così da poter creare un albero delle espressioni per accedere a tale metodo. È necessario limitarsi al sottoinsieme delle API Reflection disponibili nella piattaforma .NET Core. Anche queste tecniche possono essere estese ad altri alberi delle espressioni.

Informazioni dettagliate sul codice di compilazione

Non ci sono limiti alle possibilità di creare usando queste API. Tuttavia, più complicato è l'albero delle espressioni che si vuole compilare, più difficile sarà da gestire e leggere il codice.

Viene ora creato un albero delle espressioni che è l'equivalente di questo codice:

Func<int, int> factorialFunc = (n) =>
{
    var res = 1;
    while (n > 1)
    {
        res = res * n;
        n--;
    }
    return res;
};

Il codice precedente non ha compilato l'albero delle espressioni, ma semplicemente il delegato. Usando la classe Expression non è possibile compilare espressioni lambda dell'istruzione. Ecco il codice necessario per compilare la stessa funzionalità. Non esiste un'API per la compilazione di un ciclo while. È invece necessario creare un ciclo contenente un test condizionale e una destinazione dell'etichetta per interrompere il ciclo.

var nArgument = Expression.Parameter(typeof(int), "n");
var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value
LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,
// and decrements the value of 'n'
var block = Expression.Block(
    Expression.Assign(result,
        Expression.Multiply(result, nArgument)),
    Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.
BlockExpression body = Expression.Block(
    new[] { result },
    initializeResult,
    Expression.Loop(
        Expression.IfThenElse(
            Expression.GreaterThan(nArgument, Expression.Constant(1)),
            block,
            Expression.Break(label, result)
        ),
        label
    )
);

Il codice per creare l'albero delle espressioni per la funzione fattoriale è più lungo, più complesso e include numerose etichette e istruzioni di interruzione che è preferibile evitare nelle attività quotidiane di scrittura del codice.

Per questa sezione è stato scritto codice per visitare ogni nodo in questo albero delle espressioni e scrivere informazioni sui nodi creati in questo esempio. È possibile visualizzare o scaricare il codice di esempio dal repository GitHub dotnet/docs. Provare a compilare ed eseguire da soli gli esempi.

Eseguire il mapping dei costrutti di codice alle espressioni

L'esempio di codice seguente illustra un albero delle espressioni che rappresenta l'espressione lambda num => num < 5 usando l'API.

// Manually build the expression tree for
// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
    Expression.Lambda<Func<int, bool>>(
        numLessThanFive,
        new ParameterExpression[] { numParam });

L'API degli alberi delle espressioni supporta anche le assegnazioni e le espressioni del flusso di controllo, ad esempio cicli, blocchi condizionali e blocchi try-catch. Tramite l'API è possibile creare alberi delle espressioni più complessi rispetto a quelli che è possibile creare da espressioni lambda con il compilatore di C#. L'esempio seguente illustra come creare un albero delle espressioni che calcola il fattoriale di un numero.

// Creating a parameter expression.
ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.
ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.
LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.
BlockExpression block = Expression.Block(
    // Adding a local variable.
    new[] { result },
    // Assigning a constant to a local variable: result = 1
    Expression.Assign(result, Expression.Constant(1)),
        // Adding a loop.
        Expression.Loop(
           // Adding a conditional block into the loop.
           Expression.IfThenElse(
               // Condition: value > 1
               Expression.GreaterThan(value, Expression.Constant(1)),
               // If true: result *= value --
               Expression.MultiplyAssign(result,
                   Expression.PostDecrementAssign(value)),
               // If false, exit the loop and go to the label.
               Expression.Break(label, result)
           ),
       // Label to jump to.
       label
    )
);

// Compile and execute an expression tree.
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

Per altre informazioni, vedere l'articolo Generating Dynamic Methods with Expression Trees in Visual Studio 2010 (Generazione di metodi dinamici con alberi delle espressioni in Visual Studio 2010), valido anche per le versioni successive di Visual Studio.