C# System.Object类型的主要方法

所有类型都从 System.Object 派生,接口和指针是特例。下面介绍一些主要的 System.Object 提供的方法。

1) ReferenceEquals(object a, object b)

静态方法。这个方法就是判断两个引用类型对象是否指向同一个地址。有此说明后,就确定了它的使用范围,即只能对于引用类型操作。

对于任何两个值类型数据比较,即使是与自身的比较,都会返回 false。

这是因为在调用此函数的时候,值类型数据要进行装箱操作,转换为 object 类型,而两次装箱,堆上会产生两个对象:

Console.WriteLine(ReferenceEquals(1, 1));   // False

这个方法可以用来验证字符串驻留。当字符串被驻留时,不会产生新的字符串对象:

Console.WriteLine(ReferenceEquals("a", "a"));    // True

2) Equals(object a, object b)

静态方法。如果两者皆为 null,或者ReferenceEquals(a, b)返回真,则返回真。

否则,调用下面的方法(以a.Equals(b)的形式)。重写 Equals 方法通常指重写下面的方法(当然这个方法也无法重写)。

3) Equals(object o)

虚方法,实例方法。它的调用结果和调用Equals(object a, object b)是一样的(也不可能不一样吧!),即如果两个对象皆为 null,或者具有相同的引用就返回真,否则就返回假。

但是,任何类型都可以重写该方法。重写它之后,由于Equals(object a, object b)也调用它,所以跟着结果也会改变。

对于引用类型,尽管引用类型可能包含许多成员,使用 Equals 比较引用类型时,仅仅考虑两个对象是否具有相同的引用,而不会逐个成员比较。

所以对于引用类型,除非你有其他的判等逻辑,否则不需要重写该方法。

System.ValueType( 值类型 )重写了该方法,使得方法仅仅比较值是否相等。

此时,如果值类型包含很多成员(例如结构体),会使用反射,取得类型所有的成员,然后逐个进行比较。

为了避开反射造成的性能损失,所以必须重写该方法,只需要在其中遍历所有结构的属性,并一一进行比较即可。

如果你自定义的结构的相等逻辑不要求所有的属性相等才意味着相等,而只是部分属性相等就意味着相等时,你更应该重写该方法。

例如,有这样一个结构体:

struct Rectangle
{
    double width { get; set; }
    double height { get; set; }
}

需要重写 Equals 方法。根据 MSDN,重写的要求有:

  • x.Equals(null) 返回 false,因为 x 是值类型,值类型不会是 null。
  • x.Equals(x) 返回 true。
  • 交换性:x.Equals(y) 与 y.Equals(x) 返回相同的值。
  • 传递性:如果(x.Equals(y) && y.Equals(z))返回 true 则 x.Equals(2) 返回 true。
  • 只要不修改 x 和 y 所引用的对象,任何时候调用 x.Equals(y) 都返回相同的值。

通常推荐使用下面的方式来实现:

public override bool Equals(object obj)
{
    if(obj !=null&&obj is Rectangle)
    {
        //强行转换为 Rectangle 类型
        var rect = (Rectangle)obj;
        //遍历所有属性
        return (rect.width == width) && (rect.height == height);
    }
    return false;
}

值得注意的是,虽然字符串是引用类型,它也重写了该方法,其行为和值类型一样,也仅仅比较值是否相等。这是字符串的行为看起来和值类型差不多的一个原因。

如果你重写了 Equals 方法,那么还应重写 GetHashCode 方法。如果你没有这么做的话,编译器会报告一条警告消息:重写了 Equals 但不重写 GetHashCode。

这是因为,这两个方法是有连带关系的,如果两个对象相等,那么它们的哈希码必须也相等(不过反过来, 两个对象不相等,它们的哈希码也可能相等)。

4) == 和上面判等方法的关系

== 是运算符重载,它并没有什么新意。普通的情况下,如果两边都是引用类型,则等同于 ReferenceEquals。

但 string 是特例,当两边都是字符串或值类型时,== 比较值是否相同。但是,== 不能用于结构体,除非你重载它。

例如,还是使用上面的长方形结构体,这次我们规定只要面积(长乘以宽)相等,就算相等:

struct Rectangle
{
    double width { get; set; }
    double height { get; set; }
    public Rectangle(double w,double h)
    {
        width = w;
        height = h;
    }
    public override bool Equals(object obj)
    {
        if (obj != null && obj is Rectangle)
        {
            //强行转换为 Rectangle 类型
            var rect = (Rectangle)obj;
            //面积相等吗?
            return width*height == rect.width*rect.height;
        }
        return false;
    }
}

此时,下面的代码输岀的结果一如预期:

var a = new Rectangle(1, 2);
var b = new Rectangle(2, 1);

Console.WriteLine(ReferenceEquals(a, b));  //False
Console.WriteLine(a.Equals(b));                  //true

如果我们没有重写 Equals 方法的话,则应该输出两个 false。但是,我们不能直接使用 == 符号判等:

Console.WriteLine(a==b);

我们必须重载它:

public static bool operator ==(Rectangle c1, Rectangle c2)
{
    return c1.Equals(c2);
}
public static bool operator !=(Rectangle c1, Rectangle c2)
{
    return !c1.Equals(c2);
}

此时,代码就会通过编译并输出正确的值。在重载 == 时,必须跟着一起重载 !=。

上面已经给出了重载 == 和 != 的标准方式。当然,如果故意的话,也可以用其他方式重载。

不过这样一来,==和 Equals 将有可能会输出不同的值,这是不符合逻辑的。

5) GetHashCode

在 CLR 中,任何对象的任何实例都对应一个哈希码。为此,System.Object 的虚方法 GetHashCode 能获取任意对象的哈希码。

当然,相同的对象的哈希码必然相同(不过,如果不同对象的哈希码相同,则没有关系,因为可能产生哈希碰撞)。所以,重写 Equals 方法必须重写 GetHashCode 方法,保证两者具有相同的语义。

重写 GetHashCode 方法除了要保证相同对象的哈希码必然相同之外,还要保证该对象的哈希码是不可变的,所以,该对象的哈希码如果基于它的一些成员,这些成员也应当是不可变的。通常借助类型中的唯一识别成员,例如 Id 等。

6) 重写 Equals:完整版本

根据上面的讨论,我们可以得出重写 Equals 的步骤:

  • 重写 Equals 覆盖 Object.Equals。
  • 重写 GetHashCode 方法。
  • 重载等号和不等号。
  • 实现 IEquatable<T> 接口,它会使得值类型比较大小时不会被隐式地装箱。

因此,对 Equals 重写的完整案例如下:

struct Rectangle:IEquatable<Rectangle>
{
    double width { get; set; }
    double height { get; set; }
    public override bool Equals(object obj)
    {
        if (obj != null && obj is Rectangle)
        {
            //强行转换为 Rectangle 类型
            var rect = (Rectangle)obj;
            //面积相等吗?
            return (rect.width==width)&&(rect.height==height);
        }
        return false;
    }
    public override int GetHashCode()
    {
        //保证语义一致性
        return width.GetHashCode() * height.GetHashCode();
    }
    //实现接口的方法,该方法会在传入参数人Rectangle是优先于Object.Equals方法
    //从而避免装箱
    public bool Equals(Rectangle other)
    {
        //遍历所有属性
        return (other.width == width) && (other.height == height);
    }
    //重载等于号和不等号
    public static bool operator == (Rectangle c1,Rectangle c2)
    {
        return c1.Equals(c2);
    }
    public static bool operator !=(Rectangle c1, Rectangle c2)
    {
        return !c1.Equals(c2);
    }
}

7) ToString

虚方法。返回类型的完整名称(this.GetType().FullName)。重写它的可能性很大,例如你希望 ToString 遍历对象的所有属性,打印出它所有属性的值。

8) GetType

返回对象指向的类型对象,返回值的类型为 System.Type。得到类型对象之后,就可以通过反射方法获得类型对象的成员,也就是对象本身的成员,例如字段、属性、方法等。

Typeof 关键字是这个方法的一个语法糖。

当没有对象实例时,也可以使用 Type.GetType 方法。下面若干方法获得的类型对象是相同的:

static void Main(string[] args)
{
    string a = "test";
    var typea = a.GetType();
    var typeb = Type.GetType("System.String");
    var typec = typeof(string);

    Console.WriteLine(ReferenceEquals(typea, typeb));       //true
    Console.WriteLine(ReferenceEquals(typea, typec));       //true
    Console.ReadKey();
}

9) Finalize

在 GC 决定回收这个对象之后,会调用这个方法。如果要做一些额外的事,例如回收对象的非托管属性或对象,则应当重写这个方法,但只有在存在非托管对象时才需要这么做。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/22370.html

(0)
上一篇 2021年7月20日
下一篇 2021年7月20日

相关推荐

发表回复

登录后才能评论