Зачем мы реализовываем IEquatable<T>, если Equals() есть в Object?

206
16 июля 2018, 22:10

Разбирая тему обобщений (по Шиелду 4.0), потребовалось написать обобщённый метод, который вернет логическое значение true, если в массиве содержится некоторое значение. Далее в книге поясняется, что так как T - обобщённый тип, то для сравнения объектов обобщённого типа необходимо что бы класс этих объектов реализовывал интерфейс IEquatable с параметром T или IEquatable. Написано так же, что в данных интерфейсах определен метод для сравнения Equals(). Отсюда сразу же несколько вопросов:

Во-первых, почему для сравнения объектов обобщённого типа нам вообще необходимо реализовывать какие-либо интерфейсы? Метод Equals() относится к методам определённым в Object => есть в каждом объекте. Я понял что для сравнения объектов в данном примере нужно реализовать описанные выше интерфейсы, но я не понял, ПОЧЕМУ это нужно сделать.

Во-вторых, по Шиелду, как я написал выше, для сравнения объектов в обобщённом методе предлагается реализовать Equtable с параметром T или IEqutable. Один параметризирован, другой нет. Вопрос - какой в каком случае необходимо использовать?

Далее Шиелд в своей книге демонстрирует пример метода, который проверяет, находится ли в массиве некоторое значение. Вот этот метод:

 public static bool IsIn<T>(T what, T[] obs) where T : IEquatable<T>
    {
        foreach(T v in obs)
        {
            if (v.Equals(what))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }

Тут так же есть два вопроса.

Во-первых, начну с того, что данный метод не работает. Студия выдает ошибку компиляции "Не все ветви метода возвращают значение". Я несколько раз перепроверял написанный код, но ошибка остается. Как это исправить?

Во-вторых, я не уверен, правильно ли я понял, что сигнатура данного метода подразумевает, что в данный метод могут быть переданы только объекты тех типов, которые реализовывают интерфейс IEquatable? Другими словами, если я захочу передать в метод массив каких нибудь студентов, и найти в нём Васю Пупкина, то мой класс студента должен выглядить следующим образом?

class Student<T> : IEquatable<T>
{
    public string Name;
    public string Surname;
    public bool Equals(T other)
    {
        //some code for compare
        return false;
    }
}

Прошу последовательно ответить на все вопросы. Спасибо

Answer 1

Equals(object obj) появился в .NET в самом начале, а тогда еще не было дженериков. До введения дженериков в .NET 2.0, переопределение метода Equals(object obj) в большинстве случаев выглядело приблизительно так:

public bool override Equals(object obj)
{
    if (obj == null)
        return false;
    if (obj is MyClass)
    {
        MyClass other = (MyClass)obj;
        // сравниваем this и other
    }
    return false;
}

Получается, в большинстве случаев, объекты разных типов не могут быть равны. Что бы упростить жизнь, придумали интерфейс IEquatable<T>, в котором можно сразу сравнивать 2 объекта, без проверок их типов (что тоже занимает некоторое время).

По-хорошему теперь вы должны реализовать IEquatable<T> для всех классов, которые можно сравнивать. При этом вы должны не забывать про переопределение старого Equals, для совместимости. Можно делать например вот так:

public bool override Equals(object obj)
{
    return Equals(obj as MyClass);
}
public bool Equals(MyClass other)
{
    if (other== null)
        return false;
    // сравниваем объекты
}
Answer 2

Интерфейс IEquatable<> был придуман для того чтобы избегать лишней упаковки значимых типов при сравнении.

До тех пор, пока все радовались объектам и использовали только их - у метода object.Equals(object) особых недостатков не было (лишнее приведение типов особо страшным недостатком не является). Но когда в языке появились обобщенные типы - все стало куда хуже.

Попытка вызвать метод object.Equals(object other) для значимого типа приводит к тому, что:

  1. нулевой параметр (this) упаковывается и размещается в куче,
  2. первый параметр (other) упаковывается и размещается в куче,
  3. оба параметра распаковываются обратно и сравниваются,
  4. упакованные копии параметров забываются (и в следующий раз упаковка будет происходить заново).

Вообще говоря, обобщенные типы придумали именно для того чтобы избегать лишних упаковок структур, поэтому необходимость упаковывать их только для того чтобы сравнить - катастрофа. Фактически, обобщенная коллекция System.Collections.Generics.Dictionary<,> из-за постоянных упаковок может работать даже хуже чем более старый вариант System.Collections.Hashtable.

Поэтому и был придуман новый интерфейс, который позволяет сравнить два значения без приведения типов (и, как следствие, без упаковки).

Теперь про ваш метод. Начну с того, что рекомендация из книги не совсем правильная: в стандартной библиотеке принято другое соглашение относительно подобных методов. Вместо того чтобы требовать реализации IEquatable<>, можно воспользоваться классом EqualityComparer<>, который внутри использует либо IEquatable<>, либо старый метод object.Equals если первый недоступен. Кроме того, желательно давать возможность передать в метод любую реализацию IEqualityComparer<> (например, для сравнения строк без учета регистра).

Вот так будет правильнее:

public static bool IsIn<T>(T what, T[] obs, IEqualityComparer<T> comparer = null)
{
    if (comparer == null) comparer = EqualityComparer<T>.Default;
    foreach(T v in obs)
    {
        if (comparer.Equals(what, v))
        {
            return true;
        }
    }
    return false;
}

(Ну и про вторую ошибку в методе я уже писал тут: В условном операторе выполняются обе ветки // поиск по массиву не работает)

С такой реализацией метода его можно использовать как с простыми классами (объекты таких классов сравниваются просто по ссылке):

class Student
{
    public string Name;
    public string Surname;
}

так и с более продвинутыми:

sealed class Student : IEquatable<Student>
{
    public readonly string Name;
    public readonly string Surname;
    public Student(string name, string surname)
    {
        Name = name;
        Surname = surname;
    }
    public override bool Equals(object other) => Equals(other as Student);
    public bool Equals(Student other) 
    {
        if (this == other) return true;
        if (other == null) return false;
        if (Name != other.Name) return false;
        if (Surname!= other.Surname) return false;
        return true;
    }
    public override int GetHashCode()
    {
        unchecked 
        {
            // https://stackoverflow.com/a/263416/4340086
            int hash = 2166136261;
            hash = (16777619 * hash) ^ (Name?.GetHashCode() ?? 0);
            hash = (16777619 * hash) ^ (Surname?.GetHashCode() ?? 0);
            return hash;
        }
    }
}
Answer 3

Насчет "Не все ветви метода возвращают значение": если obs пустой, то код внутри foreach никогда не выполнится, следовательно метод не встретит ни одного оператора return. поэтому ошибка.

Насколько я понимаю, код метода должен быть таким:

public static bool IsIn<T>(T what, T[] obs) where T : IEquatable<T>
{
    foreach(T v in obs)
    {
        if (v.Equals(what))
        {
            return true;
        }
    }
    return false;
}

Да, сигнатура данного метода подразумевает, что в данный метод могут быть переданы только объекты тех типов, которые реализовывают интерфейс IEquatable.

READ ALSO
Не выполняется запрос INSERT в c#

Не выполняется запрос INSERT в c#

База данных AccessПервичный ключ id

205
Вывод в консоль списка процессов

Вывод в консоль списка процессов

Обычное консольное приложениеNET Core

185
Долгое преобразование данных из MySql

Долгое преобразование данных из MySql

Начал изучать C#Проблема вот в чем

197
C# обобщения vs object

C# обобщения vs object

Какая разница между этими двумя класами? Что в первом универсальный тип данных - что во второмБолее того, насколько я знаю, компилятор тип...

292