Menu

C# 类和对象:字段、方法、new 与引用

如何在 C# 中声明类、为它添加字段和方法、用 new 创建对象,以及类是引用类型意味着什么:共享的对象、null 与 NullReferenceException、ToString 和对象初始化器。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

类是你自己定义的类型:它把数据(字段)和操作这些数据的代码(方法)组合在一起。对象是类的一个实例,在运行时用 new 创建。一个类可以有很多对象,每个对象都有自己的值。

声明类并创建对象

类的声明在大括号里列出它的成员。字段保存数据,方法实现行为。new ClassName() 会分配一个对象,并给你一个指向它的引用。

输出:

Maya: 120.50
Omar: 40
Withdraw 200 succeeded: False

每个对象都有自己的 Owner 和 Balance。在方法内部,Balance 的意思是“调用这个方法的那个对象的余额”,所以 maya.Deposit 和 omar.Deposit 改变的是不同的数字。需要明确指代这个隐含的对象时,可以用 this。

没有赋值的字段从默认值开始:数字是 0,bool 是 false,字符串和其他类类型是 null。

这里的成员都是 public,这样 Main 才能访问它们。实际的类通常把字段设为 private,改为通过属性对外公开,具体规则见访问修饰符页面。类还可以声明构造函数,在创建对象时设置字段,详见构造函数页面。

类是引用类型

类类型的变量并不包含对象本身,而是包含一个引用,指向存放在堆上的对象。给变量赋值复制的是引用,所以两个变量可以指向同一个对象。

输出:

5
6
True
False

这里体现了三个结果:

  • 通过 second 修改对象,通过 first 也能看到变化,因为只有一个对象。
  • 把对象传给方法时传的是引用。方法可以修改对象的字段,调用方也能看到。(在方法内部给参数赋一个新对象不会影响调用方的变量;那需要用 ref。)
  • 对两个类实例使用 ==,默认比较的是引用:判断的是“是不是同一个对象?”,而不是“值是否相同?”。lookalike 的数据完全相同,但仍然是另一个对象。

如果你希望赋值时复制、比较时按值比较,可以看看结构体(它是值类型),或者按值比较的 record。

null 与 NullReferenceException

引用变量可以保存 null,表示“没有对象”。通过 null 访问成员会抛出 NullReferenceException,这是 C# 代码中最常见的运行时异常。

输出:

True
no city on file
NullReferenceException: Address was never set
Lisbon

解决办法几乎从来不是捕获这个异常。要找出引用为什么是 null,然后要么提前创建对象,要么在使用前检查,要么使用 null 条件运算符 ?.,它会停下来返回 null,而不是抛出异常。再配合 ?? 提供一个备用值。这两个运算符在 可空类型 中有详细讲解。

重写 ToString

每个类都继承自 object,而 object 提供了 ToString()。继承来的版本返回类型的完整名称(如果有命名空间,也包括命名空间),这很少是你想打印的内容。重写它:

输出:

Plain
Mug (8.50)
In cart: Mug (8.50)
Total line: Mug (8.50)

Console.WriteLine、字符串插值以及字符串的 + 拼接都会自动调用 ToString()。调试器也会显示它,所以即使你自己从不打印这个对象,重写它也很有用。

对象初始化器

对象初始化器在对象构造完成后,立即在同一个表达式中设置公共字段和属性。它比一串赋值语句更易读,在集合字面量中也很好用。

输出:

A12: 25 (0 extras)
A13: 40 (2 extras)

第二张票有两个细节。Price = 40m 覆盖了字段初始值 25m,因为对象初始化器在对象构建之后才运行。Extras = { "Parking", "Poster" } 没有 new:它是对字段中已有的列表调用 Add,而不是替换这个列表。

对象初始化器不是构造函数。它只能设置调用代码能访问到的成员,也无法强制某个成员一定被设置。如果缺少某些值对象就无效,请在构造函数中要求提供这些值。

分部类

partial 关键字可以把一个类拆分到多个文件中。编译器会把这些部分合并成一个类。

// Invoice.cs
public partial class Invoice
{
    public decimal Subtotal;
    public decimal Total() => Subtotal + Tax();
}

// Invoice.Tax.cs
public partial class Invoice
{
    private decimal Tax() => Subtotal * 0.2m;
}

每个部分都必须写 partial,并且位于同一个命名空间和同一个项目中。常见用途是生成的代码:WinForms 设计器、源代码生成器和一些 ORM 会写其中一部分,另一部分留给你,这样重新生成代码时永远不会覆盖你的代码。仅仅为了让一个手写的类变短而把它拆到多个文件里,通常会掩盖一个本该拆成两个类的类。

常见错误

  • 在创建对象之前就使用它。 类类型的字段在你给它赋值 new ... 之前都是 null。第一次访问成员就会抛出 NullReferenceException。
  • 以为赋值会复制对象。 对类来说,var b = a; 只是给同一个对象起了第二个名字。需要独立的对象时,请创建新对象并复制值。
  • 用 == 比较对象,却期望按值比较。 对类来说它比较的是引用,除非类重载了 ==(string 就是这样)。请比较相关字段,或者重写 Equals。
  • 打印对象却得到类型名。 重写 ToString()。
  • 把所有成员都声明为 public。 在小示例里没问题,但它让任何代码都能把对象置于无效状态,比如负数余额。把字段设为私有,通过会检查规则的方法来修改它们。

常见问题

C# 中的类是什么?

类是你自己定义的类型:一组有名字的数据(字段和属性)和行为(方法)。class Account { public decimal Balance; public void Deposit(decimal amount) { Balance += amount; } } 就声明了一个类。类是一份蓝图,在你用 new 从它创建对象之前,它什么也不做。

C# 中类和对象有什么区别?

类是定义,在源代码中只写一次。对象是它的一个实例,在运行时用 new 创建,每个实例字段都有自己的一份。两次 new Account() 会得到同一个类的两个对象,它们的余额互不影响。

C# 中如何创建对象?

使用 new,后面跟类名和构造函数参数:var account = new Account();。你可以用对象初始化器在同一个表达式里设置公共字段和属性:new Account { Owner = "Maya" }。变量保存的是指向新对象的引用,而不是对象本身。

C# 中为什么会出现 NullReferenceException?

你通过一个值为 null 的变量访问了成员(.Name、.Length 或调用方法),而那里根本没有对象。常见原因有:字段从未被赋值(类类型的字段初始为 null)、查找操作返回了 null,或者方法在失败时返回了 null。可以用 if (x != null) 检查,用 x?.Name,或者确保在使用前已创建对象。

C# 中如何重写 ToString?

在类中声明 public override string ToString(),返回你想要的文本,例如 public override string ToString() => $"{Name} ({Price:F2})";。如果不重写,Console.WriteLine(obj) 和字符串插值打印的是类名,因为 object.ToString() 返回的就是它。

Coddy 编程语言插图

和 Coddy 一起学编程

开始