dart学习第 7 节:面向对象(上)—— 类与对象的基本概念

80 阅读3分钟

从今天开始,我们将进入面向对象编程(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. 空安全的核心原则

  • 非空类型(intString 等) :必须初始化(不能为 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,性别:男
}


五、对象的内存模型(简单理解)

为了更好地理解类和对象,我们简单了解一下它们在内存中的存储方式:

  1. 类是一种 “模板”,只在内存中存在一份
  2. 每个对象是根据模板创建的实例,各自占用独立的内存空间
  3. 对象中存储的是成员变量的值,成员方法则共享类的定义
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 方法的定义
}