Condividi tramite


Eseguire alberi di espressioni

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

È possibile eseguire solo alberi di espressioni che rappresentano espressioni lambda. Gli alberi delle espressioni che rappresentano espressioni lambda sono di tipo LambdaExpression o Expression<TDelegate>. Per eseguire questi alberi delle espressioni, chiamare il metodo Compile per creare un delegato eseguibile e quindi invocare il delegato.

Annotazioni

Se il tipo del delegato non è noto, ovvero l'espressione lambda è di tipo LambdaExpression e non Expression<TDelegate>, chiamare il DynamicInvoke metodo 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, chiamando il Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) metodo . È quindi possibile eseguire l'espressione lambda come descritto in precedenza in questa sezione.

Espressioni lambda alle funzioni

È possibile convertire qualsiasi LambdaExpression o qualsiasi tipo derivato da LambdaExpression in IL eseguibile. Altri tipi di espressione non possono essere convertiti direttamente nel codice. Questa restrizione ha poco effetto in pratica. Le espressioni lambda sono gli unici tipi di espressioni da eseguire convertendo in linguaggio intermedio eseguibile (IL). Si pensi a cosa significa eseguire direttamente un oggetto System.Linq.Expressions.ConstantExpression. Vuol dire qualcosa di utile?) Qualsiasi albero delle espressioni che è un System.Linq.Expressions.LambdaExpressionoggetto o un tipo derivato da LambdaExpression può essere convertito in IL. Il tipo di System.Linq.Expressions.Expression<TDelegate> espressione è l'unico esempio concreto nelle librerie .NET Core. Viene utilizzato per rappresentare un'espressione che si associa a qualsiasi tipo di delegato. Poiché questo tipo esegue il mapping a un tipo delegato, .NET può esaminare l'espressione e generare IL per un delegato appropriato che corrisponde alla firma dell'espressione lambda. Il tipo delegato è basato sul tipo di espressione. È necessario conoscere il tipo restituito e l'elenco di argomenti se si desidera utilizzare l'oggetto delegato in modo fortemente tipizzato. Il LambdaExpression.Compile() metodo restituisce il Delegate tipo . È necessario fare il cast al tipo delegato corretto per fare in modo che gli strumenti in fase di compilazione controllino l'elenco di argomenti o il tipo restituito.

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

Il System.Linq.Expressions.LambdaExpression tipo contiene LambdaExpression.Compile e LambdaExpression.CompileToMethod i membri che si userebbero per convertire un albero delle espressioni in codice eseguibile. Il Compile metodo crea un delegato. Il CompileToMethod metodo aggiorna un System.Reflection.Emit.MethodBuilder oggetto con IL 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 fornire un oggetto System.Runtime.CompilerServices.DebugInfoGenerator che riceve le informazioni di debug dei simboli per l'oggetto delegato generato. DebugInfoGenerator fornisce informazioni di debug complete sul delegato generato.

È possibile convertire un'espressione in un delegato usando 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 illustrato come eseguire un albero delle espressioni che rappresenta la generazione di un numero a una potenza creando un'espressione lambda ed eseguendola. Viene visualizzato il risultato, che rappresenta il numero elevato a una 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 cicli di vita

Il codice viene eseguito richiamando il delegato creato quando si chiama LambdaExpression.Compile(). Il codice precedente, add.Compile(), restituisce un delegato. Richiamare il delegato chiamando func(), che esegue il codice.

Tale delegato rappresenta il codice nell'albero delle espressioni. È possibile conservare l'handle del delegato e richiamarlo in un secondo momento. Non è necessario compilare l'albero delle espressioni ogni volta che si vuole eseguire il codice rappresentato. Tenere presente che gli alberi delle espressioni non sono modificabili e la compilazione dello stesso albero delle espressioni in un secondo momento crea un delegato che esegue lo stesso codice.

Attenzione

Non creare meccanismi di memorizzazione nella cache più sofisticati per migliorare le prestazioni evitando chiamate di compilazione non necessarie. Confrontando due alberi di espressioni arbitrarie per determinare se rappresentano lo stesso algoritmo è un'operazione dispendiosa in termini di tempo. Il tempo di calcolo risparmiato evitando qualsiasi chiamata aggiuntiva a LambdaExpression.Compile() è probabilmente più che utilizzato dal tempo in cui viene eseguito il codice che determina se due alberi delle espressioni diversi generano lo stesso codice eseguibile.

Avvertenze

La compilazione di un'espressione lambda in un delegato e la chiamata di tale delegato è una delle operazioni più semplici che è possibile eseguire con un albero delle espressioni. Tuttavia, anche con questa semplice operazione, ci sono avvertenze che è necessario essere consapevoli di.

Le espressioni lambda creano le chiusure su qualsiasi variabile locale a cui viene fatto riferimento nell'espressione. È necessario garantire che tutte le variabili che farebbero parte del delegato siano utilizzabili nella posizione in cui si chiama Compilee quando si esegue il delegato risultante. Il compilatore garantisce che le variabili siano incluse nell'ambito. Tuttavia, se l'espressione accede a una variabile che implementa IDisposable, è possibile che il codice possa eliminare l'oggetto mentre è ancora mantenuto dall'albero delle espressioni.

Ad esempio, questo codice funziona correttamente, perché 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 constantlocale . Tale variabile è accessibile in qualsiasi momento in un secondo momento, quando viene eseguita la funzione restituita da CreateBoundFunc .

Si consideri tuttavia la classe seguente (piuttosto contrivata) 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 illustrato 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 ha chiuso sopra l'oggetto constant, che è stato smaltito. È stato eliminato perché è stato dichiarato in un'istruzione using .

Ora, quando si esegue il delegato restituito da questo metodo, viene generata un'eccezione di tipo ObjectDisposedException al momento dell'esecuzione.

Sembra strano avere un errore di runtime che rappresenta un costrutto in fase di compilazione, ma questo è il mondo in cui ti trovi quando si lavora con gli alberi delle espressioni.

Esistono numerose permutazioni di questo problema, quindi è difficile offrire indicazioni generali per evitarlo. Prestare attenzione all'accesso alle variabili locali durante la definizione delle espressioni e prestare attenzione all'accesso allo stato nell'oggetto corrente (rappresentato da this) durante la creazione di un albero delle espressioni 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 richiamato il delegato risultante. Incontri un ReferencedAssemblyNotFoundException nei casi in cui non è presente.

Riassunto

Gli alberi delle espressioni che rappresentano espressioni lambda possono essere compilati per creare un delegato che puoi eseguire. Gli alberi delle espressioni forniscono un meccanismo per eseguire il codice rappresentato da un albero delle espressioni.

L'albero delle espressioni rappresenta il codice che verrebbe eseguito per qualsiasi costrutto specificato creato. Finché l'ambiente in cui si compila ed esegue il codice corrisponde all'ambiente in cui si crea l'espressione, tutto funziona come previsto. In caso contrario, gli errori sono prevedibili e vengono rilevati nei primi test di qualsiasi codice usando gli alberi delle espressioni.