50

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

例如,我希望能够执行以下操作:

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

我将如何实施这样的警卫?

4

8 回答 8

68

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

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

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

于 2011-08-15T14:01:30.170 回答
40

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

private bool _eventHasSubscribers = false;
private EventHandler<MyDelegateType> _myEvent;

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

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

编辑请查看为什么上面的代码是一个坏主意而不是线程安全的评论。

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

尚未订阅

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

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

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

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

编辑 31/7/2009:请参阅@Sam Saffron 的评论。正如我已经说过的,Sam 同意这里介绍的第一种方法不是修改事件订阅行为的明智方法。类的消费者需要了解其内部实现以了解其行为。不大好。
@Sam Saffron 还评论了线程安全。我假设他指的是可能的竞争条件,其中两个订阅者(接近)同时尝试订阅并且他们最终都可能订阅。可以使用锁来改善这一点。如果您打算更改事件订阅的工作方式,那么我建议您阅读有关如何使订阅添加/删除属性线程安全的信息。

于 2008-12-15T05:24:29.457 回答
7

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

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

于 2008-12-15T05:44:41.147 回答
5

您可以使用 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;

有关详细信息,请查看实现 Postsharp EventInterceptionAspect 以防止事件处理程序被钩住两次

于 2012-04-20T16:40:27.703 回答
3

您需要存储一个单独的标志来指示您是否订阅,或者,如果您可以控制 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。

于 2008-12-15T05:23:48.600 回答
2

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

查看C# 模式以防止事件处理程序被挂钩两次(标记为该问题的副本),给出更接近但仍然不起作用的答案,可能是因为多线程导致新事件对象不同,或者可能因为我使用的是自定义事件类。我最终得到了与上述问题的已接受答案类似的解决方案。

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如果您还没有使用它,您还需要包含它。

这个解决方案并不完全漂亮,但它确实有效。

于 2020-07-30T19:17:19.980 回答
0

我最近这样做了,我就把它放在这里,这样它就可以保留:

private bool subscribed;

if(!subscribed)
{
    myClass.MyEvent += MyHandler;
    subscribed = true;
} 

private void MyHandler()
{
    // Do stuff
    myClass.MyEvent -= MyHandler;
    subscribed = false;
}
于 2020-02-24T09:43:09.100 回答
-1

在 raise 时只调用不同的元素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);
}
于 2021-07-01T16:13:36.960 回答