Equals method and immutable objects

J

Jon Skeet [C# MVP]

Edward Diener said:
hashtable[x] = "hello";
x.ChangeSomething();
x.ChangeSomethingElse();

then I wouldn't expect reading hashtable[x] to work, because the
hashcode would have changed by then, in my scheme of things.

The issue is that GetHashCode would return the same value since it is based
on an immutable field, but Equals would not. Then in case of a hash code
collision, the Equals method wouldn't be able to find the value.

I was suggesting basing GetHashCode and Equals on the same data, so
that they were always in step. ie Equals returning true => GetHashCode
returning the same value. That violates the GetHashCode constraint that
the same object always returns the same hash code, but only when the
values are changed.
The key in the hashtable wouldn't have changed but the original object which
serves as the key for finding the value could have changed if the object is
mutable.

But essentially that's then an entirely different object in .NET terms
- the "object" of a value type is precisely its values. When you change
the values, it's a different "object", just as:

int i = 5;
i = 4;

isn't changing the value of 5 to 4, it's just changing the value of i
to 4.
We are going around in circles here. My original point is simply that
classes which implement GetHashCode and Equals will not be in sync once one
of the mutable values upon which Equals is based changes.

*If* they abide by the rule which says that GetHashCode can't return
different values, which I've always said is a rule which I think can be
violated reasonably.
I am reticent to use Equals as my method to test for object equality, but
will create some other function for it in case an end-user needs to test
whether or not one of my components is "equal" to another of my components
of the same type. I continue to regard the doc that GetHashCode and Equals
must be in sync as flawed.

If it's a flaw, it's a necessary one. Essentially there are two
"views" of a key, and one of them is cached. The caching is necessary
for performance. If you keep the views in sync, the cache will get out
of date with regard to the original key. If you don't keep the views in
sync, the cache will get out of date with regard to a new but equal
key.
 
E

Edward Diener

Jon said:
Edward Diener said:
hashtable[x] = "hello";
x.ChangeSomething();
x.ChangeSomethingElse();

then I wouldn't expect reading hashtable[x] to work, because the
hashcode would have changed by then, in my scheme of things.

The issue is that GetHashCode would return the same value since it
is based on an immutable field, but Equals would not. Then in case
of a hash code collision, the Equals method wouldn't be able to find
the value.

I was suggesting basing GetHashCode and Equals on the same data, so
that they were always in step. ie Equals returning true => GetHashCode
returning the same value. That violates the GetHashCode constraint
that the same object always returns the same hash code, but only when
the values are changed.

I think this is fine, and how I would view it and want to program it also.
The document suggests otherwise, however. Nonetheless I will program my
components that way. The end user will be told not to change the properties
of my component after they choose to put it into a hash table as a key.
However I am less concerned about this, because it is pretty doubtful that
my components will be used as hashtable keys, than about effectively being
able to implement Equals so that they can compare two instances if my
component. With the proviso that GetHashCode can indeed change to reflect
the changes in the component, I think everything is fine and I know how to
program GetHashCode and Equals. But I think you do understand my point that
if GetHashCode is always immutable, and Equals is not, then there is no way
that they can reflect each other.
But essentially that's then an entirely different object in .NET terms
- the "object" of a value type is precisely its values. When you
change the values, it's a different "object", just as:

int i = 5;
i = 4;

isn't changing the value of 5 to 4, it's just changing the value of i
to 4.


*If* they abide by the rule which says that GetHashCode can't return
different values, which I've always said is a rule which I think can
be violated reasonably.

I will quote you on this when the software police arrest me for breaking the
rule said:
If it's a flaw, it's a necessary one. Essentially there are two
"views" of a key, and one of them is cached. The caching is necessary
for performance. If you keep the views in sync, the cache will get out
of date with regard to the original key. If you don't keep the views
in sync, the cache will get out of date with regard to a new but equal
key.

The only thing I ask for is for the flaw to be better documented. If one
takes the documentation verbatim, then their is no real solution.

Thanks for your input on this matter.
 
J

Jon Skeet [C# MVP]

Edward Diener said:
But I think you do understand my point that
if GetHashCode is always immutable, and Equals is not, then there is no way
that they can reflect each other.

No, I entirely understand your point and agree with it.

<snip rest of stuff we already agreed about>
 
E

Edward Diener

Jay said:
Edward,

You are taking that statement by itself, I am combining all three
statements, plus applying an understanding of how Equals &
GetHashCode need to work in relation to Hashtable KEYS.

The problem is that Equals may also be used totally outside of the hashtable
situation as a form of 'is equal to' ( == in C#, C++ ). In fact that is
probably its main functionality.
Value types (__value class) are boxed when used as a key, you cannot
really modify a boxed value without an interface. You can however
replace a boxed value.

But the original object which is used as the key to find a value in a hash
table may change.
Mutable objects may not be good KEYS, they are excellent VALUES for
hashtables. Mutable objects will be good KEYS if the fields that are
used for GetHashCode & Equals are immutable.

This places way too much of a constraint to the Equals method. In my
experience, a generic Equals method, as a stand-in for determining whether
two objects have essentially the same 'value', must take into account all
properties of the object.
Consider the following
mutable class, it is a good key for a HashTable, as the fields used
for equality & hash code are immutable. It doesn't matter that the
value field itself can change...

Public NotInheritable Class KeyPair

Private ReadOnly m_key1, m_key2 As Integer
Private m_value As String

Public Sub New(ByVal key1 As Integer, ByVal key2 As Integer)
m_key1 = key1
m_key2 = key2
End Sub

Public Property Value() As String
Get
Return m_value
End Get
Set(ByVal value As String)
m_value = value
End Set
End Property

Public Overrides Function GetHashCode() As Integer
Return m_key1.GetHashCode() Xor m_key2.GetHashCode()
End Function

Public Overloads Function Equals(ByVal other As KeyPair) As
Boolean Return m_key1 = other.m_key1 AndAlso m_key2 =
other.m_key2 End Function

Public Overloads Overrides Function Equals(ByVal obj As
Object) As Boolean
If TypeOf obj Is KeyPair Then
Return Me.Equals(DirectCast(obj, KeyPair))
Else
Return False
End If
End Function

End Class

By mutable object I mean normal C# or VB.NET class, not a C# struct.
Remember a C# struct will be boxed when used as a key, effectively
making it immutable! Also I hope you realize that my earlier example
of Point is NOT a good key as it always returns 0 for GetHashCode,
forcing all keys to be in the first hash bucket which means you have
degraded to a "simple list".

Yes, the situation with Point is noted.

My conclusion is that I will be able to keep GetHashCode and Equals in sync
if I accept the fact that GetHashCode will be based on the values used by
Equals and that GetHashCode will be mutable. If anyone wants to use my
component as a key in a hashtable, they will simply be told that they can
not change the component after putting it into the hashtable as a key.
Constraining myself to use immutable fields for Equals is simply not
something I want to do, as it basically kills any effective use of Equals,
outside of its potential use in a hashtable, as a means of finding out if
one instance of my component is equal to another one.

The documentation should clarify this issue, but I believe I have
nevertheless presented a flaw in the GetHashCode/Equals suggested
relationship.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top