식 트리 빌드
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);
x
및 y
에 대한 매개 변수 식을 만드는 것부터 시작합니다.
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의 최신 버전에도 적용됩니다.
.NET