C#中基于接口的编程的操作员重载


73

背景

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


假设条件

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

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

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

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

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

这个平等的接口

当使用诸如Contains,IndexOf,LastIndexOf和Remove这样的方法测试是否相等时,IEquatable接口将由Dictionary,List和LinkedList等通用集合对象使用。应该为可能存储在通用集合中的任何对象实现它。


制约因素

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

问题

  • 当operator ==的两端都是接口时,没有底层具体类型的operator ==重载方法签名匹配,因此将调用默认的Object operator ==方法。
  • 在类上重载运算符时,二进制运算符的至少一个参数必须为包含类型,否则会生成编译器错误(错误BC33021 http://msdn.microsoft.com/zh-cn/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()方法。
2009年

7
+1表示清楚和有问题。
西里尔·古普塔

kvb-我更新了第二个假设,在阅读了John的回答和更多MSDN文档之后,该假设是错误的。我已经在上面指出了。谢谢!西里尔-谢谢!
扎克·伯林格姆

Answers:


57

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


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

除非编译器能够确切地知道将运算符应用到的对象的类型,否则编译器将不会编译。由于编译器无法确定anIAddress将具有已==定义的替代项,因此它会退回到的默认operator ==实现System.Object

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


造成挫败的部分原因可能是因为Object实现了operator ==,而所有内容都是Object,因此编译器可以成功解析a == b所有类型的操作。当您覆盖时==,您希望看到相同的行为,但没有看到,这是因为编译器可以找到的最佳匹配是原始Object实现。

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

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

但是Equals()operator ==不是同一回事。每当您需要覆盖时operator ==,都应该覆盖Equals(),但几乎绝不会相反。operator ==在语法上更方便。某些CLR语言(例如Visual Basic.NET)甚至不允许您覆盖均等运算符。


1
正确,在重新阅读了两个MSDN文档之后,Equals是检查值相等性的正确方法,而operator ==是引用相等性的正确方法,除非它可能是不可变的类型。我用说明我的假设是错误的文档更新了第二个假设的表示法。谢谢!
扎克·伯林格姆

3
很高兴我能帮助你。快速说明:我喜欢使用短语“语义相等”而不是“值相等”,因为两个对象可能具有不同的值,但仍然被认为是相等的(例如,两个邮政地址,其中一个使用“ St”,其他用途为“街道”)。
约翰·费米内拉

好点子!我一直习惯于使用值和引用,因为这是MSDN所使用的。这并不是说MSDN使用的总是最好的,甚至是正确的。
Zach Burlingame,2009年

好吧,请不要误会我-价值平等绝对是一个公认的术语。我只想在可能的地方更清楚地区分。
John Feminella 09年

2
实际上,VB.NET确实允许重写相等运算符(msdn.microsoft.com/en-us/library/ms379613%28VS.80%29.aspx),并且它是一种更好的语言,因为它Is用于引用相等而不是=像C#一样共享。
Gideon Engelberth

4

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

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

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

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

替换模式是

$i1$.Equals($i2$)

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

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

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

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

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


+1对于[CannotApplyEqualityOperator]属性,此属性在不太可能使用引用相等性的接口上很有用。
JoshSub
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.