Перегрузка операторов при интерфейсном программировании на C#
-
05-09-2019 - |
Вопрос
Фон
Я использую программирование на основе интерфейсов в текущем проекте и столкнулся с проблемой при перегрузке операторов (в частности, операторов равенства и неравенства).
Предположения
- Я использую 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], который может помочь.