람다식은 메서드(함수)를 최대한 간결하게 표시하는 방법을 말하며 무명 함수를 만드는 데 사용할 수 있습니다.
class Program
{
delegate int Cal(int i, int j);
static void Main(string[] args)
{
Cal c = (i, j) => i + j;
WriteLine(c(100, 200));
}
}
예제에서 Cal 대리자는 무명 함수를 위해 선언되었으며 '(i, j) => i + j;' 부분이 함수의 매개변수와 본문입니다. 이처럼 매개변수와 함수의 실행 부분으로 나뉘어 있는데 => 연산자가 매개변수로 전달된 값을 실행 부분으로 옮겨주는 역할을 합니다.
매개변수는 '(i, j)'로 간결하게 작성되었는데 본래 '(int i, int j)'와 같이 데이터 타입이 올 수도 있지만 대리자를 통해 타입을 유추할 수 있으므로 생략할 수 있습니다.
람다식에서 함수의 실행부분은 여러 줄에 걸쳐야 하는 경우라면 중괄호({})를 사용해 구현할 수 있으며
class Program
{
delegate int Cal(int i, int j);
static void Main(string[] args)
{
Cal c = (i, j) => {
if (i < 0) {
i = 100;
}
return i + j;
};
WriteLine(c(100, 200));
}
}
매개변수는 필요없으면 괄호(())만 사용합니다.
class Program
{
delegate void Cal();
static void Main(string[] args)
{
Cal c = () => {
WriteLine("안뇽!");
WriteLine("반가워");
};
c();
}
}
위와 같은 방법을 통해서 간단히 무명 함수를 만들어 보았는데 가만히 보면 무명 함수를 만들기 위해 필요한 대리자를 같이 동반하고 있다는 것을 알 수 있습니다. 대리자를 같이 사용해야 한다는 불편함 때문에 C#에서는 Action와 Func라는 2개의 대리자를 미리 선언해 두었고 이를 활용하면 별도의 대리자를 선언하지 않아도 됩니다.
먼저 Action입니다.
class Program
{
static void Main(string[] args)
{
Action<int> echo = (i) => { WriteLine($"입력값 : {i}"); };
echo(100);
}
}
반환 값이 없는 함수인 경우에 사용할 수 있으며 형식 매개변수로 매개변수의 데이터 타입을 지정하고 있습니다. 형식 매개변수는 매개변수의 개수에 맞게 지정해야 하는데 만약 매개변수가 (int i, string s)와 같은 경우라면 Action도 Action<int, string>으로 형식에 맞춰야 합니다. 이런 식으로 형식 매개변수에 따라서 최대 17개의 매개변수를 지정할 수 있습니다. 물론 필요한 매개변수가 없으면 별도의 형식 매개변수 없이 Action만 사용합니다.
다음으로 Func입니다.
class Program
{
static void Main(string[] args)
{
Func<int, int, int> cal = (i, j) => { return i + j; };
WriteLine(cal(10, 20));
}
}
Func는 반환 값이 있는 함수에서 사용할 수 있습니다. 여기서 형식 매개변수는 '반환 형식, 매개변수 형식'순으로 지정합니다. 이때 제일 앞에 오는 형식이 반환되는 데이터의 형식을 의미하므로 Func는 Action과 달리 반드시 하나이상의 형식 매개변수를 필요로 합니다. Func도 마찬가지로 최대 17개의 매개변수를 지정할 수 있습니다.
보시는 바와 같이 Action과 Func에서 중요한 점은 이전의 예제와 달리 별도의 대리자를 선언하지 않아도 된다는 것입니다. 하지만 오해하면 안 됩니다. 선언하지 않아도 된다는 것이지 필요 없다는 뜻은 아니며 어디까지나 .NET에 미리 선언되어 있는 대리자를 사용할 뿐입니다.
● System.Linq.Expressions
C#에서는 코드 안에서 동적으로 무명 함수를 만들 수 있으며 이를 위해 사용하는 클래스가 System.Linq.Expression 네임스페이스에 정의되어 있습니다.
Expression 클래스를 통해 무명 함수를 만들 때 이를 식 트리로 표현하는데, 트리라는 것은 자료구조에서 부모와 자식, 단말로 불리는 노드로 구성된 구조를 말합니다.
A, B, C, D, E, F, G, H, I, J 등 각각을 노드라고 표시하며 A는 B, C, D의 부모이고 B, C, D는 A노드의 자식 노드가 될 수 있습니다. 또한 E, F는 B의 자식 노드이고 B는 다시 E, F의 부모 노드입니다. 그리고 제일 마지막에 있는 E, F, G, H, I, J는 마지막 노드로서 단말 노드가 됩니다.
식 트리라고 하는 것은 위의 구조처럼 식을 트리구조로 만들어둔 것을 말합니다. 예를 들어 '1 * 2 + 3 / 4'라는 식을 를 트리구조로 나타내면 연산자의 우선순위에 따라 다음과 같은 구조를 가지게 됩니다.
식 트리에서 연산자가 부모로, 피연산자는 자식으로 나뉘게 되며 C#에서 이러한 식 트리를 Expression클래스를 통해 표현하고 사용하게 되는 것입니다.
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클래스를 사용했는데 그 외에 다음과 같은 클래스를 사용해 해당 식을 표현할 수 있습니다.
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 | 단항 연산자를 갖는 식을 표현합니다. |
예제에서는 '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-Bodied Member)
식으로만 구성된 멤버를 말합니다.
class Car
{
private string color = string.Empty;
private int mileage = 500000;
private int speed = 0;
private string[] passenger = { "kim", "lee", "hong", "son" };
//생성자
public Car() => WriteLine("생성자 호출");
//읽기쓰기속성
public string Color
{
get => color;
set => color = value;
}
//읽기전용속성
public int Mileage => mileage;
//메서드
public void Drive(int speed) => this.speed = speed;
//인덱서
public string this[int i] {
get => passenger[i];
set => passenger[i] = value;
}
}
본래 생성자, 속성, 메서드, 인덱서 등은 본문이 중괄호({})로 이루어진 것으로 예제의 Car클래스에서는 이들 멤버를 모두 식으로 구현하였습니다. 중괄호로 본문이 만들어져 있는 것을 위에서 처럼 식으로도 옮겨줄 수 있는 것입니다.
다만 예제에서와 같이 단일식만 허용합니다. 복잡한 메서드나 속성이 구현되어야 하는 경우는 식 본문 멤버로 바꿀 수 없습니다.
'.NET > C#' 카테고리의 다른 글
[C#] 리플렉션과 애트리뷰트 (0) | 2021.10.20 |
---|---|
[C#] LINQ (0) | 2021.10.19 |
[C#] 대리자와 이벤트 (0) | 2021.10.18 |
[C#] 예외처리 (0) | 2021.10.15 |
[C#] 일반화 프로그래밍 (0) | 2021.10.14 |