Перегрузка оператора с помощью интерфейсного программирования в С#

Фон

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


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

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

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

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

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

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

и Равносимвольный интерфейс

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


контрсилы

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

Проблема

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

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


Вопрос

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


Ссылки

== Оператор (ссылка на С#)

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


См. также


код

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.

Ответы

Ответ 1

Короткий ответ: Я думаю, ваше второе предположение может быть ошибочным. 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) даже не позволяют переопределить оператор равенства.

Ответ 2

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

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

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

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

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

$i1$.Equals($i2$)

а степень серьезности - "Показать как ошибку".

Аналогично имеем $i1$ != $i2$

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

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

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