Dart:面向对象(OOP)

对象和类

类(Class)的定义

类是对象的模板,定义了对象的属性(数据)和方法(行为)。

在 Dart 中,用 class 关键字定义类。

class Person {
  String name;   // 属性
  int age;       // 属性

  void sayHello() {  // 方法
    print('Hello, my name is $name');
  }
}

对象(Object)的创建

对象是类的实例

Dart 2 之后,创建对象时可以省略 new 关键字(推荐省略)。

void main() {
  var p1 = Person(); // 省略 new
  var p2 = new Person(); // 旧写法,功能一样
}

构造函数(Constructor)

构造函数在创建对象时被调用,用来初始化对象的状态。

默认构造函数

如果你没写构造函数,Dart 会自动生成一个无参构造函数。

class Person {
  // Null Safety 之后:非空类型字段必须初始化。
  // Dart 会帮你生成无参构造函数,但前提是所有非空字段都有默认值或在构造函数里赋值。
  // 如果没有提供初始值,必须在构造函数中初始化。
  String name = '';
  int age = 0;
}

void main() {
  var p = Person(); // 自动调用默认构造函数
  print(p.name); // 输出: ''
  print(p.age); // 输出: 0
}

自定义构造函数

最常用的写法是直接用参数初始化属性。

class Person {
  String name;
  int age;

  Person(this.name, this.age); // 简化写法
}

void main() {
  var p = Person('Alice', 25);
  print('Name: ${p.name}, Age: ${p.age}'); // 输出: Name: Alice, Age: 25
}

初始化列表

是什么

在 Dart 里,初始化列表(initializer list)就是构造函数在执行函数体 { ... } 之前,用来给实例变量赋值的一个特殊语法。
它用一个冒号 : 开头,写在构造函数参数列表后面。

为什么有初始化列表

Dart 对象的创建过程分两步:

  1. 初始化阶段
    • Dart 会先调用初始化列表,把字段(实例变量)赋上值。
    • 这个阶段必须保证 非空字段(non-nullable fields) 已经有值。
  2. 构造函数体执行阶段
    • 初始化完成后,才会执行 { ... } 里的代码。

因为 Dart 的 null safety 要求非空字段在初始化阶段必须赋值,所以初始化列表是非常重要的工具。

案例

class Point {
  int x;
  int y;

  // 初始化列表
  Point(int a, int b)
    : x = a, // 在构造函数体执行前赋值
      y = b {
    print('构造函数执行前:a = $a, b = $b'); // 输出: a = 3, b = 4
    x = x + 1;
    y = y + 1;
    print('构造函数体执行后:x = $x, y = $y'); // 输出: x = 4, y = 5
  }
}

void main() {
  var p = Point(3, 4);
  print('${p.x}, ${p.y}'); // 输出: 4, 5
}

执行顺序:

  1. 调用 Point(3, 4)

  2. 初始化列表先执行 → x = 3, y = 4

  3. 然后执行构造函数体

    {
      print('构造函数执行前:a = $a, b = $b'); // 输出: a = 3, b = 4
      x = x + 1;
      y = y + 1;
      print('构造函数体执行后:x = $x, y = $y'); // 输出: x = 4, y = 5
    }
    

命名构造函数

可以定义多个不同名字的构造函数。

class Point {
  num x, y;
  Map origin1, origin2;

  Point.fromJson(Map json)
    : x = json['x'],
      y = json['y'],
      origin1 = {'x': json['x'], 'y': json['y']},
      origin2 = {'x': json['x'] + 10, 'y': json['y'] + 10};
}

void main(List<String> args) {
  var p = Point.fromJson({"x": 1, "y": 2});
  print('x: ${p.x}, y: ${p.y}'); // 输出: x: 1, y: 2
  print('origin1: ${p.origin1}'); // 输出: origin1: {x: 1, y: 2}
  print('origin2: ${p.origin2}'); // 输出: origin2: {x: 11, y: 12}
}

重定向构造函数(Redirecting Constructor)

当一个构造函数不自己执行初始化逻辑,而是把工作交给类中的另一个构造函数时,就用重定向构造函数。
这样可以避免重复代码,尤其是在多个构造函数里初始化逻辑一样的情况下。

class Person {
  String name;
  int age;

  // 主构造函数
  Person(this.name, this.age);

  // 重定向构造函数:交给 Person(this.name, this.age) 处理
  Person.guest() : this('Guest', 0);
}

void main() {
  var p1 = Person('Alice', 25);
  var p2 = Person.guest();

  print('${p1.name}, ${p1.age}'); // Alice, 25
  print('${p2.name}, ${p2.age}'); // Guest, 0
}

特点

  • 重定向构造函数没有函数体(没有 {}
  • 必须直接调用 this(...)this.namedConstructor(...)
  • 只能调用本类的其他构造函数,不能调用父类的构造函数(父类用 super

成员(字段 & 方法)

一般使用

字段(Field):存储对象的状态。

方法(Method):定义对象的行为。

class Circle {
  double radius;

  Circle(this.radius);

  double area() {
    return 3.14 * radius * radius;
  }
}

静态方法和方法

属于类本身,而不是某个实例

static 关键字声明

无法访问实例成员(只能访问静态成员)

class MathUtils {
  static double pi = 3.14159;

  static double square(double num) {
    return num * num;
  }
}

void main() {
  print(MathUtils.pi);
  print(MathUtils.square(5));
}

Dart 对象方法 vs Java 对象方法 的对比表

特性 Dart Java 说明
实例方法定义 直接写在类里,默认 public 写在类里,需要访问修饰符(publicprivateprotected Dart 没有访问修饰符,靠 _ 控制库级私有
静态方法 static 关键字 static 关键字 用法一致,但 Dart 静态方法不能访问实例成员
抽象方法 abstract 修饰类,在抽象类中定义方法签名 abstract 修饰方法(类必须是抽象类) 两者几乎一致
重写(Override) @override 注解(可省略) @Override 注解(可省略) 推荐都写,方便 IDE 检查
可见性范围 _ 私有(库级别),无 protected public/protected/private/包级可见 Dart 没有 protected
this 可省略(无命名冲突时) 可省略(无命名冲突时) 用法几乎一样
方法参数 支持命名参数 {} 和位置参数 [] 只支持位置参数 Dart 方法定义更灵活
可选参数 有(命名/位置可选) 无(需重载方法) Dart 可以少写很多重载方法
构造方法 ClassName(...),支持命名构造函数、初始化列表、重定向构造 与类名同名,无返回类型,支持重载 Dart 的构造更灵活,支持 ClassName.named()
可调用类(call) 支持 call() 方法,让对象像函数一样调用 不支持 Dart 特有功能
访问成员 obj.method() obj.method() 调用方式相同
必须声明返回值类型 不是必须,可以省略写 dynamic 或不写(默认 dynamic 必须明确写返回类型(void、具体类型等) Dart 更宽松,但强类型项目最好写上
支持可空类型 String? 表示返回值可为 null Java 所有引用类型默认可为 null(无语法提示) Dart 的 ? 更安全,编译期能提醒
void void 表示无返回值 void 表示无返回值 一致
函数作为返回值 支持返回函数对象(高阶函数) Java 8+ 支持 lambda/函数式接口 Dart 更直接

可选参数 vs Java 重载

Dart

class Person {
  void greet({String name = 'Guest'}) {
    print('Hello, $name');
  }
}

Java

class Person {
  void greet() { 
    greet("Guest"); 
  }
  void greet(String name) { 
    System.out.println("Hello, " + name); 
  }
}

➡ Dart 一个方法就能完成 Java 需要方法重载的场景。

Dart 返回类型推断规则表

flowchart TD
    A[方法声明时是否写返回类型?] -->|是| B[按声明的类型编译检查]
    A -->|否| C[方法体中有 return 吗?]
    C -->|否| D[推断为 void]
    C -->|return; 空| D
    C -->|return 值| E[所有 return 值类型相同?]
    E -->|是| F[推断为该类型]
    E -->|否| G[推断为公共父类类型 Object 或 Object?]

不写类型 → 编译器会根据 return 推断类型;没有 return 就是 void

写了类型 → 必须返回该类型的值(null-safety 下路径必须全覆盖)。

Setter 必须 void(可不写,但效果一样)。

OOP: 封装性与GetSet方法

只有两种可见性

Dart 和 Java、C++ 这些语言不太一样,它没有 public / protected / private / default 这样的访问修饰符关键字。它的访问控制规则非常简单:

  • 公有(public):默认就是公有,类的属性和方法外部都能访问
  • 私有(private):在名字前面加下划线 _,只能在**同一个库(library)**内部访问
class Circle {
  double radius;      // 公有属性
  double _diameter;   // 私有属性(仅当前库可见)

  Circle(this.radius) : _diameter = radius * 2;

  double area() {     // 公有方法
    return 3.14 * radius * radius;
  }

  double _perimeter() { // 私有方法
    return 2 * 3.14 * radius;
  }
}

库(library)级别的私有

Dart 里的 _ 私有并不是“类级别的”,而是“库级别的”。

  • 同一个库里,即使是不同类,也能访问 _ 开头的成员
  • 不同库,即使是继承,也不能访问 _ 成员

什么叫做同一个库里

默认情况下

  • 一个 Dart 文件 就是一个独立的库

  • _ 开头的成员只能在这个文件(库)里访问

📂 project/
 ├── main.dart          // 一个库
 ├── utils.dart         // 另一个库

多文件合并成一个库

如果你用 part / part of,可以把多个文件合成一个库,这样它们之间可以互访私有成员。

📂 project/
 ├── my_library.dart    // 主库文件
 ├── src/
 │    ├── file_a.dart   // part of 主库
 │    ├── file_b.dart   // part of 主库

my_library.dart

library my_library;

part 'src/file_a.dart';
part 'src/file_b.dart';

file_a.dart

part of my_library;

String _secret = 'from A';

file_b.dart

part of my_library;

void showSecret() {
  print(_secret); // ✅ 可以访问 file_a.dart 的私有变量
}

简图展示

📂 project/
 ├── main.dart        ← 库 A(只能访问自己 _ 成员)
 ├── utils.dart       ← 库 B(只能访问自己 _ 成员)
 ├── my_library.dart  ← 库 C(包含多个 part 文件)
 │
 └── src/
      ├── file_a.dart ← part of 库 C(可访问库 C 所有 _ 成员)
      ├── file_b.dart ← part of 库 C(可访问库 C 所有 _ 成员)

OOP:封装与隐藏

概念

  • 封装指的是把数据(属性)和操作数据的方法(行为)包装在类中,防止外部随意访问和修改内部细节。
  • 隐藏则是限制外部访问,保证对象的内部状态安全。

Dart 中的封装与隐藏体现:

  • 私有成员:Dart 通过变量或方法名前加 _(下划线)实现库级私有,即只在当前 Dart 文件内可见,外部文件无法访问。
  • 这样可以隐藏类的内部实现细节,只暴露必要的接口。
class BankAccount {
  String _accountNumber;  // 私有字段
  double _balance = 0;    // 私有字段

  BankAccount(this._accountNumber);

  double get balance => _balance;  // 只读公开访问器

  void deposit(double amount) {
    if (amount > 0) {
      _balance += amount;
    }
  }

  void withdraw(double amount) {
    if (amount > 0 && amount <= _balance) {
      _balance -= amount;
    }
  }
}

void main() {
  var account = BankAccount('123456');
  account.deposit(1000);
  print(account.balance);  // 1000
  // account._balance = 5000;  // 报错:私有变量不能访问
}

为什么要把属性设置成私有?

  • 控制访问权限:直接暴露属性给外部意味着外部代码可以随意读写,容易导致对象状态被任意更改,破坏数据完整性。
  • 隐藏内部实现细节:封装的目的是屏蔽实现细节,只暴露接口,让使用者只关心“能做什么”,而不是“怎么做”。
  • 方便后续维护和扩展:如果以后你想对属性访问做额外逻辑(校验、转换、懒加载等),用 getter/setter 就能做到。如果直接暴露属性,后续修改会很困难,甚至破坏向后兼容。

既然有 getter 和 setter,为什么还要私有属性?

getter 和 setter 是“受控访问”,它们是你定义好的访问接口,你可以在里面加入逻辑:

  • 验证数据是否合法
  • 触发事件、通知
  • 懒加载计算

如果属性是公有的,外部随时改值,你没法拦截,也没法做额外处理。

class Person {
  String _name;

  Person(this._name);

  // getter
  String get name => _name;

  // setter,设置时检查非空
  set name(String value) {
    if (value.isEmpty) {
      throw ArgumentError('Name cannot be empty');
    }
    _name = value;
  }
}

void main() {
  var p = Person('Tom');
  p.name = '';  // 会抛异常
}

如果不设置私有,会怎样?

外部可以随意赋值,破坏对象状态,比如:

class Person {
  String name;  // 公开属性
}

void main() {
  var p = Person();
  p.name = "";   // 可能导致后续逻辑错误
}

如果你后来想加入验证,只能改接口,影响调用方。

代码耦合度高,不易维护。

OOP:继承与extend

基本使用

继承:子类(Subclass)从父类(Superclass)获得属性方法,并可以扩展或**重写(override)**它们。

在 Dart 中:

  • extends 表示类继承
  • 只能单继承(一个类只能有一个直接父类)
  • 如果没写 extends,默认继承 Object

extends 的作用

  • 继承父类成员(非私有的属性和方法)
  • 允许子类重写父类方法
  • 可以调用父类的构造函数和方法(通过 super
class Animal {
  String name = '';

  void speak() {
    print('$name makes a sound.');
  }
}

class Dog extends Animal {
  void bark() {
    print('$name says: Woof!');
  }

  // 重写父类方法
  @override
  void speak() {
    print('$name is barking.');
  }
}

void main() {
  var dog = Dog();
  dog.name = 'Buddy';  // 继承来的属性
  dog.speak();         // 调用重写的方法
  dog.bark();          // 调用子类自己的方法
}

调用父类构造函数

class Animal {
  String name;

  Animal(this.name);
  getAnimalInfo() {
    return 'Animal name: $name';
  }
}

class Dog extends Animal {
  int age;
  // 使用初始化列表调用父类构造函数。但是也可以直接使用 super.name 来调用父类的构造函数。
  // Dog(String name, this.age) : super(name);
  // super.name 是指父类的 name 属性,super(name) 是调用父类的构造函数。
  // 如果父类有多个构造函数,可以使用 super.构造函数名()
  Dog(super.name, this.age); // 调用父类构造函数
}

在 Dart 中,父类的 非默认构造函数 必须显式调用。

class Animal {
  String name;

  Animal(this.name);
  Animal.namedConstructor(String namedConstructor) : name = namedConstructor {
    print('Named constructor called with name: $namedConstructor');
  }

  getAnimalInfo() {
    return 'Animal name: $name';
  }
}

class Dog extends Animal {
  int age;
  // 如果父类有多个构造函数,可以使用 super.构造函数名()
  Dog(String name, this.age) : super.namedConstructor(name);
}

OOP:多态–继承、抽象与接口

基本概念

什么是多态(Polymorphism)

多态指的是:同一个方法调用,根据对象的实际类型不同,会表现出不同的行为

换句话说——父类引用指向不同的子类对象时,调用同名方法能产生不同效果

多态的核心意义:

  1. 解耦调用方与实现方:调用方只依赖父类(或接口)类型,不关心具体子类是谁。
  2. 可扩展性强:增加新子类不需要改调用方代码,只需保证实现父类方法。
  3. 统一接口,差异实现:比如 draw() 方法:圆、矩形、三角形实现不同,但调用方都用 Shape 接口调用。

Dart 中体现多态的语法

  • 继承(extends)
  • 抽象类(abstract class)
  • 接口实现(implements)

继承(extends)

父类定义通用方法,子类重写(override)它

class Animal {
  void speak() {
    print('Animal sound');
  }
}

class Dog extends Animal {
  @override
  void speak() => print('Dog barks');
}

class Cat extends Animal {
  @override
  void speak() => print('Cat meows');
}
void main() {
  Animal a = Dog(); // 父类引用指向子类的实例
  a.speak(); // Dog barks(运行时看对象类型,不是引用类型)
}

抽象类(abstract class)

什么是抽象类

抽象类是 不能直接实例化 的类,通常用来 定义一组通用的属性和方法规范,并让子类去实现它们。

它的关键特征:

  • 可以包含抽象方法(没有方法体)
  • 也可以包含普通方法(有方法体)
  • 只能被继承(extends)或者实现(implements
  • 抽象方法必须由子类实现

语法:

abstract class Animal {
  void speak(); // 抽象方法(无方法体)
  void sleep() { // 普通方法(有实现)
    print('Sleeping...');
  }
}

为什么要有抽象类

  1. 定义规范:父类规定所有子类必须实现哪些方法(比如接口要求)。
  2. 减少重复代码:抽象类可以写通用逻辑(普通方法),子类直接继承使用。
  3. 多态支持:可以用父类类型去引用不同的子类实例,实现多态。

基本例子

abstract class Animal {
  void speak(); // 抽象方法
  void eat() {
    print('Animal is eating');
  }
}

class Dog extends Animal {
  @override
  void speak() {
    print('Dog barks');
  }
}

void main() {
  // Animal a = Animal(); ❌ 报错:不能直接实例化
  Animal dog = Dog();
  dog.eat();   // Animal is eating
  dog.speak(); // Dog barks
}

抽象类 与 多态

void makeSpeak(Animal animal) {
  animal.speak();
}

void main() {
  makeSpeak(Dog()); // Dog barks
  makeSpeak(Cat()); // Cat meows
}

这里 makeSpeak 只认 Animal,但实际调用的是子类版本 → 多态。

接口实现(implements

什么是 Dart 的 implements

在 Dart 里,任何类都可以当作接口来实现,不需要专门用 interface 关键字。

implements 关键字表示 实现一个类或抽象类定义的所有成员,并且必须自己重新写一份实现(即使父类里已经有默认实现,也不能直接继承用)。

基本语法:

class 接口类 { ... }
class 实现类 implements 接口类 { ... }

把类当接口用

class Person {
  String name = 'Unknown';   // 公共属性
  int _age = 0;              // 私有属性(库内可见)

  void greet() {             // 公共方法
    print('Hello, my name is $name.');
  }

  void _showAge() {          // 私有方法(库内可见)
    print('I am $_age years old.');
  }
}

// 不再同一个文件夹:
import 'Person.dart';
class Student implements Person {
  @override
  String name = '';
  int _age = 0; // 自己的私有字段(和 Person 的 _age 无关)


  // 必须实现 greet 方法
  @override
  void greet() {
    print('Hi, I am $_name, a student.');
  }
}
类成员类型 implements 是否需要实现 原因
公共属性 (name) ✅ 必须实现 公共 API 属于接口的一部分
私有属性 (_age) ❌ 不需要实现 私有成员在库外不可见,不属于接口规范
公共方法 (greet()) ✅ 必须实现 公共 API 属于接口的一部分
私有方法 (_showAge()) ❌ 不需要实现 私有成员在库外不可见,不属于接口规范

私有成员(以 _ 开头)在 库外(不同文件且不同库)不可见,不会进入 implements 约束。

公共成员(属性和方法)会被 implements 转换成需要实现的抽象方法/getter/setter。

implements 是完全按接口契约来,不会继承任何实现,必须自己重写。

把抽象类做接口

abstract class IPerson {
  String name;
  int age;

  IPerson(this.name, this.age);

  String info() {
    return 'Name: $name, Age: $age';
  }
}

class Teacher implements IPerson {
  @override
  String name;

  @override
  int age;

  Teacher(this.name, this.age);

  @override
  String info() {
    return 'Teacher -> Name: $name, Age: $age';
  }
}

class Student implements IPerson {
  @override
  int age;

  @override
  String name;

  Student(this.name, this.age);

  @override
  String info() {
    return 'Student -> Name: $name, Age: $age';
  }
}

// 调用这个方法:体现接口的多态性
void makePersonInfo(IPerson user) => print(user.info());
void main(List<String> args) {
  var t = Teacher('ducafecat', 99);
  t.age = 10; // 可以修改 age,因为 Teacher 实现了 IPerson 接口
  t.name = 'abc'; // 可以修改 name,因为 Teacher 实现了 IPerson 接口
  makePersonInfo(t); // 输出: Teacher -> Name: abc, Age: 10

  var s = Student('hans', 66);
  s.age = 20; // 可以修改 age,因为 Student 实现了 IPerson 接口
  s.name = 'def'; // 可以修改 name,因为 Student 实现了 IPerson 接口
  makePersonInfo(s); // 输出: Student -> Name: def, Age: 20
}

履行多接口

// 定义一个接口 IPerson 和 ISchool
abstract class IPerson {
  String name;
  int age;

  IPerson(this.name, this.age);

  String info() {
    return 'Name: $name, Age: $age';
  }
}

abstract class ISchool {
  int grade;

  ISchool(this.grade);

  String schoolInfo() {
    return 'Grade: $grade';
  }
}

// Teacher 实现了 IPerson 接口
class Teacher implements IPerson {
  @override
  String name;

  @override
  int age;

  Teacher(this.name, this.age);

  @override
  String info() {
    return 'Teacher -> Name: $name, Age: $age';
  }
}

// Student 实现了 IPerson 和 ISchool 接口
// 注意:Student 类必须实现所有接口中的属性和方法
class Student implements IPerson, ISchool {
  @override
  int age;

  @override
  String name;

  @override
  int grade;

  Student(this.name, this.age, this.grade);

  @override
  String info() {
    return 'Student -> Name: $name, Age: $age';
  }

  @override
  String schoolInfo() {
    return 'School -> Name: $name, Age: $age, Grade: $grade';
  }
}

// 调用这个方法:体现接口的多态性
// 这里的 IPerson 和 ISchool 是接口
void makePersonInfo(IPerson user) => print(user.info());
void makeSchoolInfo(ISchool user) => print(user.schoolInfo());

void main(List<String> args) {
  var t = Teacher('ducafecat', 99);
  makePersonInfo(t); // 输出: Teacher -> Name: ducafecat, Age: 99

  var s = Student('hans', 66, 5);
  makePersonInfo(s); // 输出: Student -> Name: hans, Age: 66
  makeSchoolInfo(s); // 输出: School -> Name: hans, Age: 66, Grade: 5
}

工厂函数

什么是工厂函数?

  • 工厂函数是一种特殊的构造函数,用 factory 关键字声明。
  • 不一定返回新实例,可以返回已有对象、子类实例,或者缓存的对象。
  • 本质上是一个静态方法,负责“生产”对象,但可以灵活控制对象的创建过程。

工厂函数怎么使用?

  • 在类中定义 factory 构造函数,返回一个类实例。

  • 通常配合私有构造函数 _ 使用,控制实例化过程。

单例模式

class Logger {
  Logger._internal(); // 私有构造函数

  static final Logger _instance = Logger._internal();
  
  // 工厂构造函数,返回单例实例
  factory Logger() {
    return _instance; // 返回单例
  }
}

void main() {
  var a = Logger(); // 这里调用了工厂构造函数
  var b = Logger(); // 再次调用工厂构造函数
  print(identical(a, b)); // true,a 和 b 是同一个实例
}

Logger._internal(); 是什么?

  • 这是 一个私有命名构造函数
  • _internal构造函数的名字,可以是任意合法标识符。
  • 在 Dart 中,前缀 _ 表示私有成员,只在当前库(文件)内可见,外部不能访问。
  • 它的作用是:阻止外部直接调用默认构造函数创建实例,只能通过工厂函数来创建/获取实例

static final Logger _instance = Logger._internal(); 是什么?

  • 这是 定义了一个静态的、最终的(不可变)实例变量,存储一个 Logger 类的实例。
  • 这个实例是通过调用私有的命名构造函数 Logger._internal() 创建的。
  • 因为 _instancestatic,它属于类本身而非某个对象,整个程序中只有这一个实例。

单例模式的作用和意义

作用 说明
控制资源访问 例如数据库连接、日志记录器等需要唯一实例避免冲突
节省内存和性能开销 避免重复创建对象,降低资源消耗
全局共享状态 保证多个组件访问同一个对象,数据保持一致
避免不一致和错误 避免因为多个实例造成状态不同步或不一致的问题

返回不同子类实例

abstract class Animal {
  factory Animal(String type) {
    if (type == 'dog') return Dog();
    if (type == 'cat') return Cat();
    throw 'Unknown animal';
  }
  void speak();
}

class Dog implements Animal {
  void speak() => print('Woof!');
}

class Cat implements Animal {
  void speak() => print('Meow!');
}

void main() {
  Animal a = Animal('dog');
  a.speak(); // Woof!
}
  • 在没有工厂函数之前:用户自己决定要哪个类的实例。但是客户端必须知道具体类的构造,耦合高。
  • 有了工程方法之后:
    • 客户端只调用一个构造方法 Animal(),不关心具体实现。
    • 可灵活切换返回不同子类实例,符合开放封闭原则。

×

喜欢就点赞,疼爱就打赏