4

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;
    }
}
  • 1
    You should provide the (relevant) code for the class. You should also provide the test code that's failing as code, rather than just a link. – jmcilhinney Aug 21 '23 at 04:18
  • 1
    Don't show code as an image. We can't run images. Please post a full [mcve] so that we can run your code. – Enigmativity Aug 21 '23 at 04:20

3 Answers3

2

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);
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • The problem is that the one on line 3 actually works. `text.Equals(NonEmptyString)` does return true. Which is why I am very much confused why the one on line 5 doesn't return true. Cause it should be just a proxy method. – Pavel Kalandra Aug 21 '23 at 06:26
  • @PavelKalandra - Line 3 for me didn't work until I implemented the implicit conversion operator. Did you have one defined? – Enigmativity Aug 21 '23 at 06:51
  • @PavelKalandra - Neither are "proxy" methods. `string.Equals` purely compares against a `string` with `SpanHelpers.SequenceEqual(ref Unsafe.As(ref strA.GetRawStringData()), ref Unsafe.As(ref strB.GetRawStringData()), (UIntPtr)(uint)(strA.Length * 2))` and `object.Equals` ultimately uses `==` which only compares references are equal for objects. Try `string x = "a"; bool result = (((object)"ab") == ((object)(x + "b")));` as this returns `False`. – Enigmativity Aug 21 '23 at 07:01
  • 1
    Oh snap, yeah I do have an implicit conversion. So it just calls Equality on strings and not on objects. Thanks. I accept this answer. Although I have to say that from a user perspective, I'd expect the IEquatable to change the behavior of the static Equals method and even though I now understand why it doesn't work, it still feels like a bug to me. If this behavior happened in a third-party nuget, I'd definitely go in and try to fix it. – Pavel Kalandra Aug 21 '23 at 07:09
0

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.

Peter Dongan
  • 1,762
  • 12
  • 17
  • I also thought about that , but when I go into the call of the static Equals, it ends up calling `objA.Equals(objB)` - I extended the question with this now. – Pavel Kalandra Aug 21 '23 at 05:17
-1

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.