【51CTO.com原创稿件】在 C# 中 Object 是所有类的基类,所有的结构和类都直接或间接的派生自它。前面这段话可以说所有的 C# 开发人员都知道,但是我相信其中有一部分程序员并不清楚甚至不知道我们常用的 ToString 、 Equals 和 GetHashCode 虚方法都来自于 Object 类,并且我们可以对它们进行重写。重写这三个虚方法可以说在项目开发中经常用到,只不过大部分开发人员并未留意这三个虚方法可以重写,而是自己写方法来实现。
下面我就来具体讲解一下它们三个应该怎么重写。在这里我需要说明的是本篇文章会大量涉及到设计规范和设计要求,代码只是作为辅助理解的形式出现,因此文章中的所有代码将会以代码段的形式出现。
一、ToString
ToString 重写是这三种方法中重写最简单的,也是最常用的。但是有一部分开发人员认为重写 ToString 方法意义不大,那么我在这里要说的是这种想法是错误的。当我们在对象上调用 ToString 时默认返回的是类的完全限定名称,比如说我们在 System.IO.File 对象上调用这个方法,就会返回字符串 System.IO.File ,这个结果往往并不是我们所需要的结果并且这个结果也没有什么意义。例如我们在一个 User 类中重写 ToString 方法,每次调用 User.ToString() 时返回 "XXX今年XX岁",如果我们不重写 ToString 方法的话就得不到我们想要的结果。因此我们必须重写,这时我们就可以这么写。
- public class User
- {
- public int Id {get;set;}
- public string Name {get;set;}
- public int Age {get;set;}
- public string Sex {get;set;}
- public override string ToString()
- {
- return $"{Name}今年{Age}岁!";
- }
- }
重写之后我们就可以得到我们想要的输出内容了。虽然重写 ToString 可以得到我们想要的内容,但是我们不能在任何情况下都重写 ToString, 只有在以下三种情况下方可重写 ToString :
- 代码面对的最终用户是开发人员;
- 需要写入日志;
- IDE调试输出。
在上面三种情况下重写 ToString 我们还需要遵循一些设计规范,这些设计规范并不是微软所定义的,而是开发人员在开发过程中总结出来的:
- ToString 返回的字符串长度应该简短,内容描述应该清晰;
- 不要从 ToString 方法中返回 “”,而要返回 null ;
- 不要再 ToString 方法中引发并抛出异常,针对异常应该及时捕获并处理;
- 如果返回值存在地域文化(比如语言)或存在格式化要求,那么就必须重写 ToString 方法;
- ToString 重写后必须返回独一无二的字符串来标识实例对象。
到这里为止我们讲解完了 ToString 重写的方法以及规则。相对来说 ToString 方法重写是 Object 虚方法重写中十分简单的部分,作为开发人员只需按照我前面多说的规则、方法以及实际情况来重写即可。
二、 Equals 和 ReferenceEquals
在 C# 中如果对两个对象进行相等判断,一共有两种情况分别是:判断两者的值相等 或者 判断两者的引用地址相同 。一般情况下我们需要对值类型对象判断值相等,对引用类型对象判断指向地址相同。Equals 就是用来对引用类型对象判断指向地址是否相同的。对于重写 Equals 方法,很多开发人员认为易如反掌,但是在开发中往往忘记一些很重要的细节,这些细节对于程序来说至关重要,下面我将一一进行详细讲解。
-
同一和相等 所谓的同一指的是两个对象如果引用的是同一个实例,那么我们就说这两个对象具有同一性。在 C# 中我们可以利用 object 类或者它的派生类中的 ReferenceEquals 静态方法来判断对象之间的同一性。但是同一只是相等的一种,因为在某些情况下两个对象的部分值或者全部值相等但引用不同,我们也可以说它们具有相等性。下面我们来看一个例子,这个例子通过重写相等性来实现两个对象的相等性。
- class Program
- {
- static void Main(string[] args)
- {
- Student s1 = new Student
- {
- Age = 12,
- Id = 1,
- Name = "小明"
- };
- Student s2 = new Student
- {
- Age = 13,
- Id = 1,
- Name = "小明"
- };
- if (Student.ReferenceEquals(s1, s2))
- {
- Console.WriteLine("是同一个学生");
- }
- else
- {
- Console.WriteLine("不是同一个学生");
- }
- Console.Read();
- }
- }
-
- class Student
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public int Age { get; set; }
- public static bool ReferenceEquals(Student s1, Student s2)
- {
- if (s1.Equals(s2) ||
- object.ReferenceEquals(s1, s2) ||
- s1.Id==s2.Id
- s1==s2)
- {
- return true;
- }
- else
- {
- return false;
- }
- }
- }
从上述代码中我们可以看出,虽然 s1 和 s2 引用是不相等的,但是这两个对象使用了相同的 Id ,因此我们认为 Id 相同的学生就是同一个学生。这么做可以确保数据库中不会出现重复的录入。
Tip:只有引用类型才会可能出现引用相等的情况,对于值类型来说调用 ReferenceEquals 方法永远返回的是 false ,因为值类型转换成 object 时是需要装箱的,即是传递的两个参数是同一个值,也会返回 false 。
-
Equals 判断两个对象是否相等,可以使用 Equals ,通过它可以判断出两个对象是否具有相同的数据。在 object 中这个方法只是调用了 ReferenceEquals 方法来判断同一性,因此在必要的时候我们必须重写 Equals 方法。一般来说重写 Equals 方法常用的步骤如下:
-
检查对象是否为 null ;
-
判断是否是引用类型,如果是就判断引用是否相等;
-
判断数据类型是否相等;
-
调用具体类型的辅助方法,参数必须是要比较的类型;
-
判断哈希码是否相等,这一步需进行短路操作和字段比较;
-
在基类的 Equals 方法被重写的前提下,必须检查基类的 Equals 方法;
-
判断关键字段的值是否相等;
-
重写 GetHashCode 方法;
-
重写 == 、 != 操作符。
Tip: 如果类型是密封类型,那么第三步可以省略掉。
我们不仅需要按照上述的步骤重写 Equals 方法,还需要注意如下几点:
-
GetHashCode 方法不一定返回的是独一无二的值,因此我们不能仅仅依赖它的返回值来判断两个对象是否相等;
-
我们不能在 GetHashCode 和 Equals 中引发任何异常;
-
必须保证对象之间可以随意比较,且不能触发任何异常;
-
必须实现重写 Equals 、 GetHashCode 、 == 和 != ,且重写的算法必须相同;
-
尽量不要在可变类型上重写相等性操作符。
-
三、 GetHashCode
在上一小节中我们也注意到在重写 Equals 过程中我们需要重写 GetHashCode 方法。 所谓 Hash Code 就是用来生成和对象值对应的数字,从而高效的平衡哈希表的作用。 重写 GetHashCode 方法是比较困难的,下面我就来详细讲解一下重写规则、方法和注意事项。重写 GetHashCode 方法需要从性能、安全方面考虑,同时也需要满足一些要求。
- 性能 由于哈希码的返回值是 int 类型,因此会出现部分对象包含的值比 int 取值范围大的情况,这时哈希码就肯定会存在重复的情况,所以这时我们要保证哈希码的返回值尽可能的唯一。此外针对哈希码的算法我们要尽可能的保证返回的哈希码应当在 int 类型取值范围内平均分布。在 Equals 中利用 GetHashCode 方法进行短路操作时我们必须对算法的性能进行优化,避免将类型作为字典集合中的键类型使用,因为这会导致频繁的调用 GetHashCode 方法。在设计 GetHashCode 的算法时应保证良好的平衡性,即无论哈希表如何对哈希值进行 bucketing,也不会破坏平衡性。一般来说最理想的状态是两个对象间 1 bit 的差异应该造成哈希码 16 bit 的差异。
- 安全 在安全性这方面首先应该遵循的是难以伪造哈希码对象,一般来说攻击者会向哈希表中写入大量哈希值相同的数据,这时如果哈希表实现效率不高将会收到拒绝服务攻击。我们一般会向来自相关类型的哈希码使用异或操作,且保证操作数不相近或者相等。如果出现操作数相近或者相等的情况,那么应该考虑使用位移和加法操作。但是多次使用 and 操作符会出现哈希值为 0 的情况,而多次使用 or 操作符则会出现哈希值为 1 的情况,这一点需要注意一下。更进一步的做法是,我们在开发中应该使用移位操作符来分解比 int 大的类型。
- 要求 要求是性能和安全的基础,只要完全符合了要求的规定,性能和安全才能很好的起作用。要求的第一点也是最基础的优点,相等的对象它们的哈希码也相等,其次在特定的生命周期内,特定对象的 GetHashCode 的返回值始终是一样的,最后 GetHashCode 不能引发任何异常,如果其中出现异常也必须返回一个值来表示内部出现异常。
四、总结
本篇文章主要讲解了重写 object 中虚方法的知识,其中涉及到了很多 C# 核心内容,这些内容和知识在实际开发中用的很多,但是大多数开发人员并不在意,因此我希望读者阅读完我这篇文章后能对这些内容和知识有初步的了解。
作者简介:
朱钢,笔名喵叔,国内某技术博客认证专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于一家初创公司,从事企业级安全监控系统的开发。
【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】