函数有自己的类型,可以像使用基础类型一样使用函数,包括将函数保存在变量中、保存在 vec 中、声明在结构体成员字段中。闭包函数也是函数,也有自己的类型定义。不过,函数实际上是指针类型,在 rust 所有权中属于借用的关系。
我们声明一个 Vec 对象集,并使用闭包函数做排序。接下来我们自定义一个 City 结构体类型,Vec 中的每个元素都是 City 类型,后面会根据结构体中的 population 属性做排序。
#[derive(Debug)]
struct City {
name: String,
population: i64,
country: String,
}
我们初始化 cities 变量,并向其中依次追加 3 个元素。vec 追加元素使用 push 函数,push 过程也会触发底层数组的扩容。rust 也提供了初始化指定 vec 容量的函数 with_capacity。
fn main() {
let mut cities: Vec<City> = vec![];
cities.push(City {
name: ("上海".to_string()),
population: (123i64),
country: ("China".to_string()),
});
cities.push(City {
name: ("北京".to_string()),
population: (12i64),
country: ("China".to_string()),
});
cities.push(City {
name: ("广州".to_string()),
population: (124i64),
country: ("China".to_string()),
});
sort_cities(&mut cities);
println!("{:?}", cities)
}
sort_cities 算是这篇文章的关键函数了,函数内部使用到了闭包。想要彻底明白下面的逻辑,首先要了解 sort_by_key 函数,这个函数的入参是一个闭包函数。
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(|city| -city.population);
}
声明中的 &mut self 表示方法能够可变地借用 self 实例,支持对它进行修改。闭包函数的类型是 FnMut(&T),返回的类型需要满足 Ord 约束。从声明中可以看出,闭包函数是有类型的,同时,返回的类型必须是可排序的。
代码部分,闭包函数的写法特别简单,两个竖线是闭包函数识别的标志。尽管闭包函数要求有返回值,但代码中并没有体现,都是 rust 自己默认推断实现的。
我们尝试对代码部分的闭包声明做写法上的调整,下面的闭包声明都是相同的。
// 加了代码块
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(|city| {-city.population});
}
// 加了入参声明
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(|city: &City| -city.population);
}
// 加了返回值声明
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(|city| -> i64 { -city.population });
}
闭包引用对象的所有权
在闭包函数体中使用外部的变量,会导致外部变量的所有权发生变更吗?如果是 go 语言,外部变量在闭包中是通过引用来访问的。具体到 rust 语言上,我们可以通过自定义闭包函数验证一番。
我们在原代码基础上做了简化,is_long_name 函数体中直接访问外部变量 sh,并将 sh 的名字重新赋值一个新的变量 name。编译器检测到 sh.name 发生了所有权转移,编译失败。
fn main() {
let sh = City {
name: ("上海".to_string()),
population: (123i64),
country: ("China".to_string()),
};
let is_long_name = || -> bool {
let name = sh.name;
if name.len() > 2 {
true
} else {
false
}
};
is_long_name();
println!("{:?}", sh);
}
在闭包声明的开头,大家也看到过 move 关键字。那么,这个 move 是用来解决什么问题的呢?
针对这个例子,我们专门在闭包内对 sh.name 重新做了赋值运算,变量所有权发生转移也符合认知。那么,我们现在直接使用 sh.name 进行长度判断,对应截图中的第 13 行代码。左右差异点只有:右边的闭包声明中追加生 move 关键字。
rust 中闭包取得数据有两种方式:转移和借用。move 会导致使用到的外部变量发生所有权转移。右边的例子中,闭包中的变量 sh.name 的所有权就发生了转移,导致编译失败。