C#基础(六):类的四大特性

封装、继承、多态、抽象是面向对象编程(OOP)的四大核心支柱,C# 作为纯面向对象语言,从语法和运行时层面完整支持这四大特性。它们共同支撑了代码的可复用性、可扩展性、安全性与可维护性。

一、封装(Encapsulation)

核心本质

封装是指将对象的状态数据(字段)和操作数据的行为(方法)捆绑在一起,隐藏内部实现细节,仅对外暴露必要的访问接口,通过访问权限控制保护内部状态的安全性。

简单说就是:该藏的藏起来,该露的露出来

核心价值

  1. 数据安全:防止外部随意修改内部状态,避免非法值
  2. 降低耦合:外部只依赖公开接口,内部实现修改不影响调用方
  3. 简化使用:调用方无需了解内部复杂逻辑,只需调用公开接口
  4. 可维护性:内部逻辑可自由重构,只要对外接口不变就不会破坏现有代码

实现手段:访问修饰符

C# 提供 6 种访问修饰符,精确控制成员的可见范围,这是实现封装的语法基础。

访问修饰符可见范围说明
public 完全公开,任何地方都能访问 对外暴露的接口使用
private 仅当前类内部可访问 默认访问级别,封装内部状态
protected 当前类及其子类可访问 给子类开放权限,不对外部公开
internal 同一程序集(项目)内可访问 程序集内部共享,对外隐藏
protected internal 同一程序集 OR 子类可访问 是 “或” 的关系,范围最广的非公开级别
private protected 同一程序集内的子类才可访问 是 “且” 的关系,范围最严格的保护级别

注意:类 / 结构体默认访问级别为 internal,类成员默认访问级别为 private

代码示例:从 “无封装” 到 “良好封装”

❌ 未封装的反面案例:public 字段

public class Person
{
    // 直接公开字段,外部可随意修改,完全无法控制
    public int Age;
    public string Name;
}

// 外部可以随意写入非法值
Person p = new Person();
p.Age = -100; // 年龄为负数,完全不合理,但不会有任何校验
p.Name = null;

问题:完全暴露内部状态,无法做合法性校验,后续修改字段类型或逻辑会影响所有调用方。

✅ 良好封装:私有字段 + 公开属性

public class Person
{
    // 字段私有化,外部无法直接访问
    private int _age;
    private string _name;

    // 公开属性作为访问入口,内置校验逻辑
    public int Age
    {
        get => _age;
        set
        {
            // 封装校验逻辑,保证数据合法性
            if (value < 0 || value > 150)
                throw new ArgumentOutOfRangeException(nameof(value), "年龄必须在 0-150 之间");
            _age = value;
        }
    }

    public string Name
    {
        get => _name;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("姓名不能为空");
            _name = value;
        }
    }

    // 公开方法作为行为接口,内部实现可自由修改
    public void GrowOlder()
    {
        _age++;
        // 内部可以做更多复杂逻辑,外部无感知
    }
}

优势:外部只能通过属性和方法交互,内部校验逻辑、存储方式可以随时重构,只要接口不变就不会影响调用方。

封装的粒度控制

  • 完全隐藏:private 字段、私有辅助方法,仅内部使用
  • 只读暴露:只有 get 没有 set,外部只能读取不能修改
  • 只写暴露:只有 set 没有 get(极少使用)
  • 分级开放:protected 给子类开放,internal 给同程序集开放

最佳实践与坑点

  1. 字段一律私有:99% 的场景不要声明 public 字段,统一用属性对外暴露
  2. 最小可见原则:成员能 private 就 private,能 internal 就不 public
  3. 不要过度封装:简单的数据传输对象(DTO/POCO)可以使用自动属性,不必强行写复杂校验
  4. 属性本质是方法:属性内不要写耗时逻辑,避免调用方误以为是简单字段访问

二、继承(Inheritance)

核心本质

继承是基于现有类(基类 / 父类)创建新类(派生类 / 子类)的机制,子类可以复用父类的字段、属性和方法,同时可以扩展自己的新功能,实现 is-a 的关系。

继承的核心价值是代码复用层次化分类

C# 继承核心规则

  1. 单继承:一个类只能有一个直接父类,不支持多继承(多继承通过接口实现)
  2. 传递性:继承具有传递性,C 继承 B,B 继承 A → C 间接拥有 A 的所有可访问成员
  3. 万物皆 object:所有类最终都隐式继承自 System.Object,因此所有对象都拥有 Equals()GetHashCode()ToString()GetType() 四个基础方法
  4. 派生类扩展不影响基类:子类新增成员不会改变父类,也不能删除父类成员
  5. 构造函数不可继承:父类的构造函数不会被子类继承,子类必须定义自己的构造函数,但可以通过 base 调用父类构造

基础语法与关键字

// 基类(父类)
public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void Eat()
    {
        Console.WriteLine($"{Name} 在吃东西");
    }

    // 虚方法,留待子类重写(多态部分详解)
    public virtual void MakeSound()
    {
        Console.WriteLine("动物发出叫声");
    }
}

// 派生类(子类):使用 : 继承父类
public class Dog : Animal
{
    // 子类扩展自己的成员
    public string Breed { get; set; }

    public void Run()
    {
        Console.WriteLine($"{Name} 在奔跑");
    }

    // 重写父类虚方法
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 汪汪叫");
    }
}

// 使用
Dog dog = new Dog();
dog.Name = "旺财";
dog.Eat();      // 复用父类方法
dog.MakeSound();// 调用子类重写后的方法
dog.Run();      // 子类自有方法

base 关键字:访问基类成员

base 用于在子类中显式访问父类的成员,主要有两个用途:

  1. 调用父类的构造函数
  2. 调用父类被重写的方法
public class Dog : Animal
{
    public string Breed { get; set; }

    // 1. 调用父类构造函数
    public Dog(string name, int age, string breed) : base(name, age)
    {
        Breed = breed;
    }

    public override void MakeSound()
    {
        // 2. 先调用父类的实现,再扩展子类逻辑
        base.MakeSound();
        Console.WriteLine("汪汪汪!");
    }
}

构造函数执行顺序

继承场景下构造函数的执行顺序是高频考点,遵循「静态先子类后父类,实例先父类后子类」的原则:

  1. 执行子类静态构造函数
  2. 执行父类静态构造函数
  3. 子类实例字段初始化
  4. 执行父类实例构造函数
  5. 执行子类实例构造函数体
public class Base
{
    public Base() { Console.WriteLine("父类实例构造"); }
    static Base() { Console.WriteLine("父类静态构造"); }
}

public class Derived : Base
{
    public Derived() { Console.WriteLine("子类实例构造"); }
    static Derived() { Console.WriteLine("子类静态构造"); }
}

// 首次 new Derived() 输出顺序:
// 子类静态构造
// 父类静态构造
// 父类实例构造
// 子类实例构造

protected 访问修饰符

专门为继承设计,成员在父类和子类中可访问,对外部不可见,是封装与继承的平衡。

public class Animal
{
    protected string _internalState; // 受保护字段
}

public class Cat : Animal
{
    public void Test()
    {
        _internalState = "正常"; // 子类内部可以访问
    }
}

// 外部代码无法访问 _internalState

方法隐藏:new 关键字

子类可以声明与父类同名的方法,默认会有编译器警告;使用 new 关键字可以显式隐藏父类方法,注意这不是多态

密封类与密封方法:sealed

  • sealed class:禁止被其他类继承,如 stringint 对应的运行时类都是密封类
  • sealed override:密封重写方法,禁止子类继续重写该虚方法
// 密封类:无法被继承
public sealed class FinalClass { }

// 密封方法:重写后禁止子类继续重写
public class Child : Parent
{
    public sealed override void VirtualMethod()
    {
        base.VirtualMethod();
    }
}

里氏替换原则(LSP)

面向对象设计七大原则之一,核心含义:所有使用父类的地方,都可以透明地替换为子类,而不会产生逻辑错误

  • 子类必须实现父类的所有契约
  • 子类不能改变父类方法的输入输出约定
  • 子类只能扩展功能,不能缩小父类的能力

里氏替换原则是继承正确使用的指导原则,也是多态能够正常工作的前提。

最佳实践与坑点

  1. 优先组合,慎用继承:继承是强耦合,父类修改会影响所有子类;代码复用优先考虑组合而非继承
  2. 继承层次不要过深:一般不超过 3 层,过深的继承会导致理解困难、维护成本高
  3. 不要为了复用而强行继承:没有 is-a 关系不要滥用继承,比如 “工具类继承数据类”
  4. 父类显式声明无参构造:如果父类只有有参构造,子类必须显式调用 base(参数),否则编译报错
  5. 不要滥用 protected:protected 成员也是公开接口的一部分,修改会影响所有子类

三、多态(Polymorphism)

核心本质

多态字面意思是 “多种形态”,指同一个操作作用于不同的对象,可以产生完全不同的执行结果。简单说就是:一个接口,多种实现

多态是面向对象最核心的特性,它让程序可以面向抽象 / 父类编程,而不用关心具体实现,极大提升了代码的扩展性。

分类:编译时多态 vs 运行时多态

1. 编译时多态(静态多态)

在编译阶段就确定了调用哪个版本的方法,通过方法重载运算符重载实现。

方法重载

同一个类中,方法名相同,但参数列表不同(参数类型、个数、顺序至少有一个不同),与返回值无关。

public class Calculator
{
    // 重载1:两个int
    public int Add(int a, int b) => a + b;

    // 重载2:三个int
    public int Add(int a, int b, int c) => a + b + c;

    // 重载3:两个double
    public double Add(double a, double b) => a + b;
}

// 编译时根据参数自动匹配对应方法
Calculator calc = new Calculator();
calc.Add(1, 2);       // 调用重载1
calc.Add(1.5, 2.5);   // 调用重载3

特点:调用哪个版本在编译时就已确定,性能高,灵活性低。

2. 运行时多态(动态多态)

程序运行时,根据对象的实际类型动态决定调用哪个方法,通过 virtual / override 实现,这是真正意义上的多态。

核心关键字

  • virtual:修饰父类方法,表示该方法是虚方法,子类可以重写
  • override:修饰子类方法,表示重写父类的虚方法
// 基类
public class Shape
{
    // 虚方法:提供默认实现,子类可重写
    public virtual double GetArea()
    {
        return 0;
    }
}

// 子类1:圆形
public class Circle : Shape
{
    public double Radius { get; set; }

    // 重写虚方法,提供圆形自己的实现
    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

// 子类2:矩形
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double GetArea()
    {
        return Width * Height;
    }
}

// 多态演示
// 父类引用可以指向任意子类对象
Shape shape1 = new Circle { Radius = 5 };
Shape shape2 = new Rectangle { Width = 4, Height = 3 };

Console.WriteLine(shape1.GetArea()); // 78.54,调用Circle的实现
Console.WriteLine(shape2.GetArea()); // 12,调用Rectangle的实现

核心特点:变量声明为父类类型,但实际调用的是对象真实类型的方法,这就是多态的精髓。

底层原理:虚方法表(vtable)

运行时多态的底层依靠虚方法表(Virtual Method Table, vtable) 实现:

  1. 每个包含虚方法的类,都会生成一张虚方法表,存储该类所有虚方法的实际地址
  2. 每个对象实例都有一个隐藏的「类型对象指针」,指向自己所属类的 vtable
  3. 调用虚方法时,运行时通过对象的类型指针找到对应 vtable,查找方法地址并执行
  4. 子类重写了方法,vtable 中对应位置就会替换为子类方法的地址

这就是为什么父类引用指向子类对象时,会调用子类方法 —— 因为对象的实际类型决定了 vtable。

核心对比:override 重写 vs new 隐藏

这是面试高频考点,也是最容易混淆的知识点,二者行为完全不同:

public class Base
{
    public virtual void VirtualMethod() => Console.WriteLine("父类虚方法");
    public void NormalMethod() => Console.WriteLine("父类普通方法");
}

public class Derived : Base
{
    // 重写虚方法
    public override void VirtualMethod() => Console.WriteLine("子类重写方法");
    // 隐藏普通方法
    public new void NormalMethod() => Console.WriteLine("子类隐藏方法");
}

// 测试
Base obj = new Derived(); // 父类引用,指向子类对象

obj.VirtualMethod(); // 输出:子类重写方法(多态,按实际类型调用)
obj.NormalMethod();  // 输出:父类普通方法(无多态,按声明类型调用)

多态的核心价值

  1. 极强的扩展性:新增子类时,调用父类的代码完全不需要修改,符合开闭原则
  2. 面向抽象编程:上层业务只依赖抽象父类 / 接口,不依赖具体实现,降低耦合
  3. 统一接口,多种实现:对外提供统一的调用方式,内部实现可自由替换

最佳实践与坑点

  1. 需要扩展的方法才标记 virtual:不要把所有方法都设为虚方法,会有轻微性能损耗,且破坏封装
  2. 重写方法时保持语义一致:子类重写不能改变方法的约定,违反里氏替换原则
  3. 不要在构造函数中调用虚方法:父类构造执行时,子类字段还未初始化,可能导致空引用等异常
  4. 重写时调用 base 不是必须的:根据业务决定是否保留父类逻辑,不要盲目加 base.Method()

四、抽象(Abstraction)

核心本质

抽象是指提取事物的本质特征与核心行为,隐藏非必要的实现细节,只对外展示关键契约。抽象关注 “能做什么”,而不关心 “怎么做”。

封装是隐藏内部状态,抽象是隐藏实现细节,二者相辅相成。C# 中实现抽象的两种主要方式:抽象类接口

接口会在后面单独详解,本节重点讲解抽象类与抽象方法。

抽象类(abstract class)

abstract 修饰的类,是一种不完整的类,只能作为基类被继承,不能直接实例化。抽象类可以包含抽象成员(无实现)和普通成员(有实现)。

核心特点

  1. 不能实例化:不能直接 new 抽象类,只能创建其子类的实例
  2. 可以包含抽象成员:抽象方法、抽象属性,只有声明没有实现
  3. 可以包含普通成员:字段、属性、方法、构造函数都可以有实现
  4. 子类必须实现所有抽象成员:除非子类也是抽象类
  5. 不能是密封类:抽象类天生就是用来被继承的,不能用 sealed 修饰

抽象方法

abstract 修饰的方法,只有方法签名,没有方法体,强制子类必须重写实现。

  • 抽象方法默认就是 virtual 的,不能再加 virtual
  • 抽象方法只能存在于抽象类中
  • 子类必须用 override 实现所有抽象方法
// 抽象基类:图形
public abstract class Shape
{
    // 抽象属性:无实现,子类必须重写
    public abstract string Name { get; }

    // 抽象方法:计算面积,只有签名,没有实现
    public abstract double GetArea();

    // 普通方法:有默认实现,子类可直接复用
    public void PrintInfo()
    {
        Console.WriteLine($"图形:{Name},面积:{GetArea():F2}");
    }
}

// 子类1:圆形
public class Circle : Shape
{
    public double Radius { get; set; }

    // 必须实现所有抽象成员
    public override string Name => "圆形";

    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

// 子类2:矩形
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override string Name => "矩形";

    public override double GetArea()
    {
        return Width * Height;
    }
}

// 使用
Shape circle = new Circle { Radius = 5 };
Shape rect = new Rectangle { Width = 4, Height = 3 };
circle.PrintInfo(); // 输出:图形:圆形,面积:78.54
rect.PrintInfo();   // 输出:图形:矩形,面积:12.00

抽象类的设计思想:把所有子类共有的逻辑提取到基类实现,把每个子类不同的部分声明为抽象成员,强制子类各自实现,既复用了代码,又定义了统一契约。

抽象方法 vs 虚方法

对比维度抽象方法 abstract虚方法 virtual
实现体 没有实现体 有默认实现体
所在类 只能在抽象类中 可以在普通类、抽象类中
子类要求 必须重写 可以选择重写,也可以不重写
多态性 天生支持多态 支持多态
作用 强制子类实现契约 提供默认实现,允许子类扩展

抽象类 vs 普通类

对比维度抽象类普通类
实例化 不能实例化 可以实例化
抽象成员 可以包含 不能包含
继承 只能作为基类 可以被继承也可以独立使用
作用 定义统一契约 + 复用公共代码 完整的功能实现

抽象的核心价值

  1. 定义统一契约:强制所有子类遵循相同的接口规范,保证一致性
  2. 分离关注点:上层业务只依赖抽象,不用关心具体实现细节
  3. 支撑多态:抽象是多态的基础,没有抽象就没有真正的多态
  4. 代码复用:公共逻辑放在抽象基类,避免子类重复编写

最佳实践与坑点

  1. 抽象类命名建议加 Base 后缀:如 ShapeBaseControllerBase,清晰表明是抽象基类
  2. 抽象类不要包含太多公共逻辑:避免基类过于臃肿,违反单一职责
  3. 行为契约优先用接口:如果只是定义行为规范,没有公共代码需要复用,优先使用接口而非抽象类
  4. 不要为了抽象而抽象:不需要多扩展的场景不要强行引入抽象层,增加不必要的复杂度

四大特性总结

  • 封装是基础:藏起细节,保护数据,对外提供安全的访问入口
  • 继承是手段:复用代码,构建层次体系,实现 is-a 关系
  • 多态是核心:一个接口多种实现,让程序灵活可扩展
  • 抽象是灵魂:提取本质,定义契约,面向接口编程

四者不是孤立的:封装是前提,继承是载体,抽象是指导,多态是结果,共同构成了面向对象编程的完整体系。

posted @ 2026-07-22 13:53  挺秃然的i  阅读(5)  评论(0)    收藏  举报