代表转换中断并且无法与事件断开

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

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事件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); }
   }

}

由于类使用通用事件处理程序,并且接口使用其他类型,所以我们必须进行转换。 当然,这使得事件不可能脱离。 唯一的方法是将委托存储到变量中,连接它并在需要时断开连接。 然而这是非常肮脏的做法。

有人可以告诉,如果它打算这样工作,或者它是一个错误? 如何以一种干净的方式将一个事件处理程序连接到兼容的程序并具有断开连接的能力?


这似乎是有意的。当你说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());
    }
    

    (查看演示)

    链接地址: http://www.djcxy.com/p/51519.html

    上一篇: Delegate conversion breaks equality and unables to disconnect from event

    下一篇: How to release anonymous delegates / closures correctly in C#?