Instances of my class don't want to act properly as dictionary keys interchangeably with strings
-
27-06-2021 - |
Question
I created a sort of string wrapper class and want to use its instances as dictionary keys interchangeably with usual strings. I overrode GetHashCode
and Equals
and got results that seem strange. I've isolated the issue. Please look at my code and explain me why does second lookup returns null.
void Main()
{
var foo = new StringWrapper("foo");
var h = new Hashtable {{ foo, "bar" }};
Console.WriteLine(h["foo"]);
Console.WriteLine(h[foo]); // null ??
}
public class StringWrapper
{
readonly string wrapped;
public StringWrapper(string s) {
wrapped = s;
}
public override bool Equals(object obj) {
return wrapped.Equals(obj);
}
public override int GetHashCode() {
return wrapped.GetHashCode();
}
public override string ToString() {
return wrapped;
}
}
Solution
Your equality implementation isn't symmetric, nor reflexive:
StringWrapper wrapper = new StringWrapper("foo");
Console.WriteLine(wrapper.Equals(wrapper)); // False
Console.WriteLine(wrapper.Equals("foo")); // True
Console.WriteLine("foo".Equals(wrapper)); // False
So you're violating the rules specified in the docs for Object.Equals
. Hashtable
(and other classes) expect you not to violate those rules, and won't work properly when you violate them.
It sounds like you need a custom collection, rather than a wrapper which tries to pretend that the wrapper is equal to the original value.
(As an aside, why are you still using non-generic Hashtable
rather than Dictionary<,>
?)
OTHER TIPS
Keep in mind that in Equals(object obj)
, obj
won't be a string
, it will be a StringWrapper
- and a string
will never say that it is equal to something that is not a string
. You need to extract the wrapped string from obj
. Keep in mind that you should check for null
and take into account that if someone uses StringWrapper
for something else than putting it into a hash table, obj
could also be any other type.
The problem is your Equals method:
public override bool Equals(object obj) {
// wrapped is a String, but obj is (usually) a StringWrapper
return wrapped.Equals(obj);
}
You need something like this:
public override bool Equals(object obj) {
var other = obj as StringWrapper;
return other != null && wrapped.Equals(other.wrapped);
}
EDIT: As per Jon's answer, you also might want to be able to compare to Strings:
public override bool Equals(object obj) {
var other = obj as StringWrapper;
return (other != null && wrapped.Equals(other.wrapped))
|| Object.Equals(wrapped, (obj as String));
}