在 C# 中 Object 是所有类的基类,所有的结构和类都直接或间接的派生自它。前面这段话可以说所有的 C# 开发人员都知道,但是我相信其中有一部分程序员并不清楚甚至不知道我们常用的 ToStringEqualsGetHashCode 虚方法都来自于 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 :

  1. 代码面对的最终用户是开发人员;
  2. 需要写入日志;
  3. IDE调试输出。

在上面三种情况下重写 ToString 我们还需要遵循一些设计规范,这些设计规范并不是微软所定义的,而是开发人员在开发过程中总结出来的:

  1. ToString 返回的字符串长度应该简短,内容描述应该清晰;
  2. 不要从 ToString 方法中返回 “”,而要返回 null ;
  3. 不要再 ToString 方法中引发并抛出异常,针对异常应该及时捕获并处理;
  4. 如果返回值存在地域文化(比如语言)或存在格式化要求,那么就必须重写 ToString 方法;
  5. ToString 重写后必须返回独一无二的字符串来标识实例对象。

到这里为止我们讲解完了 ToString 重写的方法以及规则。相对来说 ToString 方法重写是 Object 虚方法重写中十分简单的部分,作为开发人员只需按照我前面多说的规则、方法以及实际情况来重写即可。

一、 Equals 和 ReferenceEquals

在 C# 中如果对两个对象进行相等判断,一共有两种情况分别是:判断两者的值相等 或者 判断两者的引用地址相同 。一般情况下我们需要对值类型对象判断值相等,对引用类型对象判断指向地址相同。Equals 就是用来对引用类型对象判断指向地址是否相同的。对于重写 Equals 方法,很多开发人员认为易如反掌,但是在开发中往往忘记一些很重要的细节,这些细节对于程序来说至关重要,下面我将一一进行详细讲解。

  1. 同一和相等

    所谓的同一指的是两个对象如果引用的是同一个实例,那么我们就说这两个对象具有同一性。在 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 。

  2. Equals

    判断两个对象是否相等,可以使用 Equals ,通过它可以判断出两个对象是否具有相同的数据。在 object 中这个方法只是调用了 ReferenceEquals 方法来判断同一性,因此在必要的时候我们必须重写 Equals 方法。一般来说重写 Equals 方法常用的步骤如下:

    • 检查对象是否为 null ;
    • 判断是否是引用类型,如果是就判断引用是否相等;
    • 判断数据类型是否相等;
    • 调用具体类型的辅助方法,参数必须是要比较的类型;
    • 判断哈希码是否相等,这一步需进行短路操作和字段比较;
    • 在基类的 Equals 方法被重写的前提下,必须检查基类的 Equals 方法;
    • 判断关键字段的值是否相等;
    • 重写 GetHashCode 方法;
    • 重写 == 、 != 操作符。

    Tip: 如果类型是密封类型,那么第三步可以省略掉。

    我们不仅需要按照上述的步骤重写 Equals 方法,还需要注意如下几点:

    • GetHashCode 方法不一定返回的是独一无二的值,因此我们不能仅仅依赖它的返回值来判断两个对象是否相等;
    • 我们不能在 GetHashCode 和 Equals 中引发任何异常;
    • 必须保证对象之间可以随意比较,且不能触发任何异常;
    • 必须实现重写 Equals 、 GetHashCode 、 == 和 != ,且重写的算法必须相同;
    • 尽量不要在可变类型上重写相等性操作符。

二、 GetHashCode

在上一小节中我们也注意到在重写 Equals 过程中我们需要重写 GetHashCode 方法。 所谓 Hash Code 就是用来生成和对象值对应的数字,从而高效的平衡哈希表的作用。 重写 GetHashCode 方法是比较困难的,下面我就来详细讲解一下重写规则、方法和注意事项。重写 GetHashCode 方法需要从性能、安全方面考虑,同时也需要满足一些要求。

  1. 性能

    由于哈希码的返回值是 int 类型,因此会出现部分对象包含的值比 int 取值范围大的情况,这时哈希码就肯定会存在重复的情况,所以这时我们要保证哈希码的返回值尽可能的唯一。此外针对哈希码的算法我们要尽可能的保证返回的哈希码应当在 int 类型取值范围内平均分布。在 Equals 中利用 GetHashCode 方法进行短路操作时我们必须对算法的性能进行优化,避免将类型作为字典集合中的键类型使用,因为这会导致频繁的调用 GetHashCode 方法。在设计 GetHashCode 的算法时应保证良好的平衡性,即无论哈希表如何对哈希值进行 bucketing,也不会破坏平衡性。一般来说最理想的状态是两个对象间 1 bit 的差异应该造成哈希码 16 bit 的差异。
  2. 安全

    在安全性这方面首先应该遵循的是难以伪造哈希码对象,一般来说攻击者会向哈希表中写入大量哈希值相同的数据,这时如果哈希表实现效率不高将会收到拒绝服务攻击。我们一般会向来自相关类型的哈希码使用异或操作,且保证操作数不相近或者相等。如果出现操作数相近或者相等的情况,那么应该考虑使用位移和加法操作。但是多次使用 and 操作符会出现哈希值为 0 的情况,而多次使用 or 操作符则会出现哈希值为 1 的情况,这一点需要注意一下。更进一步的做法是,我们在开发中应该使用移位操作符来分解比 int 大的类型。
  3. 要求

    要求是性能和安全的基础,只要完全符合了要求的规定,性能和安全才能很好的起作用。要求的第一点也是最基础的优点,相等的对象它们的哈希码也相等,其次在特定的生命周期内,特定对象的 GetHashCode 的返回值始终是一样的,最后 GetHashCode 不能引发任何异常,如果其中出现异常也必须返回一个值来表示内部出现异常。

三、总结

本篇文章主要讲解了重写 object 中虚方法的知识,其中涉及到了很多 C# 核心内容,这些内容和知识在实际开发中用的很多,但是大多数开发人员并不在意,因此我希望读者阅读完我这篇文章后能对这些内容和知识有初步的了解。

本文由博客一文多发平台 OpenWrite 发布! 更多文章请扫码关注公众号:“喵叔呦”

如何重写object虚方法的更多相关文章

  1. 重写ValidateEntity虚方法实现可控的上下文验证和自定义验证

    上篇文章介绍了ValidationAttribute和IValidatableObject.Validate验证,但是这种验证还是稍微简单了,对于复杂的实体,例如:继承过来的实体.实现某接口的实体等等 ...

  2. 5、面向对象以及winform的简单运用(方法重载、隐藏、重写与虚方法)

    方法的重载: 规定一个方法可以具有不同的实现,但方法的名称是相同的.如: //同样是Man这个方法 public int Man(int age,int name) { …… } //重载 publi ...

  3. c#面向对象基础 重写、虚方法、抽象类

    虚方法 抽象类与抽象方法 1.书写规范: 在类前面加上abstract关键字,就成为了抽象类:在一个方法前面加上abstract关键字,就成为了抽象方法(抽象方法不能有实现方法,直接在后面加分号) 例 ...

  4. 隐藏父类方法的new和重写父类虚方法virtual的区别

    一.代码 public class Parent { public void Method_A() { Console.WriteLine("Parent Method_A"); ...

  5. C# 读书笔记之访问虚方法、重写方法和隐藏方法

    C#允许派生类中的方法与基类中方法具有相同的签名:基类中使用关键字virtual定义虚方法:然后派生类中使用关键字override来重写方法,或使用关键字new来覆盖方法(隐藏方法). 重写方法用相同 ...

  6. 抽象方法(abstract method) 和 虚方法 (virtual method), 重载(overload) 和 重写(override)的区别于联系

    1. 抽象方法 (abstract method) 在抽象类中,可以存在没有实现的方法,只是该方法必须声明为abstract抽象方法. 在继承此抽象类的类中,通过给方法加上override关键字来实现 ...

  7. [C#解惑] #1 在构造函数内调用虚方法

    谜题 在C#中,用virtual关键字修饰的方法(属性.事件)称为虚方法(属性.事件),表示该方法可以由派生类重写(override).虚方法是.NET中的重要概念,可以说在某种程度上,虚方法使得多态 ...

  8. C#中的抽象方法和虚方法有什么区别?

    抽象方法是只有定义.没有实际方法体的函数,它只能在抽象函数中出现,并且在子类中必须重写:虚方法则有自己的函数体,已经提供了函数实现,但是允许在子类中重写或覆盖.重写的子类虚函数就是被覆盖了.

  9. C#抽象类、抽象方法、虚方法

    定义抽象类和抽象方法: abstract 抽象类特点: 1.不能初始化的类被叫做抽象类,它们只提供部分实现,但是另一个类可以继承它并且能创建它们的实例 2.一个抽象类可以包含抽象和非抽象方法,当一个类 ...

随机推荐

  1. CBV 序列化

    一.模型表 from django.db import models # Create your models here. class Publish(models.Model): name = mo ...

  2. Kettle自定义jar包供javascript使用

    我们都知道 Kettle 是用 Java 语言开发,并且可以在 JavaScript 里面直接调用 java 类方法.所以有些时候,我们可以自定义一些方法,来供 JavaScript 使用. 本篇文章 ...

  3. 高通量计算框架HTCondor(五)——分布计算

    目录 1. 正文 1.1. 任务描述文件 1.2. 提交任务 1.3. 返回结果 2. 相关 1. 正文 1.1. 任务描述文件 前文提到过,HTCondor是通过condor_submit命令将提交 ...

  4. java效率工具 Lombok

    Java项目中,充斥着太多不友好的代码:POJO的getter/setter/toStringm异常处理,I/O流的关闭操作等等,这些样板代码既没有技术含量,又影响着代码的美观,Lombok应运而生. ...

  5. 软工造梦厂团队项目(Alpha版本发布2)

    课程 (https://edu.cnblogs.com/campus/xnsy/GeographicInformationScience) 作业要求 https://www.cnblogs.com/h ...

  6. NSQ源码剖析——主要结构方法和消息生产消费过程

    目录 1 概述 2 主要结构体及方法 2.1 NSQD 2.2 tcpServer 2.3 protocolV2 2.4 clientV2 2.5 Topic 2.6 channel 3 启动过程 4 ...

  7. Dynamics 365 CRM 配置field service mobile

    配置field service mobile其实微软是有官方文档的, 但是没有坑的微软产品不是好产品. 一些细节设置文中还是没有考虑到的. 所以这里带大家配置一下field service mobil ...

  8. mysql--->mysql的事务和锁

    mysql 事务和锁 什么是事务?及其特性? 答:事务:是一系列的数据库操作,是数据库应用的基本逻辑单位. 或者这样理解: 事务就是被绑定在一起作为一个逻辑工作单元的SQL语句分组,如果任何一个语句操 ...

  9. PKU-3580 SuperMemo(Splay模板题)

    SuperMemo 题目链接 Your friend, Jackson is invited to a TV show called SuperMemo in which the participan ...

  10. 一、Django学习之连接与建立数据库

    连接MySQL数据库 配置文件 找到DATABASES对应的设置,修改为MySQL的配置即可 DATABASES = { 'default': { 'ENGINE': 'django.db.backe ...