Dart: Dart Null Safety — 从原理到实践消灭空指针

Null Safety 是 Dart 的铠甲 — 编译器帮你挡住 95% 的空指针攻击。

1. 你将学到


2. 一个开发者的真实故事

(1) 痛点:NullPointerException 是最常见的运行时崩溃

Bob 的 DataPipeline 在生产环境中,60% 的运行时崩溃都是空指针异常。API 返回了 null 的订单 ID,CSV 中缺失了可选字段,配置项未设置就使用 — 每种情况都导致程序崩溃,平均每次事故影响 50,000 条订单的处理。

(2) Sound Null Safety 的解法

Dart 3 的 Sound Null Safety 将类型分为可空 T? 和非空 T,编译器在编译时保证非空类型永远不会为 null。

DART
// 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
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

(3) 收益


3. Sound Null Safety 原理

(1) 类型体系

100%
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"]
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:可空 vs 非空类型

DART
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)
}
TEXT 📖 仅展示
> **输出:** 在本地 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 后,自动将其提升为非空类型。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:if 检查提升

DART
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!
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:多种 null 检查方式

DART
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');
}
TEXT 📖 仅展示
> **输出:** 在本地 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) 延迟初始化

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:late 基础用法

DART
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;
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:late 的风险

DART
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';
}
TEXT 📖 仅展示
> **输出:** 在本地 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。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:! 的使用

DART
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
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

(2) 何时使用 !

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:合理的 ! 使用场景

DART
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
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
使用场景 推荐 不推荐
可空变量 ?. / ?? / if-null !
API 返回值 null 检查 !
集合 first .firstOrNull ?? default .first!
断言后 !(断言保证非空)

7. Null Safety 与集合

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:集合中的 null 处理

DART
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
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

8. Bob 场景:DataPipeline Null Safety 设计

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:可选配置的 Null Safety 设计

DART
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);
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

9. 完整示例:DataPipeline Null Safe 订单处理

DART
// ============================================
// 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();
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

输出:

TEXT 📖 仅展示
=== 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 延迟了赋值时机。


📖 小节


📝 作业

  1. 基础题(难度⭐):声明 5 个变量(2 个非空、3 个可空),用 if-null 检查、???. 三种方式安全访问可空变量,打印结果。
  2. 进阶题(难度⭐⭐):设计一个 Config 类,用 late 延迟初始化 3 个字段,在 configure() 方法中设置。故意不调用 configure() 就访问字段,观察 LateInitializationError。
  3. 挑战题(难度⭐⭐⭐):实现一个 NullSafe API 客户端,所有网络请求返回 Result<T?> 类型,正确处理:服务器返回 null、字段缺失、类型不匹配三种情况,全程不使用 !

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏