dart学习第 20 节:错误处理与日志 —— 让程序更健壮

在前几节课中,我们学习了元数据与反射,了解了代码自我描述的高级技巧。今天我们将聚焦于让程序更健壮的核心技术 ------错误处理与日志。无论多么完美的程序都可能出现意外,而优秀的错误处理机制能让程序在异常情况下优雅降级,详细的日志则能帮助我们快速定位问题根源。

一、错误与异常:Dart 中的两种错误类型

Dart 中存在两种主要的错误类型:Exception(异常)和 Error(错误),它们都继承自 Object 并实现了 Exception 接口,但适用场景截然不同。

1. Exception(异常):可预见的错误

Exception 表示程序运行过程中可能发生且应该被捕获处理的异常情况,通常是由外部因素或合理的业务逻辑错误导致的。

常见的内置 Exception 类型:

  • FormatException:格式错误(如字符串转数字失败)
  • IOException:IO 操作异常(如文件不存在)
  • TimeoutException:超时异常
  • ArgumentError:参数错误(如传递无效参数)

示例:处理格式异常

dart 复制代码
void main() {
  String numberStr = "123a";

  try {
    int number = int.parse(numberStr);
    print("转换结果:$number");
  } on FormatException {
    print("格式错误:无法将 '$numberStr' 转换为数字");
  } catch (e) {
    // 捕获其他类型的异常
    print("发生未知异常:$e");
  }
}

2. Error(错误):不可恢复的错误

Error 表示程序中的严重问题,通常是由代码逻辑错误导致的,不应该被捕获(捕获后也难以恢复),目的是让程序崩溃并暴露问题。

常见的内置 Error 类型:

  • AssertionError:断言失败(调试阶段检查条件是否成立)
  • NullThrownError:抛出了 null
  • RangeError:索引越界
  • NoSuchMethodError:调用了不存在的方法

示例:索引越界错误(不应该捕获)

dart 复制代码
void main() {
  List<int> numbers = [1, 2, 3];

  try {
    print(numbers[5]); // 索引越界
  } on RangeError catch (e) {
    // 不推荐:这类错误应该在开发阶段修复,而不是捕获
    print("捕获到错误:$e");
  }
}

最佳实践:

  • Exception 用于可预期的异常情况(如网络请求失败),应该捕获并处理
  • Error 用于程序逻辑错误(如空指针、索引越界),应该在开发阶段修复,而非捕获

3. 异常捕获的完整语法

Dart 提供了灵活的异常捕获机制,包含 tryoncatchrethrowfinally

dart 复制代码
import 'dart:io';

void main() {
  try {
    // 可能抛出异常的代码
    riskyOperation();
  } on FormatException catch (e) {
    // 捕获特定类型的异常,并获取异常对象
    print("格式错误:$e");
  } on IOException catch (e, s) {
    // 捕获特定类型的异常,并获取堆栈跟踪(StackTrace)
    print("IO错误:$e");
    print("堆栈信息:$s");
  } catch (e) {
    // 捕获所有异常(相当于 on Exception catch (e))
    print("未知异常:$e");
    rethrow; // 重新抛出异常,让上层处理
  } finally {
    // 无论是否发生异常,都会执行的代码(如资源释放)
    print("操作结束,清理资源");
  }
}

void riskyOperation() {
  throw FormatException("无效的格式");
}
  • on 用于指定捕获的异常类型
  • catch 用于获取异常对象和堆栈跟踪
  • rethrow 用于将异常重新抛出,不破坏原始堆栈信息
  • finally 用于执行必须完成的操作(如关闭文件、释放连接)

二、自定义异常:处理业务特定错误

在实际开发中,系统内置的异常类型往往不足以描述业务场景中的错误(如 "用户不存在""余额不足" 等)。这时我们可以定义自定义异常,使错误处理更精准。

1. 定义自定义异常

自定义异常通常继承自 Exception 类,并包含描述错误的信息:

dart 复制代码
// 用户相关异常
class UserException implements Exception {
  final String message;
  final String? userId; // 可选的用户ID,便于定位问题

  UserException(this.message, {this.userId});

  // 重写toString,便于打印异常信息
  @override
  String toString() {
    return "UserException${userId != null ? ' (用户ID: $userId)' : ''}: $message";
  }
}

// 支付相关异常
class PaymentException implements Exception {
  final String message;
  final double amount; // 涉及的金额

  const PaymentException(this.message, this.amount);
}

2. 抛出与捕获自定义异常

在业务逻辑中抛出自定义异常,并在合适的地方捕获处理:

dart 复制代码
// 模拟用户服务
class UserService {
  // 检查用户是否存在
  void checkUserExists(String userId) {
    // 模拟用户不存在的情况
    if (userId == "10086") {
      throw UserException("用户不存在", userId: userId);
    }
  }
}

// 模拟支付服务
class PaymentService {
  // 处理支付
  void processPayment(String userId, double amount) {
    if (amount <= 0) {
      throw ArgumentError("支付金额必须大于0");
    }

    // 模拟余额不足
    if (amount > 1000) {
      throw PaymentException("余额不足", amount);
    }
  }
}

void main() async {
  final userService = UserService();
  final paymentService = PaymentService();
  const userId = "10086";
  const amount = 1500.0;

  try {
    userService.checkUserExists(userId);
    paymentService.processPayment(userId, amount);
    print("支付成功");
  } on UserException catch (e) {
    // 处理用户相关异常
    print("用户错误:$e");
    // 可以在这里添加日志上报、提示用户等逻辑
  } on PaymentException catch (e) {
    // 处理支付相关异常
    print("支付错误:${e.message},金额:${e.amount}");
  } on ArgumentError catch (e) {
    // 处理参数错误
    print("参数错误:$e");
  } catch (e) {
    // 兜底处理其他异常
    print("发生未知错误:$e");
  }
}

输出结果:

plaintext 复制代码
用户错误:UserException (用户ID: 10086): 用户不存在

自定义异常的优势:

  • 更精准地描述业务错误场景
  • 便于分类处理不同类型的错误
  • 可以携带更多上下文信息(如用户 ID、金额等),助力问题定位

三、日志工具:记录程序运行轨迹

日志是调试和问题排查的重要依据。Dart 内置的 print 函数功能有限,实际开发中推荐使用更强大的日志库,如 logger 包。

1. 使用 logger 包

logger 包提供了分级日志、格式化输出、堆栈跟踪等功能,使用简单且强大。

(1)添加依赖

pubspec.yaml 中添加:

yaml 复制代码
dependencies:
  logger: ^2.6.1

执行 dart pub get 安装。

(2)基本使用
dart 复制代码
import 'package:logger/logger.dart';

// 创建日志实例
final logger = Logger(
  // 日志输出格式
  printer: PrettyPrinter(
    methodCount: 2, // 显示的方法调用栈数量
    errorMethodCount: 5, // 错误日志显示的方法调用栈数量
    lineLength: 120, // 每行长度
    colors: true, // 彩色输出
    printEmojis: true, // 显示 emoji
    printTime: true, // 显示时间
  ),
);

void main() {
  // 不同级别的日志
  logger.v("Verbose 日志:最详细的调试信息");
  logger.d("Debug 日志:调试过程中的信息");
  logger.i("Info 日志:正常的运行信息");
  logger.w("Warning 日志:需要注意的潜在问题");
  logger.e("Error 日志:错误信息");
  logger.wtf("WTF 日志:严重错误,可能导致程序崩溃");

  // 记录异常
  try {
    throw FormatException("无效的格式");
  } catch (e, s) {
    logger.e("解析失败", error: e, stackTrace: s);
  }
}

日志级别从低到高:verbose < debug < info < warning < error < wtf,可以通过配置控制输出哪些级别的日志(如生产环境只输出 info 及以上级别)。

2. 自定义日志输出

logger 包支持自定义日志输出方式(如写入文件、发送到服务器等):

dart 复制代码
import 'package:logger/logger.dart';
import 'dart:io';

// 自定义日志输出:同时打印到控制台和文件
class FileOutput extends LogOutput {
  final File logFile;

  FileOutput(this.logFile);

  @override
  void output(OutputEvent event) {
    // 1. 输出到控制台
    for (var line in event.lines) {
      print(line);
    }

    // 2. 写入文件(异步)
    _writeToFile(event);
  }

  Future<void> _writeToFile(OutputEvent event) async {
    final time = DateTime.now().toIso8601String();
    final logContent = event.lines.map((line) => "[$time] $line\n").join();

    try {
      await logFile.writeAsString(logContent, mode: FileMode.append);
    } catch (e) {
      print("日志写入文件失败:$e");
    }
  }
}

void main() {
  // 创建日志文件
  final logFile = File("app_logs.txt");

  // 配置日志器
  final logger = Logger(
    level: Level.debug, // 输出 debug 及以上级别
    output: FileOutput(logFile), // 使用自定义输出
    printer: SimplePrinter(), // 简单格式
  );

  logger.i("应用启动");
  logger.w("磁盘空间不足");
}

3. 日志最佳实践

  • 分级输出:根据日志重要性使用不同级别,便于过滤
  • 包含上下文:日志中应包含时间、用户 ID、请求 ID 等上下文信息
  • 敏感信息脱敏:避免记录密码、令牌等敏感信息
  • 控制体积:日志文件应定期轮转(删除旧日志),避免占满磁盘
  • 环境区分:开发环境输出详细日志,生产环境只输出关键信息

四、错误上报:线上问题监控

对于线上运行的程序,仅靠本地日志是不够的,需要一套错误上报机制,及时发现并解决用户遇到的问题。

1. 错误上报的基本流程

  1. 捕获错误:全局捕获未处理的异常和错误
  2. 收集信息:收集错误详情、设备信息、用户操作轨迹等
  3. 发送报告:将错误信息发送到后端服务器
  4. 分析处理:开发人员在后台查看错误报告并修复

2. 全局异常捕获

Dart 中可以通过 runZonedGuarded 捕获全局未处理的异常:

dart 复制代码
import 'dart:async';
import 'package:logger/logger.dart';

final logger = Logger();

// 错误上报服务
class ErrorReportingService {
  // 上报错误信息
  static Future<void> reportError(
    dynamic error,
    dynamic stackTrace, {
    Map<String, dynamic>? extraInfo,
  }) async {
    try {
      // 1. 收集错误信息
      final report = {
        'error': error.toString(),
        'stackTrace': stackTrace.toString(),
        'time': DateTime.now().toIso8601String(),
        'extra': extraInfo ?? {},
        // 可以添加设备信息、用户ID等
      };

      // 2. 本地日志记录
      logger.e("错误上报", error: error, stackTrace: stackTrace);

      // 3. 发送到服务器(实际项目中替换为真实接口)
      // await http.post(
      //   Uri.parse('https://your-api.com/errors'),
      //   body: report,
      // );

      print("错误上报成功:$report");
    } catch (e) {
      logger.e("错误上报失败", error: e);
    }
  }
}

void main() {
  // 使用 runZonedGuarded 捕获全局异常
  runZonedGuarded(
    () {
      // 程序入口逻辑
      runApp();
    },
    (error, stackTrace) {
      // 捕获未处理的异常
      ErrorReportingService.reportError(
        error,
        stackTrace,
        extraInfo: {'scene': 'app_start'},
      );
    },
  );
}

void runApp() {
  // 模拟程序运行中发生的错误
  throw UserException("用户会话过期");
}

// 自定义异常
class UserException implements Exception {
  final String message;
  UserException(this.message);
}

3. Flutter 中的错误捕获

在 Flutter 中,除了 Dart 层的异常,还需要处理 Flutter 框架层的错误:

dart 复制代码
import 'dart:async';
import 'package:flutter/material.dart';

void main() {
  // 捕获 Flutter 框架异常
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    // 上报到错误服务
    ErrorReportingService.reportError(
      details.exception,
      details.stack,
      extraInfo: {'type': 'flutter_framework_error'},
    );
  };

  // 捕获 Dart 层未处理异常
  runZonedGuarded(() => runApp(const MyApp()), (error, stackTrace) {
    ErrorReportingService.reportError(
      error,
      stackTrace,
      extraInfo: {'type': 'dart_uncaught_error'},
    );
  });
}

4. 错误上报的注意事项

  • 用户隐私保护:遵守数据保护法规,不上报敏感信息(如地理位置、通讯录)
  • 批量与压缩:错误报告应压缩后发送,避免频繁请求影响用户体验
  • 离线缓存:网络不佳时缓存错误报告,待网络恢复后发送
  • 避免递归上报:确保错误上报逻辑本身不会抛出异常,导致无限循环

五、综合案例:健壮的网络请求工具

结合错误处理、日志和错误上报,实现一个健壮的网络请求工具:

dart 复制代码
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:logger/logger.dart';

final logger = Logger();

// 自定义异常
class NetworkException implements Exception {
  final String message;
  final int? statusCode; // HTTP状态码
  final String? url;

  NetworkException(this.message, {this.statusCode, this.url});

  @override
  String toString() {
    return "NetworkException (${statusCode ?? 'unknown'}): $message${url != null ? ' (URL: $url)' : ''}";
  }
}

// 网络请求工具
class ApiClient {
  // 发送GET请求
  Future<dynamic> get(String url) async {
    logger.i("发送GET请求", error: {'url': url});

    try {
      final response = await http
          .get(Uri.parse(url))
          .timeout(const Duration(seconds: 10)); // 超时设置

      if (response.statusCode == 200) {
        logger.d("请求成功", error: {'url': url, 'statusCode': 200});
        return json.decode(response.body);
      } else {
        // 非200状态码视为错误
        throw NetworkException(
          "请求失败,状态码:${response.statusCode}",
          statusCode: response.statusCode,
          url: url,
        );
      }
    } on http.ClientException catch (e) {
      // 网络连接错误
      final exception = NetworkException("网络连接错误: ${e.message}", url: url);
      logger.e("网络请求失败", error: exception);
      // 上报错误
      _reportError(exception);
      rethrow;
    } on TimeoutException {
      // 超时错误
      final exception = NetworkException("请求超时", url: url);
      logger.e("网络请求失败", error: exception);
      _reportError(exception);
      rethrow;
    } on FormatException {
      // 解析错误
      final exception = NetworkException("响应格式错误", url: url);
      logger.e("网络请求失败", error: exception);
      _reportError(exception);
      rethrow;
    } catch (e) {
      // 其他错误
      final exception = NetworkException("未知错误: $e", url: url);
      logger.e("网络请求失败", error: exception);
      _reportError(exception);
      rethrow;
    }
  }

  // 错误上报
  void _reportError(NetworkException e) {
    // 实际项目中实现错误上报逻辑
    logger.w("错误将被上报: $e");
  }
}

// 使用示例
void main() async {
  final apiClient = ApiClient();

  try {
    final data = await apiClient.get("https://api.example.com/data");
    print("获取数据成功: $data");
  } on NetworkException catch (e) {
    // 处理网络异常(如提示用户检查网络)
    print("请求失败: $e");
  }
}

这个网络请求工具具备以下特性:

  • 捕获各种可能的网络错误(连接失败、超时、解析错误等)
  • 使用自定义异常 NetworkException 统一错误类型
  • 详细的日志记录(请求开始、成功、失败)
  • 错误上报机制
  • 对用户友好的错误提示
相关推荐
TralyFang3 小时前
flutter key:ValueKey、ObjectKey、UniqueKey、GlobalKey的使用场景
flutter
叽哥3 小时前
dart学习第 19 节:元数据与反射 —— 代码的 “自我描述”
flutter·dart
叽哥4 小时前
dart学习第 15 节:Stream—— 处理连续数据流
flutter·dart
w_y_fan5 小时前
Flutter中蓝牙开发:flutter_blue_plus的应用理解
flutter
LZQ <=小氣鬼=>5 小时前
Flutter简单讲解
flutter
来来走走7 小时前
Flutter开发 StatelessWidget与StatefulWidget基本了解
android·flutter
LZQ <=小氣鬼=>8 小时前
Flutter 事件总线 Event Bus
flutter·dart·事件总线·event bus
天岚11 小时前
温故知新-SchedulerBinding
flutter
0wioiw013 小时前
Apple基础(Xcode⑤-Flutter-Singbox-AI提示词)
flutter·macos·xcode