委托转换会破坏相等,并且无法断开与事件的连接
本文关键字:断开 连接 事件 转换 | 更新日期: 2023-09-27 18:24:42
我最近发现了一些代表的奇怪行为。看起来,将委托投给其他(兼容的,甚至相同(会破坏委托的平等。假设我们有一些带有方法的类:
public class Foobar {
public void SomeMethod(object sender, EventArgs e);
}
现在让我们做一些委托:
var foo = new Foobar();
var first = new EventHandler(foo.SomeMethod);
var second = new EventHandler(foo.SomeMethod);
当然,由于具有相同目标、方法和调用列表的委托被认为是相等的,因此此断言将通过:
Assert.AreEqual(first, second);
但这个断言不会:
Assert.AreEqual(new EventHandler(first), new EventHandler(second));
但是,下一个断言将通过:
Assert.AreEqual(new EventHandler(first), new EventHandler(first));
这是相当尴尬的行为,因为两个代表被认为是平等的。以某种方式将其转换为相同类型的委托会破坏其相等性。同样,我们定义自己的委托类型:
public delegate MyEventHandler(object sender, EventArgs e);
委托可以从EventHandler
转换为MyEventHandler
,并且方向相反,但是在此转换之后,它们将不相等。
当我们想要定义具有显式add
的事件并remove
将处理程序传递给其他对象时,这种行为非常具有误导性。因此,下面的两个事件定义的作用不同:
public event EventHandler MyGoodEvent {
add {
myObject.OtherEvent += value;
}
remove {
myObject.OtherEvent -= value;
}
}
public event EventHandler MyBadEvent {
add {
myObject.OtherEvent += new EventHandler(value);
}
remove {
myObject.OtherEvent -= new EventHandler(value);
}
}
第一个将正常工作。第二个会导致内存泄漏,因为当我们将某些方法连接到事件时,我们将无法断开连接:
var foo = new Foobar();
// we can connect
myObject.MyBadEvent += foo.SomeMethod;
// this will not disconnect
myObject.MyBadEvent -= foo.SomeMethod;
这是因为,正如它所指出的,在转换后(发生在事件add
和remove
(代表是不相等的。添加的委托与删除的委托不同。这可能会导致严重且难以发现的内存泄漏。
当然,可以说只使用第一种方法。但在某些情况下可能是不可能的,尤其是在处理泛型时。
请考虑以下方案。假设我们有来自第三方库的委托和接口,如下所示:
public delegate void SomeEventHandler(object sender, SomeEventArgs e);
public interface ISomeInterface {
event SomeEventHandler MyEvent;
}
我们希望实现该接口。它的内部实现将基于其他一些第三方库,该库有一个泛型类:
public class GenericClass<T> where T : EventArgs
{
public event EventHandler<T> SomeEvent;
}
我们希望这个泛型类向接口公开其事件。例如,我们可以做这样的事情:
public class MyImplementation : ISomeInterface {
private GenericClass<SomeEventArgs> impl = new GenericClass<SomeEventArgs>();
public event SomeEventHandler MyEvent {
add { impl.SomeEvent += new SomeOtherEventHandler(value); }
remove { impl.SomeEvent -= new SomeOtherEventHandler(value); }
}
}
因为类使用泛型事件处理程序,而接口使用其他一些,所以我们必须进行转换。当然,这使得事件无法断开连接。唯一的方法是将委托存储到变量中,连接它,并在需要时断开连接。然而,这是非常肮脏的方法。
有人可以说,它是打算像这样工作,还是是一个错误?如何以干净的方式将一个事件处理程序连接到兼容的事件处理程序,并能够断开连接?
似乎是有意为之的。1 当你说new DelegateType(otherDelegate)
时,你实际上是在创建一个新的委托,它指向的目标和方法与otherDelegate
不同,而是指向otherDelegate
作为目标,otherDelegate.Invoke(...)
作为方法。 所以他们确实是不同的代表:
csharp> EventHandler first = (object sender, EventArgs e) => {};
csharp> var second = new EventHandler(first);
csharp> first.Target;
null
csharp> first.Method;
Void <Host>m__0(System.Object, System.EventArgs)
csharp> second.Target;
System.EventHandler
csharp> second.Method;
Void Invoke(System.Object, System.EventArgs)
csharp> second.Target == first;
true
1 在检查 C# 规范时,我不清楚这在技术上是否违反了规范。 我在这里复制了 C# laguange 规范 3.03.0 中的 §7.5.10.5 的一部分:
窗体
new D(E)
的委托创建表达式的运行时处理包括以下步骤,其中D
是委托类型,E
是表达式:
- 。
- 如果
E
是委托类型的值:
- 。
新的委托实例使用与E
给出的委托实例相同的调用列表进行初始化。
也许可以通过让一个委托调用另一个委托的Invoke()
方法来认为"使用相同的调用列表初始化"可以被认为是满足的解释问题。 我倾向于在这里"不"。 (Jon Skeet倾向于"是"。
作为解决方法,您可以使用此扩展方法来转换委托,同时保留其确切的调用列表:
public static Delegate ConvertTo(this Delegate self, Type type)
{
if (type == null) { throw new ArgumentNullException("type"); }
if (self == null) { return null; }
return Delegate.Combine(
self.GetInvocationList()
.Select(i => Delegate.CreateDelegate(type, i.Target, i.Method))
.ToArray());
}
(请参阅演示。