자료 구조에서 말하는 이진 트리 자료 구조라고 생각하면 된다. 각 노드는 부모 - 자식 관계로 연결된다. 뿌리가 되는 노드를 루트(Root)노드라고 하며 루트로 부터 뻗어 나온 노드 중 가장 끝에 있는 노드를 잎(Leaf)노드, 또는 단말(Terminal)노드라고 한다.
평범한 트리 자료 구조에서는 부모 노드가 여러 개의 자식 노드를 가질 수도 있지만, 식 트리는 한 부모노드가 단 두 개만의 자식 노드를 가질수 있는 "이진 트리(Binary Tree)"이다.
식 트리(Expression Tree)란, 식을 트리로 표현한 자료 구조를 말한다.
class Program
{
static void Main(string[] args)
{
Expression const1 = Expression.Constant(1);
Expression const2 = Expression.Constant(2);
Expression expression = Expression.Add(const1, const2); //(1 + 2)
Expression<Func<int>> Cal = Expression<Func<int>>.Lambda<Func<int>>(expression, null);
Func<int> compiledFunc = Cal.Compile();
WriteLine(compiledFunc());
}
}
Expression클래스 자체는 추상 클래스(abstract)로서 Expression클래스에서 직접 객체를 생성할 수는 없고 대신 Expression클래스에서 파생된 클래스의 객체를 생성해야 하는데 이때 Expression의 팩토리 메서드를 통해 객체를 생성하고 있습니다. 팩토리 메서드는 클래스의 객체를 생성하는 메서드로 생성자 메서드만으로 부족할 때 별도로 만들어진 객체 생성 메서드입니다.
예제에서는 Expression의 Constanct() 팩토리 메서드를 통해서 ConstantExpression클래스의 인스턴스를 생성하고 있습니다. 앞서 얘기했듯이 ConstantExpression클래스는 Expreession의 파생 클래스이므로 ConstantExpression형식의 인스턴스는 Expression형식의 객체로 받아들일 수 있습니다.
ConstantExpression클래스는 상수를 생성하는 클래스로 1과 2라는 2개의 상수를 생성하고 있고 Add() 팩토리 메서드로 2개의 상수값을 더하는 BinaryExpression객체를 생성합니다. 이 객체는 1과 2라는 상수값을 더하고 그 결과를 반환하므로 다시 Func<int>
형식의 대리자로 표현될 수 있고 Lamda() 메서드를 통해 Func형식의 동적 무명 함수를 생성하고 있습니다.
마지막으로 이렇게 생성된 무명 함수는 Compile() 메서드를 통해 Func<int>
형식의 컴파일된 함수를 만들고 이를 호출하여 필요한 동작을 수행하게 됩니다.
예제에서 Expression의 파생 클래스로 ConstantExpression클래스나 BinaryExpression클래스를 사용했는데 그 외에 다음과 같은 클래스를 사용해 해당 식을 표현할 수 있습니다.
예제에서는 Expression<Func<int>>.Lambda<Func<int>>(expression, null);
부분에서 람다식 클래스의 파생 클래스인 Expression<T>
를 사용했습니다. 이를 대신해 람다식 자체를 사용하면 더 간략하게 식 트리를 구성할 수 있습니다.
class Program
{
static void Main(string[] args)
{
Expression<Func<int>> Cal = () => 1 + 2;
Func<int> func = Cal.Compile();
WriteLine(func());
}
}
Expression의 파생 클래스 | 설명 |
---|---|
BinaryExpression | 이항 연산자 식을 표현합니다. |
BlockExpression | 블록을 표현합니다. |
ConditionalExpression | 조건연산자 식을 표현합니다. |
ConstantExpression | 상수식을 표현합니다. |
DefaultExpression | 식의 기본값을 표현합니다. |
DynamicExpression | 동적 작업을 표현합니다. |
GotoExpression | 점프문을 표현합니다. |
IndexExpression | 배열의 인덱스를 표현합니다. |
InvocationExpression | 대리자나 람다식 호출을 표현합니다. |
LabelExpression | 레이블을 표현합니다. |
LambdaExpression | 람다식을 표현합니다. |
ListInitExpression | 컬렉션 생성자 호출을 표현합니다. |
LoopExpression | 무한루프를 표현합니다. |
MemberExpression | 객체 멤버를 표현합니다. |
NewArrayExpression | 배열생성및 초기화를 나타냅니다. |
NewExpression | 생성자 호출을 나타냅니다. |
ParameterExpression | 명명된 인수를 나타냅니다. |
RuntimeVariablesExpression | 변수에 대한 런타임 읽기/쓰기를 표현합니다. |
SwitchExpression | Switch문을 표현합니다. |
TryExpression | Try ~ Catch문을 표현합니다. |
TypeBinaryExpression | 형식(Type)및 식(Expression)의 연산을 표현합니다. |
UnaryExpression | 단항 연산자를 갖는 식을 표현합니다. |
C# 람다식의 성능에 대해서는 여러 의견이 있지만, 대체로 람다식을 사용하는 것이 성능 면에서 큰 문제가 없다는 것이 일반적인 견해입니다. 오히려 람다식을 사용하면 일반적인 메서드 호출보다 더 빠른 성능을 보일 수 도 있습니다. 이는 람다식을 컴파일러가 더 효율적으로 처리하기 때문입니다. 또한, 람다식은 익명 메서드를 만드는 것과 비슷하기 때문에, 익명 메서드와 비교해도 성능 상 차이가 크게 나지 않습니다.
람다식은 컴파일러가 코드를 생성하는 데 많은 자원을 사용할 수 있으므로, 매우 복잡한 람다식을 사용하면 성능 저하가 발생할 수 있습니다. 특히 반복문 내부에서 람다식을 사용하면 반복 횟수에 비례해서 성능이 저하될 수 있습니다. 또한, 람다식을 사용하면 메모리 할당이 더 많아질 수 있습니다. 람다식은 클래스를 생성하고, 그 클래스의 인스턴스를 생성하므로, 메모리 사용량이 증가할 수 있습니다. 이는 대규모 데이터 처리나 반복문에서 특히 두드러집니다. 따라서, 람다식을 사용할 때는 성능을 고려해서 적절하게 사용하는 것이 좋습니다. 간단히 작업에는 람다식을 사용해도 문제가 없지만, 복잡한 작업에는 성능 저하를 방지하기 위해 다른 방법을 고려하는 것이 좋습니다.