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}");
    }
}

构造函数执行顺序(继承场景)

  1. 执行子类静态构造函数
  2. 执行父类静态构造函数
  3. 子类实例字段初始化
  4. 执行父类实例构造函数
  5. 执行子类实例构造函数体

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 { }

典型例子:stringint 等基元类型对应的类都是密封类。

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}");
    }
}
适用场景:
  1. 生成代码 + 手写代码分离(如 WinForm、EF 实体模型)
  2. 大型类按功能拆分到不同文件,方便多人协作注意:所有部分必须都用 partial 修饰,访问修饰符必须一致。

3.4 文件局部类(C# 11.0+)

file 修饰,类型仅在当前源代码文件内可见,用于限制类型作用域,避免命名冲突。

// 仅当前文件可用,其他文件无法访问
file class FileOnlyHelper
{
    public static void HelperMethod() { }
}

四、 析构函数(终结器)

析构函数是类的特殊方法,用于释放非托管资源,语法为 ~类名(),由 GC 在回收对象前自动调用。

public class UnmanagedResource
{
    // 析构函数:无修饰符、无参数、不能手动调用
    ~UnmanagedResource()
    {
        // 释放非托管资源(如文件句柄、数据库连接、COM对象等)
        Console.WriteLine("析构函数执行");
    }
}

核心注意事项

  1. 析构函数执行时机不确定,由 GC 决定,无法精准控制
  2. 析构函数会增加 GC 开销(对象会进入终结器队列,延迟回收)
  3. 托管资源由 GC 管理,不需要写析构函数
  4. 推荐做法:实现 IDisposable 接口,配合 using 语句主动释放资源,同时提供析构函数作为兜底

五、对象初始化器

创建对象时直接给属性赋值,无需写多个构造函数重载。

// 等价于先new,再逐个给属性赋值
var person = new Person
{
    Name = "张三",
    Age = 25
};

六、常见坑点与最佳实践

最佳实践

  1. 封装优先:字段一律设为 private,通过属性 / 方法对外暴露,不要暴露 public 字段
  2. 合理拆分职责:遵循单一职责原则,一个类只做一件事,避免 “上帝类”
  3. 构造函数保证对象有效:构造完成后对象应处于合法状态,必要的参数通过构造函数传入,避免 “半初始化” 对象
  4. 静态成员慎用:静态成员生命周期和进程一致,容易导致内存泄漏和线程安全问题
  5. 工具类用静态类:纯工具方法的类声明为 static,禁止实例化
  6. 优先用自动属性:不需要自定义逻辑时用自动属性,简洁且易维护

常见坑点

  1. 默认构造函数丢失:一旦自定义了有参构造,编译器不再生成默认无参构造,需要手动添加
  2. 静态构造函数死锁:静态构造函数中不要等待其他线程,也不要触发循环依赖,容易导致类型加载死锁
  3. 引用类型参数误解:类作为参数默认是值传递引用副本,方法内重新 new 赋值不会影响原变量
  4. 分部类跨程序集无效:partial 只能在同一个程序集内拆分,不能跨程序集合并
  5. 析构函数滥用:不要随便写析构函数,只有持有非托管资源时才需要,否则会增加 GC 负担
posted @ 2026-07-09 15:14  挺秃然的i  阅读(8)  评论(0)    收藏  举报