Rust for Rubyists

Matthias Endler

面向 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 中没有 nilNull 这一概念,因此不需要 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 并没有在切片自身上提供 samplechoose 方法。这是一个设计决策:保持语言核心精简,以便未来能够不断发展语言。

这并不意味着你现在不能拥有更好的实现。例如,你可以定义一个 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)
end

Rust 也是如此。注意这里没有分号。

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 指正。

原文由 Matthias Endler 发布

本文章由 openai/gpt-5.6-luna 进行翻译