뇌를 자극하는 C# 4.0 프로그래밍/ 람다식

람다식, 너는 어디에서 왔니?

  • 람다식 (Lambda Expression)이란 1936년 수학자 알론조 처치 (Alonzo Church)가 발표한 람다 계산법(Lambda Calculus)에서 사용하는 식.
  • 알론조 처치의 제자인 존 메카시 (John McCarthy)가 이것을 프로그래밍 언어에 도입할 수 있겠다는 아이디어를 냈고 50년대 말에 LISP 이라는 언어를 만듦.

처음으로 만들어 보는 람다식

// 람다식 선언 방법
매개변수목록 => 식

// 람다식 사용 예시
delegate int Calculate (int a, int b);

static void Main(string[] args)
{
    // 두 개의 int 형식 매개 변수 a, b를 받아 이 둘을 더해 반환하는 익명 메소드를 람다식으로 선언.
    Calculate calc1 = (int a, int b) => a + b;

    // C# 컴파일러는 형식 유추(Type Inference)라는 기능이 있기 때문에 아래와 같이 선언해도 된다.
    Calculate calc2 = (a, b) => a + b;

    // 델리게이트 익명 메소드를 이용하면 아래와 같이 기술된다.
    Calculate calc3 = delegate(int a, int b)
                      {
                          return a + b;
                      }
}
  • => 연산자는 ‘입력’ 연산자이다. 이 연산자가 하는 역할은 그저 매개변수를 전달하는 것 뿐이다.
  • C#에 델리게이트 익명 메소드와 람다식이 공존하는 이유는 람다식이 더 늦게 도입됐기 때문. 델리게이트를 이용한 익명 메소드는 C# 2.0 버전에서 도입되었고 람다식은 C# 3.0 버전에 도입되었다.

문 형식의 람다식

// 문 형식의 람다식 선언 방법
(매개변수목록) => {
                      문장 1;
                      문장 2;
                      문장 3;
                  }

// 문 형식의 람다식 사용 예시
delegate void DoSomething ();

static void Main(string[] args)
{
    // 매개 변수가 없으면 ()에 아무것도 넣지 않는다.
    DoSomething DoIt = ( ) => {
                                  Console.WriteLine("1");
                                  Console.WriteLine("2");
                                  Console.WriteLine("3");
                              }
}

Func와 Action으로 더 간편하게 무명 함수 만들기

  • 익명 메소드나 무명 함수를 만들기 위해선 매번 별개의 델리게이트를 선언해야 하는데, 이게 꽤나 번거롭다. 이 번거로움을 해결하기 위해 .NET Framework에는 Func 델리게이트와 Action 델리게이트가 미리 선언되어 있다.
  • Func 델리게이트는 결과를 반환하는 메소드를 참조하고, Action 델리게이트는 결과를 반환하지 않는 메소드를 참조한다.

Func 델리게이트

// Func 델리게이트 사용 예시
Func<int> func1 = () => 10; // 입력 매개변수는 없으며 반환은 int
Func<int, int> func2 = (x) => x * 2; // 입력 매개변수는 int, 출력 매개 변수도 int
Func<int, int, int> func3 = (x, y) => x + y; // 입력 매개변수는 int, int 출력 매개 변수도 int
  • .NET Framework에는 17가지 버전의 Func 델리게이트가 준비되어 있다.
  • Func 델리게이트는 가장 마지막에 있는 매개 변수가 반환 형식이 된다. Func 델리게이트의 매개변수의 수는 ‘입력 매개 변수 수 + 출력용 매개 변수 1개’가 된다.

Action 델리게이트

// Action 델리게이트 사용 예시
Action act1 = () => Console.WriteLine("Action()");
act1();

int result = 0;
Action<int> act2 = (x) => result = x * x; // 람다식 밖에 선언된 result에 x * x가 대입 된다.
act2(3);
Console.WriteLine("result: {0}", result); // 9가 출력 됨.

Action<double, double> act3 = (x, y) => {
            double pi = x / y;
            Console.WriteLine("Action<T1, T2>({0}, {1}): {2}", x, y, pi);
        }
act3(22.0, 7.0);

식 트리

  • 트리는 위 그림과 같이 노드(Node: 마디)로 구성되며, 각 노드는 서로 부모-자식 관계로 연결된다. 위 그림에서 A의 자식 노드는 B, C가 되고 B의 자식 노드는 D, E가 된다.
  • 한편 최상위 노드인 A를 루트(Root) 노드라고 하며 가장 끝에 있는 D, E, F, G와 같은 노드를 잎(Leaf) 노드 또는 단말(Terminal) 노드라고 한다.
  • 트리 자료 구조는 부모 노드가 여러 개의 자식 노드를 가질 수도 있지만, 식 트리는 한 부모 노드가 단 두 개만의 자식 노드를 가질 수 있는 이진 트리(Binary Tree)이다.

  • 식 트리란 식을 트리로 표현한 자료 구조를 말한다. 예컨대 1*2+(7-8)을 식 트리로 표현하면 위 그림과 같아진다.
  • 식 트리에서 연산자는 부모 노드가 되며, 피연산자는 자식 노드가 된다.
    • 위 그림에서 1*2에서 *는 부모 노드, 1과 2는 *의 자식 노드가 딘다.
    • 이런 식으로 트리의 잎 노드부터 계산해서 루트 노드까지 올라가면 전체 식의 결과를 얻을 수 있다.
  • 식 트리 자료 구조는 컴파일러나 인터프리터를 제작하는데 응용된다. 컴파일러는 프로그래밍 언어의 문법을 따라 작성된 소스 코드를 분석해서 식 트리로 만든 후 이를 바탕으로 실행 파일을 만든다.
  • 완전한 C# 컴파일러는 아니지만, C#은 프로그래머가 C# 코드 안에서 직접 식 트리를 조립하고 컴파일해서 사용할 수 있는 기능을 제공한다. 다시 말해 런타임 중에 동적으로 무명 함수를 만들어 사용할 수 있게 해준다는 뜻이다.
  • 식 트리를 다루는데 필요한 클래스들은 .NET Framework의 System.Linq.Expressions 네임스페이스 안에 준비되어 있다.
Expression의 파생 클래스 설명
BinaryExpression 이항 연산자(+에서부터 <=까지)를 갖는 식을 표현한다.
BlockExpression 변수를 정의할 수 있는 식을 갖는 블록을 표현한다.
ConditionalExpression 조건 연산자가 있는 식을 나타낸다.
ConstantExpression 상수가 있는 식을 나타낸다.
DefaultExpression 형식(type)이나 비어 있는 식의 기본값을 표현한다.
DynamicExpression 동적 작업을 나타낸다.
GotoExpression return, break, continue, goto와 같은 점프문을 나타낸다.
IndexExpression 배열의 인덱스 참조를 나타낸다.
InvocationExpression 델리게이트나 람다식 호출을 나타낸다.
LabelExpression 레이블을 나타낸다.
LambdaExpression 람다식을 나타낸다.
ListInitExpression 컬렉션 이니셜라이저가 있는 생성자 호출을 나타낸다.
LoopExpression 무한 반복을 나타낸다.
MemberExpression 객체의 필드나 속성을 나타낸다.
MemberInitExpression 생성자를 호출하고 새 객체의 멤버를 초기화하는 동작을 나타낸다.
MethodCallExpression 메소드 호출을 나타낸다.
NewArrayExpression 새 배열의 생성과 초기화를 나타낸다.
NewExpression 생성자 호출을 나타낸다.
ParameterExpression 명명된 매개 변수를 나타낸다.
RuntimeVariablesExpression 변수에 댛나 런타임 읽기/쓰기 권한을 제공한다.
SwitchExpression 다중 선택 제어 식을 나타낸다.
TryExpression try~catch~finally 블록을 나타낸다.
TypeBinaryExpression 형식 테스트를 비롯한 형식(type)과 식(expression)의 연산을 나타낸다.
UnaryExpression 단항 연산자를 갖는 식을 나타낸다.

 

  • 위 표의 클래스들은 Expression 클래스의 파생 클래스들이다. Expression 클래스는 식 트리를 구성하는 노드를 표현한다. 따라서 Expression을 상속 받는 위 클래스들이 식 트리의 각 노드를 표현할 수 있게 된다.

식 트리 예제

/* 
 * 식 트리 만들기
*/

// 팩토리 메서드 패턴
Expression const1 = Expression.Constant(1); // 상수 1
Expression param1 = Expression.Parameter(typeof(int), "x"); // 매개 변수 x

Expression exp = Expression.Add(const1, param1); // 1 + x

// 여기까지는 식을 트리로 표현한 것에 불과하다.
// exp는 실행 가능한 상태가 아니고 그저 '데이터' 상태에 머물러 있다는 것
// exp가 자신의 트리 자료 구조 안에 정의되어 있는 식을 실행할 수 있으려면 람다식으로 컴파일 되어야 한다.
// 람다식으로의 컴파일은 Expression<TDelegate> 클래스를 이용한다.

Expression<Func<int, int>> lamda1 = Expresison<Func<int, int>>.Lambda<Func<int, int>>( exp, new ParameterExpression[]{ (ParameterExpression)param1 } );

Func<int, int> compiledExp = lambda1.Compile(); // 실행 가능한 코드로 컴파일

Console.WriteLine(compileExp(3)); // 코드가 1+x 이므로 4가 출력 됨.


/*
 * 람다식으로 식 트리 만들기
*/
// 2개의 인자를 받아서 1개를 출력하는 무명 함수
Expression<Func<int, int, int>> expression = (a, b) => 1 * 2 + (a - b);

// 위의 람다식으로 쓰여진 식을 컴파일한다.
Func<int, int, int> func = expresion.Compile();

Console.WriteLine("1 * 2 + ({0}-{1}) = {2}", 7, 8, func(7, 8));
  • 람다식을 이용하면 간편하게 식 트리를 만들 수 있지만 ‘동적으로’ 식 트리를 만들기는 어렵다. Expression 형식은 불변(Immutable)이기 때문에 한 번 인스턴스가 만들어지고 난 후에는 변경할 수가 없기 때문.
  • 식 트리는 코드를 “데이터”로서 보관할 수 있다. 이것은 파일에 저장할 수도 있고 네트워크를 통해 다른 프로세스에 전달할 수도 있다. 심지어 코드를 담고 있는 트리 데이터를 데이터베이스 서버에 보내서 실행시킬 수도 있다.
It's only fair to share...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기