这篇文章是由一个刚刚进入Rust世界的JavaScript开发者写的。要想从这篇文章中获得价值,并不需要有JS背景。但如果你是一个从Web开发者转为Rust的人,你会对我的观点更有同感。
在过去的十年里,各种语言似乎都在遵循一个共同的趋势:减少面向对象的模式,而采用函数式编程(FP)。
网络开发者可能已经看到FP模式出现在现代前端框架中,如React使用他们的钩子模型。但转到Rust,你会看到当你围绕它构建整个编程语言时,FP会有多么强大--而对try...catch 和null 的方法只是冰山一角!
让我们来探讨一下抛出和捕获异常的缺陷,Rust的Result 枚举和模式匹配可以为你做什么,以及这如何延伸到处理null 值。
什么是Rust?
对于新的Rustaceans(yee-claw! )来说,Rust是一种低级别的类型化语言,对所有程序员都足够友好。和C语言一样,Rust直接编译为机器码(原始二进制),所以Rust程序的编译和运行速度极快。他们也非常重视交流和文档,有一个繁荣的贡献者社区和大量的优秀教程。
为什么你不应该在Rust中使用try...catch 块?
如果你像我一样,你已经习惯了在你的JavaScript代码库中做catch 的舞蹈。就拿这个场景来说吧。
// Scenario 1: catching a dangerous database call
app.get('/user', async function (req, res) {
try {
const user = await dangerousDatabaseCall(req.userId)
res.send(user)
} catch(e) {
// couldn't find the user! Time to tell the client
// it was a bad request
res.status(400)
}
})
这是一个典型的服务器模式。调用数据库,工作时向用户发送响应,不工作时发送一些错误代码,如400 。
但我们怎么会知道在这里使用try...catch ?好吧,有了像dangerousDatabaseCall 这样的名字和一些关于数据库的直觉,我们知道当出错时它可能会抛出一个异常。
现在我们来看看这个场景。
// Scenario 2: forgetting to catch a dangerous file reading
app.get('/applySepiaFilter', async function (req, res) {
const image = await readFile("/assets/" + req.pathToImageAsset)
const imageWithSepiaFilter = applySepiaFilter(image)
res.send(imageWithSepiaFilter)
})
当然,这是个臆造的例子。但是,简而言之,每当我们调用applySepiaFilter ,我们想从我们服务器的/assets 中读取请求的文件并应用那个颜色过滤器。
但是,等一下,我们忘了在这周围包上一个try...catch!所以,每当我们请求一些不存在的文件时,我们会收到一个讨厌的内部服务器错误。这最好是一个400 "坏请求 "状态。
现在你可能在想,"好吧,但我不会忘记那个try...catch..."这是可以理解的!一些Node.js程序员可能会立即意识到,readFile 抛出异常。
但是,当我们要么使用没有记录异常的库函数,要么使用我们自己的抽象(如果你像我一样潦倒,也许根本就没有记录 ),这就变得更加难以预测。
总结一下JS异常处理的一些核心问题。
- 如果一个函数曾经
throws,调用者必须记得处理这个异常。不,你的花哨的ESlint设置在这里帮不了你!这可能会导致我所说的try...catch焦虑:把所有东西都包裹在一个try块中,以防出错。或者更糟的是,你会完全忘记catch异常,导致像我们未捕获的readFile调用那样的令人震惊的失败。 - 该异常的类型可能是不可预测的。这对于
try...catch围绕多个故障点的包装器来说可能是个问题。例如,如果我们的readFile爆炸应该返回一个状态代码,而applySepiaFilter失败应该返回另一个状态代码呢?我们有多个try...catch块吗?如果我们需要查看异常的name字段(在浏览器端可能是不可靠的)怎么办?
让我们来看看Rust的Result 枚举。
使用Rust的Result 枚举和模式匹配
这里有一个惊喜。Rust并没有一个try...catch 块。见鬼,他们甚至没有我们所熟知的 "异常"。
了解Rust中的match
如果你已经了解了模式匹配,可以跳到下一节。
在探讨这一点之前,让我们先了解一下Rust中模式匹配的概念。这里有一个场景。
一个饥饿的顾客从我们的韩国街头食品菜单中要了一个meal ,我们想根据他们选择的orderNumber ,为他们提供不同的meal 。
在JavaScript中,你可能会接触到一系列像这样的条件语句。
let meal = null
switch(orderNumber) {
case 1:
meal = "Bulgogi"
break
case 2:
meal = "Bibimbap"
break
default:
meal = "Kimchi Jjigae"
break
}
return meal
这足够可读,但它有一个明显的缺陷(除了使用一个丑陋的switch 语句)。我们的meal 需要以null 开始*,并且需要在我们的switch 的情况下使用let 进行重新赋值。如果switch 能像这样真正返回*一个值就好了...
// Note: this is not real JavaScript!
const meal = switch(orderNumber) {
case 1: "Bulgogi"
case 2: "Bibimbap"
default: "Kimchi Jjigae"
}
你猜怎么着?Rust可以让你做到这一点。
let meal = match order_number {
1 => "Bulgogi"
2 => "Bibimbap"
_ => "Kimchi Jjigae"
}
神圣的语法,蝙蝠侠!
这就是Rust表达式驱动设计的魅力所在。在这种情况下,match 被认为是一个表达式,可以。
- 即时执行一些逻辑(将我们的订单号与一个膳食字符串相匹配)
- 在最后返回该值(可分配给
meal)。
条件式也可以是表达式。在JavaScript开发人员可能会伸手去拿三元组的地方。
const meal = orderNumber === 1 ? "Bulgogi" : "Something else"
Rust只是让你写一个if 语句。
let meal = if order_number == 1 { "Bulgogi" } else { "Something else" }
是的,你可以跳过return 这个词。Rust表达式的最后一行总是返回值。
将match 应用于异常
好吧,那么这如何应用于异常呢?
这次让我们先跳到例子中。假设我们正在编写先前的那个applySepiaFilter 端点。为了清楚起见,我将使用同样的req 和res 帮助器。
use std::fs::read_to_string;
// first, read the requested file to a string
match read_to_string("/assets/" + req.path_to_image_asset) {
// if the image came back ay-OK...
Ok(raw_image) => {
// apply the filter to that raw_image...
let sepia_image = apply_sepia_filter(raw_image)
// and send the result.
res.send(sepia_image)
}
// otherwise, return a status of 400
Err(_) => res.status(400)
}
嗯,那些Ok 和Err 包装器是怎么回事?让我们比较一下Rust的read_to_string 和Node的readFile 的返回类型。
- 在Node中,
readFile返回一个你可以立即使用的string。 - 在Rust中,
read_to_string并不返回一个字符串,而是返回一个包裹着字符串的Result类型。完整的返回类型是这样的:Result<std::string::String, std::io::Error>。换句话说,这个函数返回的结果要么是一个字符串*,要么是*一个I/O错误(就是你在读写文件时得到的那种错误)。
这意味着我们不能使用read_to_string ,直到我们 "解开 "它(即,弄清楚它是一个字符串还是一个错误)。下面是如果我们试图把一个Result ,好像它已经是一个字符串会发生什么。
let image = read_to_string("/assets/" + req.path_to_image_asset)
// ex. try to get the length of our image string
let length = image.len()
// 🚨 Error: no method named `len` found for enum
// `std::result::Result<std::string::String, std::io::Error>`
第一种更危险的解包方式是自己调用unwrap() 函数。
let raw_image = read_to_string("/assets/" + req.path_to_image_asset).unwrap()
但这并不十分安全!如果你尝试调用
unwrap ,而read_to_string 返回某种错误,整个程序就会因所谓的恐慌而崩溃。记住,Rust没有try...catch ,所以这可能是一个非常糟糕的问题。
第二种更安全的方法是通过模式匹配来解包我们的结果。让我们重新审视一下前面的那个块,并做一些澄清的评论。
match read_to_string("/assets/" + req.path_to_image_asset) {
// check whether our result is "Ok," a subtype of Result that
// contains a value of type "string"
Result::Ok(raw_image) => {
// here, we can access the string inside that wrapper!
// this means we're safe to pass that raw_image to our filter fn...
let sepia_image = apply_sepia_filter(raw_image)
// and send the result
res.send(sepia_image)
}
// otherwise, check whether our result is an "Err," another subtype
// that wraps an I/O error.
Result::Err(_) => res.status(400)
}
注意我们在最后的Err 里面使用了一个下划线_ 。这是Rust-y的说法,即 "我们不关心这个值",因为我们总是返回一个400 的状态。如果我们确实关心这个错误对象,我们可以像抓取raw_image 一样抓取它,甚至可以根据异常类型做另一层的模式匹配。
为什么模式匹配是处理异常的更安全的方式?
那么,为什么要处理所有这些不方便的 "包装器",如Result ?乍一看似乎很烦人,但它们在设计上确实很烦人,因为。
- 你不得不在错误出现的时候处理它们,用模式匹配为成功和失败的情况定义行为。而且,在你真正想得到你的结果并继续前进的时候,你可以选择使用不安全行为。
unwrap() - 你总是知道一个函数何时会根据它的返回类型出错,这意味着不再有
try...catch,也不再有古怪的类型检查。
如何在Rust中使用null
这是Rust可以解决的JS中另一个棘手的角落。对于函数的返回值,当我们有某种特殊或默认的情况需要考虑时,我们会使用null (或undefined )。当某些转换失败时,我们可能会抛出一个null ,一个对象或数组元素不存在,等等。
但在这些情况下,null只是一个无名的异常!我们可能会接触到 的返回值。我们可能会在JS中使用null 返回值,因为throwing一个异常感觉不安全或者很极端。我们想要的是一种引发异常的方法,但不需要错误类型或错误信息的麻烦,希望调用者使用try...catch 。
Rust也认识到了这一点。所以,Rust将null 从语言中驱逐出去,并引入了 Option 包装器。
假设我们有一个get_waiter_comment 函数,根据顾客留下的小费给他一个赞美。我们可能会使用这样的东西。
fn get_waiter_comment(tip_percentage: u32) -> Option<String> {
if tip_percentage <= 20 {
None
} else {
Some("That's one generous tip!".to_string())
}
}
当我们不想要赞美的时候,我们可以返回一个空字符串"" 。但是通过使用Option (很像使用null ),我们更容易弄清是否有赞美之词要显示。看看这个match 语句的可读性如何。
match get_waiter_comment(tip) {
Some(comment) => tell_customer(comment)
None => walk_away_from_table()
}
什么时候使用Option 与。Result
Result 和Option 之间的界限是模糊的。我们可以很容易地将前面的例子重构成这样。
fn get_waiter_comment(tip_percentage: u32) -> Result<String> {
if tip_percentage <= 20 {
Err(SOME_ERROR_TYPE)
} else {
Result("That's one generous tip!".to_string())
}
}
...
match get_waiter_comment(tip) {
Ok(comment) => tell_customer(comment)
Err(_) => walk_away_from_table()
}
唯一的区别是,我们需要为我们的Err 案例提供一些错误对象,这可能是一个麻烦,因为被调用者需要拿出一个错误type / 信息来使用,而调用者需要检查这个错误信息是否真的值得阅读和匹配。
但在这里,很明显,错误信息不会给我们的get_waiter_comment 函数带来多少价值。这就是为什么我通常会达到Option ,直到我有一个很好的理由来切换到Result 类型。不过,决定权还是在你手里!
总结(没有双关语的意思)
Rust对exception 和null 的处理方法是类型安全的一个巨大胜利。掌握了表达式、模式匹配和封装类型的概念,我希望你已经准备好在你的应用程序中安全地处理错误了!
The postDitching try...catch and null checks with Rustappeared first onLogRocket Blog.