首页 / C# 入门教程 / object 基类

C# 入门教程

object 基类

本教程共 100 篇 · 第 47 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程面向对象object

47. object 基类

本节目标:认识所有类型的共同基类 object,掌握重写 ToStringEquals/GetHashCode/GetType

所有类型都源自 object

在 C# 里,无论类、结构还是记录,最终都继承自 System.Object(可简写为 object)。这意味着每个对象天生带着几个通用方法。理解它们,是写好类型的基础。

正因为万物同源,任何对象都能赋值给 object 变量——这也是装箱(第 48 章)能成立的前提。你甚至可以写 object x = 任意东西;,编译器不会拦你。

ToString:对象的”自述”

默认 ToString 返回类型全名,没什么用。我们通常在自己的类里重写它,让输出更有信息量。

namespace CSharpDemo;

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }

    public override string ToString() => $"Person(Name={Name}, Age={Age})";
}

Person p = new Person { Name = "小明", Age = 18 };
Console.WriteLine(p); // 自动调用 ToString

Console.WriteLine(p) 内部会自动调用 ToString,所以重写它能直接改善调试与日志输出。当对象被放进集合、被插值字符串引用时,ToString 也都会被调用。

Tip

调试时把对象直接丢进 Console.WriteLine 或日志,若看到一堆 Namespace.Person,八成是忘了重写 ToString

Warning

ToString 会被日志、调试器、字符串插值频繁调用,所以里面不要做昂贵的计算,也不要抛异常。一旦它出错,排查起来非常痛苦,因为调用点可能遍布各处。

Equals 与 GetHashCode:相等判断

Equals 判断两个对象是否”相等”。默认引用类型比的是”是否同一块内存”,往往不合预期。

namespace CSharpDemo;

class Person
{
    public string Name { get; set; } = "";
    public override bool Equals(object? obj) =>
        obj is Person other && Name == other.Name;
    public override int GetHashCode() => Name.GetHashCode();
}

GetHashCode 返回对象的哈希码,字典、集合靠它快速定位。两条规则必须配套:一旦重写了 Equals,就必须同时重写 GetHashCode。否则两个”相等”的对象可能算出不同的哈希码,在 Dictionary 里被当成不同键。

Note

一旦重写了 Equals,就必须同时重写 GetHashCode。两条约定必须配套,否则字典(Dictionary)等集合会行为异常。

正确实现 Equals 的要点

一个稳妥的 Equals 写法要按顺序判断:非空、同类型、再比字段。借助 is 模式能一行搞定前两步。

namespace CSharpDemo;

class Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public override bool Equals(object? obj) =>
        obj is Point p && X == p.X && Y == p.Y;
    public override int GetHashCode() => HashCode.Combine(X, Y);
}

HashCode.Combine 是 .NET 提供的便捷方法,能把多个字段的哈希合并成一个,比手写异或更可靠。

Warning

GetHashCode 的值在对象”作为键”期间必须保持稳定。如果你在放入字典后又改了参与哈希的字段,再去找就找不到了。所以参与哈希的字段最好设为不可变(用 initreadonly)。

ReferenceEquals:比的是不是同一个对象

object.ReferenceEquals(a, b) 直接比较引用是否指向同一块内存,不受你重写的 Equals 影响。

namespace CSharpDemo;

Person a = new Person { Name = "小明" };
Person b = new Person { Name = "小明" };
Console.WriteLine(object.ReferenceEquals(a, b)); // False,两块内存

当你需要确认”是不是同一个实例”,而不是”值是否相等”,就用它。它永远按引用比,绕过一切重写。

GetType:拿到运行期类型

GetType() 返回对象的真实类型信息,常用于日志和反射(后续章节)。

namespace CSharpDemo;

Person p = new Person { Name = "小红" };
Console.WriteLine(p.GetType().Name); // 输出:Person

即便变量声明为 objectGetType() 拿到的仍是真实类型,这正是”运行期类型”的体现。

四个核心方法一览

方法默认行为何时重写
ToString返回类型名想输出有用信息
Equals引用比较想按”值”判断相等
GetHashCode基于引用重写 Equals 时必改
GetType返回真实类型通常不用重写
Note

GetTypesealed 的,你不能重写它。想自定义”类型信息”请用特性或别的机制。

Equals 的三条契约

Equals 不是”随便比一比”就算了。.NET 规定它必须满足三条数学契约:自反(自己和自己比必为 True)、对称(a.Equals(b)b.Equals(a) 结果一致)、传递(若 a==bb==c,则 a==c)。你重写时守住这三条,哈希集合才不会出现”逻辑矛盾”——比如两个相等的对象却查不到彼此。这也是为什么重写 Equals 必须同步重写 GetHashCode:哈希码是契约的”物理实现”,两者必须一致,否则字典会把”相等的对象”分到不同的桶里。

与 record 的对比

如果你只想要”值相等 + 漂亮输出”,手写 Equals/GetHashCode/ToString 很累。第 49 章的 record 会让编译器一次性生成好这些成员。简单说:手写 object 方法适合高度定制的类型,record 适合”以数据为中心”的类型。

新手常见坑

第一,只重写 Equals 忘了 GetHashCode。在哈希集合里两个”相等”的对象会被当成不同键。

第二,用 == 比较自定义类时,默认仍是引用比较,不会自动走你重写的 Equals。想让 == 也生效,需要额外重载 == 运算符(见运算符相关章节)。

第三,ToString 里不要做昂贵或抛异常的操作,它常在日志、调试器、字符串插值中被频繁调用。

第四,在 Equals 里比较引用类型字段时,要确认那个字段自身也正确实现了相等性,否则会”浅比较”出错。

第五,把会变化的字段放进 GetHashCode。对象进字典后改字段,会导致之后查不到,是隐蔽 bug。

综合示例:用相等性给集合去重

设想一个去重场景:把若干 Person 放进 HashSet,名字相同的算同一个。因为重写了 Equals/GetHashCode,集合能正确识别”重复”。

namespace CSharpDemo;

class Person
{
    public string Name { get; set; } = "";
    public override bool Equals(object? obj) =>
        obj is Person other && Name == other.Name;
    public override int GetHashCode() => Name.GetHashCode();
}

HashSet<Person> set = [new Person { Name = "小明" }, new Person { Name = "小明" }];
Console.WriteLine(set.Count); // 1,第二个被判定重复

这正是配套重写两个方法的回报:集合不再靠”引用”判断,而靠你定义的”值”判断。

实际写类型时,建议按这个优先级思考:先决定”它如何算相等”,再决定重写哪些方法。多数业务类只需要漂亮输出,重写 ToString 就够了;只有当对象要进字典或集合判重,Equals/GetHashCode 才需要配套登场。struct 默认已有值相等,通常不必重写;class 默认引用相等,这才是常要手写的地方。

常见疑问解答

问:ToString 能带格式吗? 能。可重载 ToString(string? format) 或实现 IFormattable,支持 ToString("N2") 这类格式。日常用无参重写版已足够。

问:Equals 和 == 到底啥关系? Equals 是虚方法,可被重写;== 是运算符,默认引用类型比引用、值类型比值。两者不是一回事,重写 Equals 不会自动让 == 变聪明。

问:值类型(struct)的默认相等从哪来? struct 默认走 System.ValueType 提供的值相等(逐字段比),所以很多 struct 不必自己写。class 则默认引用相等,这才是要手写的原因。

问:GetHashCode 和 Equals 必须一起改吗? 必须。两个对象 Equals 返回 True,它们的哈希码必须相同;否则放进哈希集合就乱套。配套是铁律。

问:为什么 GetType 不能重写? 它返回运行期真实类型,是类型系统的根基信息,必须稳定可靠,所以被 sealed 锁死。需要自定义”种类”请用特性或字段。

小结

object 把”万物同源”落到代码里。它的方法大多与”值如何比较、如何展示”有关,下一章我们聊值类型转 object 时发生的装箱与拆箱。