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

Фон

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


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

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

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

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

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

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

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

Интерфейс 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.

3 ответа

Решение

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

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

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

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

ИМО, это сбивающий с толку недостаток дизайна в С #. IMO == должно было быть точно таким же, как сейчас (в принципе, не должно было быть Equals), и если вы хотите ссылаться только на равенство, вместо этого вы должны вызвать специализированный метод, например ReferenceEquals. Это усугубляется потоками проектирования языка, связанными с перегрузкой операторов и наследованием, то есть теми, которые вы отметили, и отсутствием поддержки методов расширения для операторов.

Другие вопросы по тегам