运算符在C#中使用基于接口的编程重载

背景

我在当前项目上使用基于接口的编程,并且在重载操作符(特别是Equality和Inequality操作符)时遇到了问题。


假设

  • 我正在使用C#3.0,.NET 3.5和Visual Studio 2008

更新 – 以下假设是错误的!

  • 要求所有的比较使用Equals而不是运算符==不是一个可行的解决scheme,尤其是当您的types传递到库(如集合)。

我担心需要使用Equals而不是运算符==的原因是,我无法在.NET指南中的任何地方find它会使用Equals而不是运算符==甚至提示它。 但是,在重新读取等式和运算符==的指导原则后,我发现:

默认情况下,运算符==通过确定两个引用是否指示相同的对象来testing引用相等性。 因此,引用types不必为了获得这个function而实现operator ==。 当一个types是不可变的,也就是说,实例中包含的数据是不能改变的,重载operator ==来比较值相等而不是引用相等可以是有用的,因为作为不可变对象,它们可以被认为是长因为它们具有相同的价值。 在非不可变types中重写operator ==不是一个好主意。

和这个Equatable接口

当在Contains,IndexOf,LastIndexOf和Remove等方法中testing相等性时,IEquatable接口被generics集合对象(如Dictionary,List和LinkedList)使用。 应该为可能存储在通用集合中的任何对象执行。


约束上

  • 任何解决scheme都不一定需要将对象从接口转换为具体的types。

问题

  • 当运算符==两端都是一个接口时,底层具体types中的operator ==重载方法签名将不匹配,因此将调用默认的Object operator ==方法。
  • 在类上重载运算符时,二元运算符的至less一个参数必须是包含types,否则会生成编译器错误(错误BC33021 http://msdn.microsoft.com/en-us/library/watt39ff .aspx )
  • 无法在接口上指定实现

请参阅下面的代码和输出以显示问题。


在使用基于接口的编程时,如何为您的类提供正确的操作符重载?


参考

==运算符(C#参考)

对于预定义的值types,如果操作数的值相等,则相等运算符(==)返回true,否则返回false。 对于string以外的引用types,如果其两个操作数引用同一个对象,则==返回true。 对于stringtypes,==比较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. 

简短的回答:我认为你的第二个假设可能是有缺陷的。 Equals()是检查两个对象的语义相等的正确方法,而不是operator ==


长答案:运算符的重载parsing在编译时执行,而不是运行时

除非编译器可以明确地知道应用操作符的对象的types,否则不会编译。 由于编译器无法确定IAddress是否具有==定义的覆盖,所以它会回落到System.Object的默认operator ==实现。

要更清楚地看到这一点,请尝试为Address定义一个operator +并添加两个IAddress实例。 除非您明确强制转换为Address ,否则将无法编译。 为什么? 因为编译器不能告诉特定的IAddress是一个Address ,并且在System.Object没有默认的operator +实现。


部分挫折可能源于Object实现了一个operator == ,而且一切都是一个Object ,因此编译器可以成功地parsing所有types的操作,如a == b 。 当你重载== ,你期望看到相同的行为,但是没有,那是因为编译器能find的最好的匹配就是原始的Object实现。

要求所有比较使用Equals而不是运算符==不是一个可行的解决scheme,特别是当您的types传递到库(如集合)。

在我看来,这正是你应该做的。 Equals()是检查两个对象的语义相等的正确方法。 有时候,语义平等只是参考平等,在这种情况下,你不需要改变任何东西。 在其他情况下,就像在你的例子中那样,当你需要一个比参考等式更强的等式约定时,你将覆盖Equals 。 例如,如果两个Persons拥有相同的社会安全号码,则可能需要考虑两个Persons平等,或者如果他们具有相同的VIN,则两个Persons平等。

但是Equals()operator ==不是一回事。 每当你需要覆盖operator == ,你应该覆盖Equals() ,但几乎从来没有其他的方式。 operator ==更多是一个语法方便。 一些CLR语言(例如Visual Basic.NET)甚至不允许您覆盖相等运算符。

我们遇到了同样的问题,并find了一个很好的解决scheme:resharper自定义模式。

我们将所有用户configuration为使用除自己的通用全局模式目录之外的所有用户,并将其放入SVN中,以便为每个人进行版本化和更新。

该目录包括我们的系统中已知错误的所有模式:

$i1$ == $i2$ (其中i1和i2是我们的接口types或派生的expression式

replace模式是

$i1$.Equals($i2$)

严重程度是“显示为错误”。

同样我们有$i1$ != $i2$

希望这可以帮助。 PS Global目录是Resharper 6.1(EAP)中的function,将很快标记为最终。

更新 :我提出了一个Resharper问题来标记所有接口'=='一个警告,除非它比较为null。 如果您认为这是一个值得的function,请投票。

Update2 :Resharper也有可以帮助的[CannotApplyEqualityOperator]属性。