【问题标题】:Operator Overloading with Interface-Based Programming in C#C# 中基于接口编程的运算符重载
【发布时间】:2010-10-18 05:23:41
【问题描述】:

背景

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


假设

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

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

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

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

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

还有这个Equatable Interface

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


约束

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

问题

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

请参阅下面的代码和输出来演示该问题。


问题

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


参考文献

== Operator (C# Reference)

对于预定义的值类型,相等运算符 (==) 如果其操作数的值相等则返回 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() 方法。
  • +1 以获得清晰和有问题的细节。
  • kvb - 我更新了我的第二个假设,在阅读了约翰的回答和更多 MSDN 文档之后,这个假设是错误的。我在上面已经注意到了。谢谢!西里尔 - 谢谢!

标签: c# .net operator-overloading equals


【解决方案1】:

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


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

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

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


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

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

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

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

【讨论】:

  • 正确,在重新阅读了两个 MSDN 文档后,Equals 是检查值相等性的正确方法,而 operator== 用于引用相等性,除非它可能是不可变类型。我用说明我的假设是错误的文件更新了第二个假设的符号。谢谢!
  • 很高兴我能帮上忙。快速说明:我喜欢使用短语“语义相等”而不是“值相等”,因为两个对象可能具有不同的值但仍被视为相等(例如,两个邮政地址,其中一个使用“St”,而其他用途“街”)。
  • 好点!我刚刚习惯于使用价值和参考,因为这就是 MSDN 使用的。这并不是说 MSDN 使用的总是最好的,甚至是正确的。
  • 好吧,不要误会我的意思——价值平等绝对是一个公认的术语。我只是想尽可能地使区别更清楚。
  • 实际上,VB.NET 确实允许覆盖相等运算符 (msdn.microsoft.com/en-us/library/ms379613%28VS.80%29.aspx),并且是一种更好的语言,因为它使用 Is 进行引用相等,而不是像 C# 那样共享 = .
【解决方案2】:

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

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

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

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

替换模式是

$i1$.Equals($i2$)

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

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

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

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

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

【讨论】:

  • +1 对于 [CannotApplyEqualityOperator] 属性,在引用相等不太可能有用的接口上很有用。
【解决方案3】:

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

【讨论】:

    猜你喜欢
    • 2020-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-28
    • 1970-01-01
    • 1970-01-01
    • 2015-05-09
    • 2019-11-17
    相关资源
    最近更新 更多