C # 接口编程运算符重载

背景资料

我在一个当前项目中使用基于接口的编程,在重载操作符(特别是 EquityandIneQuality 操作符)时遇到了问题。


假设

  • 我使用的是 C # 3.0,. NET 3.5和 Visual Studio 2008

更新-下面的假设是错误的!

  • 要求所有比较都使用 Equals 而不是操作符 = = 不是一个可行的解决方案,尤其是在将类型传递给库(例如集合)时。

之所以要求使用 Equals 而不是操作符 = = ,是因为在。NET 指导方针,它声明将使用 Equals 而不是操作符 = = ,甚至建议使用 Equals。然而,重读 重写等式和运算符的准则 = = 后,我发现:

默认情况下,运算符 = = 通过确定两个引用是否指示同一个对象来测试引用相等性。因此,引用类型不必实现 Operator = = 来获得这个功能。当一个类型是不可变的,也就是说,包含在实例中的数据不能更改时,重载运算符 = = 来比较值相等而不是引用相等是有用的,因为作为不可变对象,只要它们具有相同的值,就可以认为它们是相同的。在非不可变类型中覆盖运算符 = = 不是一个好主意。

还有这个 等效接口

当测试诸如 Contain、 IndexOf、 LastIndexOf 和 delete 等方法中的相等性时,通用集合对象(如 Dictionary、 List 和 LinkedList)可以使用 IEquable 接口。它应该针对可能存储在泛型集合中的任何对象实现。


限制

  • 任何解决方案都不能要求将对象从它们的接口转换为它们的具体类型。

问题

  • 当操作符 = = 的两端都是接口时,底层具体类型的操作符 = = 重载方法签名将不匹配,因此将调用默认的 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.
18089 次浏览

Short answer: I think your second assumption may be flawed. ABC0 is the right way to check for semantic equality of two objects, not operator ==.


Long answer: Overload resolution for operators is performed at compile time, not run time.

Unless the compiler can definitively know the types of the objects it's applying an operator to, it won't compile. Since the compiler cannot be sure that an IAddress is going to be something that has an override for == defined, it falls back to the default operator == implementation of System.Object.

To see this more clearly, try defining an operator + for Address and adding two IAddress instances. Unless you explicitly cast to Address, it will fail to compile. Why? Because the compiler can't tell that a particular IAddress is an Address, and there is no default operator + implementation to fall back to in System.Object.


Part of your frustration probably stems from the fact that Object implements an operator ==, and everything is an Object, so the compiler can successfully resolve operations like a == b for all types. When you overrode ==, you expected to see the same behavior but didn't, and that's because the best match the compiler can find is the original Object implementation.

Requiring all comparisons to use Equals rather than operator== is not a viable solution, especially when passing your types to libraries (such as Collections).

In my view, this is precisely what you should be doing. Equals() is the right way to check for semantic equality of two objects. Sometimes semantic equality is just reference equality, in which case you won't need to change anything. In other cases, as in your example, you'll override Equals when you need a stronger equality contract than reference equality. For example, you may want to consider two Persons equal if they have the same Social Security number, or two Vehicles equal if they have the same VIN.

But Equals() and operator == are not the same thing. Whenever you need to override operator ==, you should override Equals(), but almost never the other way around. operator == is more of a syntactical convenience. Some CLR languages (e.g. Visual Basic.NET) don't even permit you to override the equality operator.

We ran into the same problem, and found an excellent solution: ReSharper custom patterns.

We configured ALL of our users to use a common global pattern catalog in addition to their own, and placed it into SVN so that it can be versioned and updated for everyone.

The catalog included all patterns known to be wrong in our system:

$i1$ == $i2$ (where i1 and i2 are expressions of our interface type, or derived.

the replace pattern is

$i1$.Equals($i2$)

and the severity is "Show as error".

Similarly we have $i1$ != $i2$

Hope this helps. P.S. Global catalogs is the feature in ReSharper 6.1 (EAP), will be marked as final very soon.

Update: I filed a ReSharper Issue to mark all interface '==' a warning unless it is comparing to null. Please vote if you think it is a worthy feature.

Update2: ReSharper also has [CannotApplyEqualityOperator] attribute that can help.

IMO this is a confusing design flaw in C#. IMO == should have been exactly the same as what is now Equals (basically there shouldn't have been an Equals) and if you wanted reference only equality instead you would call a specialized method like ReferenceEquals. This is compounded by language design flaws around operator overloading and inheritance - ie the ones you noted and the lack of extention method support for operators.