Dela via


Utföra uttrycksträd

Ett uttrycksträd är en datastruktur som representerar viss kod. Den är inte kompilerad och körbar kod. Om du vill köra .NET-koden som representeras av ett uttrycksträd måste du konvertera den till körbara IL-instruktioner. Att köra ett uttrycksträd kan returnera ett värde, eller så kan det bara utföra en åtgärd som att anropa en metod.

Endast uttrycksträd som representerar lambda-uttryck kan köras. Uttrycksträd som representerar lambda-uttryck är av typen LambdaExpression eller Expression<TDelegate>. För att köra dessa uttrycksträd, anropa metoden Compile för att skapa en körbar delegering och anropa sedan delegeringen.

Anmärkning

Om typen av delegering inte är känd, det vill säga att lambda-uttrycket är av typen LambdaExpression och inte Expression<TDelegate>, ska du anropa DynamicInvoke-metoden på delegeringen i stället för att anropa den direkt.

Om ett uttrycksträd inte representerar ett lambda-uttryck kan du skapa ett nytt lambda-uttryck som har det ursprungliga uttrycksträdet som brödtext genom att anropa Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) metoden. Sedan kan du köra lambda-uttrycket enligt beskrivningen tidigare i det här avsnittet.

Konvertering av lambda-uttryck till funktioner

Du kan konvertera valfri LambdaExpression eller någon typ som härletts från LambdaExpression till körbar IL. Andra uttryckstyper kan inte konverteras direkt till kod. Denna begränsning har liten effekt i praktiken. Lambda-uttryck är de enda typer av uttryck som du vill köra genom att konvertera till körbart mellanliggande språk (IL). (Tänk på vad det skulle innebära att köra en System.Linq.Expressions.ConstantExpression. Skulle det betyda något användbart?) Alla uttrycksträd som är en System.Linq.Expressions.LambdaExpression, eller en typ som härleds från LambdaExpression kan konverteras till IL. Uttryckstypen System.Linq.Expressions.Expression<TDelegate> är det enda konkreta exemplet i .NET Core-biblioteken. Den används för att representera ett uttryck som mappar till valfri delegattyp. Eftersom den här typen mappas till en ombudstyp kan .NET undersöka uttrycket och generera IL för ett lämpligt ombud som matchar lambda-uttryckets signatur. Delegattypen baseras på uttryckstypen. Du måste känna till returtypen och argumentlistan om du vill använda ombudsobjektet på ett starkt skrivet sätt. Metoden LambdaExpression.Compile() returnerar Delegate typen. Du måste konvertera den till rätt delegattyp för att eventuella kompileringsverktyg ska kontrollera argumentlistan eller returtypen.

I de flesta fall finns det en enkel mappning mellan ett uttryck och dess motsvarande ombud. Ett uttrycksträd som representeras av Expression<Func<int>> konverteras till ett ombud av typen Func<int>. För ett lambda-uttryck med valfri returtyp och argumentlista finns det en ombudstyp som är måltypen för den körbara kod som representeras av det lambda-uttrycket.

Typen System.Linq.Expressions.LambdaExpression innehåller LambdaExpression.Compile och LambdaExpression.CompileToMethod medlemmar som du använder för att konvertera ett uttrycksträd till körbar kod. Metoden Compile skapar ett ombud. Metoden CompileToMethod uppdaterar ett System.Reflection.Emit.MethodBuilder objekt med IL:en som representerar de kompilerade utdata från uttrycksträdet.

Viktigt!

CompileToMethod är endast tillgängligt i .NET Framework, inte i .NET Core eller .NET 5 och senare.

Du kan valfritt också ange en System.Runtime.CompilerServices.DebugInfoGenerator som tar emot felsökningsinformation om symboler för det genererade ombudsobjektet. DebugInfoGenerator tillhandahåller felsökningsinformation i sin helhet om den genererade delegaten.

Du konverterar ett uttryck till ett ombud med hjälp av följande kod:

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

I följande kodexempel visas de konkreta typer som används när du kompilerar och kör ett uttrycksträd.

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.

Följande kodexempel visar hur du kör ett uttrycksträd som representerar att höja ett tal till en makt genom att skapa ett lambda-uttryck och köra det. Resultatet, som representerar det tal som upphöjts till en potens, visas.

// 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

Exekvering och livstid

Du kör koden genom att anropa ombudet som skapades när du anropade LambdaExpression.Compile(). Föregående kod, add.Compile(), returnerar ett ombud. Du anropar ombudet genom att anropa func(), vilket kör koden.

Ombudet representerar koden i uttrycksträdet. Du kan behålla handtaget till ombudet och anropa det senare. Du behöver inte kompilera uttrycksträdet varje gång du vill köra koden som det representerar. (Kom ihåg att uttrycksträd är oföränderliga och att kompilera samma uttrycksträd senare skapar ett ombud som kör samma kod.)

Försiktighet

Skapa inte några mer avancerade cachelagringsmekanismer för att öka prestandan genom att undvika onödiga kompileringsanrop. Att jämföra två träd med godtyckliga uttryck för att avgöra om de representerar samma algoritm är en tidskrävande åtgärd. Den beräkningstid som du sparar för att undvika extra anrop till LambdaExpression.Compile() förbrukas troligen mer än vid körning av kod som avgör om två olika uttrycksträd resulterar i samma körbara kod.

Varningar

Att kompilera ett lambda-uttryck till ett ombud och anropa det ombudet är en av de enklaste åtgärder som du kan utföra med ett uttrycksträd. Men även med denna enkla åtgärd finns det varningar du måste vara medveten om.

Lambda-uttryck skapar stängningar över alla lokala variabler som refereras till i uttrycket. Du måste garantera att alla variabler som ingår i delegeringen kan användas på den plats där du utför anropet Compile, och när du kör den resulterande delegeringen. Kompilatorn ser till att variablerna finns i omfånget. Men om uttrycket använder en variabel som implementerar IDisposableär det möjligt att koden kan ta bort objektet medan det fortfarande finns i uttrycksträdet.

Den här koden fungerar till exempel bra eftersom int den inte implementerar 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;
}

Ombudet har samlat in en referens till den lokala variabeln constant. Den variabeln används när som helst senare, när funktionen som returneras av CreateBoundFunc körs.

Tänk dock på följande (ganska invecklade) klass som implementerar 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;
    }
}

Om du använder det i ett uttryck som visas i följande kod får du en System.ObjectDisposedException när du kör koden som refereras av Resource.Argument egenskapen:

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

Delegeringen som returneras från den här metoden har kapslat in objektet constant, som har frigjorts. (Den har kastats bort eftersom den deklarerades i ett using uttalande.)

Nu när du kör delegaten som returneras från den här metoden får du ett ObjectDisposedException kastas vid tidpunkten för körningen.

Det verkar konstigt att ha ett körningsfel som representerar en kompileringstidskonstruktion, men det är den värld du anger när du arbetar med uttrycksträd.

Det finns många permutationer av det här problemet, så det är svårt att erbjuda allmän vägledning för att undvika det. Var försiktig med att komma åt lokala variabler när du definierar uttryck och var försiktig med att komma åt tillstånd i det aktuella objektet (representeras av this) när du skapar ett uttrycksträd som returneras via ett offentligt API.

Koden i uttrycket kan referera till metoder eller egenskaper i andra sammansättningar. Den sammansättningen måste vara tillgänglig när uttrycket definieras, när det kompileras och när det resulterande ombudet anropas. Du möts av en ReferencedAssemblyNotFoundException i de fall där den inte finns.

Sammanfattning

Uttrycksträd som representerar lambda-uttryck kan kompileras för att skapa en delegering som du kan utföra. Uttrycksträd ger en mekanism för att köra koden som representeras av ett uttrycksträd.

Uttrycksträdet representerar den kod som skulle köras för alla angivna konstruktioner som du skapar. Så länge miljön där du kompilerar och kör koden matchar miljön där du skapar uttrycket fungerar allt som förväntat. När det inte inträffar är felen förutsägbara och de fångas i dina första tester av någon kod med hjälp av uttrycksträden.