关于.NET:在C#语言中使用基于接口编程的运算符重载

Operator Overloading with Interface-Based Programming in C#

背景

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

假设

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

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

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

我担心要求使用equals而不是operator==的原因是,在.NET指南中找不到它声明将使用equals而不是operator==或甚至建议使用equals的地方。但是,在重新阅读重写equals和operator==的指导原则之后,我发现了这一点:

By default, the operator == tests for reference equality by determining whether two references indicate the same object. Therefore, reference types do not have to implement operator == in order to gain this functionality. When a type is immutable, that is, the data that is contained in the instance cannot be changed, overloading operator == to compare value equality instead of reference equality can be useful because, as immutable objects, they can be considered the same as long as they have the same value. It is not a good idea to override operator == in non-immutable types.

这个相等的界面

The IEquatable interface is used by generic collection objects such as Dictionary, List, and LinkedList when testing for equality in such methods as Contains, IndexOf, LastIndexOf, and Remove. It should be implemented for any object that might be stored in a generic collection.

违章

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

问题

  • 当运算符==的两边都是接口时,来自基础具体类型的运算符==重载方法签名将不匹配,因此将调用默认的对象运算符==方法。
  • 在类上重载运算符时,二进制运算符的至少一个参数必须是包含类型,否则将生成编译器错误(错误bc33021 http://msdn.microsoft.com/en-us/library/watt39ff.aspx)
  • 不能在接口上指定实现

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

问题

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

工具书类

==操作员(C参考)

对于预定义的值类型,如果操作数的值相等,则相等运算符(==)返回"真",否则返回"假"。对于字符串以外的引用类型,==如果其两个操作数引用同一对象,则返回true。对于字符串类型,==比较字符串的值。

也见代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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.");
            }
        }
    }
}

产量

1
2
Address operator== overload called
Equal with both sides cast.


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


答案很长:操作符的过载解析是在编译时执行的,而不是运行时。

除非编译器能明确地知道它要对其应用运算符的对象的类型,否则它不会编译。由于编译器不能确定IAddress将是某个定义了==重写的对象,因此它返回到System.Object的默认operator ==实现。

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


您的挫折可能部分源于这样一个事实:Object实现了operator ==,而一切都是Object,因此编译器可以成功地解决所有类型的a == b这样的操作。当您超越==时,您希望看到相同的行为,但没有看到,这是因为编译器可以找到的最佳匹配是原始Object实现。

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

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

但是Equals()operator ==不是一回事。每当您需要覆盖operator ==时,您应该覆盖Equals(),但几乎没有相反的方法。operator ==在语法上更为方便。有些CLR语言(例如VisualBasic.NET)甚至不允许您重写相等运算符。


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

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

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

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

替换模式是

$i1$.Equals($i2$)

严重性为"显示为错误"。

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

希望这有帮助。P.S.Global Catalogs是Resharper 6.1(EAP)中的功能,将很快标记为最终产品。

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

update2:resharper还具有[canNoTapplyEqualityOperator]属性,可以提供帮助。