식 트리(expression tree)
람다를 이용하면 식의 요소들을 트리로 표현하는 데이터 구조인 식 트리(expression tree)를 만드는 것도 가능하다. 식 트리를 이용하면, 트리를 따라가며 해석하거나 특정 노드를 수정해서 코드에 변화를 줄 수 있다. 컴파일러 측면에서는 식 트리를 추상 구문 트리(abstract syntax tree, AST)라고 부른다.
대리자에 람다식을 할당하는 코드를 살펴보자.
private static Func<int, int, int> AreaREctangleDelegate = (a, b) => a * b;
이 문은 다음 세 구역으로 나눈다.
- 대리자 변수 선언 : Func<int, int, int> AreaRectangleDelegate
- 대입(할당) 연산자 : =
- 람다식 : (a, b) => a* b
이제 이 문을 데이터로 바꿔볼 것이다. 그러기 위해 Expression<T> 형식의 인스턴스를 만들어야 하는데, 여기서 T는 대리자 형식이다. Expression<T> 형식은 System.Linq.Expression 네임스페이스에서 정의하고 있으며, 이것을 이용해서 다음처럼 앞의 코드를 식으로 변환할 수 있다.
using System;
using System.Linq.Expressions;
public partial class Program
{
static void Main(string[] args)
{
Expression<Func<int, int, int>> expression = (a, b) => a * b;
}
}
이렇게 Expression<T> 형식으로 만든 식 트리는 하나의 데이터 구조를 이루며, 실행 가능한 코드는 아니다.
Expression<T> 클래스는 다음 네 개의 핵심 속성을 제공한다.
- Body : 식의 본문을 포함한다.
- Parameters : 람다식의 매개 변수를 포함한다.
- NodeType : 트리를 구성하는 노드의 ExpressionType 값을 가진다.
- Type : 식의 정적 형식을 포함한다.
expression 변수에 중단덤을 설정하고 F5를 사용해 디버그 프로세스를 시작해 보자. 다음은 중단점을 찍은 줄을 실행한 직후 Visual Studio의 지역 창에 expression 변수를 확장한 모습이다.
Body 속성은 {(a + b)}이고, NodeType은 Lambda, Type은 Func 대리자 형식 정보를 가지고 있다.
Body 속성의 Left 속성은 {a}이고 Right 속성은 {b}이다. 이 속성들을 이용하면 프로그램에서 식 트리를 탐색할 수 있는데, exploreBody {} 메서드는 Body 속성 값들을 탐색하는 예다.
using System;
using System.Linq.Expressions;
public partial class Program
{
static void Main(string[] args)
{
Expression<Func<int, int, int>> expression = (a, b) => a * b;
exploreBody(expression);
}
private static void exploreBody(Expression<Func<int, int, int>> expr)
{
BinaryExpression body = expr.Body as BinaryExpression;
ParameterExpression left = body.Left as ParameterExpression;
ParameterExpression right = body.Right as ParameterExpression;
Console.WriteLine(expr.Body);
Console.WriteLine($"The left part of the expression: {left.Name}\n The NodeType: {body.NodeType}\n The right part: {right.Name}\n The Type: {body.Type}");
}
}
이처럼 exploreBody() 메서드는 Expression<T>의 Body 속성을 프로그램에서 접근한다. Body 내용을 가져오려면 BinaryExpression 형식이 필요하며, Left와 Right 속성 값을 담으려면 ParameterExpression 형식을 이용해야 한다.
필요하다면 expression을 컴파일해서 다시 코드화할수도 있다. expression은 다음과 같다.
Expression<Func<int, int, int>> expression = (a, b) => a * b;
compilingExpr() 메서드는 expression을 컴파일하고 실행한 결과를 출력하는 방법을 보여준다.
using System;
using System.Linq.Expressions;
public partial class Program
{
static void Main(string[] args)
{
Expression<Func<int, int, int>> expression = (a, b) => a * b;
compilingExpr(expression);
}
private static void compilingExpr(Expression<Func<int, int, int>> expr)
{
int a = 2;
int b = 3;
int comResult = expr.Compile()(a, b);
Console.WriteLine($"The result of expression {expr.Body} with a = {a} and b = {b} is {comResult}");
}
}
예제에서 Expression 클래스의 Compile() 메서드로 컴파일하는 방법을 자세히 살펴보자.
int comResult = expr.Compile()(a, b);
expr.Complie() 메서드는 식의 형식에 맞게 Func<int, int, int> 형식의 대리자를 만드는데, 이 대리자의 인수로 a와 b를 시그니처에 따라 전달하며 무명 메서드를 실행한 결과로 int 값을 반환한다.
출처
'프로그래밍 > C#' 카테고리의 다른 글
C# 재귀 호출 동작 방식 (1) | 2024.03.29 |
---|---|
C# string에서 16진수로 변환하기 (0) | 2024.03.28 |
C# 공변성(Covariance)이란 무엇인가? (0) | 2024.03.26 |
C# 제네릭 대리자(Generic Delegate) (1) | 2024.03.26 |
C# list 랜덤(random) 하게 섞기 (0) | 2024.03.21 |
댓글