I implemented a class called NonEmptyString which doesn't allow creation when it's not empty. I made this class implement IEquatable<NonEmptyString>
and IEquatable<string>
. I have overrides for Equals(object obj)
, Equals(NonEmptyString other)
, Equals(string other)
, and GetHashCode()
. I then wrote some tests and saw that pretty much everything works. Except for 1 case when the static Equals method is called with the string parameter being the first parameter. See this line here.
string text = "ASDF123";
NonEmptyString nonEmptyString = NonEmptyString.CreateUnsafe("ASDF123");
Assert.True(text == nonEmptyString);
Assert.True(nonEmptyString == text);
Assert.True(text.Equals(nonEmptyString)); // This one returns true as expected.
Assert.True(nonEmptyString.Equals(text));
Assert.True(Equals(text, nonEmptyString)); //This is the only one that doesn't work.
Assert.True(Equals(nonEmptyString, text));
I'm wondering why it should be the case - when I look at the implementation of Equals method on object, it does call the virtual Equals(object obj)
method. So if that method returns false, then I'd expect that the same should happen for just text.Equals(nonEmptyString)
- but that one works. This is the implementation of the static Equals I see when I go into the call.
public static bool Equals(object? objA, object? objB)
{
if (objA == objB)
{
return true;
}
if (objA == null || objB == null)
{
return false;
}
return objA.Equals(objB);
}
I even tried overriding the ==
operators for comparing a string with a NonEmptyString in this manner (I didn't really expect that to help, but it was worth a try)
public static bool operator ==(string obj1, NonEmptyString obj2)
public static bool operator !=(string obj1, NonEmptyString obj2)
public static bool operator ==(NonEmptyString obj1, string obj2)
public static bool operator !=(NonEmptyString obj1, string obj2)
Is there anything I can do to make this work? Is it expected that this should not work? Is it a bug in .NET?
Here's the core implementation (I removed the non-important parts from it.)
public sealed class NonEmptyString : IEquatable<string>, IEquatable<NonEmptyString>
{
private NonEmptyString(string value)
{
Value = value;
}
public string Value { get; }
public static NonEmptyString CreateUnsafe(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("You cannot create NonEmptyString from whitespace, empty string or null.");
}
return new NonEmptyString(value);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override bool Equals(object obj)
{
return ReferenceEquals(this, obj) ||
obj is NonEmptyString otherNonEmpty && Equals(otherNonEmpty) ||
obj is string otherString && Equals(otherString);
}
public bool Equals(string other)
{
return Value.Equals(other);
}
public bool Equals(NonEmptyString other)
{
return Value.Equals(other?.Value);
}
public override string ToString()
{
return Value;
}
}
The issue you appear to have is when you are calling Equals
overload from either from the string
or object
classes.
Look at this code:
string text = "ASDF123";
NonEmptyString nonEmptyString = NonEmptyString.CreateUnsafe(text);
/* 3 */ Assert.True(text.Equals(nonEmptyString));
/* 5 */ Assert.True(Equals(text, nonEmptyString));
On line 3 the call to Equals
is on the string
instance which has no idea about your NonEmptyString
class - so it will always return false
regardless if the underlying value of NonEmptyString
is equal.
On line 5 the call to Equals
is on the object
instance which, again, has no idea about your NonEmptyString
class - so it will always return false
regardless if the underlying value of NonEmptyString
is equal.
Here is the compiler optimized version of your code:
NonEmptyString nonEmptyString = NonEmptyString.CreateUnsafe("ASDF123");
Assert.True("ASDF123".Equals(nonEmptyString));
Assert.True(object.Equals("ASDF123", nonEmptyString));
You are not in control of those Equals
overloads.
To make your life as simple as possible, you should implement ==
and implicit and explicit casting operators like this:
public static bool operator ==(string obj1, NonEmptyString obj2) => obj2.Equals(obj1);
public static bool operator !=(string obj1, NonEmptyString obj2) => !obj2.Equals(obj1);
public static bool operator ==(NonEmptyString obj1, string obj2) => obj1.Equals(obj2);
public static bool operator !=(NonEmptyString obj1, string obj2) => !obj1.Equals(obj2);
public static implicit operator string(NonEmptyString nes) => nes.Value;
public static explicit operator NonEmptyString(string text) => NonEmptyString.CreateUnsafe(text);