Flutter中使用Either和fpdart的功能错误处理方法

734 阅读8分钟

Dart语言提供了一些基元,如trycatchthrow ,用于处理我们应用程序中的错误和异常。

但正如我们在上一篇文章中所了解的,如果我们依赖内置的异常处理系统,很容易写出不安全的代码

  • 我们只能通过阅读一个函数的文档和/或实现来了解该函数是否会抛出异常
  • 我们不能利用静态分析和类型系统来明确地处理错误。

我们可以通过将一些函数式编程原则应用于错误处理并编写更安全、更健壮的代码来克服其中的一些问题。

而在这篇文章中,我们将通过学习fpdart包中的 Eitherfpdart包中的类型。

因为有很多东西要讲,所以这里将专注于基础知识。而在下一篇文章中,我们将学习如何在真实世界的Flutter应用中使用以下方法处理错误和异步代码 TaskEither.

在之前关于Flutter异常处理的文章中,我们已经看到如何使用Result 类型来更明确地处理错误。正如我们将看到的,EitherResult 非常相似,而且fpdart 包提供了额外的 API,帮助我们超越Result 类型的限制。

因此,让我们从函数式编程的简要概述开始吧。

什么是函数式编程?

功能性编程(FP)是一个引人入胜的话题,它提倡使用纯函数不可变数据声明式的编程风格,帮助我们写出更干净和可维护的代码。

这与面向对象编程(OOP)形成鲜明对比,后者依赖于易变的状态命令式的编程风格来描述我们系统中对象的数据行为

由于许多现代语言同时支持函数式面向对象的范式,你可以在你的代码中采用一种或另一种风格,只要你认为合适。

事实上,你可能已经在你的Dart和Flutter代码中通过以下方式使用了FP。

  • 将一个函数作为参数传递给另一个函数(如回调)。
  • map,where,reduce 函数操作符上使用Iterable 类型,如列表和流
  • 使用泛型类型推理

其他函数式编程的特点,如模式匹配重构多返回值高阶类型,已经在Dart语言漏斗中讨论过了。

错误处理是FP可以带来实质性好处的一个领域。

因此,让我们从一个小例子开始,深入了解一下。👇

例子。解析一个数字

double如果我们想把一个含有数值的String ,我们可以这样写代码。

final value = double.parse('123.45'); // ok

然而,如果我们试着运行这个,会发生什么?

final value = double.parse('not-a-number'); // throws exception

这段代码在运行时抛出一个异常。

但是,parse 函数的签名并没有告诉我们这些,我们必须阅读文档才能知道。

/// Parse [source] as a double literal and return its value.
/// Throws a [FormatException] if the [source] string is not valid
static double parse(String source);

如果我们想处理FormatException ,我们可以使用一个try/catch 块。

try {
  final value = double.parse('not-a-number');
  // handle success
} on FormatException catch (e) {
  // handle error
  print(e);
}

但在大型代码库中,很难弄清楚哪些函数可能会抛出,哪些不会。

理想情况下,我们希望我们的函数的签名 明确指出它们可以返回一个错误。

Either类型

fpdart包中的 Eitherfpdart包中的类型让我们可以指定失败成功类型作为函数签名的一部分。

import 'package:fpdart/fpdart.dart';

Either<FormatException, double> parseNumber(String value) {
  try {
    return Either.right(double.parse(value));
  } on FormatException catch (e) {
    return Either.left(e);
  }
}

Either.leftError.right 里面的值必须与我们定义的类型注释相匹配(本例中为FormatExceptiondouble )。总是Either.left 来表示错误,用Either.right 来表示返回值(成功)。

比较Either和Result

乍一看,Eithermultiple_result包中的Result 类型非常相似。

Result<FormatException, double> parseNumber(String value) {
  try {
    return Success(double.parse(value));
  } on FormatException catch (e) {
    return Error(e);
  }
}

事实上,只有基本语法有变化。

  • Either.rightSuccess
  • Either.leftError

但是Either 有一个更广泛和强大的API。让我们来看看。👇

either和tryCatch工厂构造函数

如果我们想简化我们的实现,我们可以使用 tryCatch工厂构造函数

Either<FormatException, double> parseNumber(String value) {
  return Either.tryCatch(
    () => double.parse(value),
    (e, _) => e as FormatException,
  );
}

这就是tryCatch 的实现方式。

/// Try to execute `run`. If no error occurs, then return [Right].
/// Otherwise return [Left] containing the result of `onError`.
factory Either.tryCatch(
    R Function() run, L Function(Object o, StackTrace s) onError) {
  try {
    return Either.of(run());
  } catch (e, s) {
    return Either.left(onError(e, s));
  }
}

注意,onError 回调给出了错误堆栈跟踪作为参数,并且错误类型是Object

但由于我们知道double.parse 函数只能抛出一个FormatException ,所以在我们的parseNumber 函数中把e 铸成一个FormatException 是安全的。

有趣的例子。功能性Fizz-Buzz 😎

现在我们已经看到了如何使用Either<E, S> 来表示我们代码中的错误,让我们再向前走一步,用它来操作一些数据。

例如,这里有一个Dart函数,实现了流行的fizz buzz算法

String fizzBuzz(double value) {
  if (value % 3 == 0 && value % 5 == 0) {
    // multiple of 3 and 5
    return 'fizz buzz';
  } else if (value % 3 == 0) {
    // multiple of 3
    return 'fizz';
  } else if (value % 5 == 0) {
    // multiple of 5
    return 'buzz';
  } else {
    // all other numbers
    return value.toString();
  }
}

并假设我们想实现一个定义如下的函数。

Iterable<Either<FormatException, String>>
    parseFizzBuzz(List<String> strings);

这个函数应该通过以下方式返回一个Iterable 的值列表。

  • parseNumber 函数解析输入列表中的每个字符串
  • 如果数字有效,对其应用fizzBuzz 函数,并将结果返回Either.right
  • 如果数字无效,则返回Either.left ,并附带一个FormatException

Fizz-buzz。强迫性风格

作为第一次尝试,我们可以用一些命令式代码来实现。

Iterable<Either<FormatException, String>>
    parseFizzBuzz(List<String> strings) {
  // all types are declared explicitly for clarity,
  // but we could have used `final` instead:
  List<Either<FormatException, String>> results = [];
  for (String string in strings) {
    // first, parse the input string
    Either<FormatException, double> parsed = parseNumber(string);
    // then, use map to convert valid numbers using [fizzBuzz]
    Either<FormatException, String> result =
        parsed.map((value) => fizzBuzz(value));
    // add the value
    results.add(result);
  }
  return results;
}

最有趣的一行是这样的。

Either<FormatException, String> result =
        parsed.map((value) => fizzBuzz(value));

这里我们使用Either.map 操作符,通过调用fizzBuzz 函数,将所有的值(类型为double )转换为结果(类型为String )。

如果我们检查一下文档,我们会发现Either.map EitherRight 的情况下才会应用,这意味着任何错误都会被自动带入

/// If the [Either] is [Right], then change its value from type `R` to
/// type `C` using function `f`.
Either<L, C> map<C>(C Function(R a) f);

Fizz-buzz。功能性风格

你猜怎么着?

我们可以用一种完全的函数式实现parseFizzBuzz

Iterable<Either<FormatException, String>>
    parseFizzBuzz(List<String> strings) => strings.map(
      (string) => parseNumber(string).map(
        (value) => fizzBuzz(value) // no tear-off
      )
    );

我们还可以用拆分的方式使它更短。

Iterable<Either<FormatException, String>>
    parseFizzBuzz(List<String> strings) => strings.map(
      (string) => parseNumber(string).map(
        fizzBuzz // with tear-off
      )
    );

无论哪种方式(双关语),我们都通过对字符串列表的映射取代了强制性的for循环。

最重要的是,当我们使用Either 来映射数值时,错误不会出现

运行Fizz-Buzz的实现

为了证明这一点,我们来运行这段代码。

void main() {
  final values = [
    '1', '2', '3', '4', '5',
    '6', '7', '8', '9', '10',
    '11', '12', '13', '14', '15',
    'not-a-number', 'invalid',
  ];
  // parse, then join by inserting a newline
  final results = parseFizzBuzz(values).join('\n');
  print(results);
}

这就是输出结果。

Right(1.0)
Right(2.0)
Right(fizz)
Right(4.0)
Right(buzz)
Right(fizz)
Right(7.0)
Right(8.0)
Right(fizz)
Right(buzz)
Right(11.0)
Right(fizz)
Right(13.0)
Right(14.0)
Right(fizz buzz)
Left(FormatException: Invalid double not-a-number)
Left(FormatException: Invalid double invalid)

很酷,不是吗?😎

我们还能用Either做什么?

想直接从左边或右边的值创建一个Either 的实例吗?

/// Create an instance of [Right]
final right = Either<String, int>.of(10);

/// Create an instance of [Left]
final left = Either<String, int>.left('none');

如何映射值呢?

/// Map the right value to a [String]
final mapRight = right.map((a) => '$a');

/// Map the left value to a [int]
final mapLeft = right.mapLeft((a) => a.length);

想来点模式匹配吗?

/// Pattern matching
final number = parseNumber('invalid');
/// unwrap error/success with the fold method
number.fold( // same as number.match()
  (exception) => print(exception),
  (value) => print(value),
);

我不会在这里列出所有你能用Either 的事情。请看一下文档,了解一下你可以用来创建和操作数值的40多种方法。

fpdart包有完整的文档,你不需要任何先前的函数式编程经验就可以开始使用它。查看官方文档以了解更多细节。

总结

通过学习Eitherfpdart 包,我们现在已经涉足了函数式编程的世界。

以下是关键点。

  • 当我们想在函数/方法的签名中明确声明错误时,我们可以使用Either<L, R> ,作为抛出异常的替代方法(导致代码的自我记录)。
  • 如果我们使用Either ,并且不处理错误,我们的代码就不会被编译。这比在开发过程中在运行时发现错误要好得多(或者更糟糕的是,在生产中🥶)。
  • Either 脚本有一个广泛的API,使我们很容易用有用的函数操作符来操作我们的数据,如 , , ,以及其他许多操作符。map mapLeft fold

我所介绍的例子是故意学术性的,使之更容易涵盖基础知识。

但在下一篇文章中,我们将学习如何在真实世界的Flutter应用程序中处理错误和异步代码,使用 TaskEither.

而我们将看到,当我们使用具有表现层应用层领域层和数据层的参考应用架构时,如何充分利用fpdart和TaskEither ,这很适合于大中型应用。👍