Skip to content

Latest commit

 

History

History
688 lines (494 loc) · 28.8 KB

ch14-闭包.md

File metadata and controls

688 lines (494 loc) · 28.8 KB

闭包(Closures)

保存环境! 今天创建一个闭包! --Cormac Flanagan

原文

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

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

integers.sort();

因此,一个悲哀的事实是,当我们想要对一些数据排序时,它几乎不可能是整数的向量.我们通常有某种记录,内置的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记录并提取 键(key) ,即我们要用其对数据进行排序的字段.(它返回一个负数,因为sort按递增顺序排列数字,我们希望降序:首先是人口最多的城市.) sort_by_key方法将此键函数作为参数.

这很好用,但是将辅助函数写为 闭包(closure) ,一个匿名函数表达式更简洁:

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

这里的闭包是|city| -city.population.它接受一个参数city并返回-city.population.Rust根据闭包的使用方式推断出参数类型和返回类型.

接受闭包的标准库功能的其他示例包括:

  • Iterator方法,如mapfilter,用于处理顺序数据.我们将在第15章介绍这些方法.

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

  • 某些有条件地需要计算默认值的方法,例如HashMap条目的or_insert_with方法.此方法在HashMap中获取或创建一个条目,并且在默认值计算成本高时使用它.默认值作为闭包传入,只有在必须创建新条目时才会调用该闭包.

当然,匿名函数现在无处不在,即使是最初没有它们的Java,C#,Python和C++等语言.从现在开始,我们假设你之前已经看过匿名函数,并专注于使Rust的闭包有点不同的原因.在本章中,你将学习如何与标准库方法一起使用闭包,闭包如何"捕获(capture)"其作用域内的变量,如何编写自己的将闭包作为参数的函数和的方法,以及如何存储闭包为以后用作回调.我们还将解释Rust闭包的工作原理以及它们为什么比你预期的更快.

捕获变量(Capturing Variables)

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

// Sort by any of several different statistics.
fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
    cities.sort_by_key(|city| -city.get_statistic(stat));
}

这里的闭包使用stat,它由封闭函数sort_by_statistic拥有.我们说闭包"捕获(capture)"stat.这是闭包的经典功能之一,所以Rust自然会支持它;但在Rust中,此功能附带一个字符串.

在大多数带闭包的语言中,垃圾收集起着重要作用.例如,请考虑以下JavaScript代码:

// Start an animation that rearranges the rows in a table of cities.
function startSortingAnimation(cities, stat) {
    // Helper function that we'll use to sort the table.
    // Note that this function refers to stat.
    function keyfn(city) {
        return city.get_statistic(stat);
    }

    if (pendingSort)
        pendingSort.cancel();

    // Now kick off an animation, passing keyfn to it.
    // The sorting algorithm will call keyfn later.
    pendingSort = new SortingAnimation(cities, keyfn);
}

闭包keyfn存储在新的SortingAnimation对象中.它意味着在startSortingAnimation返回后调用.现在,通常当函数返回时,其所有变量和参数都超出作用域并被丢弃.但是在这里,JavaScript引擎必须以某种方式保持stat,因为闭包使用它.大多数JavaScript引擎通过在堆中分配stat并让垃圾回收器稍后回收来实现这一点.

Rust没有垃圾回收.这将如何运作?要回答这个问题,我们将看两个例子.

借用的闭包(Closures That Borrow)

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

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因此拒绝这个程序:

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`

事实上,这里存在两个问题,因为cities共享也不安全.很简单,thread::spawn创建的新线程不能指望在函数结束时销毁citystat之前完成它的工作.

这两个问题的解决方案是相同的:告诉Rust将citiesstat 移动(move) 到使用它们的闭包中,而不是借用对它们的引用.

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
    })
}

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

第一个闭包key_fn取得stat的所有权.然后第二个闭包获得了citykey_fn的所有权.

因此,Rust提供了两种方法来使闭包从封闭的作用域中获取数据:移动和借用.真的没有什么可说的了;闭包遵循我们在第4章和第5章已经介绍过的关于移动和借用的相同规则.有几个例子:

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

  • 不可复制类型(如Vec<City>)的值,确实被移动:上面的代码通过move闭包将cities转移到新线程.在创建闭包后,Rust不允许我们按名称访问cities.

  • 实际上,此代码不需要在闭包移动它之后使用cities.但是,如果我们需要,解决方法就很简单:我们可以告诉Rust克隆cities并将副本存储在另一个变量中.闭包只会窃取其中一个副本--无论它指的是哪一个.

我们通过接受Rust严格的规则来获得重要的东西:线程安全.正是因为向量被移动而不是跨线程共享,我们知道在新线程修改它时,旧线程不会释放向量.

函数和闭包类型(Function and Closure Types)

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

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

此函数接受一个参数(&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的函数值完全相同.

在这之后,闭包与函数的类型 不(not) 同可能会让人感到惊讶:

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的类型签名.而不是函数体.新版本是泛型的.它接受任何类型Ftest_fn,只要F实现特殊traitFn(&City) - > bool.此trait由所有函数和闭包自动实现,这些函数和闭包接受单个&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| city.monster_attack_risk> limit有自己的类型,不是fn类型.

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

由于每个闭包都有自己的类型,所以使用闭包的代码通常需要是泛型的,比如count_selected_cities.每次拼出通用类型有点笨拙,但要了解这个设计的优点,请继续阅读.

闭包性能(Closure Performance)

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

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

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

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

图14-1. 内存中闭包的布局.

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

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

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

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

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

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

闭包和安全性(Closures and Safety)

接下来的几页完成了关于闭合与Rust安全系统如何相互作用的解释.正如我们在本章前面所述,故事的大部分内容都很简单,当一个闭包被创建时,它会移动或借用捕获的变量.但其中一些后果并不明显.特别是,我们将讨论闭包删除或修改捕获的值时会发生什么.

杀死的闭包(Closures That Kill)

我们已经看到借用值的闭包和窃取值的闭包;它们一路坏下去只是时间问题.当然, 杀死(kill) 并不是正确的术语.在Rust中,我们 删除(drop) 值,最直接的方法是调用drop():

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

f被调用时,my_str被删除.如果我们调用它两次会发生什么?

f();
f();

让我们考虑一下.第一次调用f时,它会删除my_str,这意味着存储字符串的内存被释放,返回给系统.我们第二次调用f,同样的事情发生了.这是一个 双重释放(double free) ,是c++编程中引发未定义行为的经典错误.

在Rust中删除一个String两次也是一个同样糟糕的主意.幸运的是,Rust不会这么容易被愚弄:

f();  // ok
f();  // error: use of moved value

Rust知道这个闭包不能被调用两次.

一个只能被调用一次的闭包可能看起来像是一件非常不寻常的事情.但是我们在本书中一直在谈论所有权和生命周期.值被用掉(即,被移动)的概念是Rust的核心概念之一.它对闭包的工作原理与对其他所有东西的工作原理相同.

FnOnce(FnOnce)

让我们再试一次,让Rust删除String两次.这一次,我们将使用这个泛型函数:

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

这个泛型函数可以被传递任何实现traitFn()的闭包:即不带参数并且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如何处理"杀死的闭包(closures that kill)".它们可能会被语言完全禁止,但清理闭包有时是有用的.因此,Rust限制了它们的使用.删除值的闭包(如f)不允许有Fn.从字面上看,它们根本就不是Fn.它们实现了一个不那么强大的trait,FnOnce,可以调用一次的闭包的trait.

第一次调用FnOnce闭包时, 闭包本身就被用掉了(the closure itself is used up) .就好像FnFnOnce这两个trait,定义如下:

// 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()视为上面显示的两个trait方法之一的简写.对于Fn闭包,closure()扩展为closure.call().此方法通过引用获取self,因此不会移动闭包.但是如果闭包只能安全地调用一次,那么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(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.由于incrFnMut而不是Fn,因此该代码无法编译.但是,有一个简单的解决方案.为了理解这个修复,让我们退后一步,总结一下你对三类Rust闭包的了解.

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

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

  • 如果调用者拥有闭包,FnOnce是可以调用一次的闭包族.

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

图14-2. 三个封闭类别的维恩图.

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

现在我们已经组织了我们所知道的,很明显,为了接受最广泛的闭包,我们的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)

许多库使用 回调(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_formpost_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 `[closure@closures_bad_router.rs:40:27: 40:50]`
              found type `[closure@closures_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页的"使用哪一个(Which to Use)",我们展示了一个具有相同问题的Salad类型.

struct Salad<V: Vegetable> {
    veggies: Vec<V>
}

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

type BoxedCallback = Box<Fn(&Request) -> Response>;

struct BasicRouter {
    routes: HashMap<String, BoxedCallback>
}

每个box可以包含不同类型的闭包,因此单个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的两个限制:一个特定的Fntrait,以及'static生命周期.Rust使我们添加这个'static限制.没有它,对Box::new(callback)的调用将是一个错误,因为如果它包含借用的对即将超出作用域的变量的引用,则存储闭包是不安全的.)

最后,我们的简单路由器已准备好处理传入的请求:

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

高效地使用闭包(Using Closures Effectively)

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

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

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

图14-3. 模型-视图-控制器(Model-View-Controller)设计模式.

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

Rust的激进赌注是存在良好的替代设计.有时你可以通过让每个闭包接收它所需的引用作为参数来解决闭包所有权和生命周期的问题.有时你可以为系统中的每个事物分配一个数字,并传递数字而不是引用.或者,你可以实现MVC的众多变体之一,其中对象并非都具有彼此的引用.或者在具有单向数据流的非MVC系统之后建模你的工具包,如Facebook的Flux架构,如图14-4所示.

14-4. Flux架构,一个MVC的替代.

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

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