写 Golang 的朋友应该经常接触这种代码:
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
在这种模式中,错误是值,调用方必须显式处理。
当然这里不是为了辩经这种错误模型与 try/catch 孰优孰劣。例如在读取文件时,文件不存在导致 IOException 是意料之外的问题,throw 出去就行,而使用 Apach POI 尝试读取 PDF 失败就可以视为 Error,调用方或许应当转而尝试 PDFBox。
许多框架在接口侧设计了 Result<T> 类型,也是起到类似的作用,然而一些框架在控制流中也使用这种类型,就场景语义来说是不合适的。
因此专门为框架设计了 R<T, E> 类型,提供一种函数式、安全、可组合的方式来表示操作结果,将把“可能失败”这件事提升为类型系统的一部分。
R<T, E> 是一个泛型结果容器,封装了成功值 (T value)和失败值 (E error);
构造方法、常用方法与示例
// 成功
R<Integer, String> r1 = R.ok(10);
// 失败
R<Integer, String> r2 = R.err("失败信息");
R<Integer, String> r3 = R.err(new RuntimeException("Oops"));
如果需要从一个可能抛出异常的操作中获取结果,使用 R#of 方法将任何可能抛出异常的操作转化为安全的 R 对象。
R<Integer, Throwable> r3 = R.of(() -> 10 / 2);
// ok(5)
R<Integer, Throwable> r4 = R.of(() -> 10 / 0);
// err(ArithmeticException)
判断状态:
if (r1.isOk()) {
logger.info(r1.value());
} else {
logger.error("操作失败", r1.error());
}
map & flatMap
如果控制流中需要进行一系列可能会失败的操作,通常的写法可能会变成 if 金字塔:
if (ok1) {
if (ok2) {
if (ok3) {
}
}
}
函数式编程的朋友们这时也许会掏出 Monad,而 R 提供了 map 和 flatMap。
R<Integer, String> doubled = r1.map(x -> x * 2);
只对成功值生效,失败值保持不变,支持链式调用,用于组合复杂操作
R<Integer, String> r1 = R.ok(5)
.map(x -> x + 10)
.map(x -> x * 2);
// r1 = ok(30)
R<Integer, String> r2 = R.err("Oops")
.map(x -> x + 10)
.map(x -> x * 2);
// r2 = err("Oops")
需要链式操作多个可能失败的函数?使用 flatMap
R<Integer, String> r1 = R.ok(11)
.flatMap(x -> x > 10 ? R.ok(x - 5) : R.err("小于阈值"))
.flatMap(x -> R.ok(x * 2));
// r1 = ok(12)
R<Integer, String> r2 = R.ok(9)
.flatMap(x -> x > 10 ? R.ok(x - 5) : R.err("小于阈值"))
.flatMap(x -> R.ok(x * 2));
// r2 = err("小于阈值")
失败值处理
提供默认值,或对错误信息进行包装
R<Integer, String> recovered = R.err("失败").recover(e -> 0);
// ok(0)
R<Integer, Integer> mappedError = R.err("Oops").mapError(String::length);
// err(4)
// 虽然不知道 length of error message 有什么用就是了