Programming Rust Fast, Safe Systems Development(译) 闭包(第十四章)

   2023-02-09 学习力0
核心提示:Save the environment! Create a closure today!— Cormac Flanagan对整数向量进行排序很容易integers.sort();因此,一个可悲的事实是,当我们想要一些数据排序时,它几乎不是整数的向量。我们通常有某种记录,内置排序方法通常不起作用:struct City { name:

Save the environment! Create a closure today!
— Cormac Flanagan

对整数向量进行排序很容易

integers.sort();

因此,一个可悲的事实是,当我们想要一些数据排序时,它几乎不是整数的向量。我们通常有某种记录,内置排序方法通常不起作用:

struct City {
 name: String,
 population: i64,
 country: String,
 ...
}
fn sort_cities(cities: &mut Vec<City>) {
 cities.sort(); // error: how do you want them sorted?
}

Rust抱怨City没有实现std :: cmp :: Ord。我们需要指定排序顺序,如下所示:

/// Helper function for sorting cities by population.
fn city_population_descending(city: &City) -> i64 {
 -city.population
}
fn sort_cities(cities: &mut Vec<City>) {
 cities.sort_by_key(city_population_descending); // ok
}

辅助函数city_population_descending接受City记录并提取**,即我们想要对数据进行排序的字段。 (它返回一个负数,因为排序按递增顺序排列数字,我们希望降序:首先是人口最多的城市。)sort_by_key方法将此键函数作为参数。

这工作正常,但将辅助函数编写为闭包,匿名函数表达式更简洁:

关闭这里是|城市| -city.population。它需要一个参数city并返回-city.population。 Rust根据闭包的使用方式推断出参数类型和返回类型。接受闭包的标准库功能的其他示例包括:

fn sort_cities(cities: &mut Vec<City>) {
 cities.sort_by_key(|city| -city.population);
}

•迭代器方法,如map和filter,用于处理顺序数据。我们将在第15章介绍这些方法。

•线程API,如thread :: spawn,它启动一个新的系统线程。 Concur-rency就是将工作转移到其他线程,而闭包则方便地代表工作单元。我们将在第19章介绍这些功能。

•有条件地需要计算默认值的一些方法,例如HashMap条目的or_insert_with方法。此方法在HashMap中获取或创建一个条目,并在默认值计算成本高时使用。默认值作为闭包传入,只有在必须创建新条目时才会调用该闭包。当然,匿名函数现在无处不在,即使是最初没有它们的Java,C#,Python和C ++等语言。从现在开始,我们假设您之前已经看过匿名函数,并专注于使Rust的闭包有点不同的原因。在本章中,您将学习如何使用标准库方法的闭包,闭包如何“捕获”其范围内的变量,如何编写自己的函数和将闭包作为参数的方法,以及如何为以后存储闭包用作回调。我们还将解释Rust闭包的工作原理以及它们为什么比您预期的更快。

捕获变量

闭包可以使用属于封闭函数的数据。例如:

这里的闭包使用stat,它由封闭函数sort_by_statistic拥有。我们说关闭“捕获”统计数据。这是封闭的经典特征之一,所以Rust自然会支持它;但在Rust中,此功能附带一个字符串。在大多数带闭包的语言中,垃圾收集起着重要作用。例如,请考虑以下JavaScript代码:

闭包keyfn存储在新的SortingAnimation对象中。它意味着在startSortingAnimation返回后调用。现在,通常在函数返回时,其所有变量和参数都超出范围并被丢弃。但是在这里,JavaScript引擎必须以某种方式保持统计,因为闭包使用它。大多数JavaScript引擎通过在堆中分配stat并让垃圾回收器稍后回收来实现这一点。 Rust没有垃圾回收。这将如何运作?要回答这个问题,我们将看两个例子。

借用的闭包

首先,让我们重复本节的开头示例:

fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
 cities.sort_by_key(|city| -city.get_statistic(stat));
}

在这种情况下,当Rust创建闭包时,它会自动借用stat的引用。这是有道理的:关闭是指stat,所以它必须有一个参考。

其余的很简单。封闭符合我们在第5章中描述的有关借用和生命周期的规则。特别是,由于封闭包含对stat的引用,Rust不会让它超过stat。由于闭包仅在排序期间使用,因此这个例子很好。

简而言之,Rust通过使用生命周期而不是垃圾收集来确保安全性。 Rust的方式更快:即使快速GC分配也比在堆栈上存储stat要慢,就像Rust在这种情况下那样。

Closures That Steal

第二个例子比较棘手:

use std::thread;
fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
 -> thread::JoinHandle<Vec<City>>
{
 let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };
 thread::spawn(|| {
 cities.sort_by_key(key_fn);
 cities
 })
}

这有点像我们的JavaScript示例所做的那样:thread :: spawn接受一个闭包并在一个新的系统线程中调用它。请注意||是闭包的空参数列表。

新线程与调用者并行运行。当闭包返回时,新线程退出。 (闭包的返回值作为JoinHandle值发送回调用线程。我们将在第19章中介绍它。)

同样,闭包key_fn包含对stat的引用。但这一次,Rust无法保证引用安全使用。因此Rust拒绝这个程序:

事实上,这里存在两个问题,因为城市也是不安全的。很简单,thread :: spawn创建的新线程不能指望在函数结束时销毁city和stat之前完成它的工作。

error[E0373]: closure may outlive the current function, but it borrows `stat`,
 which is owned by the current function
 --> closures_sort_thread.rs:33:18
 |
33 | let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };
 | ^^^^^^^^^^^^^^^^^^^^ ^^^^
 | | `stat` is borrowed here
 | may outlive borrowed value `stat`

这两个问题的解决方案是相同的:告诉Rust将cities和stat移动到使用它们的闭包中,而不是借用对它们的引用。

我们唯一改变的是在两个闭包中的每一个之前添加move关键字。 move关键字告诉Rust,闭包不会借用它使用的变量:它会窃取它们。

第一个闭包key_fn取得stat的所有权。然后第二个闭包获得了city和key_fn的所有权。因此,Rust提供了两种方法来使闭包从封闭范围中获取数据:移动和借用。真的没有什么可说的了;闭包遵循我们在第4章和第5章已经介绍过的关于移动和借用的相同规则。有几个例子:

fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
 -> thread::JoinHandle<Vec<City>>
{
 let key_fn = move |city: &City| -> i64 { -city.get_statistic(stat) };
 thread::spawn(move || {
 cities.sort_by_key(key_fn);
 cities
 })
}

•正如语言中的其他任何地方一样,如果闭包将移动可复制类型的值(如i32),则会复制该值。因此,如果Statistic恰好是可复制类型,即使在创建使用它的移动闭包之后,我们也可以继续使用stat。

•不可复制类型的值,例如Vec ,确实被移动:上面的代码通过移动闭包将城市转移到新线程。在创建闭包后,Rust不允许我们按名称访问城市。

•实际上,此代码不需要在闭包移动它之后使用城市。但是,如果我们这样做了,解决方法就很容易了:我们可以告诉Rust克隆城市并将副本存储在另一个变量中。关闭只会窃取其中一个副本 - 无论它指的是哪一个。我们通过接受Rust严格的规则来获得重要的东西:线程安全。正是因为向量被移动而不是跨线程共享,我们知道旧线程在新线程修改它时不会释放向量。

函数和闭包类型

在本章中,我们已经看到函数和闭包被用作值。当然,这意味着他们有类型。例如:

fn city_population_descending(city: &City) -> i64 {
 city.population
}

此函数接受一个参数(a&City)并返回i64。它的类型为fn(&City) - > i64。

您可以使用其他值的函数执行所有相同的操作。您可以将它们存储在变量中。您可以使用所有常用的Rust语法来计算函数值:

let my_key_fn: fn(&City) -> i64 =
 if user.prefs.by_population {
 city_population_descending
 } else {
 city_monster_attack_risk_descending
 };
cities.sort_by_key(my_key_fn);

结构可能具有函数类型的字段。像Vec这样的通用类型可以存储大量函数,只要它们共享相同的fn类型即可。函数值很小:fn值是函数机器代码的内存地址,就像C ++中的函数指针一样。

函数可以将另一个函数作为参数。例如:

/// Given a list of cities and a test function,
/// return how many cities pass the test.
fn count_selected_cities(cities: &Vec<City>,
 test_fn: fn(&City) -> bool) -> usize
{
 let mut count = 0;
 for city in cities {
 if test_fn(city) {
 count += 1;
 }
 }
 count
}
/// An example of a test function. Note that the type of
/// this function is `fn(&City) -> bool`, the same as
/// the `test_fn` argument to `count_selected_cities`.
fn has_monster_attacks(city: &City) -> bool {
 city.monster_attack_risk > 0.0
}
// How many cities are at risk for monster attack?
let n = count_selected_cities(&my_cities, has_monster_attacks);

如果你熟悉C / C ++中的函数指针,你会发现Rust的函数值完全相同。在这之后,闭包与函数的类型不同可能会让人感到惊讶:

let limit = preferences.acceptable_monster_risk();
let n = count_selected_cities(
 &my_cities,
 |city| city.monster_attack_risk > limit); // error: type mismatch

第二个参数导致类型错误。要支持闭包,我们必须更改此函数的类型签名。它需要看起来像这样:

fn count_selected_cities<F>(cities: &Vec<City>, test_fn: F) -> usize
 where F: Fn(&City) -> bool
{
 let mut count = 0;
 for city in cities {
 if test_fn(city) {
 count += 1;
 }
 }
 count
}

我们只更改了count_selected_cities的类型签名,而不是正文。新版本是通用的。只要F实现特殊特征Fn(&City) - > bool,它就需要任何类型F的test_fn。此特征由所有函数和闭包自动实现,这些函数和闭包将单个&City作为参数并返回布尔值

fn(&City) -> bool // fn type (functions only)
Fn(&City) -> bool // Fn trait (both functions and closures)

这种特殊语法内置于该语言中。 - >和返回类型是可选的;如果省略,则返回类型为()。
新版本的count_selected_cities接受函数或闭包:

count_selected_cities(
 &my_cities,
 has_monster_attacks); // ok
count_selected_cities(
 &my_cities,
 |city| city.monster_attack_risk > limit); // also ok

为什么我们的第一次尝试不起作用?好吧,一个闭包可以调用,但它不是一个fn。关闭|城市| city.monster_attack_risk> limit有自己的类型,不是fn类型。

实际上,您编写的每个闭包都有自己的类型,因为闭包可能包含数据:从封闭的范围借用或窃取的值。这可以是任何数量的变量,在任何类型的组合中。因此,每个闭包都有一个由编译器创建的ad hoc类型,大小足以容纳该数据。没有两个闭包具有完全相同的类型。但每个闭包都实现了Fn特征;我们的例子中的闭包实现了Fn(&City) - > i64。

由于每个闭包都有自己的类型,因此使用闭包的代码通常需要是通用的,比如count_selected_cities。每次拼出通用类型有点笨拙,但要看到这个设计的优点,请继续阅读。

闭包性能

Rust的闭包设计得很快:比函数指针更快,速度足以让你甚至可以使用它们
炙手可热,性能敏感的代码。
如果您熟悉C ++ lambdas,您会发现Rust闭包同样快速而紧凑,但更安全。

在大多数语言中,闭包在堆中分配,动态调度和垃圾收集。因此,创建它们,调用它们并收集它们每个都需要额外的CPU时间。更糟糕的是,闭包倾向于排除内联,这是编译器用来消除函数调用开销并实现大量其他优化的关键技术。总而言之,在这些语言中,闭包很慢,因此值得手动将它们从紧密的内循环中移除。

防锈罩没有这些性能缺陷。他们不是垃圾收集。与Rust中的其他所有内容一样,除非将它们放在Box,Vec或其他容器中,否则它们不会在堆上分配。由于每个闭包都有一个不同的类型,每当Rust编译器知道你正在调用的闭包的类型时,它就可以内联该特定闭包的代码。这样就可以在紧密的循环中使用闭包,而Rust程序经常会这样做,这将在第15章中看到。

图14-1显示了Rust闭包在内存中的布局。在图的顶部,我们展示了我们的闭包将引用的几个局部变量:字符串食物和简单的枚举天气,其数值恰好是27。

Programming Rust Fast, Safe Systems Development(译) 闭包(第十四章)

闭包(a)使用两个变量。显然我们正在寻找同时拥有炸玉米饼和龙卷风的城市。在内存中,这个闭包看起来像一个小结构,包含对它使用的变量的引用。

请注意,它不包含指向其代码的指针!这不是必需的:只要Rust知道闭包的类型,它就知道在调用它时要运行的代码。

闭包(b)完全相同,除了它是一个移动闭包,因此它包含值而不是引用。

闭包(c)不使用其环境中的任何变量。结构是空的,所以这个闭包根本不占用任何内存。

如图所示,这些闭合不占用太多空间。但实际上并不总是需要那几个字节。通常,编译器可以内联对闭包的所有调用,然后甚至图中所示的小结构也被优化掉了。

在“回调”(第316页)中,我们将展示如何在堆中分配闭包并使用特征对象动态调用它们。这有点慢,但它仍然与任何其他特征对象方法一样快。

闭包和安全

接下来的几页完成了关于封闭如何与Rust的安全系统相互作用的解释。正如我们在本章前面所述,大多数故事只是在创建闭包时,它会移动或借用捕获的变量。但其中一些后果并不明显。特别是,我们将讨论当闭包丢弃或修改捕获的值时会发生什么。

杀死的闭包

我们已经看到关闭借用偷窃它们的价值和封闭;他们一路走坏只是时间问题。当然,杀人并不是真正合适的术语。在Rust,我们放弃了价值观。最直接的方法是调用drop():

let my_str = "hello".to_string();
let f = || drop(my_str);

当调用f时,my_str被删除。
那么如果我们两次打电话会怎么样?

f();
f();

让我们思考一下吧。第一次调用f时,它会丢弃my_str,这意味着存储字符串的内存被释放,返回给系统。我们第二次打电话给f,同样的事情发生了。这是一个双重***,是C ++编程中的一个经典错误,它触发了未定义的行为。

在Rust中删除两次字符串也是一个同样糟糕的主意。幸运的是,Rust不容易被愚弄:

f();
f();

Rust知道这个闭包不能被调用两次。一个只能被调用一次的闭包可能看起来像是一件非常不寻常的事情。但是我们在本书中一直在谈论所有权和生命周期。价值被用尽(即被移动)的想法是Rust的核心概念之一。它与闭包一样,与其他一切一样。

FnOnce

让我们再次尝试将Rust删除两次。这一次,我们将使用这个通用函数:

fn call_twice<F>(closure: F) where F: Fn() {
 closure();
 closure();
}

这个泛型函数可以传递任何实现特征Fn()的闭包:即不带参数的闭包和return()。 (与函数一样,如果是(),则可以省略返回类型; Fn()是Fn() - >()的简写。)

现在如果我们将不安全的闭包传递给这个泛型函数会发生什么?

let my_str = "hello".to_string();
let f = || drop(my_str);
call_twice(f);

同样,闭包将在调用时丢弃my_str。召唤两次将是双倍免费。但同样,Rust并没有被愚弄:

error[E0525]: expected a closure that implements the `Fn` trait, but
 this closure only implements `FnOnce`
 --> closures_twice.rs:12:13
 |
12 | let f = || drop(my_str);
 | ^^^^^^^^^^^^^^^
 |
note: the requirement to implement `Fn` derives from here
 --> closures_twice.rs:13:5
 |
13 | call_twice(f);
 | ^^^^^^^^^^

这个错误消息告诉我们更多关于Rust如何处理“杀死的闭包。”他们本可以完全禁止语言,但清理闭包有时很有用。因此,Rust限制了它们的使用。丢弃值的闭包(如f)不允许有Fn。从字面上看,它们根本就没有Fn。它们实现了一个不那么强大的特性,即FnOnce,可以调用一次的闭包特征。

第一次调用FnOnce闭包时,闭包本身就用完了。好像两个特征Fn和FnOnce被定义如下:

// Pseudocode for `Fn` and `FnOnce` traits with no arguments.
trait Fn() -> R {
 fn call(&self) -> R;
}
trait FnOnce() -> R {
 fn call_once(self) -> R;
}

就像a + b这样的算术表达式是方法调用的简写,Add :: add(a,b),Rust将closure()视为上面显示的两个特征方法之一的简写。对于Fn闭包,closure()扩展为closure.call()。此方法通过引用获取自身,因此不会移动闭包。但是如果闭包只能安全地调用一次,那么closure()会扩展为closure.call_once()。该方法采用self取值,因此闭包用完了。

当然,我们通过使用drop()故意在这里惹麻烦。在实践中,你大多是偶然会遇到这种情况。它不会经常发生,但是在很长一段时间内你会编写一些无意中使用了值的闭包代码:

let dict = produce_glossary();
let debug_dump_dict = || {

for (key, value) in dict { // oops!
 println!("{:?} - {:?}", key, value);
 }
};

然后,当您不止一次调用debug_dump_dict()时,您将收到如下错误消息:

error[E0382]: use of moved value: `debug_dump_dict`
 --> closures_debug_dump_dict.rs:18:5
 |
17 | debug_dump_dict();
 | --------------- value moved here
18 | debug_dump_dict();
 | ^^^^^^^^^^^^^^^ value used here after move
 |
 = help: closure was moved because it only implements `FnOnce`

为了调试这个,我们必须弄清楚为什么闭包是一个FnOnce。哪个值在这里用完了?我们唯一指的是dict。啊,有错误:我们通过直接迭代它来使用dict。我们应该循环遍历&dict而不是普通的dict,以通过引用访问值:

let debug_dump_dict = || {
 for (key, value) in &dict { // does not use up dict
 println!("{:?} - {:?}", key, value);
 }
};

这解决了错误;该函数现在是一个Fn,可以被调用任意次。

FnMut

还有一种闭包,包含可变数据或mut引用。

Rust认为非mut值可以安全地跨线程共享。但是共享包含mut数据的非mut闭包是不安全的:从多个线程调用这样的闭包可能导致各种竞争条件,因为多个线程试图同时读取和写入相同的数据。

因此,Rust还有一个类别的闭包,FnMut,即写入的闭包类别。 FnMut闭包是通过mut引用调用的,就好像它们是这样定义的:

// Pseudocode for `Fn`, `FnMut`, and `FnOnce` traits.
trait Fn() -> R {
 fn call(&self) -> R;
}
trait FnMut() -> R {
 fn call_mut(&mut self) -> R;
}
trait FnOnce() -> R {
 fn call_once(self) -> R;
}

任何需要对值进行mut访问但不删除任何值的闭包都是FnMut闭包。例如:

let mut i = 0;
let incr = || {
 i += 1; // incr borrows a mut reference to i
 println!("Ding! i is now: {}", i);
};
call_twice(incr);

我们编写call_twice的方式,它需要一个Fn。由于incr是FnMut而不是Fn,因此该代码无法编译。但是,有一个简单的解决方案。为了理解这个修复,让我们退后一步,总结一下你对三类Rust闭包的了解。

•Fn是闭包和函数系列,您可以无限制地多次调用。这个最高类别还包括所有fn功能。

•FnMut是闭包族,如果闭包本身被声明为mut,则可以多次调用。

•FnOnce是一系列闭包,如果调用者拥有闭包,则可以调用一次。

每个Fn都满足FnMut的要求,每个FnMut都满足FnOnce的要求。如图14-2所示,它们不是三个独立的类别。

Programming Rust Fast, Safe Systems Development(译) 闭包(第十四章)

相反,Fn()是FnMut()的子特征,它是FnOnce()的子特征。这使得Fn成为最独特和最强大的类别。 FnMut和FnOnce是更广泛的类别,包括具有使用限制的闭包。

现在我们已经组织了我们所知道的,很明显,为了接受最广泛的闭包,我们的call_twice函数真的应该接受所有FnMut闭包,如下所示:

fn call_twice<F>(mut closure: F) where F: FnMut() {
 closure();
 closure();
}

第一行的界限是F:Fn(),现在是F:FnMut()。通过这个更改,我们仍然接受所有Fn闭包,我们还可以在变量数据的闭包上使用call_twice:

let mut i = 0;
call_twice(|| i += 1); // ok!
assert_eq!(i, 2);

Callbacks

许多库使用回调作为其API的一部分:用户提供的函数,供以后调用库。实际上,您已经看过本书中已有的一些API。回到第2章,我们使用Iron框架编写一个简单的Web服务器。它看起来像这样:

fn main() {
 let mut router = Router::new();
 router.get("/", get_form, "root");
 router.post("/gcd", post_gcd, "gcd");
 println!("Serving on http://localhost:3000...");
 Iron::new(router).http("localhost:3000").unwrap();
}

路由器的目的是将来自Internet的传入请求路由到处理该特定类型请求的Rust代码。在这个例子中,get_form和post_gcd是我们使用fn关键字在程序中其他地方声明的一些函数的名称。但我们可以改为通过闭包,如下所示:

let mut router = Router::new();
router.get("/", |_: &mut Request| {
 Ok(get_form_response())
}, "root");
router.post("/gcd", |request: &mut Request| {
 let numbers = get_numbers(request)?;
 Ok(get_gcd_response(numbers))
}, "gcd");

这是因为Iron被编写为接受任何线程安全的Fn作为参数。

我们怎样才能在自己的程序中做到这一点?让我们尝试从头开始编写我们自己的非常简单的路由器,而不使用Iron的任何代码。我们可以从声明一些类型来表示HTTP请求和响应开始:

struct Request {
 method: String,
 url: String,
 headers: HashMap<String, String>,
 body: Vec<u8>
}
struct Response {
 code: u32,
 headers: HashMap<String, String>,
 body: Vec<u8>
}

现在,路由器的工作就是存储一个将URL映射到回调的表,以便可以根据需要调用正确的回调。 (为简单起见,我们只允许用户创建与单个精确URL匹配的路由。)

struct BasicRouter<C> where C: Fn(&Request) -> Response {
 routes: HashMap<String, C>
}
impl<C> BasicRouter<C> where C: Fn(&Request) -> Response {
 /// Create an empty router.
 fn new() -> BasicRouter<C> {
 BasicRouter { routes: HashMap::new() }
 }
 /// Add a route to the router.
 fn add_route(&mut self, url: &str, callback: C) {
 self.routes.insert(url.to_string(), callback);
 }
}

不幸的是,我们犯了一个错误。你注意到了吗?

只要我们只添加一个路由,这个路由器就可以正常工作:

let mut router = BasicRouter::new();
router.add_route("/", |_| get_form_response());

这很多编译和运行。不幸的是,如果我们添加另一条路线

router.add_route("/gcd", |req| get_gcd_response(req));

然后我们得到错误:

error[E0308]: mismatched types
 --> closures_bad_router.rs:41:30
 |
41 | router.add_route("/gcd", |req| get_gcd_response(req));

 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | expected closure, found a different closure
 |
 = note: expected type `[[email protected]_bad_router.rs:40:27: 40:50]`
 found type `[[email protected]_bad_router.rs:41:30: 41:57]`
note: no two closures, even if identical, have the same type
help: consider boxing your closure and/or using it as a trait object

我们的错误在于我们如何定义BasicRouter类型:

struct BasicRouter<C> where C: Fn(&Request) -> Response {
 routes: HashMap<String, C>
}

我们无意中声明每个BasicRouter都有一个回调类型C,而HashMap中的所有回调都属于那种类型。回到第243页的“使用中”,我们展示了一个具有相同问题的Salad类型。

owed a Salad type that had the same problem.
struct Salad<V: Vegetable> {
 veggies: Vec<V>
}

这里的解决方案与沙拉相同:因为我们想要支持各种类型,我们需要使用盒子和特征对象。

type BoxedCallback = Box<Fn(&Request) -> Response>;
struct BasicRouter {
 routes: HashMap<String, BoxedCallback>
}

每个框都可以包含不同类型的闭包,因此单个HashMap可以包含各种回调。请注意,类型参数C已消失。

这需要对方法进行一些调整:

impl BasicRouter {
 // Create an empty router.
 fn new() -> BasicRouter {
 BasicRouter { routes: HashMap::new() }
 }
 // Add a route to the router.
 fn add_route<C>(&mut self, url: &str, callback: C)
 where C: Fn(&Request) -> Response + 'static
 {
 self.routes.insert(url.to_string(), Box::new(callback));
 }
}

(注意add_route类型签名中C的两个边界:一个特定的Fn特征,以及’静态生命周期.Rust使我们添加这个’静态边界。没有它,对Box :: new(回调)的调用将是一个错误,因为如果它包含对即将超出范围的变量的借用引用,则存储闭包是不安全的。)最后,我们的简单路由器已准备好处理传入的请求:

impl BasicRouter {
 fn handle_request(&self, request: &Request) -> Response {
 match self.routes.get(&request.url) {
 None => not_found_response(),
 Some(callback) => callback(request)
 }
 }
}

有效地使用闭包

正如我们所看到的,Rust的闭包与大多数其他语言的闭包不同。最大的区别在于,在使用GC的语言中,您可以在闭包中使用局部变量,而无需考虑生命周期或所有权。没有GC,情况就不同了。 Java,C#和JavaScript中常见的一些设计模式在Rust中不起作用而不进行更改。

例如,采用模型 - 视图控制器设计模式(简称MVC),如图14-3所示。对于用户界面的每个元素,MVC框架创建三个对象:表示UI元素状态的模型,负责其外观的视图,以及处理用户交互的控制器。多年来已经实现了MVC的无数变体,但总体思路是三个对象以某种方式分配UI职责。

这是问题所在。通常,每个对象都直接或通过回调引用其他对象中的一个或两个,如图14-3所示。只要其中一个对象发生任何事情,它就会通知其他对象,所以一切都会立即更新。关于哪个对象“拥有”其他对象的问题永远不会出现。

Programming Rust Fast, Safe Systems Development(译) 闭包(第十四章)

如果不进行某些更改,则无法在Rust中实现此模式。必须明确所有权,并且必须消除参考周期。模型和控制器不能直接相互引用。

Rust的激进赌注是存在良好的替代设计。有时你可以通过让每个闭包接收它作为参数所需的引用来解决闭包所有权和生命周期的问题。有时您可以为系统中的每个事物分配一个数字并传递数字而不是引用。或者你可以实施

MVC的众多变体之一,其中对象并不都具有彼此的引用。或者在具有单向数据流的非MVC系统之后建模您的工具包,如Facebook的Flux架构,如图14-4所示。

Programming Rust Fast, Safe Systems Development(译) 闭包(第十四章)

简而言之,如果你试图使用Rust闭合来制造“物体之海”,你将会遇到困难。但也有其他选择。在这种情况下,软件工程作为一门学科似乎已经倾向于替代方案,因为它们更简单。

在下一章中,我们将讨论关闭真正发挥作用的主题。我们将编写一种代码,充分利用Rust闭包的简洁,快速和高效,并且编写起来很有趣,易于阅读,而且非常实用。接下来:Rust迭代器。

 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • bloom-server 基于 rust 编写的 rest api cache 中间件
    bloom-server 基于 rust 编写的 rest api cache
    bloom-server 基于 rust 编写的 rest api cache 中间件,他位于lb 与api worker 之间,使用redis 作为缓存内容存储, 我们需要做的就是配置proxy,同时他使用基于share 的概念,进行cache 的分布存储,包含了请求端口(proxy,访问数据) 以及cache 控制端口(
    03-08
  • #新闻拍一拍# Oracle 调研如何避免让 Java 开发者投奔 Rust 和 Kotlin | Linux 中国
    #新闻拍一拍# Oracle 调研如何避免让 Java 开发
     导读:• 英特尔对迟迟不被 Linux 主线接受的 SGX Enclave 进行了第 38 次修订 • ARM 支持开源的 Panfrost Gallium3D 驱动本文字数:977,阅读时长大约:1分钟作者:硬核老王Oracle 调研如何避免让 Java 开发者投奔 Rust 和 KotlinOracle 委托分析公司 Omd
    03-08
  • Linux系统下Rust快速安装:国内镜像加速
    Linux系统下Rust快速安装:国内镜像加速
    官方网址和方法Install Rust - Rust Programming Language然而速度慢得让人难以置信。利用国内镜像进行windows的Linux子系统的Rust安装。rust 使用国内镜像,快速安装方法参考:RUST安装慢怎么办,使用镜像方式安装_网络_为中华之崛起而编程-CSDN博客我的操作
    03-08
  • Rust到底值不值得学--Rust对比、特色和理念
    前言其实我一直弄不明白一点,那就是计算机技术的发展,是让这个世界变得简单了,还是变得更复杂了。当然这只是一个玩笑,可别把这个问题当真。然而对于IT从业者来说,这可不是一个玩笑。几乎每一次的技术发展,都让这个生态变得更为复杂。“英年早秃”已经成
    03-08
  • 超33000行新代码,为Linux内核添加Rust支持的补丁已准备就绪
    超33000行新代码,为Linux内核添加Rust支持的补
    https://mp.weixin.qq.com/s/oKw9aBJSdmRoO6-rbLAkNw7 月 4 日,一套修订后的补丁被提交至 Linux 内核的邮件列表中,该补丁为在 Linux 内核中以 Rust 作为辅助编程语言提供了支持,借助 Rust 可以提高 Linux 内核和内存的安全。整套补丁包含 17 个子项,不光
    03-08
  • 【译】Rust 的 Result 类型入门
    【译】Rust 的 Result 类型入门
    A Primer on Rust’s Result Type 译文原文链接:https://medium.com/@JoeKreydt/a-primer-on-rusts-result-type-66363cf18e6a原文作者:Joe Kreydt译文出处:https://github.com/suhanyujie/article-transfer-rs译者:suhanyujietips:水平有限,翻译不当之
    03-08
  • Rust实战系列-基本语法
    Rust实战系列-基本语法
    主要介绍 Rust 的语法、基本类型和数据结构,通过实现一个简单版 grep 命令行工具,来理解 Rust 独有的特性。本文是《Rust in action》学习总结系列的第二部分,更多内容请看已发布文章:一、Rust实战系列-Rust介绍“主要介绍 Rust 的语法、基本类型和数据结
    03-08
  • 全栈程序员的新玩具Rust(三)板条箱
    上次用到了stdout,这次我们来写一个更复杂一点的游戏rust的标准库叫做std,默认就会引入。这次我们要用到一个随机数函数,而随机数比较尴尬的一点是这玩意不在标准库中,我们要额外依赖一个库。很多编程方案都有自己的模块化库系统,rust也不例外,不过rust
    02-10
  • 全栈程序员的新玩具Rust(六)第一个WASM程序
    全栈程序员的新玩具Rust(六)第一个WASM程序
    先上代码https://gitee.com/lightsever/rust_study/tree/master/wasm_hello01webassembly就不用再赘述了,耳朵里面快磨出茧子来了。rustwasm是火狐自家的玩具,让我们来继续做实验,让rust飞起来吧。环境安装安装好rust环境之后仍然需要 一个 wasm 工具包carg
    02-10
  • 【Rust】标准库-Result rust数据库
    环境Rust 1.56.1VSCode 1.61.2概念参考:https://doc.rust-lang.org/stable/rust-by-example/std/result.html示例main.rsmod checked {#[derive(Debug)]pub enum MathError {DivisionByZero,NonPositiveLogarithm,NegativeSquareRoot,}pub type MathResult =
    02-09
点击排行