[ 람다식 ]
- 람다 대수의 형식을 C#에서 구현한 문법이다. 코드로서의 람다식(익명 메서드의 간편 표기 용도), 데이터로서의 람다 식(람다 식 자체가 데이터가 되어 구문 분석의 대상이 된다.) 으로 구분된다.
[ 코드로서의 람다 식 ]
- 익명 메서드의 구문을 더욱 단순화 할 수 있다.
using System;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(
delegate (object obj)
{
Console.WriteLine("ThreadFunc in anonymous method called!");
});
// Thread 타입의 생성자가 void (object obj) 형식의 델리게이트
// 인자를 하나 받는다는 것을 이미 알고 있다.
Thread thread2 = new Thread(
(obj) =>
{
Console.WriteLine("ThreadFunc in anonymous method called!");
});
}
}
}
using System;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
delegate int? MyDivide(int a, int b);
static void Main(string[] args)
{
MyDivide myFunc = (a, b) =>
{
if (b == 0) return null;
return a / b;
};
Console.WriteLine("10 / 2 == " + myFunc(10, 2));
Console.WriteLine("10 / 0 == " + myFunc(10, 0));
}
}
}
- 기본적으로 값이 반환된다는 가정하에 return 문을 생략할 수 있는데, 이 경우 람다 식의 연산자인 => 기호 다음에 오는 중괄호까지 생략한다.
using System;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
delegate int? MyAdd(int a, int b);
static void Main(string[] args)
{
MyAdd myFunc = (a, b) => a + b;
Console.WriteLine("10 / 2 == " + myFunc(10, 2));
Console.WriteLine("10 / 0 == " + myFunc(10, 0));
}
}
}
- 람다 식은 델리게이트에 대응된다. 사실, 람다 식은 기존의 메서드와는 달리 일화성으로 사용되는 간단한 코드를 표현할 때 사용되는데, 정작 그러한 목적으로 델리게이트를 일일이 정의해야 한다는 불편함이 발생한다.
- 마이크로소프트에서는 이러한 불편을 덜기 위해 자주 사용되는 델리게이트의 형식을 제네릭의 도움으로 일반화해서 BCL에 Action과 Func로 포함시켰다.
public delegate void Action<T>(T obj); // 반환 값이 없는 델리게이트로써 T형식 매개 변수는 입력될 인자 1개
public delegate TResult Func<TResult>(); // 반환값이 있는 델리게이트로써 TResult 형식 매개변수는 반환될 타입
- 마이크로소프트에서는 인자를 16개까지 받을 수 있는 Action과 Func를 미리 정의해뒀다.
따라서, 인자 16개까지는 사용자가 직접 델리게이트를 정의할 필요 없이, Action/Func 델리게이트를 사용한다.
public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
......
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
// 예시. 처음 int 2개는 인자 두개에 대응되고,
// 나머지 1개는 반환값
Func<int, int, int> myFunc = (a, b) => a + b;
Console.WriteLine("10 + 2 == " + myFunc(10,2));
- 확장 메서드, 람다식, Action, Func은 기존 컬렉션의 기능을 더욱 풍부하게 개선했다.
using System;
using System.Collections.Generic;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 3, 1, 4, 5, 2 };
foreach(var item in list)
{
Console.WriteLine(item + " * 2 == " + (item * 2));
}
Console.WriteLine();
// List<T>에 정의된 ForEach
list.ForEach(elem => { Console.WriteLine(elem + " * 2 == " + (elem * 2)); });
Console.WriteLine();
// Array에 정의된 ForEach
Array.ForEach(list.ToArray(),
(elem) => { Console.WriteLine(elem + " * 2 == " + (elem * 2)); });
Console.WriteLine();
// 람다식이 아닌 익명 메서드도 가능
list.ForEach(delegate (int elem)
{
Console.WriteLine(elem + " * 2 == " + (elem * 2));
});
}
}
}
- ForEach 메서드는 Action<T> 델리게이트를 인자로 받아 컬렉션의 모든 요소를 열람하면서 하나씩 Action<T>의 인자로 요소값을 전달한다. 즉, 요소 수만큼 Action<T> 델리게이트가 수행된다.
- FindAll 메서드를 이용해 컬렉션의 요소 중 짝수만 걸러내는 예제를 간단하게 짤 수 있다.
using System;
using System.Collections.Generic;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List<int> evenList = list.FindAll((elem) => elem % 2 == 0);
evenList.ForEach((elem) => { Console.WriteLine(elem); });
}
}
}
- FindAll의 경우 메서드 실행이 완료되는 순간 람다 식이 컬렉션의 모든 요소를 대상으로 실행되어 조건을 만족하는 목록을 반환하는 반면, Where의 경우 메서드가 실행됐을 때는 어떤 코드도 실행되지 않은 상태이고, 이후 열거자를 통해 요소를 순회했을 때에야 발소 람다식이 하나씩 실행된다. 이를 지연된 평가(lazy evaluation)라 한다.
- Where 메서드의 반환값은 IEnumerable<T> 이다. IEnumerable<T>를 반환하는 모든 메서드가 이렇게 동작한다.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> enumList = list.Where((elem) => elem > 3);
Array.ForEach(enumList.ToArray(), (elem) => {Console.WriteLine(elem);});
}
}
}
- Select는 컬렉션의 개별 요소를 다른 타입으로 변환한다. 단순 형변환뿐만 아니라 객체도 반환 가능하며, 익명 타입으로 구성해 반환하는 것도 가능하다. Select 역시 IEnumerable<T> 타입을 반환하므로 지연 평가에 해당한다. 참고로, ConvertAll의 지연평가 버전이 Select이다.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApp1
{
class Person
{
public int Age;
public string Name;
}
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<double> doubleList = list.Select((elem) => (double)elem);
Array.ForEach(doubleList.ToArray(), (elem) => { Console.WriteLine(elem); });
IEnumerable<Person> personList = list.Select((elem) => new Person() { Age = elem, Name = Guid.NewGuid().ToString() });
Array.ForEach(personList.ToArray(), (elem) => { Console.WriteLine(elem.Name); });
var itemList = list.Select((elem) => new { TypeNo = elem, CreatedDate = DateTime.Now.Ticks });
Array.ForEach(itemList.ToArray(), (elem) => { Console.WriteLine(elem.TypeNo); });
}
}
}
[ 데이터로서의 람다 식 ]
- 람다 식을 CPU에 의해 실행되는 코드가 아닌, 그 자체로 "식을 표현한 데이터"로도 사용할 수 있다. 이처럼, 데이터 구조로 표현된 것을 "식 트리(expression tree)"라고 한다.
- 식 트리로 담긴 람다 식은 익명 메서드의 대체물이 아니기 때문에, 델리게이트가 아닌 System.Linq.Expressions.Expression 타입의 인스턴스가 된다. 또한 이름에서 알 수 있듯 tree 자료구조의 형태로 담고 있다. (중위 표현)
using System;
using System.Linq.Expressions;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Expression<Func<int, int, int>> exp = (a, b) => a + b;
// 람다 식 본체의 루트는 이항 연산자인 + 기호
BinaryExpression opPlus = exp.Body as BinaryExpression;
Console.WriteLine(opPlus.NodeType); // 출력 결과 : Add
// 이항 연산자의 좌측 연산자를 나타내는 표현식
ParameterExpression left = opPlus.Left as ParameterExpression;
Console.WriteLine(left.NodeType + ": " + left.Name); // 출력 결과 : Parameter: a
// 이항 연산자의 우측 연산자를 나타내는 표현식
ParameterExpression right = opPlus.Right as ParameterExpression;
Console.WriteLine(right.NodeType + ": " + right.Name); // 출력 결과 : Parameter: b
}
}
}
- 데이터로 담겨 있는 람다 식은 컴파일하는 것도 가능하며, 이를 위해 Expression<T> 타입에서는 Compile 메서드를 제공한다. 이 메서드를 호출하면 Expression<T>의 형식 매개변수 타입에 해당하는 델리게이트가 반환되고 자연스럽게 함수 호출도 가능해진다.
Func<int, int, int> func = exp.Compile();
Console.WriteLine(func(10, 2));
- 또 다른 특징으로는, Expression<T> 객체를 람다 식으로 초기화하지 않고 직접 코드와 관련된 Expression 객체로 구성할 수도 있다는 점이다. 이렇게 개별 Expression을 생성하는 parameter와 add method를 일컬어 Expression 타입의 팩터리 메서드(factory method)라고 한다.
using System;
using System.Linq.Expressions;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
ParameterExpression leftExp = Expression.Parameter(typeof(int), "a");
ParameterExpression rightExp = Expression.Parameter(typeof(int), "b");
BinaryExpression addExp = Expression.Add(leftExp, rightExp);
Expression<Func<int, int, int>> addLambda =
Expression<Func<int, int, int>>.Lambda<Func<int, int, int>>(
addExp, new ParameterExpression[] { leftExp, rightExp }
);
Console.WriteLine(addLambda.ToString());
Func<int, int, int> addFunc = addLambda.Compile();
Console.WriteLine(addFunc(10, 2));
}
}
}
'Programming > C#' 카테고리의 다른 글
C# 4.0 변경점 - 선택적 매개변수/명명된 인자, dynamic 예약어 (0) | 2019.04.15 |
---|---|
C# 3.0 변경점(3) - LINQ(Language Integrated Query) (0) | 2019.04.09 |
C# 3.0 변경점(1) - var, 자동 구현 속성, 객체/컬렉션 초기화, 익명 타입, 확장 메서드 (0) | 2019.04.03 |
C# 2.0 변경점(2) - yield return/break, partial class, Nullable, 익명 메서드, 정적 클래스 (0) | 2019.03.31 |
C# 2.0 변경점(1) - 제네릭스(Generics), ?? 연산자 (0) | 2019.03.31 |