2009년 6월호로 월간 마이크로소프트웨어에 기고한 기사입니다.
우리가 프로그램을 작성할 때 많이 사용하게 되는 것 중 하나가 델리게이트(delegate, 대리자 혹은 위임)일 것이다. 델리게이트는 이벤트핸들러(event handler) 등록, 콜백(callback) 메서드 정의, 그리고 메서드(method)의 파라미터(parameter)로 전달 등 많은 곳에서 쓰이고 있다. 특히 메서드의 파라미터로 사용할 경우 아주 유연한 개발이 가능하다. 닷넷 프레임워크(.NET Framework) 3.5 LINQ에서 Where 확장메서드(Extension Method)의 경우처럼, 여러 조건으로 필터링하는 메소드를 만든다고 가정해 보자. 검색 조건은 상황에 따라 아주 다양할 것이다. 그럼 모든 상황별 메서드를 모두 만들 것인가? 사실상 불가능 하다. 그래서 Where 확장메서드는 필터링을 처리하는 델리게이트를 파라미터 받을 수 있게 되어있다. 필요한 그때 그때 필터링하는 기능을 만들어서 델리게이트로 넘기면 된다. 기능 구현은 닷넷프레임워크 2.0의 익명메서드(Anonymous Method)나 닷넷프레임워크 3.5의 람다식(Lambda Expressions)를 통해 명시적 메서드 구현 없이도 쉽게 사용 가능하다. 여기서 또 한가지 의문을 가질 수 있다. 실제적은 기능 구현은 익명메서드나 람다식으로 하면 되지만 정작 Where 같은 메서드를 정의할 때 쓰일 델리게이트는 어떻게 할 것인가? 다양한 파리미터 조건을 가지고 싶다면? 그에 맞게 델리게이트를 명시적으로 모두 정의할 것인가? 이 또한 쉽지 않다. 그래서 닷넷프레임워크 3.5에서는 이런 고민들 쉽게 해결할 특수(?) 델리게이트를 지원해 주고 있다. 바로 “Func”와 “Action” 델리게이트이다. 이들 델리게이트는 [표 1]과 [표 2]에서 보는 것처럼 제네릭(generic) 타입으로 정의되어 있으며, 몇 가지 유형으로 오버라이드(override) 되어 있어 다양한 요구 조건을 모두 만족 할 수 있다. “Func”와 “Action” 의 차이점은 위임된 기능이 처리되고 반환되는 결과값이 있느냐, 없느냐의 차이만 있다.
Func : 내부 처리 완료 후 반환되는 결과값이 있음. 특정 조건으로 필터링하고 그 결과를 반환하는 Where 같은 메소드를 구현할 때 쓰면 유용할 것이다.
Action : 내부 처리 완료 후 반환되는 결과값이 없음. 어떤 처리 진행을 보여 주는 UI 업데이트 같은 처리를 할 때 유용할 것이다. |
delegate void Action <T> (T arg) 는 이미 닷넷프레임워크 2.0에 존재 하였으며 다른 델리게이트들과 다르게 mscorlib(mscorlib.dll) 어셈블리에 정의되어 있다(네임스페이스는 동일하게 System에 존재한다).
[정의]
네임스페이스: System 어셈블리: System.Core (System.Core.dll) |
[표 1] Action 델리게이트
delegate void Action ( ); delegate void Action <T> (T arg); delegate void Action <T1, T2> (T1 arg1, T2 arg2); delegate void Action <T1, T2, T3> (T1 arg1, T2 arg2, T3 arg3); delegate void Action <T1 ,T2, T3, T4> (T1 arg1, T2 arg2, T3 arg3, T4 arg4); |
[표 2] Func 델리게이트
delegate TResult Func <TResult> ( ); delegate TResult Func <T, TResult> (T arg); delegate TResult Func <T1, T2, TResult> (T1 arg1, T2 arg2); delegate TResult Func <T1, T2, T3, TResult> (T1 arg1, T2 arg2, T3 arg3); delegate TResult Func <T1, T2, T3, T4, TResult> (T1 arg1, T2 arg2, T3 arg3,T4 arg4); |
예를 하나 살펴 보자. 숫자로 구성된 배열이 존재하고, 다양한 조건으로 배열에서 값을 가져오는 메서드를 정의하고 싶다고 가정하자. 여기서는 “다양한 조건으로 검색이 가능하도록”이 중요한 포인트이다. 검색을 다양하게 하려면 호출되는 시점에 필터링 조건을 주면 된다. 해당값이 조건이 맞는지 확인해서 bool 형식을 반환하는 코드가 필요하므로 “Func”를 사용하여 [코드1]과 같이 메소드를 정의 하였다.
[코드 1] 숫자 배열에서 여러 조건으로 필터링 할 수 있는 메서드 정의
private static List<int> FilterOfInts(int[] source, Func<int, bool> filter) { List<int> result = new List<int>(); foreach (int i in source) { if (filter(i)) { result.Add(i); } } return result; }
|
이제 정의된 메서드를 통해, 명시적으로 정의된 메서드 혹은 익명메서드, 람다식 모두를 사용하여 원하는 결과를 볼 수 있다. [코드 2]를 보면 FilterOfInts를 모두 호출하지만 람다식으로 다양한 조건을 파라미터로 넘기고 있다. 각 조건에 맞는 메소드를 모두 구현하는 것이 아니라 단일 메소드를 호출 하므로 보다 유연한 모듈 개발이 가능한 것을 확인 할 수 있다.
[코드 2] FilterOfInts를 통한 다양한 필터 조건을 사용
int[] source = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //===홀수 Filter=== List<int> oddNumbers = FilterOfInts(source, i => ((i & 1) == 1)); //===짝수 Filter=== List<int> evenNumbers = FilterOfInts(source, i => ((i & 1) == 0)); //===소수 Filter=== List<int> primeNumbers = FilterOfInts(source, checkNumber => { BitArray numbers = new BitArray(checkNumber + 1, true); for (int i = 2; i < checkNumber + 1; i++) { if (numbers[i]) { for (int j = i * 2; j < checkNumber + 1; j += i) { numbers[j] = false; } if (numbers[i]) { if (checkNumber == i) { return true; } } } } return false; })
|