안녕하세요. 명월입니다.
이 글은 C#에서의 람다식(익명 함수)과 Action, Func 함수 사용법, 그리고 클로저(Closure)에 대한 글입니다.
이전 글에서 제가 델리게이트(delegate)에 대해서 설명한 적이 있습니다.
링크 - [C#] 22. 델리 게이트(delegate)
델리게이트란 함수 포인터로 함수를 인스턴스의 포인터처럼 관리가 가능하게 만드는 기능을 말합니다. 즉, 함수를 포인터로 관리가 가능하다면 조금 더 깊게 생각한다면 굳이 함수의 이름을 선언할 것 없이 함수를 작성할 수 있습니다.
람다식이란 익명 함수의 의미처럼 함수의 이름 없이 생성이 가능한 함수입니다.
using System;
namespace Example
{
// 클래스 생성
class Program
{
// 델리게이트 선언
private delegate void Print(string val);
// 델리게이트 리스트
private static Print list;
// 실행 함수
static void Main(string[] args)
{
// 익명 함수 추가
list += (val) =>
{
// 콘솔 출력
Console.WriteLine("Lambda1 " + val.ToString());
};
// 익명 함수 추가
list += (val) =>
{
// 콘솔 출력
Console.WriteLine("Lambda2 " + val.ToString());
};
// 델리게이트 리스트
list("Hello world");
// 아무 키나 누르시면 종료합니다.
Console.WriteLine("Press any key...");
Console.ReadLine();
}
}
}
위 예제를 보시면 반환식은 없고 String의 파라미터를 받을 수 있는 델리게이트를 만들었습니다. 그리고 Main 함수에서 (val) => {} 의 형식의 람다식(익명 함수)를 만들어서 델리게이트 리스트에 추가했습니다.
여기서 val의 자료형은 델리게이트에서 선언한 string의 데이터가 되고 반환값은 없기 때문에 return은 필요가 없습니다.
그리고 중괄호({})의 스택 영역에 함수가 호출이 되면 실행되는 프로그램 명령어를 구현합니다.
실행하면 list의 델리게이트를 실행하여 등록되어 있는 람다식 두개가 실행이 되는 것을 확인할 수 있습니다.
그럼 이 람다식을 사용하기 위해서는 그에 맞는 델리게이트를 항상 선언해야 하는 것인가 하는 것 같지만, Func 함수와 Action 함수를 이용하면 델리게이트가 필요없습니다.
using System;
namespace Example
{
// 클래스 생성
class Program
{
// 실행 함수
static void Main(string[] args)
{
// 익명 함수, 가장 마지막의 파라미터는 반환값
Func<int, string> func = (val) =>
{
// int 값을 받아 100을 곱한다.
int ret = val * 100;
// string형식으로 반환한다.
return ret.ToString();
};
// 익명 함수, 반환값이 없다.
Action<string> action = (val) =>
{
// 콘솔 출력
Console.WriteLine("Action = " + val);
};
// 익명 함수 func를 호출 -> 파라미터는 int형을 넘기고 반환값은 string값
string data = func(10);
// 익명 함수 action을 호출 -> 파라미터 string형을 넘김
action(data);
// 아무 키나 누르시면 종료합니다.
Console.WriteLine("Press any key...");
Console.ReadLine();
}
}
}
위 예제에서는 익명 함수를 만들었지만, delegate를 선언하지 않고 Func과 Action 함수를 사용했습니다.
Func함수는 반환값이 있는 익명 함수고 Action함수는 반환값이 없는 익명 함수입니다.
Func함수의 마지막은 반환값의 자료형입니다. 그 외는 순서대로의 자료형입니다.
람다식이라는게 사실 문법 규약을 가차없이 파괴하는 방법입니다. 람다식이 사용하기도 편하고 델리게이트(delegate)나 리스트와 Func과 Action을 적정하게 섞어서 사용하면 프로그램 작성할 때 무지 편하기는 합니다.
문제는 람다식이 많이 지면 소스는 무지하게 복잡해지고 가독성이 매우 떨어지는 단점이 있습니다. 그렇기 때문에 일반 함수를 사용할 수 있으면 되도록 함수명을 작성하고 람다식을 사용하는 것을 자제하는 것이 좋습니다.
그렇다면 람다식을 언제 사용할까요? 디자인 패턴의 옵져버 패턴을 구현할 때, 즉, 이벤트나 콜백 함수를 구현할 때 사용하는 것이 좋습니다.
using System;
namespace Example
{
// 클래스 생성
class Program
{
// 출력 함수, 뒤 Action 함수는 콜백 함수로 함수가 끝나는 타이밍에 호출된다.
// 기본은 null로 파라미터를 넣지 않으면 null이 된다.
static void Print(string data, Action<string> cb = null)
{
// 파라미터 data의 값에 문자열 추가
string ret = "Print " + data;
// 콘솔 출력
Console.WriteLine(ret);
// 콜백 함수가 null 이 아니면
if (cb != null)
{
// 콜백 함수 호출
cb(ret);
}
}
// 실행 함수
static void Main(string[] args)
{
// Print 함수 호출
Print("Test");
// 개행
Console.WriteLine();
// Print 함수 호출, 함수 처리가 끝나면 콜백 함수가 호출된다.
Print("Hello world", (val) =>
{
// 데이터를 받고 문자열을 추가하고
string ret = val + " Call back!";
// 콘솔 출력
Console.WriteLine(ret);
});
// 아무 키나 누르시면 종료합니다.
Console.WriteLine("Press any key...");
Console.ReadLine();
}
}
}
위 식은 디자인 패턴의 옵져버 패턴입니다. javascript 코드에서 흔히 보이는 콜백 함수입니다.
객체 지향 클래스에서 데이터를 객체(Object)화 할 때, 필요한 콜백 형식이나 이벤트 형식, 즉, 특정 함수가 호출 되었을 때 연계되는 함수가 호출이되는 형식으로 작성할 때를 이야기합니다.
이벤트는 event 키워드를 설명할 때 자세히 설명하겠습니다.
기본적으로 옵져버 패턴으로 설계할 때, 람다식을 사용하지만 그 외에는 클로저(Closure) 기능을 사용하기 위해서 사용합니다.
using System;
namespace Example
{
// 클래스 생성
class Program
{
// 실행 함수
static void Main(string[] args)
{
// 변수 선언
string val = "Hello world";
// 익명 함수 생성
Action action = () =>
{
// 콘솔 출력
Console.WriteLine(val);
};
// 함수 호출
action();
// 아무 키나 누르시면 종료합니다.
Console.WriteLine("Press any key...");
Console.ReadLine();
}
}
}
위 예제는 Main에서 Action 함수를 사용해서 익명 함수를 만들었습니다. 그런데 이 익명 함수에는 파라미터가 없습니다.
그런데 Main 함수에서 선언한 string val 값을 끌어와서 사용할 수 있습니다. 만약 이게 익명 함수가 아니고 일반 함수였다면 말이 안되는 식입니다. 왜냐하면 스택 영역이 완전히 다른 부분이니깐요.
그런데 이 람다식은 소스 상의 바로 위 스택 영역의 값을 가져올 수 있습니다. 물론 가져와서 수정도 가능합니다. 이것이 클로저(Closure) 기능입니다.
이러한 기능은 의외로 함수의 복잡도를 상당히 줄일 수 있습니다. 즉, 파라미터로 넘겨야 하는 데이터 혹은 맴버 변수로 만들어야 하는 값들을 로컬 변수로 클로저(Closure)로 단순하게 프로그램을 만들 수 있습니다.
그러나 이것도 너무 지나치면 가독성이 매우 떨어지니 상황에 맞게 사양에 맞게 작성하면 좋을 것 같습니다.
여기까지 C#에서의 람다식(익명 함수)과 Action, Func 함수 사용법, 그리고 클로저(Closure)에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'Study > C#' 카테고리의 다른 글
[C#] 27. 리스트(List)와 딕셔너리(Dictionary), 그리고 Linq식 사용법 (0) | 2021.09.13 |
---|---|
[C#] 26. var 키워드와 dynamic 키워드 (0) | 2021.09.10 |
[C#] 25. 예외 처리(try ~ catch)하는 방법 (0) | 2021.09.09 |
[C#] 24. 이벤트(event) 키워드 사용법 (0) | 2021.09.08 |
[C#] 22. 델리 게이트(delegate) (0) | 2021.09.06 |
[C#] 21. 인덱서(Indexer)를 사용하는 방법 (0) | 2020.07.31 |
[C#] 20. 프로퍼티 (Property) (0) | 2020.07.30 |
[C#] 19. 객체 지향(OOP) 프로그래밍의 4대 원칙(캡슐화, 추상화, 상속, 다형성) (0) | 2020.07.28 |