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.

Spustit lze pouze ty stromy výrazů, které představují lambda výrazy. 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 vyvolejte delegáta.

Poznámka:

Pokud typ delegáta není znám, tedy výraz lambda je typu LambdaExpression, a ne Expression<TDelegate>, volejte metodu DynamicInvoke na delegátovi místo jeho přímého 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 libovolný 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 System.Linq.Expressions.LambdaExpression, nebo typ 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 delegátní typ, může .NET výraz prozkoumat a vygenerovat IL pro delegátní typ, 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ě typovaným způsobem, 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. Tento DebugInfoGenerator poskytuje úplné ladicí informace 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 demonstruje konkrétní typy použité při kompilaci a vykonávání stromu výrazů.

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

Provádění a doby ž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ů. Referenci na daného delegáta 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í dvou stromů libovolných výrazů za účelem zjištění, zda představují stejný algoritmus, je časově náročná operace. Výpočetní čas, který ušetříte tím, že se vyhnete dalším voláním LambdaExpression.Compile(), bude pravděpodobně více než spotřebován časem potřebným k provedení kódu, který určuje, zda 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é mají být 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 dosahu. 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é máte přístup kdykoli později, když se funkce vrácená CreateBoundFunc vykoná.

Však zvažte následující (spíše umělou) 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. (Bylo zrušeno, protože bylo deklarováno v příkazu using.)

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 zvláštní, že chybu za běhu programu představuje konstrukce při času kompilace, ale to je svět, do kterého vstupujete, když pracujete se stromy výrazů.

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ů, který je vrácen prostřednictvím veřejného rozhraní API, buďte opatrní při přístupu ke stavu v aktuálním objektu (reprezentovaném 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í přítomen, se setkáte s 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ů skutečně představuje kód, který by se spustil pro libovolný daný konstrukt, 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ů.