SequenceEqual not calling Equals in parent type

A parent type:

public class IdObject : IComparable<IdObject>, IEquatable<IdObject>
{
    public int id { get; set; }

    public bool Equals(IdObject other)
    {
        if (other == null) return this == null;
        if (this == null) return false;
        var test = other.id.CompareTo(this.id);

        return other.id.CompareTo(this.id) == 0;
    }

    public int CompareTo(IdObject other)
    {
        return other.id.CompareTo(this.id);
    }
}

A child:

public class NamedObject : IdObject
{
    public string name { get; set; }
}

Comparing lists of IdObjects

var list1 = new List<IdObject>()
{
    new IdObject() { id = 42 },
    new IdObject() { id = 43 }
};
var list2 = new List<IdObject>()
{
    new IdObject() { id = 43 },
    new IdObject() { id = 42 }
};
list1.Sort();
list2.Sort();
var test = list1.SequenceEqual(list2); // True

Comparing lists of Nameds

var list1 = new List<NamedObject>()
{
    new NamedObject() { id = 42 },
    new NamedObject() { id = 43 }
};
var list2 = new List<NamedObject>()
{
    new NamedObject() { id = 43 },
    new NamedObject() { id = 42 }
};
list1.Sort();
list2.Sort();
var test = list1.SequenceEqual(list2); // False

I realized that IdObject::Equals is not called through a NamedObject context.

Am I doing something wrong ?
Isn't supposed to call the inherited Equals ?
How can I use the parent's Equals ?

Jon Skeet
people
quotationmark

Basically, you've got a problem because your type doesn't override object.Equals(object) in a way consistent with your IEquatable<T> implementation and you're dealing with a collection of the subclasses.

SequenceEqual will be using EqualityComparer<NamedObject>.Default. That will check whether NamedObject implements IEquatable<NamedObject> - and will find that it doesn't, so it will fall back to calling object.Equals(object). You can see this here:

using System;
using System.Collections.Generic;

public class Base : IEquatable<Base>
{
    public override bool Equals(object other)
    {
        Console.WriteLine("Equals(object)");
        return false;
    }

    public bool Equals(Base other)
    {
        Console.WriteLine("Equals(Base)");
        return false;
    }

    public override int GetHashCode() => 0;
}

public class Derived : Base
{
}

public class Test
{
    static void Main()
    {
        var comparer = EqualityComparer<Derived>.Default;        
        Console.WriteLine(comparer.Equals(new Derived(), new Derived()));
    }
}

You don't override object.Equals(object), so you've effectively got reference equality.

I would recommend that you override object.Equals(object) and object.GetHashCode() in your base class.

You could then also implement IEquatable<NamedObject> in NamedObject, just delegating to the base implementation or (better) checking the name as well, unless you really don't want that to be taken into account.

people

See more on this question at Stackoverflow