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

时间:2014-09-27 18:18:50

标签: c# .net events memory-leaks

我最近发现了一些代表的奇怪行为。看来,将委托转发给其他人(兼容,甚至相同)会破坏委托的平等。假设我们有一些方法:

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,反之亦然,但在转换后,它们将不相等。

当我们想要使用显式addremove定义事件以将处理程序传递给其他对象时,此行为会产生误导。因此,下面的两个事件定义的行为都不同:

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 个答案:

答案 0 :(得分:1)

这似乎是预期的。 1 当您说new DelegateType(otherDelegate)时,您实际上是在创建一个新的委托,该委托不指向与{相同的目标和方法{1}}确实如此,但指向otherDelegate作为目标,otherDelegate作为方法。所以他们确实是不同的代表:

otherDelegate.Invoke(...)

1 在检查C#规范时,我不清楚这是否在技术上违反了规范。我在这里复制了C #laguange规范3.03.0中的§7.5.10.5的一部分:

  

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 格式的委托创建表达式的运行时处理,其中new D(E)委托类型并且D表达式,包含以下步骤:

     
      
  • ...
  •   
  • 如果E委托类型的值:      
        
    • ...
    •   
    • 使用与E给出的委托实例相同的调用列表初始化新的委托实例。
    •   
  •   

通过让一个委托调用另一个委托的E方法,可能会认为是否满足“使用相同的调用列表初始化”是一个解释问题。我倾向于倾向于“不”。 (Jon Skeet leans towards "yes."


作为一种变通方法,您可以使用此扩展方法转换委托,同时保留其确切的调用列表:

Invoke()

See a demo。)