Dela via


Kö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>. Om du vill köra dessa uttrycksträd anropar du Compile metoden för att skapa ett körbart ombud och anropar sedan ombudet.

Kommentar

Om typen av ombud inte är känd, det vill säga lambda-uttrycket är av typen LambdaExpression och inte Expression<TDelegate>, anropar DynamicInvoke du metoden på ombudet i stället för att anropa det 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.

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. Ombudstypen 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 skicka den till rätt ombudstyp 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 också ange en System.Runtime.CompilerServices.DebugInfoGenerator som tar emot information om symbolfelsökning för det genererade ombudsobjektet. DebugInfoGenerator Innehåller fullständig felsökningsinformation om det genererade ombudet.

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 strömmen, 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

Körning och livslängd

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.)

Varning

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 ombudet kan användas på den plats där du anropar Compileoch när du kör det resulterande ombudet. 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;
    }
}

Ombudet som returneras från den här metoden har stängts över objektet constant , som har tagits bort. (Den har tagits bort eftersom den deklarerades i en using instruktion.)

Nu när du kör ombudet som returneras från den här metoden får du ett ObjectDisposedException utlöses 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 har träffats av en ReferencedAssemblyNotFoundException i fall där den inte finns.

Sammanfattning

Uttrycksträd som representerar lambda-uttryck kan kompileras för att skapa ett ombud som du kan kö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.