람다식(λ-Expression) 은 익명 메소드를 만드는 또 하나의 방법이다. C#뿐만 아니라 C++, JAVA, Python과 같은 주류 프로그래밍언어에서는 대부분 람다식을 지원하고있다.
A. 람다식의 형식
//매개 변수 목록 => 식
delegate int Calculate(int a, int b); //익명메소드를 만들기위해 대리자가 필요하다.
static void Main()
{
Calculate calc = (int a, int b) => a+b; //두개의 int 형식 매개변수 a,b를 받아서
//이 둘을 더해 반환하는 익명 메소드를 람다식으로 만들었다.
}
※ 추가적으로 C# 컴파일러는 위 코드를 간결하게 만들 수 있도록 "형식유추(Type Interface)" 라는 기능을 제공한다.
delegate int Calculate(int a, int b);
static void Main()
{
Calcultae calc = (a,b)=>a+b;// C# 컴파일러는 Calculator 대리자의 선언 코드로부터
// 이 람다식이 만드는 익명 메소드의 매개 변수의 형식을 유추해낸다.
}
//아래는 기존 대리자를 이용해 익명메소드를 구현하던 방식
Calculate calc = delegate(int a, int b)
{
return a+b;
}
위와같이 대리자를 이용해 익명메소드를 만드는것 대신 람다식을 이용하면 코드양을 줄일수있다.
B. 문 형식의 람다식
위에서 사용한 람다식은 "식(expression)" 형식을 사용했으며, "문 형식의 람다식(Statement Lambda)" 은 => 연산자의 오른편에 식 대신 {과 }으로 둘러싸인 코드 블록이 위치한다. 사용법은 다음과 같다.
//(매개변수목록) => {
// 문장1;
// 문장2;
// 문장3;
// }
delegate void DoSomething()
static void Main()
{
DoSomething DoIt = ( )=> //매개변수가 없는경우이므로 (과 ) 사이에 아무것도 넣지않는다.
{
Console.WriteLine("뭔가를");
Console.WriteLine("출력한다");
}; //문 형식의 람다식은 {과 }로 둘러싼다.
DoIt();
}
- 예시코드
class MainAPP
{
delegate string Concatenate(string[] args);
static void Main(string[] args)
{
Concatenate concat = (arr) =>
{
string result = "";
foreach (string s in arr)
result += s;
return result;
};
Console.WriteLine(concat(args));
}
}
C. Func와 Action으로 더 간단하게 무명함수 만들기
익명 메소드와 무명 함수는 코드를 보다 간결하게 만들어주는 요소이지만, 대부분의 경우 익명 메소드나 무명 함수를 만들기 위해 매번 별개의 대리자를 선언해야한다.
하지만, MS는 이 문제를 해결하기위해 .NET 프레임워크에
결과를 반환하는 메소드를 참조하기위한 Func 대리자를 선언해뒀고,
결과를 반환하지 않는 메소드를 참조하기위한 Action 대리자를 미리 선언해뒀으므로 상황에 맞게 사용하면된다.
1. Func 대리자
Func 대리자에는 입력매개변수가 없는 버전부터, 최대 16개의 입력매개변수를 사용할 수 있는 버전이 있으며, 마지막 형식 매개변수에는 반환형식이 들어가야한다.
public delegate TResult Func<out TResult>() // 입력 매개변수가 없는버전
...
public delegate TResult Func<in T1, in T2, in T3, in T4,..., in T16, out TResult>(T1 arg1,...,T16 arg16)
//입력 매개변수가 16개인 버전
//* 참고로 <> 사이에는 형식매개변수, ()사이에는 입력매개변수를 입력한다.
- 사용 예시
static void Main(string[] args)
{
//입력 매개변수가 없는 Func 대리자
Func<int> func1 = () => 10;
Console.WriteLine(func1());
//입력 매개변수가 하나인 Func 대리자
Func<int, int> func2 = (x) => x * 2;
Console.WriteLine(func2(3));
//입력 매개변수가 두개인 Func 대리자
Func<int, int, int> func3 = (x, y) => x + y;
Console.WriteLine(func3(2, 3));
}
2. Action 대리자
반환형식이 없는 대리자이며, Func대리자와 마찬가지로 17개 버전이 .NET 프레임워크에 선언되어있다.
하지만, Func와 달리 어떤 결과를 반환하는 것을 목적으로 하지 않고, 일련의 작업을 수행하는것이 목적이다.
public delegate void Action<>() // 입력 매개변수가 없는버전
...
public delegate TResult Func<in T1, in T2, in T3, in T4,..., in T16>(T1 arg1,...,T16 arg16)
//입력 매개변수가 16개인 버전
//* 참고로 <> 사이에는 형식매개변수, ()사이에는 입력매개변수를 입력한다.
- 사용 예시
static void Main(string[] args)
{
int result = 0;
Action<int> act2 = (x) => result = x * x; //람다식 밖에서 선언한 result에 x*x 결과를 저장한다.
act2(3);
Console.WriteLine($"result : {result}");
Action<double, double> act3 = (x,y) =>
{
double pi = x/y;
Console.WriteLine($"Action<T1,T2>({x},{y}) : {pi}");
}
act3(22.0,7.0);
}
D. 식 트리 (Expression Tree)
자료구조의 트리 개념과 같으며 트리 자료구조는 아래와 같이 구성되어있다.
평범한 트리 자료구조에서는 부모노드가 여러개의 자식노드를 가질 수 있지만, 식 트리는 한개의 부모노드가 단 두 개만의 자식 노드를 가질 수 있는 이진 트리(Binary Tree)이다.
식 트리에서 연산자는 부모노드가 되며, 피 연산자는 자식 노드가 된다.
* 식 트리 자료구조를 사용하는 이유? 완전한 C# 컴파일러는 아니지만, C#은 프로그래머가 C# 코드안에서 직접 식 트리를 조립하고 컴파일해서 사용할 수 있게하는 기능을 제공한다. 따라서, 프로그램 실행 중에 동적으로 무명함수를 만들어 사용할 수 있게 해준다. |
다음은, .NET 프레임워크의 System.Linq.Expressions 네임스페이스 내에 있는 식 트리를 다루는데 필요한 클래스들이다.
순번 | Expression의 파생 클래스 | 설명 |
1 | BinaryExpression | 이항 연산자(+,-,*,/,%,&,==,!=,>,>=,<,<=)를 갖는 식을 표현한다. |
2 | BlockExpression | 변수를 정의할 수 있는 식을 갖는 블록을 표현한다. |
3 | ConditionalExpression | 조건 연산자가 있는 식을 나타낸다. |
4 | ConstantExpression | 상수가 있는 식을 나타낸다. |
5 | DefaultExpression | 형식(type)이나 비어 있는 식의 기본값을 표현한다. |
6 | DynamicExpression | 동적 작업을 나타낸다. |
7 | GotoExpression | return, break, continue, goto와 같은 점프문을 나타낸다. |
8 | IndexExpression | 배열의 인덱스 참조를 나타낸다. |
9 | InvocationExpression | 대리자나 람다식 호출을 나타낸다. |
10 | LabelExpression | 레이블을 나타낸다. |
11 | LambdaExpression | 대리자나 람다식 호출을 나타낸다. |
12 | ListInitExpression | 컬렉션 이니셜라이저가 있는 생성자 호출을 나타낸다. |
13 | LoopExpression | 무한 루프를 나타낸다. 무한루프는 break를 통해 종료한다. |
14 | MemberExpression | 객체의 필드나 속성을 나타낸다. |
15 | MemberInitExpression | 생성자를 호출하고 새 객체의 멤버를 초기화하는 동작을 나타낸다. |
16 | MethodCallExpression | 메소드 호출을 나타낸다. |
17 | NewArrayExpression | 새 배열의 생성과 초기화를 나타낸다. |
18 | NewExpression | 생성자 호출을 나타낸다. |
19 | ParameterExpression | 명명된 매개 변수를 나타낸다. |
20 | RuntimeVariablesExpression | 변수에 대한 런타임 읽기/쓰기 권한을 제공한다. |
21 | SwitchExpression | 다중 선택 제어 식을 나타낸다. |
22 | TryExpression | try~catch~finally 블록을 나타낸다. |
23 | TypeBinaryExpression | 형식 테스트를 비롯한 형식(Type)과 식(Expression)의 연산을 나타낸다. |
24 | UnaryExpression | 단항 연산자를 갖는 식을 나타낸다. |
Expression 클래스는 식 트리를 구성하는 노드를 표현한다. 그래서 Expression을 상속받는 위 표의 클래스들이 식 트리의 각 노드를 표현할수있다.
Expression 클래스 자신은 abstract로 선언되어 자신의 인스턴스를 만들수 없지만, 파생 클래스의 인스턴스를 생성하는 *정적 팩토리 메소드를 제공하고있다.
※ 팩토리 메소드(Factory Method)란? 클래스의 인스턴스를 생성하는 일을 담당하는 메소드를 가리키는 용어이며, C#에는 객체를 생성하는 생성자 메소드가 있지만, 객체의 생성에 복잡한 논리가 필요한 경우, 객체 생성과정을 별도의 메소드에 구현해놓으면 코드복잡도를 줄일수 있게된다. Expression 클래스의 정적 팩토리 메소드들은 Expression 클래스의 파생 클래스인 ConstantExpression, BinaryExpression 클래스 등의 인스턴스를 생성하는 기능을 제공함으로써 편의를 제공한다. |
- 사용 예시
static void Main(string[] args)
{
Expression const1 = Expression.Constant(1);
//Expression.Constant() 팩토리 메소드로 ConstantExpression형식의 const1객체 선언
Expression param1 = Expression.Parameter(typeof(int), "x");
//Expression.Parameter() 팩토리 메소드로 ParameterExpression형식의 param1객체 선언
Expression exp = Expression.Add(const1, param1); // 1+x
}
이 때, 모든 Expression 파생클래스들은 Expression 형식의 참조를 통해 가리킬 수 있으므로, 각 노드의 타입을 신경쓰지 않아도 된다. 필요시에는 각 형식으로 형변환을 하면된다.
위의 const1,param1,exp등은 실행가능한 상태가 아니므로, 자신의 트리 자료 구조 안에 정의되어있는 식을 실행할 수 있으려면 람다식으로 컴파일되어야한다.
람다식으로의 컴파일은 Expression<TDelegate> 클래스를 이용한다 (Expression <TDelegate>는 위 표에도 나타난 LambdaExpression 클래스의 파생 클래스이다).
static void Main(string[] args)
{
Expression const1 = Expression.Constant(1);
//Expression.Constant() 팩토리 메소드로 ConstantExpression형식의 const1객체 선언
Expression param1 = Expression.Parameter(typeof(int), "x");
//Expression.Parameter() 팩토리 메소드로 ParameterExpression형식의 param1객체 선언
Expression exp = Expression.Add(const1, param1); // 1+x
// 아래는 exp를 람다식으로 컴파일하는 과정
Expression<Func<int,int>> lambda1 =
Expression<Func<int, int>>.Lambda<Func<int,int>>(
exp,
new ParameterExpression[]{ (ParameterExpression)param1 } );
Func<int,int> compiledExp = lambda1.Compile(); //실행가능한 코드로 컴파일
Console.WriteLine( compiledExp(3) ); //x=3이면 1+x=4이므로 4출력
}
- 이해를 위한 예제프로그램
static void Main(string[] args)
{
//1*2 + (x-y)
Expression const1 = Expression.Constant(1);
Expression const2 = Expression.Constant(2);
Expression leftExp = Expression.Multiply(const1, const2); //1*2
Expression param1 = Expression.Parameter(typeof(int)); //x를 위한 변수
Expression param2 = Expression.Parameter(typeof(int)); //y를 위한 변수
Expression rightExp = Expression.Subtract(param1, param2); //x-y
Expression exp = Expression.Add(leftExp, rightExp);
Expression<Func<int, int, int>> expression = //람다식 클래스의 파생 클래스인 Expression<TDelegate>를 사용한다.
Expression<Func<int, int, int>>.Lambda<Func<int, int, int>> (
exp, new ParameterExpression[]
{
(ParameterExpression)param1,
(ParameterExpression)param2
});
Func<int, int, int> func = expression.Compile();
//x=7, y=8
Console.WriteLine($"1*2+({7}-{8}) = {func(7, 8)}");
//************* 위 전체과정을 람다식을 이용하면 더 간편하게 식 트리를 만들 수 있다.
//************* 하지만 Expression 형식은 불변(Immutable)이기 때문에 한번 인스턴스가 만들어지고 난 후에는 변경할 수 없으므로, "동적으로" 식 트리를 만들기는 어려워진다.
Expression<Func<int, int, int>> expression2 = (a, b) => 1 * 2 + (a - b);
Func<int, int, int> func2 = expression2.Compile();
//x=7,y=8
Console.WriteLine($"1*2+(7-8)={func(7, 8)}");
}
지금까지의 내용으로 봤을때, 식 트리는 코드를 "데이터"로써 보관할 수 있으므로, 이것은 파일에 저장할 수도 있고, 네트워크를 통해 다른 프로세스에 전달할 수도있다.
데이터베이스 처리를 위한 식 트리는 LINQ(Language Integrated Query)에서 사용되므로, LINQ를 이해하려면 람다식과 식 트리를 이해해야한다.
E. 식으로 이루어지는 멤버 (Expression-Bodied Member)
메소드, 속성(property), 생성자, 종료자 등은 모두 클래스의 멤버로수 본문이 중괄호 { } 로 이뤄져있따.
이런 멤버의 본문을 식(Expression)으로 구현하는것이 가능하다.
식으로 표현된 멤버는 "식 본문 멤버(Expression-Bodied Member)"라고 한다.
*식 본문 멤버의 문법 멤버 => 식; |
class FriendList
{
public void Add(string name) => list.Add(name);
public void Remove(string name) => list.Remove(name);
public FriendList() => Console.WriteLine("FriendList()"); //생성자
-FriendList() => Console.WriteLine("-FriendList()"); //종료자
public int Capacity
{
get => list.Capacity;
set => list.Capacity = value; // 속성
}
//public int Capacity => list.Capacity; // 읽기 전용 속성, get 키워드 생략가능
public string this[int index]
{
get => list[index];
set => list[index] = value; //인덱서
}
//public string this[int index] => list[index]; // 읽기 전용 인덱서, get 키워드 생략가능
}
- 예제 코드
class MainAPP
{
class FriendList
{
private List<string> list = new List<string>();
public void Add(string name) => list.Add(name);
public void Remove(string name) => list.Remove(name);
public void PrintAll()
{
foreach (var s in list)
Console.Write(s+' ');
Console.WriteLine();
}
public FriendList() => Console.WriteLine("FriendList()");
~FriendList() => Console.WriteLine("FriendList()");
//public int Capacity => list. Capacity; // 읽기 전용
public int Capacity//속성
{
get => list.Capacity;
set => list.Capacity = value;
}
//public string this[int index] => list[index]; //읽기전용
public string this[int index]
{
get => list[index];
set => list[index] = value;
}
}
static void Main(string[] args)
{
FriendList obj = new FriendList();
obj.Add("Eeny");
obj.Add("Meeny");
obj.Add("Miny");
obj.Remove("Eeny");
obj.PrintAll();
Console.WriteLine($"obj.Capacity={obj.Capacity}");
obj.Capacity = 10;
Console.WriteLine($"obj.Capacity={obj.Capacity}");
Console.WriteLine($"{obj[0]}");
obj[0] = "Moe";
obj.PrintAll();
}
}
'C# 공부 > C# 기본 문법' 카테고리의 다른 글
대리자(Delegate)와 이벤트 - 2 (이벤트, 대리자와 이벤트의 차이) (0) | 2020.08.02 |
---|---|
대리자(Delegate)와 이벤트 - 1 (대리자, 대리자를 사용하는 이유와 상황, 일반화 대리자, 대리자 체인, 익명 메소드) (0) | 2020.08.01 |
예외 처리하기 (try~catch문, System.Exception 클래스, 예외 던지기, finally, 사용자 정의 예외클래스, 예외 필터(when), 예외처리의 장점) (0) | 2020.07.31 |
일반화(Generic) 프로그래밍 (일반화 메소드/클래스, 형식매개변수 제약, 일반화 컬렉션) (1) | 2020.07.28 |
인덱서 (인덱서의 선언과 사용, foreach가 가능한 객체 만들기) (0) | 2020.07.26 |