이벤트 선언에 익명의 빈 대리자를 추가하는 데 단점이 있습니까?


82

이 관용구에 대한 몇 가지 언급을 보았습니다 ( SO 포함 ) :

// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};

장점은 분명합니다. 이벤트를 발생시키기 전에 null을 확인할 필요가 없습니다.

그러나 단점이 있는지 이해하고 싶습니다. 예를 들어, 널리 사용되고 있고 유지 관리 문제를 일으키지 않을만큼 투명한 것입니까? 빈 이벤트 구독자 호출로 인해 눈에 띄는 성능 저하가 있습니까?

답변:


36

유일한 단점은 여분의 빈 델리게이트를 호출 할 때 매우 약간의 성능 저하입니다. 그 외에는 유지 보수 패널티 또는 기타 단점이 없습니다.


19
이벤트에 정확히 하나의 구독자가있는 경우 ( 매우 일반적인 경우) 더미 핸들러는 두 개의 구독자를 갖게됩니다. 하나의 핸들러가있는 이벤트는 두 개의 핸들러가있는 이벤트 보다 훨씬 효율적 으로 처리 됩니다.
supercat

46

성능 오버 헤드를 유발하는 대신 확장 방법사용하여 두 문제를 모두 완화 하는 것이 좋습니다 .

public static void Raise(this EventHandler handler, object sender, EventArgs e)
{
    if(handler != null)
    {
        handler(sender, e);
    }
}

정의 된 후에는 다시 null 이벤트 검사를 수행 할 필요가 없습니다.

// Works, even for null events.
MyButtonClick.Raise(this, EventArgs.Empty);

2
일반 버전은 여기를 참조하십시오. stackoverflow.com/questions/192980/…
Benjol

1
내 도우미 트리니티 라이브러리가 정확히 무엇을하는지, 우연히 : thehelpertrinity.codeplex.com
Kent Boogaart

3
이것은 null 검사의 스레드 문제를 확장 메서드로 이동하지 않습니까?
PatrickV 2013

6
아니요. 처리기는 메서드에 전달되며이 시점에서 해당 인스턴스는 수정할 수 없습니다.
유다 가브리엘 히 망고 2011-04-30

@PatrickV 아니요, 유다가 맞습니다. handler위 의 매개 변수 는 메소드의 값 매개 변수입니다. 메서드가 실행되는 동안에는 변경되지 않습니다. 이를 위해서는 ref매개 변수 (분명히 매개 변수가 refthis수정자를 모두 가질 수 없음 )이거나 필드 여야합니다 .
Jeppe Stig Nielsen

42

시스템의 이벤트를 많이 사용하고 성능이 중요하다 , 당신은 확실히 적어도하기를 원할 것입니다 고려 이렇게하지. 빈 델리게이트로 이벤트를 발생시키는 비용은 먼저 null 확인을 사용하여 이벤트를 발생시키는 비용의 약 두 배입니다.

내 컴퓨터에서 벤치 마크를 실행하는 몇 가지 수치는 다음과 같습니다.

For 50000000 iterations . . .
No null check (empty delegate attached): 530ms
With null check (no delegates attached): 249ms
With null check (with delegate attached): 452ms

그리고 다음은 이러한 수치를 얻는 데 사용한 코드입니다.

using System;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        public event EventHandler<EventArgs> EventWithDelegate = delegate { };
        public event EventHandler<EventArgs> EventWithoutDelegate;

        static void Main(string[] args)
        {
            //warm up
            new Program().DoTimings(false);
            //do it for real
            new Program().DoTimings(true);

            Console.WriteLine("Done");
            Console.ReadKey();
        }

        private void DoTimings(bool output)
        {
            const int iterations = 50000000;

            if (output)
            {
                Console.WriteLine("For {0} iterations . . .", iterations);
            }

            //with anonymous delegate attached to avoid null checks
            var stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("No null check (empty delegate attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }


            //without any delegates attached (null check required)
            stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithoutAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("With null check (no delegates attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }


            //attach delegate
            EventWithoutDelegate += delegate { };


            //with delegate attached (null check still performed)
            stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithoutAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("With null check (with delegate attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }
        }

        private void RaiseWithAnonDelegate()
        {
            EventWithDelegate(this, EventArgs.Empty);
        }

        private void RaiseWithoutAnonDelegate()
        {
            var handler = EventWithoutDelegate;

            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
    }
}

11
농담이지? 호출은 5 나노초를 추가하고이를 수행하지 않도록 경고합니까? 그보다 더 비합리적인 일반 최적화는 생각할 수 없습니다.
Brad Wilson

8
흥미 롭군. 귀하의 결과에 따르면 null을 확인하고 확인하지 않고 호출하는 것보다 대리인을 호출하는 것이 더 빠릅니다. 나에게 옳지 않은 것 같습니다. 그러나 어쨌든 이것은 매우 작은 차이이므로 가장 극단적 인 경우를 제외하고는 눈에 띄지 않는다고 생각합니다.
Maurice

22
Brad, 저는 특히 이벤트를 많이 사용하는 성능이 중요한 시스템에 대해 말했습니다. 그 장군은 어때?
Kent Boogaart

3
빈 델리게이트를 호출 할 때 성능 페널티에 대해 이야기하고 있습니다. 누군가가 실제로 이벤트를 구독하면 어떻게됩니까? 빈 델리게이트가 아닌 구독자의 성능에 대해 걱정해야합니다.
비우 Trifoi

2
"실제"가입자가 0 인 경우가 아니라 1 인 경우에 성능 비용이 큽니다. 이 경우 구독, 구독 취소 및 호출은 호출 목록으로 아무것도 할 필요가 없기 때문에 매우 효율적이어야하지만 이벤트가 (시스템의 관점에서) 구독자가 하나가 아니라 두 명이라는 사실은 사용을 강제 할 것입니다. "0"또는 "1"경우에 최적화 된 코드가 아니라 "n"구독자를 처리하는 코드입니다.
supercat

7

/ lot /으로 수행하는 경우 델리게이트 인스턴스의 볼륨을 줄이기 위해 재사용 할 단일 정적 / 공유 빈 델리게이트를 원할 수 있습니다. 컴파일러는 어쨌든 (정적 필드에서) 이벤트마다이 델리게이트를 캐시하므로 이벤트 정의당 하나의 델리게이트 인스턴스 일 뿐이므로 절감 은 아니지만 가치가있을 수 있습니다.

물론 각 클래스의 인스턴스 별 필드는 동일한 공간을 차지합니다.

internal static class Foo
{
    internal static readonly EventHandler EmptyEvent = delegate { };
}
public class Bar
{
    public event EventHandler SomeEvent = Foo.EmptyEvent;
}

그 외에는 괜찮아 보입니다.


1
컴파일러가 이미 단일 인스턴스에 대한 최적화를 수행하는 것으로 여기에있는 대답 인 stackoverflow.com/questions/703014/…
Govert

3

극단적 인 상황을 제외하고는 의미있는 성능 저하가 없습니다.

그러나 C # 6.0에서는 언어가 null 일 수있는 대리자를 호출하는 대체 구문을 제공하기 때문에이 트릭은 관련성이 떨어집니다.

delegateThatCouldBeNull?.Invoke(this, value);

위의 null 조건부 연산자 ?.는 null 검사와 조건부 호출을 결합합니다.



2

나는 당신이 다음과 같은 것을하도록 유혹하기 때문에 약간 위험한 구조라고 말하고 싶습니다.

MyEvent(this, EventArgs.Empty);

클라이언트가 예외를 던지면 서버도 예외를 함께합니다.

따라서 아마도 다음과 같이 할 수 있습니다.

try
{
  MyEvent(this, EventArgs.Empty);
}
catch
{
}

그러나 여러 구독자가 있고 한 구독자가 예외를 throw하면 다른 구독자는 어떻게됩니까?

이를 위해 null 검사를 수행하고 구독자 측의 예외를 삼키는 몇 가지 정적 도우미 메서드를 사용해 왔습니다 (idesign에서 가져온 것입니다).

// Usage
EventHelper.Fire(MyEvent, this, EventArgs.Empty);


public static void Fire(EventHandler del, object sender, EventArgs e)
{
    UnsafeFire(del, sender, e);
}
private static void UnsafeFire(Delegate del, params object[] args)
{
    if (del == null)
    {
        return;
    }
    Delegate[] delegates = del.GetInvocationList();

    foreach (Delegate sink in delegates)
    {
        try
        {
            sink.DynamicInvoke(args);
        }
        catch
        { }
    }
}

nitpick은 아니지만 <code> if (del == null) {return; } Delegate [] delegates = del.GetInvocationList (); </ code>가 경쟁 조건 후보입니까?
Cherian

좀 빠지는. 대리자는 값 유형이므로 del은 실제로 UnsafeFire 메서드 본문에만 액세스 할 수있는 대리자 체인의 개인 복사본입니다. (주의 : UnsafeFire가 인라인되면 실패하므로 [MethodImpl (MethodImplOptions.NoInlining)] 속성을 사용하여 이에 대해
inure

1) 델리게이트는 참조 유형입니다. 2) 불변이므로 경쟁 조건이 아닙니다. 3) 인라인이이 코드의 동작을 변경한다고 생각하지 않습니다. del 매개 변수가 인라인 될 때 ​​새로운 지역 변수가 될 것으로 예상합니다.
CodesInChaos

'예외가 발생하면 어떻게되는지'에 대한 해결책은 모든 예외를 무시하는 것입니까? 난 항상 또는 거의 항상 (편리한 사용 시도에있는 모든 이벤트 구독을 감싸는 이유가 Try.TryAction()기능하지 명시 적 try{}블록),하지만 난 예외를 무시하지 않는다, 나는 ... 그들을보고
데이브 Cousineau

0

"빈 대리자"접근 방식 대신 간단한 확장 메서드를 정의하여 이벤트 처리기를 null에 대해 검사하는 기존 방법을 캡슐화 할 수 있습니다. 여기여기에 설명 되어 있습니다 .


-2

지금까지이 질문에 대한 답으로 빠진 한 가지가 있습니다. null 값 확인을 피하는 것은 위험합니다 .

public class X
{
    public delegate void MyDelegate();
    public MyDelegate MyFunnyCallback = delegate() { }

    public void DoSomething()
    {
        MyFunnyCallback();
    }
}


X x = new X();

x.MyFunnyCallback = delegate() { Console.WriteLine("Howdie"); }

x.DoSomething(); // works fine

// .. re-init x
x.MyFunnyCallback = null;

// .. continue
x.DoSomething(); // crashes with an exception

문제는 누가 어떤 방식으로 코드를 사용할지 결코 알 수 없다는 것입니다. 코드 버그를 수정하는 동안 몇 년 동안 이벤트 / 핸들러가 null로 설정되면 알 수 없습니다.

항상 if 체크를 작성하십시오.

도움이되는 희망;)

추신 : 성능 계산에 감사드립니다.

pps : 이벤트 케이스에서 콜백 예제로 편집했습니다. 피드백을 주셔서 감사합니다. 저는 Visual Studio없이 예제를 "코딩"하고 제가 염두에 둔 예제를 이벤트에 맞게 조정했습니다. 혼란을 드려 죄송합니다.

ppps : 여전히 스레드에 맞는지 모르겠지만 중요한 원칙이라고 생각합니다. stackflow의 다른 스레드 도 확인하십시오.


1
x.MyFunnyEvent = null; <-그것은 컴파일조차하지 않습니다 (클래스 밖에서). 이벤트의 요점은 + = 및-= 만 지원합니다. 그리고 이벤트 getter가 quasi이므로 클래스 외부에서 x.MyFunnyEvent- = x.MyFunnyEvent를 수행 할 수도 없습니다 protected. 클래스 자체 (또는 파생 클래스)에서만 이벤트를 중단 할 수 있습니다.
CodesInChaos

당신 말이 맞아요 ... 사건에 대한 진실 ... 간단한 핸들러를 가진 사건이있었습니다. 죄송합니다. 편집 해 보겠습니다.
Thomas

물론 대리인이 공개 된 경우 사용자가 무엇을할지 모르기 때문에 위험 할 수 있습니다. 그러나 개인 변수에 빈 델리게이트를 설정하고 + = 및-=를 직접 처리하면 문제가되지 않으며 null 검사는 스레드로부터 안전합니다.
ForceMagic 2013-12-11
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.