Перегрузка операторов при интерфейсном программировании на C#

StackOverflow https://stackoverflow.com/questions/728434

Вопрос

Фон

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


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

  • Я использую C# 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)
  • Невозможно указать реализацию на интерфейсе

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


Вопрос

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


Рекомендации

== Оператор (Справочник по C#)

Для предопределенных типов значений оператор равенства (==) возвращает true, если значения его операндов равны, и false в противном случае.Для ссылочных типов, отличных от строки, == возвращает 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.
Это было полезно?

Решение

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

Другие советы

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

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

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

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

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

$i1$.Equals($i2$)

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

Аналогично у нас есть $i1$ != $i2$

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

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

Обновление2:В Resharper также есть атрибут [CannotApplyEqualityOperator], который может помочь.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top