【问题标题】:How to ensure an event is only subscribed to once如何确保一个事件只订阅一次
【发布时间】:2010-09-26 22:06:15
【问题描述】:

我想确保我只在特定类中为实例上的事件订阅一次。

例如,我希望能够做到以下几点:

if (*not already subscribed*)
{
    member.Event += new MemeberClass.Delegate(handler);
}

我将如何实施这样的保护?

【问题讨论】:

    标签: c# events event-handling subscription


    【解决方案1】:

    我在所有重复的问题中都添加了这个,只是为了记录。这种模式对我有用:

    myClass.MyEvent -= MyHandler;
    myClass.MyEvent += MyHandler;
    

    请注意,每次注册处理程序时都这样做将确保您的处理程序只注册一次。

    【讨论】:

    • 也适用于我,这在我看来是您无法访问课程的最佳解决方案(在我的情况下为 Form.KeyDown)
    • 这个解决方案有个警告。如果您在事件发生时取消订阅,您将错过该事件,因此请确保在取消订阅和订阅之间 100% 没有事件发生。
    • 在我的例子中,如果我试图避免 WPF 控制事件重复,这个解决方案是最简单和最干净的方法。
    • 这对我们大多数人来说似乎很明显,但我想明确一点:它只有在您取消订阅/订阅一个类的相同实例时才有效。例如,与其他答案的“_eventHasSubscribers”方法不同,它不适用于不同的实例。
    • 这对我不起作用,我认为这是因为多线程。 “MyHandler”与以前版本的事件不是同一个对象,因此它从未被删除并且一次又一次地添加等等。我必须在添加 MyHandler 事件之前验证 MyHandler.Method 尚未添加。
    【解决方案2】:

    如果您正在谈论您可以访问其源代码的类上的事件,那么您可以将守卫放在事件定义中。

    private bool _eventHasSubscribers = false;
    private EventHandler<MyDelegateType> _myEvent;
    
    public event EventHandler<MyDelegateType> MyEvent
    {
       add 
       {
          if (_myEvent == null)
          {
             _myEvent += value;
          }
       }
       remove
       {
          _myEvent -= value;
       }
    }
    

    这将确保只有一个订阅者可以订阅提供事件的类的这个实例上的事件。

    编辑请参阅 cmets,了解为什么上面的代码是一个坏主意并且不是线程安全的。

    如果您的问题是客户端的单个实例多次订阅(并且您需要多个订阅者),那么客户端代码将需要处理该问题。所以替换

    尚未订阅

    在您第一次订阅事件时设置客户端类的 bool 成员。

    编辑(接受后):根据@Glen T(问题的提交者)的评论,他接受的解决方案的代码位于客户端类中:

    if (alreadySubscribedFlag)
    {
        member.Event += new MemeberClass.Delegate(handler);
    }
    

    其中 alreadySubscribedFlag 是客户端类中的一个成员变量,用于跟踪对特定事件的首次订阅。 在这里查看第一个代码 sn-p 的人,请注意 @Rune 的评论 - 以不明显的方式更改订阅事件的行为不是一个好主意。

    编辑 31/7/2009: 请参阅@Sam Saffron 的 cmets。正如我已经说过的,Sam 同意这里介绍的第一种方法不是修改事件订阅行为的明智方法。类的消费者需要了解其内部实现以了解其行为。不是很好。
    @Sam Saffron 还讨论了线程安全。我假设他指的是可能的竞争条件,其中两个订阅者(接近)同时尝试订阅并且他们最终都可能订阅。可以使用锁来改善这一点。如果您打算改变事件订阅的工作方式,那么我建议您read about how to make the subscription add/remove properties thread safe

    【讨论】:

    • 我想我会继续使用布尔成员变量方法。但是,我有点惊讶没有其他方法可以检查客户是否已经订阅。我原以为给定客户只想订阅一次更常见?
    • 根据您的设置,如果事件已经有订阅者,您可能希望抛出异常。如果您在运行时添加订阅者,这将通知他们错误而不是什么都不做。在不通知用户的情况下更改默认行为并不是最佳做法。
    • 这也是一个主要的反模式,任意的消费者需要知道你的事件实现才能理解它的行为。
    • @Sam Saffron:感谢 cmets。已尝试将有关第一种方法的一些(进一步)警告放入答案中。
    【解决方案3】:

    正如其他人所展示的,您可以覆盖事件的添加/删除属性。或者,您可能想要放弃该事件,而只是让该类在其构造函数(或其他方法)中将委托作为参数,而不是触发事件,而是调用提供的委托。

    事件意味着任何人都可以订阅它们,而委托是您可以传递给类的一个方法。如果您只在真正想要它通常提供的一对多语义时才使用事件,那么对于您的库的用户来说可能就不那么令人惊讶了。

    【讨论】:

      【解决方案4】:

      您可以使用 Postsharper 只编写一个属性并在正常事件上使用它。重用代码。代码示例如下。

      [Serializable]
      public class PreventEventHookedTwiceAttribute: EventInterceptionAspect
      {
          private readonly object _lockObject = new object();
          readonly List<Delegate> _delegates = new List<Delegate>();
      
          public override void OnAddHandler(EventInterceptionArgs args)
          {
              lock(_lockObject)
              {
                  if(!_delegates.Contains(args.Handler))
                  {
                      _delegates.Add(args.Handler);
                      args.ProceedAddHandler();
                  }
              }
          }
      
          public override void OnRemoveHandler(EventInterceptionArgs args)
          {
              lock(_lockObject)
              {
                  if(_delegates.Contains(args.Handler))
                  {
                      _delegates.Remove(args.Handler);
                      args.ProceedRemoveHandler();
                  }
              }
          }
      }
      

      就这样使用吧。

      [PreventEventHookedTwice]
      public static event Action<string> GoodEvent;
      

      详情请看Implement Postsharp EventInterceptionAspect to prevent an event Handler hooked twice

      【讨论】:

        【解决方案5】:

        您需要存储一个单独的标志来指示您是否已订阅,或者,如果您可以控制 MemberClass,则需要为事件提供 add 和 remove 方法的实现:

        class MemberClass
        {
                private EventHandler _event;
        
                public event EventHandler Event
                {
                    add
                    {
                        if( /* handler not already added */ )
                        {
                            _event+= value;
                        }
                    }
                    remove
                    {
                        _event-= value;
                    }
                }
        }
        

        要确定是否添加了处理程序,您需要比较 GetInvocationList() 返回的 _event 和 value 的 Delegates。

        【讨论】:

          【解决方案6】:

          我知道这是一个老问题,但当前的答案对我不起作用。

          查看C# pattern to prevent an event handler hooked twice(标记为此问题的副本),给出的答案更接近,但仍然不起作用,可能是因为多线程导致新事件对象不同,或者可能是因为我是使用自定义事件类。我最终得到了与上述问题的已接受答案类似的解决方案。

          private EventHandler<bar> foo;
          public event EventHandler<bar> Foo
          {
              add
              {
                  if (foo == null || 
                      !foo.GetInvocationList().Select(il => il.Method).Contains(value.Method))
                  {
                      foo += value;
                  }
              }
          
              remove
              {
                  if (foo != null)
                  {
                      EventHandler<bar> eventMethod = (EventHandler<bar>)foo .GetInvocationList().FirstOrDefault(il => il.Method == value.Method);
          
                      if (eventMethod != null)
                      {
                          foo -= eventMethod;
                      }
                  }
              }
          }
          

          这样,您还必须使用foo.Invoke(...) 而不是Foo.Invoke(...) 来触发您的事件。如果您还没有使用System.Linq,您还需要包含它。

          这个解决方案不是很漂亮,但它确实有效。

          【讨论】:

            【解决方案7】:

            我最近做了这个,我就把它放在这里让它保留:

            private bool subscribed;
            
            if(!subscribed)
            {
                myClass.MyEvent += MyHandler;
                subscribed = true;
            } 
            
            private void MyHandler()
            {
                // Do stuff
                myClass.MyEvent -= MyHandler;
                subscribed = false;
            }
            

            【讨论】:

              【解决方案8】:

              在提升时仅调用来自 GetInvocationList 的不同元素:

              using System.Linq;
              ....
              public event HandlerType SomeEvent;
              ....
              //Raising code
              foreach (HandlerType d in (SomeEvent?.GetInvocationList().Distinct() ?? Enumerable.Empty<Delegate>()).ToArray())
                   d.Invoke(sender, arg);
              

              示例单元测试:

              class CA 
              {
                  public CA()
                  { }
                  public void Inc()
                      => count++;
                  public int count;
              }
              [TestMethod]
              public void TestDistinctDelegates()
              {
                  var a = new CA();
                  Action d0 = () => a.Inc();
                  var d = d0;
                  d += () => a.Inc();
                  d += d0;
                  d.Invoke();
                  Assert.AreEqual(3, a.count);
                  var l = d.GetInvocationList();
                  Assert.AreEqual(3, l.Length);
                  var distinct = l.Distinct().ToArray();
                  Assert.AreEqual(2, distinct.Length);
                  foreach (Action di in distinct)
                      di.Invoke();
                  Assert.AreEqual(3 + distinct.Length, a.count);
              }
              [TestMethod]
              public void TestDistinctDelegates2()
              {
                  var a = new CA();
                  Action d = a.Inc;
                  d += a.Inc;
                  d.Invoke();
                  Assert.AreEqual(2, a.count);
                  var distinct = d.GetInvocationList().Distinct().ToArray();
                  Assert.AreEqual(1, distinct.Length);
                  foreach (Action di in distinct)
                      di.Invoke();
                  Assert.AreEqual(3, a.count);
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2023-03-15
                • 2016-04-22
                • 1970-01-01
                • 2018-07-30
                • 2018-05-22
                • 1970-01-01
                • 2021-05-30
                • 1970-01-01
                相关资源
                最近更新 更多