본문 바로가기
C#

[C#] C# Event와 Unity Event 소개 및 비교

by 소리쿤 2021. 12. 1.
여기서 Event란 C#의 delegate를 이용한 방송자와 구독자로 이루어지는 Pair 프로그래밍 방식을 의미합니다.

- delegate는 다음과 같이 선언하며, 같은 반환 형식, 매개 변수 형식을 갖는 함수를 모아다가 대리로 수행해줍니다.

delegate int Transformer(int x); // 대리자 클래스 Transformer, 반환 타입 int, 매개 변수 타입 int

 

https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/delegates/

 

대리자 - C# 프로그래밍 가이드

C#의 대리자는 매개 변수 목록 및 반환 형식이 있는 메서드를 나타내는 형식입니다. 대리자는 메서드를 다른 메서드에 인수로 전달하는 데 사용됩니다.

docs.microsoft.com


이 delegate는 주로 어떤 일(Event)이 발생했을 때, 그 일에 대한 파급 효과(Call Back)를 발생시켜야 할 때 사용합니다.

그렇기 때문에 자신의 콜백 함수를 등록하는 '구독자'와 모든 콜백 함수를 수행하는 '방송자'라는 역할 구분이 생기고,
그 사이에서 delegate가 이 둘을 이어주는 매개체 역할을 하게 됨으로써 '구독자''방송자' 사이의 커플링을 끊어줍니다.

하지만 delegate는 방법론적 존재이고, 위 개념으로 바로 추상화가 되긴 어렵습니다.
그래서 이런 기능은 Event라는 형태의 표준으로 래핑되어 C#, Unity에서 각각 제공합니다.


C#의 System.Event



C#은 이러한 delegate "객체"를 선언할 때 event 키워드를 붙일 수 있게 해놨는데,
event가 붙은 채 생성된 delegate 객체는 해당 클래스 외부에서 +=, -= 연산만 수행할 수 있습니다.
(외부에선 구독자의 역할만 할 수 있다)

이는 구독자가 다른 구독자를 건드리거나, delegate를 비워버리는 등의 일이 가능하기 때문에 걸어놓은 제약입니다.

public delegate void PriceChangedHander(decimal oldPrice, decimal newPrice);

public class Stock
{
    string symbol;
    decimal price;

    public Stock(string symbol) { this.symbol = symbol; }

    public event PriceChangedHander priceChanged; // 이벤트 키워드 붙임

    public decimal Price 
    { 
        get { return price; } 
        set
        {
            if(price == value) return;
            decimal oldPrice = price;
            price = value;
            if(priceChanged != null) priceChanged(oldPrice, price); // 클래스 내부에서만 사용이 가능함
        }
    } 
}
public static void Main() 
{
    Stock stock = new Stock("solhwi"); 
    stock.priceChanged += Foo; // 올바른 event delegate 사용 예시
} 

public static void Foo(decimal oldPrice, decimal newPrice) { }

부적절한 event delegate 사용 예시


위와 같이 올바르게 방송자 / 구독자 모형을 분리하고, 더욱이
event를 호출할 떄 Generic한 매개 변수를 넘기도록 하면 확장성도 좋아질 것입니다.

위 코드의 decimal 타입의 oldPrice, price를 넘겨주는 방식 대신, C#은 System.EventArgs 인터페이스를 넘겨줍시다.

System.EventArgs는 내부에 static 멤버인 Empty 속성만 존재하는 인터페이스지만,
이 EventArgs를 상속받아 새 클래스를 만들어 전달하면 무엇이든 전달할 수 있습니다.

public class PriceChangedEventArgs : System.EventArgs
{
    public decimal lastPrice;
    public decimal newPrice;

    public PriceChangedEventArgs(decimal d1, decimal d2) { lastPrice = d1; newPrice = d2; }
}

// 이와 같이 EventArgs를 상속하여 확장한 매개 변수 타입을

public delegate void PriceChangedHander(PriceChangedEventArgs arg);

// 일반화하여 받도록 하면 모든 타입들을 받을 수 있을 것이다.


마지막으로 콜백을 모아다가 수행해야 하는 Event 상황의 특성 상 반환값은 큰 의미가 없을 확률이 높다고 추측할 수 있습니다.

그래서 이러한 특성을 이용해 C#은 System.EventHandler<> 라는 제네릭 delegate를 만들어 놓았으며,

이 제네릭 delegate는 반환 형식이 void이며,
(1) object /* 방송자 */ (2) EventArgs /* 추가 정보 */ 매개 변수를 받습니다.

너무 글이 길어질 것 같으므로, 예시만 적고 Unity Event로 넘어가겠습니다.

다음 예시는 Main(Stock 내 핸들러에 콜백 등록) ㅡ Stock(핸들러 보유자, 이벤트 감지) ㅡ EventHandler(콜백 수행자)
의 형태를 가진 구조입니다.

using System;

public class Program
{
    public class PriceChangedEventArgs : EventArgs
    {
        public readonly decimal lastPrice;
        public readonly decimal newPrice;

        public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
        {
            this.lastPrice = lastPrice;
            this.newPrice = newPrice;
        }
    }

    public class Stock
    {
        string symbol;
        decimal price;
        public Stock(string symbol) { this.symbol = symbol; }

        public event EventHandler<EventArgs> priceChanged;

        protected virtual void OnPriceChanged(EventArgs e)
        {
            priceChanged?.Invoke(this, e);
        }
        
        public decimal Price
        {
           get { return price; }
           set
           {
               if (price == value) return;

               decimal oldPrice = price;
               price = value;
               OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
           }
        }

    }

    static void Main()
    {
        Stock kakaoStock = new Stock("Kakao");

        kakaoStock.Price = 50.0M;

        kakaoStock.priceChanged += Alert; // Main은 주식의 변화가 있으면 알기 위해서 자신의 이벤트 콜백을 등록한다.

        kakaoStock.Price = 70.0M; // 변화 시 Price Set ㅡ> OnPriceChanged 래핑 함수 호출 ㅡ> 안전한 Invoke()
    }

    static void Alert(object sender, EventArgs param)
    {
        PriceChangedEventArgs arg = param as PriceChangedEventArgs;

        if(arg.newPrice - arg.lastPrice / arg.lastPrice > 0.1M )
        {
            Console.WriteLine("주식 가격이 10% 올랐습니다.");
        }
    }
}

무사히 주식 가격이 올랐음


Unity Event



Unity 자체에서도 EventHandler<T>와 비슷한 UnityEvent<T>를 제공합니다.

추상 클래스이기 때문에 상속하여 형태를 잡아주고, (아래의 AlertEvent : UnityEvent<EventParam>)
객체로 만들어준 다음 이벤트를 감지하고, 이벤트가 발생하면 UnityEvent를 Invoke()합니다.

아래 예시는 방송자, 구독자로 나누어 사용한 훌륭한 예시는 아니지만,
클래스 내의 Start()가 구독자, EventTest가 방송자, OnBecameVisible()이 이벤트 발생자,
unityEvent가 콜백 수행자라고 생각하면 될 것 같습니다.

using UnityEngine; 
using UnityEngine.Events; 

public class EventTest : MonoBehaviour 
{ 
    AlertEvent unityEvent = new AlertEvent(); 
    
    void Start() 
    { 
        unityEvent.AddListener(OnEvent); // C#의 +=, -= 연산자를 쓰기 편하도록 Add와 Remove로 래핑 
    } 
    
    void OnEvent(EventParam e) 
    { 
        Debug.Log(e.Context); 
    } 
    
    void OnBecameVisible() 
    { 
        unityEvent.Invoke(new EventParam("큐브가 보인다")); 
    } 
} 



// ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡEvent와 Paramㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ 

public class AlertEvent : UnityEvent<EventParam> {} 

public class EventParam : System.EventArgs 
{ 
    protected string context; 
    public string Context { get { return context; } set { context = value; } } 
    
    public EventParam(string s) { context = s; } 
}

C#과 Unity Event의 성능 비교



코드를 통해 C#과 Unity Event가 Listener 당 얼마나 많은 GC를 할당하게 되는 지 살펴보겠습니다.

기준은 다음과 같습니다.

  • Listener 개수에 따른 성능 비교
    using System;
     
    using UnityEngine;
    using UnityEngine.Events;
     
    public class TestScript : MonoBehaviour
    {
    	event Action csharpEv0;
    	UnityEvent unityEv0 = new UnityEvent();
     
    	void Start()
    	{
    		AddCsharpListener();
    		AddUnityListener();
    		AddCsharpListener2();
    		AddUnityListener2();
    		AddCsharpListener3();
    		AddUnityListener3();
    		AddCsharpListener4();
    		AddUnityListener4();
    	}
     
    	void AddCsharpListener() => csharpEv0 += NoOp;
     
    	void AddUnityListener() => unityEv0.AddListener(NoOp);
     
    	void AddCsharpListener2() => csharpEv0 += NoOp;
     
    	void AddUnityListener2() => unityEv0.AddListener(NoOp);
     
    	void AddCsharpListener3() => csharpEv0 += NoOp;
     
    	void AddUnityListener3() => unityEv0.AddListener(NoOp);
    
    	void AddCsharpListener4() => csharpEv0 += NoOp;
     
    	void AddUnityListener4() => unityEv0.AddListener(NoOp);
    
    	static void NoOp(){}
    }
    if Listener &amp;amp;gt; 1 , C # Event &amp;amp;lt; Unity Event

 

  • Invoke에 따른 성능 비교
    using System;
     
    using UnityEngine;
    using UnityEngine.Events;
     
    public class TestScript : MonoBehaviour
    {
    	event Action csharpEv0;
    	UnityEvent unityEv0 = new UnityEvent();
     
    	string report;
     
    	void Start()
    	{
    		csharpEv0 += NoOp0;
    		csharpEv0 += NoOp0;
    		unityEv0.AddListener(NoOp0);
    		unityEv0.AddListener(NoOp0);
    		DispatchCsharpEvent();
    		DispatchUnityEvent();
    	}
     
    	void DispatchCsharpEvent() => csharpEv0.Invoke();
    
    	void DispatchUnityEvent() => unityEv0.Invoke();
    
    	static void NoOp0(){}
    }​

Every time , C # Event &amp;amp;gt; Unity Event


C# Event와 Unity Event의 전체적인 성능표와 결론

 

Num Args
Num Listeners
C# Event Time
UnityEvent Time
0
1
30
206
0
2
89
306
0
3
151
406
0
4
206
514
0
5
272
612
1
1
33
685
1
2
91
807
1
3
151
980
1
4
212
1096
1
5
274
1224
2
1
30
1187
2
2
102
1371
2
3
172
1547
2
4
226
1709
2
5
296
1879

 

  • Listener가 2개 이상인 경우 UnityEvent가 C# Event에 비해 GC 할당량이 적습니다.
  • Invoke 시 C# Event가 쓰레기를 생성하지 않는 데에 반해, Unity Event는 첫 Invoke 시 쓰레기를 생성합니다.
  • 대체적으로 Unity Event가 2배 느리고, 최악의 경우 40배까지 느려집니다.

 


https://www.jacksondunstan.com/articles/3335

 

JacksonDunstan.com |   Event Performance: C# vs. UnityEvent

Event Performance: C# vs. UnityEvent January 25, 2016 Tags: event, performance, unityevent Unity programmers have their choice of two kinds of events. We could use the built-in C# event keyword or Unity’s UnityEvent classes. Which is faster? Which one cr

www.jacksondunstan.com

 

'C#' 카테고리의 다른 글

[C#] Indexer  (0) 2022.06.01
[C#] volatile이 무슨 키워드임?  (0) 2022.04.30
[C#] 어셈블리 내 클래스 타입 가져오기  (0) 2022.02.24
[C#] enum과 foreach 주의할 점  (0) 2022.02.24
[C#] Add와 AddRange  (0) 2022.02.16