Перегрузка оператора с помощью интерфейсного программирования в 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)
- Невозможно указать реализацию на интерфейсе
См. Код и Вывод ниже, демонстрирующие проблему.
Вопрос
Как обеспечить правильные перегрузки операторов для ваших классов при использовании программирования на основе интерфейса?
Рекомендации
Для предопределенных типов значений оператор равенства (==) возвращает 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. Это усугубляется потоками проектирования языка, связанными с перегрузкой операторов и наследованием, то есть теми, которые вы отметили, и отсутствием поддержки методов расширения для операторов.