C# 重载 Equals() 方法、重载运算符、声明显隐式转换的简要整理

自动生成

可以使用 JetBrains ReSharper 的代码生成功能来自动生成各种结构性的或可重载的成员,而不必自行手写,因为非常麻烦且易错。

如确需手写,可参考本文。

引用类型和值类型

本文不赘述二者的概念。但要首先说明的是,二者由于在机制和特性上存在一些差别,有些方法在重载时也应采用不同的方式分开讨论。下文也按照这个思路来叙述的,而且,所提及到的“引用类型”均以class为例,“值类型”均以struct为例,因为:

  • class是引用类型。需要注意的是,如果一个class没有用户指定的父类,则其父类为System.Object
  • struct是结构类型,但结构类型隶属于值类型。需要注意的是,可以认为任意一个struct的 “父亲” 都是System.ValueType

关于某个类型的父类(或“父亲”),在重载Equals(object)方法时将会涉及到。

如尚有疑问,可参考:

重载 Equals(object) 方法

设用户定义类型为T,则:

引用类型

实现

T为引用类型时,应遵照下列步骤:

  1. 调用ReferenceEquals(object, object)方法检查传入参数是否为null, 如果为null,那么返回false, 否则执行步骤 2。
  2. 调用ReferenceEquals(object, object)方法检查传入参数(obj)与当前对象(this)(下称“二者”)是否为同一个对象,如果是,那么返回true; 否则执行步骤 3。
  3. 分别使用二者的GetType() 方法,判断二者是否为同一类型,如果不是,那么返回false,否则执行步骤 4。
  4. 自己编写一个Equals(T)方法(其参数类型应为T,返回类型应为bool;也可通过继承 System.IEquatable<T> 接口来实现该方法)。调用这个方法并将obj转换(酌情使用(T)as T)为具体的自定义类型T(而非object)传入其中,然后返回其执行结果。
  5. 在自己编写的Equals(T)方法中,调用GetHashCode()方法(也应为重载的,见后文)判断传入参数与当前对象(this)的返回值是否相同,如果不同则返回false,否则执行步骤 6。
  6. 在自己编写的Equals(T)方法中,如果T的父类为System.Object类型,则执行步骤 7;如果T的父类不为System.Object类型,则调用base.Equals(T),如果其返回false,那么该方法返回false,否则执行步骤 7。
  7. 比较T内部的所有字段是否相等,如果相等则返回true,否则返回false
  8. 除此之外,还需要重载GetHashCode()方法以及==!=运算符。

以上是完整的重载步骤,使用时可以酌情跳过一些。具体示例如下:

class Circle
{
    
    
    protected bool Equals(Circle other)
    {
    
    
        return _radius == other._radius
           && _center.Equals(other._center)
           && _description == other._description;
    }

    public override bool Equals(object obj)
    {
    
    
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Circle) obj);
    }

    public override int GetHashCode()
    {
    
    
        // Use 'System.HashCode' to implement 'GetHashcode' — if this checkbox is
        // selected, ReSharper will use the Combine from System.HashCode (which uses
        // xxHash32) in the generated GetHashcode implementation. Otherwise, ReSharper
        // will generate its own custom implementation.
        //
        // 注:此方法在某些 .Net 环境下不支持(如 Mono),此处仅为示意,
        // 应考虑其他的实现方法,它们在后文的章节中有所提及
        return HashCode.Combine(_radius, _center, _description);
    }

    /// <summary>Radius of the circle</summary>
    int _radius;

    /// <summary>Center point of the circle</summary>
    Point _center;

    /// <summary>Description of the circle</summary>
    string _description;
}

原因解释(可跳过):

The implementation of equality methods (that is Equals() and GetHashCode()) as well as equality operators (that is == and !=) in the Object class guarantees reference equality. In a type you create (which ultimately derives from Object and thus implements reference equality by default), you may want to implement value equality for objects of this type and use the hashcode as a unique object identifier for hashing purposes. In this case, you need to override equality methods and operators for your type.

来源

父类为用户定义类型时的实现

T的父类不为System.Object而为一个用户定义类型TParent时,也参照上面的步骤即可。具体示例参见文章 MSDN:Equals() 和运算符 == 的重写准则(C# 编程指南)

值类型

实现

T为值类型时,重载步骤与当其为引用类型时基本相同,也参照上面的步骤即可,但:

  • 步骤 2 应该省略;
  • 步骤 6 照常。

示例如下:

public struct Point : IEquatable<Point> // 此接口非必需
{
    
    
    public Point(int x, int y)
    {
    
    
        X = x;
        Y = y;
    }

    public int X {
    
     get; }

    public int Y {
    
     get; }

    public override int GetHashCode()
    {
    
    
        return X ^ Y;
    }

    public override bool Equals(object obj)
    {
    
    
        if (!(obj is Point))
            return false;

        return Equals((Point)obj);
    }

    public bool Equals(Point other) // 步骤 4,同时也实现了 IEquatable<Point> 接口
    {
    
    
        if (X != other.X)
            return false;

        return Y == other.Y;
    }

    public static bool operator ==(Point point1, Point point2)
    {
    
    
        return point1.Equals(point2);
    }

    public static bool operator !=(Point point1, Point point2)
    {
    
    
        return !point1.Equals(point2);
    }
}

来源

相等和相等性比较

相等和相等性比较,本质上就是在介绍重载Equals(object)方法的流程;和上文内容差不多,但更注重对深层概念的讲解。

如果对前文中涉及的概念或方法仍不清楚,或者想看到更多例子,可以参见本节;否则建议跳过,容易看太多想太多。

微软已经介绍了相等的相关概念、相等性比较的前后原委,并且对其实现进行了类和结构体的分别举例。详见下面几篇文章。

重载 == 和 != 运算符

设用户定义类型为T。这里先考虑重载==(T, T)!=(T, T)的情况,其语法为:

public static bool operator ==(T lhs, T rhs)
{
    
    
    // 此部分实现与 T 是引用类型还是值类型有关,请参看下文
}

public static bool operator !=(T lhs, T rhs)
{
    
    
    return !(lhs == rhs); // 可以通过否定 '==' 来实现
}

原则:

  1. 重载的运算符==实现不应引发异常。
  2. 重载运算符==时还应重载运算符!=

引用类型

实现

class ThreeDPoint
{
    
    
    public readonly int x, y, z;

    public ThreeDPoint(int x, int y, int z)  //constructor
    {
    
    
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    public static bool operator ==(ThreeDPoint a, ThreeDPoint b)
    {
    
    
        // If both are null, or both are same instance, return true.
        if (System.Object.ReferenceEquals(a, b))
        {
    
    
            return true;
        }

        // If one is null, but not both, return false.
        if (((object)a == null) || ((object)b == null))
        {
    
    
            return false;
        }

        // Return true if the fields match:
        return a.x == b.x && a.y == b.y && a.z == b.z;
    }

    public static bool operator !=(ThreeDPoint a, ThreeDPoint b)
    {
    
    
        return !(a == b);
    }
}

默认情况下,运算符==通过判断两个引用是否指示同一对象来测试引用是否相等。因此引用类型不需要实现运算符==就能获得此功能。当类型不可变(即实例中包含的数据不可更改)时,通过重载运算符==来比较值是否相等而不是比较引用是否相等可能会很有用,因为作为不可变的对象,只要其值相同,就可以将其视为相同。建议不要在非不可变类型中重载运算符==

说明:(在引用类型的)运算符==的重载中的常见错误是使用(a == b)(a == null)(b == null)来检查引用相等性。这会调用重载的运算符==,从而导致无限循环。应使用ReferenceEquals(object, object)或将类型强制转换为Object来避免无限循环。

来源

值类型

实现

public struct Point : IEquatable<Point> // 此接口非必需
{
    
    
    public Point(int x, int y)
    {
    
    
        X = x;
        Y = y;
    }

    public int X {
    
     get; }

    public int Y {
    
     get; }

    public override int GetHashCode()
    {
    
    
        return X ^ Y;
    }

    public override bool Equals(object obj) // 实现重载 Equals(object) 方法的步骤 4,同时也实现了 IEquatable<Point> 接口
    {
    
    
        if (!(obj is Point))
            return false;

        return Equals((Point)obj);
    }

    public bool Equals(Point other)
    {
    
    
        if (X != other.X)
            return false;

        return Y == other.Y;
    }

    public static bool operator ==(Point point1, Point point2)
    {
    
    
        return point1.Equals(point2);
    }

    public static bool operator !=(Point point1, Point point2)
    {
    
    
        // 或 return !(point1 == point2);
        return !point1.Equals(point2);
    }
}

应当注意的是:用户定义的struct默认情况下不支持==运算符。 要支持==运算符,用户定义的结构必须重载它。

来源

重载 ==(T, U) 和 !=(T, U) 运算符

实现

实际上,一般地,除了上文提到的重载==(T, T)!=(T, T) 以外,还允许声明==(T, U)!=(T, U) ,其中U是另外一个类型,相当于实现如何判断类型T与另一其他类型U相等。应注意:

  • U可以是已存在的类型,也可以是用户定义类型;
  • TU不一定必须同时为引用类型或值类型。

个人认为,虽然 [ ==(T, U)!=(T, U) ] 与 [ ==(T, U)!=(T, U) ] 在形式上相同,但此时不应再称之为“重载”,而可以称之为“声明”。

其语法为:

public static bool operator ==(T lhs, U rhs)
{
    
    
    // ...
}

public static bool operator !=(T lhs, U rhs)
{
    
    
    return !(lhs == rhs);
}

由于TU不一定必须同时为引用类型或值类型,而且相等的判定依据也是由用户定义的,故实现时应根据具体情况具体分析,没有固定的步骤或格式可以参考。但是,仍应遵循本节开头提到的原则,即:在任何情况下均不产生异常;声明==时也必须同时声明!=

应该注意的是,需要区别 [ ==(T, U)!=(T, U) ] 与 [ TU之间的隐式转换和显式转换] 。

来源

自己的想法。

重载其他运算符

更一般地,除了可重载==!=运算符以外,C# 还允许更普遍的运算符重载。

实现

可重载的运算符分为一元运算符和二元运算符:

  • 一元运算符是下列运算符中的一个:+ - ! ~ ++ — true false(其中+表示自身,-表示相反数,二者均对自身运算,而不引入第二个变量变成x±y);
  • 二元运算符是下列运算符中的一个:+ - * / % & | ^ << >> == != > < >= <=(其中==!=的重载比较复杂,已在上节单独讨论);

按照语法:

// 重载一元运算符
public static result_type operator unary_operator(op_type operand)
// 重载二元运算符
public static result_type operator binary_operator(op_type operand, op_type2 operand2)

其中:

  • result_type是运算符的结果类型;
  • unary_operator是一元运算符;
  • binary_operator是二元运算符;
  • op_type是第一个(或唯一一个)参数的类型;
  • operand是第一个(或唯一一个)参数的名称;
  • op_type2第二个参数的类型;
  • operand2第二个参数的名称;

op_typeop_type2中必须至少有一个是封闭类型(即运算符所属的类型,或理解为用户定义类型)。

特殊情况:

  • 比较运算符(==!=<><=>=)必须成对重载,且result_type必须为bool
  • 条件逻辑运算符(&&||)的处理应参见 MSDN
  • 复合赋值运算符(如+=*=,等)不能显式重载,而是由编译器自动实现(如果重载了对应的二元运算符的话);
  • 有些运算符不能重载(如=typeof,等)。

来源

声明转换运算符

即声明隐式转换和(或)显式转换:

// 隐式转换
public static implicit operator conv_type_out(conv_type_in operand)
// 显式转换
public static explicit operator conv_type_out(conv_type_in operand)

其中:

  • conv_type_out是类型转换运算符的目标类型;
  • conv_type_in是类型转换运算符的输入类型;
  • operand是参数的名称;

conv_type_outconv_type_in中必须有且只有一个是封闭类型(即运算符所属的类型,或理解为用户定义类型)。

设计思路

隐式转换

  • conv_type_in转换为conv_type_out时,在任何情况下不会抛出异常而且不会丢掉信息,应该被设计成隐式转换。
  • 换句话说,设计隐式转换时应保证在任何情况下不会抛出任何异常。
  • 如:正方形转换为矩形,在任意情况下均成立。

显式转换

  • conv_type_in转换为conv_type_out时,在某些情况下可能产生异常(例如因为源变量超出了范围)或丢掉信息(例如丢掉高位),那么应该被定义为显式转换。
  • 如:矩形转换为正方形,在宽高不等的情况下将抛出异常。
  • 在发生异常时,应务必抛出System.InvalidCastException

孰 T 孰 U

T是用户定义类型,U是另一个类型,其中:

  • U可以是已存在的类型,也可以是用户定义类型;
  • TU不一定必须同时为引用类型或值类型;
  • 可能有conv_type_inTconv_type_outU的情况,即T转换为U
  • 或者有conv_type_inUconv_type_outT的情况,即U转换为T

则有:

三种情况

  • T转换为U是隐式的,U转换为T是显式的;或者反过来;
  • T转换为U,和U转换为T,都是隐式的;
  • T转换为U,和U转换为T,都是显式的。

定义位置

  • 常在T内定义T转换为U,并在U内定义U转换为T
  • U的源代码不可获取或不可更改时,可以在T内同时定义T转换为U,和U转换为T
  • T转换为U,或U转换为T,不允许被定义在另一个无关的类型V中,因为这违背了封闭性的要求。

引用类型

实现

public class Rectangle
{
    
    
    public int width {
    
     get; set; }
    public int height {
    
     get; set; }

    public Rectangle(int width, int height)
    {
    
    
        this.width = width;
        this.height = height;
    }

    // 矩形转换为正方形,应定义为显式转换,因为可能抛出异常
    public static explicit operator Square(Rectangle value)
    {
    
    
        if (ReferenceEquals(value, null))
        {
    
    
            return null;
        }

        if (value.width != value.height)
        {
    
    
            throw new InvalidCastException();
        }

        return new Square(value.width);
    }
}

public class Square
{
    
    
    public int length {
    
     get; set; }

    public Square(int length)
    {
    
    
        this.length = length;
    }

    // 正方形转换为矩形,应定义为隐式转换,因为永远不会抛出异常
    public static implicit operator Rectangle(Square value)
    {
    
    
        if (ReferenceEquals(value, null))
        {
    
    
            return null;
        }

        return new Rectangle(value.length, value.length);
    }
}

来源

值类型

实现

// 矩形
public struct Rectangle
{
    
    
    public int width {
    
     get; set; }
    public int height {
    
     get; set; }

    public Rectangle(int width, int height)
    {
    
    
        this.width = width;
        this.height = height;
    }

    // 矩形转换为正方形,应定义为显式转换,因为可能抛出异常
    public static explicit operator Square(Rectangle value)
    {
    
    
        if (value.width != value.height)
        {
    
    
            throw new InvalidCastException();
        }

        return new Square(value.width);
    }
}

// 正方形
public struct Square
{
    
    
    public int length {
    
     get; set; }

    public Square(int length)
    {
    
    
        this.length = length;
    }

    // 正方形转换为矩形,应定义为隐式转换,因为永远不会抛出异常
    public static implicit operator Rectangle(Square value)
    {
    
    
        return new Rectangle(value.length, value.length);
    }
}

来源

效果

Rectangle rectangle = new Rectangle(20, 20);
Square square = new Square(10);

// 隐式转换:直接使用赋值号完成类型转换
rectangle = square;

// 显式转换:允许使用强制转换完成类型转换
square = (Square)rectangle;

重载 GetHashCode() 方法

在实践中,经常出问题的情况是把GetHashCode()相等和Equals(object)相等等价(<=>)了,但正确的情况应该是GetHashCode()不相等 => Equals(object)不相等,或者说Equals(object)相等 => GetHashCode()相等;反之则不正确。

关于 GetHashCode() 的重载有很多原则和算法。

未完待续。

猜你喜欢

转载自blog.csdn.net/xzqsr2011/article/details/126590652
今日推荐