Tworzenie drzew wyrażeń

Kompilator języka C# utworzył wszystkie drzewa wyrażeń, które zostały do tej pory wyświetlone. Utworzono wyrażenie lambda przypisane do zmiennej typizowanej Expression<Func<T>> jako typ lub podobny. W przypadku wielu scenariuszy tworzysz wyrażenie w pamięci w czasie wykonywania.

Drzewa wyrażeń są niezmienne. Niezmienne oznacza, że musisz skompilować drzewo z liści do katalogu głównego. Interfejsy API używane do tworzenia drzew wyrażeń odzwierciedlają ten fakt: metody używane do kompilowania węzła przyjmują wszystkie jego elementy podrzędne jako argumenty. Przyjrzyjmy się kilku przykładom, aby pokazać techniki.

Tworzenie węzłów

Zaczynasz od wyrażenia dodawania, z którym pracujesz w tych sekcjach:

Expression<Func<int>> sum = () => 1 + 2;

Aby utworzyć to drzewo wyrażeń, należy najpierw skonstruować węzły liścia. Węzły liścia są stałymi. Constant Użyj metody , aby utworzyć węzły:

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));

Następnie skompiluj wyrażenie dodawania:

var addition = Expression.Add(one, two);

Po utworzeniu wyrażenia dodawania utworzysz wyrażenie lambda:

var lambda = Expression.Lambda(addition);

To wyrażenie lambda nie zawiera żadnych argumentów. W dalszej części tej sekcji zobaczysz, jak mapować argumenty na parametry i tworzyć bardziej skomplikowane wyrażenia.

W przypadku wyrażeń takich jak ten można połączyć wszystkie wywołania w jedną instrukcję:

var lambda2 = Expression.Lambda(
    Expression.Add(
        Expression.Constant(1, typeof(int)),
        Expression.Constant(2, typeof(int))
    )
);

Tworzenie drzewa

W poprzedniej sekcji przedstawiono podstawy tworzenia drzewa wyrażeń w pamięci. Bardziej złożone drzewa zwykle oznaczają więcej typów węzłów i więcej węzłów w drzewie. Uruchomimy jeszcze jeden przykład i pokażemy dwa kolejne typy węzłów, które zwykle kompilujesz podczas tworzenia drzew wyrażeń: węzłów argumentów i węzłów wywołań metody. Utwórzmy drzewo wyrażeń, aby utworzyć to wyrażenie:

Expression<Func<double, double, double>> distanceCalc =
    (x, y) => Math.Sqrt(x * x + y * y);

Zacznij od utworzenia wyrażeń parametrów dla x i y:

var xParameter = Expression.Parameter(typeof(double), "x");
var yParameter = Expression.Parameter(typeof(double), "y");

Tworzenie wyrażeń mnożenia i dodawania jest zgodne ze wzorcem, który został już wyświetlony:

var xSquared = Expression.Multiply(xParameter, xParameter);
var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

Następnie należy utworzyć wyrażenie wywołania metody dla wywołania metody .Math.Sqrt

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) }) ?? throw new InvalidOperationException("Math.Sqrt not found!");
var distance = Expression.Call(sqrtMethod, sum);

Wywołanie GetMethod może zwrócić null wartość , jeśli metoda nie zostanie znaleziona. Najprawdopodobniej jest to spowodowane błędną nazwą metody. W przeciwnym razie może to oznaczać, że wymagany zestaw nie jest załadowany. Na koniec należy umieścić wywołanie metody w wyrażeniu lambda i zdefiniować argumenty w wyrażeniu lambda:

var distanceLambda = Expression.Lambda(
    distance,
    xParameter,
    yParameter);

W tym bardziej skomplikowanym przykładzie przedstawiono jeszcze kilka technik, które często trzeba utworzyć drzewa wyrażeń.

Najpierw należy utworzyć obiekty reprezentujące parametry lub zmienne lokalne przed ich użyciem. Po utworzeniu tych obiektów można ich używać w drzewie wyrażeń wszędzie tam, gdzie są potrzebne.

Po drugie, należy użyć podzestawu interfejsów API Emocje ion do utworzenia System.Reflection.MethodInfo obiektu, aby można było utworzyć drzewo wyrażeń w celu uzyskania dostępu do tej metody. Musisz ograniczyć się do podzbioru interfejsów API Emocje ion dostępnych na platformie .NET Core. Ponownie te techniki rozszerzają się na inne drzewa wyrażeń.

Szczegółowe kompilowanie kodu

Nie ograniczasz się do tego, co można utworzyć przy użyciu tych interfejsów API. Jednak bardziej skomplikowane drzewo wyrażeń, które chcesz skompilować, tym trudniej jest zarządzać i odczytywać kod.

Utwórzmy drzewo wyrażeń, które jest odpowiednikiem tego kodu:

Func<int, int> factorialFunc = (n) =>
{
    var res = 1;
    while (n > 1)
    {
        res = res * n;
        n--;
    }
    return res;
};

Powyższy kod nie skompiluje drzewa wyrażeń, ale po prostu delegata. Expression Przy użyciu klasy nie można skompilować instrukcji lambdas. Oto kod wymagany do utworzenia tej samej funkcjonalności. Nie ma interfejsu API do tworzenia while pętli, zamiast tego należy utworzyć pętlę zawierającą test warunkowy i docelową etykietę, aby wyłamać pętlę.

var nArgument = Expression.Parameter(typeof(int), "n");
var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value
LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,
// and decrements the value of 'n'
var block = Expression.Block(
    Expression.Assign(result,
        Expression.Multiply(result, nArgument)),
    Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.
BlockExpression body = Expression.Block(
    new[] { result },
    initializeResult,
    Expression.Loop(
        Expression.IfThenElse(
            Expression.GreaterThan(nArgument, Expression.Constant(1)),
            block,
            Expression.Break(label, result)
        ),
        label
    )
);

Kod do kompilowania drzewa wyrażeń dla funkcji czynnikowej jest nieco dłuższy, bardziej skomplikowany i jest pośmieszwany etykietami i instrukcjami podziału oraz innymi elementami, których chcesz uniknąć w naszych codziennych zadaniach kodowania.

W tej sekcji napisaliśmy kod, aby odwiedzić każdy węzeł w tym drzewie wyrażeń i zapisać informacje o węzłach utworzonych w tym przykładzie. Przykładowy kod można wyświetlić lub pobrać w repozytorium dotnet/docs w witrynie GitHub. Poeksperymentuj dla siebie, tworząc i uruchamiając przykłady.

Mapowanie konstrukcji kodu na wyrażenia

Poniższy przykład kodu przedstawia drzewo wyrażeń reprezentujące wyrażenie num => num < 5 lambda przy użyciu interfejsu API.

// Manually build the expression tree for
// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
    Expression.Lambda<Func<int, bool>>(
        numLessThanFive,
        new ParameterExpression[] { numParam });

Interfejs API drzew wyrażeń obsługuje również przypisania i wyrażenia przepływu sterowania, takie jak pętle, bloki warunkowe i try-catch bloki. Za pomocą interfejsu API można tworzyć drzewa wyrażeń, które są bardziej złożone niż te, które można utworzyć na podstawie wyrażeń lambda przez kompilator języka C#. W poniższym przykładzie pokazano, jak utworzyć drzewo wyrażeń, które oblicza współczynniki liczby.

// Creating a parameter expression.
ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.
ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.
LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.
BlockExpression block = Expression.Block(
    // Adding a local variable.
    new[] { result },
    // Assigning a constant to a local variable: result = 1
    Expression.Assign(result, Expression.Constant(1)),
        // Adding a loop.
        Expression.Loop(
           // Adding a conditional block into the loop.
           Expression.IfThenElse(
               // Condition: value > 1
               Expression.GreaterThan(value, Expression.Constant(1)),
               // If true: result *= value --
               Expression.MultiplyAssign(result,
                   Expression.PostDecrementAssign(value)),
               // If false, exit the loop and go to the label.
               Expression.Break(label, result)
           ),
       // Label to jump to.
       label
    )
);

// Compile and execute an expression tree.
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

Aby uzyskać więcej informacji, zobacz Generowanie metod dynamicznych za pomocą drzew wyrażeń w programie Visual Studio 2010, który ma zastosowanie również do nowszych wersji programu Visual Studio.