Condividi tramite


Eseguire alberi delle espressioni

Un albero delle espressioni è una struttura dei dati che rappresenta il codice. Il codice non è compilato ed eseguibile. Se si vuole eseguire il codice .NET che è rappresentato da un albero delle espressioni, è necessario convertirlo in istruzioni IL eseguibili. L'esecuzione di un albero delle espressioni può restituire un valore o può eseguire solo un'azione, ad esempio la chiamata a un metodo.

Possono essere eseguite solo gli alberi delle espressioni che rappresentano espressioni lambda. Gli alberi delle espressioni che rappresentano espressioni lambda sono di tipo LambdaExpression o Expression<TDelegate>. Per eseguire gli alberi delle espressioni, chiamare il metodo Compile per creare un delegato eseguibile e quindi richiamare il delegato.

Nota

Se il tipo del delegato non è noto, ovvero l'espressione lambda è di tipo LambdaExpression e non Expression<TDelegate>, è necessario invocare il metodo DynamicInvoke sul delegato anziché richiamarlo direttamente.

Se un albero delle espressioni non rappresenta un'espressione lambda, è possibile creare una nuova espressione lambda con l'albero delle espressioni originale come corpo, invocando il metodo Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>). Sarà quindi possibile eseguire l'espressione lambda come descritto precedentemente in questa sezione.

Espressioni lambda per funzioni

È possibile convertire qualsiasi LambdaExpression o qualsiasi tipo derivato da LambdaExpression in IL eseguibile. Altri tipi di espressioni non possono essere convertite direttamente in codice. Questa restrizione ha un effetto limitato nella pratica. Le espressioni lambda sono gli unici tipi di espressioni che potrebbero essere eseguite convertendole in linguaggio intermedio eseguibile (IL). (Riflettere su cosa significherebbe eseguire direttamente una System.Linq.Expressions.ConstantExpression. Sarebbe utile?) Un albero delle espressioni che è una System.Linq.Expressions.LambdaExpression o un tipo derivato da LambdaExpression può essere convertito in IL. Il tipo di espressione System.Linq.Expressions.Expression<TDelegate> è l'unico esempio concreto nelle librerie di .NET Core. Viene usato per rappresentare un'espressione che esegue il mapping a qualsiasi tipo delegato. Poiché questo tipo è mappato a un tipo delegato, .NET può esaminare l'espressione e generare IL per un delegato appropriato che corrisponda alla firma dell'espressione lambda. Il tipo di delegato è basato sul tipo di espressione. Se si vuole usare l'oggetto delegato in modo fortemente tipizzato, è necessario conoscere il tipo restituito e l'elenco di argomenti. Il metodo LambdaExpression.Compile() restituisce il tipo Delegate. È necessario eseguire il cast al tipo di delegato corretto affinché gli strumenti in fase di compilazione verifichino l'elenco degli argomenti o il tipo di ritorno.

Nella maggior parte dei casi, esiste un mapping semplice tra un'espressione e il delegato corrispondente. Ad esempio, un albero delle espressioni che è rappresentato da Expression<Func<int>> viene convertito in un delegato del tipo Func<int>. Per un'espressione lambda con qualsiasi tipo restituito e un elenco di argomenti, esiste un tipo delegato che rappresenta il tipo di destinazione per il codice eseguibile rappresentato dall'espressione lambda.

Il tipo System.Linq.Expressions.LambdaExpression contiene i membri LambdaExpression.Compile e LambdaExpression.CompileToMethod usati per convertire un albero delle espressioni in codice eseguibile. Il metodo Compile crea un delegato. Il metodo CompileToMethod aggiorna un oggetto System.Reflection.Emit.MethodBuilder con il linguaggio intermedio che rappresenta l'output compilato dell'albero delle espressioni.

Importante

CompileToMethod è disponibile solo in .NET Framework, non in .NET Core o .NET 5 e versioni successive.

Facoltativamente, è anche possibile specificare un System.Runtime.CompilerServices.DebugInfoGenerator che riceva le informazioni di debug dei simboli per l'oggetto delegato generato. DebugInfoGenerator fornisce informazioni di debug complete sul delegato generato.

È necessario convertire un'espressione in un delegato tramite il codice seguente:

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

Nell'esempio di codice seguente vengono illustrati i tipi concreti usati durante la compilazione e l'esecuzione di un albero delle espressioni.

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

Nell'esempio di codice seguente viene descritto come eseguire un albero delle espressioni che rappresenta l'elevamento di un numero a una potenza mediante la creazione e l'esecuzione di un'espressione lambda. Verrà visualizzato il risultato che rappresenta il numero elevato a potenza.

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

Esecuzione e durate

Si esegue il codice richiamando il delegato creato durante la chiamata a LambdaExpression.Compile(). Il codice precedente, add.Compile(), restituisce un delegato. Invocare il delegato chiamando func(), che esegue il codice.

Il delegato rappresenta il codice nell'albero delle espressioni. È possibile mantenere il punto di controllo al delegato e richiamarlo in un secondo momento. Non è necessario compilare l'albero delle espressioni ogni volta che si vuole eseguire il codice che rappresenta. Tenere presente che gli alberi delle espressioni non sono modificabili e che la compilazione successiva dello stesso albero delle espressioni crea un delegato che esegue lo stesso codice.

Attenzione

È consigliabile prestare la massima attenzione quando si tenta di creare un meccanismo di memorizzazione nella cache più sofisticato per migliorare le prestazioni evitando chiamate di compilazione non necessarie. Il confronto tra due alberi delle espressioni arbitrari per determinare se rappresentano lo stesso algoritmo è un'operazione che richiede molto tempo. Il tempo di calcolo risparmiato evitando chiamate aggiuntive a LambdaExpression.Compile()è probabilmente superiore al tempo di esecuzione del codice che determina se due diversi alberi delle espressioni generano lo stesso codice eseguibile.

Precisazioni

La compilazione di un'espressione lambda a un delegato e la chiamata al delegato è una delle operazioni più semplici che si possono eseguire con un albero delle espressioni. Anche con questa semplice operazione vi sono tuttavia alcune avvertenze da tenere in considerazione.

Le espressioni lambda creano chiusure su tutte le variabili locali a cui si fa riferimento nell'espressione. È necessario garantire che tutte le variabili che fanno parte del delegato siano utilizzabili in corrispondenza della posizione in cui viene chiamato Compile e quando si esegue il delegato risultante. Il compilatore garantisce che le variabili siano incluse nell'ambito. Se tuttavia l'espressione accede a una variabile che implementa IDisposable, è possibile che il codice elimini l'oggetto mentre è ancora mantenuto attivo dall'albero delle espressioni.

Ad esempio, questo codice funziona correttamente poiché int non implementa IDisposable:

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

Il delegato ha acquisito un riferimento alla variabile locale constant. Tale variabile è accessibile in qualsiasi momento successivo, quando viene eseguita la funzione restituita da CreateBoundFunc.

Tenere presente tuttavia la seguente classe (piuttosto improbabile) che implementa System.IDisposable:

public class Resource : IDisposable
{
    private bool _isDisposed = false;
    public int Argument
    {
        get
        {
            if (!_isDisposed)
                return 5;
            else throw new ObjectDisposedException("Resource");
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

Se la si usa in un'espressione, come mostrato nel codice seguente, si ottiene un System.ObjectDisposedException quando si esegue il codice a cui fa riferimento la proprietà Resource.Argument:

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

Il delegato restituito da questo metodo è stato chiuso sull'oggetto constant, che è stato eliminato. (È stato eliminato, perché è stato dichiarato in un'istruzione using.)

A questo punto, quando si esegue il delegato restituito da questo metodo, si avrà una ObjectDisposedException generata nel punto di esecuzione.

Può sembrare strano ricevere un errore di runtime che rappresenta un costrutto in fase di compilazione, ma questo è ciò che avviene negli alberi delle espressioni.

Esistono numerose permutazioni di questo problema, pertanto è difficile offrire indicazioni generali per evitarlo. Prestare attenzione all'accesso alle variabili locali durante la definizione delle espressioni e all'accesso allo stato nell'oggetto corrente (rappresentato da this) durante la creazione di un albero delle espressioni che può essere restituito tramite un'API pubblica.

Il codice nell'espressione può fare riferimento a metodi o proprietà in altri assembly. Tale assembly deve essere accessibile quando viene definita l'espressione, quando viene compilata e quando viene invocato il delegato risultante. Nei casi in cui non è presente, si verifica un ReferencedAssemblyNotFoundException.

Riepilogo

Gli alberi delle espressioni che rappresentano le espressioni lambda possono essere compilati per creare un delegato che è possibile eseguire. Gli alberi delle espressioni offrono un meccanismo per eseguire il codice rappresentato da un albero delle espressioni.

L'albero delle espressioni non rappresenta il codice da eseguire per qualsiasi costrutto specifico creato. Finché l'ambiente in cui viene compilato ed eseguito il codice corrisponderà all'ambiente in cui si crea l'espressione, tutto funzionerà come previsto. In caso contrario, gli errori sono molto prevedibili e vengono individuati nei primi test di qualsiasi codice basato sugli alberi delle espressioni.