代理转换会中断相等且无法断开与事件的连接 [英] Delegate conversion breaks equality and unables to disconnect from event

查看:124
本文介绍了代理转换会中断相等且无法断开与事件的连接的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  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 删除来定义一个事件时,会将处理程序传递给其他对象。因此,以下两个事件定义的作用不同:

  public event EventHandler MyGoodEvent {
add {
myObject其他事件+ =值;
}
删除{
myObject.OtherEvent - = value;
}
}

公共事件EventHandler MyBadEvent {
add {
myObject.OtherEvent + = new EventHandler(value);
}
删除{
myObject.OtherEvent - = new EventHandler(value);
}
}

第一个可以正常工作。第二个会导致内存泄漏,因为当我们连接一些方法到事件时,我们将无法断开连接:

  var foo = new Foobar(); 
//我们可以连接
myObject.MyBadEvent + = foo.SomeMethod;
//这不会断开
myObject.MyBadEvent - = foo.SomeMethod;

正因为如此,转换后(发生在事件添加删除)代理不相等。被添加的代理不一样,删除。这可能会导致严重的,很难找到内存泄漏。



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



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

  public delegate void SomeEventHandler(object sender ,SomeEventArgs e); 

public interface ISomeInterface {
event SomeEventHandler MyEvent;
}

我们希望实现该界面。内部实现将基于一些其他具有通用类的第三方库:

  public class GenericClass< T>其中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); }
}

}

因为类使用通用事件处理程序,和界面使用一些其他,我们必须做一个转换。当然,这使得事件无法断开连接。唯一的方法是将代理存储到变量中,连接它,并在需要时断开连接。这是非常脏的方法。



有人可以告诉你,如果这样做是为了这样工作,还是一个bug?如何以一种干净的方式将一个事件处理程序与一个事件处理程序连接起来,以断开连接?

解决方案

1 当您说新的DelegateType(otherDelegate)时,您实际上正在创建一个不相同的新委托目标和方法为 otherDelegate ,但是指向 otherDelegate 作为目标, otherDelegate。调用(...)作为方法。所以他们确实是不同的代表:

  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的一部分:


运行时处理 新的D(E)形式的委托创建表达式,其中 D 是一个>代表类型和 E 是一个表达式,包括以下步骤:




  • ...

  • 如果 E 代表的值-type


    • ...

    • 使用相同的调用列表初始化新的委托实例作为由 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());
}

见演示


I recently found some strange behaviour of delegates. It appears, that casting a delegate to some other (compatible, or even the same) breaks the equality of delegates. Suppose we have some class with method:

public class Foobar {
   public void SomeMethod(object sender, EventArgs e);
}

Now let's make some delegates:

var foo = new Foobar();
var first = new EventHandler(foo.SomeMethod);
var second = new EventHandler(foo.SomeMethod);

Of course, because delegates with the same target, method and invocation list are considered equal, this assertion will pass:

Assert.AreEqual(first, second);

But this assertion won't:

Assert.AreEqual(new EventHandler(first), new EventHandler(second));

However, the next assertion will pass:

Assert.AreEqual(new EventHandler(first), new EventHandler(first));

This is quite awkward behaviour, since both delegates are considered equal. Converting it to a delegate of even the same type in some way breaks its equality. The same will be, we define our own type of delegate:

public delegate MyEventHandler(object sender, EventArgs e);

Delegates can be converted from EventHandler to MyEventHandler, and in reverse direction, however after this conversion, they won't be equal.

This behaviour is very misleading when we want to define an event with explicit add and remove to pass handler to some other object. Therefore both of event definitions below are acting differently:

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

The first one will work fine. The second one will cause memory leaks, because when we connect some method to the event, we won't be able to disconnect:

var foo = new Foobar();
// we can connect
myObject.MyBadEvent += foo.SomeMethod;
// this will not disconnect
myObject.MyBadEvent -= foo.SomeMethod;

This is because, as it was pointed, after conversion (which occurs in event add and remove) delegates are not equal. Delegate which is added is not the same, as which is removed. This can lead to serious, and hard to find memory leaks.

Of course one can say to use only first approach. But it may be impossible in some circumstances, especially when dealing with generics.

Consider the following scenario. Let's suppose, that we have delegate and interface from third party library, which look like this:

public delegate void SomeEventHandler(object sender, SomeEventArgs e);

public interface ISomeInterface {
   event SomeEventHandler MyEvent;
}

We would like to implement that interface. Inner implementation of this will be based on some other third party library, which has a generic class:

public class GenericClass<T> where T : EventArgs
{    
    public event EventHandler<T> SomeEvent;
}

We want this generic class to expose its event to the interface. For example, we can do something like that:

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

}

Because class uses generic event handler, and interface uses some other, we have to make a conversion. Of course, this makes an event impossible to disconnect from. The only way is to store a delegate into a variable, connect it, and disconnect when needed. This is however very dirty approach.

Can someone tell, if it is intended to work like this, or it is a bug? How to connect one event handler to compatible one in a clean way with the ability to disconnect it?

解决方案

This appears to be intended.1 When you say new DelegateType(otherDelegate) you are actually creating a new delegate that points not to the same target and method as otherDelegate does, but that points to otherDelegate as the target and otherDelegate.Invoke(...) as the method. So they are indeed different delegates:

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 Upon examining the C# specification, it is not clear to me if this is technically in violation of the spec. I reproduce here part of §7.5.10.5 from the C# laguange specification 3.03.0:

The run-time processing of a delegate-creation-expression of the form new D(E), where D is a delegate-type and E is an expression, consists of the following steps:

  • ...
  • If E is a value of a delegate-type:
    • ...
    • The new delegate instance is initialized with the same invocation list as the delegate instance given by E.

Perhaps it is a matter of interpretation whether "initialized with the same invocation list" can be considered satisfied by having one delegate call the other delegate's Invoke() method. I tend to lean towards "no" here. (Jon Skeet leans towards "yes.")


As a workaround, you can use this extension method to convert delegates while retaining their exact invocation list:

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

(See a demo.)

这篇关于代理转换会中断相等且无法断开与事件的连接的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆