类是你自己定义的类型:它把数据(字段)和操作这些数据的代码(方法)组合在一起。对象是类的一个实例,在运行时用 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() 返回的就是它。