Переопределение IEquatable <T>, когда T является интерфейсом, и хеш-коды различаются между производными типами. - PullRequest
1 голос
/ 30 сентября 2019

У меня есть A и B классы, оба реализующие интерфейс I.

public interface I
{
    int SomeInt { get; }
    bool SomeBool { get; }
    float SomeFloat { get; }
}


public class A : I
{
    public int SomeInt { get; }
    public bool SomeBool { get; }
    public float SomeFloat { get; }

    private readonly string _someARelatedStuff;
    // Rest of class...
}

public class B : I
{
    public int SomeInt { get; }
    public bool SomeBool { get; }
    public float SomeFloat { get; }

    private string readonly _someBRelatedStuff;
    private double readonly _someOtherBRelatedStuff;
    // Rest of class...
}

Иногда я хочу проверить равенство между A и B (обычно при сравнении списков A и списки B), основанные на равенстве их I свойств (SomeInt, SomeBool, SomeFloat), поэтому я реализовал IEquatable<I> для обоих и сравнил их на основе их общего I значения свойств.

Проблема в том, что у меня уже есть реализация для GetHashCode() для A и B, которая производит разные хэши, потому что я учитываю дополнительные элементы.

B не зависитна A, поэтому я использую интерфейс I для их сравнения, и у него есть список свойств с геттерами.

Я прочитал в ответе StackOverflow , что:

Если вы реализуете класс, вы всегда должны убедиться, что два равных объекта имеют одинаковый хеш-код.

Значит ли это, что каждый раз, когда класс A хочет быть реализованным интерфейсом I, и я хочу иметь возможность сравнивать экземпляры, которые реализуют I, я должен убедиться, что хеш-код рассчитывается одинаково для всех экземпляров I и использовать только свойства I?

Мне кажется, что я не собираюсь реализовывать IEquatable<T>, когда T является интерфейсом, но мои альтернативы:

  1. Использование регулярного наследования с базовым классом - я скорее избегаю наследования wЭто возможно, и это решение не сработает, если B нужно извлечь из некоторого каркасного класса C из-за единственного наследования
  2. Реализовать проверки на равенство между A и B с помощью метода либо на A или B - создаст дублирование кода
  3. Имеет метод проверки равенства между I экземплярами, определенными в I - звучит как лучший вариант

Есть ли какие-либо опциичто мне не хватает?

1 Ответ

1 голос
/ 30 сентября 2019

Подумайте о создании класса IEqualityComparer<> для сравнения общих значений.

Я переименовал интерфейс в ICommon для удобства чтения

public interface ICommon
{
    int SomeInt { get; }
    bool SomeBool { get; }
    float SomeFloat { get; }
}

public class CommonComparer : IEqualityComparer<ICommon>
{
    public bool Equals(ICommon x, ICommon y)
    {
        return x.SomeInt.Equals(y.SomeInt)
            && x.SomeBool.Equals(y.SomeBool)
            && x.SomeFloat.Equals(y.SomeFloat);
    }

    public int GetHashCode(ICommon obj)
    {
        unchecked
        {
            int hc = -1817952719;
            hc = (-1521134295)*hc + obj.SomeInt.GetHashCode();
            hc = (-1521134295)*hc + obj.SomeBool.GetHashCode();
            hc = (-1521134295)*hc + obj.SomeFloat.GetHashCode();
            return hc;
        }
    }
}

, и программа может различать одинаковые элементы в двух списках.

class Program
{
    static void Main(string[] args)
    {
        var listA = new List<A>
        {
            new A(1001, true, 1.001f, "A1"),
            new A(1002, true, 1.002f, "A2"),
            new A(1003, false, 1.003f, "A1"),
            new A(1004, false, 1.004f, "A4")
        };

        var listB = new List<B>
        {
            new B(1001, true, 1.001f, "B1", 2.5),
            new B(1002, false, 1.002f, "B2", 2.8),
            new B(1003, true, 1.003f, "B3", 2.9),
            new B(1004, false, 1.004f, "B4", 2.9)
        };

        var common = Enumerable.Intersect(listA, listB, new CommonComparer()).OfType<ICommon>();


        Console.WriteLine($"{"SomeInt",-8} {"Bool",-6} {"SomeFloat",-10}");
        foreach (var item in common)
        {
            Console.WriteLine($"{item.SomeInt,-8} {item.SomeBool,-6} {item.SomeFloat,-10}");
        }
        //SomeInt  Bool   SomeFloat
        //1001     True   1.001
        //1004     False  1.004

    }
}

и остальные определения кода

public class A : ICommon, IEquatable<A>
{
    static readonly CommonComparer comparer = new CommonComparer();

    public int SomeInt { get; }
    public bool SomeBool { get; }
    public float SomeFloat { get; }

    private readonly string _someARelatedStuff;
    // Rest of class...
    public A(ICommon other, string someARelatedStuff)
        : this(other.SomeInt, other.SomeBool, other.SomeFloat, someARelatedStuff)
    { }
    public A(int someInt, bool someBool, float someFloat, string someARelatedStuff)
    {
        this.SomeInt = someInt;
        this.SomeBool = someBool;
        this.SomeFloat = someFloat;
        this._someARelatedStuff = someARelatedStuff;
    }

    public override string ToString() => _someARelatedStuff;

    #region IEquatable Members
    public override bool Equals(object obj)
    {
        if (obj is A other)
        {
            return Equals(other);
        }
        return false;
    }


    public virtual bool Equals(A other)
    {
        return comparer.Equals(this, other)
            && _someARelatedStuff.Equals(other._someARelatedStuff);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hc = comparer.GetHashCode(this);
            hc = (-1521134295)*hc + _someARelatedStuff.GetHashCode();
            return hc;
        }
    }

    #endregion

}

public class B : ICommon, IEquatable<B>
{
    static readonly CommonComparer comparer = new CommonComparer();

    public int SomeInt { get; }
    public bool SomeBool { get; }
    public float SomeFloat { get; }

    readonly string _someBRelatedStuff;
    readonly double _someOtherBRelatedStuff;
    // Rest of class...

    public B(ICommon other, string someBRelatedStuff, double someOtherBRelatedStuff)
        : this(other.SomeInt, other.SomeBool, other.SomeFloat, someBRelatedStuff, someOtherBRelatedStuff)
    { }
    public B(int someInt, bool someBool, float someFloat, string someBRelatedStuff, double someOtherBRelatedStuff)
    {
        this.SomeInt = someInt;
        this.SomeBool = someBool;
        this.SomeFloat = someFloat;
        this._someBRelatedStuff = someBRelatedStuff;
        this._someOtherBRelatedStuff = someOtherBRelatedStuff;
    }

    public override string ToString() => $"{_someBRelatedStuff}, {_someOtherBRelatedStuff.ToString("g4")}";

    #region IEquatable Members

    public override bool Equals(object obj)
    {
        if (obj is B other)
        {
            return Equals(other);
        }
        return false;
    }

    public virtual bool Equals(B other)
    {
        return comparer.Equals(this, other)
            && _someBRelatedStuff.Equals(other._someBRelatedStuff)
            && _someOtherBRelatedStuff.Equals(other._someOtherBRelatedStuff);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hc = comparer.GetHashCode(this);
            hc = (-1521134295)*hc + _someBRelatedStuff.GetHashCode();
            hc = (-1521134295)*hc + _someOtherBRelatedStuff.GetHashCode();
            return hc;
        }
    }

    #endregion
}
...