Dart: Dart Null Safety — 从原理到实践消灭空指针
Null Safety 是 Dart 的铠甲 — 编译器帮你挡住 95% 的空指针攻击。
1. 你将学到
- Sound Null Safety 原理:可空类型
T?vs 非空类型T - null 检查与提升(type promotion)
- late 关键字:延迟初始化与安全性保证
!操作符的使用场景与风险- Bob 场景:DataPipeline 中可选配置的 Null Safety 设计
2. 一个开发者的真实故事
(1) 痛点:NullPointerException 是最常见的运行时崩溃
Bob 的 DataPipeline 在生产环境中,60% 的运行时崩溃都是空指针异常。API 返回了 null 的订单 ID,CSV 中缺失了可选字段,配置项未设置就使用 — 每种情况都导致程序崩溃,平均每次事故影响 50,000 条订单的处理。
(2) Sound Null Safety 的解法
Dart 3 的 Sound Null Safety 将类型分为可空 T? 和非空 T,编译器在编译时保证非空类型永远不会为 null。
// Non-nullable: compiler guarantees non-null
String orderId = 'ORD-001'; // Cannot be null
// orderId = null; // Compile error!
// Nullable: must be checked before use
String? nickname; // Can be null
int length = nickname?.length ?? 0; // Safe access
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
(3) 收益
- 空指针异常从运行时提前到编译时,相关 bug 减少 95%
- 非空类型让 API 契约更明确:哪些字段必填一目了然
- null 检查提升让代码更简洁,不需要手动类型转换
3. Sound Null Safety 原理
(1) 类型体系
flowchart TD
A[Variable Declaration] --> B{Can be null?}
B -->|Yes| C["T? Nullable type"]
B -->|No| D["T Non-nullable type"]
C --> E["Must check before use"]
E --> F["if (x != null) → promoted to T"]
C --> G["?? Provide default value"]
C --> H["!. Force unwrap - RISKY"]
D --> I["Use directly - SAFE"]
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:可空 vs 非空类型
void main() {
// Non-nullable types - cannot hold null
String name = 'DataPipeline';
int count = 100;
double amount = 1500.0;
// name = null; // Compile error!
// count = null; // Compile error!
// Nullable types - can hold null
String? nickname;
int? maxRetries;
double? discountRate;
print(nickname); // null
print(maxRetries); // null
print(discountRate); // null
// Nullable types require checking before use
// print(nickname.length); // Compile error! Might be null
print(nickname?.length); // null (safe)
print(nickname?.length ?? 0); // 0 (with default)
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 类型 | 可为 null | 使用前检查 | 示例 |
|---|---|---|---|
T |
否 | 不需要 | String name = 'Bob' |
T? |
是 | 必须 | String? name |
4. null 检查与类型提升
(1) 类型提升(Type Promotion)
当编译器确认可空变量不为 null 后,自动将其提升为非空类型。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:if 检查提升
void main() {
String? name = 'Bob';
// Before check - nullable
// print(name.length); // Error!
// After null check - promoted to non-nullable
if (name != null) {
print(name.length); // OK! name is promoted to String
print(name.toUpperCase()); // OK!
}
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:多种 null 检查方式
void main() {
String? city;
// Method 1: if-null check
if (city != null) {
print(city.length); // Promoted
}
// Method 2: null-coalescing operator ??
String safeCity = city ?? 'Unknown';
print(safeCity.length); // Always safe
// Method 3: null-aware access ?.
int? length = city?.length;
print(length); // null
// Method 4: late initialization
late String resolvedCity;
resolvedCity = city ?? 'Unknown';
print(resolvedCity.length); // Safe
// Method 5: assert in debug mode
assert(city != null, 'City must not be null');
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 检查方式 | 语法 | 提升效果 | 安全性 |
|---|---|---|---|
| if-null | if (x != null) |
提升为 T | 最高 |
| ?? | x ?? default |
返回 T | 高 |
| ?. | x?.method() |
返回 T? | 高 |
| ! | x!.method() |
视为 T | 低(可能崩溃) |
5. late 关键字
(1) 延迟初始化
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:late 基础用法
class DataPipeline {
// late - will be initialized later, but guaranteed before use
late String outputPath;
late final DateTime startTime;
void configure(String path) {
outputPath = path; // First assignment
}
void start() {
startTime = DateTime.now(); // late final - set once
print('Pipeline started at $startTime');
}
}
void main() {
final pipeline = DataPipeline();
pipeline.configure('/tmp/reports');
pipeline.start();
// Late initialization with initializer
late final int maxRecords = _loadConfig();
// maxRecords is computed only on first access
print('Max records: $maxRecords');
}
int _loadConfig() {
print('Loading config...');
return 1000000;
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:late 的风险
class RiskyPipeline {
late String config;
void process() {
// If config not set, throws LateInitializationError
print(config); // Potential runtime crash!
}
}
void main() {
final pipeline = RiskyPipeline();
// pipeline.process(); // LateInitializationError!
// Safe pattern: initialize in constructor
final safePipeline = SafePipeline('/etc/config.yaml');
safePipeline.process(); // OK
}
class SafePipeline {
late String config;
SafePipeline(String configPath) {
config = _loadConfig(configPath);
}
void process() => print('Config: $config');
String _loadConfig(String path) => 'Loaded from $path';
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| late 形式 | 初始化 | 可赋值次数 | 风险 |
|---|---|---|---|
late T |
使用前 | 多次 | 中(未初始化就访问崩溃) |
late final T |
使用前 | 1次 | 中 |
late final T = expr |
首次访问 | 1次(自动) | 低 |
6. ! 操作符
(1) 强制解包
! 告诉编译器"我确定这个值不为 null",跳过 null 检查。但如果值为 null,会抛出 TypeError。
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:! 的使用
void main() {
String? name = 'Bob';
// When you KNOW the value is non-null
print(name!.length); // 3 - OK because name is 'Bob'
// DANGEROUS: if null, crashes!
String? maybeNull;
// print(maybeNull!.length); // Runtime TypeError!
// Safe alternative: use ?? or if-null check
print(maybeNull?.length ?? 0); // 0 - safe
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
(2) 何时使用 !
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:合理的 ! 使用场景
class Order {
final String id;
final double amount;
Customer? customer; // Optional relationship
Order({required this.id, required this.amount});
// Use ! when business logic guarantees non-null
String get customerName {
// This is risky - customer might be null
// return customer!.name; // BAD
// Better: safe access with default
return customer?.name ?? 'Unknown Customer';
}
}
class Customer {
final String name;
Customer(this.name);
}
void main() {
final order = Order(id: 'ORD-001', amount: 1500.0);
print(order.customerName); // Unknown Customer
// After setting customer
order.customer = Customer('Alice');
print(order.customerName); // Alice
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
| 使用场景 | 推荐 | 不推荐 |
|---|---|---|
| 可空变量 | ?. / ?? / if-null |
! |
| API 返回值 | null 检查 | ! |
| 集合 first | .firstOrNull ?? default |
.first! |
| 断言后 | !(断言保证非空) |
— |
7. Null Safety 与集合
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:集合中的 null 处理
void main() {
// List with nullable elements
List<String?> names = ['Alice', null, 'Bob', null, 'Charlie'];
// Filter out nulls - type promotion works
final nonNull = names.whereType``<String>``().toList();
print(nonNull); // [Alice, Bob, Charlie]
// Map with nullable values
Map<String, double?> revenue = {
'Electronics': 3600.0,
'Books': null,
'Clothing': 890.0,
};
// Filter entries with non-null values
final validRevenue = Map.fromEntries(
revenue.entries.where((e) => e.value != null),
);
print(validRevenue); // {Electronics: 3600.0, Clothing: 890.0}
// Safe access with default
final booksRevenue = revenue['Books'] ?? 0;
print(booksRevenue); // 0
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
8. Bob 场景:DataPipeline Null Safety 设计
▶ 示例
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
:可选配置的 Null Safety 设计
class PipelineConfig {
// Required fields - non-nullable
final String appName;
final String version;
// Optional fields - nullable with defaults
final String? outputPath;
final String? logLevel;
final double? customTaxRate;
final int? maxRetries;
// Computed from optional - non-nullable
String get effectiveOutputPath => outputPath ?? '/tmp/datapipeline/output';
String get effectiveLogLevel => logLevel ?? 'info';
double get effectiveTaxRate => customTaxRate ?? 0.08;
int get effectiveMaxRetries => maxRetries ?? 3;
const PipelineConfig({
required this.appName,
required this.version,
this.outputPath,
this.logLevel,
this.customTaxRate,
this.maxRetries,
});
String get summary => '''
$appName v$version
Output: $effectiveOutputPath
Log: $effectiveLogLevel
Tax: ${(effectiveTaxRate * 100).toStringAsFixed(1)}%
Retries: $effectiveMaxRetries
''';
}
void main() {
// Minimal config - only required fields
final minimal = PipelineConfig(
appName: 'DataPipeline',
version: '1.0.0',
);
print(minimal.summary);
// Full config
final full = PipelineConfig(
appName: 'DataPipeline',
version: '2.0.0',
outputPath: '/data/reports',
logLevel: 'debug',
customTaxRate: 0.10,
maxRetries: 5,
);
print(full.summary);
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
9. 完整示例:DataPipeline Null Safe 订单处理
// ============================================
// DataPipeline Null-Safe Order Processing
// Complete demonstration of Null Safety patterns
// ============================================
class Customer {
final String name;
final String? email;
final Address? address;
Customer({required this.name, this.email, this.address});
String get displayName => email ?? name;
String get city => address?.city ?? 'Unknown City';
}
class Address {
final String city;
final String? state;
final String country;
Address({required this.city, this.state, required this.country});
String get fullRegion => state != null ? '$city, $state' : city;
}
class Order {
final String id;
final double amount;
final String status;
final Customer? customer;
final String? discountCode;
final double? discountPercent;
Order({
required this.id,
required this.amount,
required this.status,
this.customer,
this.discountCode,
this.discountPercent,
}) : assert(amount > 0, 'Amount must be positive');
double get effectiveDiscount => discountPercent ?? 0;
double get discountedAmount => amount * (1 - effectiveDiscount);
double get tax => discountedAmount * 0.08;
double get total => discountedAmount + tax;
String get customerName => customer?.name ?? 'Guest';
String get customerCity => customer?.city ?? 'Unknown';
String get formattedTotal =>
'\$${total.toStringAsFixed(2)} USD';
@override
String toString() =>
'Order($id, $customerName, ${formattedTotal}, $status)';
}
class OrderProcessor {
final List``<Order>`` _orders = [];
final List``<String>`` _warnings = [];
void addOrder(Order order) {
_orders.add(order);
// Null-safe checks with warnings
if (order.customer == null) {
_warnings.add('Order ${order.id}: No customer assigned');
}
if (order.discountCode != null && order.discountPercent == null) {
_warnings.add('Order ${order.id}: Discount code without rate');
}
}
double get totalRevenue =>
_orders.fold(0.0, (sum, o) => sum + o.total);
List``<Order>`` get ordersWithCustomers =>
_orders.where((o) => o.customer != null).toList();
Map<String, double> revenueByCity() {
final result = <String, double>{};
for (final order in _orders) {
final city = order.customerCity;
result.update(city, (v) => v + order.total, ifAbsent: () => order.total);
}
return result;
}
void printReport() {
print('=== DataPipeline Order Report ===');
print('Orders: ${_orders.length}');
print('Revenue: \$${totalRevenue.toStringAsFixed(2)} USD');
print('With customers: ${ordersWithCustomers.length}');
print('\nRevenue by City:');
for (final entry in revenueByCity().entries) {
print(' ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
}
if (_warnings.isNotEmpty) {
print('\nWarnings:');
for (final w in _warnings) {
print(' $w');
}
}
}
}
void main() {
final processor = OrderProcessor();
processor.addOrder(Order(
id: 'ORD-001',
amount: 1500.0,
status: 'completed',
customer: Customer(
name: 'Alice',
email: 'alice@example.com',
address: Address(city: 'New York', state: 'NY', country: 'US'),
),
discountPercent: 0.10,
));
processor.addOrder(Order(
id: 'ORD-002',
amount: 3200.0,
status: 'completed',
// No customer - nullable field
));
processor.addOrder(Order(
id: 'ORD-003',
amount: 890.0,
status: 'pending',
customer: Customer(
name: 'Bob',
address: Address(city: 'London', country: 'UK'),
),
discountCode: 'SAVE20',
// discountPercent is null - warning!
));
processor.printReport();
}
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
输出:
=== DataPipeline Order Report ===
Orders: 3
Revenue: $5172.36 USD
With customers: 2
Revenue by City:
New York: $1458.00 USD
Unknown: $3456.00 USD
London: $961.20 USD
Warnings:
Order ORD-002: No customer assigned
Order ORD-003: Discount code without rate
❓ 常见问题
Q:Sound Null Safety 和非 Sound 有什么区别? A:Sound Null Safety 保证非空类型在整个程序中永远不会为 null(编译器全局验证)。非 Sound 模式(已废弃)允许某些路径绕过检查。Dart 3 默认 Sound。
Q:什么时候用 late 而不用可空类型? A:当你确定变量在使用前一定会初始化,且不希望它为 null 时用 late。如果变量确实可能为 null(语义上可选),用 T?。
Q:
!应该完全避免吗? A:不是完全避免,但要尽量少用。合理场景:断言后、测试代码、业务逻辑明确保证非空时。大多数场景用??或?.更安全。
Q:null 检查提升在闭包中有效吗? A:不一定。如果变量可能在闭包外被修改,编译器无法保证提升。用局部 final 变量捕获来解决。
Q:泛型类型参数默认可空吗? A:不是。
T默认不可空,T?可空。这确保了泛型的类型安全。T?实际上是T的超类型。
Q:如何处理 JSON 解析中的大量可空字段? A:推荐用
??提供默认值,或使用 json_serializable 包自动生成带默认值的 fromJson 方法。避免到处用!。
Q:late final 和 final 有什么区别? A:final 必须在声明时或构造函数初始化列表中赋值;late final 可以在构造函数体或后续方法中首次赋值。late final 延迟了赋值时机。
📖 小节
- Sound Null Safety 将类型分为 T(非空)和 T?(可空),编译器全局保证
- null 检查后编译器自动类型提升,不需要手动类型转换
- late 延迟初始化,但未初始化就访问会崩溃;late final + 初始化表达式最安全
!强制解包有风险,应尽量用??或?.替代- DataPipeline 配置设计模式:必填字段非空,可选字段可空 + getter 提供默认值
📝 作业
- 基础题(难度⭐):声明 5 个变量(2 个非空、3 个可空),用 if-null 检查、
??、?.三种方式安全访问可空变量,打印结果。 - 进阶题(难度⭐⭐):设计一个
Config类,用 late 延迟初始化 3 个字段,在 configure() 方法中设置。故意不调用 configure() 就访问字段,观察 LateInitializationError。 - 挑战题(难度⭐⭐⭐):实现一个 NullSafe API 客户端,所有网络请求返回
Result<T?>类型,正确处理:服务器返回 null、字段缺失、类型不匹配三种情况,全程不使用!。