다음을 통해 공유


표현식 트리 생성

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

팩터리 함수에 대한 식 트리를 빌드하는 코드는 훨씬 더 길고 복잡하며, 일상적인 코딩 작업에서 피하려는 레이블 및 중단 문 및 기타 요소로 가득합니다.

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

코드 구성 요소를 식에 매핑하다

다음 코드 예제에서는 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.

자세한 내용은 Visual Studio 2010에서 식 트리를 사용하여 동적 메서드 생성을 참조하세요. 이 메서드는 이후 버전의 Visual Studio에도 적용됩니다.