面向 Ruby 开发者的 Rust
最近我读到了一篇讲解 Ruby 惯用写法的精彩文章。我算不上优秀的 Ruby 开发者,但我发现,其中许多模式在 Rust 中也很常见。下面将对两种语言中的惯用代码进行并列比较。
Ruby 代码示例均来自原文。
Map 与高阶函数
第一个例子是使用 map 对容器中的元素进行非常基础的迭代。
user_ids = users.map { |user| user.id }map 这一概念在 Rust 中同样十分常见。与 Ruby 相比,这里需要稍微明确一些:如果 users 是一个由 User 对象组成的向量,我们首先需要从它创建一个迭代器:
let user_ids = users.iter().map(|user| user.id);你可能会觉得这有些冗长,但这个额外的抽象让我们能够表达一个重要概念:迭代器究竟会不会取得向量的所有权?
- 使用
iter(),你会得到向量的“只读视图”。迭代结束后,向量不会改变。 - 使用
into_iter(),你会取得向量的所有权。迭代结束后,向量就不存在了。用 Rust 的术语来说,它已经被 移动(moved)。 - 你可以在这里进一步了解
iter()与into_iter()的区别。
上面的 Ruby 代码可以这样简化:
user_ids = users.map(&:id)在 Ruby 中,高阶函数(例如 map)会接收代码块或 proc 作为参数,而该语言还为方法调用提供了便捷的简写形式——&:id 与 {|o| o.id()} 等价。
Rust 中也可以做类似的事情:
let id = |u: &User| u.id ;不过,这可能不是最符合 Rust 惯用写法的方式。更常见的是在这种情况下使用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 开始,现在也可以这样写:
["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 不接收谓词。你应该使用 filter:
let even_numbers = vec![1, 2, 3, 4, 5]
.iter()
.filter(|&element| element % 2 == 0);或者,将结果构造为一个向量:
// 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!是用于创建向量的宏。- 我没有使用
iter,而是使用了into_iter。这样,我就取得了向量中元素的所有权。使用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 这样的第三方 package(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。
假设你想把一个字符串向量转换为整数。
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。我们可以对Result调用ok(),将其转换为Option3。
返回值包含所有成功转换的字符串:
[1, 2, 3]filter_map 与 Ruby 中的 select 方法类似:
[1, 2, 3, 4, 5].select { |element| element.even? }随机数
下面是在 Ruby 中从数组获取随机数的方法:
[1, 2, 3].sample这非常简洁,也很符合 Ruby 的惯用写法!将它与 Rust 比较一下:
let mut rng = thread_rng();
rng.choose(&[1, 2, 3, 4, 5])要让这段代码正常工作,你需要 rand crate。点击代码片段即可查看可运行的示例。
它与 Ruby 有一些区别。具体来说,我们需要更明确地指定自己究竟想要什么随机数生成器。这里我们选择由系统设定种子、延迟初始化的线程本地随机数生成器。在这个例子中,我使用的是slice(切片),而不是向量。主要区别在于,切片大小固定,而向量大小不固定。
在标准库中,Rust 并没有在切片自身上提供 sample 或 choose 方法。这是一个设计决策:保持语言核心精简,以便未来能够不断发展语言。
这并不意味着你现在不能拥有更好的实现。例如,你可以定义一个 Choose trait(特征),并为 [T] 实现它。
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 中,你只能将元组解构为元组,例如不能将向量解构为元组。因此,下面这样可以工作:
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 指正。↩
随机一篇博客