76

背景

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


假设

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

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

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

我担心要求使用 Equals 而不是 operator== 的原因是,我在 .NET 指南中找不到任何地方表明它将使用 Equals 而不是 operator== 甚至建议使用它。但是,在重新阅读了覆盖等于和运算符 == 的指南后,我发现了这一点:

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

和这个Equatable Interface

IEquatable 接口由通用集合对象(例如 Dictionary、List 和 LinkedList)在 Contains、IndexOf、LastIndexOf 和 Remove 等方法中测试相等性时使用。应该为可能存储在通用集合中的任何对象实现它。


约束

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

问题

  • 当 operator== 的两边都是接口时,没有来自底层具体类型的 operator== 重载方法签名将匹配,因此将调用默认的 Object operator== 方法。
  • 在类上重载运算符时,二元运算符的至少一个参数必须是包含类型,否则会产生编译器错误(错误 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.
4

3 回答 3

60

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


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

除非编译器可以明确知道它应用运算符的对象的类型,否则它不会编译。由于编译器无法确定 anIAddress是否会成为已定义覆盖的东西,==因此它会回退到operator ==.System.Object

为了更清楚地看到这一点,请尝试定义一个operator +forAddress并添加两个IAddress实例。除非您显式转换为Address,否则它将无法编译。为什么?因为编译器无法判断某个特定IAddress是 an Address,并且没有默认operator +实现可以回退到 in System.Object


您的部分挫败感可能源于Object实现 an的事实operator ==,并且一切都是 an Object,因此编译器可以成功地解析a == b所有类型的操作。当您覆盖时==,您希望看到相同的行为但没有,这是因为编译器可以找到的最佳匹配是原始Object实现。

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

在我看来,这正是你应该做的。Equals()是检查两个对象的语义相等性的正确方法。有时语义相等只是引用相等,在这种情况下您不需要更改任何内容。在其他情况下,如您的示例中,Equals当您需要比引用相等性更强的相等性合同时,您将覆盖。例如,如果两个Persons人具有相同的社会安全号码,您可能需要考虑两个相等,或者Vehicles如果他们具有相同的 VIN,则两个相等。

但是Equals()operator ==不是一回事。每当你需要覆盖operator ==时,你应该覆盖Equals(),但几乎从不反过来。operator ==更多的是语法上的便利。某些 CLR 语言(例如 Visual Basic.NET)甚至不允许您覆盖相等运算符。

于 2009-04-08T04:22:08.033 回答
4

我们遇到了同样的问题,并找到了一个很好的解决方案:ReSharper 自定义模式。

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

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

$i1$ == $i2$ (其中 i1 和 i2 是我们的接口类型的表达式,或派生的。

替换模式是

$i1$.Equals($i2$)

严重性为“显示为错误”。

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

希望这可以帮助。PS 全局目录是 ReSharper 6.1 (EAP) 中的功能,很快就会被标记为最终版本。

更新:我提交了一个ReSharper 问题以将所有接口“==”标记为警告,除非它与 null 进行比较。如果您认为这是一个有价值的功能,请投票。

Update2:ReSharper 还具有可以提供帮助的 [CannotApplyEqualityOperator] 属性。

于 2011-12-16T15:35:49.710 回答
0

IMO 这是 C# 中一个令人困惑的设计缺陷。IMO == 应该和现在的完全一样Equals(基本上不应该有Equals),如果你想要只引用相等,你可以调用一个专门的方法,比如 ReferenceEquals。围绕运算符重载和继承的语言设计缺陷加剧了这种情况 - 即您注意到的那些以及缺乏对运算符的扩展方法支持。

于 2021-02-14T02:01:56.863 回答