[C#] 23. 람다식(익명 함수)과 Action, Func 함수 사용법, 그리고 클로저(Closure)


Study/C#  2021. 9. 7. 11:17

안녕하세요. 명월입니다.


이 글은 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)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.