面试备战 Flutter 02:Dart 类型系统、空安全与 Mixin

面试备战 Flutter 02:Dart 类型系统、空安全与 Mixin

Dart 面试不是语法题。它考的是你能否用类型系统减少运行时错误,能否理解空安全、异步、mixin 和 Flutter Framework 的结合。

1. sound null safety

Dart 空安全是 sound 的,编译器能在类型层面区分可空和非空。

String name = 'Tom';
String? nickname;

非空变量不能为 null,可空变量使用前必须处理。

常用:

  • ?:可空。
  • !:强制非空。
  • ?.:空安全访问。
  • ??:空合并。
  • late:延迟初始化。

2. ! 不是解决方案

user!.name

这只是告诉编译器“我保证不为空”。如果运行时为 null,直接崩。

工程里应优先:

  • 明确状态模型。
  • loading/error/empty 分支。
  • 构造函数 required。
  • 类型上表达可空。

3. late 的真实风险

late AnimationController controller;

适合生命周期保证的初始化:


void initState() {
  super.initState();
  controller = AnimationController(vsync: this);
}

风险:使用前未赋值会运行时异常。

另外 late 带初始化器时是惰性求值:late T x = expensive(); 只有首次访问才求值;late final 允许延迟一次性赋值。

所以 late 是生命周期承诺,不是逃避空安全。

4. dynamic vs Object

dynamic 关闭静态检查:

dynamic a = 1;
a.foo(); // 编译可能过,运行时报错

Object 仍受类型检查:

Object a = 1;
// a.foo(); 编译不通过

工程建议:业务模型尽量不用 dynamic,跨端协议也要尽量 schema 化。

5. 泛型

Flutter 大量依赖泛型:

class MyState extends State<MyWidget> {}

泛型价值:

  • 类型安全。
  • 减少强转。
  • 表达容器元素。
  • 提升 API 可读性。

进阶点:泛型可用 extends 加边界约束(T extends num);Dart 泛型默认协变,会带来运行时类型检查。

6. extends / implements / mixin

extends

继承实现,表达 is-a。

implements

实现接口,需要实现所有成员。

mixin

复用能力,不建立父子关系。

Flutter 常见:

class _State extends State<Page>
    with SingleTickerProviderStateMixin {}

mixin 适合横向复用能力,但不要承载复杂业务状态。

线性化与 super 调用顺序

with 多个 mixin 时按从左到右叠加,右边覆盖左边。Dart 把继承链拍平成一条线性序列(linearization),super 沿这条线向上找:

mixin A { void foo() => print('A'); }
mixin B { void foo() { print('B'); super.foo(); } }

class C extends Object with A, B {}
// C().foo() 输出:B、A
// 查找链 C -> B -> A -> Object,B 里的 super 指向 A

on 约束

on 限定 mixin 只能混入某个基类的子类,从而保证 mixin 内 super 调用的类型安全:

mixin TickerMix on State { /* 可安全使用 State 的成员 */ }

SingleTickerProviderStateMixin 就是 on State,所以只能用在 State 上。

Dart 3 的关键字限制

Dart 3 起,普通 class 默认不能再被 with。要给别人混入需用 mixin 声明;既想当类又想被混入,用 mixin class

7. extension

extension 是静态扩展:

extension StringExt on String {
  bool get isBlank => trim().isEmpty;
}

它不是真的修改原类,也没有 Runtime 动态派发那种能力——extension 按静态类型解析。所以把对象声明为 dynamic 时 extension 方法会失效(抛 NoSuchMethodError),这是常见陷阱。

高频追问

Q1:late 和 nullable 怎么选?

如果变量生命周期保证使用前一定初始化,用 late。如果业务上确实可能为空,用 nullable。

Q2:mixin 和继承区别?

继承表达类型层级,mixin 表达能力复用。Dart 不支持多继承,但支持多个 mixin。

Q3:为什么少用 dynamic?

dynamic 把错误推迟到运行时,破坏 IDE、重构和类型安全。跨端 Map 协议滥用 dynamic 会导致线上问题。

深挖追问:Dart 类型系统要答到 soundness 和工程约束

Sound null safety 的意思不是“不会有空指针”,而是:

在完整迁移、没有破坏类型系统的前提下,非空类型在运行时不会是 null。编译器可以利用这个事实做检查和优化。

会破坏这种保证的常见入口:

  • dynamic
  • as 强转。
  • ! 强行解包。
  • legacy library 边界。
  • JSON/Channel Map 这种弱 schema 数据。

late 被追问时:

写法本质风险
T?状态可能为空调用方必须处理空
late T承诺使用前初始化初始化顺序错会运行时异常
late final T延迟一次赋值多次赋值异常

所以 late 不是逃避空安全,而是把“初始化时序”变成运行时契约。适合生命周期保证明确的字段,不适合随便绕过编译器。

mixin 深挖:

  • Dart 没有多继承,mixin 是代码复用机制。
  • on 约束要求混入目标具备某个父类型能力。
  • 多个 mixin 有线性化顺序,后面的成员可能覆盖前面的。
  • mixin 不适合承载强状态和复杂生命周期,否则组合顺序会变成隐性耦合。

🔬 深度扩展:空安全迁移与late陷阱

扩展1:空安全的类型层级

类型关系:

Object (所有类型的父类)
  ├─ Object? (可空顶层)
  └─ Object (非空)
      ├─ int
      ├─ String
      └─ ...

Null类型只能赋值给T?,不能赋值给T

null check提升:

String? name;

if (name != null) {
  // 这里name被提升为String(非空)
  print(name.length);  // 无需!
}

扩展2:late的三种用途

1. 延迟初始化(非空字段):

class MyWidget extends StatefulWidget {
  
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late AnimationController _controller;  // 承诺在initState初始化
  
  
  void initState() {
    super.initState();
    _controller = AnimationController(...);  // 必须赋值
  }
}

2. 懒加载(首次访问初始化):

class HeavyResource {
  late String _data = _loadData();  // 首次访问_data时才执行
  
  String _loadData() {
    print('Loading...');
    return 'Heavy data';
  }
}

3. 循环引用打破:

class A {
  late B b;
}

class B {
  late A a;
}

final a = A();
final b = B();
a.b = b;
b.a = a;

陷阱:

late String name;

void use() {
  print(name);  // 💥 LateInitializationError
}

扩展3:Mixin的线性化顺序

多Mixin顺序:

class A {
  void method() => print('A');
}

mixin B {
  void method() => print('B');
}

mixin C {
  void method() => print('C');
}

class D extends A with B, C {}

void main() {
  D().method();  // 输出:C(最后的mixin优先)
}

线性化算法:

D的方法查找顺序:
D → C → B → A → Object

后面的mixin可以覆盖前面的

扩展4:Mixin的on约束

约束父类型:

mixin LoggerMixin on StatefulWidget {
  void log(String msg) {
    print('[${runtimeType}] $msg');
  }
}

class MyWidget extends StatefulWidget with LoggerMixin {
  // LoggerMixin要求继承StatefulWidget
}

扩展5:泛型的协变与逆变

协变(covariant):

class Animal 
class Dog extends Animal {}

class Box<T> {
  void put(T item) {}
}

Box<Animal> animalBox = Box<Dog>();  // ❌ Dart不支持

使用covariant关键字:

class Animal {
  void chase(covariant Animal x) {}
}

class Dog extends Animal {
  
  void chase(covariant Dog x) {}  // 参数类型收窄
}

扩展6:dynamic vs Object vs var

类型编译期检查运行时类型使用场景
dynamic任意JSON、Channel、真正不确定类型
Object任意需要任意类型但有类型检查
var推断类型明确但懒得写

示例:

dynamic d = 'hello';
d.foo();  // 编译通过,运行时可能报错

Object o = 'hello';
o.foo();  // ❌ 编译错误

var v = 'hello';
v.foo();  // ❌ 编译错误(推断为String,没有foo方法)

扩展7:空安全迁移的边界处理

legacy库调用:

// 未迁移的库返回String?
// 但你的代码期望String
String name = legacyLibrary.getName();  // ❌ 可能为null

// 正确处理
String? name = legacyLibrary.getName();
if (name != null) {
  use(name);
}

JSON处理:

// JSON是dynamic,需要显式检查
Map<String, dynamic> json = jsonDecode(response);

String? name = json['name'] as String?;  // 可能为null
int age = json['age'] as int? ?? 0;      // 默认值

扩展8:null-aware操作符

?. (空安全调用):

String? name;
int? length = name?.length;  // name为null时,length也为null

?? (空值合并):

String? name;
String displayName = name ?? 'Guest';  // name为null时用'Guest'

??= (空值赋值):

String? name;
name ??= 'Default';  // 只有name为null时才赋值

! (非空断言):

String? name = getName();
print(name!.length);  // 断言name不为null,否则抛异常

补充总结

Dart类型系统的深度记忆点:

  1. 空安全层级:Object?顶层、Null只能赋T?
  2. late用途:延迟初始化、懒加载、打破循环引用
  3. late陷阱:未初始化访问抛LateInitializationError
  4. Mixin顺序:线性化,后面的覆盖前面的
  5. on约束:限制Mixin的使用场景
  6. dynamic vs Object:dynamic无编译检查
  7. 空安全迁移:legacy库边界、JSON处理
  8. null-aware:?.、??、??=、!

面试追问时要能讲出:

  • late的三种用途和陷阱
  • Mixin的线性化顺序(后面覆盖前面)
  • dynamic和Object的区别(编译期检查)
  • 空安全迁移的边界处理(legacy库、JSON)

extension 深挖:

extension 是静态解析,不是 Runtime 给类加方法。变量静态类型不同,能看到的 extension 也不同;声明成 dynamic 时不会走 extension,而是运行时动态调用,找不到就 NoSuchMethodError

工程表达:

在跨端协议、JSON model 和状态模型里,我会尽量用强类型和 codegen/schema,而不是 dynamic Map。类型系统越靠近边界,线上越少出现“字段有但类型不对”的问题。

一句话总结

Dart 类型系统的价值是让错误尽量在编译期暴露;空安全、泛型和 mixin 都应该服务于更清晰的生命周期和边界设计。