Delegate Equal() returning false when it shouldn't?

  • Thread starter Thread starter Israel
  • Start date Start date
I

Israel

I've run into this odd issue that I've never seen before. I have code
that has worked fine for a few years using .NET 1.1 and then when we
switched to .NET 2.0 it started not working. There were no code
changes related to this class or anything uses it.

I have a class which has a data member that's holding on to a delegate
that was set at one point and then later it is compared against what
was set and for some reason the Equal() method thinks it's different
even though I know they're the same. The hash codes are the same for
the delegates. The hash codes of the internal targets are the same
and all data members I can see in the debugger show that they are
absolutely identical.
Secondly I do this hundreds of times through out our product and all
other instances seem to work but for some reason this one just stopped
working.

Basically it comes down to this:
Class Dummy
{
void MethodA()
{
m_Handler = new XXXXHandler(TheHandler);
}

void MethodB()
{
MethodC(new XXXXHandler(TheHandler));
}

void MethodC(XXXXHandler handler)
{
if (m_Handler != handler)
{
throw new Exception("Ouch quite it!"); // this always
throws
}
}

void TheHandler(object sender, XXXXEventArgs args)
{
}

XXXXHandler m_Handler;
}
 
Israel said:
I've run into this odd issue that I've never seen before. I have code
that has worked fine for a few years using .NET 1.1 and then when we
switched to .NET 2.0 it started not working. There were no code
changes related to this class or anything uses it.

I have a class which has a data member that's holding on to a delegate
that was set at one point and then later it is compared against what
was set and for some reason the Equal() method thinks it's different
even though I know they're the same. The hash codes are the same for
the delegates. The hash codes of the internal targets are the same
and all data members I can see in the debugger show that they are
absolutely identical.

But does first.Target.Equals(second.Target)? If you've got a badly
written Equals method, that would explain the problem.

Could you post a short but complete program which demonstrates the
problem?

See http://www.pobox.com/~skeet/csharp/complete.html for details of
what I mean by that.
 
But does first.Target.Equals(second.Target)? If you've got a badly
written Equals method, that would explain the problem.
No objects have overriden Equals(). In the above example
first.Target.Equals(second.Target) returns false so at least that's
consistent with the delegate equals returning false.
Could you post a short but complete program which demonstrates the
problem?

What I have is a dialog in the UI connecting to events through another
class which connects to events from another process via a remoted
wrapper. When the dialog is closed it disconnects from the event
which bubbles down and finally throws after the check noted by "THESE
ARE NEVER EQUAL"
* I should re-itterate that this is the exact same model used for
every event in our system which is easily on the order of 100 or so.
This also worked perfectly find in .NET 1.1.

Here is the code which I copied directly and only removed lines for
brevity nothing has been added or renamed to protect to innocent:
public class SampleManagementDlg : System.Windows.Forms.Form
{
ISampleManagement m_SampleManagement;

public SampleManagementDlg(ISampleManagement sampleManagement)
{
m_SampleManagement = sampleManagement;
m_SampleManagement.SampleLoadStatusChanged +=
new
SampleLoadStatusChangedHandler(OnSampleLoadStatusChanged);
}

protected override void OnClosing(CancelEventArgs e)
{
// Disconnect from events
m_SampleManagement.SampleLoadStatusChanged -=
new
SampleLoadStatusChangedHandler(OnSampleLoadStatusChanged);
}
}

public class SampleManagement : ISampleManagement
{
SampleLoadStatusChangedEventWrapper
_SampleLoadStatusChangedWrapper = null;
// Note: _SystemServer and _SystemServer.Vacuum are remoted
objects from another process

public event SampleLoadStatusChangedHandler
SampleLoadStatusChanged
{
add
{
if (_SampleLoadStatusChangedWrapper == null)
_SampleLoadStatusChangedWrapper = new
SampleLoadStatusChangedEventWrapper();

_SystemServer.Vacuum.SampleLoadStatusChanged +=
_SampleLoadStatusChangedWrapper.AddProxyHandler(new
SampleLoadStatusChangedHandler(value));
}

remove
{
if (_SampleLoadStatusChangedWrapper != null)
_SystemServer.Vacuum.SampleLoadStatusChanged -=

_SampleLoadStatusChangedWrapper.RemoveProxyHandler(new
SampleLoadStatusChangedHandler(value));
}
}

public class SampleLoadStatusChangedEventWrapper :
MarshalByRefObject
{
public SampleLoadStatusChangedEventWrapper()
{
m_ExternalEventHandler = null;
}

private SampleLoadStatusChangedHandler m_ExternalEventHandler;

public event SampleLoadStatusChangedHandler OnLocalEvent
{
add
{
if( m_ExternalEventHandler == null )
m_ExternalEventHandler = value;
else
throw new Exception(
"Event wrappers, for remoting, can have one
and only one handler");
}
remove
{
if ( m_ExternalEventHandler == value ) // THESE ARE
NEVER EQUAL
m_ExternalEventHandler = null;
else
throw new Exception("Cannot remove event handlers
that were never added");
}
}

public SampleLoadStatusChangedHandler AddProxyHandler(
SampleLoadStatusChangedHandler clientHandler )
{
OnLocalEvent += clientHandler;
return new
SampleLoadStatusChangedHandler( LocallyHandleMessageArrived );
}

public SampleLoadStatusChangedHandler RemoveProxyHandler(
SampleLoadStatusChangedHandler clientHandler )
{
OnLocalEvent -= clientHandler;
return new
SampleLoadStatusChangedHandler(LocallyHandleMessageArrived);
}

public void LocallyHandleMessageArrived( object obj,
SampleLoadStatusChangedEventArgs arg )
{
m_ExternalEventHandler( obj, arg );
}

public override object InitializeLifetimeService()
{
return null;
}
}
}
 
Israel said:
No objects have overriden Equals(). In the above example
first.Target.Equals(second.Target) returns false so at least that's
consistent with the delegate equals returning false.

Okay, so for some reason your code is now creating two different
targets, when it didn't before.

I notice there's marshalling and proxying going on - I *strongly*
suspect that that will be at the heart of what's going on, but it's
hard to say without a complete example to prod. (I understand that
producing such an example would be hard in this case.)
 
There was a minor typo in that last post; I put a brace in the wrong
place so I didn't mean to imply that
SampleLoadStatusChangedEventWrapper was a contained class within
SampleManagement. They are independent classes.
 
Ok, so I'm not sure I really understand WHY I had the problem before
but I made a simple change that fixed it.

I noticed that in the SampleManagement's SampleLoadStatusChanged add
and remove method I was newing up another delegate and passing in the
caller's delegate to the c'tor:
_SystemServer.Vacuum.SampleLoadStatusChanged -=
_SampleLoadStatusChangedWrapper.RemoveProxyHandler(new
SampleLoadStatusChangedHandler(value));

I figured that this was an extra new but would equate to the same
thing since I can new two delegates that point to the same method on
the same target and they're equal even though I technically new'd two
objects. In this case, with the c'tor, it seems that somehow the
target is different? Does it get copied somehow and therefore become
another object? Or is the target somehow really the original delegate
yet it has the same hashcode as the original target?
 
Israel said:
Ok, so I'm not sure I really understand WHY I had the problem before
but I made a simple change that fixed it.

I noticed that in the SampleManagement's SampleLoadStatusChanged add
and remove method I was newing up another delegate and passing in the
caller's delegate to the c'tor:
_SystemServer.Vacuum.SampleLoadStatusChanged -=
_SampleLoadStatusChangedWrapper.RemoveProxyHandler(new
SampleLoadStatusChangedHandler(value));

I figured that this was an extra new but would equate to the same
thing since I can new two delegates that point to the same method on
the same target and they're equal even though I technically new'd two
objects. In this case, with the c'tor, it seems that somehow the
target is different? Does it get copied somehow and therefore become
another object? Or is the target somehow really the original delegate
yet it has the same hashcode as the original target?

Well, hashcodes are effectively irrelevant - but creating a new
delegate there shouldn't change the target.

I'm afraid your example is pretty complicated, with several different
events etc - and I'm currently not well, which doesn't help. If you're
ever able to get a complete example which demonstrates it reliably
(failing in .NET 2.0 but working in 1.1), I'd be interested to see
it...
 
I'm afraid your example is pretty complicated, with several different
events etc - and I'm currently not well, which doesn't help. If you're
ever able to get a complete example which demonstrates it reliably
(failing in .NET 2.0 but working in 1.1), I'd be interested to see
it...

Ok, so I tried this example using 1.1 and 2.0 and it fails in both.
Somehow this problem didn't arise until we switched from 2003 to 2005
and I can't easily copy test software onto machines that are currently
running out software using .NET 1.1. for political reasons not
technical.

In anycase, independent of whether this working 1.1 or not, I would
expect handle1 and handle2 to be equal in this example but they are
not.

public class Dummy
{
public void Test()
{
EventHandler handler1 = new EventHandler(Handler);
EventHandler handler2 = new EventHandler(handler1);

if (handler1 != handler2)
{
throw new Exception("Ouch quite it!");
}
}

public void Handler(object sender, EventArgs args)
{
}
}
 
Israel said:
Ok, so I tried this example using 1.1 and 2.0 and it fails in both.
Somehow this problem didn't arise until we switched from 2003 to 2005
and I can't easily copy test software onto machines that are currently
running out software using .NET 1.1. for political reasons not
technical.
:)

In anycase, independent of whether this working 1.1 or not, I would
expect handle1 and handle2 to be equal in this example but they are
not.

Very interesting - and unexpected. I think I can see (from the IL) why
this happens though: when you write:

EventHandler handler2 = new EventHandler(handler1);

it's saying "the method to call when you invoke handler2 is the Invoke
method of handler1". In other words, they won't have the same
invocation list internally.

The spec is unclear on this one, but I'll post it to the C# team to see
what the reaction is.
 
The spec is unclear on this one, but I'll post it to the C# team to see
what the reaction is.

Actually, the spec isn't unclear after all. Here's a bit of section
15.3:

<quote>
An instance of a delegate is created by a delegate-creation-expression
(§7.5.10.5) or a conversion to a delegate type. The newly created
delegate instance then refers to either:

o The static method referenced in the delegate-creation-expression, or
o The target object (which cannot be null) and instance method
referenced in the delegate-creation-expression, or
o Another delegate.
</quote>

And basically the third option is not the same as "the existing methods
in the other delegate's invocation list".
 
And basically the third option is not the same as "the existing methods
in the other delegate's invocation list".

Interesting... In the last example I gave the target was very
obviously not the same yet in my original example the target's both
looked identical because I was creating identical looking delegates in
the add and remove method but they were in fact NOT identical.
In retrospect I did think it odd that the watch window wasn't showing
the type of my original dialog class but for some reason I overlooked
that.
 
Back
Top