C#基础(五):类(Class)
类是 C# 面向对象编程的核心载体,是一种引用类型的自定义数据结构,用于封装数据(字段 / 属性)与行为(方法 / 事件),支持继承、多态、封装三大面向对象特性。
一、类的本质与内存分配
核心定义
类是引用类型的模板,通过 new 关键字创建实例(对象):
- 实例数据存储在托管堆上
- 变量中存储的是堆对象的内存地址(引用)
- 多个变量可以指向同一个堆对象,修改会相互影响
- 所有类隐式继承自
System.Object
代码示例:引用类型赋值特性
public class Person { public string Name; public int Age; } // 测试引用赋值 Person p1 = new Person(); p1.Name = "张三"; p1.Age = 20; Person p2 = p1; // 仅复制引用地址,指向同一个堆对象 p2.Name = "李四"; Console.WriteLine(p1.Name); // 输出:李四,p1和p2指向同一个对象 Console.WriteLine(object.ReferenceEquals(p1, p2)); // true
类 vs 结构体(核心选型依据)
| 维度 | 类(class) | 结构体(struct) |
|---|---|---|
| 类型分类 | 引用类型 | 值类型 |
| 内存分配 | 托管堆 | 栈(装箱除外) |
| 赋值行为 | 复制引用 | 复制整个值 |
| 默认值 | null | 零值 |
| 继承支持 | 支持单继承 | 不支持继承 |
| 析构函数 | 支持 | 不支持 |
| 适用场景 | 复杂对象、有生命周期、需要继承 | 轻量数据、小型值对象 |
选型原则:如果类型的核心职责是存储数据,且实例较小(≤16 字节)、生命周期短、不需要继承,优先用 struct;否则用 class。
二、 类的成员全解
类的成员分为两大类:实例成员(属于具体对象,每个实例独立一份)、静态成员(属于类本身,全局共享一份)。
2.1 字段(Field)
字段是类中直接声明的变量,用于存储对象的状态数据。
分类与修饰符
public class User { // 公共实例字段(不推荐,破坏封装,建议用属性) public string? PublicField; // 私有实例字段(最常用,封装内部状态) // 命名约定:驼峰+下划线前缀 _camelCase private int _age; // 只读实例字段:只能在声明或构造函数中赋值 private readonly int _id; // 静态字段:属于类,所有实例共享 private static int _totalCount; // 静态只读字段:运行时常量 public static readonly string AppName = "MyApp"; // 常量:编译时常量,隐式静态 public const int MaxAge = 150; }
核心规则
- 字段有默认值:数值为 0、引用类型为 null、布尔为 false,和局部变量 “必须手动赋值” 不同
- 字段初始化顺序:声明时初始化 → 构造函数赋值
- 最佳实践:绝大多数场景不要暴露 public 字段,通过属性对外暴露数据,保证封装性
2.2 属性(Property)
属性是字段的封装层,本质是 get / set 访问器方法的语法糖,编译后会生成 get_XXX() 和 set_XXX(value) 两个方法,对外表现成 “像变量一样使用”。
2.2.1 自动属性(最常用)
编译器自动生成后台字段(backing field),无需手动写字段。
public class Product { // 读写自动属性:编译器自动生成私有后台字段 public string Name { get; set; } // 只读自动属性:只能在构造函数中赋值 public int Id { get; } // 带访问修饰符:公开读,私有写 public decimal Price { get; private set; } // init 只读属性(C# 9.0+):对象初始化时可赋值,之后只读 public string Category { get; init; } } // init 属性使用 var product = new Product { Category = "电子产品" }; // product.Category = "食品"; // 编译报错,初始化后不可修改
2.2.1 完整属性(带自定义后台字段)
需要在 get/set 中加自定义逻辑(如校验、通知、懒加载)时使用。
public class Person { // 私有后台字段 private int _age; // 完整属性 public int Age { get => _age; set { // 自定义校验逻辑 if (value < 0 || value > 150) throw new ArgumentOutOfRangeException(nameof(value), "年龄不合法"); _age = value; } } }
2.2.1 计算属性(无后台字段)
get 中通过计算返回结果,不存储数据。
public class Rectangle { public double Width { get; set; } public double Height { get; set; } // 计算属性:每次访问都重新计算 public double Area => Width * Height; // 表达式体属性,等效于: // public double Area { get { return Width * Height; } } }
2.2.1 属性的本质(编译后)
// 你写的代码 public string Name { get; set; } // 编译器生成的等效代码 private string <Name>k__BackingField; public string get_Name() => <Name>k__BackingField; public void set_Name(string value) => <Name>k__BackingField = value;
因此,属性本质是方法,支持多态、抽象、重写,这也是它和字段的核心区别。
2.3 构造函数(Constructor)
构造函数是创建实例时自动执行的特殊方法,用于初始化对象状态,方法名和类名相同,无返回值。
2.3.1 实例构造函数
public class Person { public string Name { get; } public int Age { get; } // 无参构造函数 public Person() { Name = "未知"; Age = 0; } // 有参构造函数:重载 public Person(string name, int age) { Name = name; Age = age; } }
2.3.2 构造函数链:this 调用
通过 this() 在一个构造函数中调用同类的其他构造函数,减少代码重复。
public class Person { public string Name { get; } public int Age { get; } public string Address { get; } public Person() : this("未知", 0) // 调用双参构造 { } public Person(string name, int age) : this(name, age, "未知地址") // 调用三参构造 { } public Person(string name, int age, string address) { Name = name; Age = age; Address = address; } }
2.3.3 静态构造函数
用 static 修饰,用于初始化静态成员,整个程序生命周期只执行一次,由 CLR 在首次访问类之前自动调用。
public class AppConfig { public static string ConfigPath { get; } // 静态构造函数:无访问修饰符、无参数 static AppConfig() { ConfigPath = Path.Combine(AppContext.BaseDirectory, "config.json"); Console.WriteLine("静态构造函数执行了"); } }
注意:静态构造函数不能手动调用,CLR 保证线程安全且只执行一次;不要在静态构造函数中写耗时或可能死锁的代码。
2.3.4 私有构造函数
构造函数设为 private,外部无法直接 new 实例,常用于单例模式、静态工具类。
public class Singleton { // 私有构造函数:禁止外部new private Singleton() { } // 全局唯一实例 public static Singleton Instance { get; } = new Singleton(); }
2.3.5 主构造函数(C# 12.0+)
简化构造函数写法,参数直接在类名后声明,可在类成员中直接使用。
// 主构造函数参数 name、age 可在类内直接使用 public class Person(string name, int age) { // 直接使用主构造参数初始化属性 public string Name { get; set; } = name; public int Age { get; } = age; public void PrintInfo() { Console.WriteLine($"姓名:{Name},年龄:{Age}"); } }
构造函数执行顺序(继承场景)
- 执行子类静态构造函数
- 执行父类静态构造函数
- 子类实例字段初始化
- 执行父类实例构造函数
- 执行子类实例构造函数体
2.4 方法(Method)
方法是类的行为成员,用于封装可复用的逻辑。
public class Calculator { // 实例方法:通过对象调用,可访问实例成员 public int Add(int a, int b) => a + b; // 静态方法:通过类名调用,只能访问静态成员 public static double PI => Math.PI; public static double CircleArea(double radius) => PI * radius * radius; // 表达式体方法:单行逻辑简写 public int Multiply(int a, int b) => a * b; // 方法重载:方法名相同,参数列表不同(类型/个数/顺序) public double Add(double a, double b) => a + b; }
参数传递进阶
对于引用类型的参数,默认是值传递(传递引用的副本):
- 方法内修改对象的属性 / 字段,会影响原对象(因为指向同一个堆对象)
- 方法内重新给参数赋值 new 新对象,不会影响原变量(因为只是修改了副本)
加 ref 后变为引用传递,方法内重新赋值会修改原变量的指向。
public void ChangePerson(Person p) { p.Name = "新名字"; // 会影响原对象 p = new Person(); // 不会影响原变量,只是修改了副本 } public void ChangePersonRef(ref Person p) { p = new Person(); // 会修改原变量的指向 }
2.5 索引器(Indexer)
让类可以像数组一样通过 [索引] 访问元素,是集合类的核心特性,使用 this 关键字定义。
public class StringCollection { private string[] _items = new string[10]; // 索引器定义 public string this[int index] { get => _items[index]; set => _items[index] = value; } } // 使用 var col = new StringCollection(); col[0] = "hello"; Console.WriteLine(col[0]);
索引器支持重载,支持多个参数(如二维矩阵的 [行,列])。
2.5 嵌套类
在一个类内部定义另一个类,称为嵌套类,默认访问级别为 private,仅外层类可访问。
public class OuterClass { // 私有嵌套类,仅OuterClass内部可用 private class InnerHelper { public void DoWork() { } } public void Execute() { var helper = new InnerHelper(); // 内部可正常使用 helper.DoWork(); } } // 外部无法访问 InnerHelper
适用场景:辅助类仅为外层类服务,不需要对外暴露时使用,提升封装性。
三、特殊类类型
3.1 静态类(static class)
用 static 修饰的类,不能实例化、不能继承、只能包含静态成员,常用于工具类、扩展方法容器。
public static class StringUtils { // 只能有静态成员 public static bool IsNullOrEmpty(string? str) => string.IsNullOrEmpty(str); // 不能有实例构造函数 // 不能声明实例字段/属性/方法 } // 使用 bool empty = StringUtils.IsNullOrEmpty("");
3.2 密封类(sealed class)
用 sealed 修饰,禁止被其他类继承,常用于不希望被扩展的类型,同时有微小的性能提升(CLR 可优化虚方法调用)。
public sealed class FinalClass { // 无法被继承 } // 编译报错:无法从密封类派生 // public class SubClass : FinalClass { }
典型例子:
string、int等基元类型对应的类都是密封类。
3.3 分部类(partial class)
用 partial 修饰,将同一个类的代码拆分到多个文件中,编译器编译时会合并成一个类。
// 文件1:Person.cs public partial class Person { public string Name { get; set; } } // 文件2:Person.Methods.cs public partial class Person { public void SayHello() { Console.WriteLine($"Hello, I'm {Name}"); } }
适用场景:
-
生成代码 + 手写代码分离(如 WinForm、EF 实体模型)
-
大型类按功能拆分到不同文件,方便多人协作注意:所有部分必须都用 partial 修饰,访问修饰符必须一致。
3.4 文件局部类(C# 11.0+)
用 file 修饰,类型仅在当前源代码文件内可见,用于限制类型作用域,避免命名冲突。
// 仅当前文件可用,其他文件无法访问 file class FileOnlyHelper { public static void HelperMethod() { } }
四、 析构函数(终结器)
析构函数是类的特殊方法,用于释放非托管资源,语法为 ~类名(),由 GC 在回收对象前自动调用。
public class UnmanagedResource { // 析构函数:无修饰符、无参数、不能手动调用 ~UnmanagedResource() { // 释放非托管资源(如文件句柄、数据库连接、COM对象等) Console.WriteLine("析构函数执行"); } }
核心注意事项
- 析构函数执行时机不确定,由 GC 决定,无法精准控制
- 析构函数会增加 GC 开销(对象会进入终结器队列,延迟回收)
- 托管资源由 GC 管理,不需要写析构函数
- 推荐做法:实现
IDisposable接口,配合using语句主动释放资源,同时提供析构函数作为兜底
五、对象初始化器
创建对象时直接给属性赋值,无需写多个构造函数重载。
// 等价于先new,再逐个给属性赋值 var person = new Person { Name = "张三", Age = 25 };
六、常见坑点与最佳实践
最佳实践
- 封装优先:字段一律设为 private,通过属性 / 方法对外暴露,不要暴露 public 字段
- 合理拆分职责:遵循单一职责原则,一个类只做一件事,避免 “上帝类”
- 构造函数保证对象有效:构造完成后对象应处于合法状态,必要的参数通过构造函数传入,避免 “半初始化” 对象
- 静态成员慎用:静态成员生命周期和进程一致,容易导致内存泄漏和线程安全问题
- 工具类用静态类:纯工具方法的类声明为 static,禁止实例化
- 优先用自动属性:不需要自定义逻辑时用自动属性,简洁且易维护
常见坑点
- 默认构造函数丢失:一旦自定义了有参构造,编译器不再生成默认无参构造,需要手动添加
- 静态构造函数死锁:静态构造函数中不要等待其他线程,也不要触发循环依赖,容易导致类型加载死锁
- 引用类型参数误解:类作为参数默认是值传递引用副本,方法内重新 new 赋值不会影响原变量
- 分部类跨程序集无效:partial 只能在同一个程序集内拆分,不能跨程序集合并
- 析构函数滥用:不要随便写析构函数,只有持有非托管资源时才需要,否则会增加 GC 负担

浙公网安备 33010602011771号