Sdílet prostřednictvím


Spouštění stromů výrazů

Strom výrazu je datová struktura, která představuje určitý kód. Není kompilovaný a spustitelný kód. Pokud chcete spustit kód .NET reprezentovaný stromem výrazu, musíte ho převést na spustitelné instrukce IL. Spuštění stromu výrazů může vrátit hodnotu, nebo může provést pouze akci, jako je volání metody.

Lze spustit pouze stromy výrazů, které představují výrazy lambda. Stromy výrazů, které představují výrazy lambda, jsou typu LambdaExpression nebo Expression<TDelegate>. Chcete-li tyto stromy výrazů spustit, zavolejte metodu Compile pro vytvoření spustitelného delegáta a pak vyvoláte delegáta.

Poznámka:

Pokud typ delegáta není znám, to znamená, výraz lambda je typu LambdaExpression , a ne Expression<TDelegate>, volání DynamicInvoke metody na delegáta namísto jeho přímé vyvolání.

Pokud strom výrazu nepředstavuje výraz lambda, můžete vytvořit nový výraz lambda, který má původní strom výrazu jako jeho tělo, zavoláním Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) metody. Potom můžete výraz lambda spustit, jak je popsáno výše v této části.

Výrazy lambda pro funkce

Můžete převést libovolnou lambdaExpression nebo jakýkoli typ odvozený z LambdaExpression na spustitelný il. Jiné typy výrazů nelze přímo převést na kód. Toto omezení má v praxi malý vliv. Výrazy lambda jsou jedinými typy výrazů, které chcete provést převodem na spustitelný zprostředkující jazyk (IL). (Zamyslete se nad tím, co by znamenalo přímé spuštění System.Linq.Expressions.ConstantExpression. Znamenalo by to něco užitečného?) Jakýkoli strom výrazu, který je nebo System.Linq.Expressions.LambdaExpressiontyp odvozený z LambdaExpression , lze převést na IL. Typ výrazu System.Linq.Expressions.Expression<TDelegate> je jediný konkrétní příklad v knihovnách .NET Core. Používá se k reprezentaci výrazu, který se mapuje na libovolný typ delegáta. Vzhledem k tomu, že tento typ se mapuje na typ delegáta, může .NET výraz prozkoumat a vygenerovat il pro odpovídající delegáta, který odpovídá podpisu výrazu lambda. Typ delegáta je založen na typu výrazu. Pokud chcete objekt delegáta použít silným typem, musíte znát návratový typ a seznam argumentů. Metoda LambdaExpression.Compile() vrátí Delegate typ. Musíte ho přetypovat na správný typ delegáta, aby všechny nástroje pro kompilaci kontrolovaly seznam argumentů nebo návratový typ.

Ve většině případů existuje jednoduché mapování mezi výrazem a odpovídajícím delegátem. Například strom výrazu reprezentovaný Expression<Func<int>> by byl převeden na delegáta typu Func<int>. Pro výraz lambda s libovolným návratovým typem a seznamem argumentů existuje typ delegáta, který je cílovým typem spustitelného kódu reprezentovaný tímto výrazem lambda.

Typ System.Linq.Expressions.LambdaExpression obsahuje LambdaExpression.Compile a LambdaExpression.CompileToMethod členy, které byste použili k převodu stromu výrazů na spustitelný kód. Metoda Compile vytvoří delegáta. Metoda CompileToMethod aktualizuje System.Reflection.Emit.MethodBuilder objekt pomocí IL, který představuje zkompilovaný výstup stromu výrazů.

Důležité

CompileToMethod je k dispozici pouze v rozhraní .NET Framework, nikoli v .NET Core nebo .NET 5 a novější.

Volitelně můžete také zadat System.Runtime.CompilerServices.DebugInfoGenerator , který obdrží informace o ladění symbolů pro vygenerovaný delegovaný objekt. Poskytuje DebugInfoGenerator úplné informace o ladění o vygenerovaném delegátu.

Výraz byste převedli na delegáta pomocí následujícího kódu:

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

Následující příklad kódu ukazuje konkrétní typy používané při kompilaci a spuštění stromu výrazu.

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.

Následující příklad kódu ukazuje, jak spustit strom výrazu, který představuje zvýšení čísla na mocninu vytvořením výrazu lambda a jeho spuštěním. Zobrazí se výsledek, který představuje číslo umocněné na mocninu.

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

Spouštění a životnosti

Kód spustíte vyvoláním delegáta vytvořeného při volání LambdaExpression.Compile(). Předchozí kód add.Compile()vrátí delegáta. Tento delegát vyvoláte voláním func(), který spustí kód.

Tento delegát představuje kód ve stromu výrazů. Popisovač pro daného delegáta si můžete ponechat a vyvolat ho později. Strom výrazů nemusíte kompilovat pokaždé, když chcete spustit kód, který představuje. (Nezapomeňte, že stromy výrazů jsou neměnné a pozdější kompilace stejného stromu výrazů vytvoří delegáta, který spustí stejný kód.)

Upozornění

Nevytvávejte žádné sofistikovanější mechanismy ukládání do mezipaměti pro zvýšení výkonu tím, že se vyhnete zbytečným voláním kompilace. Porovnáním dvou stromů libovolných výrazů určíte, jestli představují stejný algoritmus, je časově náročná operace. Výpočetní doba, kterou ušetříte, aby se zabránilo dalším voláním LambdaExpression.Compile() , která budou pravděpodobně vyšší než spotřebovaná časem provádění kódu, který určuje, jestli dva různé stromy výrazů vedou ke stejnému spustitelnému kódu.

Upozornění

Kompilace výrazu lambda delegátovi a vyvolání tohoto delegáta je jednou z nejjednodušších operací, které můžete provést se stromem výrazu. I při této jednoduché operaci však musíte mít na paměti upozornění.

Výrazy lambda vytvářejí uzavření pro všechny místní proměnné, na které se ve výrazu odkazuje. Musíte zaručit, že všechny proměnné, které by byly součástí delegáta, jsou použitelné v místě, kde voláte Compile, a při spuštění výsledného delegáta. Kompilátor zajišťuje, aby proměnné byly v oboru. Pokud ale výraz přistupuje k proměnné, která implementuje IDisposable, je možné, že váš kód může objekt odstranit, zatímco je stále uchovávaný stromem výrazu.

Tento kód například funguje správně, protože int neimplementuje 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;
}

Delegát zachytil odkaz na místní proměnnou constant. K této proměnné se dostanete kdykoli později, když funkce vrácená spuštěním CreateBoundFunc .

Zvažte však následující (spíše kontraktivní) třídu, která implementuje 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;
    }
}

Pokud ho použijete ve výrazu, jak je znázorněno v následujícím kódu, získáte System.ObjectDisposedException při spuštění kódu odkazovaného vlastností 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;
    }
}

Delegát vrácený z této metody se zavřel nad constant objektem, který byl odstraněn. (Byl uvolněn, protože byl deklarován v using příkazu.)

Nyní, když spustíte delegát vrácený z této metody, máte ObjectDisposedException vyvolán v okamžiku spuštění.

Zdá se, že při práci se stromy výrazů dochází k chybě za běhu, která představuje konstruktor kompilace, ale to je svět, který zadáte.

Existuje mnoho permutací tohoto problému, takže je obtížné nabídnout obecné pokyny, abyste se tomu vyhnuli. Při definování výrazů buďte opatrní při přístupu k místním proměnným a při vytváření stromu výrazů vráceného prostřednictvím veřejného rozhraní API buďte opatrní při přístupu ke stavu v aktuálním objektu (reprezentované).this

Kód ve výrazu může odkazovat na metody nebo vlastnosti v jiných sestaveních. Toto sestavení musí být přístupné při definování výrazu, při kompilaci a při vyvolání výsledného delegáta. V případech, kdy není k dispozici, jste se setkal s určitou osobou ReferencedAssemblyNotFoundException .

Shrnutí

Stromy výrazů, které představují výrazy lambda, lze zkompilovat a vytvořit delegáta, který můžete spustit. Stromy výrazů poskytují jeden mechanismus pro spuštění kódu reprezentovaný stromem výrazu.

Strom výrazů představuje kód, který by se spustil pro libovolný daný konstruktor, který vytvoříte. Pokud prostředí, ve kterém kód zkompilujete a spustíte, odpovídá prostředí, ve kterém výraz vytvoříte, vše funguje podle očekávání. Pokud k tomu nedojde, chyby jsou předvídatelné a jsou zachyceny v prvních testech jakéhokoli kódu pomocí stromů výrazů.