结构{structs},枚举{enums}和匹配{match}

目录

Rust 喜欢 move 它, move 它

我想稍微回退一下,给你看一些惊奇的东西:

// move1.rs
fn main() {
    let s1 = "hello dolly".to_string();
    let s2 = s1;
    println!("s1 {}", s1);
}

我们得到以下错误:

error[E0382]: use of moved value: `s1`
 --> move1.rs:5:22
  |
4 |     let s2 = s1;
  |         -- value moved here
5 |     println!("s1 {}", s1);
  |                      ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`,
  which does not implement the `Copy` trait

Rust 与其他语言有不同的行为。 其他语言的变量总是会引用{references} (如 Java 或 Python),s2成为对引用到s1的字符串对象的又一个引用。 在 C ++ 中,s1是一种值{value},它会 复制s2。 但 Rust 会移动该值。 它没有看到字符串{strings}是具有 可复制性的 (”没有实现 Copy trait”,也就是相应的复制方法,它并没有)。

我们不会看到像数字这样的”原始”类型不能复制,因为它们只是数值; 他们被允许复制,因为他们复制成本堪称便宜。 但,String是已经分配了包含”Hello dolly”的内存,而要复制这内容,将涉及分配更多内存还要复制字符{char}。Rust 才不会静悄悄地做这样的事情。

考虑一个String,它包含”Moby-Dick”的全文。 它不是一个很大的结构,只有文本的内存地址,以及它的大小以及分配块的大小。要复制这String会是昂贵的,因为该内存分配在堆上,和复制品也需要自己的内容分配区。

    String
    | addr | ---------> Call me Ishmael.....
    | size |                    |
    | cap  |                    |
                                |
    &str                        |
    | addr | -------------------|
    | size |

    f64
    | 8 bytes |

第二个值是一个字符串切片 (&str),它与第一个字符串指向相同的内存,再加个大小 - 它仅仅只是(地址)名字。便宜复制!

第三个值是一个f64- 只有 8 个字节。 它不涉及任何其他内存,所以它的复制和移动一样便宜。

复制{Copy}值只能通过它们在内存中的表示来定义,而当 Rust 拷贝时,它只是在其他地方复制这些字节。类似地, 一个没有复制{Copy}值{value}也是 只是移动了{moved}。 与 C ++ 不同的是, Rust 在复制和移动方面没有自作聪明。

译者: 对 具有引用的变量 隐形移动{move}该变量, 在 Rust 是错误的。

用函数调用重写,将显示完全相同的错误:

// move2.rs

fn dump(s: String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(s1);
    println!("s1 {}", s1); // <---error: 'value used here after move'
}

在这里,你有一个选择。 您可以传递对该字符串的引用{&},或者使用它的clone方法来明确拷贝。一般来说,第一种是更好的方法。

fn dump(s: &String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(&s1);
    println!("s1 {}", s1);
}

错误消失。 但你很少看到一个简朴String像这样的引用,因为传递一个字符串文字是非常丑陋的, 还要 涉及创建一个临时字符串。


# #![allow(unused_variables)]
#fn main() {
    dump(&"hello world".to_string());
#}

因此,声明该函数的最佳方式是:


# #![allow(unused_variables)]
#fn main() {
fn dump(s: &str) {
    println!("{}", s);
}
#}

那么, dump(&s1)dump("hello world")这两种情况都会 好好工作。 (这里就是Deref起的作用, Rust 会为你转换&String&str。 )

总而言之,非复制{non-Copy}的分配工作,会将 值 从一个位置移动{move}到另一个位置。不然的话,Rust 将被迫 隐式 做一个副本{copy},并打破 Rust 本身 明确分配 的承诺。

变量的范围

所以,经验法则是更愿意保留对原始数据的引用{&} - 以此来"借用{borrow}"它。

但,一个引用必须 不能 长命过拥有人{owner}!

首先, Rust 是一个 块范围的{block-scoped} 语言。 变量仅在其代码块持续时间内存在:


# #![allow(unused_variables)]
#fn main() {
{
    let a = 10;
    let b = "hello";
    {
        let c = "hello".to_string();
        // a,b 和 c 有
    }
    //  c 没有了
    // a,b  有
    for i in 0..a {
        let b = &b[1..];
        // 原来的 b 不再可见 - 它被罩住了。
    }
    //  b 没有了
    // i 没有了
}
#}

循环变量 (如i) 有点不同,它们只在循环代码块中可见。 创建一个使用相同名称的新变量并不是一个错误 ('覆盖') ,但它可能会造成混淆。

当一个变量’超出范围’,那么它会 扔掉了{dropped}。 任何使用的内存都会被回收,而该变量的其他 资源{resources} 将返回给系统 - 例如,扔掉一个文件{File},就等于关闭它。 这是一件好事。不用的资源在不需要时立即回收。

(另一个 Rust 的特色问题是,变量看起来可能在范围内,但其值已经是移动{move}了的。 )

这里有一个rs1,其引用到tmp值, 而引用只在其区块{}内存在:

01 // ref1.rs
02 fn main() {
03    let s1 = "hello dolly".to_string();
04    let mut rs1 = &s1;
05    {
06        let tmp = "hello world".to_string();
07        rs1 = &tmp; // <==
08    }
09    println!("ref {}", rs1);
10 }

我们先借用{borrow}s1值,然后再借用tmp值。但tmp在(05~08)区块之外就被扔掉了!

error: `tmp` does not live long enough
  --> ref1.rs:8:5
   |
7  |         rs1 = &tmp;
   |                --- borrow occurs here
8  |     }
   |     ^ `tmp` dropped here while still borrowed
9  |     println!("ref {}", rs1);
10 | }
   | - borrowed value needs to live until here

tmp哪里去了? 走了,死了,回到了天空中的堆中,故名: 扔掉了{dropeed}。 Rust 把你从 C 的可怕的’悬挂指针’问题中拯救出来,问题具体就是:一个指向陈旧数据的引用。

在 区块中, rs1-指向-> &tmp, 但在区块结束后, tmp 整个都被 扔掉了{dropped} , 这个时候 rs1 就变成一个指向陈旧(已扔掉)数据的引用。

元组

有时,从函数返回多个值,会非常有用。元组就是一个方便的解决方案:

// tuple1.rs

fn add_mul(x: f64, y: f64) -> (f64,f64) {
    (x + y, x * y)
}

fn main() {
    let t = add_mul(2.0,10.0);

    // 可以 调试打印
    println!("t {:?}", t);

    // 可以 给出值'索引'
    println!("add {} mul {}", t.0,t.1);

    // 可以 _提取_ 值
    let (add,mul) = t;
    println!("add {} mul {}", add,mul);
}
// t (12, 20)
// add 12 mul 20
// add 12 mul 20

元组能包含 不同 类型,这也是它与数组的主要区别。


# #![allow(unused_variables)]
#fn main() {
let tuple = ("hello", 5, 'c');

assert_eq!(tuple.0, "hello");
assert_eq!(tuple.1, 5);
assert_eq!(tuple.2, 'c');
#}

下面出现在一些迭代器{Iterator}方法。 enumerate就像同名的 Python 生成器(generator) 一样:


# #![allow(unused_variables)]
#fn main() {
    for t in ["zero","one","two"].iter().enumerate() {
        print!(" {} {};",t.0,t.1);
    }
    //  0 zero; 1 one; 2 two;
#}

zip会将两个迭代器,组合成一个 包含来自两者的值的元组 的迭代器:


# #![allow(unused_variables)]
#fn main() {
    let names = ["ten","hundred","thousand"];
    let nums = [10,100,1000];
    for p in names.iter().zip(nums.iter()) {
        print!(" {} {};", p.0,p.1);
    }
    //  ten 10; hundred 100; thousand 1000;
#}

结构{Structs}

元组很方便,但是要追踪每个部分的含义,t.1的这种写法不够直接与明了。

Rust 结构 就不同,它包含命名 字段{fields} :

// struct1.rs

struct Person {
    first_name: String,
    last_name: String
}

fn main() {
    let p = Person {
        first_name: "John".to_string(),
        last_name: "Smith".to_string()
    };
    println!("person {} {}", p.first_name,p.last_name);
}

虽然,不应该假定任何特定的内存布局,但是结构体的值将在内存中相邻放置,因为编译器是要高效,而不是节省大小的手段,来组织内存,哦,还有存在填充的可能。

初始化这个结构有点笨拙,所以我们想要把构造一个Person,融入其自身的函数。通过把它放进impl块, 这初始函数可以做成Person的一个 关联函数 :

// struct2.rs

struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

}

fn main() {
    let p = Person::new("John","Smith");
    println!("person {} {}", p.first_name,p.last_name);
}

这个new名字,没有什么魔力或其他东西,随你喜欢。要注意的是,它使用类似 C ++ 进行访问 - 使用双冒号的符号::

下面是个Person 方法,需要一个 自我引用{reference self} 参数:


# #![allow(unused_variables)]
#fn main() {
impl Person {
    ...

    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }

}
...
    println!("fullname {}", p.full_name());
// fullname John Smith
#}

明确使用该self,并作为引用传递。 (你可以把&self想成self: &Person简写。 )

还有,关键字Self(自身:注意首大写)指的是结构类型 - 你可以在脑海中用Person替换掉Self:


# #![allow(unused_variables)]
#fn main() {
    fn copy(&self) -> Self {
        Self::new(&self.first_name,&self.last_name)
    }
#}

方法可以允许修改数据, 用到 可变的自我{mutable self} 参数:


# #![allow(unused_variables)]
#fn main() {
    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }
#}

当使用简单的self参数时,数据会 移动{move}


# #![allow(unused_variables)]
#fn main() {
    fn to_tuple(self) -> (String,String) {
        (self.first_name, self.last_name)
    }
#}

(试试使用&self- 结构不会在没有过争斗的情况下,放开数据!)

注意,v.to_tuple()被调用之后,v已经移动并且不再可用。

总结:

  • 没有self相关参数: 您可以将函数与结构关联,如new“构造函数”。
  • &self参数: 可以使用结构体的值,但不能改变它们。
  • &mut self参数: 可以修改这些值。
  • self参数: 将消耗值,因它移动了。

如果您尝试对Person执行一个调试打印,你会得到一个信息错误:

error[E0277]: the trait bound `Person: std::fmt::Debug` is not satisfied
  --> struct2.rs:23:21
   |
23 |     println!("{:?}", p);
   |                     ^ the trait `std::fmt::Debug` is not implemented for `Person`
   |
   = note: `Person` cannot be formatted using `:?`; if it is defined in your crate,
    add `#[derive(Debug)]` or manually implement it
   = note: required by `std::fmt::Debug::fmt`

编译器提供建议,所以我们放了#[derive(Debug)]Person前面,现在有实用的输出:

Person { first_name: "John", last_name: "Smith" }

指示{directive} 注释会让编译器对Person生成一个 Debug 实现, 简单且有效。对于你的结构来说,这是一个很好的事情,简单加上一句注释,它们就可以打印出来。

译者:该指令注释,是有关 Rust 宏方面的知识,若想了解更多

这是最后的小程序:

// struct4.rs
use std::fmt;

#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

    fn full_name(&self) -> String {
        format!("{} {}",self.first_name, self.last_name)
    }

    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }

    fn to_tuple(self) -> (String,String) {
        (self.first_name, self.last_name)
    }
}

fn main() {
    let mut p = Person::new("John","Smith");

    println!("{:?}", p);

    p.set_first_name("Jane");

    println!("{:?}", p);

    println!("{:?}", p.to_tuple());
    // p has now moved.

}
// Person { first_name: "John", last_name: "Smith" }
// Person { first_name: "Jane", last_name: "Smith" }
// ("Jane", "Smith")

生命周期{Lifetimes}开始咬人啦

通常,结构体包含值,但通常它们还需要包含引用{&}。 假设我们想在一个结构中放置一个字符串切片{&str},而不是一个字符串值。

// life1.rs

#[derive(Debug)]
struct A {
    s: &str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}
error[E0106]: missing lifetime specifier
 --> life1.rs:5:8
  |
5 |     s: &str
  |        ^ expected lifetime parameter

为了理解编译器的投诉,你必须从 Rust 的角度看问题。

如果不知道一个‘引用’的生命周期,是不允许你存储它。 所有引用{&}都是从某个值那里借用{borrowed}的,而且所有的都是有生命周期{lifetimes}的。引用的生命周期不能长于该值的生命周期。Rust 不能允许这种 引用可能突然失效 的情况。

译者: 这时,你可以停一停了,好好想想上面这段话的含义,且自行概略如下问题的答案。 问:值 与 引用 的关系?

现在,字符串切片是从 字符串常量 借用的,像”hello”或是String值。 字符串常量在整个程序期间都存在,也称为”静态{static}”生命周期。

所以,下面写法是有效的 - 我们向 Rust 保证字符串切片,总是指向这静态{static}字符串:

// life2.rs

#[derive(Debug)]
struct A {
    s: &'static str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}
// A { s: "hello dammit" }

确实,这不是最 漂亮 符号,但有时丑,是精确的必要代价。

这也可以用来指明,从函数返回的字符串切片:


# #![allow(unused_variables)]
#fn main() {
fn how(i: u32) -> &'static str {
    match i {
    0 => "none",
    1 => "one",
    _ => "many"
    }
}
#}

这是静态字符串的特殊情况,但应该严格对待。

不过嘛,我们也可以指定引用{&}的生命周期,与结构本身 至少一样长

// life3.rs

#[derive(Debug)]
struct A <'a> { // 注意写法
    s: &'a str
}

fn main() {
    let s = "I'm a little string".to_string(); // string
    let a = A { s: &s }; // <== 结构

    println!("{:?}", a);
}

生命周期{Lifetimes}通常被称为’a’,’b’等,不过您也可以写’我{me}’,随你喜欢,自己知道且简洁就好。

之后看看main函数的内容,我们的a结构和s字符串受到严格的合同约束: a借用了s,并且不能长命过s

接下来,用这个 A 结构体定义,我们想写一个函数,它返回一个A值:


# #![allow(unused_variables)]
#fn main() {
fn makes_a() -> A {
    let string = "I'm a little string".to_string();
    A { s: &string }
}
#}

A 需要一个生命周期 - “要预期的生命周期参数{expected lifetime parameter}”:

  = help: this function's return type contains a borrowed value,
   but there is no value for it to be borrowed from
  = help: consider giving it a 'static lifetime

rustc提供建议,所以我们遵循它:


# #![allow(unused_variables)]
#fn main() {
fn makes_a() -> A<'static> {
    let string = "I'm a little string".to_string();
    A { s: &string }
}
#}

而现在的错误是

8 |      A { s: &string }
  |              ^^^^^^ does not live long enough
9 | }
  | - borrowed value only lives until here

这是无法安全工作的,因为string将在函数结束时被删除,并且引用不可以长命过string

您可以将生命周期参数,视为一个值类型的一部分,会有所帮助。

有时候,结构中包含一个值 从该值借用的引用,看,似乎是个好主意。 但,这基本上是不可能的,因为结构必须是 可移动的,而任何移动都将使引用无效。其实也没有必要这样做 - 例如,如果你的结构有一个字符串字段-string,并且还想要提供切片,那么,它完全可以保留索引,再加个方法,来生成实际的切片。

特点{Traits}

译者: Traits 的 中文意思名字有好几个,但,本质是: 定义结构的一系列行为/方法。

请注意 Rust 不会拼写struct 。 关键字在其他语言中是如此超载,意味着,它有效地击毙了原真的想法。

让我们这样说吧: Rust 结构不能 继承 来自其他结构; 他们都是独特的类型。 没有 sub-typing{子类型} 。他们都是愚蠢的数据.

所以,一个类型之间的关系又应该怎样 呢? 这正是 Traits 的作用。

rustc经常谈到实现{implementing} X 的特点{trait},所以现在恰是讨论 Traits 的时候了。

这里有一个定义 Traits 的例子, 帮特定类型去 实现 它。

// trait1.rs

trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("eight-byte float {}", self)
    }
}

fn main() {
    let answer = 42;
    let maybe_pi = 3.14;
    let s1 = answer.show();
    let s2 = maybe_pi.show();
    println!("show {}", s1);
    println!("show {}", s2);
}
// show four-byte signed 42
// show eight-byte float 3.14

它太酷了; 我们增加了i32f64两者泛型的 一种新方法 !

熟悉 Rust ,就要学习标准库的基本 trait (他们倾向于成群结队)。

非常普遍的有Debug。 我们给Person一个方便的默认实现,#[derive(Debug)],但,假如我们想要一个完整的Person-Debug 实现:


# #![allow(unused_variables)]
#fn main() {
use std::fmt;

impl fmt::Debug for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.full_name())
    }
}
...
    println!("{:?}", p);
    // John Smith
#}

write!是一个非常有用的宏 - 内部的f是实现了Write的东西。 (这也适用于File- 甚至是一个String. )

而,显示{Display}控制如何使用”{}”打印值,当然也要有对应的实现,就像Debug一样。 作为一个有用的副作用,任何实现了Display的,其ToString也自动可用。 所以,如果我们实现了PersonDisplay, p.to_string()也可用了。

Clone定义了clone方法,可简单用”#[deriv(Clone)]”进行定义,如果要所有的字段都实现Clone的话。

示例: 遍历浮点范围的迭代器

之前,我们已经遇到范围表达 (0..n) ,但它们不适用于浮点值。 ( 强行 去做,最终你会得到一个无趣的 1.0。 )

回想一下,迭代器的非正式定义; 它是一个带有结构体,具有一个可能会返回SomeNonenext方法。 在这个过程中,迭代器本身被修改,它保持迭代的状态 (如 next 索引等等)。 迭代的数据通常不会改变, (但,可以参阅Vec::drain,对于修改其数据的有趣迭代器)。

这里是正式的定义: 迭代器(Iterator) trait.


# #![allow(unused_variables)]
#fn main() {
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    ...
}
#}

我们在这里,看到了Iterator trait 的关联类型{associated type}。这个 trait 必须与任意类型合作,所以你必须以某种方式指定返回类型。 方法next可以在不使用特定类型的情况下编写 - 而是通过Self引用该类型参数的Item

f64的迭代器 trait ,是写入Iterator<Item=f64>,它可以理解为:”迭代器的关联类型 Item 设置为 f64”。

至于,...表达语句指的是Iterator提供的方法 。 你只需要定义Itemnext,那该表达语句就可为你所用。

// trait3.rs

struct FRange {
    val: f64,
    end: f64,
    incr: f64
}

fn range(x1: f64, x2: f64, skip: f64) -> FRange {
    FRange {val: x1, end: x2, incr: skip}
}

impl Iterator for FRange {
    type Item = f64;

    fn next(&mut self) -> Option<Self::Item> {
        let res = self.val;
        if res >= self.end {
            None
        } else {
            self.val += self.incr;
            Some(res)
        }
    }
}


fn main() {
    for x in range(0.0, 1.0, 0.1) {
        println!("{} ", x);
    }
}

而相当凌乱的结果是

0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999

这是因为 0.1 不能精确表示为一个浮点数,所以需要一些格式化帮助。 更换成println!


# #![allow(unused_variables)]
#fn main() {
println!("{:.1} ", x);
#}

我们得到更干净的输出 (这个格式的意思是’小数点后一位小数’。 ) 所有默认的迭代器方法都是可用,所以,我们可以将这些值收集到一个向量{Vec}中,通过 map方法来使用它们。等等。


# #![allow(unused_variables)]
#fn main() {
    let v: Vec<f64> = range(0.0, 1.0, 0.1).map(|x| x.sin()).collect();
#}

泛型函数

我们需要一个函数,来抛出实现了Debug的任何值。 以下是对泛型函数的第一次尝试,我们可以在其中传递一个 任何 值类型的引用。T是一个类型参数,需要在函数名称后面声明:


# #![allow(unused_variables)]
#fn main() {
fn dump<T> (value: &T) {
    println!("value is {:?}",value);
}

let n = 42;
dump(&n);
#}

但是, Rust 显然对这种泛型类型T一无所知:

error[E0277]: the trait bound `T: std::fmt::Debug` is not satisfied
...
   = help: the trait `std::fmt::Debug` is not implemented for `T`
   = help: consider adding a `where T: std::fmt::Debug` bound

为了这个工作, 需要告知 Rust ,这个T要实现Debug了的!


# #![allow(unused_variables)]
#fn main() {
fn dump<T> (value: &T)
where T: std::fmt::Debug {
    println!("value is {:?}",value);
}

let n = 42;
dump(&n);
// value is 42
#}

Rust 泛型函数需要 Traits bounds 类型 - 我们在这里说,”T 是实现了 Debug 的任意类型”。 rustc是非常有用的,并且确切地说明需要提供什么界限(bound)。

译者: Traits bounds (特征界限),本质上说: 参数的类型,约束 在,要是实现了对应的 Trait。

现在,Rust 知道这个T的 特征界限,它可以给你敏锐的编译器消息:


# #![allow(unused_variables)]
#fn main() {
struct Foo {
    name: String
}

let foo = Foo{name: "hello".to_string()};

dump(&foo)
#}

错误是:”Foo 没有实现 std::fmt::Debug trait”。

函数在动态语言中已经是泛型的,因为值会带有它们的实际类型,并且类型检查会在运行时发生 - 或者惨败。 对于较大的程序,我们确实想在编译时想知道问题! 这些语言的程序员不应平静地坐在编译器的错误之中,而必须处理程序运行时,才会出现的问题。 墨菲定律,告诉我们这些问题往往会发生在 最不方便/灾难性 的时刻。

平方数的操作函数是泛型的: x * x要适用整数,浮点数和任意知道关于乘法运算符*的类型。 但是,其类型界限又是什么?

// gen1.rs

fn sqr<T> (x: T) -> T {
    x * x
}

fn main() {
    let res = sqr(10.0);
    println!("res {}",res);
}

第一个问题是 Rust 不知道T可以做乘法:

error[E0369]: binary operation `*` cannot be applied to type `T`
 --> gen1.rs:4:5
  |
4 |     x * x
  |     ^
  |
note: an implementation of `std::ops::Mul` might be missing for `T`
 --> gen1.rs:4:5
  |
4 |     x * x
  |     ^

遵循编译器的建议,让我们使用这个 Traits限制该类型参数,这个 Traits 用来实现乘法运算符*:


# #![allow(unused_variables)]
#fn main() {
fn sqr<T> (x: T) -> T
where T: std::ops::Mul {
    x * x
}
#}

仍,不起作用:

rror[E0308]: mismatched types
 --> gen2.rs:6:5
  |
6 |     x * x
  |     ^^^ expected type parameter, found associated type
  |
  = note: expected type `T`
  = note:    found type `<T as std::ops::Mul>::Output`

rustc是说有关x * x的类型,是T::Output关联类型,而不是T。 实际上,x * xx类型没有道理是相同的,例如,两个向量的积是一个标量。


# #![allow(unused_variables)]
#fn main() {
fn sqr<T> (x: T) -> T::Output
where T: std::ops::Mul {
    x * x
}
#}

现在的错误是:

error[E0382]: use of moved value: `x`
 --> gen2.rs:6:7
  |
6 |     x * x
  |     - ^ value used here after move
  |     |
  |     value moved here
  |
  = note: move occurs because `x` has type `T`, which does not implement the `Copy` trait

所以,我们需要进一步限制类型!


# #![allow(unused_variables)]
#fn main() {
fn sqr<T> (x: T) -> T::Output
where T: std::ops::Mul + Copy {
    x * x
}
#}

(终于) 起作用了。要冷静地倾听编译器,每次都会让你更接近原力点,... 终会流畅编译。

确实, 在 C ++ 中, 更简单一点:

template <typename T>
T sqr(x: T) {
    return x * x;
}

但, (说实话) C ++ 在这里采用了牛仔策略。C ++ 的模板{template}错误很不好,因为,编译器都知道的所有, (最终) 是某些操作符或方法没有被定义。 C ++ 委员会知道这是一个问题,所以他们正在努力让concepts工作起来,这与 Rust 中的trait约束类型参数非常相似。

Rust 泛型函数,一开始可能看起来有点难接受,但是,显式,就是明确定义,就能确切地知道可以安全地提供哪种值。

这些函数是 单态{monomorphic} 调用的,与 多态{polymorphic} 合作。 函数的主体都会为每个 唯一类型 分别编译的。通过多态函数,相同的机器代码可以与每种匹配类型一起工作, 动态地 调度{dispatching} 正确的方法。

Monomorphic生成更快的代码,专用于特定类型,并且,常是 内联{inlined} 起来。所以,当sqr(x)被看到,它会被有效地用x * x取代。 缺点是,大的泛型函数为每一种可能导致的类型,产生大量的代码,引起 代码膨胀。但与往常一样,总是有折衷的方式; 有经验的人学会为工作,做出正确的选择。

简单的枚举

枚举{enums}类型具有一些确定的值。 例如,一个方向只有四个可能的值。(上下左右)


# #![allow(unused_variables)]
#fn main() {
enum Direction {
    Up,
    Down,
    Left,
    Right
}
...
    // `start` is type `Direction`
    let start = Direction::Left;
#}

可以在枚举上定义方法,就像结构一样。 该match表达语句是处理enum值的基本方式。


# #![allow(unused_variables)]
#fn main() {
impl Direction {
    fn as_str(&self) -> &'static str {
        match *self { // *self 有 Direction 类型
            Direction::Up => "Up",
            Direction::Down => "Down",
            Direction::Left => "Left",
            Direction::Right => "Right"
        }
    }
}
#}

标点符号很重要。 注意match后面的self之前的*。 很容易忘记,因为 Rust 经常会推断它 (我们说self.first_name,而不是(*self).first_name)。 但是,匹配{matching}是更精确的工作。若将它排除在外,会产生一大堆消息,这些消息可归结为这种类型的不匹配:

   = note: expected type `&Direction`
   = note:    found type `Direction`

这是因为self&Direction类型,所以我们必须投入* 遵循 该值。

像结构一样,枚举可以实现 traits,我们的朋友#[derive(Debug)],可以添加到Direction:


# #![allow(unused_variables)]
#fn main() {
        println!("start {:?}",start);
        // start Left
#}

所以,as_str方法并不是真的必要,因为我们总是可以从Debug得到名字。 (但as_str不分配{not allocate} ,这可能很重要。)

你不应该在这里,假设任何特定的顺序 - 这里没有默许的”起始”整数值。

这里有一个方法,来定义每个方向值的’后继者’。 非常方便的通配符用法,将枚举名称暂时放入方法上下文中:


# #![allow(unused_variables)]
#fn main() {
    fn next(&self) -> Direction {
        use Direction::*; // <===
        match *self {
            Up => Right,
            Right => Down,
            Down => Left,
            Left => Up
        }
    }
    ...

    let mut d = start;
    for _ in 0..8 {
        println!("d {:?}", d);
        d = d.next();
    }
    // d Left
    // d Up
    // d Right
    // d Down
    // d Left
    // d Up
    // d Right
    // d Down
#}

结果就是,这个特定的,任意的顺序中,各个方向一直循环。 它 (事实上)是非常简单的状态机器。

这些枚举值,无法比较:

assert_eq!(start, Direction::Left);

error[E0369]: binary operation `==` cannot be applied to type `Direction`
  --> enum1.rs:42:5
   |
42 |     assert_eq!(start, Direction::Left);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: an implementation of `std::cmp::PartialEq` might be missing for `Direction`
  --> enum1.rs:42:5

解决办法就是,在enum Direction前面加上#[derive(Debug,PartialEq)]

这是一个重点 - Rust 用户定义的类型一开始就是这么新鲜和朴素。

你通过实现共同的 traits ,给予他们合理的默认行为。这也适用于结构 - 如果你要求 Rust 为一个结构体 derive PartialEq,它会做出同样合理的事情,但要,假设所有的字段都实现它,并构建了一个对照结果。 如果不是这样,或者你想重新定义相等性质,那么你可以明确地自定义PartialEq

Rust 也有’C 风格的枚举’:

// enum2.rs

enum Speed {
    Slow = 10,
    Medium = 20,
    Fast = 50
}

fn main() {
    let s = Speed::Slow;
    let speed = s as u32;
    println!("speed {}", speed);
}

它们用一个整数值进行初始化,并可以通过类型转换(as),将其转换为整数。

你只需要给名字一个值,然,每次自动增加一个值:


# #![allow(unused_variables)]
#fn main() {
enum Difficulty {
    Easy = 1,
    Medium,  // is 2
    Hard   // is 3
}
#}

顺便说一下,枚举内字段的’名字’一词太模糊了,就像一直在说’物质’。 这里的合适名词,是 变种{variant} - Speed枚举有SlowMediumFast的变种。

这些枚举 有一个自然的顺序,但你必须问得好。在enum Speed前面放置#[derive(PartialEq,PartialOrd)]之后,Speed::Fast > Speed::SlowSpeed::Medium != Speed::Slow才是对的。

枚举的全部荣耀

完全形式的 rust 类似于类固醇上的 C 联盟,like a Ferrari compared to a Fiat Uno。考虑以 类型-安全的方式 存储不同值的问题。

// enum3.rs

#[derive(Debug)]
enum Value {
    Number(f64),
    Str(String),
    Bool(bool)
}

fn main() {
    use Value::*;
    let n = Number(2.3);
    let s = Str("hello".to_string());
    let b = Bool(true);

    println!("n {:?} s {:?} b {:?}", n,s,b);
}
// n Number(2.3) s Str("hello") b Bool(true)

同样,这个枚举只能包含这些值的 一个 ;其大小将是 最大变体 的大小。

到目前为止,并不是真正的超级跑车,虽然枚举知道如何打印出来是很酷的。 但,他们也知道它们包含的 哪一种 值,和 还有 match的超级力量:


# #![allow(unused_variables)]
#fn main() {
fn eat_and_dump(v: Value) {
    use Value::*;
    match v {
        Number(n) => println!("number is {}", n),
        Str(s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}
....
eat_and_dump(n);
eat_and_dump(s);
eat_and_dump(b);
//number is 2.3
//string is 'hello'
//boolean is true
#}

(而这就是OptionResult的本质 - 都是枚举。)

我们喜欢这个eat_and_dump函数,但我们希望将该值作为引用传递,因为当前移动{move}了,并且该值被’吃掉’了:


# #![allow(unused_variables)]
#fn main() {
fn dump(v: &Value) {
    use Value::*;
    match *v {  // type of *v is Value
        Number(n) => println!("number is {}", n),
        Str(s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}

error[E0507]: cannot move out of borrowed content
  --> enum3.rs:12:11
   |
12 |     match *v {
   |           ^^ cannot move out of borrowed content
13 |     Number(n) => println!("number is {}",n),
14 |     Str(s) => println!("string is '{}'",s),
   |         - hint: to prevent move, use `ref s` or `ref mut s`
#}

这次, 你无法处理借用引用。 Rust 不会让你 提取 包含在原始值中的字符串。 它没有抱怨Number,因为它很高兴复制f64,但是String是没有实现Copy的。

我之前提到过,match对精确 类型 是挑剔的,在这里,我们按照提示进行操作(加 ref); 现在,我们只是借用对包含字符串的引用。

译者: 据我了解,现 rustc 编译器已不再提示这个示例的错误,因它自行修正了此错误。2019.2.24


# #![allow(unused_variables)]
#fn main() {
fn dump(v: &Value) {
    use Value::*;
    match *v {
        Number(n) => println!("number is {}", n),
        Str(ref s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}
    ....

    dump(&s);
    // string is 'hello'
#}

在我们继续前进之前,感受下 Rust 编译成功的欣快感,也让我们暂停一下。rustc在生成足够上下文,以供人类使用非常优秀的 修正 错误,却不一定要 理解 错误。现在我们来理解下。

这个问题是 match 的正确性,以及 借用检查者阻止任何违反规则的企图的结合。 其中一条规则是你不能抽出所属某种拥有类型的值。 C ++ 的一些知识在这里是一个障碍,因为 C ++ 会用复制它的方式绕过这个问题,甚至还 说得通

如果你尝试从一个 Vec 中抽出一个字符串,你会得到完全相同的错误,也就是*v.get(0).unwrap() (因为索引返回的是引用,所以使用*。 ),而它不会让你这样做。 (有时在这种情况下,clone并不是一个坏的解决方案。)

(顺便一提,正是出于这个原因,v[0]不适用于像字符串这样的非可复制值。 你必须借用&v[0]或使用 v[0].clone() 复制来达到目的)

至于match,你可以看到Str(s)=>,其作为Str(s: String)=>的简称。 局部变量(通常称为一个 绑定 值 ) 被创建。 当你吃掉一个值,并提取其内容时,通常推断的类型是可行,但我们真正需要的是s: &String,而ref暗示,可以确保这一点: 我们只是想借用该字符串。

在这里,我们确实想提取该字符串,并且不关心之后的枚举值。 _像往常一样会匹配任何东西。


# #![allow(unused_variables)]
#fn main() {
impl Value {
    fn to_str(self) -> Option<String> {
        match self {
        Value::Str(s) => Some(s),
        _ => None
        }
    }
}
    ...
    println!("s? {:?}", s.to_str());
    // s? Some("hello")
    // println!("{:?}", s) // error! s has moved...
#}

函数命名很重要 - 这叫做to_str,而不是as_str。 你可以编写一个方法,借用该字符串,作为(as)一个Option<&String> (这个引用需要与 枚举变量 具有相同的生命周期。 ) ,这样,你就不能命名为to_str

你也可以写to_str - 它完全等价的:


# #![allow(unused_variables)]
#fn main() {
    fn to_str(self) -> Option<String> {
        if let Value::Str(s) = self {
            Some(s)
        } else {
            None
        }
    }
#}

关于匹配的 更多

回想一下,元组的值可以用’()’来提取:


# #![allow(unused_variables)]
#fn main() {
    let t = (10,"hello".to_string());
    ...
    let (n,s) = t;
    // t 已 移动了. 不再存在
    // n 是 i32, s 是 String
#}

这是一个 解构{destructuring} 特例; 我们有一些数据,希望将其分开来 (像这里) ,或只是借用它的值。无论哪种方式,我们都可以得到结构的各个部分。

语法与在match中使用的相似。 在这里,我们明确地借用了这些值。


# #![allow(unused_variables)]
#fn main() {
    let (ref n,ref s) = t;
    // n 和 s 从 t 那里借用. t 还存在!
    // n 是 &i32, s 是 &String
#}

解构与结构一起工作:


# #![allow(unused_variables)]
#fn main() {
    struct Point {
        x: f32,
        y: f32
    }

    let p = Point{x:1.0,y:2.0};
    ...
    let Point{x,y} = p;
    // p 还在, 直到 x 和 y 已复制
    // x 和 y 都是 f32
#}

下面时间,看看match的新模式。前两种模式与let解构相同 - 它只匹配第一个元素为零的元组,和一个 任何 字符串; 第二个模式增加了一个if,所以它只匹配(1, "hello")。 最后,只是一个匹配 任何 的 变量。但,如果match要应用一个表达式,而你不希望将变量绑定到该表达式,那会被忽略的_就会很有用,这是一个match结尾的常用方法。


# #![allow(unused_variables)]
#fn main() {
fn match_tuple(t: (i32,String)) {
    let text = match t {
        (0, s) => format!("zero {}", s),
        (1, ref s) if s == "hello" => format!("hello one!"),
        tt => format!("no match {:?}", tt),
        // 或 使用  _ => format!("no match")
        // 若你对变量不感兴趣。
     };
    println!("{}", text);
}
#}

为什么该函数不匹配match_tuple((1,"hello"))? 匹配是一个精确的工作,而编译器会抱怨:

  = note: expected type `std::string::String`
  = note:    found type `&'static str`

我们为什么需要ref s? 如果你有一个需要借用的if-守卫,这时存在个稍微隐晦的问题 (查找 E0008 错误),因为如果 if-守卫 是在不同的上下文中发生,就会发生移动。这是隐晦漏洞的示例情况。

译者 TODO: 添加 E0008 错误的中文翻译

如果类型 &str,那么我们直接匹配它:


# #![allow(unused_variables)]
#fn main() {
    match (42,"answer") {
        (42,"answer") => println!("yes"),
        _ => println!("no")
    };
#}

match用到if let的情况。这有个很酷的例子,因为如果我们得到一个Some,我们可以匹配里面的,只从元组中提取字符串。 所以在这里没有必要嵌套if let表达式。我们用_,因为我们对元组的第一部分不感兴趣。


# #![allow(unused_variables)]
#fn main() {
    let ot = Some((2, "hello".to_string());

    if let Some((_,ref s)) = ot {
        assert_eq!(s, "hello");
    }
    // 我们只是借用该字符串, 而不是 '不可挽回地破坏结构'
#}

使用parse时会出现一个有趣的问题 (或任何需要从上下文中,计算出其返回类型 的函数)


# #![allow(unused_variables)]
#fn main() {
    if let Ok(n) = "42".parse() {
        ...
    }
#}

那么,这n是什么类型的? 不管怎样,你必须提供一个提示 - 什么样的整数?它是否是一个整数?


# #![allow(unused_variables)]
#fn main() {
    if let Ok(n) = "42".parse::<i32>() {
        ...
    }
#}

这种不太优雅的语法被称为”涡轮运算符{turbofish operator}”.

如果你有正在返回Result的一个函数,那么问号运算符提供了一个更加优雅的解决方案:


# #![allow(unused_variables)]
#fn main() {
    let n: i32 = "42".parse()?;
#}

但是,解析错误需要转换为Result的错误变种,这是我们稍后讨论时要讨论的话题-6.错误处理.

闭包{Closures}

Rust 的很多力量来源于 闭包。 它们最简单的形式就像快捷函数一样:


# #![allow(unused_variables)]
#fn main() {
    let f = |x| x * x;

    let res = f(10);

    println!("res {}", res);
    // res 100
#}

在这个例子中没有明确的类型 - 一切都是从整数常量 10 ,开始推导出来的。

如果我们运行,会收到f具有不同类型的错误 - Rust 已经决定f必须在整数类型上调用:

    let res = f(10);

    let resf = f(1.2);
  |
8 |     let resf = f(1.2);
  |                  ^^^ expected integral variable, found floating-point variable
  |
  = note: expected type `{integer}`
  = note:    found type `{float}`

所以,第一次调用修复了参数的类型x。这相当于这个函数:


# #![allow(unused_variables)]
#fn main() {
    fn f (x: i32) -> i32 {
        x * x
    }
#}

但,函数和闭包之间存在很大差异,具体 体现 在明确类型的需要。 这里,我们先执行一个线性函数:


# #![allow(unused_variables)]
#fn main() {
    let m = 2.0;
    let c = 1.0;

    let lin = |x| m*x + c;

    println!("res {} {}", lin(1.0), lin(2.0));
    // res 3 5
#}

你不能用明确的fn形式 - 因它不知道闭包范围内的变量。闭包函数是从其上下文 借用了 mc

现在,这lin是什么类型? 只有rustc知道。 在引擎盖下,闭包是一个 结构 ,且是可调用的 (’实现调用操作符’) 。它的行为就好像这样写出来的:


# #![allow(unused_variables)]
#fn main() {
struct MyAnonymousClosure1<'a> {
    m: &'a f64,
    c: &'a f64
}

impl <'a>MyAnonymousClosure1<'a> {
    fn call(&self, x: f64) -> f64 {
        self.m * x  + self.c
    }
}
#}

当然,编译器就出来做事了,把简单的闭包语法变成完整的代码! 你需要知道的是,闭包为一个 结构 和它 借用 来自其环境的值。因此它有一个 lifetime

所有闭包都是独特的类型,但它们有共同的 traits。 所以即使我们不知道确切的类型,我们知道泛型约束:


# #![allow(unused_variables)]
#fn main() {
fn apply<F>(x: f64, f: F) -> f64
where F: Fn(f64)->f64  {
    f(x)
}
...
    let res1 = apply(3.0,lin);
    let res2 = apply(3.14, |x| x.sin());
#}

子曰: applyT这样的 任何 且具备Fn(f64) -> f64的类型工作 - 也就是说,这是一个需要f64并返回f64的函数。

运行apply(3.0,lin)后,试图访问lin会给出一个有趣的错误:

    let l = lin;
error[E0382]: use of moved value: `lin`
  --> closure2.rs:22:9
   |
16 |     let res = apply(3.0,lin);
   |                         --- value moved here
...
22 |     let l = lin;
   |         ^ value used here after move
   |
   = note: move occurs because `lin` has type
    `[closure@closure2.rs:12:15: 12:26 m:&f64, c:&f64]`,
     which does not implement the `Copy` trait

就是这样,apply吃了我们的闭包函数。 还有,这个结构的实际类型,rustc会弥补实现它。 始终,将闭包视为结构是有帮助的。

调用一个闭包就是一个 方法调用: 三种函数 trait 对应于三种方法:

  • Fn 结构传递为&self
  • FnMut 结构传递为&mut self
  • FnOnce 结构传递为self

所以,闭包可能会改变它的 来自上层 引用:


# #![allow(unused_variables)]
#fn main() {
    fn mutate<F>(mut f: F)
    where F: FnMut() {
        f()
    }
    let mut s = "world";
    mutate(|| s = "hello");
    assert_eq!(s, "hello");
#}

注意mut-f需要可变来工作.

但是,你无法逃避借用规则。考虑一下:


# #![allow(unused_variables)]
#fn main() {
let mut s = "world";

// 闭包搞了个 s 的 可变借用
let mut changer = || s = "world";

changer();
// 再搞个 s 不可变借用
assert_eq!(s, "world");
#}

无法完成! 错误是:在 assert 声明中,我们不能借用s,因为它之前作为可变借用,已经被闭包changer搞走了。 只要闭包存在,其他代码就不能访问s,所以解决方案是通过将闭包放在一个 有限的范围 内,来控制这个生命周期:


# #![allow(unused_variables)]
#fn main() {
let mut s = "world";
{
    let mut changer = || s = "world";
    changer();
}
assert_eq!(s, "world");
#}

在这一点上,如果你习惯了 JavaScript 或 Lua 等语言,你可能会感到 Rust 闭包的复杂性,而不是在这些语言中的直截了当。 这正是 Rust 承诺不作出任何分配的必要成本。 在 JavaScript 中,等效的mutate(function() {s = "hello";}),将始终,导致动态分配闭包。

有时,你不希望闭包借用这些变量,而是 移动 他们。


# #![allow(unused_variables)]
#fn main() {
    let name = "dolly".to_string();
    let age = 42;

    let c = move || {
        println!("name {} age {}", name,age);
    };

    c();

    println!("name {}",name);
#}

最后的错误println是: “使用了移动值: name“,所以这里有一个解决方案 - 如果我们 想保持 name活着 - 就将 复制的副本 移入闭包{move}中:


# #![allow(unused_variables)]
#fn main() {
    let cname = name.to_string();
    let c = move || {
        println!("name {} age {}",cname,age);
    };
#}

为什么需要移动的闭包? 因为我们可能需要在 原始上下文不再存在 的地方调用它们。 经典案例是创建一个 thread{线程}。 移动的闭包不借用,就没有生命周期。

移动后, 线程中, 所使用的变量, 就会与 原上下文 没有关系了。

迭代器方法中,主要使用闭包。 回想一下,我们定义的遍历一系列浮点数的range迭代器。使用闭包对此 (或任何其他迭代器) 进行操作都很简单:


# #![allow(unused_variables)]
#fn main() {
    let sine: Vec<f64> = range(0.0,1.0,0.1).map(|x| x.sin()).collect();
#}

map没有在 Vec 上定义 (尽管,很容易创建一个这样的 trait),因为那样的话, 每次 map 都将创建一个新的 Vec。就这样,选择很明显了。

这个sum,不存在创建临时对象:


# #![allow(unused_variables)]
#fn main() {
 let sum: f64 = range(0.0,1.0,0.1).map(|x| x.sin()).sum();
#}

它 (事实上) 会像明确的循环一样快! 如果 Rust 闭包与 Javascript 闭包一样”没有摩擦火花”,那么这种性能保证就不可能。

filter是另一种有用的迭代器方法 - 它只允许,通过匹配条件的值:


# #![allow(unused_variables)]
#fn main() {
    let tuples = [(10,"ten"),(20,"twenty"),(30,"thirty"),(40,"forty")];
    let iter = tuples.iter().filter(|t| t.0 > 20).map(|t| t.1);

    for name in iter {
        println!("{} ", name);
    }
    // thirty
    // forty
#}

三种迭代器

三种类型 (再次) 对应于三种基本参数类型。

假设我们有一个String值的 Vec 。以下是明确的迭代器类型,和 隐式{implicitly},以及迭代器返回的实际类型。


# #![allow(unused_variables)]
#fn main() {
for s in vec.iter() {...} // &String
for s in vec.iter_mut() {...} // &mut String
for s in vec.into_iter() {...} // String

// 隐式!
for s in &vec {...} // &String
for s in &mut vec {...} // &mut String
for s in vec {...} // String
#}

就我个人而言,我更喜欢明确,但,了解这两种形式及其含义是非常重要的。

into_iter 消耗 Vec ,并提取它的字符串,所以之后 Vec 不再可用 - 它已被移动。 这是 Pythonistas 过去常说的一个确定问题for s in vec!

所以,隐含的形式for s in &vec通常才是你想要的,就像&T在向函数传递参数时,是一个很好的默认值。

理解这三种类型是如何工作是很重要的,因为 Rust 严重依赖于类型推导 - 在闭包参数中,你不会经常看到明确的类型。 这是一件好事, 因为如果所有这些类型都明确的话, 它的 写法 会很嘈杂。 当然,这个紧凑的代码的代价,是你需要知道隐式类型究竟是什么!

map取得迭代器返回的任何值,并将其转换为其他值,但是filter需要的是一个该值的 引用。 在这种正在使用iter的情况下,迭代器 item 的类型是&String。 注意filter接收的是这种类型的引用.


# #![allow(unused_variables)]
#fn main() {
for n in vec.iter().map(|x: &String| x.len()) {...} // n 是 usize
....
}

for s in vec.iter().filter(|x: &&String| x.len() > 2) { // s 是 &String
...
}
#}

在调用方法(如:x.len())时, Rust 会自动 解引用,所以问题不明显。 但|x:&& String|x ==”one”|将 不会 工作, 因为操作符号对 类型匹配 更加严格。 rustc会抱怨&&String&str没有这样进行比较的。 所以你需要明确的 解引用 ,让&&String变成能 完成 比较 的&String


# #![allow(unused_variables)]
#fn main() {
for s in vec.iter().filter(|x: &&String| *x == "one") {...}
// 等价的隐式写法:
for s in vec.iter().filter(|x| *x == "one") {...}
#}

如果省略显式类型,则可以修改参数,使s的类型就是现在的&String:


# #![allow(unused_variables)]
#fn main() {
for s in vec.iter().filter(|&x| x == "one")
#}

看你如何看待它。

具有动态数据的结构

一个最强大的技术是 一个包含对自身引用的结构

这里是一个 二叉树 的基本构建块,用 C 语言 表示 (每个人最喜欢的老亲戚都喜欢使用没有保护的电动工具。 )


# #![allow(unused_variables)]
#fn main() {
    struct Node {
        const char *payload;
        struct Node *left;
        struct Node *right;
    };
#}

你不能 直接{directly} 在 Rust 这样做 - 包含Node字段,因为Node的大小取决于Node的大小... 它无法计算。 所以我们使用指针指向Node结构,因为指针的大小总是已知的。

如果left不是NULL,那Node将有一个left字段,其指向另一个节点,一直无限下去。

Rust 不会NULL (至少不 安全) , 所以这显然是一份Option的工作。 但你,不能只是把一个Node放在Option里面,因为我们不知道Node的大小 (等等)。 这又是Box的工作,因为它分配了包含一个指向数据的指针,并且一直具有固定大小。

所以这里是 Rust 的等价物,使用type创建一个别名:


# #![allow(unused_variables)]
#fn main() {
type NodeBox = Option<Box<Node>>;

#[derive(Debug)]
struct Node {
    payload: String,
    left: NodeBox,
    right: NodeBox
}
#}

( Rust 以这种方式解决问题 - 不需要前瞻性声明。 )

下面,第一个测试程序:

impl Node {
    fn new(s: &str) -> Node {
        Node{payload: s.to_string(), left: None, right: None}
    }

    fn boxer(node: Node) -> NodeBox {
        Some(Box::new(node))
    }

    fn set_left(&mut self, node: Node) {
        self.left = Self::boxer(node);
    }

    fn set_right(&mut self, node: Node) {
        self.right = Self::boxer(node);
    }

}


fn main() {
    let mut root = Node::new("root");
    root.set_left(Node::new("left"));
    root.set_right(Node::new("right"));

    println!("arr {:#?}", root);
}

由于”{:#?}” (’#’表示’扩开’) ,输出结果非常漂亮.

root Node {
    payload: "root",
    left: Some(
        Node {
            payload: "left",
            left: None,
            right: None
        }
    ),
    right: Some(
        Node {
            payload: "right",
            left: None,
            right: None
        }
    )
}

现在, root变量若被丢弃会发生什么 ? 所有字段都被删除; 如果树的”分支”被丢弃,就会扔掉 它们 的字段等等。 Box::new可能是最接近new关键字的呢,但我们没有必要delete要么free

我们现在必须为这棵树制定一个用法。请注意,可以指定字符串 顺序: ‘bar’<‘foo’,’abba’>’aardvark’; 所谓的”字母顺序”。 (严格来说,这是词汇顺序,因为人类语言非常多样化,并且有着奇怪的规则。)

这是一个按字符串的顺序,插入节点的方法。我们将新数据与当前节点进行比较 - 如果较少,则尝试插入左侧,否则尝试插入右侧。 左边可能没有节点,那么就set_left等等。

    fn insert(&mut self, data: &str) {
        if data < &self.payload {
            match self.left {
                Some(ref mut n) => n.insert(data),
                None => self.set_left(Self::new(data)),
            }
        } else {
            match self.right {
                Some(ref mut n) => n.insert(data),
                None => self.set_right(Self::new(data)),
            }
        }
    }

    ...
    fn main() {
        let mut root = Node::new("root");
        root.insert("one");
        root.insert("two");
        root.insert("four");

        println!("root {:#?}", root);
    }

注意match- 我们会提供一个可变的引用给到 box,如果OptionSome的话,并应用insert方法。 否则,我们需要为左侧创建一个新的Node等等。 Box是一个 聪明 指针; 请注意,不需要”拆箱{unboxing}”来呼叫Node方法!

这里是输出树:

root Node {
    payload: "root",
    left: Some(
        Node {
            payload: "one",
            left: Some(
                Node {
                    payload: "four",
                    left: None,
                    right: None
                }
            ),
            right: None
        }
    ),
    right: Some(
        Node {
            payload: "two",
            left: None,
            right: None
        }
    )
}

比,其他字符串’小于’的字符串放在左侧,,则放在右侧。

参观时间。 这是 按顺序遍历 - 我们访问左边,在节点上做点什么,然后访问右边。


# #![allow(unused_variables)]
#fn main() {
    fn visit(&self) {
        if let Some(ref left) = self.left {
            left.visit();
        }
        println!("'{}'", self.payload);
        if let Some(ref right) = self.right {
            right.visit();
        }
    }
    ...
    ...
    root.visit();
    // 'four'
    // 'one'
    // 'root'
    // 'two'
#}

所以,我们按顺序访问这些字符串! 请注意重复出现的ref - if let使用与match完全相同的规则。

泛型结构

考虑前面的二叉树的例子。 这将是 严重刺激 ,不得不重写它, 当为了所有可能的 payload 类型。 所以,我们的泛型Node与它的类型参数T.


# #![allow(unused_variables)]
#fn main() {
type NodeBox<T> = Option<Box<Node<T>>>;

#[derive(Debug)]
struct Node<T> {
    payload: T,
    left: NodeBox<T>,
    right: NodeBox<T>
}
#}

该实现显示了语言之间的差异。 payload 的基本操作是比较,所以 T 必须与之相当< ,等等, 实现 PartialOrd。 必须在impl其中声明类型参数:

impl <T: PartialOrd> Node<T> {
    fn new(s: T) -> Node<T> {
        Node{payload: s, left: None, right: None}
    }

    fn boxer(node: Node<T>) -> NodeBox<T> {
        Some(Box::new(node))
    }

    fn set_left(&mut self, node: Node<T>) {
        self.left = Self::boxer(node);
    }

    fn set_right(&mut self, node: Node<T>) {
        self.right = Self::boxer(node);
    }

    fn insert(&mut self, data: T) {
        if data < self.payload {
            match self.left {
                Some(ref mut n) => n.insert(data),
                None => self.set_left(Self::new(data)),
            }
        } else {
            match self.right {
                Some(ref mut n) => n.insert(data),
                None => self.set_right(Self::new(data)),
            }
        }
    }
}


fn main() {
    let mut root = Node::new("root".to_string());
    root.insert("one".to_string());
    root.insert("two".to_string());
    root.insert("four".to_string());

    println!("root {:#?}", root);
}

所以,泛型结构要像 C ++ 一样,需要在 <> 中指定泛型类型参数(们)。 Rust 通常很聪明,可以从上下文中得出这个类型参数 - 它知道它有一个Node<T>,还知道它的insert方法需要T参数。 insert 的第一次运行,会把T钉成为String。如果有任何进一步的运行不一致,它会投诉。

但是,你确实需要适当地限制这种类型!