이 글에서는 델리게이트 복습 → Func → Action → 기존 delegate와 비교 → LINQ에서의 사용 순서로 정리해본다.
Func와 Action을 이해하려면, 일단 델리게이트(delegate)를 알고 있어야 한다.
델리게이트는 이렇게 요약할 수 있다:
delegate = 메서드를 가리키는 변수 타입
예를 들어,
delegate bool NumberTest(int n);
// int 하나 받아서 bool(true/false)을 반환하는 메서드를 가리킬 수 있는 타입
이렇게 쓰면 된다.
static bool IsEven(int n) { return n % 2 == 0; }
static bool IsOdd(int n) { return n % 2 != 0; }
static int Count(int[] numbers, NumberTest test)
{
int count = 0;
foreach (int n in numbers)
{
if (test(n))
count++;
}
return count;
}
int[] arr = { 3, 5, 4, 2, 6, 7, 8 };
int evenCount = Count(arr, IsEven);
int oddCount = Count(arr, IsOdd);
여기까지가 “사용자 정의 delegate 타입(NumberTest)을 만들어서 쓰는 패턴”이다.
이제 같은 걸 더 일반적인 타입 Func / Action으로 바꿔볼 수 있다.
Func<...> = 값을 반환하는 델리게이트를 표현하는 제네릭 타입이다.
예를 들어:
Func<int> f1;
// 매개변수 없음, int를 반환
Func<int, int> f2;
// int 하나를 받아서 int 반환
Func<int, int, int> f3;
// int, int 두 개를 받아서 int 반환
Func<string, int> f4;
// string 하나를 받아서 int 반환
예전에 이렇게 만들었던 델리게이트:
delegate bool NumberTest(int n);
이건 Func로 바꾸면 이렇게 표현할 수 있다:
Func<int, bool> test;
// int를 입력으로 받고, bool을 반환하는 함수
즉,
delegate bool NumberTest(int n); ↔ Func<int, bool>
의미적으로 같은 역할을 하는 셈이다.
using System;
class Program
{
static void Main()
{
// int 두 개를 받아서 int를 반환하는 함수: 더하기
Func<int, int, int> add = (a, b) => a + b;
int r1 = add(3, 5); // 8
// string 하나 받아서 길이를 반환하는 함수
Func<string, int> len = s => s.Length;
int r2 = len("Hello"); // 5
Console.WriteLine(r1);
Console.WriteLine(r2);
}
}
예전에 따로 delegate int Calc(int a, int b);를 만들고 Calc calc = Add; 이런 식으로 쓰던 걸,
이제는 그냥 Func<int, int, int>로 표현한 것뿐이다.
Action<...> = 반환값이 없는(void) 델리게이트를 표현하는 제네릭 타입이다.
예를 들면:
Action a1;
// 매개변수 없음, void 메서드
Action<string> a2;
// string 하나를 받고 void
Action<int, string> a3;
// int, string을 받고 void
using System;
class Program
{
static void Main()
{
// 매개변수 없고, 그냥 메시지만 출력
Action hello = () =>
{
Console.WriteLine("Hello Action!");
};
// string 하나 받아서 콘솔에 출력만 하는 함수
Action<string> print = msg =>
{
Console.WriteLine("메시지: " + msg);
};
hello();
print("안녕");
}
}
여기서 hello, print는 둘 다 “호출 가능한 변수”라고 보면 된다.
hello()도 함수 호출, print("안녕")도 함수 호출이다.
// 기존 사용자 정의 delegate
delegate bool NumberTest(int n);
// Func로 표현
Func<int, bool> test;
NumberTest 타입 대신 그냥 Func<int, bool>를 쓰면 된다.
// 기존 사용자 정의 delegate
delegate void MyAction(string msg);
// Action으로 표현
Action<string> myAction;
둘 다 “string 하나 받아서 void인 메서드”를 나타낸다.
이전에 NumberTest를 쓰던 예제를 떠올려 보자.
delegate bool NumberTest(int n);
static int Count(int[] numbers, NumberTest test)
{
int count = 0;
foreach (int n in numbers)
{
if (test(n))
count++;
}
return count;
}
이걸 Func<int, bool>로 바꾸면 이렇게 된다.
using System;
class Program
{
static void Main()
{
int[] arr = { 3, 5, 4, 2, 6, 7, 8, 11, 13, 15, 20 };
// Func<int, bool> : int를 받아서 bool을 반환
int evenCount = Count(arr, n => n % 2 == 0); // 짝수
int oddCount = Count(arr, n => n % 2 != 0); // 홀수
int gtTen = Count(arr, n => n > 10); // 10 초과
Console.WriteLine("짝수 개수: " + evenCount);
Console.WriteLine("홀수 개수: " + oddCount);
Console.WriteLine("10보다 큰 수 개수: " + gtTen);
Console.ReadKey();
}
// 델리게이트 타입 대신 Func<int, bool> 사용
static int Count(int[] numbers, Func<int, bool> test)
{
int count = 0;
foreach (int n in numbers)
{
if (test(n))
count++;
}
return count;
}
}
여기서 Func<int, bool> test는 예전에 썼던 NumberTest test와 같은 역할을 한다.
단지 사용자 정의 delegate 타입을 새로 만들 필요 없이, Func 한 줄로 표현한 것뿐이다.
LINQ 메서드 대부분이 내부적으로 Func를 인자로 받는다.
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6 };
// 짝수만 고르기
var evens = numbers
.Where(n => n % 2 == 0) // 여기서 n => n % 2 == 0 은 Func<int, bool> 타입
.ToList();
foreach (var n in evens)
Console.WriteLine(n);
}
}
Where의 시그니처(개념적으로)는 대략 이런 느낌이다.
IEnumerable<T> Where(Func<T, bool> predicate)
즉, Where는 “조건을 나타내는 Func<T, bool>”를 받아서, 그 조건을 만족하는 요소만 걸러준다.
var squares = numbers
.Select(n => n * n); // Func<int, int> 사용 (int 받아서 int 반환)
이런 패턴이 바로 “Func를 이용해서 동작(조건, 변환)을 바깥에서 주입하는 스타일”이다.