Flutter面试:Dart基础2

254 阅读18分钟

文章目录

Dart是不是单线程模型?是如何运行的?

一句话总结

  • Dart 是单线程模型,它的核心是事件循环机制,通过微任务队列事件队列来高效地处理异步操作,从而保证了UI线程的流畅性。

核心概念

Dart是单线程模型,不必像在Java或C#中那样担心共享内存的锁竞争和线程同步问题。运行机制可以从以下三个层面来理解:

1. 核心机制:事件循环 (Event Loop)

Dart 的事件循环依赖两个有序队列,按优先级执行:

  • 微任务队列 (Microtask Queue) :优先级最高;通过 scheduleMicrotask() 显式添加,附着在这个 Future 上的回调(包括 thencatchErrorwhenComplete 以及 await 的续延代码)都会被调度到微任务队列中执行。
  • 事件队列 (Event Queue) :我们日常使用的绝大部分异步操作都来源于这里,例如I/O操作(文件、网络请求)、手势识别绘图消息Timer (Future.delayed)。
事件循环的工作流程图:
flowchart TD
    A[事件循环启动] --> B{微任务队列<br>是否为空?}
    B -- 否 --> C[取出并执行<br>一个微任务]
    C --> B
    B -- 是 --> D{事件队列<br>是否为空?}
    D -- 否 --> E[取出并执行<br>一个事件]
    E --> F[事件执行完毕]
    F --> B
    D -- 是 --> G[等待新事件到来]
    G --> B
事件循环代码演示:
void main() {
  // ------------------------------
  // 1. 同步代码(优先执行,事件循环启动前)
  // ------------------------------
  print("1. 同步代码 - 开始执行");

  // ------------------------------
  // 2. 微任务队列(Microtask Queue):优先级高于事件队列
  //    通过 Future.microtask 添加
  // ------------------------------
  Future.microtask(() {
    print("2. 微任务A - 微任务队列");
  });

  // ------------------------------
  // 3. 事件队列(Event Queue):优先级低于微任务队列
  //    通过 Future 或 Future.delayed 添加(即使延迟0秒)
  // ------------------------------
  // 事件1:执行时会新增微任务
  Future(() {
    print("4. 事件1 - 事件队列(开始执行)");
    // 在事件执行过程中新增微任务
    Future.microtask(() {
      print("5. 微任务B(事件1中新增) - 微任务队列");
    });
    print("6. 事件1 - 事件队列(执行结束)");
  });

  // 再添加一个微任务(微任务队列按添加顺序执行)
  Future.microtask(() {
    print("3. 微任务C - 微任务队列");
  });

  // 事件2:在事件1之后执行
  Future.delayed(Duration.zero, () {
    print("7. 事件2 - 事件队列");
  });

  // ------------------------------
  // 1. 同步代码(继续执行)
  // ------------------------------
  print("8. 同步代码 - 执行结束(事件循环即将启动)");
}
执行结果:
1. 同步代码 - 开始执行
8. 同步代码 - 执行结束(事件循环即将启动)
2. 微任务A - 微任务队列
3. 微任务C - 微任务队列
4. 事件1 - 事件队列(开始执行)
6. 事件1 - 事件队列(执行结束)
5. 微任务B(事件1中新增) - 微任务队列
7. 事件2 - 事件队列
步骤拆解(对应执行结果):
  1. 同步代码优先执行
    打印 1 和 8(同步代码不进入任何队列,事件循环启动前就执行完毕)。
  2. 事件循环启动,先清空微任务队列
    按添加顺序执行微任务 A 和 C,打印 2 和 3(微任务队列此时为空)。
  3. 处理事件队列的第一个事件(事件 1)
    • 执行事件 1 的代码,打印 4 和 6
    • 事件 1 执行过程中新增微任务 B(加入微任务队列)。
  4. 事件 1 执行完毕后,再次清空微任务队列
    执行新增的微任务 B,打印 5(微任务队列再次为空)。
  5. 处理事件队列的下一个事件(事件 2)
    执行事件 2,打印 7(事件队列此时为空)。
  6. 所有队列清空,程序结束

2. 实现异步的关键:Future 和 async/await

  • Future:它不是一个并行计算的结果,而是一个  “承诺”  ,承诺在未来某个事件循环轮次中给你一个值(或一个错误)。当你调用http.get()Future.delayed()时,Dart会立刻返回一个Future对象,并立即返回(不会阻塞),I/O操作(网络/文件)由操作系统异步处理(Dart线程之外),主线程(Dart线程)仅处理回调。
  • async/await:这只是编写异步代码的  “语法糖”  ,让异步代码看起来和同步代码一样直观。在函数前加上async关键字,其返回值会被自动包装为Futureawait关键字的作用是:告诉事件循环“我这边有个Future需要一些时间才能完成,你先去处理其它事件(微任务或UI事件),等这个Future有结果了再回来继续执行我后面的代码。”  这非常重要,它不会阻塞线程,而是让出执行权。
代码演示:
// 需安装http库 flutter pub add http
import 'package:http/http.dart' as http;

void loadData() async {
  print('1: 开始请求');
  var data = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
  ); // 不会阻塞,只是“等待”
  print('3: 收到数据: $data');
}

void main() {
  loadData();
  print('2: 执行其它操作');
}
执行结果:
1: 开始请求
2: 执行其它操作
3: 收到数据: Instance of 'Response'
梳理流程:
  1. 发起请求(同步) :

    • http.get() 被调用。
    • 它同步地创建一个 Future 对象并返回。
    • 它同步地发起一个系统调用(比如用 libcurl 或系统的 HTTP 库),将这个网络 I/O 操作委托给操作系统在 Dart 线程之外执行。此时,Dart 线程本身是空闲的,可以继续执行事件循环。
  2. 遇到 await(同步 -> 异步) :

    • await 关键字检查它右边的 Future。如果这个 Future 尚未完成(http.get 返回的肯定未完成),它会暂停当前函数的执行
    • 向这个未完成的 Future 订阅一个回调。这个回调包含了 await 之后的所有代码(即 print('3: ...'))。
    • 这个回调的调度机制,与 future.then(...) 的回调调度机制是完全相同的。
  3. 等待与完成(在Dart线程外) :

    • Dart 事件循环继续运行,处理微任务和UI事件(比如执行 print('2'))。
    • 操作系统在后台处理网络请求。
  4. 请求完成,通知Dart(事件队列) :

    • 当操作系统完成网络请求后,它会通过一种机制(如 epollkqueue 或 IOCP)通知 Dart 运行时。
    • 这个“通知”作为一个事件,被放入 Dart 的事件队列(Event Queue)中。 这个事件本身不包含数据处理逻辑,只是一个信号。
  5. 事件循环处理完成事件(从事件队列到微任务队列) :

    • 事件循环从事件队列中取出这个“网络请求完成”的事件。
    • 处理这个事件的代码(Dart 运行时内部代码)会完成之前 http.get 返回的那个 Future 对象(即调用 _completer.complete(response))。
    • 最关键的一步来了:当 future.complete() 被调用时,Dart 的运行时会 schedule 一个微任务。这个微任务的任务就是:执行所有通过 thencatchError 或 await 注册在这个 Future 上的回调函数。
  6. 执行回调(微任务队列) :

    • 当事件循环处理完当前的事件后,它会检查微任务队列。
    • 它发现了第5步中被调度的微任务,于是执行它。
    • 这个微任务的内容就是执行 print('3: 收到数据: $data')

操作系统完成I/O -> 发送信号至 Dart 事件队列 -> 事件循环处理该信号 -> Future.complete()被调用 -> 调度一个微任务来执行所有回调 -> 事件循环处理微任务 -> 执行 await 之后的代码。

3. 处理真正耗时计算:Isolate

事件循环和异步处理对于I/O操作是完美的,但对于CPU密集型计算(如图像处理、加密解密、复杂JSON解析),如果计算本身就在Dart线程上运行,它就会阻塞事件循环,导致队列中的所有其他任务(包括UI渲染)都无法处理,应用就会卡顿。

Dart的解决方案是 Isolate

  • 不共享内存:每个Isolate都有自己的独立内存堆(Heap)和事件循环。Isolate之间不共享任何状态,通信的唯一方式是通过消息传递(Passing Messages)
  • 真正的并行:由于现代设备是多核CPU,不同的Isolate可以被调度到不同的CPU核心上真正地并行运行。
  • 通信成本:因为不共享内存,Isolate之间传递消息时,数据会发生拷贝。虽然对于简单数据很快,但对于大型数据(如图片、大列表),这个拷贝成本会很高。通常使用SendPortReceivePort进行通信。

在Flutter中,你可以使用Isolate.spawn()或更高级的compute()函数来将繁重任务抛到新的Isolate中执行。

代码演示:
import 'package:flutter/foundation.dart';

// 使用 compute (Flutter提供的简便API)
void main() async {
  var result = await compute(heavyTask, 1000000000);
  print(result);
}

// 这个函数会在新的Isolate中执行
int heavyTask(int count) {
  int sum = 0;
  for (int i = 0; i < count; i++) {
    sum += i;
  }
  return sum;
}


dart是值传递还是引用传递?

一句话总结

  • Dart 是严格的值传递语言,所有参数传递的都是 “值的副本”。

核心概念

Dart 中的数据类型分为基本类型(Primitive Types)  和对象类型(Object Types) ,两者在值传递时的表现不同,容易造成 “引用传递” 的误解:

1. 基本类型(数值、布尔、字符串、null)

基本类型的值传递是直接传递值的副本,函数内部修改参数不会影响外部变量。

代码演示:
void modifyInt(int a) {
  a = 100; // 修改的是副本
  print("函数内:$a"); // 输出:100
}

void main() {
  int x = 10;
  modifyInt(x); // 传递x的副本(值为10)
  print("函数外:$x"); // 输出:10(原始值未变)
}
  • 解析:x的值10被复制一份传递给a,函数内修改a不会影响x,符合值传递特性。
2. 对象类型(类实例、列表、映射等)

对象类型的值传递是传递对象引用的副本(即内存地址的副本)。这意味着:

  • 函数内部和外部的变量持有同一个对象的不同引用副本(但指向同一块内存)。
  • 若修改对象的内部状态(如属性、元素),会影响外部变量(因为指向同一对象)。
  • 若直接修改参数的引用指向(如重新赋值新对象),则不会影响外部变量(因为修改的是副本引用)。
代码演示:

示例 1:修改对象内部状态

class Person {
  String name;
  Person(this.name);
}

void modifyPerson(Person p) {
  p.name = "Bob"; // 修改对象内部状态(影响外部)
  print("函数内:${p.name}"); // 输出:Bob
}

void main() {
  Person person = Person("Alice");
  modifyPerson(person); // 传递person引用的副本(指向同一对象)
  print("函数外:${person.name}"); // 输出:Bob(内部状态被修改)
}
  • person 变量持有一个指向 Person('Alice') 对象的引用(可以理解为内存地址)。
  • 当调用 modifyObject(person) 时,传递的是这个引用的值(即内存地址)的一个副本
  • 现在,有两个引用指向同一个对象:外部的 person 和函数内部的 p
  • 通过函数内部的引用 p 去修改对象的属性(p.name = 'Bob'),因为外部引用 person 也指向这同一个对象,所以这个修改对两者都是可见的。这常常被误认为是“引用传递”。

示例 2:修改参数的引用指向

class Person {
  String name;
  Person(this.name);
}

void replacePerson(Person p) {
  p = Person("Charlie"); // 修改引用副本的指向(不影响外部)
  print("函数内:${p.name}"); // 输出:Charlie
}

void main() {
  Person person = Person("Alice");
  replacePerson(person);
  print("函数外:${person.name}"); // 输出:Alice(原始引用未变)
}
  • 同样,开始时内部的 p 和外部的 person 指向同一个对象。
  • 但当执行 p = Person('Charlie') 时,你是在让内部的引用 p(它只是外部引用的一个副本)指向一个全新的对象
  • 并没有改变原来那个对象(Person('Alice'))的任何状态,也没有改变外部引用 person 的值。外部引用 person 依然坚定地指向最初的那个对象。所以外部的打印结果没有变化。

对 Flutter 开发的实际影响

  1. 不可变对象的优势
    对于Stringint等不可变类型(以及自定义不可变类),值传递时无需担心内部状态被修改,适合作为状态管理中的数据载体(如ProviderBloc中的状态)。

  2. 列表 / 映射的传递注意事项
    传递ListMap时,若需避免函数内部修改原集合,应传递副本(如List.from(list)Map.from(map)):

    void safeModify(List<int> list) {
      list = List.from(list); // 创建副本后再修改
      list.add(4);
    }
    
  3. 状态管理中的数据更新
    在 Flutter 状态管理中,修改对象内部状态后若需触发 UI 重建,需确保状态引用发生变化(如创建新对象),因为 Widget 会对比引用是否相同来决定是否重建。



mixin extends implement之间的关系。

一句话总结

  • extends 用于继承,建立‘is-a’的关系,继承并可重写父类实现,但受限于单继承。
  • implements:用于实现接口,建立 “can-do” 的关系,必须重写接口所有成员,可以实现多个接口。
  • mixin/with 用于混入代码,建立 “包含” 的关系,直接复用方法实现,解决单继承局限,后写的覆盖先写的,可以混入多个。

核心概念

1. extends:类的单继承("是一个" 的关系)

  • 单继承:Dart 是单继承语言,一个类只能直接继承一个父类
  • 继承实现:子类继承父类的所有非私有成员(字段、方法)
  • 构造函数不继承:子类不继承父类的构造函数,但可以调用它们
  • 方法重写:子类可以重写父类的方法
代码演示:
基本继承示例
class Animal {
  String name;
  int age;
  
  Animal(this.name, this.age);
  
  void speak() {
    print('Animal sound');
  }
  
  void sleep() {
    print('$name is sleeping');
  }
}

class Dog extends Animal {
  String breed;
  
  // 调用父类构造函数
  Dog(String name, int age, this.breed) : super(name, age);
  
  // 重写父类方法
  @override
  void speak() {
    print('Woof! Woof!');
  }
  
  // 添加新方法
  void fetch() {
    print('$name is fetching a ball');
  }
}

void main() {
  var dog = Dog('Buddy', 3, 'Golden Retriever');
  dog.speak();    // 输出: Woof! Woof! (重写的方法)
  dog.sleep();    // 输出: Buddy is sleeping (继承的方法)
  dog.fetch();    // 输出: Buddy is fetching a ball (新方法)
}
继承中的构造函数处理
class Person {
  String name;
  int age;
  
  Person(this.name, this.age);
  
  Person.newborn(String name) : this(name, 0);
  
  void introduce() {
    print('Hi, I'm $name, $age years old');
  }
}

class Employee extends Person {
  String company;
  
  Employee(String name, int age, this.company) : super(name, age);
  
  // 子类的命名构造函数
  Employee.freshGraduate(String name, String company) 
      : this(name, 22, company);
  
  @override
  void introduce() {
    super.introduce();
    print('I work at $company');
  }
}
属性重写(Getter/Setter)
import 'dart:math';

class Rectangle {
  double width;
  double height;
  
  Rectangle(this.width, this.height);
  
  double get area => width * height;
  
  set area(double value) {
    // 保持宽高比例
    final ratio = width / height;
    width = sqrt(value * ratio);
    height = width / ratio;
  }
}

class Square extends Rectangle {
  Square(double side) : super(side, side);
  
  @override
  double get area => super.area;
  
  @override
  set area(double value) {
    // 正方形面积设置
    width = sqrt(value);
    height = width;
  }
}
继承抽象类
abstract class Animal {
  String name;
  
  Animal(this.name);
  
  void makeSound(); // 抽象方法
  
  void sleep() {
    print('$name is sleeping');
  }
}

class Cat extends Animal {
  Cat(String name) : super(name);
  
  @override
  void makeSound() {
    print('Meow!');
  }
}

class Dog extends Animal {
  Dog(String name) : super(name);
  
  @override
  void makeSound() {
    print('Woof!');
  }
  
  @override
  void sleep() {
    super.sleep(); // 调用父类实现
    print('...and dreaming about bones');
  }
}
继承泛型类
class Box<T> {
  T content;
  
  Box(this.content);
  
  T getContent() => content;
}

class NumberBox extends Box<int> {
  NumberBox(int value) : super(value);
  
  // 方法可以操作具体类型
  int square() => content * content;
}

class PairBox<A, B> extends Box<A> {
  B second;
  
  PairBox(A first, this.second) : super(first);
  
  (A, B) getPair() => (content, second);
}
继承中的 super 关键字
class Vehicle {
  void start() {
    print('Vehicle starting');
  }
}

class Car extends Vehicle {
  @override
  void start() {
    super.start(); // 调用父类方法
    print('Car specific startup procedure');
  }
}

class ElectricCar extends Car {
  @override
  void start() {
    print('Electric systems check');
    super.start(); // 调用父类(Car)的方法
  }
}
继承中的静态成员

静态成员不会被继承:

class MathUtils {
  static double pi = 3.14159;
  
  static double circleArea(double radius) {
    return pi * radius * radius;
  }
}

class AdvancedMathUtils extends MathUtils {
  // 静态成员不会被继承
  // 不能直接访问 pi 或 circleArea
  
  static double e = 2.71828;
  
  // 必须重新定义静态方法
  static double circleArea(double radius) {
    return MathUtils.pi * radius * radius; // 通过类名访问
  }
}

void main() {
  print(MathUtils.pi);           // 3.14159
  // print(AdvancedMathUtils.pi);   // 错误: 没有这样的getter
  print(AdvancedMathUtils.e);    // 2.71828
}
继承的最佳实践
  1. 遵循里氏替换原则:子类应该能够替换父类而不影响程序正确性
  2. 使用继承表示"是一个"关系:子类应该是父类的特殊化
  3. 优先使用组合 over 继承:如果不是真正的"是一个"关系,考虑使用组合
  4. 避免深继承层次:深层次的继承难以理解和维护
  5. 使用抽象类定义接口:为预期会被继承的类提供清晰的契约

2. implements:接口实现("像一个" 的关系)

  • 接口契约:当一个类实现一个接口时,需实现接口中所有非静态成员(方法、属性、getter、setter)。
  • 不继承实现:与 extends 不同,implements 不继承任何实现,只遵循接口定义的 “规范”。
  • 多接口实现:一个类可以实现多个接口
代码演示:
实现普通类的接口:
class Vehicle {
  String name;
  int speed;
  
  Vehicle(this.name, this.speed);
  
  void move() {
    print('$name is moving at $speed km/h');
  }
  
  void stop() {
    print('$name has stopped');
    speed = 0;
  }
}

class Car implements Vehicle {
  @override
  String name;
  
  @override
  int speed;
  
  Car(this.name, this.speed);
  
  @override
  void move() {
    print('Car $name is driving at $speed km/h');
  }
  
  @override
  void stop() {
    print('Car $name is braking');
    speed = 0;
  }
}
实现抽象类的接口:
abstract class Animal {
  String name;
  int age;
  
  Animal(this.name, this.age);
  
  void makeSound(); // 抽象方法
  
  void sleep() {
    print('$name is sleeping');
  }
}

class Dog implements Animal {
  @override
  String name;
  
  @override
  int age;
  
  Dog(this.name, this.age);
  
  @override
  void makeSound() {
    print('$name says: Woof!');
  }
  
  @override
  void sleep() {
    print('Dog $name is sleeping soundly');
  }
}
静态成员在接口实现中的特殊行为

在 Dart 中,静态成员不是接口的一部分。当一个类实现另一个类或接口时,它不需要实现静态成员。这是因为静态成员属于类本身,而不是类的实例。

class ExampleWithStatic {
  // 实例成员
  String instanceField = 'instance';
  void instanceMethod() => print('Instance method');

  // 静态成员
  static String staticField = 'static';
  static void staticMethod() => print('Static method');
}

class Implementation implements ExampleWithStatic {
  @override
  String instanceField = 'implemented instance';

  @override
  void instanceMethod() {
    print('Implemented instance method');
  }

// 注意:不需要实现静态成员
// static String staticField = 'something'; // 这不是必需的
// static void staticMethod() {} // 这不是必需的
}
实现多个接口
abstract class Flyable {
  void fly();
}

abstract class Swimmable {
  void swim();
}

abstract class Walkable {
  void walk();
}

class Duck implements Flyable, Swimmable, Walkable {
  @override
  void fly() {
    print('Duck is flying');
  }
  
  @override
  void swim() {
    print('Duck is swimming');
  }
  
  @override
  void walk() {
    print('Duck is waddling');
  }
}
实现混合接口
// 普通类
class Named {
  String name;
  Named(this.name);
}

// 抽象类
abstract class Aged {
  int get age;
  set age(int value);
}

// Mixin
mixin Colored {
  String color = 'unknown';
  
  void describeColor() {
    print('Color: $color');
  }
}

// 实现多个接口
class Person implements Named, Aged, Colored {
  @override
  String name;
  
  @override
  int age;
  
  @override
  String color;
  
  Person(this.name, this.age, this.color);
  
  // Colored mixin 的方法也需要实现
  @override
  void describeColor() {
    print('Person $name has $color color');
  }
}
处理私有成员
// 在同一个库中
class _PrivateClass {
  int _privateValue = 0; // 私有字段
  int publicValue = 0;   // 公共字段
  
  void _privateMethod() {} // 私有方法
  void publicMethod() {}   // 公共方法
}

// 这会报错,因为需要实现私有成员
class Implementation implements _PrivateClass {
  @override
  int publicValue = 0;
  
  @override
  void publicMethod() {}
  
  // 错误: 缺少私有成员的实现
}
实现泛型接口
abstract class Repository<T> {
  T findById(int id);
  List<T> findAll();
  void save(T entity);
  void delete(int id);
}

class UserRepository implements Repository<User> {
  final List<User> _users = [];
  
  @override
  User findById(int id) {
    return _users.firstWhere((user) => user.id == id, orElse: () => null);
  }
  
  @override
  List<User> findAll() {
    return List.from(_users);
  }
  
  @override
  void save(User user) {
    final index = _users.indexWhere((u) => u.id == user.id);
    if (index >= 0) {
      _users[index] = user;
    } else {
      _users.add(user);
    }
  }
  
  @override
  void delete(int id) {
    _users.removeWhere((user) => user.id == id);
  }
}

class User {
  final int id;
  final String name;
  
  User(this.id, this.name);
}
处理 getter 和 setter
abstract class Person {
  String get firstName;
  set firstName(String value);
  String get lastName;
  set lastName(String value);
  String get fullName; // 只读属性
}

class Employee implements Person {
  String _firstName;
  String _lastName;
  
  Employee(this._firstName, this._lastName);
  
  @override
  String get firstName => _firstName;
  
  @override
  set firstName(String value) {
    _firstName = value;
  }
  
  @override
  String get lastName => _lastName;
  
  @override
  set lastName(String value) {
    _lastName = value;
  }
  
  @override
  String get fullName => '$firstName $lastName';
}
实现接口时的最佳实践:
  1. 明确接口契约:使用抽象类或纯接口定义清晰的契约
  2. 避免实现包含私有成员的类:这会导致难以维护的代码
  3. 使用组合替代复杂接口实现:当需要复用多个类的功能时,考虑使用组合
  4. 保持接口简洁:遵循接口隔离原则,创建小而专注的接口
  5. 文档化接口:为接口提供清晰的文档说明预期行为
// 好的实践:小而专注的接口
abstract class Savable {
  void save();
}

abstract class Deletable {
  void delete();
}

abstract class Findable<T> {
  T findById(int id);
}

class User {
  final int id;
  final String name;

  User(this.id, this.name);
}

// 实现多个小接口
class UserRepository implements Savable, Deletable, Findable<User> {
  @override
  void delete() {
    // TODO: implement delete
  }

  @override
  User findById(int id) {
    // TODO: implement findById
    throw UnimplementedError();
  }

  @override
  void save() {
    // TODO: implement save
  }
  // 实现...
}

mixin:代码复用("包含" 的关系)

  • mixin关键字定义,不能有构造函数(避免与混入类的构造函数冲突)。
  • 一个类可通过with关键字混入多个 mixin(按顺序生效,后混入的 mixin 会覆盖先混入的同名方法)。
  • 可通过on关键字限制 mixin 的使用范围(如mixin M on A表示 M 只能混入 A 的子类)。
代码演示:
声明限制

Mixin 不能有构造函数,也不能被实例化:

mixin Logger {
  // 不能有构造函数
  // Logger(); // 错误: Mixin 不能声明构造函数
  
  void log(String message) {
    print('LOG: $message');
  }
}
使用 on 关键字限制应用范围
class Animal {
  void breathe() {
    print('Breathing');
  }
}

// 这个 Mixin 只能用于 Animal 或其子类
mixin Swimming on Animal {
  void swim() {
    print('Swimming');
    breathe(); // 可以访问 Animal 的方法
  }
}

// 正确: Bird 是 Animal 的子类
class Bird extends Animal with Swimming {}

// 错误: Robot 不是 Animal 的子类
// class Robot with Swimming {} // 编译错误
Mixin 的线性化

Dart 使用线性化算法来确定方法解析顺序。当类使用多个 Mixin 时,方法的解析顺序是从右到左:

mixin A {
  void method() {
    print('A.method');
  }
}

mixin B {
  void method() {
    print('B.method');
  }
}

mixin C {
  void method() {
    print('C.method');
  }
}

class MyClass with A, B, C {
  // 方法解析顺序: MyClass -> C -> B -> A -> Object
}

void main() {
  var obj = MyClass();
  obj.method(); // 输出: C.method (最右边的 Mixin 优先)
}
Mixin 的最佳实践
  1. 单一职责:每个 Mixin 应该只负责一个特定的功能
  2. 命名清晰:使用描述性的名称,通常以 "-able" 或 "-er" 结尾
  3. 避免状态污染:谨慎使用实例变量,避免意外的状态共享
  4. 文档化约束:使用 on 关键字明确指定 Mixin 的使用范围
  5. 测试独立:确保 Mixin 可以独立测试,不依赖于特定的类层次结构


Dart中如何取消正在执行中的异步任务?

一句话总结

  • 通过 Completer 来控制 Future 的完成状态。
  • 通过CancelableOperation 包装Future,实现 “逻辑取消”。
  • 基于流的操作,可以使用 StreamSubscription 的取消机制。
  • 对于计算密集型任务,使用 Isolate 通过kill()方法强制终止。

核心概念

1. 使用 Future 和 Completer 实现取消

Completer是 Dart 中用于手动控制Future完成状态的工具类,它可以主动触发Future的完成(成功 / 失败)。

import 'dart:async';

class CancellableTask {
  Completer<void> _completer = Completer<void>();

  Future<void> execute() async {
    _completer = Completer<void>();
    _doWork();
  }

  Future<void> _doWork() async {
    for (int i = 0; i < 100; i++) {
      // 检查是否被取消
      if (_completer.isCompleted) {
        print('任务被取消');
        return;
      }

      await Future.delayed(Duration(milliseconds: 100));

      print('处理项目 $i');
    }
    return _completer.future;
  }

  void cancel() {
    if (!_completer.isCompleted) {
      _completer.complete();
    }
  }
}

// 使用示例
void main() async {
  final task = CancellableTask();

  // 启动任务
  task.execute();

  // 2秒后取消
  await Future.delayed(Duration(seconds: 2));
  task.cancel();
}

2. 使用 CancelableOperation (package:async)

  • 需要安装async库。CancelableOperation 本质是对Future的封装,通过一个 “取消令牌” 标记任务状态。取消后,后续的then/whenComplete等回调不会执行,且会触发onCancel回调释放资源。
  • 第三方库(如dio)通常封装了取消机制,本质是基于CancelableOperation或标志位。
import 'dart:async';
import 'package:async/async.dart';

// 模拟耗时任务(如网络请求)
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 3)); // 模拟3秒耗时
  return "请求结果";
}

void main() async {
  // 包装Future为可取消操作
  final cancelable = CancelableOperation.fromFuture(
    fetchData(),
    onCancel: () {
      // 取消时释放资源(如关闭网络连接)
      print("任务已取消,释放资源");
    },
  );

  // 1秒后取消任务
  Timer(Duration(seconds: 1), () {
    cancelable.cancel(); // 取消操作
  });

  // 监听结果(取消后不会执行)
  final result = await cancelable.value;
  print("任务完成:$result");
}
  • 取消后,原始Future可能仍在后台执行(无法强制终止),但结果会被忽略。
  • 适合处理 “即使取消也不会造成严重资源浪费” 的任务(如短时间网络请求)。

3.Stream任务的取消:通过StreamSubscription控制

通过stream.listen()获取StreamSubscription对象,调用其cancel()方法可终止流的事件传递,且会触发流的onCancel回调释放资源。

import 'dart:async';

void main() {
  // 创建定时发送事件的Stream(每1秒一次)
  final stream = Stream.periodic(Duration(seconds: 1), (count) => count);

  // 订阅流,获取订阅对象
  final subscription = stream.listen(
    (data) => print("接收事件:$data"),
    onDone: () => print("流已完成"),
  );

  // 3秒后取消订阅
  Timer(Duration(seconds: 3), () {
    subscription.cancel(); // 取消后,不再接收事件
    print("流已取消");
  });
}
// 输出:
// 接收事件:0
// 接收事件:1
// 接收事件:2
// 流已取消

4.Isolate任务的取消:通过kill()强制终止

Isolate拥有独立的内存和执行线程,kill()会直接终止其运行,释放所有资源,终止后无法恢复。

import 'dart:async';
import 'dart:isolate';

// 耗时计算任务(在Isolate中执行)
void heavyCompute(SendPort sendPort) {
  int result = 0;
  for (int i = 0; i < 1000000000; i++) {
    result += i;
    // 定期检查是否需要退出(可选,优化终止响应速度)
    if (i % 100000000 == 0) {
      print("计算中:$i");
    }
  }
  sendPort.send(result);
}

void main() async {
  // 创建端口接收结果
  final receivePort = ReceivePort();
  // 启动Isolate
  final isolate = await Isolate.spawn(heavyCompute, receivePort.sendPort);

  // 监听结果
  receivePort.listen((data) {
    print("计算结果:$data");
    receivePort.close();
  });

  // 2秒后终止Isolate(取消任务)
  Timer(Duration(seconds: 2), () {
    isolate.kill(priority: Isolate.immediate); // 立即终止
    print("Isolate已终止");
    receivePort.close();
  });
}
  • kill()是 “暴力终止”,需确保 Isolate 中没有未释放的资源(如文件句柄)。
  • 建议在 Isolate 内部添加 “取消检查点”(如定期检查标志),配合kill()提升终止效率。


Dart中Future和Stream区别?

一句话总结

  • Future:处理单次异步结果,表示一个未来可能完成的单个异步操作,结果只有两种状态 ——“成功返回一个值” 或 “失败抛出一个错误”。
  • Stream:处理连续异步事件流,表示一个异步事件序列,可以持续产生多个值(或错误),最终可能正常结束或出错终止。

核心概念

Future表示单个异步操作的结果,它有三种状态: 未完成: 异步操作正在进行中; 已完成: 成功(返回一个结果) 失败(返回一个错误) 常用于 “一次性” 异步操作,如网络请求、文件读取、延迟执行等。 就像是一次性的外卖订单:你下单,等待,然后收到一份完整的餐点。

Stream表示一系列异步事件的序列,可以持续产生多个结果(或错误),类似 “数据流”。它的核心是 “订阅 - 发射” 模式。就像是餐厅的传送带:食物(数据)会连续不断地传送过来,你可以随时拿取。

  • 通过stream.listen()订阅事件;

  • 异步操作可以通过add()持续发射数据、addError()发射错误,最后通过close()结束流。

  • 可以产生多个结果(零个、一个或多个),是 “持续的异步操作”;

  • 常用于 “多次触发” 的场景,如实时数据更新(WebSocket 消息、传感器数据、文件分片读取)。 用一个简单的比喻:

代码演示:

Future 示例:获取单个异步结果
class User {
  final int id;
  final String name;
  User({required this.id, required this.name});
}

// Future: 获取用户信息(单次操作)
Future<User> fetchUserData(int userId) async {
  // 模拟网络请求
  await Future.delayed(Duration(seconds: 2));
  return User(id: userId, name: 'John Doe');
}

// 使用
void main() async {
  print('开始获取用户数据...');
  final user = await fetchUserData(1);
  print('用户数据: ${user.name}'); // 单次结果
}
Stream 示例:监听连续的数据流
class Location {
  final double latitude;
  final double longitude;

  Location({required this.latitude, required this.longitude});
}

// Stream: 监听实时位置更新
Stream<Location> getLocationUpdates() async* {
  // 模拟连续的位置更新
  for (int i = 0; i < 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield Location(latitude: 37.0 + i * 0.1, longitude: -122.0 + i * 0.1);
  }
}

// 使用
void main() {
  print('开始监听位置更新...');
  final subscription = getLocationUpdates().listen(
    (location) {
      print('位置更新: ${location.latitude}, ${location.longitude}');
    },
    onDone: () => print('监听完成'),
    onError: (error) => print('错误: $error'),
  );

  // 可以随时取消监听
  // subscription.cancel();
}

Future 的典型使用场景:

// 网络请求:HTTP API 调用
Future<Response> response = http.get(Uri.parse('https://api.example.com/data'));
// 文件读写:读取/写入文件
Future<String> contents = File('data.txt').readAsString();
// 数据库操作:单次查询
Future<User> user = database.findUser(1);
// 用户交互:显示对话框
Future<bool> choice = showDialog(
  context: context,
  builder: (context) => AlertDialog(...),
);

Stream 的典型使用场景:

// 实时数据:WebSocket、Firestore 实时更新
Stream<QuerySnapshot> stream = Firestore.instance.collection('messages').snapshots();
// 用户输入:搜索框输入监听
searchController.textChanges
    .debounceTime(Duration(milliseconds: 300))
    .listen((query) => search(query));
// 传感器数据:位置、加速度计更新
Stream<Location> locationStream = Geolocator.getPositionStream();
// 状态管理:BLoC、Riverpod 的状态流
Stream<AppState> appStateStream = bloc.stream;

性能和使用建议:

何时选择 Future
  • 只需要单次异步结果时
  • 操作有明确的开始和结束时
  • 不需要中间状态更新时
  • 简单的异步操作

何时选择 Stream

  • 需要处理连续的数据序列时
  • 需要实时更新和状态变化时
  • 需要取消和暂停机制时
  • 复杂的事件处理场景
  1. 避免 Stream 过度使用:如果 Future 足够,不要使用 Stream。
  2. 及时取消订阅:防止内存泄漏。
  3. 使用 StreamBuilder 优化 UI