开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
- 文章标题:Rust 中的一些小技巧-1
- 原文链接:github.com/suhanyujie/…
- 原文作者:suhanyujie
- 文章来自:github.com/suhanyujie/…
- ps:水平有限,如有不当之处,欢迎指正
- 标签:Rust,let else
在之前的“打造自己的 Rust 项目脚手架”文章中,已经提到了一下 Rust 中的惯用法以及一些技巧,如通过 New Type 解决 Rust 中的孤儿原则、项目中自定义错误类型等。这篇文章也再次介绍两种 Rust 小技巧。
let else
在 11 月份发布的 Rust 1.65 版本中,Rust 提供了很多新特性。其中就有一个 let else。
根据官博介绍,Rust 在该版本之前,已经有了 let 语句,可用于结构体、元组、数组等类型的解构,但它无法为条件匹配(conditional matche),提取枚举变体提供帮助,直到 let else 的出现,我们可以使用可反驳模式匹配并绑定变量到附近的作用域中,这样当匹配不成功时,可以利用 break,return,panic! 等进行提前返回。
单看官博提供的例子,可能还无法立即体会到其便利之处:
fn get_count_item(s: &str) -> (u64, &str) {
let mut it = s.split(' ');
let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
panic!("Can't segment count item pair: '{s}'");
};
let Ok(count) = u64::from_str(count_str) else {
panic!("Can't parse integer: '{count_str}'");
};
(count, item)
}
assert_eq!(get_count_item("3 chairs"), (3, "chairs"));
还是这个示例,在 1.65 版本前,我们的写法可能如下所示:
// str: `1001 value1001`
fn get_count_item_v1(s: &str) -> Result<(u64, &str)> {
if let Some((s1, s2)) = s.split_once(' ') {
if let Ok(val) = s1.trim().parse::<u64>() {
Ok((val, s2))
} else {
Err(errors::Error::Generic("invalid item".to_string()))
}
} else {
Err(errors::Error::Generic("invalid item".to_string()))
}
}
将一段字符串通过空格 ' ' 切割,拿到对应的 key 和 value。代码有很多嵌套的 if let 语句,看起来是不太好看,因此我们改进一下:
fn get_count_item_v2(s: &str) -> Result<(u64, &str)> {
if let Some((s1, s2)) = s.split_once(' ') {
if let Ok(val) = s1.trim().parse::<u64>() {
return Ok((val, s2));
}
}
Err(errors::Error::Generic("invalid item".to_string()))
}
好一些了,我们直接将重复的 return err 直接写到函数的最后一行,省掉了一些重复代码,但嵌套的 if let 挥之不去。
但有了 let else 之后,我们可以这样写:
fn get_count_item_v3(s: &str) -> Result<(u64, &str)> {
let Some((s1, s2)) = s.split_once(' ') else {
return Err(errors::Error::Generic("invalid item".to_string()));
};
let Ok(val) = s1.trim().parse::<u64>() else {
return Err(errors::Error::Generic("invalid item".to_string()));
};
Ok((val, s2))
}
好了消除了 if let 的嵌套,整体看起来简洁很多。这中用法很像我们编写函数时,验证参数失败后,提前返回的用法,能让代码看起来更清晰、干净。
函数入参泛型化
有一个如下函数:
fn do_something(val: &str) {
let val = val.to_string();
// do something with String
println!("{val}");
}
#[test]
fn test_do_something1() {
let param = "hello world".to_string();
do_something(¶m);
}
函数接收一个 &str 类型,将其转换为拥有所有权的 String,然后做一些处理。执行测试用例:
running 1 test
hello world
test test::test_do_something1 ... ok
但是这样做,会出现 2 次字符串的内存分配:.to_string()。
于是,我们可能会想,直接将拥有所有权的 String 传入。但是这样,我们又需要修改函数签名了:
fn do_something(val: String) {
// do something with String
println!("{val}");
}
这样,甚至会影响其他调用该函数的地方。有没有两全其美的方式呢?
有的,我们可以将参数泛型化:
fn do_something_v2(val: impl Into<String>) {
let val = val.into();
// do something with String
println!("{val}");
}
Into 是标准库中自带的一个 trait,它定义了将一个类型转换为另一个类型的能力/约束。这里,我们将入参类型改成 impl Into<String>,这样入参就不会是特定的某个类型了。它可以是 &str,也可以是 String:
fn test_do_something1() {
let param = "hello world".to_string();
do_something_v2(param);
let param = "hello world";
do_something_v2(param);
}
running 1 test
hello world
hello world
test test::test_do_something1 ... ok
这样,在一定程度上,就完美了。
结语
Rust 本身是一门学习曲线陡峭的语言,但学起来却妙趣横生,尤其是有着活跃的社区,以及与其他编程语言不太一样的技巧,会让你感觉很棒。如果你也有类似的技巧和方法,欢迎一起交流学习。