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

77

背景

我正在一个项目中使用基于接口的编程,但在重载运算符(特别是相等和不相等运算符)时遇到了问题。


假设

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

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

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

我之所以担心需要使用Equals而不是operator==,是因为我找不到任何地方在.NET指南中说明它会使用Equals而不是operator==,甚至建议使用它。然而,在重新阅读Guidelines for Overriding Equals and Operator==之后,我发现了这个:

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

和这个Equatable Interface

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


限制

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

问题

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

请参见下面的代码和输出,演示了这个问题。


问题

在使用基于接口的编程时,如何为您的类提供适当的运算符重载?


参考资料

==运算符 (C# 参考)

对于预定义值类型,等号运算符(==)在其操作数的值相等时返回 true,否则返回 false。对于除 string 以外的引用类型,== 在其两个操作数引用同一对象时返回 true。对于 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()方法。 - kvb
7
在提问时,清晰明了并提供详细信息会得到额外的加分。 - Cyril Gupta
kvb - 我更新了我的第二个假设,在阅读了John的答案和更多的MSDN文档后,发现这个假设是错误的。我已经在上面做了注释。谢谢!Cyril - 谢谢你! - Zach Burlingame
3个回答

60
简短回答:我认为你的第二个假设可能存在问题。使用equals()方法检查两个对象是否语义上等价是正确的方式,而不是使用operator==运算符。
长篇回答:运算符的重载分辨率是在编译时而不是运行时执行的。
除非编译器能够确定它正在应用运算符的对象的类型,否则它将无法编译。由于编译器无法确定IAddress是否是已定义了==覆盖的对象,因此它会回退到System.Object默认的operator==实现。
为了更清楚地看到这一点,请尝试为Address定义一个operator+并添加两个IAddress实例。除非您明确转换为Address,否则它将无法编译。为什么?因为编译器无法确定特定的IAddress是一个Address,也没有在System.Object中提供默认的operator+实现。
你的部分挫折可能源于Object实现了一个operator==,而所有东西都是一个Object,所以编译器可以成功地解析所有类型的操作比较如a==b。当你重写了==时,你期望看到相同的行为,但却没有,这是因为编译器能够找到的最佳匹配是原始的Object实现。
“要求所有比较都使用Equals而不是operator==不是一个可行的解决方案,特别是在将您的类型传递给库(如集合)时。”

在我看来,这正是你应该做的。 Equals() 是检查两个对象语义等价性的正确方法。 有时候语义等价性就是引用等价性,在这种情况下你不需要改变任何东西。但在另一些情况下,比如你的例子,当你需要比引用等价性更强的等价关系时,你需要重写 Equals���例如,如果你想要认为两个 Persons 在他们拥有相同社会安全号码的情况下是相等的,或者认为两个 Vehicles 拥有相同VIN时是相等的。

但是,Equals()operator == 不是同一回事。每当你需要重写 operator == 时,��应该重写 Equals(),但几乎从不反过来。 operator == 更多地是一种语法上的便利。一些CLR语言(比如Visual Basic.NET)甚至不允许你重写等式运算符。


1
重新阅读了两篇MSDN文档后,我确认Equals是检查值相等的正确方式,而operator==用于引用相等,除非它是不可变类型。我已经更新了第二个假设的注释,并提供了证明我的假设是错误的文档。谢谢! - Zach Burlingame
3
很高兴能帮忙。简短说明:我喜欢使用“语义相等”这个词组,而不是“值相等”,因为两个对象的值可能不同但仍被认为是相等的(例如两个邮政地址,其中一个使用“St”,另一个使用“Street”)。 - John Feminella
2
实际上,VB.NET允许重写等号运算符(http://msdn.microsoft.com/en-us/library/ms379613%28VS.80%29.aspx),并且它是一个更好的语言来实现这一点,因为它使用`Is`来表示引用相等性,而不像C#那样使用`=`。 - Gideon Engelberth
实际上,C# 并没有真正为 Object 过载等号运算符。相反,有两个运算符可以表示 == - 可过载的相等运算符或不可过载的引用比较运算符 - 当第一个失败时,C# 将尝试使用第二个。这不是我喜欢的设计,因为给定 bool CheckEquals<T>(T p1, T p2) where T:class {return p1 == p2;},调用 CheckEquals("123", 123.ToString()); 将检查引用标识而不是匹配值,即使带有 String== 通常会比较值。在类似 VB.NET 的东西中,... - supercat
如果一个函数使用不同的“Is”运算符,那么如果尝试使用等式检查运算符,该函数将拒绝编译;如果使用Is ,则可以正常编译,但在这种情况下,很明显正在测试引用标识。 - supercat
显示剩余5条评论

4
我们遇到了同样的问题,并找到了一个很好的解决方案:ReSharper自定义模式。
我们配置了所有用户使用一个共同的全局模式目录,除了他们自己的目录之外,并将其放入SVN中,以便为每个人进行版本控制和更新。
目录包括我们系统中已知的所有错误模式: $i1$ == $i2$(其中i1和i2是我们接口类型或派生表达式)。
替换模式是:

$i1$.Equals($i2$)

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

类似地,我们有$i1$ != $i2$

希望这可以帮助到您。 P.S. 全局目录是 ReSharper 6.1(EAP)中的功能,很快将被标记为最终版本。

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

Update2: ReSharper 还有 [CannotApplyEqualityOperator] 属性可以帮助。


+1 对于 [CannotApplyEqualityOperator] 属性,在接口上非常有用,因为引用相等性不太可能有帮助。 - JoshSub

0

在我看来,这是 C# 中一个令人困惑的设计缺陷。IMO 应该完全与现在的 Equals 相同(基本上不应该有 Equals),如果你只想要引用相等,你可以调用像 ReferenceEquals 这样的专门方法。这加剧了关于操作符重载和继承的语言设计缺陷 - 即您指出的问题以及运算符的扩展方法不支持。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接