在 Rust 的学习旅程中,我们经常会遇到需要对集合进行转换的情况。比如,你有一个数字列表,想要计算每个数的平方;或者有一组字符串,想把它们都转成大写形式。这种操作在函数式编程中被称为 "map" 操作,它是一种非常基础且强大的模式。
今天我们就来深入探讨 Exercism 上的 "accumulate" 练习,看看如何用 Rust 实现一个灵活而强大的 map 函数。
问题描述
在开始之前,让我们先看一下问题的核心。我们有一个函数签名:
rust
/// What should the type of _function be?
pub fn map(input: Vec<i32>, _function: ???) -> Vec<i32> {
unimplemented!("Transform input vector {:?} using passed function", input);
}
我们的任务是完成这个函数,使其能够接收一个向量和一个函数,并返回一个新的向量,其中包含原向量中每个元素经过函数处理后的结果。
简单实现
最简单的实现方式是这样的:
rust
pub fn map(input: Vec<i32>, function: fn(i32) -> i32) -> Vec<i32> {
let mut result = Vec::new();
for item in input {
result.push(function(item));
}
result
}
这确实能工作,但存在一些限制。它只能接受特定类型的函数(fn(i32) -> i32),无法使用闭包捕获环境变量,也无法处理不同类型的输入和输出。
泛型实现
为了使我们的 map 函数更加强大和灵活,我们需要使用泛型。来看一个更完整的实现:
rust
pub fn map<I, O, F>(input: Vec<I>, function: F) -> Vec<O>
where
F: Fn(I) -> O,
{
let mut result = Vec::new();
for item in input {
result.push(function(item));
}
result
}
在这个版本中:
I表示输入向量中元素的类型O表示输出向量中元素的类型F是转换函数的类型,它实现了Fn(I) -> Otrait
这样,我们的函数就能处理任意类型的输入和输出了!
使用迭代器优化
Rust 的迭代器系统已经提供了 map 方法,我们可以利用它让代码更加简洁:
rust
pub fn map<I, O, F>(input: Vec<I>, function: F) -> Vec<O>
where
F: Fn(I) -> O,
{
input.into_iter().map(function).collect()
}
这段代码简洁明了,它将输入向量转换为迭代器,应用映射函数,然后收集结果为新的向量。
测试案例解析
通过查看测试案例,我们可以看到这个函数的强大之处:
rust
fn square(x: i32) -> i32 {
x * x
}
#[test]
fn func_single() {
let input = vec![2];
let expected = vec![4];
assert_eq!(map(input, square), expected);
}
这里展示了如何传递一个普通函数作为参数。
rust
#[test]
fn closure() {
let input = vec![2, 3, 4, 5];
let expected = vec![4, 9, 16, 25];
assert_eq!(map(input, |x| x * x), expected);
}
这个例子演示了如何使用闭包作为参数。
rust
#[test]
fn change_in_type() {
let input: Vec<&str> = vec!["1", "2", "3"];
let expected: Vec<String> = vec!["1".into(), "2".into(), "3".into()];
assert_eq!(map(input, |s| s.to_string()), expected);
}
特别值得注意的是,我们的函数甚至可以在不同类型之间转换,比如从 &str 到 String。
更进一步:支持闭包状态
有些测试还检查了闭包是否能维护状态:
rust
#[test]
fn mutating_closure() {
let mut counter = 0;
let input = vec![-2, 3, 4, -5];
let expected = vec![2, 3, 4, 5];
let result = map(input, |x: i64| {
counter += 1;
x.abs()
});
assert_eq!(result, expected);
assert_eq!(counter, 4);
}
为了支持这种有状态的闭包,我们必须使用 Fn trait 而不是普通的函数指针。
结论
通过这个练习,我们学到了几个重要的 Rust 概念:
- 泛型 - 如何编写适用于多种类型的代码
- trait bounds - 如何限制泛型类型必须实现的功能
- 函数作为参数 - 如何将函数或闭包传递给其他函数
- 闭包 - Rust 中匿名函数的概念及其强大功能
这些概念构成了 Rust 函数式编程的基础,也是标准库中 Iterator::map 方法的工作原理。掌握了这些知识,你就能够在自己的 Rust 项目中写出更加优雅和灵活的代码了。