1. 入坑rust

1.1 rust发展历程

  • 2006年,Mozilla 员工 “Graydon Hoare” 开发了Rust。
  • 2015年5月15日,Rust编程语言核心团队正式宣布发布Rust 1.0版本,之后连续4年,在Stack Overflow开发者「最受喜爱编程语言」评选中获得第一名。
  • 2019年7月4日,社交网络巨头Facebook联合其他100个行业巨头,对外宣布准备建设Libra(天秤座)项目。该项目的技术特点是:构建于区块链技术之上,并且基于Rust实现。
  • 2019年7月18日,微软安全响应中心(MSRC)发文宣称:我们需要更安全的系统编程语言。
  • 在suricata5.0上,rust已经成为一个必选组件,被用来编写应用层解析代码。

需要基于suricata开发项目的我,不得不入坑rust了。

1.2 rust安装

rustup 是rust官方的版本管理工具。应当作为安装 Rust 的首选。
在linux/mac下,运行一条命令就行了:

curl https://sh.rustup.rs -sSf | sh

但是由于众所周知的防火墙问题,我们需要配置一下中科大的源。

详细参考:https://zhuanlan.zhihu.com/p/26944087

2. Rust初印象

  • rust对标c/c++,是一个编译型系统级编程语言(可以用来编写操作系统的那种)。
  • Rust借用了C和C++的语法,它不允许空指针和悬挂指针,二者是C和C++中系统崩溃、内存泄露和不安全代码的根源。
  • Rust做到了内存安全而无需.NET和Java编程语言中实现自动垃圾收集器的开销,这是通过所有权/借用机制、生命周期、以及类型系统来达到的。
  • Rust 使用实现(implementation)、特征(trait)和结构化类型(structured type)而不是类(class)。这点,与基于继承的OO语言 C++, Java 有相当大的差异。

第一印象:

fn main() {
    println!("Hello, world!");
}

复杂一点的猜猜看游戏

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

对于熟悉c/c++的我,读起来还是挺通顺的。

3. rust语言基础

3.1 变量和可变性

变量默认是不可改变的(immutable),当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

比如这样是不对的,rust编译就会出错。

我们可以在变量名之前加 mut 来使其可变。

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

变量还有一个“隐藏“的玩法,这样我们就可以像python那样,”改变“一个变量的类型了。(注意不是真正改变了变量类型,而是隐藏了之前的变量)

let spaces = "   ";
let spaces = spaces.len();

3.2 数据类型

Rust是静态类型(staticallytyped)语言,也就是说在编译时就必须知道所有变量的类型。
比如,当我们想要将String转换为数字时,必须增加类型注解,

let guess = "42".parse().expect("Not a number!"); // 错误
let guess: u32 = "42".parse().expect("Not a number!"); // 正确

rust的原生数据类型(primitive types)包括:

// boolean type
let t = true;
let f: bool = false;

// char type
let c = 'c';

// numeric types
let x = 42;
let y: u32 = 123_456;
let z: f64 = 1.23e+2;
let zero = z.abs_sub(123.4);
let bin = 0b1111_0000;
let oct = 0o7320_1546;
let hex = 0xf23a_b049;

// string types
let str = "Hello, world!";
let mut string = str.to_string();

// arrays
let a = [0, 1, 2, 3, 4];

// tuples
let tuple: (i32, &str) = (50, "hello");

// raw pointers
let x = 5;
let raw = &x as *const i32;

// functions
fn plus_one(x: i32) -> i32 {
    x + 1
}

// structure
# struct User {
#     username: String,
#     email: String,
#     sign_in_count: u64,
#     active: bool,
# }
#
let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");

3.3 控制流

rust的控制流和C/C++很类似。

条件:if/else if/else

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

循环:loop、while 和 for
loop

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    assert_eq!(result, 20);
}

while

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    println!("LIFTOFF!!!");
}

for

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

4. 认识所有权

所有权(系统)是 Rust 最独特的功能,它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全。
所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!

4.1 作用域

{                      // s 在这里无效, 它尚未声明
    let s = "hello";   // 从此处起,s 是有效的

    // 使用 s
}                      // 此作用域已结束,s 不再有效

4.2 内存分配/释放

我们需要寻找一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。
Rust的字符串类型,String是被分配到堆上的,能够存储在编译时未知大小的文本.

let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 将打印 `hello, world!`

我们都知道,在c/c++中,分配到堆上的内存使用malloc/new,使用完需要释放free/delete。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。

{
    let s = String::from("hello"); // 从此处起,s 是有效的

    // 使用 s
}                                  // 此作用域已结束,
                                   // s 不再有效

4.3 堆变量的移动

我们来看一个堆变量移动的例子:

let s1 = String::from("hello");
let s2 = s1;

我们将 s1 赋值给 s2,String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据

Ruby语言和Rust语言 ruby rust_作用域


之前我们提到过当变量离开作用域后,Rust 自动调用 drop 函数并清理变量的堆内存。这就有了一个问题:当 s2 和 s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。

let s1 = String::from("hello");
let s2 = s1; // 这段代码执行之后,s1将不再能使用
println!("{}, world!", s1); // 错误

如下图所示,这种方式叫做变量“移动”。不是深拷贝也不是浅拷贝。

Ruby语言和Rust语言 ruby rust_Ruby语言和Rust语言_02

4.4 所有权与函数

将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

4.5 返回值与作用域

返回值也可以转移所有权

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中, 
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

4.6 引用

rust中的引用,类似c++中的应用,用&表示。

我们看一个例子

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

&s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。

Ruby语言和Rust语言 ruby rust_作用域_03

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  // 所以什么也不会发生

当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。

我们将获取引用作为函数参数称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

如果我们尝试修改借用的变量呢?剧透:这行不通!

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {  // & 表示引用(不可改变)
    some_string.push_str(", world");  // 错误
}

我们通过一个小调整就能修复代码中的错误:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);  
}

fn change(some_string: &mut String) { // &mut 就是可变引用啦
    some_string.push_str(", world"); // 正确
}

4.7 slice 部分引用

字符串 slice(string slice)是 String 中一部分值的引用,它看起来像这样:

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

Ruby语言和Rust语言 ruby rust_作用域_04

5. 项目管理–cargo

cargo是rust的项目管理器,用过node.js的猿们,应该对node.js中的神器npm、grunt、gulp等工具印象深刻。作为新一代静态语言中的翘楚,rust官方参考了现有语言管理工具的优点,于是就产生了cargo。

言而总之,作为rust的代码组织管理工具,cargo提供了一系列的工具,从项目的建立、构建到测试、运行直至部署,为rust项目的管理提供尽可能完整的手段。

5.1 包(package)

当运行 cargo new 时是在创建一个包(package)

$ cargo new hello_world --bin

$ cd hello_world
$ tree .
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

项目编译

$ cargo build

项目运行

$ cargo run
    Running `target/debug/hello_world`
Hello, world!

5.2 crate

Cargo 的约定是如果在代表包的 Cargo.toml 的同级目录下包含 src 目录且其中包含 main.rs 文件的话,Cargo 就知道这个包带有一个与包同名的二进制 crate,且 src/main.rs 就是 crate 根。

如果包(package)同时包含 src/main.rs 和 src/lib.rs,那么它带有两个 crate:一个库和一个二进制项目

5.3 模块(module)

随着工程的增大,把所有代码写在一个文件里面,是一件极其初等及愚蠢的作法。模块允许我们将代码组织起来,模块是几乎所有语言的基础设施,尽管叫法各有不同。

Rust 提供了一个关键字 mod,它可以在一个文件中定义一个模块,或者引用另外一个文件中的模块。

通常,我们会在单独的文件中写模块内容,然后使用 mod 关键字来加载那个文件作为我们的模块。

比如,我们在 src 下新建了文件 aaa.rs。现在目录结构是下面这样子:

foo
├── Cargo.toml
└── src
    └── aaa.rs
    └── main.rs

我们在 aaa.rs 中,写上:

pub fn print_aaa() {
    println!("{}", 25);
}

在 main.rs 中,写上:

mod aaa;

use aaa::print_aaa;

fn main () {
    print_aaa();
}

细心的朋友会发现,aaa.rs 中,没有使用 mod xxx {} 这样包裹起来,是因为 mod xxx; 相当于把 xxx.rs 文件用 mod xxx {} 包裹起来了。初学者往往会多加一层,请注意。

rust也支持层级的模块,这点和python很像。
详细参考:https://wiki.jikexueyuan.com/project/rust-primer/module/module.html

6. rust集合

就像C++的stl一样,Rust提供了一系列的基础且通用的容器类型。善用这些集合类型,可以让Rust编程更加方便轻松。

6.1 动态数组Vec

  • 和我们之前接触到的Array不同,Vec具有动态的添加和删除元素的能力,并且能够以O(1)的效率进行随机访问。
  • 同时,对其尾部进行push或者pop操作的效率也是平摊O(1)的。
  • 同时,Vec的所有内容项都是生成在堆空间上的,也就是说,你可以轻易的将Vec move出一个栈而不用担心内存拷贝影响执行效率——毕竟只是拷贝的栈上的指针。

来段示例代码:

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

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

用过c++的朋友,是不是很熟悉

6.2 哈希 map

和动态数组Vec一样,哈希表(HashMap)也是Rust内置的集合类型之一,同属std::collections模块下。
它提供了一个平均复杂度为O(1)的查询方法,是实现快速搜索必备的类型之一。

来段代码:

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name);

7. 错误处理

错误处理是保证程序健壮性的前提,在编程语言中错误处理的方式大致分为两种:抛出异常(exceptions)和作为值返回。

Rust 将错误作为值返回并且提供了原生的优雅的错误处理方案。

7.1 Result

熟练使用Result是编写 Rust 代码的关键,Rust 优雅的错误处理离不开值返回的错误形式,编写代码时提供给使用者详细的错误信息是值得推崇的。

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

T 和 E 是泛型类型参数;T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。
因为 Result 有这些泛型类型参数,我们可以将 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)
        },
    };
}

这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f。match 之后,我们可以利用这个文件句柄来进行读写。

match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自 panic! 宏的输出:

thread 'main' panicked at 'There was a problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12

来个更复杂的例子:

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("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
            },
            other_error => panic!("There was a problem opening the file: {:?}", other_error),
        },
    };
}

7.2 panic

Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。
panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。
Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。

在适当的场景使用 panic! 和 Result 将会使你的代码在面对不可避免的错误时显得更加可靠。

8. 模式匹配

模式匹配,多出现在函数式编程语言之中,为其复杂的类型系统提供一个简单轻松的解构能力。
rust的模式匹配依靠match和pattern两个关键字。

8.1 match

先来段match代码

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
        _ => println!("West"),
    };
}

有点类似c语言的switch对不对
rust的match,比c语言的switch要强大得多,比如match可以对现有的数据结构进行解构,轻易的可以拿出其中的数据部分来:

enum Action {
    Say(String),
    MoveTo(i32, i32),
    ChangeColorRGB(u16, u16, u16),
}

fn main() {
    let action = Action::Say("Hello Rust".to_string());
    match action {
        Action::Say(s) => {
            println!("{}", s);
        },
        Action::MoveTo(x, y) => {
            println!("point from (0, 0) move to ({}, {})", x, y);
        },
        Action::ChangeColorRGB(r, g, _) => {
            println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored",
                r, g,
            );
        }
    }
}

8.2 pattern

写rust代码,match和pattern更配哦
pattern对现有的数据结构进行解构,轻易的可以拿出其中的数据部分来

struct Point {
    x: i64,
    y: i64,
}
let point = Point { x: 0, y: 0 };
match point {
    Point { x, y } => println!("({},{})", x, y),
}

模式匹配中,当我想要匹配一个数字(字符)范围的时候,我们可以用…来表示:

let x = 1;

match x {
    1 ... 10 => println!("一到十"),
    _ => println!("其它"),
}

let c = 'w';

match c {
    'a' ... 'z' => println!("小写字母"),
    'A' ... 'Z' => println!("大写字母"),
    _ => println!("其他字符"),
}

当我们只是单纯的想要匹配多种情况的时候,可以使用 | 来分隔多个匹配条件

let x = 1;

match x {
    1 | 2 => println!("一或二"),
    _ => println!("其他"),
}


参考

https://www.rust-lang.org/zh-CN/learn

https://learnku.com/docs/rust-lang/2018

https://doc.rust-lang.org/stable/rust-by-example/

https://doc.rust-lang.org/cargo/index.html