Перегрузка операторов с помощью интерфейсного программирования в C # - PullRequest
70 голосов
/ 08 апреля 2009

Фон

Я использую интерфейсное программирование в текущем проекте и столкнулся с проблемой при перегрузке операторов (в частности, операторов равенства и неравенства).


Предположения

  • Я использую C # 3.0, .NET 3.5 и Visual Studio 2008

ОБНОВЛЕНИЕ - Следующее предположение было неверным!

  • Требование всех сравнений использовать Equals вместо оператора == не является жизнеспособным решением, особенно при передаче ваших типов в библиотеки (такие как Коллекции).

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

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

и этот Эквивалентный интерфейс

Интерфейс IEquatable используется общими объектами коллекции, такими как Dictionary, List и LinkedList, при проверке на равенство в таких методах, как Contains, IndexOf, LastIndexOf и Remove. Он должен быть реализован для любого объекта, который может храниться в общей коллекции.


контрсилы

  • Любое решение не должно требовать приведения объектов от их интерфейсов к их конкретным типам.

Проблема

  • Когда обе стороны оператора == являются интерфейсом, никакой оператор == сигнатура метода перегрузки из базовых конкретных типов не будет совпадать, и, таким образом, будет вызываться метод оператора по умолчанию == объекта.
  • При перегрузке оператора в классе, по крайней мере один из параметров двоичного оператора должен быть содержащего типа, в противном случае генерируется ошибка компилятора (Ошибка BC33021 http://msdn.microsoft.com/en-us/library/watt39ff.aspx)
  • Невозможно указать реализацию на интерфейсе

См. Код и вывод ниже, демонстрирующие проблему.


Вопрос

Как обеспечить правильные перегрузки операторов для ваших классов при использовании программирования на основе интерфейса?


Ссылки

== Оператор (C # Reference)

Для предопределенных типов значений оператор равенства (==) возвращает true, если значения его операндов равны, в противном случае - false. Для ссылочных типов, отличных от string, == возвращает true, если два его операнда ссылаются на один и тот же объект. Для типа строки == сравнивает значения строк.


Смотрите также


Код

using System;

namespace OperatorOverloadsWithInterfaces
{
    public interface IAddress : IEquatable<IAddress>
    {
        string StreetName { get; set; }
        string City { get; set; }
        string State { get; set; }
    }

    public class Address : IAddress
    {
        private string _streetName;
        private string _city;
        private string _state;

        public Address(string city, string state, string streetName)
        {
            City = city;
            State = state;
            StreetName = streetName;
        }

        #region IAddress Members

        public virtual string StreetName
        {
            get { return _streetName; }
            set { _streetName = value; }
        }

        public virtual string City
        {
            get { return _city; }
            set { _city = value; }
        }

        public virtual string State
        {
            get { return _state; }
            set { _state = value; }
        }

        public static bool operator ==(Address lhs, Address rhs)
        {
            Console.WriteLine("Address operator== overload called.");
            // If both sides of the argument are the same instance or null, they are equal
            if (Object.ReferenceEquals(lhs, rhs))
            {
                return true;
            }

            return lhs.Equals(rhs);
        }

        public static bool operator !=(Address lhs, Address rhs)
        {
            return !(lhs == rhs);
        }

        public override bool Equals(object obj)
        {
            // Use 'as' rather than a cast to get a null rather an exception
            // if the object isn't convertible
            Address address = obj as Address;
            return this.Equals(address);
        }

        public override int GetHashCode()
        {
            string composite = StreetName + City + State;
            return composite.GetHashCode();
        }

        #endregion

        #region IEquatable<IAddress> Members

        public virtual bool Equals(IAddress other)
        {
            // Per MSDN documentation, x.Equals(null) should return false
            if ((object)other == null)
            {
                return false;
            }

            return ((this.City == other.City)
                && (this.State == other.State)
                && (this.StreetName == other.StreetName));
        }

        #endregion
    }

    public class Program
    {
        static void Main(string[] args)
        {
            IAddress address1 = new Address("seattle", "washington", "Awesome St");
            IAddress address2 = new Address("seattle", "washington", "Awesome St");

            functionThatComparesAddresses(address1, address2);

            Console.Read();
        }

        public static void functionThatComparesAddresses(IAddress address1, IAddress address2)
        {
            if (address1 == address2)
            {
                Console.WriteLine("Equal with the interfaces.");
            }

            if ((Address)address1 == address2)
            {
                Console.WriteLine("Equal with Left-hand side cast.");
            }

            if (address1 == (Address)address2)
            {
                Console.WriteLine("Equal with Right-hand side cast.");
            }

            if ((Address)address1 == (Address)address2)
            {
                Console.WriteLine("Equal with both sides cast.");
            }
        }
    }
}

выход

Address operator== overload called
Equal with both sides cast.

Ответы [ 2 ]

55 голосов
/ 08 апреля 2009

Краткий ответ: я думаю, что ваше второе предположение может быть ошибочным. Equals() - это правильный способ проверки семантического равенства двух объектов, а не operator ==.

<Ч />

Длинный ответ: Разрешение перегрузки для операторов выполняется во время компиляции, а не во время выполнения .

Если компилятор не может точно знать типы объектов, к которым применяется оператор, он не будет компилироваться. Поскольку компилятор не может быть уверен, что IAddress будет чем-то, что имеет переопределение для ==, он возвращается к реализации operator == по умолчанию System.Object.

Чтобы увидеть это более четко, попробуйте определить operator + для Address и добавить два IAddress экземпляра. Если вы явно не приведете к Address, компиляция не удастся. Зачем? Поскольку компилятор не может сказать, что конкретный IAddress является Address, и не существует реализации по умолчанию operator +, к которой можно обратиться в System.Object.

<Ч />

Частично ваше разочарование, вероятно, связано с тем, что Object реализует operator ==, а все - Object, поэтому компилятор может успешно разрешать такие операции, как a == b для всех типов. Когда вы переопределили ==, вы ожидали увидеть то же поведение, но не увидели, и это потому, что лучшее соответствие, которое может найти компилятор, - это оригинальная реализация Object.

Требование всех сравнений использовать Equals вместо оператора == не является жизнеспособным решением, особенно при передаче ваших типов в библиотеки (такие как Коллекции).

На мой взгляд, это именно то, что вы должны делать. Equals() - это правильный способ проверки семантического равенства двух объектов. Иногда семантическое равенство - это просто ссылочное равенство, и в этом случае вам не нужно ничего менять. В других случаях, как в вашем примере, вы переопределите Equals, когда вам нужен более сильный контракт равенства, чем ссылочное равенство. Например, вы можете считать, что два Persons равны, если у них одинаковый номер социального страхования, или два Vehicles равны, если у них одинаковый VIN.

Но Equals() и operator == - это не одно и то же. Всякий раз, когда вам нужно переопределить operator ==, вы должны переопределить Equals(), но почти никогда наоборот. operator == больше синтаксического удобства. Некоторые языки CLR (например, Visual Basic.NET) даже не позволяют переопределить оператор равенства.

4 голосов
/ 16 декабря 2011

Мы столкнулись с той же проблемой и нашли отличное решение: пользовательские шаблоны Resharper.

Мы настроили ВСЕХ наших пользователей на использование общего глобального каталога шаблонов в дополнение к их собственному и поместили его в SVN, чтобы его можно было обновлять и обновлять для всех.

В каталог включены все шаблоны, известные как неправильные в нашей системе:

$i1$ == $i2$ (где i1 и i2 выражения нашего типа интерфейса или производные.

шаблон замены

$i1$.Equals($i2$)

и серьезность - «Показывать как ошибку».

Точно так же у нас есть $i1$ != $i2$

Надеюсь, это поможет. Постскриптум Глобальные каталоги - это функция в Resharper 6.1 (EAP), очень скоро она будет помечена как окончательная.

Обновление : я подал Resharper Issue , чтобы пометить весь интерфейс '==' предупреждением, если он не сравнивается с нулевым. Пожалуйста, проголосуйте, если считаете, что это достойная функция.

Update2 : Resharper также имеет атрибут [CannotApplyEqualityOperator], который может помочь.

...