식 트리 빌드

C# 컴파일러는 지금까지 본 모든 식 트리를 만들었습니다. Expression<Func<T>> 또는 유사한 형식으로 형식이 할당된 변수에 할당된 람다 식을 만들었습니다. 많은 시나리오에서 런타임 시 메모리에 식을 빌드합니다.

식 트리는 변경할 수 없습니다. 변경할 수 없다는 것은 리프에서 루트까지 위로 트리를 작성해야 한다는 의미입니다. 식 트리를 작성하는 데 사용할 API에 이 사실이 반영됩니다. 즉, 노드를 작성하는 데 사용되는 메서드는 모든 자식을 인수로 사용합니다. 이 기술을 보여 주는 몇 가지 예제를 살펴보겠습니다.

노드 만들기

다음 섹션 전체에서 작업해 온 덧셈 식으로 시작합니다.

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

해당 식 트리를 구성하려면 먼저 리프 노드를 구성합니다. 리프 노드는 상수입니다. Constant 메서드를 사용하여 노드를 만듭니다.

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

다음으로 추가 식을 빌드합니다.

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

덧셈 식을 빌드한 후에는 람다 식을 만듭니다.

var lambda = Expression.Lambda(addition);

이 람다 식에는 인수가 없습니다. 이 섹션의 뒷부분에서는 인수를 매개 변수에 매핑하고 더 복잡한 식을 작성하는 방법을 살펴봅니다.

이와 같은 식의 경우 모든 호출을 단일 문으로 결합할 수 있습니다.

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

트리 빌드

이전 섹션에서는 메모리에 식 트리를 빌드하는 기본 사항을 보여 주었습니다. 좀 더 복잡한 트리는 일반적으로 노드 유형과 트리의 노드가 더 많음을 의미합니다. 예제를 하나 더 실행하고 식 트리를 만들 때 일반적으로 작성하는 인수 노드와 메서드 호출 노드라는 노드 유형을 두 개 더 살펴보겠습니다. 식 트리를 작성하여 다음 식을 만들어 보겠습니다.

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

xy에 대한 매개 변수 식을 만드는 것부터 시작합니다.

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

곱하기와 더하기 식을 만들 때 이미 살펴본 패턴을 따릅니다.

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

다음으로 Math.Sqrt를 호출하기 위한 메서드 호출 식을 만들어야 합니다.

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

메서드를 찾을 수 없는 경우 GetMethod 호출은 null을 반환할 수 있습니다. 메서드 이름의 철자를 잘못 입력했기 때문일 가능성이 높습니다. 그렇지 않으면 필요한 어셈블리가 로드되지 않았음을 의미할 수 있습니다. 마지막으로 메서드 호출을 람다 식에 넣고 람다 식에 대한 인수를 정의해야 합니다.

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

더 복잡한 이 예제에서는 식 트리를 만드는 데 자주 필요한 기술을 몇 가지 더 확인할 수 있습니다.

먼저 매개 변수 또는 지역 변수를 나타내는 개체를 만든 후에 사용해야 합니다. 이러한 개체를 만들었으면 필요할 때마다 식 트리에서 사용할 수 있습니다.

두 번째로 해당 메서드에 액세스하는 식 트리를 만들 수 있도록 리플렉션 API의 하위 집합을 사용하여 System.Reflection.MethodInfo 개체를 만들어야 합니다. .NET Core 플랫폼에서 사용할 수 있는 리플렉션 API의 하위 집합으로 제한해야 합니다. 다시 말하지만 이러한 기술은 다른 식 트리로 확장됩니다.

심층적인 코드 빌드

이러한 API를 사용하여 빌드할 수 있는 항목으로 제한되지 않습니다. 그러나 작성하려는 식 트리가 복잡할수록 코드를 관리하고 읽기가 더 어려워집니다.

다음 코드에 해당하는 식 트리를 작성해 보겠습니다.

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

앞의 코드는 식 트리를 빌드하지 않고 단순히 대리자를 빌드했습니다. Expression 클래스를 사용하여 문 람다를 빌드할 수 없습니다. 다음은 동일한 기능을 빌드하는 데 필요한 코드입니다. while 루프를 빌드하기 위한 API는 없습니다. 대신 조건부 테스트가 포함된 루프와 루프에서 벗어날 레이블 대상을 빌드해야 합니다.

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

계승 함수에 대한 식 트리를 작성하는 코드는 훨씬 더 길고 더 복잡하며, 레이블과 break 문 및 일상적인 코딩 작업에서 방지하려는 기타 요소로 인해 복잡해집니다.

이 섹션에서는 이 식 트리의 모든 노드를 방문하고 이 샘플에서 만들어진 노드에 대한 정보를 작성하는 코드를 작성했습니다. GitHub의 dotnet/docs 리포지토리에서 샘플 코드를 보거나 다운로드할 수 있습니다. 샘플을 빌드하고 실행하여 직접 실험합니다.

코드 구문을 식에 매핑

다음 코드 예에서는 API를 사용하여 람다 식 num => num < 5를 나타내는 식 트리를 보여 줍니다.

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

식 트리 API는 루프, 조건부 블록 및 try-catch 블록과 같은 할당 및 제어 흐름 식도 지원합니다. API를 사용하면 C# 컴파일러를 통해 람다 식에서 작성할 수 있는 것보다 복잡한 식 트리를 만들 수 있습니다. 다음 예제에서는 숫자의 계승을 계산하는 식 트리를 만드는 방법을 보여 줍니다.

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

자세한 내용은 Generating Dynamic Methods with Expression Trees in Visual Studio 2010(Visual Studio 2010에서 식 트리를 사용하여 동적 메서드 생성)을 참조하세요. 이 내용은 Visual Studio의 최신 버전에도 적용됩니다.