面向对象三板斧:封装、继承、多态,到底在“装“什么、“承“什么、“变“什么?

一句话概括: 封装是"把细节锁进黑盒,只留一个开关给外人";继承是"儿子天生继承老爹的家产,还能自己再添置新家当";多态是"同一个命令,不同的人做出不同的反应"。这三个词看似抽象,其实就是我们生活中最朴素的组织智慧------这篇文章会用生活化的类比 + 层层递进的代码案例,把它们讲得明明白白。


目录

  1. 面向对象到底在解决什么问题
  2. 封装:把复杂性锁进黑盒
  3. 继承:代码复用的"血缘关系"
  4. 多态:同一个命令,不同的反应
  5. 三者如何协同作战:一个完整的实战案例
  6. 高手都会踩的 6 个坑
  7. 面向对象 vs 面向过程:什么时候该用哪个
  8. 写在最后 & 课后练习

一、面向对象到底在解决什么问题

1.1 先看一段"面向过程"的代码

假设你要管理一个动物园,记录猫、狗、鸟各自的信息并让它们叫唤:

csharp 复制代码
string catName = "咪咪";
int catAge = 2;

string dogName = "旺财";
int dogAge = 3;

string birdName = "皮皮";
int birdAge = 1;

void CatMeow(string name) => Console.WriteLine($"{name}:喵喵喵~");
void DogBark(string name) => Console.WriteLine($"{name}:汪汪汪~");
void BirdChirp(string name) => Console.WriteLine($"{name}:啾啾啾~");

CatMeow(catName);
DogBark(dogName);
BirdChirp(birdName);

这段代码能跑,但问题很明显:

  1. 数据(catName、catAge)和行为(CatMeow)是分离的 ,全靠命名约定硬凑在一起,毫无约束力------谁能保证 DogBark 不会被误传成 catName?
  2. 动物园新增第 100 种动物,就要新增一堆变量和一个新函数,代码规模线性膨胀,毫无复用性。
  3. 如果要写一个"遍历所有动物,让它们依次叫唤"的功能,你会发现根本无从下手------因为猫、狗、鸟之间没有任何"统一的身份"。

1.2 面向对象给出的答案

面向对象编程(OOP)的核心思想:把数据和操作数据的行为,捆绑成一个个"对象",并通过封装、继承、多态这三大特性,让代码组织变得更贴近真实世界的逻辑,也更易于扩展。

这三大特性并不是三个孤立的知识点,而是层层递进、环环相扣的一套完整方法论------封装解决"如何组织单个对象",继承解决"如何复用共性",多态解决"如何优雅地处理差异性"。接下来我们逐一击破。


二、封装:把复杂性锁进黑盒

2.1 生活中的封装:一台电视机

你按遥控器的"开机"键,电视就亮了------你完全不需要知道电路板内部是怎么工作的 。这就是封装的精髓:把内部实现细节隐藏起来,只暴露一个简单、安全的操作接口给外界。

2.2 从"裸奔的数据"说起

csharp 复制代码
public class BankAccount
{
    public decimal Balance;   // ❌ 直接暴露字段,任何人都能随意修改
}

var account = new BankAccount();
account.Balance = 1000;
account.Balance = -99999;   // ❌ 外部可以把余额改成负数,业务逻辑完全没有保护!

这就是不封装的代价:外部代码可以绕过任何业务规则,直接篡改内部状态。

2.3 用属性 + 私有字段实现封装

csharp 复制代码
public class BankAccount
{
    private decimal balance;   // 私有字段:外部无法直接访问

    public decimal Balance    // 公开属性:作为唯一的访问入口
    {
        get => balance;
        private set => balance = value;   // 只允许类内部修改
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("存款金额必须大于0");

        Balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("取款金额必须大于0");
        if (amount > Balance)
            throw new InvalidOperationException("余额不足");

        Balance -= amount;
    }
}

var account = new BankAccount();
account.Deposit(1000);
account.Withdraw(300);
Console.WriteLine(account.Balance);   // 700

// account.Balance = -99999;   // ❌ 编译错误:Balance 的 set 是 private,外部无法直接赋值
account.Withdraw(99999);        // ❌ 运行时抛出异常:余额不足,业务规则生效

现在,Balance 只能通过 Deposit 和 Withdraw 这两个"受控入口"来修改,所有的业务规则(金额必须为正、余额不能为负)都被牢牢锁在类内部,外部代码不可能绕过。

2.4 四种访问修饰符:封装的"权限等级"

修饰符 谁能访问 类比
public 任何地方 商店的大门,谁都能进
private 仅本类内部 自己家的保险柜,只有自己有钥匙
protected 本类 + 派生类 家族内部资料,子女能看,外人不行
internal 仅同一程序集(项目)内 公司内部文件,外部合作方看不到

经验法则: 默认把所有字段设为 private,只在确实需要对外暴露时,才通过 public 属性开放访问,并且尽量在属性中加入校验逻辑------这是封装思维的核心习惯。

2.5 封装带来的三大好处

  1. 数据安全:内部状态不会被外部随意篡改,业务规则得到保障(如上面的余额校验)。
  2. 降低耦合 :外部代码只依赖"接口"(Deposit、Withdraw),不依赖具体实现------哪怕未来你把内部存储方式从 decimal 改成更复杂的多币种结构,只要接口不变,外部代码完全不用改。
  3. 提高可维护性:修改内部实现细节时,影响范围被严格限制在类内部,不会波及整个系统。

三、继承:代码复用的"血缘关系"

3.1 生活中的继承:儿子继承老爹的家产

儿子天生就有老爹的姓氏、部分财产,同时他还能自己打拼、添置只属于自己的新家当 。这就是继承最直观的类比------子类自动拥有父类的成员,还能在此基础上扩展自己独有的内容。

3.2 从重复代码说起

回到动物园的例子,如果我们用类来建模:

csharp 复制代码
public class Cat
{
    public string Name;
    public int Age;
    public void Eat() => Console.WriteLine($"{Name} 正在吃猫粮");
    public void Sleep() => Console.WriteLine($"{Name} 正在睡觉");
    public void Meow() => Console.WriteLine($"{Name}:喵喵喵~");
}

public class Dog
{
    public string Name;
    public int Age;
    public void Eat() => Console.WriteLine($"{Name} 正在吃狗粮");   // 逻辑几乎一样
    public void Sleep() => Console.WriteLine($"{Name} 正在睡觉");   // 完全重复!
    public void Bark() => Console.WriteLine($"{Name}:汪汪汪~");
}

Name、Age、Sleep() 这些内容在 Cat 和 Dog 里一模一样,这就是典型的"应该被继承,却被复制粘贴"的坏味道。

3.3 提取公共基类

csharp 复制代码
// 基类(父类):存放所有动物共有的属性和行为
public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void Sleep() => Console.WriteLine($"{Name} 正在睡觉");

    // virtual:允许子类重写这个方法的具体实现
    public virtual void Eat() => Console.WriteLine($"{Name} 正在进食");
}

// 派生类(子类):通过 : 继承 Animal,自动拥有 Name、Age、Sleep
public class Cat : Animal
{
    public override void Eat() => Console.WriteLine($"{Name} 正在吃猫粮");   // 重写父类方法
    public void Meow() => Console.WriteLine($"{Name}:喵喵喵~");            // 猫独有的新方法
}

public class Dog : Animal
{
    public override void Eat() => Console.WriteLine($"{Name} 正在吃狗粮");
    public void Bark() => Console.WriteLine($"{Name}:汪汪汪~");
}
csharp 复制代码
var cat = new Cat { Name = "咪咪", Age = 2 };
cat.Sleep();   // 直接继承自 Animal,不用重新写
cat.Eat();     // 咪咪 正在吃猫粮(子类重写后的版本)
cat.Meow();    // 猫自己独有的行为

Name、Age、Sleep() 只在 Animal 里写了一次,Cat 和 Dog 自动获得,且各自只需要关注自己独有的差异化逻辑(Eat 的重写、Meow/Bark)------这正是继承消除重复的价值所在。

3.4 base 关键字:调用父类的实现

csharp 复制代码
public class Dog : Animal
{
    public override void Eat()
    {
        base.Eat();   // 先执行父类原本的逻辑
        Console.WriteLine("......而且吃得特别快");   // 再补充子类自己的逻辑
    }
}

new Dog { Name = "旺财" }.Eat();
// 旺财 正在进食
// ......而且吃得特别快

base.成员名 可以让子类在"重写"的同时,仍然复用父类原有的逻辑,而不是完全推倒重来------这在很多场景下(比如子类只想"追加"逻辑而不是"替换"逻辑)非常实用。

3.5 sealed:禁止被继续继承

csharp 复制代码
public sealed class FinalReport
{
    // sealed 修饰的类,无法再被其他类继承
}

// public class ExtendedReport : FinalReport { }   // ❌ 编译错误

sealed 用于明确表达"这个类的设计已经封版,不希望被进一步扩展"------常用于工具类、性能敏感类(sealed 类的方法调用可以被编译器做一些额外优化)。

3.6 组合优于继承:一个重要的设计原则

继承虽好,但滥用继承会导致类层次结构变得极其复杂、脆弱(经典的"菱形继承问题"、"父类改一处,一堆子类全部遭殃")。现代面向对象设计中,有一条广为流传的原则:

"Favor composition over inheritance"(组合优于继承)

csharp 复制代码
// 继承方式:Duck "是一个" FlyableAnimal ------ 语义上有点牵强
public class Duck : FlyableAnimal { }

// 组合方式:Duck "拥有一个" FlyBehavior ------ 更灵活,可以在运行时切换飞行方式
public class Duck
{
    public IFlyBehavior FlyBehavior { get; set; }
    public void PerformFly() => FlyBehavior.Fly();
}

经验法则: 当子类和父类之间是清晰的"is-a"(是一个)关系时用继承(狗是一种动物);当关系更像"has-a"(拥有一个)或者需要"运行时动态切换行为"时,优先考虑组合。这是判断"该不该用继承"的黄金准则。


四、多态:同一个命令,不同的反应

4.1 生活中的多态:一声"开始表演",各显神通

主持人喊一声"开始表演",唱歌的开始唱歌,跳舞的开始跳舞,魔术师开始变魔术------同一个指令,不同的对象做出了不同的、符合自己身份的反应。这就是多态。

4.2 没有多态时的窘境

csharp 复制代码
void MakeSound(object animal)
{
    if (animal is Cat cat) cat.Meow();
    else if (animal is Dog dog) dog.Bark();
    else if (animal is Bird bird) bird.Chirp();
    // 每新增一种动物,这里就要加一个 else if ------ 典型的"开闭原则"违反者
}

这种写法有一个致命缺陷:每次系统新增一种动物类型,你都必须回来修改这个 MakeSound 方法------代码对"扩展"是封闭的,对"修改"却是开放的,正好和良好设计追求的"开闭原则(对扩展开放,对修改关闭)"背道而驰。

4.3 多态的正确打开方式

csharp 复制代码
public abstract class Animal   // abstract:抽象类,不能直接实例化,只能被继承
{
    public string Name { get; set; }

    public abstract void MakeSound();   // 抽象方法:只定义"有这个行为",具体怎么做交给子类
}

public class Cat : Animal
{
    public override void MakeSound() => Console.WriteLine($"{Name}:喵喵喵~");
}

public class Dog : Animal
{
    public override void MakeSound() => Console.WriteLine($"{Name}:汪汪汪~");
}

public class Bird : Animal
{
    public override void MakeSound() => Console.WriteLine($"{Name}:啾啾啾~");
}
csharp 复制代码
List<Animal> zoo = new()
{
    new Cat { Name = "咪咪" },
    new Dog { Name = "旺财" },
    new Bird { Name = "皮皮" },
};

foreach (Animal animal in zoo)
{
    animal.MakeSound();   // 神奇的地方:同一行代码,每次调用的却是"各自真正类型"对应的实现!
}
// 咪咪:喵喵喵~
// 旺财:汪汪汪~
// 皮皮:啾啾啾~

这就是多态的威力: zoo 这个列表存储的类型是 Animal,但当我们调用 animal.MakeSound() 时,程序会在运行时自动判断这个对象"真正的类型"是什么,并调用它自己重写过的版本 ------这个机制叫动态绑定(Dynamic Binding) ,也叫运行时多态。

现在再回头看开闭原则:如果动物园要新增一种"兔子",你只需要新建一个 Rabbit : Animal 类,MakeSound 这个遍历调用的代码一行都不用改!

4.4 virtual / override / abstract 三兄弟辨析

这是很多人容易混淆的地方,我们放在一起对比:

关键字 用在哪 含义
virtual 父类的方法 "这个方法可以被子类重写,也可以不重写,用父类默认实现"
abstract 父类的方法(必须在抽象类中) "这个方法必须被子类重写,父类本身不提供具体实现"
override 子类的方法 "我要重写父类中标记为 virtual 或 abstract 的这个方法"
csharp 复制代码
public abstract class Shape
{
    public abstract double GetArea();          // 抽象方法:子类必须实现,否则编译报错
    public virtual void Describe()             // 虚方法:子类可以选择重写,也可以不重写
        => Console.WriteLine($"这是一个图形,面积是 {GetArea()}");
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double GetArea() => Math.PI * Radius * Radius;   // 必须实现
    // Describe 方法可以不重写,直接继承父类默认实现
}

public class Square : Shape
{
    public double Side { get; set; }
    public override double GetArea() => Side * Side;
    public override void Describe()             // 也可以重写,定制自己的描述
        => Console.WriteLine($"这是一个正方形,边长 {Side},面积 {GetArea()}");
}

4.5 编译时多态 vs 运行时多态

多态其实分两种,很多资料只强调后者,但前者你早就在用了:

类型 实现方式 决定"调用哪个"的时机
编译时多态(静态多态) 方法重载(Overload) 编译期,根据参数类型静态决定
运行时多态(动态多态) 方法重写(Override)+ 虚方法机制 运行期,根据对象的真实类型动态决定
csharp 复制代码
// 编译时多态:调用哪个 Add,编译器在编译阶段就已经确定
static int Add(int a, int b) => a + b;
static double Add(double a, double b) => a + b;

// 运行时多态:调用哪个 MakeSound,要等到程序真正运行、拿到对象的真实类型时才能确定
Animal a = new Cat();
a.MakeSound();   // 编译器只知道 a 是 Animal,但运行时会调用 Cat 的实现

4.6 里氏替换原则:多态成立的理论基础

里氏替换原则(Liskov Substitution Principle):任何使用父类引用的地方,都应该能无缝替换成子类的实例,而不改变程序的正确性。

csharp 复制代码
void FeedAnimal(Animal animal)   // 参数类型是父类 Animal
{
    animal.MakeSound();
}

FeedAnimal(new Cat { Name = "咪咪" });   // ✅ 传入子类实例,完全没问题
FeedAnimal(new Dog { Name = "旺财" });   // ✅ 同样没问题

这正是多态在实际工程中最常见的应用形式------方法参数声明为父类类型,调用时传入任意子类实例,代码不需要关心具体是哪个子类,也不需要修改。这是实现"可扩展系统"的核心设计手法。


五、三者如何协同作战:一个完整的实战案例

我们用一个"图形计算器"综合演示封装、继承、多态如何配合工作。

csharp 复制代码
using System;
using System.Collections.Generic;

// ━━━━━━━━━━ 封装:内部字段私有,通过属性受控访问 ━━━━━━━━━━
public abstract class Shape
{
    private string color = "黑色";

    public string Color
    {
        get => color;
        set => color = string.IsNullOrWhiteSpace(value) ? "黑色" : value;   // 封装校验逻辑
    }

    // ━━━━━━━━━━ 多态:抽象方法,交给子类各自实现 ━━━━━━━━━━
    public abstract double GetArea();
    public abstract double GetPerimeter();

    // 虚方法:提供默认实现,子类可选择性重写
    public virtual void PrintInfo()
    {
        Console.WriteLine($"[{GetType().Name}] 颜色:{Color},面积:{GetArea():F2},周长:{GetPerimeter():F2}");
    }
}

// ━━━━━━━━━━ 继承:Circle、Rectangle、Square 共享 Shape 的公共能力 ━━━━━━━━━━
public class Circle : Shape
{
    public double Radius { get; set; }
    public Circle(double radius) => Radius = radius;

    public override double GetArea() => Math.PI * Radius * Radius;
    public override double GetPerimeter() => 2 * Math.PI * Radius;
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public Rectangle(double width, double height) { Width = width; Height = height; }

    public override double GetArea() => Width * Height;
    public override double GetPerimeter() => 2 * (Width + Height);
}

// Square 继承自 Rectangle:正方形"是一种"特殊的长方形,符合 is-a 关系
public class Square : Rectangle
{
    public Square(double side) : base(side, side) { }   // 用 base 调用父类构造函数

    public override void PrintInfo()   // 重写父类的虚方法,定制自己的描述
    {
        Console.WriteLine($"[正方形] 颜色:{Color},边长:{Width},面积:{GetArea():F2}");
    }
}

class Program
{
    static void Main()
    {
        List<Shape> shapes = new()
        {
            new Circle(5) { Color = "红色" },
            new Rectangle(4, 6) { Color = "蓝色" },
            new Square(3) { Color = "绿色" },
        };

        double totalArea = 0;

        // 多态的核心价值:完全不需要关心具体是什么形状,统一处理
        foreach (Shape shape in shapes)
        {
            shape.PrintInfo();       // 运行时自动调用各自重写后的版本
            totalArea += shape.GetArea();
        }

        Console.WriteLine($"\n所有图形总面积:{totalArea:F2}");
    }
}

运行结果

text 复制代码
[Circle] 颜色:红色,面积:78.54,周长:31.42
[Rectangle] 颜色:蓝色,面积:24.00,周长:20.00
[正方形] 颜色:绿色,边长:3,面积:9.00

所有图形总面积:111.54

这个案例里,三大特性各司其职:

  • 封装 :Color 属性通过校验逻辑,保证颜色值永远不会是空字符串
  • 继承 :Rectangle 复用了 Shape 的 Color 属性和整体结构;Square 进一步复用了 Rectangle 的宽高逻辑(base(side, side))
  • 多态 :List<Shape> 统一存储三种不同图形,foreach 循环里的 shape.PrintInfo() 和 shape.GetArea() 在运行时自动分发到各自正确的实现------新增一种图形(比如三角形),这段遍历逻辑完全不用改

六、高手都会踩的 6 个坑

坑 1:new 关键字隐藏而非重写父类方法

csharp 复制代码
public class Animal
{
    public void Speak() => Console.WriteLine("动物发出声音");
}

public class Cat : Animal
{
    public new void Speak() => Console.WriteLine("喵喵喵");   // ⚠️ new 而不是 override!
}

Animal a = new Cat();
a.Speak();   // 输出:"动物发出声音" ------ 而不是"喵喵喵"!

这是一个极其隐蔽的坑: new 关键字只是"隐藏"了父类的同名方法,并没有参与多态机制 ------通过父类引用调用时,依然会执行父类的版本,而不是子类的版本。只有 virtual + override 才能真正实现运行时多态。日常开发中,除非你非常清楚自己在做什么,否则应尽量避免用 new 隐藏方法,容易造成难以排查的 bug。

坑 2:构造函数中调用虚方法

csharp 复制代码
public class Base
{
    public Base()
    {
        Initialize();   // ⚠️ 危险:在构造函数中调用虚方法
    }
    public virtual void Initialize() => Console.WriteLine("Base 初始化");
}

public class Derived : Base
{
    private string name = "默认值";
    public override void Initialize() => Console.WriteLine($"Derived 初始化,name = {name}");
}

new Derived();
// 输出:Derived 初始化,name =        ← name 竟然是空的!

原理: C# 对象初始化的顺序是"先执行父类构造函数,再执行子类字段初始化,最后执行子类构造函数 "。当父类构造函数里调用虚方法时,实际执行的是子类重写后的版本 (多态在构造阶段依然生效),但此时子类的字段还没来得及被赋值 ,就会出现这种"读到默认值"的诡异现象。结论:永远不要在构造函数中调用虚方法。

坑 3:过度使用继承,导致类层次爆炸

csharp 复制代码
// ❌ 反面教材:为了复用一点点逻辑,硬造出一条离谱的继承链
Animal → Mammal → Pet → DomesticPet → HouseholdCat → IndoorPersianCat

层级过深的继承体系,会让代码变得极难理解和维护------牵一发动全身,父类的一个小改动可能影响六层之下的子类。第三节提到的"组合优于继承"原则,正是为了规避这种问题。

坑 4:忘记调用 base 构造函数导致的初始化缺失

csharp 复制代码
public class Animal
{
    public string Name { get; }
    public Animal(string name) => Name = name;
}

public class Cat : Animal
{
    // ❌ 编译错误:Animal 没有无参构造函数,Cat 必须显式调用 base(name)
    public Cat(string name) { }
}

// ✅ 正确写法
public class Cat : Animal
{
    public Cat(string name) : base(name) { }
}

如果父类只定义了带参数的构造函数 (没有无参构造函数),子类构造函数必须显式通过 base(...) 调用父类构造函数,否则编译不通过。

坑 5:属性封装了,但校验逻辑形同虚设

csharp 复制代码
public class Product
{
    public decimal Price { get; set; }   // ⚠️ 虽然用了属性,但 set 没有任何校验,等于没封装
}

var p = new Product();
p.Price = -999;   // 依然能设置成负数!

用属性代替字段只是封装的"形式",真正的封装必须在 set 里加上业务校验逻辑,否则只是"换了个写法的裸奔"。

坑 6:误以为多态只能通过继承实现

多态还有另一种非常常见的实现方式------接口(Interface):

csharp 复制代码
public interface ISpeaker
{
    void Speak();
}

public class Cat : ISpeaker
{
    public void Speak() => Console.WriteLine("喵喵喵");
}

public class Robot : ISpeaker   // Robot 和 Cat 没有任何继承关系,但都实现了 ISpeaker
{
    public void Speak() => Console.WriteLine("滴滴滴,我是机器人");
}

List<ISpeaker> speakers = new() { new Cat(), new Robot() };
foreach (var s in speakers) s.Speak();   // 同样是多态!

接口实现的多态,比继承更灵活 ------因为 C# 不支持多重继承(一个类只能有一个基类),但一个类可以实现多个接口,这让完全不相关的类(猫和机器人)也能共享"统一调用方式"的能力。在很多现代 C# 设计中,"面向接口编程"比"面向继承编程"更受推崇。


七、面向对象 vs 面向过程:什么时候该用哪个

面向对象不是"银弹",也不是任何场景都该无脑套用。一个诚实的对比:

维度 面向过程 面向对象
代码组织方式 函数 + 数据分离 数据与行为捆绑成对象
适合场景 简单脚本、一次性工具、性能极致敏感场景 中大型系统、需要长期维护和扩展的业务代码
学习/理解成本 较低,逻辑直白 较高,需要理解抽象层次
扩展性 新增需求往往牵一发动全身 通过继承/多态,新增功能对已有代码影响小

一个朴素的判断标准: 如果你的程序只是"写一次跑一次"的小脚本,面向过程完全够用,甚至更简单直接;但只要涉及到"团队协作"、"长期维护"、"业务规则会持续演进",面向对象的封装、继承、多态几乎总能带来长期的收益------这也是为什么几乎所有主流的大型软件系统,都是用面向对象语言构建的。


八、写在最后

封装、继承、多态,说到底解决的是同一个终极问题:如何让代码在"复杂度不断增长"的过程中,依然保持可读、可扩展、可维护。

  • 封装教会我们"边界感"------把该藏的藏好,只暴露该暴露的
  • 继承教会我们"复用共性"------别人已经写好的东西,不要重复造轮子
  • 多态教会我们"拥抱差异性"------用统一的接口,优雅地处理形形色色的具体实现

这三者从来不是孤立的知识点,而是一套完整的思维方式。当你未来设计任何一个新类时,不妨问自己:这个类的内部状态该不该被外部直接修改(封装)?有没有和其他类共享的公共逻辑(继承)?未来会不会出现同一操作、不同实现的场景(多态)?想清楚这三个问题,你的面向对象设计就已经迈过了入门的门槛。


📝 课后练习

  1. 设计一个 Employee(员工)基类和 Manager(经理)、Developer(开发者)两个子类,Manager 额外有"团队人数"属性,Developer 额外有"擅长的编程语言"属性,两者都要重写一个 CalculateBonus()(计算奖金)方法,规则自定。
  2. 尝试解释:为什么 C# 允许一个类实现多个接口 ,却不允许一个类继承多个基类?这和"菱形继承问题"有什么关系?
  3. 把第五节"图形计算器"案例中的 Shape 改造成接口 IShape 而不是抽象类,对比两种写法各自的优劣(提示:接口不能包含字段和已实现的普通方法逻辑,但抽象类可以)。

觉得有收获?欢迎点赞收藏,下次设计类结构时,试着用封装、继承、多态这三把尺子,重新审视一遍你的代码 😉

相关推荐
我可能是个假开发1 小时前
FTP Unicode 文件名导致 `550` 的问题分析与解决方案
java·开发语言·spring
WL学习笔记2 小时前
日期类(C++)
开发语言·c++·日期类实现
谢亮_vipxieliang3 小时前
Go map与结构体的正确使用
开发语言·golang·哈希算法
H.莓飛3 小时前
【C++】命名空间、缺省参数、函数重载与引用
linux·开发语言·c++·visual studio
xxwl5853 小时前
数据结构知识点和代码实现总结(C语言实现)
c语言·开发语言·数据结构
2501_926978334 小时前
给自指系统接一个外部锚 —— 一个关于「用 AI 观察自己」的方法
人工智能·经验分享·笔记·机器学习·ai写作
GlobalTradercrb6 小时前
展会获客和线上获客,预算先加给谁?答案看客单价
经验分享
code2cat6 小时前
Java进阶篇之AtomicMarkableReference:引用还在,标记已经改变
java·开发语言·cas·并发编程·逻辑删除
CappuccinoRose7 小时前
FormData数据处理
开发语言·前端·javascript·表单数据