写给 Ruby 开发者的 Rust
原文由 Matthias Endler 于 发布,订阅该博客
最近我读到了一篇关于地道 Ruby 写法的精彩文章。虽说我绝不是什么出色的 Ruby 开发者,但我发现其中很多模式在 Rust 中也十分常见。接下来将对两种语言的地道代码进行并列对比。
Ruby 代码示例均来自原文。
Map 与高阶函数
第一个例子是对容器中的元素进行基本的遍历,使用 map。
user_ids = users.map { |user| user.id }map 这一概念在 Rust 中也同样常见。相比 Ruby,我们需要写得更明确一些:如果 users 是一个存放 User 对象的 vector,我们首先需要从它创建一个迭代器:
let user_ids = users.iter().map(|user| user.id);你可能会觉得这样写有点啰嗦,但这种额外的抽象让我们能够表达一个重要的概念:迭代器究竟会不会取得 vector 的所有权?
- 使用
iter()时,你得到的是对 vector 的“只读视图”。迭代结束后,它保持不变。 - 使用
into_iter()时,你会取得 vector 的所有权。迭代结束后,vector 就不存在了。用 Rust 的术语来说,它已经被 moved(移动)了。 - 在这里阅读更多关于
iter()和into_iter()区别的介绍。
上面的 Ruby 代码还可以这样简化:
user_ids = users.map(&:id)在 Ruby 中,高阶函数(如 map)以块(block)或 proc 作为参数,语言本身为方法调用提供了一个便捷的简写——&:id 就等同于 {|o| o.id()}。
在 Rust 中也可以做类似的事情:
let id = |u: &User| u.id;
let user_ids = users.iter().map(id);不过,这可能不是最地道的写法。更常见的是在这种情况下使用通用函数调用语法(Universal Function Call Syntax):1
let user_ids = users.iter().map(User::id);在 Rust 中,高阶函数以 函数 作为参数。因此 users.iter().map(Users::id) 在很大程度上等同于 users.iter().map(|u| u.id())。2
另外,Rust 中的 map() 返回的是另一个迭代器,而不是集合。如果你想要得到集合,就得像后面会看到的那样,在其上调用collect()。
使用 Each 进行迭代
说到迭代,我在 Ruby 代码中经常看到这样的模式:
["Ruby", "Rust", "Python", "Cobol"].each do |lang|
puts "Hello #{lang}!"
end自 Rust 1.21 起,这在 Rust 中也可以实现了:
["Ruby", "Rust", "Python", "Cobol"]
.iter()
.for_each(|lang| println!("Hello {lang}!", lang = lang));不过,在 Rust 中更常见的写法是普通的 for 循环:
for lang in ["Ruby", "Rust", "Python", "Cobol"].iter() {
println!("Hello {lang}!", lang = lang);
}Select 与 filter
假设你想从一个集合中筛选出所有的偶数,在 Ruby 中可以这样写。
even_numbers = [1, 2, 3, 4, 5].map { |element| element if element.even? } # [ni, 2, nil, 4, nil]
even_numbers = even_numbers.compact # [2, 4]在这个例子中,调用 compact 之前,我们的 even_numbers 数组里包含了 nil 条目。而在 Rust 中,并没有 nil 或 Null 的概念。你不需要 compact。而且 map 也不接受谓词(predicate)。你应该为此使用 filter:
let even_numbers = vec![1, 2, 3, 4, 5]
.iter()
.filter(|&element| element % 2 == 0);或者,把结果构造成一个 vector
// Result: [2, 4]
let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
.into_iter()
.filter(|element| element % 2 == 0).collect();一些提示:
- 我在那里使用了类型标注
Vec<i64>,因为如果没有它,Rust 在调用collect时就不知道我想构建哪种集合。 vec!是用于创建 vector 的宏。- 我在这里用的是
into_iter而不是iter。这样我就能取得 vector 中元素的所有权。如果用iter(),得到的会是Vec<&i64>。
在 Rust 中,数字并没有 even 方法,但这并不妨碍我们自己定义一个!
let even = |x: &i64| x % 2 == 0;
let even_numbers = vec![1, 2, 3, 4, 5].into_iter().filter(even);在真实场景中,你可能会使用像 num 这样的第三方包(crate)来处理数值计算:
extern crate num;
use num::Integer;
fn main() {
let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
.into_iter()
.filter(|x| x.is_even()).collect();
}一般来说,在 Rust 中为标准库之外的功能使用 crate 是非常普遍的。之所以能被广泛接受,部分原因在于 cargo 是一个非常出色的包管理器。(也许是因为它正是由 Ruby 界大名鼎鼎的 Yehuda Katz 打造的。 😉)
如前所述,Rust 没有 nil。不过,操作失败的概念依然存在。用于表达这一概念的规范类型叫做 Result。
假设你想把一个字符串 vector 转换成整数。
let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
.into_iter()
.map(|i| i.parse::<u64>())
.collect();这样写看起来不错,但输出可能有点出乎意料。numbers 中同样会包含解析错误:
[Ok(1), Ok(2), Err(ParseIntError { kind: InvalidDigit }), Err(ParseIntError { kind: InvalidDigit }), Ok(3)]有时你只关心成功的操作。一个简单过滤掉错误的方法是使用 filter_map:
let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
.into_iter()
.filter_map(|i| i.parse::<u64>().ok())
.collect();我在这里改了两处:
- 原来用的是
map,现在改成了filter_map。 parse返回的是Result,但filter_map期望的是Option。我们可以通过在其上调用ok()来把Result转换成Option3。
返回值包含了所有成功转换的字符串:
[1, 2, 3]filter_map 类似于 Ruby 中的 select 方法:
[1, 2, 3, 4, 5].select { |element| element.even? }随机数
在 Ruby 中,从数组中获取一个随机数可以这样写:
[1, 2, 3].sample这非常简洁,也很地道!来对比一下 Rust 的写法:
let mut rng = thread_rng();
rng.choose(&[1, 2, 3, 4, 5])要让这段代码正常工作,你需要引入 rand crate。点击代码片段可查看可运行的示例。
与 Ruby 相比有一些不同。也就是说,我们需要更明确地指定究竟想要哪一种随机数生成器。我们选择了一个惰性初始化的线程局部随机数生成器,由系统提供种子。在这个例子中,我使用的是 切片(slice) 而不是 vector。主要区别在于切片的大小是固定的,而 vector 则不是。
在标准库范围内,Rust 并没有在切片本身上提供 sample 或 choose 方法。这是一个设计上的取舍:语言的核心被保持得尽可能小,以便未来能够持续演进。
这并不意味着你现在就无法拥有更优雅的实现。例如,你可以为 [T] 定义一个 Choose trait 并为其实现:
extern crate rand;
use rand::{thread_rng, Rng};
trait Choose<T> {
fn choose(&self) -> Option<&T>;
}
impl<T> Choose<T> for [T] {
fn choose(&self) -> Option<&T> {
let mut rng = thread_rng();
rng.choose(&self)
}
}这些样板代码可以放到一个 crate 中,供他人复用。这样,我们就能得到一个足以媲美 Ruby 优雅程度的解决方案。
[1, 2, 4, 8, 16, 32].choose()隐式返回与表达式
Ruby 方法会自动返回最后一条语句的结果。
def get_user_ids(users)
users.map(&:id)
endRust 也是如此。注意缺少的分号。
fn get_user_ids(users: &[User]) -> Vec<u64> {
users.iter().map(|user| user.id).collect()
}但在 Rust 中,这仅仅是个开始,因为一切皆为表达式。下面的代码块将字符串拆分为字符,移除其中的 h,并将结果作为 HashSet 返回。这个 HashSet 会被赋值给 x。
let x: HashSet<_> = {
// Get unique chars of a word {'h', 'e', 'l', 'o'}
let unique = "hello".chars();
// filter out the 'h'
unique.filter(|&char| char != 'h').collect()
};条件判断也是如此:
let x = if 1 > 0 { "absolutely!" } else { "no seriously" };由于 match 语句同样也是表达式,你也可以将其结果赋值给变量!
enum Unit {
Meter,
Yard,
Angstroem,
Lightyear,
}
let length_in_meters = match unit {
Unit::Meter => 1.0,
Unit::Yard => 0.91,
Unit::Angstroem => 0.0000000001,
Unit::Lightyear => 9.461e+15,
};多重赋值
在 Ruby 中,你可以一步将多个值赋给多个变量:
def values
[1, 2, 3]
end
one, two, three = values在 Rust 中,你只能将元组解构为元组,例如,无法将 vector 解构为元组。所以这样写是可以的:
let (one, two, three) = (1, 2, 3);但这样不行:
let (one, two, three) = [1, 2, 3];
// ^^^^^^^^^^^^^^^^^ expected array of 3 elements, found tuple这样也不行:
let (one, two, three) = [1, 2, 3].iter().collect();
// a collection of type `(_, _, _)` cannot be built from an iterator over elements of type `&{integer}`不过在 nightly 版本的 Rust 中,你现在可以这样写:
let [one, two, three] = [1, 2, 3];另一方面,通过解构你能做到的远不止多重赋值。你可以利用模式语法编写出优美、符合人体工学的代码。
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}这会打印
no,因为 if 条件适用于整个模式4 | 5 | 6,而不仅仅是最后一个值 6。
字符串插值
Ruby 拥有丰富的字符串插值支持。
programming_language = "Ruby"
"#{programming_language} is a beautiful programming language"可以这样翻译:
let programming_language = "Rust";
format!("{} is also a beautiful programming language", programming_language);命名参数也是可行的,尽管要少见得多:
println!("{language} is also a beautiful programming language", language="Rust");Rust 的 println!() 语法甚至比 Ruby 的还要丰富。如果你好奇还能做些什么,可以查阅文档。
就到这里!
Ruby 为许多常见的使用模式提供了语法糖,从而能够写出非常优雅的代码。底层编程和极致性能并不是该语言的主要目标。
如果你正好需要这些,Rust 可能是个不错的选择,因为它在提供细粒度的硬件控制的同时,也具备可比拟的人体工学。如果拿不准,Rust 更倾向于显式;它避免使用魔法。
是不是已经对地道的 Rust 心动了?可以看看 这个 GitHub 项目。非常期待你的贡献。
脚注
1. 感谢 Florian Gilcher 的提示。↩
2. 感谢 masklin 指出了多处不准确之处。↩
3. 在第一版中,我曾说 ok() 会把 Result 转换成 boolean,这是错误的。感谢 isaacg 的指正。↩
随机一篇博客
评论
登录后参与讨论