본문 바로가기
프로그래밍/C#

식 트리와 람다식

by bantomak 2024. 3. 27.

식 트리(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 값을 반환한다.

 

출처

 

Functional C# - 예스24

C# 개발자를 위한 함수형 프로그래밍 학습서다. 명령형 프로그래밍 방식과 함수형 프로그래밍을 비교하고, 함수형 프로그래밍을 위한 C#의 언어적 지원과 이를 이용한 실제 구현 예를 살펴보면

www.yes24.com

댓글