委托转换会破坏相等,并且无法断开与事件的连接

本文关键字:断开 连接 事件 转换 | 更新日期: 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;

这是因为,正如它所指出的,在转换后(发生在事件addremove(代表是不相等的。添加的委托与删除的委托不同。这可能会导致严重且难以发现的内存泄漏。

当然,可以说只使用第一种方法。但在某些情况下可能是不可能的,尤其是在处理泛型时。

请考虑以下方案。假设我们有来自第三方库的委托和接口,如下所示:

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());
}

(请参阅演示。