从今天开始,我们将进入面向对象编程(OOP)的学习。面向对象是现代编程语言的核心思想,Dart 作为一门纯面向对象的语言,完全支持这一范式。本节课我们先从最基础的类与对象开始讲起,同时会特别注意 Dart 空安全机制带来的编程规范。
一、类与对象:面向对象的核心
在面向对象的世界里,类(Class) 是对事物的抽象描述,而对象(Object) 是类的具体实例。
举个例子:
- “汽车” 是一个类(它描述了汽车有轮子、能行驶等共同特征)
- 你家的那辆红色轿车就是 “汽车” 类的一个对象(具体存在的实例)
在 Dart 中,所有东西都是对象,包括数字、字符串等基础类型,它们都继承自 Object 类。
二、类的定义与对象创建
1. 定义类
使用 class 关键字定义类,类名通常采用大驼峰命名法(每个单词首字母大写)。
// 定义一个“人”类
class Person {
// 类的成员(变量和方法)将在这里定义
}
2. 创建对象
使用 new 关键字(可省略)创建类的实例(对象):
class Person {
// 空类(暂时没有成员)
}
void main() {
// 创建 Person 类的对象
Person p1 = new Person(); // 使用 new 关键字
Person p2 = Person(); // 省略 new(Dart 推荐写法)
print(p1); // 输出:Instance of 'Person'(表示这是 Person 类的实例)
print(p2); // 输出:Instance of 'Person'
}
三、成员变量与方法(空安全)
类由成员变量(也叫属性,描述事物的特征)和方法(描述事物的行为)组成。在 Dart 空安全机制下,非空类型的成员变量必须初始化,否则会报错。
1. 成员变量的定义与初始化
class Person {
// 方式1:直接初始化非空变量(推荐用于有默认值的场景)
String name = ""; // 非空字符串,初始化为空字符串
int age = 0; // 非空整数,初始化为0
// 方式2:声明为可空类型(后面加 ?,默认值为 null)
double? height; // 可空类型,允许为 null
String? address; // 可空字符串
}
void main() {
Person person = Person();
// 访问和修改成员变量(使用 . 语法)
person.name = "张三";
person.age = 20;
person.height = 1.75;
person.address = "北京市";
print(
"姓名:${person.name},年龄:${person.age},身高:${person.height}米,地址:${person.address}",
);
// 输出:姓名:张三,年龄:20,身高:1.75米,地址:北京市
}
2. 成员方法
方法是类内部定义的函数,用于实现对象的行为。
class Person {
// 成员变量(已初始化)
String name = "";
int age = 0;
// 成员方法(打招呼)
void sayHello() {
print("大家好,我叫$name,今年$age岁");
}
// 带参数的方法(计算几年后年龄)
int getAgeAfterYears(int years) {
return age + years;
}
}
void main() {
Person person = Person();
person.name = "李四";
person.age = 22;
// 调用方法
person.sayHello(); // 输出:大家好,我叫李四,今年22岁
int futureAge = person.getAgeAfterYears(5);
print("5年后年龄:$futureAge"); // 输出:5年后年龄:27
}
3. this 关键字
this 代表当前对象的引用,用于区分成员变量和局部变量(尤其是名称相同时):
class Person {
String name = "";
int age = 0;
// 使用 this 区分成员变量和参数(名称相同的场景)
void setInfo(String name, int age) {
this.name = name; // this.name 指成员变量,name 指参数
this.age = age; // this.age 指成员变量,age 指参数
}
void sayHello() {
// 名称不同时可省略 this
print("大家好,我叫$name,今年$age岁");
}
}
void main() {
Person person = Person();
person.setInfo("王五", 19); // 传入参数
person.sayHello(); // 输出:大家好,我叫王五,今年19岁
}
4. 空安全的核心原则
- 非空类型(
int、String等) :必须初始化(不能为null),且后续赋值也不能为null。 - 可空类型(
int?、String?等) :可以为null,默认值就是null,使用时需判断是否为null(可配合??运算符)。
class Person {
String? name; // 可空类型
int age = 0; // 非空类型
}
void main() {
Person p = Person();
print(p.name ?? "未知姓名"); // 输出:未知姓名(因 name 为 null)
print(p.age); // 输出:0(非空类型有初始值)
}
四、构造函数:初始化对象的特殊方法
构造函数是一种特殊的方法,用于在创建对象时初始化成员变量(尤其是非空类型)。它的名称与类名相同,没有返回值。
1. 默认构造函数
如果没有显式定义构造函数,Dart 会自动生成一个无参默认构造函数,但此时非空成员变量必须提前初始化:
class Person {
String name = ""; // 非空变量必须初始化
int age = 0; // 非空变量必须初始化
}
void main() {
Person p = Person(); // 调用默认构造函数
print("${p.name}, ${p.age}"); // 输出:, 0(使用初始值)
}
2. 自定义构造函数
我们可以自定义构造函数,在创建对象时直接初始化成员变量(推荐用于非空变量的初始化):
class Person {
String name; // 非空类型(在构造函数中初始化)
int age; // 非空类型(在构造函数中初始化)
double? height; // 可空类型(无需强制初始化)
// 自定义构造函数(带参数)
Person(this.name, this.age); // 简化写法:直接将参数赋值给成员变量
// 等价于完整写法:
// Person(String name, int age) {
// this.name = name;
// this.age = age;
// }
void sayHello() {
print("大家好,我叫$name,今年$age岁");
}
}
void main() {
// 创建对象时必须传入参数(初始化非空变量)
Person person = Person("赵六", 25);
person.sayHello(); // 输出:大家好,我叫赵六,今年25岁
}
3. 命名构造函数
一个类只能有一个默认构造函数,但可以有多个命名构造函数,用于提供不同的初始化方式。命名构造函数的格式是 类名.函数名。
class Person {
String name;
int age;
String? address; // 可空类型
// 默认构造函数
Person(this.name, this.age);
// 命名构造函数1:带地址的初始化
Person.withAddress(this.name, this.age, this.address);
// 命名构造函数2:从JSON数据初始化(初始化列表语法)
Person.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? "", // 初始化列表:确保非空(?? 是空值运算符)
age = json['age'] ?? 0 {
// 构造函数体:处理其他逻辑
address = json['address'];
}
void printInfo() {
print("姓名:$name,年龄:$age,地址:${address ?? '未知'}");
}
}
void main() {
// 使用默认构造函数
Person p1 = Person("孙七", 30);
p1.printInfo(); // 输出:姓名:孙七,年龄:30,地址:未知
// 使用命名构造函数 withAddress
Person p2 = Person.withAddress("周八", 28, "北京市");
p2.printInfo(); // 输出:姓名:周八,年龄:28,地址:北京市
// 使用命名构造函数 fromJson
Map<String, dynamic> json = {'name': "吴九", 'age': 35, 'address': "上海市"};
Person p3 = Person.fromJson(json);
p3.printInfo(); // 输出:姓名:吴九,年龄:35,地址:上海市
}
注意:命名构造函数中,初始化列表(
:后面的部分)用于初始化成员变量,必须放在函数体({})之前,主要用于处理非空变量的初始化。
4. 构造函数的可选参数
构造函数也可以使用可选参数(位置可选或命名可选),让初始化更灵活:
class Person {
String name;
int age;
String? gender; // 性别(可选)
// 命名构造函数:使用命名可选参数
Person(this.name, this.age, {this.gender});
void printInfo() {
print("姓名:$name,年龄:$age,性别:${gender ?? '未指定'}");
}
}
void main() {
Person p1 = Person("郑十", 22); // 不指定性别
p1.printInfo(); // 输出:姓名:郑十,年龄:22,性别:未指定
Person p2 = Person("王十一", 26, gender: "男"); // 指定性别
p2.printInfo(); // 输出:姓名:王十一,年龄:26,性别:男
}
五、对象的内存模型(简单理解)
为了更好地理解类和对象,我们简单了解一下它们在内存中的存储方式:
- 类是一种 “模板”,只在内存中存在一份
- 每个对象是根据模板创建的实例,各自占用独立的内存空间
- 对象中存储的是成员变量的值,成员方法则共享类的定义
class Person {
String name;
Person(this.name);
void sayHello() => print("Hello, $name");
}
void main() {
Person p1 = Person("A");
Person p2 = Person("B");
// p1 和 p2 是两个独立的对象,name 分别存储 "A" 和 "B"
// 但它们共享同一个 sayHello 方法的定义
}