델리게이트
델리게이트의 개념과 기본 사용
- 델리게이트는 메서드를 참조하는 형식임
- 델리게이트도 참조형이니 Null 상태가 있다.
- 메서드 호출이 아니라 참조하는 이유 : 나중에 호출하기 위해 참조하는 것이다.
- 참조를 하면? : 메서드에 다른 메서드를 매개변수로 호출해서 사용할 수 있게되는 것이다.
- 메서드의 시그니처(반환 타입과 매개변수)를 정의함
- 선언한 델리게이트와 동일한 시그니처 만 참조가 가능하다.
- 참조하는 시그니처가 같다 하더라도 각각 선언한 델리게이트는 다른 데이터형이다.
- 호출될 메서드를 대신 호출해주는 역할을 함
- C++의 함수객체와 비슷한 역할이다.
기본 문법과 사용
using System;
// 델리게이트 타입 정의
delegate int Transformer(int x); //델리게이트 선언
class DelegateExample
{
// 대상 메서드
static int Square(int x) => x * x;
static void Main()
{
// 델리게이트 인스턴스 생성
Transformer t = Square; //메서드를 참조할때는 메서드의 이름만 작성한다.
// 델리게이트를 통한 메서드 호출
int result = t(3);
Console.WriteLine(result); // 출력: 9
}
}
- 메서드를 참조할때는 메서드의 이름만 작성한다.
- 굳이 이렇게 하는 이유? : Square를 호출하면 해당 기능만 사용 가능하지만, t에 참조한 메서드를 변경하면 다른 기능을 사용 가능하기 때문이다.
- 위 예제는 다음과 동일한 의미임:
- 위와같이 Transformer t = Square;로 작성하더라도, 실제 컴파일러는 암시적으로 아래와 같이 판단해서 동작한다.
- 코드 작성할때는 위와같이 작성한다. - 동작하는 방식만 알아두라는 것이다.
Transformer t = new Transformer(Square);
델리게이트를 이용한 플러그인 메서드 작성
- 델리게이트를 매개변수로 받아 실행 시점에 동작을 결정할 수 있음
using System;
class PluginExample
{
static void Main()
{
int[] values = { 1, 2, 3 };
Transform(values, Square);
foreach (int i in values)
Console.Write(i + " "); // 출력: 1 4 9
}
static void Transform(int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t(values[i]);
}
static int Square(int x) => x * x;
static int Cube(int x) => x * x * x;
}
delegate int Transformer(int x);
인스턴스 메서드와 정적 메서드 대상
정적 메서드 대상
using System;
class StaticTargetExample
{
static void Main()
{
Transformer t = Test.Square;
Console.WriteLine(t(10)); // 출력: 100
}
}
class Test
{
public static int Square(int x) => x * x;
}
delegate int Transformer(int x);
인스턴스 메서드 대상
using System;
class InstanceTargetExample
{
static void Main()
{
Test test = new Test();
Transformer t = test.Square; //호출이 아니라 할당
//Test.Square는 안된다. -> static 으로 선언한것이 아니기 때문이다.
Console.WriteLine(t(10)); // 출력: 100
}
}
class Test
{
public int Square(int x) => x * x;
}
delegate int Transformer(int x);
멀티캐스트 델리게이트
- 여러 메서드를 하나의 델리게이트에 연결할 수 있음
- += 연산자로 메서드를 추가하고 -= 연산자로 제거함
using System;
public delegate void ProgressReporter(int percentComplete);
class MulticastExample
{
static void Main()
{
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile; //추가 참조
// 두 메서드 모두 호출됨
Util.HardWork(p);
}
static void WriteProgressToConsole(int percentComplete)
=> Console.WriteLine(percentComplete);
static void WriteProgressToFile(int percentComplete)
=> System.IO.File.WriteAllText("progress.txt",
percentComplete.ToString()); //txt 파일 생성 함수
}
public class Util
{
public static void HardWork(ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p(i * 10);
System.Threading.Thread.Sleep(100); //0.1초 대기
}
}
}
//결과 : 0 ~ 90까지 출력
- 즉, 참조하고있는 함수 전부를 호출하는 것이다.
제네릭 델리게이트와 Func/Action
제네릭 델리게이트
using System;
// 제네릭 델리게이트 정의
public delegate T Transformer<T>(T arg);
class GenericDelegateExample
{
static void Main()
{
int[] values = { 1, 2, 3 };
Util.Transform(values, Square);
foreach (int i in values)
Console.Write(i + " "); // 출력: 1 4 9
}
static int Square(int x) => x * x;
}
public class Util
{
public static void Transform<T>(T[] values, Transformer<T> t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t(values[i]);
}
}
Func와 Action 델리게이트
- 미리 선언되어있는 델리게이트 데이터형이다.다.
- Func: 반환값이 있는 메서드를 참조
- Func<매개변수 데이터형, 반환형>
- Func<반환형> - 매개변수 없는 반환형 메서드 참조
- FUnc<매개변수 , 매개변수, 반환형>
- Action: 반환값이 없는 메서드를 참조
- Action<매개변수>
- Action<매개변수, 매개변수, …>
using System;
class FuncActionExample
{
static void Main()
{
// Func 사용 예
Func<int, int> square = x => x * x;
Console.WriteLine(square(3)); // 출력: 9
// Action 사용 예
Action<string> print = x => Console.WriteLine(x);
print("Hello"); // 출력: Hello
}
}
- x ⇒ x * x에서 반환형이 안붙는 이유 : Func에서 이미 반환형을 지정했기 때문이다.
- 델리게이트 이름에 의미를 주고싶은 경우에 델리게이트를 쓰고, 그 이외에 경우에는 Func, Action, Predicate를 쓴다.
Predicate<매개변수>
- bool 반환이 고정되어있는 Func 델리게이트다.
델리게이트와 인터페이스 비교
- 다음과 같은 경우 델리게이트가 더 적합함:
- 인터페이스가 하나의 메서드만 정의할 때
- 멀티캐스트 기능이 필요할 때
- 구독자가 인터페이스를 여러번 구현해야 할 때
- 인터페이스 단점 : 메서드 정의를 위해 인터페이스를 상속한 클래스가 필요하다.
- 델리게이트 장점 : 함수 없이 ‘람다식’으로 대체가 가능하다.
using System;
// 델리게이트 방식
delegate int Transformer(int x);
// 인터페이스 방식
interface ITransformer
{
int Transform(int x);
}
class ComparisonExample
{
static void Main()
{
// 델리게이트 사용
int[] values1 = { 1, 2, 3 };
Transformer t = Square;
TransformArray(values1, t);
// 인터페이스 사용
int[] values2 = { 1, 2, 3 };
TransformAll(values2, new Squarer());
}
// 델리게이트 버전
static void TransformArray(int[] values, Transformer t)
{
//들어온 인자를 제곱해서 다시 저장하는 코드
for (int i = 0; i < values.Length; i++)
values[i] = t(values[i]);
}
static int Square(int x) => x * x;
// 인터페이스 버전
static void TransformAll(int[] values, ITransformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t.Transform(values[i]);
}
}
class Squarer : ITransformer
{
public int Transform(int x) => x * x;
}
델리게이트 호환성(Delegate Compatibility)
타입 호환성
- 시그니처가 같더라도 서로 다른 델리게이트 타입은 호환되지 않음
- 단, 델리게이트 인스턴스가 같은 메서드를 참조하면 서로 같다고 판단함
using System;
class TypeCompatibilityExample
{
static void Main()
{
// 서로 다른 델리게이트 타입
D1 d1 = Method1;
//D2 d2 = d1; // 컴파일 에러
// 명시적 변환은 가능
D2 d2 = new D2(d1);
// 같은 메서드를 참조하는 델리게이트 인스턴스는 동일함
D1 d3 = Method1;
D1 d4 = Method1;
Console.WriteLine(d3 == d4); // 출력: True
}
static void Method1() { }
}
delegate void D1();
delegate void D2();
매개변수 호환성
- 델리게이트의 매개변수는 대상 메서드보다 더 구체적인 타입을 가질 수 있음
- 즉, 업캐스팅으로 형변환이 가능하다면 매개변수로 받아들일 수 있다.
- 이를 반공변성(Contravariance)이라 함
using System;
class ParameterCompatibilityExample
{
static void Main()
{
// object를 매개변수로 받는 메서드를
// string을 매개변수로 받는 델리게이트에 할당 가능
StringAction sa = ActOnObject;
sa("hello"); // 출력: hello
}
static void ActOnObject(object o) => Console.WriteLine(o);
}
delegate void StringAction(string s);
반환 타입 호환성
- 델리게이트의 대상 메서드는 더 구체적인 타입을 반환할 수 있음
- 참조하는 반환형이 델리게이트 반환형으로 업캐스팅 할 수 있으면 참ㅈ가 가능하다는 뜻이다.
- 이를 공변성(Covariance)이라 함
using System;
class ReturnTypeCompatibilityExample
{
static void Main()
{
// string을 반환하는 메서드를
// object를 반환하는 델리게이트에 할당 가능
ObjectRetriever o = RetrieveString;
object result = o();
Console.WriteLine(result); // 출력: hello
}
static string RetrieveString() => "hello";
}
delegate object ObjectRetriever();
제네릭 델리게이트 타입 매개변수 가변성
- 반환값에만 사용되는 타입 매개변수는 공변성(out)을 표시할 수 있음
- 매개변수에만 사용되는 타입 매개변수는 반공변성(in)을 표시할 수 있음
using System;
class GenericVarianceExample
{
static void Main()
{
// 공변성(out) 예제
Func<string> strFunc = () => "test";
Func<object> objFunc = strFunc; // 가능
// 반공변성(in) 예제
Action<object> objAction = (object o) => Console.WriteLine(o);
Action<string> strAction = objAction; // 가능
// 실행
Console.WriteLine(objFunc()); // 출력: test
strAction("hello"); // 출력: hello
}
}
이벤트(event)
이벤트 개념
- 이벤트는 특정 상황이 발생했음을 다른 객체에게 알리는 기능임
- 이벤트를 사용할 때는 두 가지 주요 역할이 있음:
- 브로드캐스터(Broadcaster): 이벤트를 포함하는 타입으로, 이벤트 발생을 결정함
- 구독자(Subscriber): 이벤트를 수신하는 측으로, 이벤트 발생 시 실행될 메서드를 등록함
이벤트 선언
- 이벤트는 delegate 필드 앞에 event 키워드를 붙여 선언함
- 예제:
// 델리게이트 정의
public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
public class Broadcaster
{
// 이벤트 선언
public event PriceChangedHandler PriceChanged;
}
이벤트 내부 동작
- 이벤트가 선언되면 컴파일러는 다음과 같은 작업을 수행함:
// 다음 코드는 컴파일러가 자동으로 생성하는 코드를 보여줌
private PriceChangedHandler priceChanged; // private delegate 필드
public event PriceChangedHandler PriceChanged
{
add { priceChanged += value; } // 이벤트 구독
remove { priceChanged -= value; } // 이벤트 구독 취소
}
실전 예제: 주식 가격 변동 알림
- 주식 가격이 변경될 때 이벤트를 발생시키는 예제:
// 주식 클래스 정의
public class Stock
{
string symbol;
decimal price; //0으로 초기화된다.
public Stock(string symbol) => this.symbol = symbol;
// 이벤트 선언
public event PriceChangedHandler PriceChanged;
public decimal Price
{
get => price;
set
{
if (price == value) return;
decimal oldPrice = price;
price = value;
if (PriceChanged != null)
PriceChanged(oldPrice, price);
}
}
}
// 사용 예제
Stock stock = new Stock("MSFT");
//매개변수를 받아서 우측 기능을 동작하는 '람다식'이다.
stock.PriceChanged += (oldPrice, newPrice) =>
Console.WriteLine($"가격 변동: {oldPrice} -> {newPrice}");
stock.Price = 30.00m; // 이벤트 발생
- event가 없으면 그냥 public으로 사용하는것과 같지만, event가 붙으면 외부에서는 += 으로만 사용가능하고 그 이외에 사용은은 event가 선언된 클래스 내부에서만 사용가능하게 된다.
- 이벤트 확인 ‘null’로 체크한다.
표준 이벤트 패턴
- .NET에서는 이벤트에 대한 표준 패턴이 있음
- EventArgs 클래스를 상속하여 이벤트 데이터를 전달함:
// 이벤트 데이터 클래스
public class PriceChangedEventArgs : EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice;
NewPrice = newPrice;
}
}
// 표준 이벤트 패턴 적용 예제
public class Stock
{
string symbol;
decimal price;
public Stock(string symbol) => this.symbol = symbol;
// EventHandler<T> 사용
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
public decimal Price
{
get => price;
set
{
if (price == value) return;
decimal oldPrice = price;
price = value;
OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
}
}
}
// 사용 예제
Stock stock = new Stock("MSFT");
stock.PriceChanged += (sender, args) =>
{
if ((args.NewPrice - args.LastPrice) / args.LastPrice > 0.1m)
Console.WriteLine("주식 가격 10% 이상 상승!");
};
stock.Price = 27.10m; // 가격 설정
stock.Price = 31.59m; // 가격 변경으로 이벤트 발생
- Invoke : Null이 아니라면 참조하는 메서드를 호출한다.
이벤트 접근자
- 이벤트의 add와 remove 접근자를 직접 구현할 수 있음
- 예제:
private EventHandler priceChanged;
public event EventHandler PriceChanged
{
add { priceChanged += value; }
remove { priceChanged -= value; }
}
이벤트 수정자
- 이벤트도 메서드처럼 virtual, override, abstract, sealed 수정자 사용 가능
- static 이벤트도 선언 가능
- 예제:
public class Foo
{
public static event EventHandler<EventArgs> StaticEvent;
public virtual event EventHandler<EventArgs> VirtualEvent;
}
람다식
람다 표현식의 기본 개념
- 람다 표현식(Lambda Expression)은 무명 메서드를 작성하는 간단한 방법임
- 대리자(Delegate) 인스턴스를 생성할 때 주로 사용함
- 컴파일러는 람다 표현식을 다음 중 하나로 변환함:
- 대리자 인스턴스
- 식 트리(Expression Tree)
람다 표현식의 기본 구문
- 기본 형식: (매개변수) => 식-또는-문장블록
- 매개변수가 하나이고 타입 유추가 가능한 경우 괄호 생략 가능함
// 기본 예제
Transformer t = x => x * x; // 매개변수 x를 제곱하여 반환
Console.WriteLine(t(3)); // 출력: 9
// 대리자 타입 정의
delegate int Transformer(int i);
람다 식과 문장 블록
- 단일 식(Expression)으로 작성 가능한 경우:
x => x * x
- 문장 블록(Statement Block)이 필요한 경우:
x => { return x * x; }
Func와 Action 대리자 활용
- 람다 표현식은 Func와 Action 대리자와 함께 자주 사용함
- Func: 반환 값이 있는 메서드를 대리함
- Action: 반환 값이 없는 메서드를 대리함
// Func 사용 예제
Func<int, int> square = x => x * x;
Console.WriteLine(square(3)); // 출력: 9
// Action 사용 예제
Action<string> print = message => Console.WriteLine(message);
print("Hello"); // 출력: Hello
// 두 개의 매개변수를 받는 예제
Func<string, string, int> totalLength = (s1, s2) => s1.Length + s2.Length;
int total = totalLength("hello", "world"); // total은 10
매개변수와 반환 타입 명시
- 컴파일러가 타입을 유추할 수 없는 경우 명시적으로 지정해야 함
// 매개변수 타입 명시
Func<int, int> square = (int x) => x * x;
// C# 10부터는 반환 타입도 명시 가능
var square = int (int x) => x * x;
외부 변수 캡처
- 람다 표현식은 정의된 범위의 외부 변수를 사용할 수 있음
- 해당 람다식이 있는 코드영역에서 접근할 수 있는 지역변수, 멤버 변수 전부 접근 가능하다.
- 캡처된 변수는 람다 표현식이 실행될 때 평가됨
int factor = 2;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine(multiplier(3)); // 출력: 6
factor = 10;
Console.WriteLine(multiplier(3)); // 출력: 30
- 람다 표현식은 캡처된 변수를 직접 수정할 수 있음:
- 왜 직접 수정할수있냐? : 레퍼런스에 접근하는것이기 때문이다.
int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine(natural()); // 출력: 0
Console.WriteLine(natural()); // 출력: 1
Console.WriteLine(seed); // 출력: 2
- 캡처된 변수는 대리자의 수명까지 연장됨:
- 캡쳐된 변수는 스코프가 끝나도 스택 영역에서 지역 변수가 사라지지 않는다.
- 왜? : 리턴을 참조하고있기 때문에 지역변수가 사라지면 에러가 발생하기 때문이다.
Func<int> natural = Natural();
Console.WriteLine(natural()); // 출력: 0
Console.WriteLine(natural()); // 출력: 1
static Func<int> Natural()
{
int seed = 0;
return () => seed++; // 클로저 반환
}
- 람다 식 내부에서 선언된 변수는 매 호출마다 새로 생성됨:
Func<int> natural = Natural();
Console.WriteLine(natural()); // 출력: 0
Console.WriteLine(natural()); // 출력: 0 // 매번 0 출력
static Func<int> Natural()
{
return () => { int seed = 0; return seed++; };
}
반복문에서의 변수 캡처 주의사항
- for 루프의 반복 변수를 캡처할 때는 변수가 공유된다는 점에 주의해야 함
- 다음 예제는 의도와 다르게 동작할 수 있음:
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
actions[i] = () => Console.Write(i);
foreach (Action a in actions)
a(); // 출력: 333
- i가 레퍼런스로 들어가지기 때문에 i는 같은 값을 공유하게 된다.
- i도 없어져야하지만 람다식에 캡쳐되있기 때문에 사라지지 않고 남아있는다.
- 해결책: 루프 내에서 지역 변수를 새로 선언하여 사용
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
int loopScopedi = i;
actions[i] = () => Console.Write(loopScopedi);
}
foreach (Action a in actions)
a(); // 출력: 012
정적 람다 식 (C# 9)
- static 키워드를 사용하여 외부 변수 캡처를 방지할 수 있음
- 성능 최적화가 필요한 경우 유용함
// 정적 람다 식
Func<int, int> multiplier = static n => n * 2;
// 다음은 컴파일 오류 발생
int factor = 2;
Func<int, int> error = static n => n * factor; // 컴파일 오류
람다 표현식과 지역 메서드 비교
재귀 호출
- 지역 메서드는 자연스러운 재귀 호출이 가능함
- 람다 식은 재귀 구현이 복잡함
// 지역 메서드로 재귀 구현 - 자연스러움
int Factorial(int n)
{
return n <= 1 ? 1 : n * Factorial(n - 1);
}
// 람다 식으로 재귀 구현 - 복잡함
Func<int, int> factorial = null;
factorial = n => n <= 1 ? 1 : n * factorial(n - 1);
타입 선언 방식
- 지역 메서드는 일반 메서드처럼 직관적으로 선언함
- 람다 식은 대리자 타입을 명시해야 함
// 람다 식 - 대리자 타입 명시 필요
Func<int, bool> predicate = x => x > 0;
Action<string> printer = s => Console.WriteLine(s);
// 지역 메서드 - 일반적인 메서드 선언
bool IsPositive(int x) => x > 0;
void Print(string s) => Console.WriteLine(s);
성능 특성
- 지역 메서드:
- 대리자 생성 비용이 없음
- 직접 호출로 인한 성능상 이점이 있음
- 변수 캡처시 더 효율적인 코드 생성됨
- 람다 식:
- 대리자 인스턴스 생성 비용 발생
- 간접 호출로 인한 오버헤드 존재
void ProcessData()
{
var items = new List<int> { 1, 2, 3 };
// 람다 식 - 대리자 생성 및 간접 호출
var filtered = items.Where(x => x > 1);
// 지역 메서드 - 직접 호출
bool IsGreaterThanOne(int x) => x > 1;
var filtered2 = items.Where(IsGreaterThanOne);
}
적합한 사용 시기
- 람다 식이 유리한 경우:
- LINQ 쿼리 작성시
- 대리자를 매개변수로 받는 메서드 호출시
- 간단한 일회성 함수 필요시
- 지역 메서드가 유리한 경우:
- 재귀 호출이 필요한 경우
- 여러 곳에서 재사용되는 로직 필요시
- 성능이 중요한 경우