今天我们将深入面向对象的另外两个核心特性 ------继承 与多态,以及抽象类和接口的使用。这些特性能帮助我们构建更灵活、更具扩展性的代码结构。
一、继承(extends):代码复用的利器
继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,并可以在此基础上添加新的属性和方法,或修改父类的方法。这极大地提高了代码的复用性。
1. 基本语法与使用
使用 extends
关键字实现继承:
dart
// 父类(基类)
class Animal {
String name;
int age;
// 父类构造函数
Animal(this.name, this.age);
// 父类方法
void eat() {
print("$name 在吃东西");
}
void sleep() {
print("$name 在睡觉");
}
}
// 子类(派生类):继承自 Animal
class Dog extends Animal {
// 子类新增属性
String breed;
// 子类构造函数:必须调用父类构造函数(通过 super)
Dog(String name, int age, this.breed) : super(name, age);
// 子类新增方法
void bark() {
print("$name($breed)在汪汪叫");
}
}
void main() {
// 创建子类对象
Dog dog = Dog("旺财", 3, "金毛");
// 调用继承自父类的属性和方法
print("名字:${dog.name},年龄:${dog.age}"); // 输出:名字:旺财,年龄:3
dog.eat(); // 输出:旺财 在吃东西
dog.sleep(); // 输出:旺财 在睡觉
// 调用子类自身的方法
dog.bark(); // 输出:旺财(金毛)在汪汪叫
}
在这个例子中:
Animal
是父类,定义了所有动物的共同特征(name
、age
)和行为(eat
、sleep
)。Dog
是子类,通过extends Animal
继承了父类的属性和方法,同时新增了breed
属性和bark
方法。
2. 方法重写(@override):定制化父类行为
子类可以重写父类的方法,以实现自己的特有逻辑。使用 @override
注解标识重写的方法(可选,但推荐,提高代码可读性)。
dart
class Animal {
String name;
Animal(this.name);
void makeSound() {
print("$name 发出声音");
}
}
class Cat extends Animal {
Cat(String name) : super(name);
// 重写父类的 makeSound 方法
@override
void makeSound() {
print("$name 在喵喵叫"); // 覆盖父类的实现
}
}
class Bird extends Animal {
Bird(String name) : super(name);
// 重写父类的 makeSound 方法
@override
void makeSound() {
print("$name 在叽叽喳喳叫");
}
}
void main() {
Cat cat = Cat("咪咪");
cat.makeSound(); // 输出:咪咪 在喵喵叫
Bird bird = Bird("啾啾");
bird.makeSound(); // 输出:啾啾 在叽叽喳喳叫
}
方法重写的核心是:子类用自己的实现覆盖父类的方法,让相同的方法名表现出不同的行为 ------ 这正是多态的基础。
3. super 关键字:访问父类成员
super
关键字用于在子类中访问父类的属性、方法或构造函数:
super.方法名()
:调用父类的方法super.属性名
:访问父类的属性(较少用,通常直接访问)- 构造函数中
super(参数)
:调用父类的构造函数
dart
class Parent {
String name;
Parent(this.name);
void greet() {
print("Hello, I'm $name");
}
}
class Child extends Parent {
Child(String name) : super(name); // 调用父类构造函数
@override
void greet() {
super.greet(); // 调用父类的 greet 方法
print("I'm a child"); // 新增子类逻辑
}
}
void main() {
Child child = Child("小明");
child.greet();
// 输出:
// Hello, I'm 小明
// I'm a child
}
注意 :子类构造函数必须先调用父类构造函数(
super(...)
必须放在初始化列表的第一位),确保父类成员先初始化。
二、多态:同一行为的不同表现
多态是指父类引用可以指向子类对象,调用方法时会表现出子类的具体实现。简单说就是 "一个接口,多种实现"。
dart
// 父类
class Shape {
// 父类方法(多态的"接口")
double getArea() {
return 0.0;
}
}
// 子类1:圆形
class Circle extends Shape {
double radius;
Circle(this.radius);
@override
double getArea() {
return 3.14 * radius * radius; // 圆的面积公式
}
}
// 子类2:矩形
class Rectangle extends Shape {
double width;
double height;
Rectangle(this.width, this.height);
@override
double getArea() {
return width * height; // 矩形的面积公式
}
}
void main() {
// 父类引用指向子类对象(多态的核心)
Shape shape1 = Circle(5); // 圆的半径为5
Shape shape2 = Rectangle(4, 6); // 矩形的宽4、高6
// 调用相同的方法,表现出不同的行为
print("圆的面积:${shape1.getArea()}"); // 输出:圆的面积:78.5
print("矩形的面积:${shape2.getArea()}"); // 输出:矩形的面积:24.0
}
多态的优势在于:可以编写通用的代码操作父类,而无需关心具体的子类类型 ,当新增子类(如三角形)时,通用代码无需修改,符合 "开闭原则"。
三、抽象类(abstract):定义规范的模板
抽象类是不能被实例化 的类,通常用于定义一组必须被子类实现的方法(抽象方法),自身可以包含已实现的方法。使用 abstract
关键字定义。
1. 基本用法
dart
// 抽象类
abstract class Vehicle {
// 抽象方法(只有声明,没有实现,子类必须重写)
void run();
// 普通方法(有实现)
void stop() {
print("车辆停止");
}
}
// 子类1:汽车
class Car extends Vehicle {
@override
void run() {
print("汽车在公路上行驶");
}
}
// 子类2:自行车
class Bicycle extends Vehicle {
@override
void run() {
print("自行车在自行车道行驶");
}
}
void main() {
// 抽象类不能实例化(编译错误)
// Vehicle v = Vehicle();
Vehicle car = Car();
car.run(); // 输出:汽车在公路上行驶
car.stop(); // 输出:车辆停止
Vehicle bike = Bicycle();
bike.run(); // 输出:自行车在自行车道行驶
}
抽象类的核心作用是定义 "规范" :通过抽象方法规定子类必须实现的功能,而普通方法提供通用逻辑,实现 "规范与实现分离"。
四、接口(implements):多维度的规范约束
Dart 中没有专门的 interface
关键字,任何类都可以作为接口 。通过 implements
关键字让一个类 "实现" 接口,此时该类必须重写接口中的所有方法和属性(无论是否抽象)。
接口与继承的区别:
- 继承(
extends
):子类与父类是 "is-a" 关系(如 Dog 是 Animal),复用代码。 - 实现接口(
implements
):类与接口是 "has-a" 关系(如 Bird 实现 Flyable),表示具备某种能力。
1. 基本用法
dart
// 定义接口(普通类)
class Flyable {
// 接口中的方法(隐式要求实现类重写)
void fly() {
// 接口的实现通常为空,仅作为规范
}
}
class Swimmable {
void swim() {}
}
// 实现一个接口
class Bird implements Flyable {
@override
void fly() {
print("鸟在天上飞");
}
}
// 实现多个接口(用逗号分隔)
class Duck implements Flyable, Swimmable {
@override
void fly() {
print("鸭子在低空飞");
}
@override
void swim() {
print("鸭子在水里游");
}
}
void main() {
Bird bird = Bird();
bird.fly(); // 输出:鸟在天上飞
Duck duck = Duck();
duck.fly(); // 输出:鸭子在低空飞
duck.swim(); // 输出:鸭子在水里游
}
2. 用抽象类作为接口
更推荐用抽象类作为接口,因为抽象方法更明确地表示 "必须实现":
dart
// 抽象类作为接口
abstract class Runnable {
void run(); // 抽象方法,无实现
}
class Person implements Runnable {
@override
void run() {
print("人在跑步");
}
}
class Robot implements Runnable {
@override
void run() {
print("机器人在移动");
}
}
五、继承与接口的综合案例
我们用一个 "游戏角色" 案例综合运用继承和接口:
dart
// 父类:角色
class Role {
String name;
int hp;
Role(this.name, this.hp);
void takeDamage(int damage) {
hp -= damage;
print("$name 受到 $damage 点伤害,剩余血量:$hp");
}
}
// 接口:攻击能力
abstract class Attackable {
void attack(Role target);
}
// 接口:治疗能力
abstract class Healable {
void heal(Role target, int amount);
}
// 战士:继承 Role,实现 Attackable
class Warrior extends Role implements Attackable {
Warrior(String name, int hp) : super(name, hp);
@override
void attack(Role target) {
print("$name 用剑攻击了 ${target.name}");
target.takeDamage(20);
}
}
// 牧师:继承 Role,实现 Attackable 和 Healable
class Priest extends Role implements Attackable, Healable {
Priest(String name, int hp) : super(name, hp);
@override
void attack(Role target) {
print("$name 用魔杖攻击了 ${target.name}");
target.takeDamage(10);
}
@override
void heal(Role target, int amount) {
target.hp += amount;
print("$name 治疗了 ${target.name},恢复 $amount 点血量,当前血量:${target.hp}");
}
}
void main() {
Warrior warrior = Warrior("战士", 100);
Priest priest = Priest("牧师", 80);
// 战士攻击牧师
warrior.attack(priest); // 输出:战士 用剑攻击了 牧师;牧师 受到 20 点伤害,剩余血量:60
// 牧师治疗自己
priest.heal(priest, 15); // 输出:牧师 治疗了 牧师,恢复 15 点血量,当前血量:75
// 牧师攻击战士
priest.attack(warrior); // 输出:牧师 用魔杖攻击了 战士;战士 受到 10 点伤害,剩余血量:90
}
这个案例中:
Role
作为父类,封装了所有角色的共同属性(name
、hp
)和方法(takeDamage
)。Attackable
和Healable
作为接口,定义了 "攻击" 和 "治疗" 的能力规范。Warrior
和Priest
通过继承和实现接口,形成了不同的角色特性,体现了继承的复用性和接口的灵活性。