【发布时间】:2010-09-26 22:06:15
【问题描述】:
我想确保我只在特定类中为实例上的事件订阅一次。
例如,我希望能够做到以下几点:
if (*not already subscribed*)
{
member.Event += new MemeberClass.Delegate(handler);
}
我将如何实施这样的保护?
【问题讨论】:
标签: c# events event-handling subscription
我想确保我只在特定类中为实例上的事件订阅一次。
例如,我希望能够做到以下几点:
if (*not already subscribed*)
{
member.Event += new MemeberClass.Delegate(handler);
}
我将如何实施这样的保护?
【问题讨论】:
标签: c# events event-handling subscription
我在所有重复的问题中都添加了这个,只是为了记录。这种模式对我有用:
myClass.MyEvent -= MyHandler;
myClass.MyEvent += MyHandler;
请注意,每次注册处理程序时都这样做将确保您的处理程序只注册一次。
【讨论】:
如果您正在谈论您可以访问其源代码的类上的事件,那么您可以将守卫放在事件定义中。
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。
【讨论】:
正如其他人所展示的,您可以覆盖事件的添加/删除属性。或者,您可能想要放弃该事件,而只是让该类在其构造函数(或其他方法)中将委托作为参数,而不是触发事件,而是调用提供的委托。
事件意味着任何人都可以订阅它们,而委托是您可以传递给类的一个方法。如果您只在真正想要它通常提供的一对多语义时才使用事件,那么对于您的库的用户来说可能就不那么令人惊讶了。
【讨论】:
您可以使用 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
【讨论】:
您需要存储一个单独的标志来指示您是否已订阅,或者,如果您可以控制 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。
【讨论】:
我知道这是一个老问题,但当前的答案对我不起作用。
查看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,您还需要包含它。
这个解决方案不是很漂亮,但它确实有效。
【讨论】:
我最近做了这个,我就把它放在这里让它保留:
private bool subscribed;
if(!subscribed)
{
myClass.MyEvent += MyHandler;
subscribed = true;
}
private void MyHandler()
{
// Do stuff
myClass.MyEvent -= MyHandler;
subscribed = false;
}
【讨论】:
在提升时仅调用来自 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);
}
【讨论】: