Bean Li 2020-10-07T09:36:08+00:00 beanli.coder@gmail.com rayon join in Rust 2020-10-07T11:00:00+00:00 Bean Li http://bean-li.github.io/Rayon-in-Rust 前言

Rust提供了std::thread::spawn,可以通过Fork-Join模式,完成并发。这个基本概念对于熟悉C/C++编程的程序员,并没有什么太多的挑战。介绍Rust并发的资料中,很多资料都不约而同地提到了Rayon,这个library非常有趣,也非常强大,他是Niko Matakis这位大神完成的。对这个大神感兴趣的,可以读他的blog

extern creat rayon ;
use rayon::prelude::* ;

let (v1, v2) = rayon::join(fn1, fn2); 

giant_vector.par_iter().for_each(|value| {
    do_something_with_value(value) ; 
});

rayon这个库,提供了非常方便的接口,程序员很容易将串行的接口改造成成并行的:

extern crate rayon ;
use rayon::prelude::*;

//sequential
let total_price = stores.iter()
                        .map(|store| store.compute_price(&list))
                        .sum();
//parallel                       
let total_price = stores.par_iter()
                        .map(|store| store.compute_price(&list))
                        .sum();                      

我们看到,将迭代器iter()变成了par_iter()就完成了并行处理,对串行代码的改造非常方便,其中par_iter是parallel iterator的缩写。

Rayon’s goal is to make it easy to add parallelism to your sequential code

Rayon的核心原语 join

join是Rayon的核心原语,前面提到的par_iter是构建在join之上的。因此,理解rayon,需要先理解join。

join的使用非常简单:

join(|| do_something(), || do_something_else());

其函数原型如下:

pub fn join<A, B, RA, RB>(oper_a: A, oper_b: B) -> (RA, RB) 
where
    A: FnOnce() -> RA + Send,
    B: FnOnce() -> RB + Send,
    RA: Send,
    RB: Send, 

在rayon实现中,是否会有并发线程一起处理两个closure,取决于是否有空闲的CPU core,即join的两个闭包是one by one地串行执行还是并发之行,取决于实际情况。

rayon采用了一种叫做work-stealing的技术,简单地说, join(a,b),我们有两个任务要处理,a和b,而且这两个任务是并发安全的。我们并不知道,threadpool中是否有idle的thread可以处理, 处理方法如下:

  • 把b放入到pending work queue
  • 执行a
  • 如果存在一个thread 空闲,扫瞄pending queue,如果找到任务,则执行它
  • 执行a任务的线程执行a完毕后,检查b的情况:
    • 是否有其他线程执行了b,如果没有,该线程负责执行b
    • 如果存在其他线程执行了b,等待期间,可以偷其他任务完成。

其伪代码大概如下:

fn join<A,B>(oper_a: A, oper_b: B)
    where A: FnOnce() + Send,
          B: FnOnce() + Send,
{
    // Advertise `oper_b` to other threads as something
    // they might steal:
    let job = push_onto_local_queue(oper_b);
    
    // Execute `oper_a` ourselves:
    oper_a();
    
    // Check whether anybody stole `oper_b`:
    if pop_from_local_queue(oper_b) {
        // Not stolen, do it ourselves.
        oper_b();
    } else {
        // Stolen, wait for them to finish. In the
        // meantime, try to steal from others:
        while not_yet_complete(job) {
            steal_from_others();
        }
        result_b = job.result();
    }
}

rayon join 示例

let mut v = vec![5, 1, 8, 22, 0, 44];
quick_sort(&mut v);
assert_eq!(v, vec![0, 1, 5, 8, 22, 44]);

fn quick_sort<T:PartialOrd+Send>(v: &mut [T]) {
   if v.len() > 1 {
       let mid = partition(v);
       let (lo, hi) = v.split_at_mut(mid);
       rayon::join(|| quick_sort(lo),
                   || quick_sort(hi));
   }
}

fn partition<T:PartialOrd+Send>(v: &mut [T]) -> usize {
    let pivot = v.len() - 1;
    let mut i = 0;
    for j in 0..pivot {
        if v[j] <= v[pivot] {
            v.swap(i, j);
            i += 1;
        }
    }
    v.swap(i, pivot);
    i
}

上面给出了一个rayon join的示例,该示例中,join充分利用了多核,即多个CPU一起发挥作用参与排序。在一个4-Core的Macbook Pro上,我们可以看到,随着数组长度的增大,排序效率比单线程的quicksort快很多,因为是4-Core,因此最多也是快4倍,无法更快了。

Array Length Speedup
1K 0.95x
32K 2.19x
64K 3.09x
128K 3.52x
512K 3.84x
1024K 4.01x

这个结果是原作者对代码做了一些优化,即如数组长度低于5K,就使用串行的排序:

fn quick_sort<J:Joiner, T:PartialOrd+Send>(v: &mut [T]) {
    if v.len() <= 1 {
        return;
    }

    if J::is_parallel() && v.len() <= 5*1024 {
        return quick_sort::<Sequential, T>(v);
    }

    let mid = partition(v);
    let (lo, hi) = v.split_at_mut(mid);
    J::join(|| quick_sort::<J,T>(lo),
            || quick_sort::<J,T>(hi));
}
]]>
Arc in Rust 2020-10-07T11:00:00+00:00 Bean Li http://bean-li.github.io/Arc-in-Rust 前言

在Rust语言中,所有权大多数情况下是明确的,对于一个给定的值,你可以准确地判断出,哪个变量拥有它。但是也存在一些情况,单个值可能同时被多个所有者持有。

请看下面的例子:

`  fn process_files_in_parallel(filenames: Vec<string>,  glossary: &GigabyteMap) 
     -> io::Result<()> 
 {
      ...
      for worklist in worklists
      {
          thread_handlers.push(
              spawn(move || process_files(worklist, glossary)
          );
      }
      ....
 }
`

上面的例子中,拟采用多个进程,并发地处理文件。但是处理文件需要一个消耗巨大内存的数据结构, glossary,可能数据结构大小超过GB,这种情况下,拷贝多个副本给不同线程使用无疑是无法承受的。但是上面的代码,存在一个问题,即glossary的生命周期。

因为glossary并不能保证在所有的子进程完成任务之前不被销毁,如果被销毁,会导致子进程无法正常完成任务。

Rc and Arc

Rust提供了一个名为Rc的类型来支持多重所有权,它名称中的Rc是Reference Counting 引用计数的缩写。Rc类型会在内部维护一个记录值引用计数的计数器,从而确定这个值是否仍在被使用。如果对一个值的引用计数变成了0,意味着这个值可以被安全地清理掉。

那Arc是干啥的呢?Arc具备Rc的一切特征,那是Rc不是线程安全的,所以Rust又提供了一个Arc的类型,来确保多个线程之前操作某个变量的安全性。这个Arc中的A指的是Atomic,即Atomic Reference Counting。

那看起来是Arc是更强大的Rc,能够保证安全地用在并发的场景,那为什么不干脆消灭掉Rc,统统只使用Arc呢?

没有免费的午餐,Arc更强大,但是这种强大,需要付出一些性能开销才能做到,如果是单线程条件下,我们其实不需要付出这种开销。因此

  • 单线程条件下,可以使用Rc
  • 多线程并发条件下,建议使用Arc
use std::thread;
use std::time::Duration;
use std::sync::Arc;


fn main() {
    let foo = Arc::new(vec![0]);
    for _ in 0..10 {
        let bar = Arc::clone(&foo);
        thread::spawn(move || {
            thread::sleep(Duration::from_millis(200));
            println!("{:?}", &bar);
        });
    }
    println!("{:?}", foo);
}

上面的用法中, Arc::clone 并不是复制了一份vec,而是仅仅增减了一个引用计数,这是多个线程之间共享数据的一个方法。

回到我们最开始的问题,巨大的数据结构,多个线程之间共享:

fn process_files_in_parallel(filenames: Vec<String>, glossary: Arc<GigabyteMap>)
   -> io::Result<()>
{
   ....
   for worklist in worklists 
   {
       let glossary_for_child = glossary.clone() ;
       thread_handlers.push(
           spawn(move || process_files(worklist, &glossary_for_child))
       );
   }
   ....
   
}
]]>
Option和Result相关的组合算子 2020-05-05T20:00:00+00:00 Bean Li http://bean-li.github.io/Option-Result-combinator 前言

上一篇文章,从宏观的角度介绍了Rust错误处理的方法。提到了Option,作为弱化版的Result。

enum Option<T>{
	None,
	Some(T),
}

Option作为Rust的的系统类型,用来表示值不存在的可能。在编程中,这是一个非常好的做法,因为它强制Rust检测和处理值不存在的情况。

Option和Result,Rust都提供了一些组合算子来,来简化代码的书写,提供更强大的表达力。

Option相关的组合算子

ok_or和ok_or_else

这两个是一组,作用都是从Option转成Result。

pub fn ok_or<E>(self, err: E) -> Result<T, E>

pub fn ok_or_else<E, F>(self, err: F) -> Result<T, E>
where
    F: FnOnce() -> E, 

其作用细细来讲,如下:

  • Some(v) –> Ok(v)
  • None –> Err(err)

其行为大概如下所示:

fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
    match option {
        Some(val) => Ok(val),
        None => Err(err),
    }
}

下面以一个例子来展示这个组合算子的作用

use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

如果执行,不给任何参数:

Error: Please give at least one argument

给一个参数为5,输出如下:

5

nth(1) 因为没有第一个参数(0-index),所以Option的值为None,ok_or将Option转成了Result,最终返回的Err(err)。

map

这个组合算子的作用是将Option 转换成Option

  • Some(T) ->Some(U)
  • None -> None
fn main() {
    let maybe_some_string = Some(String::from("Hello, World!"));
    let maybe_some_len = maybe_some_string.map(|s| s.len());

    assert_eq!(maybe_some_len, Some(13));
}

注意map是值传递,会消耗掉原始的值。

and_then

and_then 又被称为flatmap,它的作用是从一个Option转成另一Option:

  • None -> None
  • Some(x) -> Some(f(x))

对于Some而言,and_then把包裹的值作为参数

fn sq(x: u32) -> Option<u32> { Some(x * x) }
fn nope(_: u32) -> Option<u32> { None }

assert_eq!(Some(2).and_then(sq).and_then(sq), Some(16));
assert_eq!(Some(2).and_then(sq).and_then(nope), None);
assert_eq!(Some(2).and_then(nope).and_then(sq), None);
assert_eq!(None.and_then(sq).and_then(sq), None);

注意sq函数的入参,是u32型,并不是Some()。

Result相关的组合算子

is_ok and is_err

result.is_ok和result.is_err返回bool型的结果。

对于is_ok而言:

  • Ok(T) 返回true
  • Err(E) 返回false

对于is_err而言:

  • Ok(T)返回false
  • Err(E)返回true
let x: Result<i32, &str> = Err("Some error message");
assert_eq!(x.is_err(), true);

let x: Result<i32, &str> = Ok(-3);
assert_eq!(x.is_ok(), true);

ok and err

pub fn ok(self) -> Option<T>
pub fn err(self) -> Option<E>

注意,这两个算子都是将Result转成Option类型,一个关注成功的情况,一个关注出错的情况。

result.ok() 如果Result返回Ok(T),那么返回值是Option,即Some(success_value)。如果Result失败,那么该函数返回None,把Err的值丢弃掉。

let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

result.err()如果Result失败,返回Err(E),那么返回返回Option

let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));

and_then

Option也有and_then 算子,Result和Option一样的,语义是一样的。而且有很多这样的算子。

use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

还是前面的例子, ok_or()之后,Option就转成了Result,如果Ok的情况下,类型是字符串,那么and_then 会尝试将字符串解析成i32。

pub fn and_then<U, F>(self, op: F) -> Result<U, E>
where
    F: FnOnce(T) -> Result<U, E>, 

如果Result的值是Ok(T),那么执行op(T),否则直接返回Err。

n sq(x: u32) -> Result<u32, u32> { Ok(x * x) }
fn err(x: u32) -> Result<u32, u32> { Err(x) }

assert_eq!(Ok(2).and_then(sq).and_then(sq), Ok(16));
assert_eq!(Ok(2).and_then(sq).and_then(err), Err(4));
assert_eq!(Ok(2).and_then(err).and_then(sq), Err(2));
assert_eq!(Err(3).and_then(sq).and_then(sq), Err(3));

map and map_err

上面的例子中给出了map_err的使用。map和map_err是对Result中的Ok(T)或者Err(E)执行某个函数或者执行某种变换,得到新的Result:

pub fn map<U, F>(self, op: F) -> Result<U, E>
where
    F: FnOnce(T) -> U, 
    
pub fn map_err<F, O>(self, op: O) -> Result<T, F>
where
    O: FnOnce(E) -> F, 

对于map函数而言:

  • 如果Result值是Ok(T),对T执行函数
  • 如果Result的值是Err(E),保持不变,新的结果Option的值也是Err(E)

如下实例代码:

let line = "1\n2\n3\n4\n";

for num in line.lines() {
    match num.parse::<i32>().map(|i| i * 2) {
        Ok(n) => println!("{}", n),
        Err(..) => {}
    }
}

对于map_err而言:

  • 如果Result的值是Err(E),对E执行函数
  • 如果Result的值是Ok(T),保持不变。结果Result和输入Result一致。
fn stringify(x: u32) -> String { format!("error code: {}", x) }

let x: Result<u32, u32> = Ok(2);
assert_eq!(x.map_err(stringify), Ok(2));

let x: Result<u32, u32> = Err(13);
assert_eq!(x.map_err(stringify), Err("error code: 13".to_string()));

map_err可以用来对错误进行转换。

代码赏析

最后的最后,赏析一段Error Handle的代码

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    File::open(file_path)
        .map_err(|err| err.to_string())
        .and_then(|mut file| {
            let mut contents = String::new();
            file.read_to_string(&mut contents)
                .map_err(|err| err.to_string())
                .map(|_| contents)
        })
    .and_then(|contents| {
        contents.trim().parse::<i32>()
            .map_err(|err| err.to_string())
    })
    .map(|n| 2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

上面这段代码中,有很多种不同的错误类型:

  • io::Error
  • num::ParseIntError

通过map_err都转成了Result<i32,String>类型。

上述写法看起来技巧性很强,但是我不喜欢,属于炫技派做法,心智负担比较重。

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    let mut file = match File::open(file_path) {
        Ok(file) => file,
        Err(err) => return Err(err.to_string()),
    };
    let mut contents = String::new();
    if let Err(err) = file.read_to_string(&mut contents) {
        return Err(err.to_string());
    }
    let n: i32 = match contents.trim().parse() {
        Ok(n) => n,
        Err(err) => return Err(err.to_string()),
    };
    Ok(2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

这段代码,看起来更舒服,更易懂。

]]>
Rust错误处理 2020-05-04T23:00:00+00:00 Bean Li http://bean-li.github.io/Error-Handle-in-Rust 前言

本文介绍Rust的错误处理。Rust的错误处理有其独到之处,非常值得学习。Rust将错误分成两类:

  • 不可恢复的错误
  • 可恢复的错误

不可恢复的错误一般是程序编写出现疏漏,不应该发生的事情发生了

  • 比如除以0 ,引发SIGFPE
  • 访问数组元素越界
  • thread::spawn 无法创建线程 (比如内存耗尽 process bomb等原因)

这些错误是不该发生的,一般伴随着程序员代码中的某种错误,这种情况下,一般是不可处理的,程序员也不知道该如何处理这种错误,程序不宜继续运行下去。对于这种错误,Rust一般采用panic的做法,即让程序崩溃,立刻停止执行。

可恢复的错误,是指一些特殊的情况,但是这种情况发生了,我们完全可以处理。

比如说我们要创建一个配置文件,我们会尝试open这个文件,如果open失败,报不存在这个文件,这尽管是个报错,但是没关系,我们可以处理,我们可以创建此文件。对于这种类型的错误,Rust引入了Result,来处理。

下面我们分别针对panic和Result两种处理方式,分开阐述。

不可恢复的错误与panic!

一般来讲,不可恢复的错误是不该发生的,如下面场景:

  • 访问数组元素越界
  • 除以0
  • 资源耗尽,无法创建进程

这种事情一旦发生,一般伴随着程序中存在bug。这种情况下,任何的修复尝试都是徒劳的,程序尽快终止才是最合理的做法。Rust提供了panic!宏。

panic!宏会接收println!-style的参数,将出错信息打印出来。

fn my_div(n: i32, m: i32) -> i32 {
    if m == 0 {
        panic!("Invalid number: {}, div 0", m);
    }
    n/m
}

fn main() {
    println!("result is {}", my_div(9, 2));
    println!("result is {}", my_div(4, 0));
}

​ 输出如下:

result is 4
thread 'main' panicked at 'Invalid number: 0, div 0', src/main.rs:3:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

出了上面列出的通用场景可能会引发panic,从编程角度讲,有哪些情况可能会产生panic呢?

  • Option::None 遭遇 unwrap()
  • Option::None 遭遇 expect()
  • Result::Err 遭遇unwrap()
  • Result::Err 遭遇 expect()
  • 直接调用panic!宏

发生panic或者调用panic!宏之后,Rust的行为有两种可选

  • unwind (展开)
  • abort (终止)

unwind

unwind是默认的行为,采用这种行为的条件下,下面事情会发生:

  • 打印报错信息
  • 程序会利用栈回退(stack unwind)机制,确保栈内构造的局部变量和指针的析构函数被一一调用掉,完成清理动作。
  • 触发panic的线程会exit。

注意第三点,panic是per thread的。对于一个多线程程序而言,如果一个线程panic了,那么其他线程可以继续正常运行。这是多线程章节里面的内容,此处不展开。

abort

遇到panic!另一种处理方法是abort,即立即退出,不做清理动作。我们可以在Cargo.toml中的[profile]中添加:

panic = 'abort'

或者采用如下方法:

rustc -C panic=abort src/main.rs

选择发生发生panic时,直接abort。

result is 4
thread 'main' panicked at 'Invalid number: 0, div 0', src/main.rs:3:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
[1]    22576 abort (core dumped)  ./main

catch_unwind

我们很多人都学习过C语言,语言崩溃的时候,也可以打印出stack,但是对于这种意想不到的事情发生的时候,尽可能丰富的打印当时的情况下信息,是后续调试 debug和改进的关键。

Rust在std::panic::catch_unwind 来捕捉panic。我们可以看下刚才的程序。

use std::panic ;

fn my_div(n: i32, m: i32) -> i32 {
    if m == 0 {
        panic!("Invalid number: {}, div 0", m);
    }
    n/m
}

fn main() {
    let result = panic::catch_unwind(|| {
        println!("result is {}", my_div(9, 2));
        println!("result is {}", my_div(4, 0));
    }) ;

    match result {
        Ok(()) => println!("every thing goes OK"),
        Err(_) => println!("catch panic")
    }
}

我们调用了panic::catch_unwind ,将我们觉得有危险的部分可能panic的部分,包了起来。如果发生了panic,会被捕捉到。

运行结果如下:

result is 4
thread 'main' panicked at 'Invalid number: 0, div 0', src/main.rs:5:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
catch panic

注意,panic的默认行为还是unwind,所以你看到了错误信息打印之类的,但是进程并没有退出,后面的catch panic得到了执行。

如果不想打印出来panicked at 之类的信息,应该怎么办?

std::panic提供了set_hook和take_hook两个方法:

use std::panic ;

fn my_div(n: i32, m: i32) -> i32 {
    if m == 0 {
        panic!("Invalid number: {}, div 0", m);
    }
    n/m
}

fn main() {
    /*注册新的hook*/
    panic::set_hook(Box::new(|_info| {
        //do_nothing
    }));

    let result = panic::catch_unwind(|| {
        println!("result is {}", my_div(9, 2));
        println!("result is {}", my_div(4, 0));
    }) ;

    match result {
        Ok(()) => println!("every thing goes OK"),
        Err(_) => println!("catch panic")
    }

    let _ = panic::take_hook();

    let result = panic::catch_unwind(|| {
        println!("result is {}", my_div(9, 2));
        println!("result is {}", my_div(4, 0));
    }) ;

    match result {
        Ok(()) => println!("every thing goes OK"),
        Err(_) => println!("catch panic")
    }

}

因为首先用set_hook注册了新的hook,而新的hook什么也不做,所以,前半部的输出如下:

result is 4
catch panic

后半部分,take_hook的作用是 unregister当前的hook,并且返回当前hook值。所以后半部分,有回归到了默认的行为,尽管被捕捉,但是还是会执行默认的unwind。

catch_unwind这个方法,很容易被熟悉python语言或者Java语言的人当作 try-catch来用,用于捕捉异常。但是下面的话需要务必重视:

极其不推荐使用catch_unwind处理常规错误。

不要用catch_unwind来做正常的流程控制,它的主要用途在:

  1. 在FFI场景下的时候,如果说C语言调用了Rust的函数,在Rust内部出现了panic,如果这个panic在Rust内部没有处理好,直接扔到C代码中去,会导致C语言产生“未定义行为(undefined behavior)”
  2. 某些高级抽象机制,需要阻止栈展开,比如线程池,如果一个线程中出现了panic,我们希望只把这个线程关闭,而不至于将整个线程池一起被拖下水

Result

Rust其实提供了一个分层的错误处理方案:

  • if something might reasonably be absent, Option is used
  • if something goes wrong and can reasonably be handled, Result is used
  • if something goes wrong and cannot reasonably be handled, the thread panics
  • if something catastrophic happens, the program aborts

这个分层方案中,第一层就是Option,第二层是Result,其实严格意义上讲,Option,是Result的特例:

enum Option<T>{
	None,
	Some(T),
}

enum Result<T, E>{
	Ok(T),
	Err(E),
}

使用Option的时候,一般表示不太关心出错原因,出错的时候,直接返回None,而Result的表达力要比Option更强,将错误的不同原因也包括进来。因此从这个角度讲,Option不过是Result的特例:

Option<T>  ~ Result<T, ()>

比如从HashMap中取值或者对Vector进行pop操作,前者出错的原因只有键值即key不存在,后者出错的原因只有Vector已经是空的了,这种情况下,Option比Result更合适。

比如从File中读取某个信息,出错的可能性很多,比如文件不存在,没有读取权限,数据不合法等,这种情况下,Option就力不逮己了,需要用Result<T,E>。

Option

什么时候使用Option?

  • 如果一个函数的某个参数的可选的
  • 一个函数有返回值,但是其返回值可能是空的
  • 一个数据类型,其值可能会为空

对于第一种情况:

fn get_full_name(fname: &str, lname: &str, mname: Option<&str>) -> String { // middle name can be empty
  match mname {
    Some(n) => format!("{} {} {}", fname, n, lname),
    None => format!("{} {}", fname, lname),
  }
}

fn main() {
  println!("{}", get_full_name("Galileo", "Galilei", None));
  println!("{}", get_full_name("Leonardo", "Vinci", Some("Da")));
}

middle name可能会空,所以用Option类型传递,如果为空,传None,如果不为空,传递Some(T)。

输出如下:

Galileo Galilei
Leonardo Da Vinci

对于第二种情况,比较典型的是从HashMap查找某键值对应的value,可能会不存在,或者从Vector中pop,可能会Vector已经为空,那么函数的返回值,最好是Option,因为Option类型会强制函数调用方必须要检查是None的情形,防止程序员编程时忽略。

我们以我们的除法为例,除以0会触发panic,但是我们可以封装一个除法,检查除数是否是0,如果除数是0,那么返回None,否则,返回Some(T)。

fn checked_div(n: i32, m: i32) -> Option<i32> {
    if m == 0 {
        None
    }
    else {
        Some(n/m)
    }
}

fn try_div(dividened: i32, divisor: i32){
    match checked_div(dividened, divisor){
        None => println!("{} / {} failed", dividened, divisor),
        Some(result) => {
            println!("{} / {} = {}", dividened, divisor, result)
        },
    }
}

fn main() {
    try_div(9,2);
    try_div(9,0);
}

返回值是Option,所以try_div函数,必须要判断结果等于None时候,防止默认结果总是存在的。输出如下:

9 / 2 = 4
9 / 0 failed

另一个比较典型的例子是dirs::home_dir,在Linux下很多用户可能存在HOME 目录,但是也可能不存在,因此返回值是Option类型:

pub fn home_dir() -> Option<PathBuf>

示例代码如下:

extern crate dirs;

fn main() {
    let home_path = dirs::home_dir();
    match home_path {
        Some(p) => println!("{:?}", p), 
        None => println!("Can not find the home directory!"),
    }
}

输出如下:

"/home/manu"

Result

Option 介绍完毕之后,就可以安心介绍最重要的Error Handle方法了,就是Result:

enum Result<T, E>{
	Ok(T),
	Err(E),
}

Result本质是一种enum,Ok表示执行成功的情况,Err表示执行失败的情况,其中T和E分别是Ok或者Err时,对应返回值的类型。如果你的函数或者方法,可能失败可能出错,建议返回类型选择Result。

枚举类型实例表示定义中的成员中的任意一项,反映到我们Result类型,就是要么时Ok,要么是Err,但是不可能既是Ok,又是Err,因此,如果函数返回Result类型,那么结果处于薛定谔的猫,即处于生和死的叠加,即Result处于Ok和Err的叠加态,函数不返回,无从得知到底是否成功。

那用这种数据类型有什么好处呢?好处在于,无论程序是否出错,函数的返回值的类型是相同的,函数调用者可以用相同的方式处理Result。从这个角度说,无论成功与否,函数调用者都要处理Result,错误处理称为业务逻辑的一部分,而不能随意的忽略。

(过去几年间,我处理过非常多的现场问题,我也修复过非常多的bug,我看过太多bug,仅仅是忽视Error Handle,程序编写者对于可能出现的错误并不在意,轻率或者轻浮地认为,很多错误并不会发生,或者发生了也就随它去吧,并不会认真地思考,这种错误发生的时候,应该怎么处理才比较妥当。)

match处理 Result类型结果

因为Result类型的值是enum,所以典型的处理方法是模式匹配match。

Rust By Example中的的例子比较典型:

mod checked {
    // Mathematical "errors" we want to catch
    #[derive(Debug)]
    pub enum MathError {
        DivisionByZero,
        NonPositiveLogarithm,
        NegativeSquareRoot,
    }

    pub type MathResult = Result<f64, MathError>;

    pub fn div(x: f64, y: f64) -> MathResult {
        if y == 0.0 {
            // This operation would `fail`, instead let's return the reason of
            // the failure wrapped in `Err`
            Err(MathError::DivisionByZero)
        } else {
            // This operation is valid, return the result wrapped in `Ok`
            Ok(x / y)
        }
    }

    pub fn sqrt(x: f64) -> MathResult {
        if x < 0.0 {
            Err(MathError::NegativeSquareRoot)
        } else {
            Ok(x.sqrt())
        }
    }

    pub fn ln(x: f64) -> MathResult {
        if x <= 0.0 {
            Err(MathError::NonPositiveLogarithm)
        } else {
            Ok(x.ln())
        }
    }
}

// `op(x, y)` === `sqrt(ln(x / y))`
fn op(x: f64, y: f64) -> f64 {
    // This is a three level match pyramid!
    match checked::div(x, y) {
        Err(why) => panic!("{:?}", why),
        Ok(ratio) => match checked::ln(ratio) {
            Err(why) => panic!("{:?}", why),
            Ok(ln) => match checked::sqrt(ln) {
                Err(why) => panic!("{:?}", why),
                Ok(sqrt) => sqrt,
            },
        },
    }
}

fn main() {
    // Will this fail?
    println!("{}", op(1.0, 10.0));
}

在上述例子中: \(f(x,y) = sqrt(ln(x/y))\) 这个计算中有很多的失败可能:

  • y = 0 ,导致 x/y panic 即 DivisionByZero
  • x/y的结果 < 0 , 导致 ln运算异常 即NonPositiveLogarithm
  • ln(x/y)的结果 < 0 ,导致sqrt异常 (不考虑无理数)即NegativeSquareRoot

因为有异常的可能,因此,每一步运算的结果,都是Result类型。每一层运算,都要用match来模式匹配,判断是否有异常发生。

thread 'main' panicked at 'NegativeSquareRoot', src/main.rs:48:29
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

因为用match来处理错误是最常用最推崇的方法,我们下面再给出一个例子,来介绍用match来处理Result。

use std::fs::File ;

fn main()
{
    let f = File::open("hello.txt");

    let _f = match f {
        Ok(file) => file ,
        Err(error) => {
            panic!("There was a problem opening the file {:?}", error)
        },
    };
}

结果用match匹配,如果出错,处理方法是panic!。

thread 'main' panicked at 'There was a problem opening the file Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:10:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

上面两个例子中,出现异常都是粗暴的panic,但是以open为例,如果hello.txt不存在的话,可能我们可以接受,比如我们创建一个空文件出来。

use std::fs::File ;
use std::io::ErrorKind ;

fn main()
{
    let f = File::open("hello.txt");

    let _f = match f {
        Ok(file) => file ,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hell.txt") {
                Ok(fc) => fc ,
                Err(e) => panic!("Try to create file but encounter a problem{:?}", e)
            },
            other_error => panic!("There was a problem opening the file {:?}", other_error),
        },
    };
}

open失败的话,如果是NotFound错误,那么我们就尝试创建该文件。

创建file的接口为 std::fs::File::create ,这一次没有错误的写成creat。C接口中,创建文件的系统调用为creat,被戏称成最大的遗憾。go语言没有此遗憾,Rust一样没有此遗憾。

unwrap 和 expect

处理Result类型,一般是用match,对于Err也要必须处理。但是这个世界上懒人是很多的,如果程序员确实不愿意处理Err,或者说处理也无益,直接崩溃是比较合理的做法,Rust对于这种情况提供了快捷方式,即unwrap和expect。

use std::fs::File ;

fn main()
{
    let _f = File::open("hello.txt").unwrap();
}

输出如下:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

如果使用expect函数:

use std::fs::File ;

fn main()
{
    let _f = File::open("hello.txt").expect("failed to open hello.txt");
}

输出如下:

thread 'main' panicked at 'failed to open hello.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

在我们上面的代码中,File::open返回io::Result, Rust标准库中,有很多Result类型,其中io::Result就是其中之一。io::Result实现了unwrap和expect方法。如果函数执行成功,那么unwrap 和 expect方法就将正确的值取出来,如果出错,不做任何处理,直接panic。expect函数和unwrap函数功能点很接近,unwrap会打印出标准库内置的错误信息,而expect函数允许用户定义一个字符串,在程序挂掉的时候显示。

尽管我们介绍了unwrap和expect,但是这并不是推荐的错误处理方式,事实上,在严肃的商用程序上,不希望看到这两个函数出现。我们一般讲这两个函数用于原型设计。

传播错误

Rust允许程序像其他语言处理异常一样,讲错误扔给更上一层调用者,这被称为传播错误, Propagating Error。

use std::io ;
use std::io::Read;
use std::fs::File ;


fn read_username_from_file() -> Result<String, io::Error>{
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new() ;
    match f.read_to_string(&mut s){
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }

}
fn main()
{
    let _s = read_username_from_file().unwrap();
}

我们看下上面的代码,从hello.txt中读取内容到String中,这个过程中可能会遇到很多错误:

  • open 失败
  • 读取失败

写程序的时候,会写很多的match,总体来讲,这些错误都是同一种类型的错误即 io::Error,对于read_username_from_file而言,对于这些io::Error也并没有做什么特殊的处理,遇到任何io::Error,提前返回错误,否则,将String返回。

Rust提供了传播错误的快捷方式,这个堪称比较好用的语法糖了。

use std::io ;
use std::io::Read;
use std::fs::File ;

fn read_username_from_file() -> Result<String, io::Error>{
    let mut f = File::open("hello.txt")?;
    let mut s = String::new() ;
    f.read_to_string(&mut s)? ;
    Ok(s)

}
fn main()
{
    let _s = read_username_from_file().unwrap();
}

第五行 open后面的? ,作用如下:

  • open成功,将Result中Ok包裹的值,赋值给变量f
  • open如果失败,?会让整个函数退出执行,并且返回Err(error)给调用者。

有了这个神器,代码就可以写的比较简洁了,不需要一坨代码,match来match去。

事实上上面的代码还可以写的更加简洁:

fn read_username_from_file() -> Result<String, io::Error>{
    let mut s = String::new() ;
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}
]]>
迭代器消费器(Iterator Consumer) 2020-05-02T10:29:00+00:00 Bean Li http://bean-li.github.io/Iterator-Consumer 前言

前面提到,Rust中的迭代器都是惰性的,也就是说,无人触发的情况下,不会自己遍历。最直接消费迭代器数据的方法就是for循环。for循环会隐式地调用迭代器的next方法,从而达到循环的目的。

为了方便编程,Rust也提供了for循环之外的用于消费迭代器内数据的方法,他们被称为消费器 Consumer。

any

any用来遍历迭代器,查找是否存在满足条件的元素。any只要找到了满足条件的元素,就停止遍历, 即不会继续Consume迭代的元素。

fn main() {
    let id = "Iterator" ;
    assert_eq!(id.chars().any(char::is_uppercase), true);
    assert_eq!(id.chars().any(char::is_uppercase), false);
}

all

all用来判定是否迭代器的所有元素都满足某个条件。当存在一个元素不满足条件,就不必继续下去了,因为一定会返回false。

空的迭代器总是返回true。

fn main() {
    let a = [1, 2, 3];
    let mut iter = a.iter();
    assert!(!iter.all(|&x| x != 2));
    assert_eq!(iter.next(), Some(&3));
}

fold

fold我喜欢翻译成折叠器,即遍历迭代器的所有元素,通过执行执行的运算,最终折叠成一个元素。比如,将迭代器所有元素累加在一起,累乘在一起。

fn main() {
    let a = [1, 2, 3,4];

    println!("the count of element is {}", a.iter().fold(0, |n, _| n+1));
    println!("the sum of element is {}", a.iter().fold(0, |n, e| n+e));
    println!("the multiple of element is {}", a.iter().fold(1, |n, e| n*e));
}

运行结果为:

the count of element is 4
the sum of element is 10
the multiple of element is 24

依次对迭代器的所有元素执行某种运算,很明显,需要一个初始值。

  • 对于累加而言,0是合理的初始值
  • 对于累乘而言,1是合理的初始值

folder方法的签名如下:

fn fold<B, F>(self, init: B, f: F) -> B
where
    F: FnMut(B, Self::Item) -> B, 

count,sum和product

fold是个通用的方法,对于上面提到的常用的fold方法,Rust有提供了专门的方法:

方法 作用
count 迭代器中元素的个数
sum 迭代器中所有元素之和
product 迭代器中所有元素的乘积
max 迭代器中最大的元素(Iterator必须实现了std::cmp::Ord)
min 迭代器中最小的元素(Iterator必须实现了std::cmp::Ord)
fn main() {
    let a = [1, 2, 3,4];

    println!("the count of element is {}", a.iter().fold(0, |n, _| n+1));
    println!("the sum of element is {}", a.iter().fold(0, |n, e| n+e));
    println!("the multiple of element is {}", a.iter().fold(1, |n, e| n*e));

    println!("the count of element is {}", a.iter().count());
    println!("the sum of element is {}", a.iter().sum::<u32>());
    println!("the multiple of element is {}", a.iter().product::<u32>());
}

其中max和min,要必须是实现了std::cmp::Ord的类型才能调用。比如f32和f64类型是不行的,因为浮点数中有NaN。

collect

collect消费器应该算是使用最广泛的一种了。collect有搜集的意思。它将迭代器通过next方法获得的元素,”搜集”起来,收集到指定的集合容器中。

比如我们很常见命令行获取入参会有如下的代码:

let args: Vec<String> = std::env::args().collect() ;

std::env::args()是一个入参的迭代器,我们通过collect方法,将迭代器转成vector。

因为collect转成vector是最常见的一种用法,很容易让人产生误解,即collect是用来完成iterator到vector转换的。这种理解是不对的。只要我们愿意,我们可以将迭代器转成各种不同类型的collection。

let args: HashSet<String> = std::env::args().collect();
let args: BTreeSet<String> = std::env::args().collect();
let args: LinkedList<String> = std::env::args().collect();

let args: HashMap<String, usize> = std::env::args().zip(0..).collect();

看到这个地方,可能会很奇怪,为什么HashSet /BTreeSet/LinkedList等可以从迭代器转化而来?因为这些类型都实现了std::iter::FromIterator trait,它会调用该trait的from_iter方法

]]>
迭代器适配器(Iterator Adapter) 2020-05-02T10:29:00+00:00 Bean Li http://bean-li.github.io/Iterator-Adapter 前言

本文介绍Iterator Adapters,即迭代器适配器。Iterator Adapter消耗一个迭代元素转换成另一个迭代元素。

迭代器就好像是现实世界的水管,现实世界中,水管是标准化的接口,打开水管就可以出水。厨房用水可能需要加热,洗澡间用水可能需要冷热混合,淋浴花洒可能需要将水分割成若干个小水流。干这些事情的,我们称之为适配器。

map and filter

map很简单,将方法映射到迭代器产生的每一个元素,从而输出新的迭代元素。

fn main() {
    let text = " Panda \n    giraffes\niguanas \nsquid".to_string();
    let v:Vec<&str> = text.lines()
        .map(str::trim)
        .collect() ;

    assert_eq!(v, ["Panda", "giraffes", "iguanas", "squid"]);
}

String和&str提供了lines方法,该方法也会产生一个迭代器,这是上一篇产生迭代器方法中没有提到的。类似的方法有很多:

  • s.bytes()
  • s.chars()
  • s.split_whitespace()
  • s.lines()
  • s.split(‘/’)
  • s.matches(char::is_numeric)

所以text.lines 方法返回的是一个迭代器,即以回车字符为界,每次迭代产生出一行的内容。

map将每一行的内容,执行str::trim方法,即将每一行头部和尾部的空白字符删除。

collect方法将迭代器转换成Vec,从而完成将一段文本,按行生成一个Vec,同时去除头部和尾部的无用空白字符。

map很容易理解,即对每一个元素都执行对应的方法,得到新的迭代元素,从而得到新的迭代器。filter,顾名思义,即过滤,即对每个迭代元素执行对应方法,得到bool型的结果,只有bool值为true的迭代元素才会保留下来,bool值为负的元素被滤出掉,从而形成新的迭代器。

fn main() {
    let text = " Panda \n    giraffes\niguanas \nsquid".to_string();
    let v:Vec<&str> = text.lines()
        .map(str::trim)
        .filter(|s| *s != "iguanas")
        .collect() ;

    assert_eq!(v, ["Panda", "giraffes", "squid"]);
}

如上示范代码,滤除等于”iguanas”的迭代元素。

这两个适配器的方法签名如下:

fn map<B, F>(self, f: F)->some Iterator<Item=B>
	 where Self: Sized, F: FnMut(Self::Item) -> B
	 
fn filter<P> (self, predicate: P) -> some Iterator<Item=Self::Item>
	where Self: Sized, P: Fnmut(&Self::Item)-> bool

我们细细品map和filter,可以得到如下结论:

  • map 是值传递,而filter是共享引用传递。
  • map之后新的迭代器的元素和原始迭代器产生的元素,未必是同一种类型
  • filter之后,新的迭代器的元素和原始迭代器的元素是同一种类型

filter是共享引用,这也是为什么我们示例代码中,为 *s != “iguanas”的原因。

最后需要指出的是,迭代器适配器是惰性的,要有真正的消费行为,才会产生迭代,否则就只是个迭代器而已,无人调用。回到我们的范例,最后的collect方法调用,才真正产生了消费行为,才会不断调用迭代器的next方法。

我们给个反面的示范:

["earth", "water", "air", "fire"].iter()
	.map(|elt| println!("{}", elt));

上面方法看起来很好,但是打印每一个元素的值,但是实际上,map适配器是产生出新的迭代器,但是没有任何方法要求迭代器产生出新的元素,因此,Rust会发出警告。

enumerate

enumerate 适配器是一个看起来相当无聊,但在实际编程中,非常常用的适配器。我相信写过python代码的人,读到这一句,都会会心一笑,赞同我所言非虚。

比如一个迭代器产生出 A , B , C这种元素,那么经过enumerate适配器之后,新的迭代器产生出(0,A),(1,B), (2,C) 这种迭代元素。

fn main() {
    let a = ['a', 'b', 'c'] ;
    let iter = a.iter().enumerate();

    for (idx, elem) in iter {
        println!("({}, {})", idx, elem)
    }
}

输出结果如下:

(0, a)
(1, b)
(2, c)

zip

zip适配器是将两个迭代器的元素,捏合成一个迭代器。比如第一个迭代器的迭代元素是”a”, “b”, “c”,第二个迭代器的迭代元素’A’, ‘B’, ‘C’,那么iter1.zip(iter2),产生的新的迭代器的元素为(‘a’, ‘A’), (‘b’, ‘B’),(‘c’, ‘C’)。

从上面的讨论可以看出,enumerate适配器,本质是zip适配器的一种特例。

(0..).zip(iter) 
iter.enumerate()

chain

zip是将两个迭代器配对成一个新的迭代器,新迭代器中每个元素是两个迭代器同一位置的元素组成的元组。

chain 适配器是将两个迭代器连接在一起,即append的意思。

以 iter1.chain(iter2)为例,返回一个新的迭代器,新的迭代器的next方法,会先返回iter1迭代器中的元素,当iter1中元素耗尽,那么开始从iter2 迭代器中获取元素,知道iter2中的元素也耗尽。简单地说,就是把iter2 放到iter1的后面,连成一个新的迭代器。

fn main() {
    let a1 = [1, 2, 3] ;
    let a2 = [4, 5, 6] ;
    let iter = a1.iter().chain(a2.iter());
    for elem in iter {
        println!("{}", elem)
    }
}

上面的做法是严格的做法,实际上,

fn chain<U>(self, other: U) -> Chain<Self, <U as IntoIterator>::IntoIter>
where  U: IntoIterator<Item = Self::Item>,

chain 适配器使用IntoIterator,我们可以传给它任何可以转换成Iterator的变量,比如Vec本身,即上面的示范,也可以写成:

let iter = a1.iter().chain(a2)

因为slice(&[T]) 实现了IntoIterator,所以可以直接传给chain作为参数。

filter_map

filter_map 适配器是个非常有用的适配器,它有点是filter 和map的结合体的意味。我们有时候遍历Result类型的迭代器的时候,有时候,可能会有失败的情况,我们就用filter_map来忽略失败的项。

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .filter_map(Result::ok)
        .collect();
    println!("Results: {:?}", numbers);
}

我们故意用了一个比较啰嗦的版本,我们来细细地分析:

  1. map( s s.parse::())返回的是类型是Result类型
  2. Result::ok()方法,如果result是成功的,那么返回Some(success_value),如果执行失败,返回None
  3. filter_map适配器会对迭代器的每一个元素调用闭包,如果闭包返回Some(element),那么element元素返回,如果闭包返回None,那么它会忽略该元素,尝试对下一个迭代器元素调用闭包。

所以我们总结下filter_map的实质:

  1. filter_map适配器,会对迭代器中的每一元素执行闭包,体现了map的方面
  2. 执行闭包之后,正常的结果元素,保留,作为结果迭代器的元素,如果闭包的返回值是None,忽略,体现filter的方面。

上面的实例代码,我们可以进一步简化:

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();
    println!("Results: {:?}", numbers);
}

flat_map

flat是平坦的意思,flatten是拍平。有些时候,可能需要将多个序列的结果级联起来,变成一个序列。

比如说我们有个数据结构称为主要城市:

  • 日本Japan的主要城市有Tokyo, Kyoto
  • 中国China的主要城市有BeiJing,ShangHai,NanJing
  • 美国USA的主要城市有Portland,Washington

如果我们需要得到一个中国 日本 美国的城市列表,我们就需要打破各自序列的界限,变成一个列表。

use std::collections::HashMap ;

fn main() {
    let mut major_cities = HashMap::new() ;

    major_cities.insert("China", vec!["Beijing", "Shanghai", "Nanjing"]);
    major_cities.insert("Japan", vec!["Tokyo", "Kyoto"]);
    major_cities.insert("USA", vec!["Portland", "New York"]);

    let counties = ["China", "Japan", "USA"];

    for &city in counties.iter().flat_map(|country| &major_cities[country]) {
        println!("{}", city);
    }
}

输出如下:

Beijing
Shanghai
Nanjing
Tokyo
Kyoto
Portland
New York

我们细细的解析flat_map

  • 首先是map,map之后,每个元素都是一个迭代器
  • 然后将map之后的多个迭代器的界限打破,平坦化,把多个迭代器变成一个迭代器。

另一个类似的例子如下:

fn main() {
    let words = ["alpha", "beta", "gamma"];

    // chars() returns an iterator
    let merged: String = words.iter()
        .flat_map(|s| s.chars())
        .collect();
    assert_eq!(merged, "alphabetagamma");
}

take和take_while

take适配器用来截取迭代器的前n个元素。

fn main() {
    let a = [1, 2, 3];
    let mut iter = a.iter().take(2);

    assert_eq!(iter.next(), Some(&1));
    assert_eq!(iter.next(), Some(&2));
    assert_eq!(iter.next(), None);
}

这个take没啥特别的,理解了take,可以接下来学习take_while适配器了。

take_while适配器和take一样,截取原始迭代器的部分元素,但是结束条件不一样:

  • take(n):很明确,即选取前n个元素,余者不取
  • take_while : 会有闭包函数,闭包函数返回bool型结果,如果bool型结果为false,即立刻停止。
fn main() {
    let message = "To: jimb\r\n\
                   From: superego <editor@oreilly.com>\r\n\
                   \r\n\
                   Did you get the any writing done today \r\n\
                   when will you stop wasting time plotting fractals\r\n";
    for header in message.lines().take_while(|l| !l.is_empty()){
        println!("{}", header)
    }
}

message是一封邮件的内容,header和body之间,有一空行。解析程序通过判断空行位置,来打印header信息:

To: jimb
From: superego <editor@oreilly.com>

#skip 和 skip_while

skip和take正好相反。skip是丢掉迭代器前n个元素。

我们做命令行解析的时候,第一个参数一般是可执行文件本身,需要被忽略掉。

for arg in std::env::args.skip(1) {
	...
}

skip_while的用法和take_while正好相反:

  • drop 或skip掉 迭代器的前面的元素
  • 直到闭包函数返回false
fn main() {
    let message = "To: jimb\r\n\
                   From: superego <editor@oreilly.com>\r\n\
                   \r\n\
                   Did you get the any writing done today \r\n\
                   when will you stop wasting time plotting fractals\r\n";
    for body in message.lines().skip_while(|l| !l.is_empty()){
        println!("{}", body)
    }
}

跳过前面的非空行,从第一个空行开始,打印后面所有的行:

Did you get the any writing done today
when will you stop wasting time plotting fractals
]]>
Iterator in Rust (1) 2020-05-01T10:29:00+00:00 Bean Li http://bean-li.github.io/Iterator-in-Rust 前言

Iterators是迭代器,迭代器用来产生一串值。Rust提供了迭代器来遍历vectors,string,hashtables等,除此外:

  • 产生文本中的每一行的内容

  • 到达网络服务器的每个网络连接 (connection)

  • 通过communication channel获得的来自其他线程的值

我们也可以创建自己的迭代器。Rust的for 循环可以使用迭代器,除此外,迭代器本身也提供很丰富的方法来使用迭代器。

迭代器是弹性灵活的,表达力很强,而且效率很高。我们以下面的函数为例:

fn triangle(n: i32)-> i32{
    let mut sum = 0;
    for i in 1..n+1 {
        sum += i
    }
    sum
}

给定入参n ,计算, 1+2+ …+n 的值。函数写的啰嗦。如果此处使用迭代器,因为迭代器有fold方法,可以用如下语句简洁地完成同样的功能:

fn triangle_v2(n: i32) -> i32 {
    (1..n+1).fold(0, |sum, item| sum+item)
}

Iterator and IntoIterator Traits

实现了std::iter::Iterator的任意数据结构,都可以称之为迭代器:

trait Iterator {
	type Item ;
	fn next(&mut self) -> Option<self::Item> ;
}

Item是迭代器产生的数据类型。该Trait要实现一个next method来产生出Some(v):

  • 要么产生出该迭代器的next value
  • 要么返回None,表示迭代器已经迭代到尽头,无法产生新的value

如果某种数据结构,存在一种自然的迭代方法,这表明,该数据结构实现了std::iter::IntoIterator Trait,该trait中提供了into_iter方法处理此事,即:

  • 输入是该数据结构本身
  • 输出是该数据结构对应的迭代器

最典型最容易理解的是vector。

fn main() {
    println!("There's:");
    let v = vec!["antimony", "arsenic", "aluminum", "tony"];

    for elem in &v {
        println!("{}", elem);
    }

    println!("===========================================");
    let mut iterator = (&v).into_iter();
    while let Some(element) = iterator.next(){
        println!("{}", element);
    }
    println!("Hello, world!");
}

输出如下:

There's:
antimony
arsenic
aluminum
tony
===========================================
antimony
arsenic
aluminum
tony
Hello, world!

for循环使用IntoIterator::into_inter 把 &v转成了一个迭代器,然后不断地执行 Iterator::next。当Iterator::next 返回Some(element)时,执行循环体的内容,如果Iterator::next 返回None,那么循环结束。

上面示范代码中,两种迭代的方式本质时一样的,上面方法for循环的本质就是下面的方法。

如果返回Iterator::next 返回None之后,继续调用next方法会发生什么?Iterator未定义这种行为,大部分迭代器只是会再次返回None,但也不是全部的迭代器都是如此。

创建迭代器

iter 和 iter_mut 方法

大多数的collection类型提供了iter和iter_mut方法,可以返回迭代器。切片如&[T]或者&str 也有iter和iter_mut方法。这种方法比较通用和自然的方法产生迭代器。

fn main() {
    let v = vec!["antimony", "arsenic", "aluminum", "tony"];
    let mut iterator = v.iter();
    assert_eq!(iterator.next(), Some(&"antimony"));
    assert_eq!(iterator.next(), Some(&"arsenic"));
    assert_eq!(iterator.next(), Some(&"aluminum"));
    assert_eq!(iterator.next(), Some(&"tony"));
}

std::path::Path 也实现了iter方法,每次产生一个路径部分。

use std::ffi::OsStr ;
use std::path::Path ;

fn main() {
    let path = Path::new("/etc/ceph/ceph.conf");
    let mut iterator = path.iter();

    assert_eq!(iterator.next(), Some(OsStr::new("/")));
    assert_eq!(iterator.next(), Some(OsStr::new("etc")));
    assert_eq!(iterator.next(), Some(OsStr::new("ceph")));
    assert_eq!(iterator.next(), Some(OsStr::new("ceph.conf")));
}

IntoIterator 实现

如果一个类型实现了IntoIterator,那么就可以通过调用 into_iter方法,转换成迭代器。

use std::collections::BTreeSet ;

fn main() {
    let mut favorite = BTreeSet::new();
    favorite.insert("Rust Programming".to_string());
    favorite.insert("Hello World".to_string());
    favorite.insert("ZFS internal".to_string());

    let mut it = favorite.into_iter();
    assert_eq!(it.next(), Some("Hello World".to_string()));
    assert_eq!(it.next(), Some("Rust Programming".to_string()));
    assert_eq!(it.next(), Some("ZFS internal".to_string()));
    assert_eq!(it.next(), None);
}

上面的BTreeSet类型实现了IntoIterator trait,所以可以调用into_iter方法。BTreeSet中的元素,以升序迭代,因此,顺序是:

  • Hello World
  • Rust Programming
  • ZFS internal

实际上,大部分的collections类型都实现了IntoIterator trait。

我们需要注意如下三种遍历方法:

for element in &collection {..}
for element in &mut collection {..}
for element in collection {..}

第一种是共享引用,第二种是可变引用,第三种是会consume collection,即会获得元素的所有权。

但是我们也要小心,不是所有的collection都提供三种实现,比如HashSet和BTreeSet,不支持可变引用。

drain 方法

很多collection提供了drain方法。这个方法对该类型使用可变应用 mutable reference,即执行完drain之后,原始的类型发生了变化。调用该方法后:

  • 返回一个新的迭代器
  • 原迭代器的内容发生了变化
use std::iter::FromIterator;

fn main() {
    let mut outer = "Earth".to_string();
    let inner = String::from_iter(outer.drain(1..4));

    assert_eq!(outer, "Eh");
    assert_eq!(inner, "art");

    let mut v = vec![1,2,3];
    let u: Vec<_> = v.drain(1..).collect();
    assert_eq!(v , &[1]);
    assert_eq!(u, &[2,3]);

    v.drain(..);
    assert_eq!(v, &[]);
}

##

]]>
Random in Rust 2020-04-01T10:29:00+00:00 Bean Li http://bean-li.github.io/randon-in-Rust 前言

本文介绍Rust中的随机数产生方法。Rust提供了rand crate来产生随机数。

产生标准类型的随机数

常见的类型:

  • i8
  • i32
  • i64
  • u8
  • u32
  • u64
  • bool
  • float

如何产生这些类型的随机数:

random方法

rand crate提供了一个快捷的方式产生随机数

use rand::random;

这个函数可以产生出各种类型的随机数:

extern crate rand ;
use rand::{random};

fn main() {

    let r_u8  :u8   = random();
    let r_u32 :u32  = random();
    let r_u64 :u64  = random();
    let r_f64 :f64  = random();

    println!("r_u8 {} r_u32 {} r_u64 {} r_f64 {}", r_u8, r_u32, r_u64, r_f64);

    let r_i8  = random::<i8>();
    let r_i32 = random::<i32>();
    let r_i64 = random::<i64>();
    let r_f32 = random::<f32>();

    println!("r_i8 {} r_i32 {} r_i64 {} r_f32 {}", r_i8, r_i32, r_i64, r_f32);

    if random()
    {
        println!("This print sometimes appears ,sometimes cannot be seen");
    }
}

输出结果如下:

manu-Inspiron-5748 CookBook/random ‹master*› » cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/random`
r_u8 108 r_u32 2741032365 r_u64 1387371133752336062 r_f64 0.46364384148921434
r_i8 -27 r_i32 -100204817 r_i64 -5883903312845635387 r_f32 0.24733853

manu-Inspiron-5748 CookBook/random ‹master*› » cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/random`
r_u8 13 r_u32 2994006999 r_u64 10822813997525460261 r_f64 0.8190726733233058
r_i8 99 r_i32 -674518030 r_i64 8391467083991742077 r_f32 0.6915216
This print sometimes appears ,sometimes cannot be seen

我们可以看到:

  • 每次执行,结果各不相同,即达到随机的效果。
  • random()函数可以产生出各种类型的随机值,包括bool型
  • random和random()产生出[0,1)范围内的浮点数

使用thread_rng函数

rand这个crate实现了Rng这个trait,这个trait的Rng的意思为 Random Number Generator,即随机数字生成器。该trait有如下重大的API:

  • gen
  • gen_range
  • sample
  • same_iter

我们可以利用gen函数来产生出我们需要的随机数。

extern crate rand ;
use rand::{Rng};

fn main() {

    let mut rng = rand::thread_rng();
    let r_u8  :u8   = rng.gen();
    let r_u32 :u32  = rng.gen();
    let r_u64 :u64  = rng.gen();
    let r_f64 :f64  = rng.gen();

    println!("r_u8 {} r_u32 {} r_u64 {} r_f64 {}", r_u8, r_u32, r_u64, r_f64);

    let r_i8  = rng.gen::<i8>();
    let r_i32 = rng.gen::<i32>();
    let r_i64 = rng.gen::<i64>();
    let r_f32 = rng.gen::<f32>();

    println!("r_i8 {} r_i32 {} r_i64 {} r_f32 {}", r_i8, r_i32, r_i64, r_f32);

    if rng.gen()
    {
        println!("This print sometimes appears ,sometimes cannot be seen");
    }
}

我们可以看到,和之前的random函数一样,通过Rng的gen函数,一样可以获取各种类型的随机数:

输出如下:
r_u8 80 r_u32 1687585909 r_u64 8352424927451894494 r_f64 0.8696927377548344
r_i8 -57 r_i32 -1884445722 r_i64 -4327157282855795895 r_f32 0.074106276

再跑一轮
r_u8 98 r_u32 2598551176 r_u64 16337455607562742807 r_f64 0.997604506929101
r_i8 114 r_i32 1535487133 r_i64 39920825847438157 r_f32 0.9148346
This print sometimes appears ,sometimes cannot be seen

Rng的gen函数不仅仅是可以产生出标准类型,还可以产生出tuple

let tuple: (u8, i32, char) = rng.gen(); 

如果在上面的代码最后加入如下两行:

    let tuple :(u8, i32, f64, bool) = rng.gen();
    println!("{:?}", tuple);

会多输入如下内容:

(242, -1061332895, 0.3170911846965502, false)

不仅可以产生tuple,数组也是一样:

    let array1 :[f32; 7] = rng.gen();
    println!("{:?}", array1)

上面代码产生7个f32类型的数组,数组中的数字是随机f32:

[0.07171655, 0.5189788, 0.68672246, 0.7315745, 0.1870473, 0.36728072, 0.66637975]

产生某范围的数字

上面章节是产生某类型的随机数字,有些时候需要产生某个范围内的某类型的随机数字。Rng提供了gen_range函数,负责此事。

使用gen_range

Rng的gen_range函数,可以获取某个范围内的随机数字,该函数接受两个参数,(low,high),其中low包含,high不包含, including low , excluding high。

extern crate rand ;
use rand::{Rng};

fn main() {
    let mut rng = rand::thread_rng();
    let num = rng.gen_range(0,10);
    let f_num = rng.gen_range(0.0,10.0);

    println!("num {} f_num {}", num, f_num);
}

输出结果如下:(多轮次结果)


num 7 f_num 2.52105525700977

num 9 f_num 6.242366880456077

num 9 f_num 7.333549347293831

num 7 f_num 5.24803166200549

如果需要反复地获取某个范围内的随机数字,处于效率的考量,我们最好采用下面方法:

使用Uniform

struct Uniform可以获取均匀分布的数值,效率要比反复调用gen_range高一些:

extern crate rand ;
use rand::distributions::{Distribution, Uniform};

fn main() {
    let mut rng = rand::thread_rng();
    let uniform = Uniform::from(1..7);

    loop {
        let throw = uniform.sample(&mut rng);
        println!("Roll the die : {}", throw);

        if throw == 6
        {
            break;
        }
    }
}

输出结果如下:

manu-Inspiron-5748 CookBook/random ‹master*› » cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/random`
Roll the die : 1
Roll the die : 3
Roll the die : 6
manu-Inspiron-5748 CookBook/random ‹master*› » cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/random`
Roll the die : 4
Roll the die : 5
Roll the die : 1
Roll the die : 3
Roll the die : 6

Uniform表示均匀分布,我们使用了from函数,除此外,Uniform还提供了new和new_inclusive两个方法:

pub fn new<B1, B2>(low: B1, high: B2) -> Uniform<X>
pub fn new_inclusive<B1, B2>(low: B1, high: B2) -> Uniform<X>

接受两个参数,low和high,区别在于是否包含high。

  • new :[low, high)
  • new_inclusive : [low, high]

我们可以用这两个方法,改造下我们的代码:

extern crate rand ;
use rand::distributions::{Distribution, Uniform};

fn main() {
    let mut rng = rand::thread_rng();
    let uniform = Uniform::new_inclusive(1,6);

    loop {
        let throw = uniform.sample(&mut rng);
        println!("Roll the die : {}", throw);

        if throw == 6
        {
            break;
        }
    }
}

输入结果如下:

manu-Inspiron-5748 CookBook/random ‹master*› » cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/random`
Roll the die : 4
Roll the die : 2
Roll the die : 3
Roll the die : 5
Roll the die : 2
Roll the die : 4
Roll the die : 3
Roll the die : 3
Roll the die : 2
Roll the die : 5
Roll the die : 5
Roll the die : 2
Roll the die : 6

关于 distribution

既然介绍到了随机,就不得不讲概率分布。概率分布是指,采样空间内的每一个值出现的概率。这个世界上并不是只有均匀分布(Uniform)一种,比如常见的正态分布。

标准分布:distributions::Standard

这个分布比较重要,实际上,Rng::gen就使用这个分布,这是产生很多类型随机数的默认分布:

  • integers : 均匀分布
  • char: 在整个Unicode内均匀分布
  • bool: 产生true or false,概率均为0.5
  • float:均匀分布在[0,1)范围内

Alphanumeric是一种特殊的Standard,它是在[a-zA-Z0-9]这个字符集上均匀分布。

extern crate rand ;
use rand::{Rng, thread_rng};
use rand::distributions::{Alphanumeric};

fn main() {
    let rand_string :String = thread_rng()
        .sample_iter(&Alphanumeric)
        .take(12)
        .collect();
    println!("rand_string : {}", rand_string)
}

产生出长度是12的随机字符:

rand_string : IZFJGQoCJFWc
rand_string : WYTW38x7lJ64

上面代码片段中,给出了sample_iter 。这是一个迭代器,如果我们想产生指定长度或者指定个数的随机数,我们可以使用这种方式:

均匀分布:Uniform

这种分布前面介绍很多了。我们需要记住,Standard分布和Uniform在很多场景下是一样的,而Standard分布并不是正态分布,切莫混淆即可。

伯努利分布 :Bernoulli

我们通过扔硬币,正反面出现的概率是相等的,即true or false 都是一半的概率。某些情况下,返回true和false的概率并不相等,比如我们希望1/3的概率返回True,2/3的概率返回true,rand提供了gen_bool函数:

extern crate rand ;
use rand::{Rng, thread_rng};

fn main() {
    let mut rng = thread_rng();

    for _ in 0..10{
        println!("{}", rng.gen_bool(1.0/4.0));
    }
}

输出结果如下:

manu-Inspiron-5748 CookBook/random ‹master*› » cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/random`
true
false
false
false
false
false
true
false
false
true

gen_bool函数,接受一个f64类型的参数:

fn gen_bool(&mut self, p: f64) -> bool

p的含义是,返回true的概率。该值的取值范围为[0,1]。

  • 当p = 0,表示永远返回false,
  • 当p = 1, 表示永远返回true
  • (0,1), p表示返回true的概率
]]>
Deref and DerefMut trait in Rust 2020-03-29T10:29:00+00:00 Bean Li http://bean-li.github.io/Deref-DerefMut Deref的基本概念

本文学习Rust中的Deref和DerefMut,通过实现Deref和DerefMut Trait,可以重载解引用运算符(*)。我非常喜欢上面描述中的重载二字,因为Rust中的Deref和DerefMut绝不是简简单单的解引用,而是允许我们强制隐式转换。

Deref和DerefMut trait的定义如下:

trait Deref{
	type Target: ?Sized;
  fn deref(&self)-> &Self::Target;
}

trait DerefMut: Deref{
  fn deref_mut(&mut self) -> &mut Self::Target;
}

注意上面的返回值,和Clone Trait不同,返回的类型的Target类型,这种关联类型,而不是Self。这就意味着通过Deref或者DerefMut,可以得到一个Self::Target 类型的变量,而这个Self::Target类型的变量,是和原类型有关联的,属于原类型的一个部分。

下面看一个例子:

struct Selector <T> {
	elements: Vec<T> ,
  current: usize 
}

use std::ops::{Deref, DerefMut} ;

impl<T> Deref for Selector<T>{
  type Target = T ;
  fn deref(&self)->&T {
    &self.elements[self.current]
  }
}

impl<T> DerefMut for Selector<T>{
  type Target=T ;
  fn deref_mut(&self)->&mut T{
    &mut self.elements[self.current]
  }
}

注意,Selector是一个结构体,但是解引用可以直接获得该结构体内的elements数组中的当前元素:

let mut s = Selector {elements: vec!['x', 'y','z'] ,               current:2};

assert_eq!(*s, 'z');
assert!(s.is_alphabetic());
*s = 'w';
assert_eq!(s.elements, ['x', 'y', 'w']);

如果我们不能理解通过Deref实现的类型转换,我们就不能理解,为什么s明明是Selector类型,为什么就和 ‘z’字符串相等了呢?

日常编程中的例子一:String 和 &str

很多内置的类型都实现了Deref,比如String类型。

fn main() {
    let a = "hello".to_string();
    let b = " world".to_string();

    let c = a + &b ;
    println!("{:?}", c);
}

注意,a 和 b都是String类型,当使用加号操作将字符串连接起来时,我们使用了&b,此时它应该是个&String类型,但是String类型实现的add方法的右值参数必须是&str类型。比如我们故意将&b其那面的&拿掉,报错信息如下:

   Compiling string v0.1.0 (/home/manu/CODE/Rust/CookBook/string)
error[E0308]: mismatched types
 --> src/main.rs:5:17
  |
5 |     let c = a + b ;
  |                 ^
  |                 |
  |                 expected &str, found struct `std::string::String`
  |                 help: consider borrowing here: `&b`
  |
  = note: expected type `&str`
             found type `std::string::String`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

从上述报错信息中,可以看出,右值期待一个&str类型。可是我们的b是String类型。我们提供了&String类型,为什么也没有报错呢。

原因是String类型实现了Deref<Target=str>:

impl ops::Deref for String{
	type Target = str ;
	fn deref(&self)->&str{
		unsafe{str::from_utf8_unchecked(&self.vec)
	}
}

正式因为String类型提供了这个,所以&String类型自动被隐式转化成了&str类型,所以,上面字符串的加法才得以正常的执行。

在此处,多啰嗦几句,来厘清String类型和str类型的区别。String的本质是Vec, 是分配在堆(heap)上的字节流,但是它保证它一定是一个有效的UTF-8序列。

&str本质是一个切片(slice),总是指向有效UTF-8序列的切片。注意,和其他切片一样,它是UnSized的,即不是固定大小的。和Trait object一样,需要胖指针。

String和&str的关系,就和Vec 和 &[T]的关系是一样的。

日常编程中的例子二: Vec 和 &[T]

Vec 和 切片&[T]的关系也是类似的。我们在此处多介绍下切片的背景知识。

[T] 不指定长度,称之为切片,它是数组或Vector的一段,因为Slice最大的问题在于它长度不一定,不是(Sized),因此,不能直接存储在变量里面,或者作为参数传给函数。因此它总是通过引用传递:

since a slice can by any length , slices cannot be stored derectly in variables or passed as function parameter. Slices are always passed by reference

因为Vec实现了Deref,所有用在Slices上的函数,都可以用在Vec上:

fn foo(s: &[i32]){
	println!("{:?}", s[0]);
}

fn main(){
	let v = vec![1,2,3];
	foo(&v);
}

因为Vec实现了Deref<Target=[T]>,所以 &Vec,会被自动转成&[T],所以foo函数才不会报错。自动解引用避免了开发者手动转换,简化编程。

]]>
AsRef and Borrow trait in Rust 2020-03-29T10:29:00+00:00 Bean Li http://bean-li.github.io/AsRef-and-Borrow-in-Rust AsRef

在Rust中,我们打开文件,经常使用如下接口:

let dot_emacs = std::fs::File::open(""/home/manu/.emacs")

注意,在std::fs的实现中,Struct File实现了open method,该method的定义如下:

pub fn open<P: AsRef<Path>>(path: P) -> Result<File>

open方法真正需要的参数是 &Path类型,可是我们知道,我们就是传个String类型或者str类型给它,类型也是匹配的,为何如此?

就在open声明中的 <P: AsRef> ,即任何实现了AsRef的类型,都OK。

  • String
  • str
  • OsString
  • OsStr

这些类型,都实现了AsRef,可以隐式地转成Path类型。当然了,Path和PathBuf更是可以作为参数传给open。

Borrow

Rust除了AsRef以外,还提供了一个Borrow的Trait,这两个Trait的意图非常接近,以至于,有人问是否是重复的 Borrow and AsRef are redundant #24140

trait AsRef<T: ?Sized>{
	fn as_ref(&self)-> &T ;
}

trait Borrow<Borrowed: ?Sized> {
  fm borrow(&self)-> &Borrowed ;
}

两者从定义上看,几乎是一模一样的,是多余重复么?

并非如此!

我们以String为例,String类型同时实现了

  • AsRef<&str>
  • AsRef<[u8]>
  • AsRef

当时这三种类型有不同的hash值,这三种中,只有&str类型保证hash值与String的hash值一致,因此String类型只实现了Borrow

从上面的讨论可以看出,Borrow通常用于HashMap和BTreeMap这种数据结构,两种类型的hash的值或者排序的值,必须是一样的,方能Borrow。所以要比AsRef要窄。

There are a few really important differences:

Borrow is used by data structures like HashMap and BTreeMap which assume that the result of hashing or ordering based on owned and borrowed values is the same. But there are many AsRef conversions for which this isn't true. So in general, Borrow should be implemented for fewer types than AsRef, because it makes stronger guarantees.
Borrow provides a blanket implementation T: Borrow<T>, which is essential for making the above collections work well. AsRef provides a different blanket implementation, basically &T: AsRef<U> whenever T: AsRef<U>, which is important for APIs like fs::open that can use a simpler and more flexible signature as a result. You can't have both blanket implementations due to coherence, so each trait is making the choice that's appropriate for its use case.
This should be better documented, but there's definitely a reason that things are set up this way.

下面我们以HashMap为例,考虑将String映射成i32,哈希表的键值是String,我们如何写查找函数?

下面是直觉之下的第一次尝试:

impl HashMap<K, V> where K: Eq + Hash 
{
    fn get(&self, key:K)-> Option<&V> {
    	...
    }
}

初看下来,还不错,但是细细一想,本次任务,我们需要String类型,用值传递,太奢侈浪费了,改成引用,得到第二版:

impl HashMap<K, V> where K: Eq + Hash 
{
    fn get(&self, key: &K)-> Option<&V> {
    	...
    }
}

第二个版本,比第一个版本是好一些了,但是我们想象,如果我们的get函数实现成这样,如果我们要使用get函数,我们的代码可能不得不写成这样:

hashtable.get(&"twenty-two".to_string())

我们不得不先分配String Buffer,然后将我们的常量字符串拷贝到堆(heap)的空间上,然后用把引用传给哈希表的get函数,然后drop掉这个String。

这个过程太荒谬,太浪费了。

  • 分配空间
  • 拷贝字符串
  • 执行drop,销毁String

这些个操作都太浪费了,我们明明有”twenty-two” 这个str。所以最终标准库里面的最终版本如下:

impl HashMap<K,V> where K: Eq  + Hash
{
	fn get<Q: ?Sized>(&self, key: &Q)->Option<&V>
	where K: Borrow<Q>,
				Q: Eq + Hash
	{...}
}

String 同时实现了Borrow和 Borrow,因此,get函数,无论是传&String类型,还是&str类型作为key,都是可以的,从而解决困境。

]]>