1.11 异常处理

基础知识

在程序运行过程中,可能会遇到各种错误或异常情况,例如网络连接失败、文件不存在、无效的用户输入等。如果不对这些异常进行妥善处理,程序可能会崩溃。Dart 提供了健壮的异常处理机制,允许我们捕获和响应这些运行时错误,从而使程序更加稳定和健壮。

Dart 中的异常是表示程序运行时发生意外情况的对象。与许多其他语言不同,Dart 中的所有异常都是非检查异常(Unchecked Exceptions),这意味着编译器不会强制你捕获它们。然而,为了程序的健壮性,仍然强烈建议捕获和处理可能发生的异常。

1. try-on-catch-finally 语句

Dart 使用 tryoncatchfinally 关键字来处理异常:

  • try:包含可能抛出异常的代码块。
  • on:用于指定要捕获的特定异常类型。如果 try 块中抛出的异常类型与 on 后面指定的类型匹配,则执行 on 块中的代码。
  • catch:用于捕获所有类型的异常,或者在 on 之后捕获特定类型的异常。catch 块可以接收两个参数:第一个是捕获到的异常对象,第二个是堆栈跟踪信息。
  • finally:无论 try 块中是否发生异常,finally 块中的代码都会被执行。它通常用于执行清理操作,例如关闭文件或释放资源。
dart
复制代码
void divide(int a, int b) {
  try {
    print("结果: ${a ~/ b}"); // 可能会抛出 IntegerDivisionByZeroException
  } on IntegerDivisionByZeroException {
    print("错误: 除数不能为零!");
  } on FormatException catch (e) {
    print("格式错误: $e");
  } catch (e, s) { // 捕获所有其他异常,并获取堆栈信息
    print("发生未知错误: $e");
    print("堆栈跟踪: $s");
  } finally {
    print("除法操作完成。");
  }
}

void main() {
  divide(10, 2); // 正常执行
  print("\n");
  divide(10, 0); // 捕获 IntegerDivisionByZeroException
  print("\n");
  // 模拟其他异常
  try {
    throw Exception("这是一个自定义异常");
  } catch (e) {
    print("捕获到自定义异常: $e");
  }
}
// 预期输出:
// 结果: 5
// 除法操作完成。

// 错误: 除数不能为零!
// 除法操作完成。

// 捕获到自定义异常: Exception: 这是一个自定义异常

2. throw 关键字

可以使用 throw 关键字显式地抛出异常。Dart 中的任何对象都可以作为异常被抛出,但通常建议抛出实现了 ErrorException 类的对象。

dart
复制代码
void checkAge(int age) {
  if (age < 0) {
    throw ArgumentError("年龄不能为负数");
  } else if (age > 150) {
    throw Exception("年龄不能超过150岁");
  }
  print("年龄有效: $age");
}

void main() {
  try {
    checkAge(30);
    checkAge(-5);
  } on ArgumentError catch (e) {
    print("捕获到参数错误: ${e.message}");
  } catch (e) {
    print("捕获到其他错误: $e");
  }
}
// 预期输出:
// 年龄有效: 30
// 捕获到参数错误: 年龄不能为负数

3. 自定义异常

可以创建自定义异常类,通过继承 Exception 类来实现。这有助于更好地组织和区分不同类型的错误。

dart
复制代码
class MyCustomException implements Exception {
  final String message;

  MyCustomException(this.message);

  @override
  String toString() => "MyCustomException: $message";
}

void performRiskyOperation(int value) {
  if (value < 0) {
    throw MyCustomException("输入值不能为负数");
  } else if (value == 0) {
    throw StateError("状态错误:输入值为零");
  }
  print("操作成功,值为: $value");
}

void main() {
  try {
    performRiskyOperation(10);
    performRiskyOperation(-1);
  } on MyCustomException catch (e) {
    print("捕获到自定义异常: ${e.message}");
  } on StateError catch (e) {
    print("捕获到状态错误: ${e.message}");
  } catch (e) {
    print("捕获到未知异常: $e");
  }
}
// 预期输出:
// 操作成功,值为: 10
// 捕获到自定义异常: 输入值不能为负数

官方文档链接

Flutter 开发中的应用案例

在 Flutter 应用中,异常处理是确保应用稳定性和用户体验的关键。无论是网络请求失败、文件读写错误,还是用户输入不合法,都需要进行适当的异常处理,以避免应用崩溃并向用户提供有用的反馈。

案例:用户登录表单的异常处理

我们将创建一个简单的用户登录表单,演示如何在处理用户输入和模拟网络请求时进行异常处理,并向用户显示相应的错误信息。

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

// 自定义登录异常
class LoginException implements Exception {
  final String message;
  LoginException(this.message);

  @override
  String toString() => 'LoginException: $message';
}

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  bool _isLoading = false;
  String? _errorMessage;

  Future<void> _login() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    final String username = _usernameController.text;
    final String password = _passwordController.text;

    try {
      // 模拟网络请求延迟
      await Future.delayed(const Duration(seconds: 2));

      // 模拟登录逻辑和异常情况
      if (username.isEmpty || password.isEmpty) {
        throw LoginException('用户名或密码不能为空');
      } else if (username == 'test' && password == 'password') {
        // 登录成功
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('登录成功!')),
        );
        // 导航到主页或其他页面
      } else if (username == 'error' && password == 'error') {
        throw Exception('模拟服务器内部错误');
      } else {
        throw LoginException('用户名或密码不正确');
      }
    } on LoginException catch (e) {
      // 捕获自定义登录异常
      setState(() {
        _errorMessage = e.message;
      });
    } catch (e) {
      // 捕获其他未知异常
      setState(() {
        _errorMessage = '发生未知错误: ${e.toString()}';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('用户登录'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(
                labelText: '用户名',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16.0),
            TextField(
              controller: _passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: '密码',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 24.0),
            if (_errorMessage != null)
              Padding(
                padding: const EdgeInsets.only(bottom: 16.0),
                child: Text(
                  _errorMessage!,
                  style: const TextStyle(color: Colors.red, fontSize: 16.0),
                ),
              ),
            ElevatedButton(
              onPressed: _isLoading ? null : _login,
              style: ElevatedButton.styleFrom(
                minimumSize: const Size(double.infinity, 50), // 按钮宽度填充,高度50
              ),
              child: _isLoading
                  ? const CircularProgressIndicator(color: Colors.white) // 加载时显示进度指示器
                  : const Text(
                      '登录',
                      style: TextStyle(fontSize: 18.0),
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: LoginScreen()));
}

案例分析:

  • class LoginException implements Exception:我们定义了一个自定义异常 LoginException,用于表示登录相关的错误。这使得我们可以更精确地捕获和处理特定类型的错误。
  • Future<void> _login() async { ... }:登录逻辑被封装在一个 async 函数中,以便进行异步操作(模拟网络请求)。
  • try { ... } on LoginException catch (e) { ... } catch (e) { ... } finally { ... }
    • try 块包含了可能抛出异常的代码,例如用户输入验证和模拟的登录请求。
    • on LoginException catch (e) 专门捕获 LoginException 类型的异常,并将其错误信息显示给用户。
    • catch (e) 捕获所有其他类型的异常,作为通用错误处理。
    • finally 块确保无论是否发生异常,_isLoading 状态都会被重置为 false,从而解除按钮的禁用状态。
  • throw LoginException('用户名或密码不能为空');:在验证用户输入时,如果发现用户名或密码为空,我们显式地抛出 LoginException。这是一种主动报告错误的方式。
  • throw Exception('模拟服务器内部错误');:为了演示通用异常的捕获,我们模拟了一个服务器内部错误,抛出了一个普通的 Exception
  • _errorMessage != null:在 UI 中,我们根据 _errorMessage 是否为 null 来决定是否显示错误信息。这是一种常见的错误反馈机制。
  • _isLoading ? null : _login:当 _isLoadingtrue 时,登录按钮的 onPressed 回调被设置为 null,从而禁用按钮,防止用户重复点击。同时,按钮的文本会变为 CircularProgressIndicator,提供视觉反馈。

这个案例清晰地展示了 Dart 的异常处理机制在 Flutter 应用中的实际应用。通过合理地使用 try-on-catch-finallythrow 关键字,我们可以构建出更加健壮、用户友好的应用程序,即使在面对各种运行时错误时也能保持稳定。