none
Equality behavior RRS feed

  • Question

  • Overlooking the perils of implementing value equality on a reference type, I understand when implementing the operator overrides why the correct approach is to call the static object.Equals method as it dispatches the virtual override. What I find interesting is when implementing the strongly typed Equals method, if you check for null using the == operator and watch the call stack, it dispatches to the override which puts you back in the strongly type Equals method and correctly returns false. How exactly is that not recursive?

    using System;
    
    internal class Program
    {
        private static void Main()
        {
            MyClass a = new MyClass
            {
                Description = "8162372c-8cb4-42b7-b8a3-9d8af4e52215"
            };
    
            MyClass b = new MyClass
            {
                Description = "f979523a-7219-4ffd-8e41-f7262e84ffc7"
            };
    
            b = null;
    
            Console.WriteLine(a.Equals(b));
        }
    }
    
    internal class MyClass : IEquatable<MyClass>
    {
        public string Description { get; set; }
    
        public static bool operator !=(MyClass left, MyClass right) => !(left == right);
    
        public static bool operator ==(MyClass left, MyClass right) => Equals(left, right);
    
        public bool Equals(MyClass other)
        {
            if (other == null)
            {
                return false;
            }
    
            //if (ReferenceEquals(null, other))
            //{
            //    return false;
            //}
    
            if (ReferenceEquals(this, other))
            {
                return true;
            }
    
            return string.Equals(this.Description, other.Description);
        }
    
        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }
    
            //if (ReferenceEquals(null, obj))
            //{
            //    return false;
            //}
    
            if (ReferenceEquals(this, obj))
            {
                return true;
            }
    
            if (obj is MyClass myClass)
            {
                return this.Equals(myClass);
            }
    
            return false;
        }
    
        public override int GetHashCode() => this.Description?.GetHashCode() ?? 0;
    }

    Thursday, November 16, 2017 2:47 PM

Answers

  • The best way to understand this is to look at the generated code.

    The call to a.Equals(b) maps to MyClass.Equals ( MyClass ). 

    The other == null maps to MyClass.op_equality (MyClass, MyClass). That overload then calls Object.Equals (object, object). 

    Going into Object.Equals it uses bne.un.s to compare the objects. Why didn't it use your equality operator? Iit was comparing 2 Objects your operator was irrelevant. Only if neither argument is null would it call Object.Equals (object) which is virtual. At that point it would trigger a call back to your overloaded Equals (object) method. But since you were comparing one of the arguments to null it wouldn't get that far in the first call you made. 

    One of the important things to note about operator overloading is that it is a C# thing. Other .NET languages may or may not support operator overloading. So whenever you do something like == or != the C# compiler will try to use the underlying operator (if it were implemented in C#) but if it doesn't find one then it'll use the base Equals (for ==, !=) or error otherwise. That is why you have to always provide non-operator methods for any overloaded operators you handle. Somewhere there is a mapping of the C# "operators" to the method they convert to. (i.e. == is op_Equality). Technically this is the "operator" that C# looks for. Any language that wants to implement operator overloading can implement these methods and C# will see the operators as overloaded.


    Michael Taylor http://www.michaeltaylorp3.net

    • Marked as answer by Ritmo2k Thursday, November 16, 2017 6:03 PM
    Thursday, November 16, 2017 4:14 PM
    Moderator

All replies

  • The best way to understand this is to look at the generated code.

    The call to a.Equals(b) maps to MyClass.Equals ( MyClass ). 

    The other == null maps to MyClass.op_equality (MyClass, MyClass). That overload then calls Object.Equals (object, object). 

    Going into Object.Equals it uses bne.un.s to compare the objects. Why didn't it use your equality operator? Iit was comparing 2 Objects your operator was irrelevant. Only if neither argument is null would it call Object.Equals (object) which is virtual. At that point it would trigger a call back to your overloaded Equals (object) method. But since you were comparing one of the arguments to null it wouldn't get that far in the first call you made. 

    One of the important things to note about operator overloading is that it is a C# thing. Other .NET languages may or may not support operator overloading. So whenever you do something like == or != the C# compiler will try to use the underlying operator (if it were implemented in C#) but if it doesn't find one then it'll use the base Equals (for ==, !=) or error otherwise. That is why you have to always provide non-operator methods for any overloaded operators you handle. Somewhere there is a mapping of the C# "operators" to the method they convert to. (i.e. == is op_Equality). Technically this is the "operator" that C# looks for. Any language that wants to implement operator overloading can implement these methods and C# will see the operators as overloaded.


    Michael Taylor http://www.michaeltaylorp3.net

    • Marked as answer by Ritmo2k Thursday, November 16, 2017 6:03 PM
    Thursday, November 16, 2017 4:14 PM
    Moderator
  • Hi Michael,
    I understand, watching the call stack and stepping through code certainly "appears" misleading as to whats happening.

    Thank you very much for the detailed response.

    Thursday, November 16, 2017 6:03 PM