IEquatable<string> doesn't work with static Equals method

138 Views Asked by At

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

There are 3 best solutions below

4
Enigmativity On BEST ANSWER

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);
1
Peter Dongan On

I think it is because Equals(object1, object2) is the same as calling object1.ReferenceEquals(object2) and ReferenceEquals() cannot be overridden. I.e.: It is trying to use the default string ReferenceEquals method to compare your class object with a string and not the comparison methods you defined to access its Value property.

1
Ehsan Nozari On

The IEquatable interface in C# provides a way to compare two objects of the same type for equality. It is typically used to override the Equals method for a custom class.

However, IEquatable does not work with a static Equals method. The reason for this is that IEquatable requires implementing the Equals method in an instance-level manner, meaning it compares the current object against another object of the same type.

A static Equals method, on the other hand, is not tied to a specific instance of a class and can compare objects of different types. It is usually used in a more generic sense to check for equality between two objects.

To make IEquatable work properly, you need to implement the Equals method at the instance level within the class that implements the interface. For example:

public class MyClass : IEquatable<MyClass>
{
    public string Property { get; set; }

    public bool Equals(MyClass other)
    {
        if (other == null)
            return false;

        return Property == other.Property;
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is MyClass))
            return false;

        return Equals((MyClass)obj);
    }

    public override int GetHashCode()
    {
        return Property.GetHashCode();
    }
}

You can then use the IEquatable interface to compare instances of MyClass using the Equals method:

MyClass obj1 = new MyClass { Property = "Test" };
MyClass obj2 = new MyClass { Property = "Test" };

bool areEqual = obj1.Equals(obj2);  // true

Note that in the above example, the static Equals method is not involved in the equality comparison.