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
)
);
팩터리 함수에 대한 식 트리를 빌드하는 코드는 훨씬 더 길고 복잡하며, 일상적인 코딩 작업에서 피하려는 레이블 및 중단 문 및 기타 요소로 가득합니다.
이 섹션에서는 이 식 트리의 모든 노드를 방문하고 이 샘플에서 만든 노드에 대한 정보를 작성하는 코드를 작성했습니다. 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에도 적용됩니다.
.NET