포스트

C# Lambda-Expression | 람다식

C# Lambda-Expression | 람다식

Q


  • 람다식 ?
  • 람다식을 선언하고 사용하는 방법
  • 문 형식의 람다식을 작성하고 사용하는 방법
  • 식 트리

Lambda-Expression | 람다식


익명-메소드를 만드는 또 하나의 방법.
람다식으로 만드는 익명 메소드는 무명-함수(Anonymous Function)라는 이름으로 부른다.

Lambda-Expression 구현

(매개변수) => 식 같은 모양으로 만든다.

=>입력 연산자 (혹은 람다 연산자(Lambda Operator)).
=>를 중심으로 왼쪽에는 매개변수, 오른쪽에는 식이 위치.
그저 왼쪽 매개 변수를 오른쪽 식에 전달하는 역할.

1
(int x, int y) => x + y

Type-Inference | 타입 추론(형식 유추)

C# 컴파일러는 코드를 한층 더 간결하게 만들 수 있도록 타입 추론(형식 유추) (Type-Inference)를 지원한다.
타입 추론을 이용하면 람다식에서 매개 변수의 타입을 제거할 수 있다.

1
2
3
4
delegate int MyDelegate(int x, int y);

// C# 컴파일러가 `delegate`의 선언 코드로부터 무명 함수의 매개 변수 타입을 추론해낸다.
MyDelegate myDelegate = (x, y) => x + y;

Lambda-Expression Why?

람다식으로 만드는 무명함수는, 대리자를 이용한 익명 메소드보다 간결하다.

더 번거로운 방법과 더 간결한 방법이 동시에 존재하는 이유 ?
대리자를 이용한 익명 메소드는 C# 2.0에서 도입, 람다식은 C# 3.0에서 도입.
하위 호환성을 유지하기 위해 두 가지 방법을 모두 지원.

1
2
3
4
5
6
7
delegate int MyDelegate(int x, int y);

// 익명-메소드
MyDelegate myDelegate = delegate (int x, int y) { return x + y; };

// 람다식
MyDelegate myDelegate = (x, y) => x + y;

Statement Lambda | 문 형식의 람다식

식 형식의 람다식=> 연산자의 오른편에 식을 사용.
문 형식의 람다식=> 연산자의 오른편에 중괄호({})를 사용하여 코드 블록을 작성.

여러 줄의 코드를 사용할 수 있으며, return 키워드를 사용하여 반환값을 지정할 수 있다.
반환 형식이 없는 무명-함수를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
(int x, int y) => { /* ... */ }
(int x, int y) =>
{
    // ...
    return x + y;
}

( ) => { /* ... */ }
( ) =>
{
    // ...
    return 0;
}

Expression-Tree | 식-트리


식을 트리로 표현한 자료 구조.

예를 들어 1 * 2 + (7 - 8) 같은 식을 트리로 표현하면 아래과 같다.

1
2
3
4
5
   +
  / \
 *   -
/ \ / \
1 2 7 8

식 트리에서 연산자는 부모 노드가 되며, 피연산자는 자식 노드가 된다.
이렇게 식 트리로 표현된 식은 트리의 잎 노드끼리 계산해서 차례차례 루트까지 올라가면 전체 식의 결과를 얻을 수 있다.

Expression-Tree Why?

완전한 C# 컴파일러는 아니지만,
프로그래머가 코드 안에서 직접 식 트리를 조립하고 컴파일해서 사용할 수 있는 기능을 제공.
다시 말해, 프로그램 실행 중에 동적으로 무명 함수를 만들어 사용할 수 있게 해준다는 이야기.

식 트리 자료 구조는 컴파일러나 인터프리터를 제작하는 데도 응용.
컴파일러는 프로그래밍 언어의 문법을 따라 작성된 소스 코드를 분석해서 식 트리로 만든 후, 이를 바탕으로 실행 파일을 만든다.

Expression


Expression Class

Expression 클래스와 아이들(파생 클래스들).
식 트리를 다루는데 필요한 클래스.

System.Linq.Expressions네임스페이스에 정의되어 있다.

Expression 클래스는 식 트리를 구성하는 노드를 표현
그래서 Expression을 상속받는 파생 클래스들 역시 식 트리의 각 노드를 표현

Expression 클래스는 파생 클래스들의 객체를 생성하는 역할도 담당
파생 클래스의 인스턴스를 생성하는 정적 팩토리 메소드를 제공.

Expression 클래스 자체는 추상 클래스이므로 직접 인스턴스를 생성할 수는 없다.

Expression 구현

Expressin 클래스의 정적 팩토리 메소드를 통한 객체 생성.

프로그래머는 각 노드가 어떤 타입인지 신경 쓰지 않고 거침없이 Expression 형식의 참조를 선언해서 사용할 수 있다.
필요한 경우에는 각 세부 형식으로 형변환을 하면 되니까.
이것이 팩토리 메소드 패턴의 매력.

1
2
3
4
5
Expression const1 = Expression.Constant(1);// 상수 표현식 객체 생성
Expression param1 = Expression.Parameter(typeof(int), "x"); // 매개변수 표현식 객체 생성

Expression exp = Expression.Add(const1, param1); // 덧셈 표현식 객체 생성
// '1 + x' 를 나타내는 식 트리

Expression 컴파일/실행

식 트리는 결국 “식”을 트리로 표현한 것에 불과.
다시 말해, 실행 가능한 상태가 아니라 그저 “데이터” 상태에 머물러 있다는 말.

자신의 트리 자료 구조 안에 정의되어 있는 식을 실행할 수 있으려면 람다식으로 컴파일되어야 한다.

람다식으로의 컴파일은 Expression<TDelegate> 클래스를 이용한다.
(Expression <- LambdaExpression <- Expression<TDelegate>)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Expression const1 = Expression.Constant(1);
Expression param1 = Expression.Parameter(typeof(int), "x");

Expression exp = Expression.Add(const1, param1);

// 람다식 LambdaExpression (정확히는 파생 클래스인 Expression<TDelegate>) 생성
Expression<Func<int, int>> lambda1 = 
    Expression.Lambda<Func<int, int>>( // 반환값이 int이고 매개변수가 int인 람다식을 만듭니다.
        exp,
        new ParameterExpression[] { (ParameterExpression)param1 });

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

// 컴파일된 람다식을 실행
Console.WriteLine(compiledExp(3)); // Output: 4

Expression 람다식 활용

람다식을 이용하면 더 간단하게 식 트리를 만들 수 있다.

다만 이 경우에는 “동적으로” 식 트리를 만들기는 어려워진다.
Expression 형식은 “불변 (Immutable)”이기 때문에, 한 번 인스턴스를 만들면 수정할 수 없다.

1
2
3
4
Expression<Func<int, int>> lambda2 = (x) => 1 + x;
Func<int, int> compiledExp = lambda2.Compile();

Console.WriteLine(compiledExp(3)); // Output: 4

Expression So?

식 트리는 코드를 “데이터”로써 보관할 수 있다.
파일에 저장할 수도 있고 네트워크를 통해 다른 프로세스에 전달할 수도 있다.

심지어 코드를 담고 있는 식 트리 데이터를 데이터 베이스 서버에 보내서 실행시킬 수도 있다.

데이터 베이스 처리를 위한 식 트리는 LINQ에서 사용된다
LINQ를 이해하려면 람다식과 식 트리를 이해하고 있어야한다.

Expression-Bodied Member | 식 본문 멤버

‘메소드, 속성(인덱서), 생성자, 종료자’의 공통된 특징?
모두 클래스의 멤버로서 본문이 중괄호로 만들어져 있다.

이러한 멤버의 본문을 식(Expression)만으로 구현하는 것이 가능하다.
멤버 => 식 형태로 구현한다.

이렇게 식으로 구현된 멤버를 식 본문 멤버(Expression-Bodied Member)라고 부른다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyClass
{
    private List<int> list = new List<int>();

    // 메소드
    public void Add(int value) => list.Add(value);

    // 읽기 전용 속성
    public int Capacity => list.Capacity;

    // 읽기 / 쓰기 가능한 속성, 인덱서 [1]
    public int this[int index]
    {
        get => list[index];
        set => list[index] = value;
    }

    // 인덱서 [2]
    public int this[int index] => list[index];

    // 생성자, 종료자
    public MyClass(int value) => this.value = value;
    ~MyClass() => Console.WriteLine("소멸자 호출");
}

메모


  • λ-Expression
  • λ는 그리스 문자에서 L에 해당하는 문자
    • 알론조 처치는 이 문자를 함수 표기를 위한 기호로 ^를 사용했는데, 이는 프린터에서 어려움이 있어 λ로 바꿨다고 함
  • => 를 화살표 연산자라고도 부른다.
    • 이 경우, => 를 이용한 식을 화살표 함수라고 부른다.
  • ‘이것이 C#이다.’, ‘MSDN’
  • C# 람다 클로저 원리

  • LINQ
  • Factory-Pattern
  • 프로그래밍 언어론과 연관지어 (식트리)

Lambda-Expression 배경

알론조 처치(Alonzo Church)
1936년에 고안한 람다 계산법(Lambda Calculus: 람다 대수)에서 유래.

  • 람다 계산법 :
    • 수학 기초론을 연구하던 중, 분명하고 간결한 방법으로 함수를 묘사하기 위해 고안
    • 크게 함수의 정의와 변수, 그리고 함수의 적용으로 구성
    • 이 계산법에서는 모든 것이 함수로 이루어져 있음, 심지어 ‘0, 1, 2 …’ 같은 숫자도 함수로 표현
    • 따라서 람다 계산법에서는 어떤 값을 변수에 대입하고 싶으면 함수를 변수에 대입하며, 이것을 함수의 적용이라고 부름

제자였던 존 매카시(John McCarthy).
이것을 단순히 수학 이론에 그치지 않고, 프로그래밍 언어에 도입할 수 있겠다는 아이디어를 제시.
50년대 말에 LISP라는 언어를 개발.

이후 람다 계산법의 개념은 이후 다른 프로그래밍 언어에도 도입되었으며,
C#, C++, Java, Python 같은 주류 프로그래밍 언어는 대부분 람다식을 지원.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.