Day 10 - 继承与多态

目标:掌握ArkTS的继承机制、方法重写、多态与抽象类
预计时间:2-2.5小时
前置知识:Day 09 类基础(属性、构造函数、访问修饰符、static、enum)


第一部分:继承基础(对标 C++ : public Base)

1.1 为什么需要继承

问题引入:假设要开发一个智能家居系统,有智能灯、智能空调、智能门锁等设备。每个设备都有id、name、isOnline状态,都要支持turnOn()和turnOff()操作。如果不使用继承,代码会怎样?

// 没有继承:大量重复代码
class SmartLight {
    id: number;
    name: string;
    isOnline: boolean = false;
    brightness: number = 100;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    turnOn(): void {
        this.isOnline = true;
        console.log(`${this.name} 已开启`);
    }
    
    turnOff(): void {
        this.isOnline = false;
        console.log(`${this.name} 已关闭`);
    }
}

class SmartAC {
    id: number;
    name: string;
    isOnline: boolean = false;
    temperature: number = 26;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    turnOn(): void {
        this.isOnline = true;
        console.log(`${this.name} 已开启`);
    }
    
    turnOff(): void {
        this.isOnline = false;
        console.log(`${this.name} 已关闭`);
    }
}
// id, name, isOnline, turnOn(), turnOff() 完全重复!

解决方案:使用继承提取公共代码到父类。


1.2 extends 语法

ArkTS使用extends关键字实现类继承:

// 父类(基类):提取公共属性和方法
class Device {
    id: number;
    name: string;
    isOnline: boolean = false;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    turnOn(): void {
        this.isOnline = true;
        console.log(`${this.name} 已开启`);
    }
    
    turnOff(): void {
        this.isOnline = false;
        console.log(`${this.name} 已关闭`);
    }
    
    getStatus(): string {
        return this.isOnline ? "在线" : "离线";
    }
}

// 子类(派生类):继承父类,添加特有功能
class SmartLight extends Device {
    brightness: number = 100;
    color: string = "white";
    
    constructor(id: number, name: string, color: string = "white") {
        super(id, name);  // 调用父类构造函数
        this.color = color;
    }
    
    setBrightness(level: number): void {
        this.brightness = Math.max(0, Math.min(100, level));
        console.log(`${this.name} 亮度设置为 ${this.brightness}%`);
    }
}

class SmartAC extends Device {
    temperature: number = 26;
    mode: string = "cool";
    
    constructor(id: number, name: string) {
        super(id, name);
    }
    
    setTemperature(temp: number): void {
        this.temperature = temp;
        console.log(`${this.name} 温度设置为 ${this.temperature}°C`);
    }
}

// 使用
let light = new SmartLight(1001, "客厅灯", "warm");
light.turnOn();           // 调用继承的方法
light.setBrightness(80);  // 调用子类特有方法
console.log(light.getStatus());  // "在线"

let ac = new SmartAC(2001, "主卧空调");
ac.turnOn();
ac.setTemperature(24);

对比C++

// C++ 继承语法
class SmartLight : public Device {  // 必须指定 public/protected/private
    // ...
};

关键区别

  • C++支持多重继承(class D : public A, public B
  • ArkTS只支持单继承,一个类只能extends一个父类
  • ArkTS只有公有继承,相当于C++的public继承

1.3 super 调用父类构造函数

问题引入:子类有自己的属性需要初始化,同时又要初始化继承自父类的属性,怎么办?

class SmartLock extends Device {
    password: string;
    isLocked: boolean = true;
    
    constructor(id: number, name: string, password: string) {
        // 必须先调用 super() 初始化父类部分
        super(id, name);
        // 然后初始化子类自己的属性
        this.password = password;
    }
    
    unlock(inputPassword: string): boolean {
        if (inputPassword === this.password) {
            this.isLocked = false;
            console.log(`${this.name} 已解锁`);
            return true;
        }
        console.log(`${this.name} 密码错误`);
        return false;
    }
}

重要规则

  1. 子类构造函数必须先调用super(),然后才能使用this
  2. super()必须在构造函数的第一行
  3. super()的参数传递给父类构造函数

对比C++

class SmartLock : public Device {
    std::string password;
    bool isLocked = true;
public:
    SmartLock(int id, const std::string& name, const std::string& pwd)
        : Device(id, name), password(pwd) {}  // 初始化列表
};

1.4 继承中的属性访问

问题引入:子类能访问父类的哪些成员?private成员能访问吗?

class BaseDevice {
    public id: number;           // 任何地方可访问
    protected firmware: string;  // 类内部和子类可访问
    private secretKey: string;   // 仅类内部可访问
    
    constructor(id: number, key: string) {
        this.id = id;
        this.firmware = "v1.0";
        this.secretKey = key;
    }
}

class DerivedDevice extends BaseDevice {
    version: string;
    
    constructor(id: number, key: string) {
        super(id, key);
        this.version = "2.0";
    }
    
    checkAccess(): void {
        console.log(`ID: ${this.id}`);           // ✅ OK,public
        console.log(`固件: ${this.firmware}`);   // ✅ OK,protected
        // console.log(this.secretKey);          // ❌ 错误,private
    }
}

let device = new DerivedDevice(1001, "secret123");
console.log(device.id);        // ✅ OK,public
// console.log(device.firmware); // ❌ 错误,protected,外部不可访问
// console.log(device.secretKey); // ❌ 错误,private

访问权限总结

修饰符 父类内部 子类内部 外部代码
public
protected
private

第二部分:super 关键字深入

2.1 super() 调用父类构造函数

问题引入:如果子类不需要额外初始化,可以省略构造函数吗?

class SimpleDevice extends Device {
    // 没有定义构造函数
    // 编译器会自动生成:
    // constructor(id: number, name: string) {
    //     super(id, name);
    // }
}

// 可以直接使用父类的构造函数参数
let simple = new SimpleDevice(3001, "简单设备");
simple.turnOn();

但如果子类有自定义属性,就必须显式定义构造函数

class ComplexDevice extends Device {
    features: string[];
    
    constructor(id: number, name: string, features: string[]) {
        // 在 super() 之前:不能使用 this 访问任何类成员
        // this.features = features;  // ❌ 错误!不能使用 this
        
        // ✅ 允许:不使用 this 的语句
        let temp = id + 1000;
        console.log("正在创建设备,ID偏移后:" + temp);
        
        // 必须先调用 super() 初始化父类部分
        super(id, name);
        
        // super() 之后才能使用 this 访问类成员
        this.features = features;
    }
}

为什么要这样设计?

对象构造顺序:先执行父类构造函数(初始化父类部分),再执行子类构造函数(初始化子类部分)。

必须先完成父类部分的构造,子类才能安全地访问继承来的成员。如果允许在 super() 之前使用 this,可能会访问到未初始化的父类成员。

核心规则super() 不必是第一行代码,但必须在任何 this.xxx 访问之前


2.2 super.method() 调用父类方法

问题引入:子类重写了父类方法,但想在重写版本中先执行父类的逻辑,怎么办?

class LoggingDevice extends Device {
    logEnabled: boolean = true;
    
    constructor(id: number, name: string, logEnabled: boolean = true) {
        super(id, name);
        this.logEnabled = logEnabled;
    }
    
    // 重写 turnOn,但先调用父类的 turnOn
    turnOn(): void {
        if (this.logEnabled) {
            console.log(`[日志] ${this.name} 正在开启...`);
        }
        super.turnOn();  // 调用父类的 turnOn 方法
        if (this.logEnabled) {
            console.log(`[日志] ${this.name} 开启完成`);
        }
    }
    
    // 重写 turnOff
    turnOff(): void {
        if (this.logEnabled) {
            console.log(`[日志] ${this.name} 正在关闭...`);
        }
        super.turnOff();
        if (this.logEnabled) {
            console.log(`[日志] ${this.name} 关闭完成`);
        }
    }
}

let logger = new LoggingDevice(4001, "日志设备");
logger.turnOn();
// 输出:
// [日志] 日志设备 正在开启...
// 日志设备 已开启
// [日志] 日志设备 开启完成

对比C++

class LoggingDevice : public Device {
public:
    void turnOn() override {
        std::cout << "[日志] 正在开启..." << std::endl;
        Device::turnOn();  // C++ 使用 BaseClass::method()
        std::cout << "[日志] 开启完成" << std::endl;
    }
};

第三部分:方法重写(对标 C++ virtual 函数覆盖)

3.1 基本方法重写

问题引入:不同类型的设备,getStatus()返回的信息应该不同。如何让子类提供自己的实现?

class Device {
    id: number;
    name: string;
    isOnline: boolean = false;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    turnOn(): void {
        this.isOnline = true;
    }
    
    turnOff(): void {
        this.isOnline = false;
    }
    
    // 父类提供一个默认实现
    getStatus(): string {
        return `[设备] ${this.name}: ${this.isOnline ? "在线" : "离线"}`;
    }
}

class SmartLight extends Device {
    brightness: number = 100;
    
    // 重写 getStatus 方法
    getStatus(): string {
        return `[智能灯] ${this.name}: ${this.isOnline ? "在线" : "离线"}, 亮度${this.brightness}%`;
    }
}

class SmartAC extends Device {
    temperature: number = 26;
    
    // 重写 getStatus 方法
    getStatus(): string {
        return `[空调] ${this.name}: ${this.isOnline ? "在线" : "离线"}, 温度${this.temperature}°C`;
    }
}

// 使用
let light = new SmartLight(1001, "客厅灯");
light.brightness = 80;
console.log(light.getStatus());  // [智能灯] 客厅灯: 离线, 亮度80%

let ac = new SmartAC(2001, "主卧空调");
ac.temperature = 24;
console.log(ac.getStatus());  // [空调] 主卧空调: 离线, 温度24°C

对比C++

class Device {
public:
    virtual std::string getStatus() const {  // C++ 需要 virtual 关键字
        return "[设备] " + name;
    }
};

class SmartLight : public Device {
public:
    std::string getStatus() const override {  // C++11 使用 override
        return "[智能灯] " + name;
    }
};

关键区别

  • C++需要显式标记virtual才能实现多态
  • ArkTS所有方法默认可重写,不需要virtual关键字

3.2 重写规则

问题引入:子类重写方法时有什么限制?

class Animal {
    name: string;
    
    constructor(name: string) {
        this.name = name;
    }
    
    // 方法可以被重写
    makeSound(): string {
        return "动物发出声音";
    }
    
    // private 方法不能被重写
    private secret(): void {
        console.log("秘密");
    }
}

class Dog extends Animal {
    breed: string;
    
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    
    // ✅ 正确重写:参数相同,返回类型相同
    makeSound(): string {
        return "汪汪汪";
    }
    
    // ❌ 错误:不能重写 private 方法
    // secret(): void { }
}

class Cat extends Animal {
    // ✅ 正确重写
    makeSound(): string {
        return "喵喵喵";
    }
}

重写规则

规则 说明 违反后果
方法名相同 必须与父类方法名完全一致 不是重写
参数列表相同 类型、数量、顺序必须相同 类型不兼容,编译错误
返回类型相同或协变 必须相同或是子类型 类型不兼容,编译错误
不能重写 private 方法 private 方法仅在类内部可见 编译错误

特殊规则:void 返回值

如果父类方法返回 void,子类方法返回任意类型都构成重写:

class Animal {
    makeSound(): void { }
}

class Dog extends Animal {
    makeSound(): string { return "汪汪"; }  // ✅ 构成重写(特殊规则)
}

注意:虽然编译通过,但会破坏类型安全,不建议这样写。

什么是协变?

协变 = IS-A 关系:子类返回类型必须是父类返回类型的子类型。

class AnimalInfo { name: string = ""; }
class DogInfo extends AnimalInfo { breed: string = ""; }

class Animal {
    getInfo(): AnimalInfo { return new AnimalInfo(); }
}

class Dog extends Animal {
    getInfo(): DogInfo {  // ✅ 协变:DogInfo IS-A AnimalInfo
        return new DogInfo();
    }
}

判断标准子类返回类型 extends 父类返回类型 则构成协变。


3.3 阻止方法重写

问题引入:如何防止子类重写某个方法?

ArkTS 中使用 readonly 修饰符 + 箭头函数:

class Base {
    // readonly 箭头函数:阻止重写
    readonly finalMethod = (): void => {
        console.log("这个方法不能被重写");
    }
    
    // 普通方法:可以被重写
    normalMethod(): void {
        console.log("普通方法");
    }
}

class Derived extends Base {
    // ❌ 错误:无法重写 readonly 箭头函数
    // finalMethod = (): void => { }
    
    // ✅ 可以重写普通方法
    normalMethod(): void {
        console.log("子类重写");
    }
}

原理

  • readonly 属性只能在声明时或构造函数中初始化
  • 箭头函数作为属性值,子类无法覆盖
  • 达到"阻止重写"的效果

对比 C++

语言 阻止重写语法 原理
C++ void method() final; final 关键字,编译器禁止虚函数表覆盖
ArkTS readonly method = () => {} readonly + 箭头函数,属性不可变机制

本质:ArkTS 用 "readonly 属性不可变" 模拟了 C++ 的 "final 方法不可重写"


3.4 调用被覆盖的父类方法

问题引入:子类重写方法后,还能调用父类的原始实现吗?

class Vehicle {
    brand: string;
    speed: number = 0;
    
    constructor(brand: string) {
        this.brand = brand;
    }
    
    accelerate(amount: number): void {
        this.speed += amount;
        console.log(`${this.brand} 加速到 ${this.speed} km/h`);
    }
    
    getInfo(): string {
        return `${this.brand}: ${this.speed} km/h`;
    }
}

class ElectricCar extends Vehicle {
    batteryLevel: number = 100;
    
    constructor(brand: string) {
        super(brand);
    }
    
    // 重写 accelerate,添加电量检查
    accelerate(amount: number): void {
        if (this.batteryLevel <= 0) {
            console.log(`${this.brand} 电量不足,无法加速`);
            return;
        }
        // 调用父类的 accelerate 完成速度增加
        super.accelerate(amount);
        // 子类特有的逻辑:耗电
        this.batteryLevel -= amount * 0.1;
        console.log(`${this.brand} 剩余电量: ${this.batteryLevel.toFixed(1)}%`);
    }
    
    // 重写 getInfo,但复用父类实现
    getInfo(): string {
        let baseInfo = super.getInfo();  // 调用父类的 getInfo
        return `${baseInfo}, 电量${this.batteryLevel.toFixed(1)}%`;
    }
}

let tesla = new ElectricCar("特斯拉");
tesla.accelerate(50);  // 先调用 Vehicle.accelerate,然后耗电
console.log(tesla.getInfo());
// 输出:
// 特斯拉 加速到 50 km/h
// 特斯拉 剩余电量: 95.0%
// 特斯拉: 50 km/h, 电量95.0%

第四部分:多态(对标 C++ 虚函数多态)

4.1 什么是多态

问题引入:有一个设备数组,里面包含智能灯、空调、门锁等不同类型的设备。如何统一处理它们?

// 父类引用可以指向子类实例
let devices: Device[] = [
    new SmartLight(1001, "客厅灯", "white"),
    new SmartAC(2001, "主卧空调"),
    new SmartLock(3001, "大门锁", "123456")
];

// 统一处理不同类型的设备
for (let i = 0; i < devices.length; i++) {
    let device = devices[i];
    console.log(device.getStatus());  // 调用各自重写的 getStatus
}

// 输出:
// [智能灯] 客厅灯: 离线, 亮度100%
// [空调] 主卧空调: 离线, 温度26°C
// [设备] 大门锁: 离线  (SmartLock 没有重写 getStatus,使用父类版本)

多态的核心:父类类型的变量可以引用子类对象,调用方法时会根据实际对象类型执行对应的方法实现。

对比C++

std::vector<std::unique_ptr<Device>> devices;
devices.push_back(std::make_unique<SmartLight>(1001, "客厅灯"));
devices.push_back(std::make_unique<SmartAC>(2001, "空调"));

for (const auto& device : devices) {
    std::cout << device->getStatus() << std::endl;  // 虚函数多态
}

4.2 多态实际应用

问题引入:如何设计一个设备管理器,能够统一管理各种设备?

class DeviceManager {
    private devices: Device[] = [];
    
    addDevice(device: Device): void {
        this.devices.push(device);
        console.log(`添加设备: ${device.name}`);
    }
    
    // 批量开启所有设备
    turnOnAll(): void {
        console.log("=== 开启所有设备 ===");
        for (let i = 0; i < this.devices.length; i++) {
            this.devices[i].turnOn();
        }
    }
    
    // 批量关闭所有设备
    turnOffAll(): void {
        console.log("=== 关闭所有设备 ===");
        for (let i = 0; i < this.devices.length; i++) {
            this.devices[i].turnOff();
        }
    }
    
    // 获取所有设备状态
    getAllStatus(): string[] {
        let result: string[] = [];
        for (let i = 0; i < this.devices.length; i++) {
            result.push(this.devices[i].getStatus());
        }
        return result;
    }
}

// 使用
let manager = new DeviceManager();
manager.addDevice(new SmartLight(1001, "客厅灯", "warm"));
manager.addDevice(new SmartAC(2001, "主卧空调"));
manager.addDevice(new SmartLock(3001, "大门锁", "123456"));

manager.turnOnAll();
let statuses = manager.getAllStatus();
for (let i = 0; i < statuses.length; i++) {
    console.log(statuses[i]);
}

4.3 instanceof 类型判断

问题引入:当遍历设备数组时,如何知道某个设备具体是智能灯还是空调?

function processDevice(device: Device): void {
    console.log(`处理设备: ${device.name}`);
    
    // 使用 instanceof 判断具体类型
    if (device instanceof SmartLight) {
        // 在这个分支中,TypeScript 知道 device 是 SmartLight 类型
        console.log(`这是一个智能灯,当前亮度: ${device.brightness}%`);
        device.setBrightness(80);  // 可以调用 SmartLight 特有方法
    } else if (device instanceof SmartAC) {
        console.log(`这是一个空调,当前温度: ${device.temperature}°C`);
        device.setTemperature(24);  // 可以调用 SmartAC 特有方法
    } else if (device instanceof SmartLock) {
        console.log(`这是一个智能锁,锁定状态: ${device.isLocked}`);
    } else {
        console.log("这是一个普通设备");
    }
}

// 使用
let myDevices: Device[] = [
    new SmartLight(1001, "客厅灯", "white"),
    new SmartAC(2001, "主卧空调"),
    new SmartLock(3001, "大门锁", "123456")
];

for (let i = 0; i < myDevices.length; i++) {
    processDevice(myDevices[i]);
}

对比C++

void processDevice(Device* device) {
    if (SmartLight* light = dynamic_cast<SmartLight*>(device)) {
        // C++ 使用 dynamic_cast 进行运行时类型检查
        std::cout << "智能灯亮度: " << light->brightness << std::endl;
    } else if (SmartAC* ac = dynamic_cast<SmartAC*>(device)) {
        std::cout << "空调温度: " << ac->temperature << std::endl;
    }
}

4.4 instanceof 结合类型收窄

问题引入:如何判断后安全地访问子类特有属性?

class SmartFan extends Device {
    speed: number = 0;
    maxSpeed: number = 3;
    
    constructor(id: number, name: string) {
        super(id, name);
    }
    
    setSpeed(level: number): void {
        this.speed = Math.max(0, Math.min(this.maxSpeed, level));
        console.log(`${this.name} 风速设置为 ${this.speed}`);
    }
}

function adjustDevice(device: Device): void {
    // 先判断类型
    if (device instanceof SmartLight) {
        // 在这个代码块中,device 被收窄为 SmartLight 类型
        console.log(`调整智能灯亮度`);
        device.setBrightness(50);  // 安全调用 SmartLight 方法
        console.log(`当前亮度: ${device.brightness}`);
    } else if (device instanceof SmartAC) {
        // device 被收窄为 SmartAC 类型
        console.log(`调整空调温度`);
        device.setTemperature(26);
        console.log(`当前温度: ${device.temperature}`);
    } else if (device instanceof SmartFan) {
        // device 被收窄为 SmartFan 类型
        console.log(`调整风扇风速`);
        device.setSpeed(2);
        console.log(`当前风速: ${device.speed}`);
    }
}

// 测试
let testDevices: Device[] = [
    new SmartLight(1001, "客厅灯", "white"),
    new SmartAC(2001, "主卧空调"),
    new SmartFan(4001, "风扇")
];

for (let i = 0; i < testDevices.length; i++) {
    adjustDevice(testDevices[i]);
}

类型收窄:在if (x instanceof Class)代码块内部,编译器会自动将变量类型收窄为对应的子类类型,可以安全访问子类特有成员。


第五部分:抽象类(对标 C++ 纯虚函数)

5.1 为什么需要抽象类

问题引入:Device类本身应该被实例化吗?一个纯粹的"设备"是什么?

// 直接实例化 Device 有意义吗?
let genericDevice = new Device(9999, "某个设备");
// 这种"通用设备"在实际业务中可能没有意义
// 我们真正需要的是具体的设备:灯、空调、门锁...

解决方案:使用抽象类,让父类不能被直接实例化,只能被继承。


5.2 abstract class 语法

// 抽象类:不能被 new,只能被继承
abstract class AbstractDevice {
    id: number;
    name: string;
    isOnline: boolean = false;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    turnOn(): void {
        this.isOnline = true;
        console.log(`${this.name} 已开启`);
    }
    
    turnOff(): void {
        this.isOnline = false;
        console.log(`${this.name} 已关闭`);
    }
    
    // 抽象方法:子类必须实现
    abstract getDeviceType(): string;
    abstract getDetailedStatus(): string;
}

// ❌ 错误:不能实例化抽象类
// let device = new AbstractDevice(1001, "设备");  // 编译错误

// ✅ 正确:创建具体子类继承抽象类
class SmartBulb extends AbstractDevice {
    brightness: number = 100;
    
    constructor(id: number, name: string) {
        super(id, name);
    }
    
    // 必须实现抽象方法
    getDeviceType(): string {
        return "SmartBulb";
    }
    
    getDetailedStatus(): string {
        return `${this.name}: 亮度${this.brightness}%, 状态${this.isOnline ? "在线" : "离线"}`;
    }
}

class AirConditioner extends AbstractDevice {
    temperature: number = 26;
    
    constructor(id: number, name: string) {
        super(id, name);
    }
    
    getDeviceType(): string {
        return "AirConditioner";
    }
    
    getDetailedStatus(): string {
        return `${this.name}: 温度${this.temperature}°C, 状态${this.isOnline ? "在线" : "离线"}`;
    }
}

// 使用
let bulb = new SmartBulb(1001, "客厅灯泡");
console.log(bulb.getDeviceType());      // SmartBulb
console.log(bulb.getDetailedStatus());

5.3 abstract 方法与 abstract 属性

问题引入:如何强制子类实现某些方法?

抽象方法

语法:使用 abstract 关键字声明没有实现的方法。

abstract class Shape {
  // 抽象属性:子类必须实现
  abstract name: string;

  // 抽象方法:只有声明,没有实现
  abstract calculateArea(): number;

  abstract calculatePerimeter(): number;

  // 具体方法:可以有实现
  getDescription(): string {
    return `${this.name}: 面积=${this.calculateArea()}, 周长=${this.calculatePerimeter()}`;
  }
}

class Rectangle extends Shape {
  name: string = "矩形";
  width: number;
  height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  // 必须实现抽象方法
  calculateArea(): number {
    return this.width * this.height;
  }

  calculatePerimeter(): number {
    return 2 * (this.width + this.height);
  }
}

class Circle extends Shape {
  name: string = "圆形";
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }

  calculatePerimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

// 使用多态
let shapes: Shape[] = [
  new Rectangle(10, 5),
  new Circle(7)
];

for (let i = 0; i < shapes.length; i++) {
  console.log(shapes[i].getDescription());
}
// 输出:
// 矩形: 面积=50, 周长=30
// 圆形: 面积=153.9380..., 周长=43.9822...

规则

  1. 抽象类不能实例化:new Shape() 会编译错误
  2. 子类必须实现所有抽象方法,否则子类也必须声明为 abstract
  3. 抽象方法不能有方法体,只有声明

对比 C++

特性 C++ ArkTS
抽象类 含纯虚函数的类 abstract class
纯虚函数 virtual void f() = 0 abstract f(): void
能否实例化 ❌ 不能 ❌ 不能
必须实现 ✅ 是 ✅ 是

抽象属性(ArkTS 特有,C++ 无此特性)

为什么需要抽象属性?

考虑场景:父类希望每个子类类型都有一个固定的特征值。

方案一:普通属性 + 构造函数(每个实例值不同)

abstract class Animal {
    name: string;
    
    constructor(n: string) {
        this.name = n;  // 由外部传入
    }
}

class Dog extends Animal {
    constructor() {
        super("狗");  // 可以传入不同值
    }
}

方案二:普通属性直接初始化(有风险)

abstract class Shape {
    name: string = "形状";  // 直接初始化
}

class Rectangle extends Shape {
    // 忘记重写 name,导致所有形状都叫"形状"
}

风险:子类可能忘记重写,导致所有子类使用相同的默认值。

方案三:抽象属性(最佳方案)

abstract class Shape {
    abstract name: string;  // 无默认值,强制子类实现
    
    getDescription(): string {
        return `这是一个${this.name}`;
    }
}

class Rectangle extends Shape {
    name: string = "矩形";  // 必须实现,编译器检查
}

class Circle extends Shape {
    name: string = "圆形";  // 必须实现,编译器检查
}

优势

  1. 强制子类定义(编译器检查)
  2. 同一子类的所有实例有相同的特征值
  3. 不会因为忘记重写而出错

与 C++ 的区别

特性 C++ ArkTS
抽象方法 virtual void f() = 0 abstract f(): void
抽象属性 ❌ 不支持 ✅ 支持

C++ 只能通过纯虚函数 getName() 来模拟,无法像 ArkTS 这样直接声明抽象属性。

abstract class Shape {
    // 抽象属性:子类必须实现
    abstract name: string;
    
    // 抽象方法:只有声明,没有实现
    abstract calculateArea(): number;
    abstract calculatePerimeter(): number;
    
    // 具体方法:可以有实现
    getDescription(): string {
        return `${this.name}: 面积=${this.calculateArea()}, 周长=${this.calculatePerimeter()}`;
    }
}

class Rectangle extends Shape {
    name: string = "矩形";
    width: number;
    height: number;
    
    constructor(width: number, height: number) {
        super();
        this.width = width;
        this.height = height;
    }
    
    // 必须实现抽象方法
    calculateArea(): number {
        return this.width * this.height;
    }
    
    calculatePerimeter(): number {
        return 2 * (this.width + this.height);
    }
}

class Circle extends Shape {
    name: string = "圆形";
    radius: number;
    
    constructor(radius: number) {
        super();
        this.radius = radius;
    }
    
    calculateArea(): number {
        return Math.PI * this.radius * this.radius;
    }
    
    calculatePerimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}

// 使用多态
let shapes: Shape[] = [
    new Rectangle(10, 5),
    new Circle(7)
];

for (let i = 0; i < shapes.length; i++) {
    console.log(shapes[i].getDescription());
}

对比C++

class Shape {
public:
    virtual double calculateArea() = 0;      // 纯虚函数 = 0
    virtual double calculatePerimeter() = 0; // 纯虚函数
    
    virtual std::string getDescription() {   // 普通虚函数
        return "形状";
    }
    
    virtual ~Shape() = default;  // 虚析构函数
};

5.4 抽象类中的具体方法

问题引入:抽象类中可以有已经实现的方法吗?

abstract class Payment {
    amount: number;
    timestamp: number;
    
    constructor(amount: number) {
        this.amount = amount;
        this.timestamp = Date.now();
    }
    
    // 抽象方法:不同支付方式有不同的实现
    abstract processPayment(): boolean;
    abstract getPaymentMethod(): string;
    
    // 具体方法:所有支付方式通用的逻辑
    getReceipt(): string {
        return `收据: ${this.getPaymentMethod()} 支付 ${this.amount}元 时间: ${this.timestamp}`;
    }
    
    // 具体方法:验证金额
    validateAmount(): boolean {
        return this.amount > 0;
    }
}

class CreditCardPayment extends Payment {
    cardNumber: string;
    
    constructor(amount: number, cardNumber: string) {
        super(amount);
        this.cardNumber = cardNumber;
    }
    
    processPayment(): boolean {
        if (!this.validateAmount()) {
            return false;
        }
        console.log(`信用卡 ${this.cardNumber} 扣款 ${this.amount}元`);
        return true;
    }
    
    getPaymentMethod(): string {
        return "信用卡";
    }
}

class AlipayPayment extends Payment {
    accountId: string;
    
    constructor(amount: number, accountId: string) {
        super(amount);
        this.accountId = accountId;
    }
    
    processPayment(): boolean {
        if (!this.validateAmount()) {
            return false;
        }
        console.log(`支付宝账号 ${this.accountId} 扣款 ${this.amount}元`);
        return true;
    }
    
    getPaymentMethod(): string {
        return "支付宝";
    }
}

// 使用
let payment1: Payment = new CreditCardPayment(100, "6222****1234");
payment1.processPayment();
console.log(payment1.getReceipt());  // 调用抽象类中的具体方法

let payment2: Payment = new AlipayPayment(200, "user@example.com");
payment2.processPayment();
console.log(payment2.getReceipt());

5.5 抽象属性

abstract class Animal {
    // 抽象属性:子类必须实现
    abstract species: string;
    abstract sound: string;
    
    name: string;
    age: number;
    
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    
    makeSound(): void {
        console.log(`${this.name} (${this.species}): ${this.sound}`);
    }
    
    abstract move(): void;
}

class Dog extends Animal {
    species: string = "犬科";
    sound: string = "汪汪汪";
    breed: string;
    
    constructor(name: string, age: number, breed: string) {
        super(name, age);
        this.breed = breed;
    }
    
    move(): void {
        console.log(`${this.name} 在奔跑`);
    }
}

class Bird extends Animal {
    species: string = "鸟类";
    sound: string = "叽叽喳喳";
    wingSpan: number;
    
    constructor(name: string, age: number, wingSpan: number) {
        super(name, age);
        this.wingSpan = wingSpan;
    }
    
    move(): void {
        console.log(`${this.name} 在飞翔`);
    }
}

let dog = new Dog("旺财", 3, "金毛");
dog.makeSound();  // 旺财 (犬科): 汪汪汪
dog.move();

let bird = new Bird("小白", 1, 30);
bird.makeSound();  // 小白 (鸟类): 叽叽喳喳
bird.move();

第六部分:继承设计原则

6.1 IS-A 关系

问题引入:什么时候应该使用继承?

// ✅ 正确的继承:SmartLight IS A Device(智能灯是一种设备)
class SmartLight extends Device {
    // ...
}

// ✅ 正确的继承:Dog IS A Animal(狗是一种动物)
class Dog extends Animal {
    // ...
}

// ❌ 错误的继承:Battery IS NOT a Device(电池不是一种设备)
// class Battery extends Device {
//     // 电池和设备是"HAS-A"关系,不是"IS-A"关系
// }

// ✅ 正确的做法:使用组合(Composition)
class DeviceWithBattery {
    device: Device;
    batteryLevel: number = 100;
    
    constructor(device: Device) {
        this.device = device;
    }
    
    checkBattery(): void {
        console.log(`${this.device.name} 电量: ${this.batteryLevel}%`);
    }
}

IS-A原则:只有当子类确实是父类的一种特殊类型时,才使用继承。


6.2 里氏替换原则(LSP)

问题引入:子类可以替换父类而不影响程序正确性吗?

// 违反 LSP 的例子:正方形和矩形
class Rectangle {
    protected _width: number = 0;
    protected _height: number = 0;
    
    setWidth(w: number): void {
        this._width = w;
    }
    
    setHeight(h: number): void {
        this._height = h;
    }
    
    getArea(): number {
        return this._width * this._height;
    }
}

// ❌ 违反 LSP:Square 改变了父类的行为
class BadSquare extends Rectangle {
    setWidth(w: number): void {
        this._width = w;
        this._height = w;  // 强制相等
    }
    
    setHeight(h: number): void {
        this._width = h;   // 强制相等
        this._height = h;
    }
}

// 测试函数期望 Rectangle 的行为
function testRectangle(rect: Rectangle): void {
    rect.setWidth(5);
    rect.setHeight(4);
    let area = rect.getArea();
    console.log(`期望面积: 20, 实际面积: ${area}`);
    // 如果是 BadSquare,面积会是 16,违反预期!
}

// ✅ 正确的做法:让 Square 和 Rectangle 都继承自 Shape
abstract class Shape {
    abstract getArea(): number;
}

class GoodRectangle extends Shape {
    private width: number;
    private height: number;
    
    constructor(width: number, height: number) {
        super();
        this.width = width;
        this.height = height;
    }
    
    getArea(): number {
        return this.width * this.height;
    }
}

class GoodSquare extends Shape {
    private side: number;
    
    constructor(side: number) {
        super();
        this.side = side;
    }
    
    getArea(): number {
        return this.side * this.side;
    }
}

里氏替换原则:子类必须能够替换父类而不改变程序的正确性。


第七部分:小结与练习

知识点对比总结表

概念 C++ ArkTS
继承语法 class D : public B class D extends B
继承类型 支持多重继承 只支持单继承
调用父类构造 初始化列表 : Base() super() 必须在 this 访问之前
调用父类方法 Base::method() super.method()
方法重写 需要 virtual 关键字 默认支持,无需 virtual
阻止重写 final 关键字 readonly 箭头函数(属性机制)
纯虚函数 virtual void f() = 0 abstract 类和方法
类型判断 dynamic_cast instanceof
抽象类 含纯虚函数的类 abstract class
抽象属性 ❌ 不支持 ✅ 支持

今日练习

练习1:基础继承

// 1. 定义 Animal 基类:
//    - 属性:name(string), age(number), isAlive(boolean=true)
//    - 构造函数初始化 name 和 age
//    - 方法:speak()(输出"动物发出声音")
//    - 方法:move()(输出"动物在移动")

// 2. 定义 Dog 类继承 Animal:
//    - 新增属性:breed(品种,string)
//    - 重写 speak()(输出"汪汪汪")
//    - 新增方法:fetch()(输出"狗狗在捡球")

// 3. 定义 Bird 类继承 Animal:
//    - 新增属性:wingSpan(翼展,number)
//    - 重写 speak()(输出"叽叽喳喳")
//    - 重写 move()(输出"鸟儿在飞翔")

// 4. 创建 Animal[] 数组,放入 Dog 和 Bird 实例
//    遍历数组调用 speak() 和 move(),观察多态效果
点击查看答案
class Animal {
    name: string;
    age: number;
    isAlive: boolean = true;

    constructor(n: string, a: number) {
        this.name = n;
        this.age = a;
    }

    speak(): void {
        console.log("动物发出声音");
    }

    move(): void {
        console.log("动物在移动");
    }
}

class Dog extends Animal {
    breed: string;

    constructor(n: string, a: number, b: string) {
        super(n, a);
        this.breed = b;
    }

    speak(): void {
        console.log("汪汪汪");
    }

    fetch(): void {
        console.log("狗狗在捡球");
    }
}

class Bird extends Animal {
    wingSpan: number;

    constructor(n: string, a: number, w: number) {
        super(n, a);
        this.wingSpan = w;
    }

    speak(): void {
        console.log("叽叽喳喳");
    }

    move(): void {
        console.log("鸟儿在飞翔");
    }
}

function test(): void {
    let animals: Animal[] = [];
    animals.push(new Dog("来福", 3, "中华田园犬"));
    animals.push(new Bird("波利", 2, 2.1));

    for (let index = 0; index < animals.length; index++) {
        const element = animals[index];
        element.speak();
        element.move();
    }
}

练习2:super 关键字与方法重写

// 1. 定义 Vehicle 基类:
//    - 属性:brand(string), speed(number=0)
//    - 方法:accelerate(amount: number) 增加速度
//    - 方法:brake() 将速度减半
//    - 方法:getStatus() 返回状态字符串

// 2. 定义 ElectricCar 继承 Vehicle:
//    - 新增属性:batteryLevel(电量,number=100)
//    - 重写 accelerate():先检查电量,调用 super.accelerate(),然后耗电(amount * 0.2)
//    - 重写 getStatus():包含电量信息,使用 super.getStatus() 获取基础信息

// 3. 定义 GasCar 继承 Vehicle:
//    - 新增属性:fuelLevel(油量,number=50)
//    - 重写 accelerate():先检查油量,调用 super.accelerate(),然后耗油(amount * 0.1)
//    - 重写 getStatus():包含油量信息

// 4. 测试:创建 ElectricCar 和 GasCar 实例,调用 accelerate 和 getStatus
点击查看答案
class Vehicle {
    brand: string;
    speed: number = 0;

    constructor(b: string) {
        this.brand = b;
    }

    accelerate(amount: number): void {
        this.speed += amount;
    }

    brake(): void {
        this.speed /= 2;
    }

    getStatus(): string {
        return `brand:${this.brand} speed:${this.speed}`;
    }
}

class ElectricCar extends Vehicle {
    batteryLevel: number = 100;

    accelerate(amount: number): void {
        if (this.batteryLevel > 0) {
            super.accelerate(amount);
            this.batteryLevel -= amount * 0.2;
        }
    }

    getStatus(): string {
        return `batteryLevel:${this.batteryLevel} ${super.getStatus()}`;
    }
}

class GasCar extends Vehicle {
    fuelLevel: number = 50;

    accelerate(amount: number): void {
        if (this.fuelLevel > 0) {
            super.accelerate(amount);
            this.fuelLevel -= amount * 0.1;
        }
    }

    getStatus(): string {
        return `fuelLevel:${this.fuelLevel} ${super.getStatus()}`;
    }
}

function test(): void {
    let eCar: ElectricCar = new ElectricCar("BYD");
    let gCar: GasCar = new GasCar("BYD");

    eCar.accelerate(10);
    console.log(`${eCar.getStatus()}`);

    gCar.accelerate(10);
    console.log(`${gCar.getStatus()}`);
}

练习3:instanceof 类型判断

// 1. 使用练习2中的 Vehicle 类层次结构

// 2. 编写函数 processVehicle(vehicle: Vehicle): void:
//    - 打印 vehicle.getStatus()
//    - 使用 instanceof 判断具体类型
//    - 如果是 ElectricCar,打印当前电量
//    - 如果是 GasCar,打印当前油量

// 3. 创建 Vehicle[] 数组,包含 ElectricCar 和 GasCar
//    遍历数组调用 processVehicle
点击查看答案
// 使用练习2中的 Vehicle 类

function processVehicle(vehicle: Vehicle): void {
    console.log(`${vehicle.getStatus()}`);
    if (vehicle instanceof ElectricCar) {
        console.log(`batteryLevel:${vehicle.batteryLevel}`);
    } else if (vehicle instanceof GasCar) {
        console.log(`fuelLevel:${vehicle.fuelLevel}`);
    }
}

function test(): void {
    let cars: Vehicle[] = [];
    cars.push(new ElectricCar("BYD"));
    cars.push(new GasCar("BYD"));

    for (let index = 0; index < cars.length; index++) {
        const car = cars[index];
        processVehicle(car);
    }
}

练习4:抽象类

// 1. 定义抽象类 Employee:
//    - 属性:id(number), name(string), baseSalary(number)
//    - 构造函数初始化以上属性
//    - 抽象方法:calculateSalary(): number
//    - 抽象方法:getRole(): string
//    - 具体方法:getInfo() 返回基本信息字符串

// 2. 定义 SalariedEmployee 继承 Employee:
//    - 新增属性:monthlySalary(月薪)
//    - 实现 calculateSalary() 返回 monthlySalary
//    - 实现 getRole() 返回 "正式员工"

// 3. 定义 HourlyEmployee 继承 Employee:
//    - 新增属性:hourlyRate(时薪), hoursWorked(工作小时数)
//    - 实现 calculateSalary() 返回 hourlyRate * hoursWorked
//    - 实现 getRole() 返回 "小时工"

// 4. 创建 Employee[] 数组,放入不同类型的员工
//    遍历数组打印每个员工的 getInfo() 和 calculateSalary()
点击查看答案
abstract class Employee {
    id: number;
    name: string;
    baseSalary: number;

    constructor(id: number, name: string, bs: number) {
        this.id = id;
        this.name = name;
        this.baseSalary = bs;
    }

    abstract calculateSalary(): number;

    abstract getRole(): string;

    getInfo(): string {
        return `id:${this.id} name:${this.name} role:${this.getRole()} bs:${this.baseSalary}`;
    }
}

class SalariedEmployee extends Employee {
    monthlySalary: number;

    constructor(id: number, name: string, bs: number, ms: number) {
        super(id, name, bs);
        this.monthlySalary = ms;
    }

    calculateSalary(): number {
        return this.monthlySalary;
    }

    getRole(): string {
        return "正式员工";
    }
}

class HourlyEmployee extends Employee {
    hourlyRate: number;
    hoursWorked: number;

    constructor(id: number, name: string, bs: number, hr: number, hw: number) {
        super(id, name, bs);
        this.hourlyRate = hr;
        this.hoursWorked = hw;
    }

    calculateSalary(): number {
        return this.hourlyRate * this.hoursWorked;
    }

    getRole(): string {
        return "小时工";
    }
}

function test(): void {
    let employees: Employee[] = [];
    employees.push(new SalariedEmployee(1, "thomas", 2000, 2500));
    employees.push(new HourlyEmployee(2, "Jim", 2000, 22, 8));

    for (let index = 0; index < employees.length; index++) {
        const employee = employees[index];
        console.log(`${employee.getInfo()} salary:${employee.calculateSalary()}`);
    }
}

练习5:综合应用

// 设计一个简单的游戏角色系统:

// 1. 定义抽象类 GameCharacter:
//    - 属性:name(string), hp(number), maxHp(number)
//    - 抽象方法:attack(target: GameCharacter): void
//    - 抽象方法:getCharacterType(): string
//    - 具体方法:takeDamage(damage: number): void 减少hp
//    - 具体方法:isAlive(): boolean 判断hp > 0
//    - 具体方法:getStatus(): string 返回角色状态

// 2. 定义 Warrior 继承 GameCharacter:
//    - 新增属性:strength(力量,number)
//    - 实现 attack():根据 strength 计算伤害
//    - 实现 getCharacterType() 返回 "战士"

// 3. 定义 Mage 继承 GameCharacter:
//    - 新增属性:mana(魔法值,number)
//    - 实现 attack():消耗 mana 造成魔法伤害
//    - 实现 getCharacterType() 返回 "法师"

// 4. 创建战斗函数 battle(char1: GameCharacter, char2: GameCharacter):
//    - 让两个角色互相攻击,直到一方死亡
//    - 使用 instanceof 判断角色类型,打印特殊信息

// 5. 测试:创建 Warrior 和 Mage 实例,进行战斗
点击查看答案
abstract class GameCharacter {
    name: string;
    hp: number;
    maxHp: number;

    constructor(name: string, maxHp: number) {
        this.name = name;
        this.maxHp = maxHp;
        this.hp = maxHp;
    }

    abstract attack(target: GameCharacter): void;
    abstract getCharacterType(): string;

    takeDamage(damage: number): void {
        this.hp -= damage;
        if (this.hp < 0) this.hp = 0;
    }

    isAlive(): boolean {
        return this.hp > 0;
    }

    getStatus(): string {
        return `${this.name}[${this.getCharacterType()}] HP:${this.hp}/${this.maxHp}`;
    }
}

class Warrior extends GameCharacter {
    strength: number;

    constructor(name: string, maxHp: number, strength: number) {
        super(name, maxHp);
        this.strength = strength;
    }

    attack(target: GameCharacter): void {
        let damage = this.strength * 2;
        target.takeDamage(damage);
        console.log(`${this.name} 用剑攻击,造成 ${damage} 点伤害!`);
    }

    getCharacterType(): string {
        return "战士";
    }
}

class Mage extends GameCharacter {
    mana: number;

    constructor(name: string, maxHp: number, mana: number) {
        super(name, maxHp);
        this.mana = mana;
    }

    attack(target: GameCharacter): void {
        if (this.mana >= 10) {
            this.mana -= 10;
            let damage = 25;
            target.takeDamage(damage);
            console.log(`${this.name} 释放火球术,造成 ${damage} 点伤害!剩余法力:${this.mana}`);
        } else {
            console.log(`${this.name} 法力不足,攻击失败!`);
        }
    }

    getCharacterType(): string {
        return "法师";
    }
}

function battle(char1: GameCharacter, char2: GameCharacter): void {
    console.log("=== 战斗开始 ===");
    console.log(char1.getStatus());
    console.log(char2.getStatus());
    
    let round = 1;
    while (char1.isAlive() && char2.isAlive()) {
        console.log(`\n--- 第 ${round} 回合 ---`);
        
        // char1 攻击 char2
        if (char1 instanceof Warrior) {
            console.log("战士发起猛烈攻击!");
        } else if (char1 instanceof Mage) {
            console.log("法师开始施法...");
        }
        char1.attack(char2);
        console.log(char2.getStatus());
        
        if (!char2.isAlive()) break;
        
        // char2 攻击 char1
        char2.attack(char1);
        console.log(char1.getStatus());
        
        round++;
    }
    
    console.log(`\n=== 战斗结束 ===`);
    console.log(char1.isAlive() ? `${char1.name} 获胜!` : `${char2.name} 获胜!`);
}

function test(): void {
    let warrior = new Warrior("亚瑟", 100, 15);
    let mage = new Mage("甘道夫", 80, 50);
    battle(warrior, mage);
}
posted @ 2026-04-14 19:02  thammer  阅读(10)  评论(0)    收藏  举报