Dart学习笔记(31):Future和异常处理

发表于2018-07-04 13:19 阅读(45)

在旧式风格的同步代码中,你可以通过try-catch代码块捕获异常,始终保证程序不会完全地崩溃,例如:

本文地址:http://www.cndartlang.com/708.html

try {
  throw new Exception("Oh, error!");
} catch (e) {
  logger.log(e); 
}

在Dart中,Future是一个特别重要的概念,并且无处不在。由于Future会将任务添加到事件队列,因此计算过程中产生的异常并不在当前代码块中,以致try-catch并不能捕获Future中的异常。

try {
  /**
  * 抛出异常,程序崩溃
  * try-catch不能捕获Future异常
  */
  new Future.error("Oh, error!"); 
} catch (e) {
  logger.log(e); 
}

当然,你可以使用Future.catchError来捕获信息。当调用Future,并且计算正常返回值的时候,then()注册的回调函数会被触发;如果计算或then()注册的回调函数返回异常,catchError()会被触发。

异常可能发生在计算过程或回调函数中,并且当计算的过程中出现异常的时候,then()注册的回调函数并不会被触发:

myFunc().then(processValue)
        .catchError(handleError);

我们可以通过myFunc().catchError()仅捕获计算过程中的异常,如果我们要处理计算返回的值,并且区分异常产生的位置,可以设置then()的onError参数:

myFunc()
  .then(successCallback, onError: (e) {
    ...
  })
  .catchError(handleError);

上面的代码中,计算过程中出现的异常由catchError注册的回调函数处理,而then()中产生的异常则由onError注册的回调函数处理。

对于捕获到的异常事件,可以在catchError注册的毁掉函数中进行判断,然后进行不同的操作。不过Future提供了更优雅的方式,通过可选参数bool test(Object error)来进行分类,当测试函数返回true的时候,执行对应的回调函数:

myFunc()
    .then(...)
    .catchError(handleFormatException,
                test: (e) => e is FormatException)
    .catchError(handleAuthorizationException,
                test: (e) => e is AuthorizationException);

如果说then().catchError()相当于try-catch的镜像函数的话,whenComplete()则等同于finally。无论是正常返回值还是出现异常,都会执行注册的回调函数:

var server = connectToServer();
server.post(myUrl, fields: {"name": "john", "profession": "juggler"})
      .then(handleResponse)
      .catchError(handleError)
      .whenComplete(server.close);

then()、catchError()、whenComplete()返回的值都是Future,因此可以继续注册then、catchError、whenComplete,形成Future链,如:

myFunc()
    .then((_) => new Future.error("Complete with Error #1"))
    .whenComplete(() => print("Done"))
    .then((_) => print("whenComplete future compelete"))
    .catchError((e) => print(e));

首先,第一个then会产生一个异常”Error #1“,并返回Future。由于注册了whenComplete,因此无论计算过程是否正常都会执行回调函数,在这个时候返回值仍然是Error Future,接下来,由于存在异常,之后的then并不会执行,最后由catchError捕获。输出结果为:

Done
Complete with Error #1

但在过程中可能又会产生新的异常,如果出现新的Error Future,则会以新的Error完成Future,即覆盖之前的Error:

myFunc()
    .then((_) => new Future.error("Complete with Error #1"))
    .whenComplete(() => new Future.error("Complete with Error #2"))
    .then((_) => print("whenComplete future compelete"))
    .catchError((e) => print(e));

输出结果为:

Complete with Error #2

在处理异常的时候还需要意以下2种情况:

情况一:异常的处理函数必须在Future完成之前注册,避免Future出现Error并抛出异常的时候,捕获异常的回调函数还没有注册成功

例如:

import 'dart:async';

myFunc() async {
  throw new Exception("Dart Exception");
  return "Dart";
}

main() {
  Future future = myFunc();

  new Future.delayed(const Duration(milliseconds: 500), () {
    future.then((value) => print(value))
        .catchError((e) => print(e));
  });
}

上面模拟注册延迟500毫秒,myFunc()被调用前,catchError并未成功注册,异常未正常捕获,如果将myFunc()放到Future.delayed(),问题解决:

main() {
  new Future.delayed(const Duration(milliseconds: 500), () {
    myFunc().then((value) => print(value))
        .catchError((e) => print(e));
  });
}

情况二:意外地混合同步类型和异步类型的异常

首先需要注意,throw new Exception产生的是同步类型的异常,如果只是字符串,等同于throw(“…”)或throw “…”,而new Future.error产生的是异步类型的异常。通常,我们写的是同步代码:

import 'dart:async';

String obtainFilePath(fileName) {
  return "D:/" + fileName;
}

String parseFileData(filePath) {
  return filePath + "\nFileData:CNDartLang";
}

Future parseAndRead() {
  String filePath = obtainFilePath("Temp.txt");
  return new Future(() {
    return print(parseFileData(filePath));
  });
}

main() {
  parseAndRead().catchError((e) {
      print(e);
    });
}

上面的代码中,obtainFilePath用于获取文件路径,parseFileData用于解析文件内容,两个都是同步函数,代码只有return,简单模拟一下过程。parseAndRead是异步函数,返回值是Future,因此main函数中,parseAndRead可以调用then、catchError等函数。

在parseAndRead函数返回值的时候,return print(…)代码中的return不能省略,逻辑为:如果计算成功,则返回一个空值的Future,如果print(…)中抛出异常,则返回Error Future。如果不return,则会返回一个空值的Future,外层的catchError不能捕获到异常信息。

运行结果:

D:/Temp.txt
FileData:CNDartLang

在obtainFilePath和parseFileData两个函数中,有可能抛出同步类型异常,因为parseFileData封装到了Future中,当出现异常的时候,会抛出Error Future,parseAndRead能够正常捕获到,封装到Future.then中同样会抛出Error Future,如:

String parseFileData(filePath) {
  throw("Parse FileData Error");
  return filePath + "\nFileData:CNDartLang";
}

但是如果异常出现在obtainFilePath中,则会出错,因为catchError不能捕获同步类型的异常:

String obtainFilePath(fileName) {
  throw("Obtain Error");
  return "D:/" + fileName;
}

//Unhandled exception:
//Obtain Error
//...

解决方案有三个:

1、使用Future.sync对代码进行封装

当回调函数返回值为非Future的时候,该函数返回对应值的Future;当回调函数抛出异常,该函数返回值为异常的Future;当回调函数返回Future的时候,包括Error Future,该函数返回Future。之前的代码中,parseAndRead可以封装如下:

Future parseAndRead() {
  return new Future.sync(() {
    String filePath = obtainFilePath("Temp.txt");
    return new Future(() {
      return print(parseFileData(filePath));
    });
  });
}

2、使用async关键字对函数进行声明,自动将同步类型代码封装为Future

parseAndRead() async {
  String filePath = obtainFilePath("Temp.txt");
  return new Future(() {
    return print(parseFileData(filePath));
  });
}

3、使用Zone对parseAndRead出现的异常进行捕获

下一篇文章会对Zone作详细介绍:

runZoned(() {
  parseAndRead().catchError((e) {
    print(e);
  });
}, onError: (e) {
  print(e);
});