Как свойства неизменяемого идентификатора влияют на равенство .NET? - PullRequest
7 голосов
/ 06 июня 2011

( Извиняюсь за долгую настройку. Здесь есть вопрос, обещаю. )

Рассмотрим класс Node, который имеет неизменный уникальный идентификатор, который назначается во время строительства. Этот идентификатор используется для сериализации при сохранении графа объекта, между прочим. Например, когда объект десериализован, он проверяется по основному графу объектов по ID, чтобы найти коллизию и отклонить ее.

Кроме того, Node создается только частной системой, и все публичные доступы к ним осуществляются через интерфейс INode.

Итак, у нас есть что-то вроде этого общего шаблона:

interface INode
{
    NodeID ID { get; }

    // ... other awesome stuff
}

class Node : INode
{
    readonly NodeID _id = NodeID.CreateNew();

    NodeID INode.ID { get { return _id; } }

    // ... implement other awesome stuff

    public override bool Equals(object obj)
    {
        var node = obj as INode;
        return ReferenceEquals(node, null) ? false : _id.Equals(node.ID);
    }

    public override int GetHashCode()
    {
        return _id.GetHashCode();
    }
}

Мои вопросы касаются этих функций сравнения .NET:

  • IEquatable<T>
  • IComparable / IComparable<T>
  • operator == / operator !=

Вот вопросы, наконец. При реализации класса, подобного Node:

  • Какой из перечисленных выше интерфейсов / операторов вы также используете? Почему?
  • Вы расширяете IEquatable<T> с INode или Node? Учитывая, что IEquatable<T>, кажется, используется только (в основном в BCL) посредством проверок типов во время выполнения, есть ли смысл расширять / не расширять INode?
  • Для тех, кого вы реализуете, вы делаете это только для самого класса, или вы дополнительно делаете это для идентификатора?
    • Например, IEquatable<NodeID> также является частью Node?
    • Вы тестируете на obj as NodeID в Equals?

Проработав в C # более десяти лет, я смущен тем, что не имею полного понимания этих интерфейсов и рекомендаций, связанных с ними.

(Обратите внимание, что наши .NET-системы построены на 95% C #, 5% C ++ / CLI, 0% VB (если это имеет значение).)

Ответы [ 4 ]

1 голос
/ 06 июня 2011

Что из IEquatable<T>, IComparable / IComparable<T>, operator == / operator != вы также реализуете? Почему?

Я реализую IEquatable<T> для обстоятельств, которые вы описываете. Поскольку словари будут использовать этот интерфейс, его можно использовать в ряде скрытых мест, особенно если задействован LInQ. Это будет более эффективно, чем переопределенный Equals(object), из-за безопасности типов и (для типов значений) отсутствия упаковки.

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

Обычно я не отменяю оператор ==, если мой тип не имеет какой-либо числовой интерпретации. То есть == /! = Вряд ли когда-либо будут единственными операторами, которые я переопределяю в классе.

Расширяете ли вы IEquatable с INode или Node? Учитывая, что IEquatable, кажется, используется только (в основном в BCL) посредством проверок типов во время выполнения, есть ли смысл расширять или не расширять INode?

Я бы добавил его к INode, если бы ожидал когда-нибудь сравнить узлы на равенство. Если нет, то я бы не стал беспокоиться. Впрочем, может быть целесообразно, если вы работаете с коллекциями INode.

Для тех, кого вы реализуете, вы делаете это только для самого класса или также дополнительно для идентификатора?

Если идентификатор является простым типом, например, целым числом, то, очевидно, в этом нет необходимости. Если это сложный тип, то разделение проблем потребует реализации равенства для типа идентификатора. Вот как я всегда думаю об этом: какой дизайн позволит мне вносить самые большие изменения с наименьшими усилиями? Если вы когда-нибудь решите изменить формат сложного объекта ID, то хотите ли вы обойти код, исправляющий все вещи, которые это сломает? Или вы хотите инкапсулировать всю эту логику в сам класс ID. Я бы наверняка выбрал последнее. (Это относится к ToString (), коду сериализации и ко всему прочему. Узел не имеет никакой возможности разбираться во внутренностях своего сложного типа идентификатора.)

Например, IEquatable<NodeID> также является частью узла?

Нет, это не будет частью узла. Node будет реализовывать IEquatable<Node>, а NodeId будет реализовывать IEquatable<NodeId>.

Проверяете ли вы obj как NodeID в Equals?

Я не совсем уверен, что вы спрашиваете, но я всегда реализую переопределение object.Equals(object) для IEquatable<T> классов, как это:

public override bool Equals(object obj)
{
    return Equals(obj as Node);
}

public bool Equals(Node node)
{
    if (node == null)
        return false;
    // ... do the type-specific comparison.
}
1 голос
/ 06 июня 2011

Мое понимание этого также может быть несовершенным, но здесь все идет ...

Я никогда не делаю IEquatable, может быть ошибка с моей стороны, но я считаю, что переопределение Equals (obj) достаточно хорошо, и я могуделайте проверку типа там (или в частных вспомогательных методах) по мере необходимости.Я, честно говоря, не вижу смысла в IEquatable, если только вы не пытаетесь явно быть уравновешенным с другим типом.

IComprable используется для операций сортировки / упорядочения типов и полезен, если есть некоторая бизнес-логика в сортировке, которую нельзя перехватить путем переопределения GetHashCode.Опять же, большую часть времени я буду либо выполнять сортировку по умолчанию с использованием GetHashCode, либо использовать linq OrderBy (), поэтому здесь нет особого смысла.

Я всегда переопределяю == /! = При переопределении Equals.Это делает поведение намного более последовательным.

1 голос
/ 06 июня 2011

Как говорит Эд, если вы сравниваете равенство по NodeID, а NodeID не является типом значения, то класс NodeID также должен реализовывать IEquatable<NodeID> (только из-за вашего синтаксиса _id.Equals(node.ID).

., если вы объявляете interface INode : IEquatable<INode> на уровне интерфейса, вам все еще нужна фактическая реализация на уровне экземпляра: например, для узла: public bool Equals(INode other)

  1. Это работает, пока вы сравниваете только насвойство, которое было объявлено в исходном интерфейсе. Если у вас есть производный класс с новым свойством, и вы также хотите использовать его для сравнения на равенство (скажем, NodeID и NodeDate), то сигнатура интерфейса не предоставит вашему методу Equals доступ, который ему необходимк этому свойству.
  2. С точки зрения вызывающей стороны (потребителя API, который хочет сравнить один узел с другим) вызывающая сторона должна привести все экземпляры к интерфейсу.пока все производные классы не представили то, что нужно вызывающему, но не объявлено в интерфейсе.
  3. Есливы обнаружите, что используете дополнительные свойства производного класса или что ваши потребители вряд ли приведут к INode, а затем просто расширили IEquatable<Node> от производного класса (узла).

Джо Альбхариимеет очень понятное и подробное объяснение обоих интерфейсов в своей C # в двух словах

Наиболее важной причиной использования IEquatable<T> является объявление в качестве контракта для потребителей вашего класса, что выгарантировать реализацию равенства по умолчанию.С точки зрения фреймворка вы объявляете IEquatable<T>, чтобы классы фреймворков, использующие алгоритм хеширования, могли сравниваться на основе вашей реализации Equals AND GetHashCode.Например, коллекция HashSet, а также расширения LINQ, такие как «Distinct».Тем не менее:

  1. Хеш-алгоритмы не будут работать должным образом, если вы не переопределите оба Equals() и GetHashCode();

  2. Вы неt нужно , чтобы объявить IEquatable, если ваше переопределение Equals(object) и GetHashCode();

  3. Или, если вы это объявите, вы можете опустить Equals(object) если вы реализуете Equals(T);

1 голос
/ 06 июня 2011

Ваш метод Node.Equals на самом деле является просто оболочкой для NodeID.Equals, так что именно здесь вам необходимо реализовать реальную логику сравнения (которая, как я полагаю, будет выглядеть как myIntId == someOtherInt). В противном случае вы просто будете использовать поведение по умолчанию .Equals, которое наследуете от object.

Если я прав, предполагая, что ваш идентификатор на самом низком уровне - просто int, вы можете реализовать IComparable<NodeID> и IEquatable<NodeID>, которые будут просто обертками вокруг вашего поля int id, поскольку Int32 уже реализует IComparable<int> и IEquatable<int>.

...