Вопрос по .net, c# – Что такое «Лучшая практика» для сравнения двух экземпляров ссылочного типа?

44

Я сталкивался с этим недавно, до сих пор я счастливо переопределял оператор равенства (==) и / илиEquals метод, чтобы увидеть, если два ссылочных типа на самом деле содержали одинаковыеdata (то есть два разных экземпляра, которые выглядят одинаково).

Я использую это еще больше, так как я больше разбираюсь в автоматизированном тестировании (сравнивая справочные / ожидаемые данные с возвращенными).

Просматривая некоторые изРуководство по стандартам кодирования в MSDN Я наткнулся настатья что советует против этого. Теперь я понимаюwhy статья говорит об этом (потому что они не совпадаютinstance) но это не отвечает на вопрос:

What is the best way to compare two reference types? Should we implement IComparable? (I have also seen mention that this should be reserved for value types only). Is there some interface I don't know about? Should we just roll our own?!

Большое спасибо ^ _ ^

Update

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

If you are implementing reference types, you should consider overriding the Equals method on a reference type if your type looks like a base type such as a Point, String, BigNumber, and so on. Most reference types should not overload the equality operator, even if they override Equals. However, if you are implementing a reference type that is intended to have value semantics, such as a complex number type, you should override the equality operator.

«Большинство ссылочных типов не должны перегружать оператор равенства, даже если они переопределяют Equals». Вау, я нахожу это немного ... хм ... странным. Таким образом, a.Equals (b) может быть истинным, а a == b может быть ложным. Если я хочу знать, равны ли ссылки (что редко, честно), я бы в любом случае использовал .ReferenceEquals (a, b). Мне нравится, что a == b возвращает то же самое, что и a.Equals (b). Разве это не «лучшая практика»? Flipster
@FlipScript: основная проблема с переопределением== оператор в том, что это действительно два оператора; когда он используется с типами, для которых существуют переопределения, он использует переопределение; в противном случае, если операнды являются ссылочными типами, это проверка на равенство ссылок. поскольку== это связано статически, а не виртуально, даже при использовании с дженериками, это поведение может привести к неожиданным результатам. В vb.net отдельные операторы используются для переопределяемого равенства и ссылочного равенства, избегая такой неоднозначности. supercat

Ваш Ответ

8   ответов
1

кажется, изменила свою мелодию, или, по крайней мере, есть противоречивая информация о том, чтобы не перегружать оператор равенства. Согласно этомуСтатья  озаглавленный Как: определить значение равенства для типа:

& quot; Операторы == и! = могут использоваться с классами, даже если класс не перегружает их. Однако стандартным поведением является проверка на равенство ссылок. В классе, если вы перегружаете метод Equals, вы должны перегружать операторы == и! =, Но это не обязательно. & Quot;

По словам Эрика Липперта в егоответ на вопрос, который я задавал оМинимальный код для равенства в C # - он говорит:

& quot; Опасность, с которой вы здесь сталкиваетесь, заключается в том, что вы получите оператор ==, определенный для вас, который по умолчанию ссылается на равенство. Вы можете легко оказаться в ситуации, когда перегруженный метод Equals выполняет равенство значений, а == ссылается на равенство, и тогда вы случайно используете равенство ссылок на вещах, не равных ссылкам, которые равны по значению. Это склонная к ошибкам практика, которую трудно обнаружить с помощью анализа человеческого кода.

Пару лет назад я работал над алгоритмом статического анализа для статистического обнаружения этой ситуации, и мы обнаружили, что частота дефектов составляет около двух экземпляров на миллион строк кода во всех изученных нами кодовых базах. При рассмотрении только кодовых баз, которые были где-то переопределены равными, уровень дефектов был явно значительно выше!

Кроме того, рассмотрите затраты против рисков. Если у вас уже есть реализации IComparable, то написание всех операторов - тривиальные однострочные, которые не будут содержать ошибок и никогда не будут изменены. Это самый дешевый код, который вы когда-либо собираетесь писать. Если бы у меня был выбор между фиксированной стоимостью написания и тестирования дюжины крошечных методов по сравнению с неограниченной стоимостью поиска и исправления трудно видимой ошибки, в которой вместо равенства значений используется равенство ссылок, я знаю, какой из них я бы выбрал. & Quot ;

.NET Framework никогда не будет использовать == или! = С любым типом, который вы пишете. Но опасность состоит в том, что случится, если кто-то другой сделает. Таким образом, если класс предназначен для третьей стороны, я бы всегда предоставлял операторы == и! =. Если бы класс предназначался только для внутреннего использования группой, я все равно, вероятно, реализовал бы операторы == и! =.

Я бы реализовал операторы & lt ;, & lt; =, & gt; и & gt; = только в случае реализации IComparable. IComparable должен быть реализован только в том случае, если тип должен поддерживать упорядочение - например, при сортировке или использовании в упорядоченном универсальном контейнере, таком как SortedSet.

Если бы у группы или компании была политика, запрещающая использование операторов == и! =, Я бы, конечно, следовал этой политике. Если бы такая политика существовала, то было бы разумно применять ее с помощью инструмента анализа кода Q / A, который помечает любое вхождение операторов == и! = При использовании со ссылочным типом.

0

что получить что-то столь же простое, как проверка правильности объектов на предмет равенства, немного сложно с дизайном .NET.

For Struct

1) РеализацияIEquatable<T>, Это заметно повышает производительность.

2) Поскольку у вас есть свой собственныйEquals теперь переопределитьGetHashCodeи соответствовать различным переопределениям проверки на равенствоobject.Equals также.

3) Перегрузка== а также!= операторы не должны быть религиозными, так как компилятор предупредит, если вы непреднамеренно приравниваете структуру к другой с== или же!=, но это хорошо, чтобы быть в соответствии сEquals методы.

public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

For Class

От MS:

Most reference types should not overload the equality operator, even if they override Equals.

Мне== ощущается как ценностное равенство, больше похоже на синтаксический сахар дляEquals метод. Пишуa == b гораздо более интуитивно, чем писатьa.Equals(b), В редких случаях нам нужно проверять равенство ссылок. На абстрактных уровнях, имеющих дело с логическими представлениями физических объектов, это не то, что нам нужно проверять. Я думаю, что иметь другую семантику для== а такжеEquals на самом деле может быть запутанным. , что это должно было быть== для ценностного равенства иEquals для справки (или как лучшеIsSameAs) равенство на первом месте.I would love to not take MS guideline seriously here, not just because it isn't natural to me, but also because overloading == doesn't do any major harm. Это в отличие от того, чтобы не переопределять неуниверсальныйEquals или жеGetHashCode который может откусить назад, потому что фреймворк не использует== где угодно, но только если мы сами это используем. Единственное реальное преимущество, которое я получаю отnot overloading == and != будет согласованность с дизайном всей структуры, над которой я не имею никакого контроля. И это действительно большая вещь,so sadly I will stick to it.

With reference semantics (mutable objects)

1) ПереопределитьEquals а такжеGetHashCode.

2) РеализацияIEquatable<T> это не обязательно, но будет хорошо, если у вас есть.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

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

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

With value semantics (immutable objects)

Это сложная часть. Может легко запутаться, если не позаботиться ..

1) ПереопределитьEquals а такжеGetHashCode.

2) перегрузка== а также!= соответствоватьEquals. Make sure it works for nulls.

2) РеализацияIEquatable<T> это не обязательно, но будет хорошо, если у вас есть.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

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

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Take special care to see how it should fare if your class can be inherited, in such cases you will have to determine if a base class object can be equal to a derived class object. Ideally, if no objects of derived class is used for equality checking, then a base class instance can be equal to a derived class instance and in such cases, there is no need to check Type equality in generic Equals of base class.

В общем, старайтесь не дублировать код. Я мог бы сделать общий абстрактный базовый класс (IEqualizable<T> или около того) в качестве шаблона, чтобы разрешить повторное использование проще, но, к сожалению, в C #, что мешает мне получить дополнительные классы.

major проблема с переопределением== Оператор для ссылочных типов (из-за того, что IMHO является недостатком в дизайне C #), состоит в том, что в C # фактически есть два разных оператора, и решение о том, какой оператор использовать, принимается статически во время компиляции. С типами значений возможно перегрузка== так что он проверяет равенство значенийin all cases the compiler will accept [4==4.0m а также4==4.0 скомпилируйте и выведите true, но4.0m==4.0 не будет компилироваться]. Это невозможно с ссылочными типами; даноvar s1="1"; var s2=1.ToString(); Object o1 = s1;, s1 == s2 и o1 == s1, но o1! = s2.
21

что вы кодируете в C #, в котором есть метод под названием Equals, который должен реализовать ваш класс, если вы хотите сравнить два объекта, используя какую-то другую метрику, чем & quot; эти два указателя (поскольку дескрипторы объектов - это просто указатели) на тот же адрес памяти?

Я взял пример кода изВот:

class TwoDPoint : System.Object
{
    public readonly int x, y;

    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }

        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

У Java очень похожие механизмы.equals() метод является частьюObject класс, и ваш класс перегружает его, если вы хотите этот тип функциональности.

Причина перегрузки "==" может быть плохой идеей для объектов является то, что, как правило, вы все еще хотите иметь возможность использовать «тот же самый указатель»; сравнения. На них обычно полагаются, например, для вставки элемента в список, где дубликаты не допускаются, и некоторые из ваших компонентов инфраструктуры могут не работать, если этот оператор перегружен нестандартным способом.

Это на самом деле одна из слабостей C #. Пока разработчик следует руководящим принципам, это не проблема, потому что семантика== не будут изменены для равных ссылок. Тем не менее, я используюobject.ReferenceEquals в критических ситуациях в C # (VB имеетIs вместо).
Вы не должны писать логику равенства в двух местах. Не уверен, как MS сделал это неправильно ..
Хороший ответ, спасибо. Я рад, что вы добавили немного о том, почемуnot перегрузить оператор равенства. Rob Cooper
0

часто вы хотите, чтобы производные ссылки использовали производные Равные, даже если сравнивать через базовую ссылку. Пожалуйста, смотрите вопрос / обсуждение / ответы здесь -Равенство и полиморфизм

1

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

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
}

public bool Equals(SecurableResourcePermission obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = (int)ResourceUid;
        result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
        result = (result * 397) ^ AllowDeny.GetHashCode();
        return result;
    }
}

Если вы хотите переопределить== и по-прежнему делать проверки, вы все еще можете использоватьObject.ReferenceEquals.

Как вы заставляете ReSharper делать это автоматически?
26

without code duplication трудно. В частности, для ссылочных типов с семантикой значения (т.е.неизменные типы, которые рассматривают эквивалентность как равенство), вы должны реализоватьSystem.IEquatable<T> interface, и вы должны реализовать все различные операции (Equals, GetHashCode а также==, !=).

Например, вот класс, реализующий равенство значений:

class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }

    public Point(int x = 0, int y = 0) { X = x; Y = y; }

    public bool Equals(Point other) {
        if (other is null) return false;
        <b>return X.Equals(other.X) && Y.Equals(other.Y);</b>
    }

    public override bool Equals(object obj) => Equals(obj as Point);

    public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

    public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

    public override int GetHashCode() => <b>X.GetHashCode() ^ Y.GetHashCode();</b>
}

Единственными подвижными частями в вышеприведенном коде являются полужирные части: вторая строка вEquals(Point other) иGetHashCode() метод. Другой код должен остаться без изменений.

Для ссылочных классов, которые не представляют неизменные значения, не реализуйте операторы== а также!=, Вместо этого используйте значение по умолчанию, которое заключается в сравнении идентичности объекта.

Кодintentionally приравнивает даже объекты производного типа класса. Часто это может быть нежелательно, потому что равенство между базовым классом и производными классами не является четко определенным. К сожалению, .NET и правила кодирования здесь не очень понятны. Код, который создает Resharper, размещенв другом ответе, подвержен нежелательному поведению в таких случаях, потому чтоEquals(object x) а такжеEquals(SecurableResourcePermission x) will относиться к этому делу по-разному.

Чтобы изменить это поведение, в строго типизированную вставку необходимо вставить дополнительную проверку типа.Equals метод выше:

public bool Equals(Point other) {
    if (other is null) return false;
    if (other.GetType() != GetType()) return false;
    <b>return X.Equals(other.X) && Y.Equals(other.Y);</b>
}
Спасибо, Конрад, мы должны исправить это в ReSharper
Почему вы думаете, что Equals и == должны быть последовательными? Это идет вразрез с тем, что указано в документации MSDN, и создает аналогичное разъединение, где == больше не означает равенство ссылок. Это создает аналогичную проблему юзабилити, поскольку такое поведение равномерно обеспечивается .NET.
Для классов, почему вы переопределяете операторы равенства и неравенства для сравнения ссылок, когда эта функциональность предоставляется по умолчанию базовым классом System.Object?
Это считается лучшей практикой, чтобы сделатьEquals а также== выполнять эквивалентные действия, всегда. Это отражено в моем фрагменте кода. Очевидно, используйте его, только если такая семантика имеет смысл. Но всегда делаюEquals а также== выполнять в согласованном порядке. Это абсолютный ужас юзабилити, если они этого не делают.
FWIW, я, конечно, могу видеть, откуда ты, особенно сам из мира C ++. Тем не менее, поскольку документация / рекомендации MSDN явно не соответствуют тому, что вы делаете, я ищу убедительный аргумент в пользу вашей позиции. Возможно, это заслуживает отдельного вопроса.
3

я ссылочных типов), а не переопределять Equals. Вы должны переопределить Equals внутри вашего объекта (ссылка или значение), если проверки на равенство будут означать нечто большее, чем проверки на ссылки. Если вы хотите интерфейс, вы также можете реализоватьIEquatable (используется общими коллекциями). Однако если вы реализуете IEquatable, вам также следует переопределить equals, как указано в разделе замечаний IEquatable:

If you implement IEquatable<T>, you should also override the base class implementations of Object.Equals(Object) and GetHashCode so that their behavior is consistent with that of the IEquatable<T>.Equals method. If you do override Object.Equals(Object), your overridden implementation is also called in calls to the static Equals(System.Object, System.Object) method on your class. This ensures that all invocations of the Equals method return consistent results.

Относительно того, следует ли вам использовать Equals и / или оператор равенства:

ОтРеализация метода равных

Most reference types should not overload the equality operator, even if they override Equals.

ОтРекомендации по реализации равенства и оператора равенства (==)

Override the Equals method whenever you implement the equality operator (==), and make them do the same thing.

Это говорит только о том, что вам нужно переопределять Equals всякий раз, когда вы реализуете оператор равенства. Оно делаетnot скажем, что вам нужно переопределить оператор равенства при переопределении Equals.

2

которые приведут к конкретным сравнениям, хорошей реализацией будет реализация IComparable и определение сравнения в методах сравнения.

Например, у нас есть «Автомобиль» объектами, где единственной разницей может быть регистрационный номер, и мы используем его для сравнения, чтобы убедиться, что ожидаемое значение, возвращаемое при тестировании, является тем, которое мы хотим.

Спасибо за это, Пол. Отмечено на интерфейсе IComparable, хотя я думаю, что в этом случае это, вероятно, будет излишним, так как я хочу просто проверить на равенство. Rob Cooper

Похожие вопросы