当前位置:   article > 正文

Rust初识_rustc couldn't read ./hello.rs:

rustc couldn't read ./hello.rs:

Rust初识

Rust 是一门系统级编程语言,被设计为保证内存和线程安全,并防止段错误。作为系统级编程语言,它的基本理念是 “零开销抽象”。理论上来说,它的速度与 C / C++ 同级。

Rust 可以被归为通用的、多范式、编译型的编程语言,类似 C 或者 C++。与这两门编程语言不同的是,Rust 是线程安全的!

Rust 编程语言的目标是,创建一个高度安全和并发的软件系统。它强调安全性、并发和内存控制。尽管 Rust 借用了 C 和 C++ 的语法,它不允许空指针和悬挂指针,二者是 C 和 C++ 中系统崩溃、内存泄露和不安全代码的根源。

Rust 中有诸如 if else 和循环语句 for 和 while 的通用控制结构。和 C 和 C++ 风格的编程语言一样,代码段放在花括号中。

Rust 使用实现(implementation)、特征(trait)和结构化类型(structured type)而不是类(class)。这点,与基于继承的OO语言 C++, Java 有相当大的差异。而跟 Ocaml, Haskell 这类函数式语言更加接近。

Rust 做到了内存安全而无需 .NET 和 Java 编程语言中实现自动垃圾收集器的开销,这是通过所有权/借用机制、生命周期、以及类型系统来达到的。

Rust 运行在以下操作系统上:Linux, OS X, Windows, FreeBSD, Android, iOS。

1. 安装

https://www.rust-lang.org/zh-CN/other-installers.html

Rust的安装必须依赖官方源,但是国内速度很慢,有一些加速的办法。

中国科学技术大学镜像源包含 rust-static,梯子暂时出问题的同学可以尝试从这里下载编译器;除此之外。还有 Crates 源,详见这里的说明。

更多细节,参考之前的Rust绿色版教程。

2. 工具

目前没有最好的IDE,按照个人喜欢使用VSCode, Atom, Idea都可以。

rustup

rustup 是rust官方的版本管理工具。应当作为安装 Rust 的首选。
项目主页是: https://github.com/rust-lang-nursery/rustup.rs

管理安装多个官方版本的 Rust 二进制程序。
配置基于目录的 Rust 工具链。
安装和更新来自 Rust 的发布通道: nightly, beta 和 stable。
接收来自发布通道更新的通知。
从官方安装历史版本的 nightly 工具链。
通过指定 stable 版本来安装。
安装额外的 std 用于交叉编译。
安装自定义的工具链。
独立每个安装的 Cargo metadata。
校验下载的 hash 值。
校验签名 (如果 GPG 存在)。
断点续传。
只依赖 bash, curl 和常见 unix 工具。
支持 Linux, OS X, Windows(via MSYS2)。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

常用命令:

rustup default [toolchain] 配置默认工具链。
rustup show 显示当前安装的工具链信息。
rustup update 检查安装更新。
rustup toolchain [SUBCOMMAND] 配置工具链
rustup override [SUBCOMMAND] 配置一个目录以及其子目录的默认工具链
rustup target [SUBCOMMAND] 配置工具链的可用目标
rustup component 配置 rustup 安装的组件
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

racer

racer是一个由rust的爱好者提供的rust自动补全和语法分析工具,被用来提供基本的补全功能和定义跳转功能。其本身完全由rust写成,补全功能已经比较完善了。

cargo install racer

为了对Rust标准库进行补全,racer需要获取Rust源码路径。

设置名为RUST_SRC_PATH的环境变量为[path_to_your_rust_source]/src

Rust Langular Server (RLS)

Rust Langular Server(下文简称RLS)可以为很多IDE或编辑器提供包括不限于自动补全、跳转定义、重命名、跳转类型的功能支持。

cargo install rustfmt

3. 入门

3.1 Hello World

rust文件以.rs为后缀,下面给出一个典型的helloworld代码:

// hello.rs
fn main() {
    println!("Hello World!");
}
  • 1
  • 2
  • 3
  • 4

编译:rustc hello.rs -O #加入优化选项

解释:

  1. 第一行中fn 表示定义一个函数,main是这个函数的名字,花括号{}里的语句则表示这个函数的内容。
  2. 名字叫做main的函数有特殊的用途,那就是作为程序的入口,也就是说程序每次都从这个函数开始运行。
  3. 函数中只有一句println!("Hello World!");,这里println!是一个Rust语言自带的宏, 这个宏的功能就是打印文本(结尾会换行),而"Hello World!"这个用引号包起来的东西是一个字符串,就是我们要打印的文本。
  4. 你一定注意到了;吧, 在Rust语言中,分号用来把语句分隔开,也就是说语句的末尾一般用分号做为结束标志。

3.2 项目管理

Rust中一般使用cargo管理项目,比如:

  • 创建项目 cargo new hellorust --bin
  • 编译项目 cargo build --release
  • 运行项目 cargo run --release
  • 清理项目 cargo clean

3.3 变量

rust使用snake_case风格来命名函数,即所有字母小写并使用下划线类分隔单词,如:foo_bar

// 固定变量
let a1 = 5;
let a2:i32 = 2;
let a3 = 2.3f32; // 类似a2
assert_eq!(a1,a2);
// 可变变量
let mut b:f64 = 1.0;
b = 1.2;
let b = b; // 变为固定变量

// let高级用法
let (a, mut b): (bool,bool) = (true, false);
println!("a = {:?}, b = {:?}", a, b);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3.4 原生类型

Rust内置的原生类型 (primitive types) 有以下几类:

  • 布尔类型:bool, 有两个值truefalse
  • 字符类型:char, 表示单个Unicode字符,存储为4个字节。
  • 数值类型:分为有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 以及浮点数 (f32, f64)。
  • 字符串类型:最底层的是不定长类型str,更常用的是字符串切片&str和堆分配字符串String, 其中字符串切片是静态分配的,有固定的大小,并且不可变,而堆分配字符串是可变的。
  • 数组:Array,[T;N] 具有固定大小,并且元素都是同种类型。
  • 动态数组:Vec被表示为Vec<T>, 其中T是一个泛型。它不是基础类型,但是用的很广泛。
  • 切片:Slice, &[T] 或者&mut [T]引用一个数组的部分数据并且不需要拷贝。
  • 元组:Tuple, (1,true,"xxx")具有固定大小的有序列表,每个元素都有自己的类型,通过解构或者索引来获得每个元素的值。
  • 指针:最底层的是裸指针*const T*mut T,但解引用它们是不安全的,必须放到unsafe块里。
  • 函数:fn, 具有函数类型的变量实质上是一个函数指针。
  • 元类型:unit, 即(),其唯一的值也是()。类似void的作用。
// 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 and slices
let a = [0, 1, 2, 3, 4];
let middle = &a[1..4];
let mut ten_zeros: [i64; 10] = [0; 10];

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

// raw pointers
let x = 5;
let raw = &x as *const i32;
let points_at = unsafe { *raw };

// functions
fn foo(x: i32) -> i32 { x }
let bar: fn(i32) -> i32 = foo;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

有几点是需要特别注意的:

  • 数值类型可以使用_分隔符来增加可读性。
  • Rust还支持单字节字符b'H'以及单字节字符串b"Hello",仅限制于ASCII字符。 此外,还可以使用r#"..."#标记来表示原始字符串,不需要对特殊字符进行转义。
  • 使用&符号将String类型转换成&str类型很廉价, 但是使用to_string()方法将&str转换到String类型涉及到分配内存, 除非很有必要否则不要这么做。
  • 数组的长度是不可变的,动态的数组称为Vec (vector),可以使用宏vec!创建。
  • 元组可以使用==!=运算符来判断是否相同。
  • 不多于32个元素的数组和不多于12个元素的元组在值传递时是自动复制的。
  • Rust不提供原生类型之间的隐式转换,只能使用as关键字显式转换。
  • 可以使用type关键字定义某个类型的别名,并且应该采用驼峰命名法。
// explicit conversion
let decimal = 65.4321_f32;
let integer = decimal as u8;
let character = integer as char;

// type aliases
type NanoSecond = u64;
type Point = (u8, u8);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.5 数组与字符串

数组 array

Rust 使用数组存储相同类型的数据集。 [T; N]表示一个拥有 T 类型,N 个元素的数组。数组的大小是固定。

fn main() {
    let mut array: [i32; 3] = [0; 3];

    array[1] = 1;
    array[2] = 2;

    assert_eq!([1, 2], &array[1..]);

    // This loop prints: 0 1 2
    for x in &array {
        println!("{} ", x);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

动态数组 Vec

动态数组是一种基于堆内存申请的连续动态数据类型,拥有 O(1) 时间复杂度的索引、压入(push)、弹出(pop)。

//创建空Vec
let v: Vec<i32> = Vec::new();
//使用宏创建空Vec
let v: Vec<i32> = vec![];
//创建包含5个元素的Vec
let v = vec![1, 2, 3, 4, 5];
//创建十个零
let v = vec![0; 10];
//创建可变的Vec,并压入元素3
let mut v = vec![1, 2];
v.push(3);
//创建拥有两个元素的Vec,并弹出一个元素
let mut v = vec![1, 2];
let two = v.pop();
//创建包含三个元素的可变Vec,并索引一个值和修改一个值
let mut v = vec![1, 2, 3];
let three = v[2];
v[1] = v[1] + 5;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

字符串

Rust 里面有两种字符串类型。Stringstr

  • &str, str 类型基本上不怎么使用,通常使用 &str 类型,它其实是[u8] 类型的切片形式 &[u8]。这是一种固定大小的字符串类型。 常见的的字符串字面值就是 &'static str类型。这是一种带有 'static生命周期的&str 类型。

    // 字符串字面值
    let hello = "Hello, world!";
    
    // 附带显式类型标识
    let hello: &'static str = "Hello, world!";
    • 1
    • 2
    • 3
    • 4
    • 5
  • String 是一个带有的 vec:Vec 成员的结构体,你可以理解为 str 类型的动态形式。 它们的关系相当于 [T] 和 Vec 的关系。 显然 String 类型也有压入和弹出。

    // 创建一个空的字符串
    let mut s = String::new();
    // 从 `&str` 类型转化成 `String` 类型
    let mut hello = String::from("Hello, ");
    // 压入字符和压入字符串切片
    hello.push('w');
    hello.push_str("orld!");
    
    // 弹出字符。
    let mut s = String::from("foo");
    assert_eq!(s.pop(), Some('o'));
    assert_eq!(s.pop(), Some('o'));
    assert_eq!(s.pop(), Some('f'));
    assert_eq!(s.pop(), None);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

3.6 结构体与枚举

// structs
struct Point {
  x: i32,
  y: i32,
}
let point = Point { x: 0, y: 0 };

// tuple structs
struct Color(u8, u8, u8);
let android_green = Color(0xa4, 0xc6, 0x39);
let Color(red, green, _) = android_green; // 这个对一般的语言差异比较大

// A tuple struct’s constructors can be used as functions.
struct Digit(i32);
let v = vec![0, 1, 2];
let d: Vec<Digit> = v.into_iter().map(Digit).collect();

// newtype: a tuple struct with only one element
struct Inches(i32);
let length = Inches(10);
let Inches(integer_length) = length;

// unit-like structs
struct EmptyStruct;
let empty = EmptyStruct;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

一个包含..的struct可以用来从其它结构体拷贝一些值或者在解构时忽略一些域:

struct Point3d {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point3d::default();
let point = Point3d { y: 1, ..origin };
let Point3d { x: x0, y: y0, .. } = point;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

需要注意,Rust在语言级别不支持域可变性 (field mutability),所以不能这么写:

struct Point {
    mut x: i32,
    y: i32,
}
  • 1
  • 2
  • 3
  • 4

这是因为可变性是绑定的一个属性,而不是结构体自身的。可以使用Cell来模拟:

use std::cell::Cell;
struct Point {
    x: i32,
    y: Cell<i32>,
}
let point = Point { x: 5, y: Cell::new(6) };
point.y.set(7);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

此外,结构体的域默认是私有的,可以使用pub关键字将其设置成公开。

3.7 枚举

Rust有一个集合类型,称为枚举 (enum),代表一系列子数据类型的集合。 其中子数据结构可以为空-如果全部子数据结构都是空的,就等价于C语言中的enum。 我们需要使用::来获得每个元素的名称。

// enums
enum Message {
    Quit,
    ChangeColor(i32, i32, i32),
    Move { x: i32, y: i32 },
    Write(String),
}
let x: Message = Message::Move { x: 3, y: 4 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

与结构体一样,枚举中的元素默认不能使用关系运算符进行比较 (如==, !=, >=), 也不支持像+和*这样的双目运算符,需要自己实现,或者使用match进行匹配。

枚举默认也是私有的,如果使用pub使其变为公有,则它的元素也都是默认公有的。 这一点是与结构体不同的:即使结构体是公有的,它的域仍然是默认私有的。 此外,枚举和结构体也可以是递归的 (recursive)。

3.8 控制流

Rust有if for while match四种流逻辑控制,但是与传统的有很大区别:

If

If是分支 (branch) 的一种特殊形式,也可以使用else和else if。 与C语言不同的是,逻辑条件不需要用小括号括起来,但是条件后面必须跟一个代码块。 Rust中的if是一个表达式 (expression),可以赋给一个变量:

let x = 5;
let y = if x == 5 { 10 } else { 15 };
  • 1
  • 2

Rust是基于表达式的编程语言,有且仅有两种语句 (statement):

  • 声明语句 (declaration statement),比如进行变量绑定的let语句。
  • 表达式语句 (expression statement),它通过在末尾加上分号;来将表达式变成语句, 丢弃该表达式的值,一律返回unit()。

表达式如果返回,总是返回一个值,但是语句不返回值或者返回(),所以以下代码会报错:

let y = (let x = 5);
let z: i32 = if x == 5 { 10; } else { 15; };
  • 1
  • 2

值得注意的是,在Rust中赋值 (如x = 5) 也是一个表达式,返回unit的值()

For

Rust中的for循环与C语言的风格非常不同,抽象结构如下:

for var in expression {
    code
}
  • 1
  • 2
  • 3

其中expression是一个迭代器 (iterator),具体的例子为0..10 (不包含最后一个值), 或者[0, 1, 2].iter()

for (i,j) in (5..10).enumerate() {
    println!("i = {} and j = {}", i, j);
}
  • 1
  • 2
  • 3

While&Loop

Rust中的while循环与C语言中的类似。对于无限循环,Rust有一个专用的关键字loop。 如果需要提前退出循环,可以使用关键字break或者continue, 还允许在循环的开头设定标签 (同样适用于for循环):

loopwhile true 的主要区别在编译阶段的静态分析。

// while
let mut x = 5; // mut x: i32
let mut done = false; // mut done: bool

while !done {
    x += x - 3;

    println!("{}", x);

    if x % 5 == 0 {
        done = true;
    }
}

// loop & label
'outer: loop {
   println!("Entered the outer loop");

   'inner: loop {
       println!("Entered the inner loop");
       break 'outer; // 跳出`outer标签所在的循环
   }
   println!("This point will never be reached");
}
println!("Exited the outer loop");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

Match

Rust中的match表达式非常强大,首先看一个例子:

let day = 5;
match day {
  0 | 6 => println!("weekend"),
  1 ... 5 => println!("weekday"),
  _ => println!("invalid"),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

其中|用于匹配多个值,...匹配一个范围 (包含最后一个值),并且_在这里是必须的, 因为match强制进行穷尽性检查 (exhaustiveness checking),必须覆盖所有的可能值。 如果需要得到|或者...匹配到的值,可以使用@绑定变量:

let x = 1;
match x {
    e @ 1 ... 5 => println!("got a range element {}", e),
    _ => println!("anything"),
}
  • 1
  • 2
  • 3
  • 4
  • 5

使用ref关键字来得到一个引用:

let x = 5;
let mut y = 5;
match x {
    // the `r` inside the match has the type `&i32`
    ref r => println!("Got a reference to {}", r),
}
match y {
    // the `mr` inside the match has the type `&i32` and is mutable
    ref mut mr => println!("Got a mutable reference to {}", mr),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

再看一个使用match表达式来解构元组的例子:

let pair = (0, -2);
match pair {
    (0, y) => println!("x is `0` and `y` is `{:?}`", y),
    (x, 0) => println!("`x` is `{:?}` and y is `0`", x),
    _ => println!("It doesn't matter what they are"),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

match的这种解构同样适用于结构体或者枚举。如果有必要,还可以使用..来忽略域或者数据:

struct Point {
    x: i32,
    y: i32,
}
let origin = Point { x: 0, y: 0 };
match origin {
    Point { x, .. } => println!("x is {}", x),
}
enum OptionalInt {
    Value(i32),
    Missing,
}
let x = OptionalInt::Value(5);
match x {
    // 这里是 match 的 if guard 表达式,我们将在以后的章节进行详细介绍
    OptionalInt::Value(i) if i > 5 => println!("Got an int bigger than five!"),
    OptionalInt::Value(..) => println!("Got an int!"),
    OptionalInt::Missing => println!("No such luck."),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

此外,Rust还引入了if letwhile let进行模式匹配:

let number = Some(7);
let mut optional = Some(0);
// If `let` destructures `number` into `Some(i)`, evaluate the block.
if let Some(i) = number {
    println!("Matched {:?}!", i);
} else {
    println!("Didn't match a number!");
}

// While `let` destructures `optional` into `Some(i)`, evaluate the block.
while let Some(i) = optional {
    if i > 9 {
        println!("Greater than 9, quit!");
        optional = None;
    } else {
        println!("`i` is `{:?}`. Try again.", i);
        optional = Some(i + 1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3.9 函数与方法

在rust中,任何函数都有返回类型,当函数返回时,会返回一个该类型的值。
当一个函数返回()时,可以省略。
rust也有return关键字,不过一般用于提前返回。
rust的函数不支持多返回值,但是我们可以利用元组来返回多个值
发散函数(diverging function)是rust中的一个特性。发散函数不返回,它使用感叹号!作为返回类型,一般用来处理错误,终止

函数

要声明一个函数,需要使用关键字fn,后面跟上函数名,比如

fn add_one(x: i32) -> i32 {
    x + 1
}
  • 1
  • 2
  • 3

其中函数参数的类型不能省略,可以有多个参数,但是最多只能返回一个值, 提前返回使用return关键字。Rust编译器会对未使用的函数提出警告, 可以使用属性#[allow(dead_code)]禁用无效代码检查。

Rust有一个特殊特性适用于发散函数 (diverging function),它不返回:

fn diverges() -> ! {
    panic!("This function never returns!");
}
  • 1
  • 2
  • 3

其中panic!是一个宏,使当前执行线程崩溃并打印给定信息。返回类型!可用作任何类型:

let x: i32 = diverges();
let y: String = diverges();
  • 1
  • 2

匿名函数

Rust使用闭包 (closure) 来创建匿名函数:

let num = 5;
let plus_num = |x: i32| x + num;
  • 1
  • 2

其中闭包plus_num借用了它作用域中的let绑定num。如果要让闭包获得所有权, 可以使用move关键字:

let mut num = 5;
{
    let mut add_num = move |x: i32| num += x;   // 闭包通过move获取了num的所有权
    add_num(5);
}
// 下面的num在被move之后还能继续使用是因为其实现了Copy特性
// 具体可见所有权(Owership)章节
assert_eq!(5, num);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

高阶函数

Rust 还支持高阶函数 (high order function),允许把闭包作为参数来生成新的函数:

fn add_one(x: i32) -> i32 { x + 1 }
fn apply<F>(f: F, y: i32) -> i32
    where F: Fn(i32) -> i32
{
    f(y) * y
}

fn factory(x: i32) -> Box<Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

fn main() {
    let transform: fn(i32) -> i32 = add_one;
    let f0 = add_one(2i32) * 2;
    let f1 = apply(add_one, 2);
    let f2 = apply(transform, 2);
    println!("{}, {}, {}", f0, f1, f2);

    let closure = |x: i32| x + 1;
    let c0 = closure(2i32) * 2;
    let c1 = apply(closure, 2);
    let c2 = apply(|x| x + 1, 2);
    println!("{}, {}, {}", c0, c1, c2);

    let box_fn = factory(1i32);
    let b0 = box_fn(2i32) * 2;
    let b1 = (*box_fn)(2i32) * 2;
    let b2 = (&box_fn)(2i32) * 2;
    println!("{}, {}, {}", b0, b1, b2);

    let add_num = &(*box_fn);
    let translate: &Fn(i32) -> i32 = add_num;
    let z0 = add_num(2i32) * 2;
    let z1 = apply(add_num, 2);
    let z2 = apply(translate, 2);
    println!("{}, {}, {}", z0, z1, z2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

方法

Rust通过impl关键字在struct、enum或者trait对象上实现方法调用语法 (method call syntax)。 关联函数 (associated function) 的第一个参数通常为self参数,有3种变体:

  • self,允许实现者移动和修改对象,对应的闭包特性为FnOnce。
  • &self,既不允许实现者移动对象也不允许修改,对应的闭包特性为Fn。
  • &mut self,允许实现者修改对象但不允许移动,对应的闭包特性为FnMut。

不含self参数的关联函数称为静态方法 (static method)。

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn new(x: f64, y: f64, radius: f64) -> Circle {
        Circle {
            x: x,
            y: y,
            radius: radius,
        }
    }

    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

fn main() {
    let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
    println!("{}", c.area());

    // use associated function and method chaining
    println!("{}", Circle::new(0.0, 0.0, 2.0).area());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

3.10 特性与接口

为了描述类型可以实现的抽象接口 (abstract interface), Rust引入了特性 (trait) 来定义函数类型签名 (function type signature):

trait HasArea {
    fn area(&self) -> f64;
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

其中函数print_area()中的泛型参数T被添加了一个名为HasArea的特性约束 (trait constraint), 用以确保任何实现了HasArea的类型将拥有一个.area()方法。 如果需要多个特性限定 (multiple trait bounds),可以使用+

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn bar<T, K>(x: T, y: K)
    where T: Clone,
          K: Clone + Debug
{
    x.clone();
    y.clone();
    println!("{:?}", y);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

其中第二个例子使用了更灵活的where从句,它还允许限定的左侧可以是任意类型, 而不仅仅是类型参数。

定义在特性中的方法称为默认方法 (default method),可以被该特性的实现覆盖。 此外,特性之间也可以存在继承 (inheritance):

trait Foo {
    fn foo(&self);

    // default method
    fn bar(&self) { println!("We called bar."); }
}

// inheritance
trait FooBar : Foo {
    fn foobar(&self);
}

struct Baz;

impl Foo for Baz {
    fn foo(&self) { println!("foo"); }
}

impl FooBar for Baz {
    fn foobar(&self) { println!("foobar"); }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

如果两个不同特性的方法具有相同的名称,可以使用通用函数调用语法 (universal function call syntax):

// short-hand form
Trait::method(args);

// expanded form
<Type as Trait>::method(args);
  • 1
  • 2
  • 3
  • 4
  • 5

关于实现特性的几条限制:

  • 如果一个特性不在当前作用域内,它就不能被实现。
  • 不管是特性还是impl,都只能在当前的包装箱内起作用。
  • 带有特性约束的泛型函数使用单态化实现 (monomorphization), 所以它是静态派分的 (statically dispatched)。

下面列举几个非常有用的标准库特性:

  • Drop提供了当一个值退出作用域后执行代码的功能,它只有一个drop(&mut self)方法。
  • Borrow用于创建一个数据结构时把拥有和借用的值看作等同。
  • AsRef用于在泛型中把一个值转换为引用。
  • Deref<Target=T>用于把&U类型的值自动转换为&T类型。
  • Iterator用于在集合 (collection) 和惰性值生成器 (lazy value generator) 上实现迭代器。
  • Sized用于标记运行时长度固定的类型,而不定长的切片和特性必须放在指针后面使其运行时长度已知, 比如&[T]Box<Trait>

泛型和多态

泛型 (generics) 在类型理论中称作参数多态 (parametric polymorphism), 意为对于给定参数可以有多种形式的函数或类型。先看Rust中的一个泛型例子:

Option在rust标准库中的定义:

enum Option<T> {
    Some(T),
    None,
}
  • 1
  • 2
  • 3
  • 4

Option的典型用法:

let x: Option<i32> = Some(5);
let y: Option<f64> = Some(5.0f64);
  • 1
  • 2

其中<T>部分表明它是一个泛型数据类型。当然,泛型参数也可以用于函数参数和结构体域:

// generic functions
fn make_pair<T, U>(a: T, b: U) -> (T, U) {
    (a, b)
}
let couple = make_pair("man", "female");

// generic structs
struct Point<T> {
    x: T,
    y: T,
}
let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

对于多态函数,存在两种派分 (dispatch) 机制:静态派分和动态派分。

前者类似于C++的模板,Rust会生成适用于指定类型的特殊函数,然后在被调用的位置进行替换, 好处是允许函数被内联调用,运行比较快,但是会导致代码膨胀 (code bloat);

后者类似于Java或Go的interface,Rust通过引入特性对象 (trait object) 来实现, 在运行期查找虚表 (vtable) 来选择执行的方法。

特性对象&Foo具有和特性Foo相同的名称, 通过转换 (casting) 或者强制多态化 (coercing) 一个指向具体类型的指针来创建。

当然,特性也可以接受泛型参数。但是,往往更好的处理方式是使用关联类型 (associated type):

// use generic parameters
trait Graph<N, E> {
    fn has_edge(&self, &N, &N) -> bool;
    fn edges(&self, &N) -> Vec<E>;
}
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 {
}
// use associated types
trait Graph {
    type N;
    type E;
    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
}
fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> uint {
}

struct Node;
struct Edge;
struct SimpleGraph;
impl Graph for SimpleGraph {
    type N = Node;
    type E = Edge;
    fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
    }
    fn edges(&self, n: &Node) -> Vec<Edge> {
    }
}

let graph = SimpleGraph;
let object = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

3.11 注释和文档

注释:

// 单行注释
/*
 * 多行注释,支持,但是不推荐
 */
  • 1
  • 2
  • 3
  • 4

文档:

Rust 自带有文档功能的注释,分别是/////!。支持 Markdown 格式。

  • ///用来描述的它后面接着的项。用于模块级注释。
  • //!用来描述包含它的项,一般用在模块文件的头部。用于文档级注释。

最后,通过rustdoc xxx.rs或者cargo doc生成html文档。

//! # The first line
//! The second line

/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let five = 5;
///
/// assert_eq!(6, add_one(5));
/// # fn add_one(x: i32) -> i32 {
/// #     x + 1
/// # }
/// ```
fn add_one(x: i32) -> i32 {
  x + 1
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

3.12 输入输出流

标准输入
标准输入也叫作控制台输入,是常见输入的一种。

// 例子1
use std::io;
fn read_input() -> io::Result<()> {
    let mut input = String::new();
    try!(io::stdin().read_line(&mut input));
    println!("You typed: {}", input.trim());
    Ok(())
}

fn main() {
    read_input();
}

// 例子2:
use std::io;
fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input).expect("WTF!");
    println!("You typed: {}", input.trim());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

这里体现了常见的标准输入的处理方式。两个例子都是声明了一个可变的字符串来保存输入的数据。 他们的不同之处在在于处理潜在输入异常的方式。

  • 例子1 使用了 try! 宏。这个宏会返回 Result<(), io::Error> 类型,io::Result<()> 就是这个类型的别名。所以例子1 需要单独使用一个 read_input 函数来接收这个类型,而不是在 main 函数里面,因为 main 函数并没有接收 io::Result<()>作为返回类型。
  • 例子2 使用了 Result<(), io::Error> 类型的 expect 方法来接收 io::stdin().read_line 的返回类型。并处理可能潜在的 io 异常。

标准输出
标准输出也叫控制台输出,Rust 里面常见的标准输出宏有 print! 和 println!。它们的区别是后者比前者在末尾多输出一个换行符。

// 例子1:
fn main() {
    print!("this ");
    print!("will ");
    print!("be ");
    print!("on ");
    print!("the ");
    print!("same ");
    print!("line ");

    print!("this string has a newline, why not choose println! instead?\n");
}

//例子2:
fn main() {
    println!("hello there!");
    println!("format {} arguments", "some");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这里两个例子都比较简单。读者可以运行一下查看输出结果对比一下他们的区别。 值得注意的是例子 2 中,{ } 会被 "some" 所替换。这是 rust 里面的一种格式化输出。

标准化的输出是行缓冲(line-buffered)的,这就导致标准化的输出在遇到一个新行之前并不会被隐式刷新。 换句话说 print!println! 二者的效果并不总是相同的。 如果说得更简单明了一点就是,您不能把 print! 当做是C语言中的 printf 譬如:

use std::io;
fn main() {
    print!("请输入一个字符串:");
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("读取失败");
    print!("您输入的字符串是:{}\n", input);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这段代码运行时则不会先出现预期的提示字符串,因为行没有被刷新。 如果想要达到预期的效果就要显示的刷新:

use std::io::{self, Write};
fn main() {
    print!("请输入一个字符串:");
    io::stdout().flush().unwrap();
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("读取失败");
    print!("您输入的字符串是:{}\n", input);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

文件输入
文件输入和标准输入都差不多,除了输入流指向了文件而不是控制台。下面例子采用了模式匹配来处理潜在的输入错误

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    // 创建一个文件路径
    let path = Path::new("hello.txt");
    let display = path.display();

    // 打开文件只读模式, 返回一个 `io::Result<File>` 类型
    let mut file = match File::open(&path) {
        // 处理打开文件可能潜在的错误
        Err(why) => panic!("couldn't open {}: {}", display,
                                                   Error::description(&why)),
        Ok(file) => file,
    };

    // 文件输入数据到字符串,并返回 `io::Result<usize>` 类型
    let mut s = String::new();
    match file.read_to_string(&mut s) {
        Err(why) => panic!("couldn't read {}: {}", display,
                                                   Error::description(&why)),
        Ok(_) => print!("{} contains:\n{}", display, s),
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

文件输出
文件输出和标准库输出也差不多,只不过是把输出流重定向到文件中。下面详细看例子。

// 输出文本
static LOREM_IPSUM: &'static str =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
";

use std::error::Error;
use std::io::prelude::*;
use std::fs::File;
use std::path::Path;

fn main() {
    let path = Path::new("out/lorem_ipsum.txt");
    let display = path.display();

    // 用只写模式打开一个文件,并返回 `io::Result<File>` 类型
    let mut file = match File::create(&path) {
        Err(why) => panic!("couldn't create {}: {}",
                           display,
                           Error::description(&why)),
        Ok(file) => file,
    };

    // 写入 `LOREM_IPSUM` 字符串到文件中, 并返回 `io::Result<()>` 类型
    match file.write_all(LOREM_IPSUM.as_bytes()) {
        Err(why) => {
            panic!("couldn't write to {}: {}", display,
                                               Error::description(&why))
        },
        Ok(_) => println!("successfully wrote to {}", display),
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

3.13 操作符

Rust的操作符号和C++大部分都是一模一样的。

一元操作符
顾名思义,一元操作符是专门对一个Rust元素进行操纵的操作符,主要包括以下几个:

  • -: 取负,专门用于数值类型。
  • *: 解引用。这是一个很有用的符号,和Deref(DerefMut)这个trait关联密切。
  • !: 取反。取反操作相信大家都比较熟悉了,不多说了。有意思的是,当这个操作符对数字类型使用的时候,会将其每一位都置反!也就是说,你对一个1u8进行!的话你将会得到一个254u8。
  • &&mut: 租借,borrow。向一个owner租借其使用权,分别是租借一个只读使用权和读写使用权。

二元操作符
算数运算符都有对应的trait的,他们都在std::ops下:

  • +: 加法。实现了std::ops::Add
  • -: 减法。实现了std::ops::Sub
  • *: 乘法。实现了std::ops::Mul
  • /: 除法。实现了std::ops::Div
  • %: 取余。实现了std::ops::Rem

和算数运算符差不多的是,位运算也有对应的trait。

  • &: 与操作。实现了std::ops::BitAnd
  • |: 或操作。实现了std::ops::BitOr
  • ^: 异或。实现了std::ops::BitXor
  • <<: 左移运算符。实现了std::ops::Shl
  • >>: 右移运算符。实现了std::ops::Shr

逻辑运算符有三个,分别是&&、||、!。其中前两个叫做惰性boolean运算符,之所以叫这个名字,是因为在Rust里也会出现其他类C语言的逻辑短路问题。所以取了这么一个高大上然并卵的名字。

其作用和C语言里的一毛一样啊!哦,对了,有点不同的是Rust里这个运算符只能用在bool类型变量上。什么 1 && 1 之类的表达式给我死开。

比较运算符

比较运算符其实也是某些trait的语法糖啦,不同的是比较运算符所实现的trait只有两个std::cmp::PartialEqstd::cmp::PartialOrd

其中, ==!=实现的是PartialEq。 而,<、>、>=、<=实现的是PartialOrd

类型转换运算符

其实这个并不算运算符,因为他是个单词as。

这个就是C语言中各位熟悉的显式类型转换了。

fn avg(vals: &[f64]) -> f64 {
    let sum: f64 = sum(vals);
    let num: f64 = len(vals) as f64;
    sum / num
}
  • 1
  • 2
  • 3
  • 4
  • 5

3.14 格式化字符串

说起格式化字符串,Rust采取了一种类似Python里面format的用法,其核心组成是五个宏和两个trait:format!、format_arg!、print!、println!、write!;Debug、Display

相信你们在写Rust版本的Hello World的时候用到了print!或者println!这两个宏,但是其实最核心的是format!,前两个宏只不过将format!的结果输出到了console而已。

那么,我们来探究一下format!这个神奇的宏吧。

在这里呢,列举format!的定义是没卵用的,因为太复杂。我只为大家介绍几种典型用法。学会了基本上就能覆盖你平时80%的需求。

首先我们来分析一下format的一个典型调用:

fn main() {
    let s = format!("{1}是个有着{0:>0width$}KG重,{height:?}cm高的大胖子",
                    81, "wayslog", width=4, height=178);
    // 我被逼的牺牲了自己了……
    print!("{}", s);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们可以看到,format!宏调用的时候参数可以是任意类型,而且是可以position参数和key-value参数混合使用的。但是要注意的一点是,key-value的值只能出现在position值之后并且不占position。例如例子里你用3$引用到的绝对不是width,而是会报错。
这里面关于参数稍微有一个规则就是,参数类型必须要实现 std::fmt mod 下的某些trait。比如我们看到原生类型大部分都实现了DisplayDebug这两个宏,其中整数类型还会额外实现一个Binary,等等。

当然了,我们可以通过{:type}的方式去调用这些参数。

format!("{:b}", 2);
// 调用 `Binary` trait
// Get : 10
format!("{:?}", "Hello");
// 调用 `Debug`
// Get : "Hello"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

另外请记住:type这个地方为空的话默认调用的是Display这个trait。关于:号后面的东西其实还有更多式子,我们从上面的{0:>0width$}来分析它。

首先>是一个语义,它表示的是生成的字符串向右对齐,于是我们得到了0081这个值。与之相对的还有<(向左对齐)和^(居中)。

再接下来0是一种特殊的填充语法,他表示用0补齐数字的空位,要注意的是,当0作用于负数的时候,比如上面例子中wayslog的体重是-81,那么你最终将得到-0081;当然了,什么都不写表示用空格填充啦; 在这一位上,还会出现+、#的语法,使用比较诡异,一般情况下用不上。

最后是一个组合式子width$,这里呢,大家很快就能认出来是表示后面key-value值对中的width=4。你们没猜错,这个值表示格式化完成后字符串的长度。它可以是一个精确的长度数值,也可以是一个以$为结尾的字符串,$前面的部分可以写一个key或者一个postion

最后,你需要额外记住的是,在widthtype之间会有一个叫精度的区域(可以省略不写如例子),他们的表示通常是以.开始的,比如.4表示小数点后四位精度。最让人遭心的是,你仍然可以在这个位置引用参数,只需要和上面width一样,用.N$来表示一个position的参数,但是就是不能引用key-value类型的。这一位有一个特殊用法,那就是.*,它不表示一个值,而是表示两个值!第一个值表示精确的位数,第二个值表示这个值本身。

这是一种很尴尬的用法,而且极度容易匹配到其他参数。因此,我建议在各位能力或者时间不欠缺的时候尽量把格式化表达式用标准的形式写的清楚明白。尤其在面对一个复杂的格式化字符串的时候。

好了好了,说了这么多,估计你也头昏脑涨的了吧,下面来跟我写一下format宏的完整用法。仔细体会并提炼每一个词的意思和位置。

// 简直眼花缭乱
format_string := <text> [ format <text> ] *
format := '{' [ argument ] [ ':' format_spec ] '}'
argument := integer | identifier

format_spec := [[fill]align][sign]['#'][0][width]['.' precision][type]
fill := character
align := '<' | '^' | '>'
sign := '+' | '-'
width := count
precision := count | '*'
type := identifier | ''
count := parameter | integer
parameter := integer '$'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

4. Cargo管理项目

项目目录

rust_cargo

对上述cargo默认的项目结构解释如下:
- cargo.tomlcargo.lock文件总是位于项目根目录下。
- 源代码位于src目录下。
- 默认的库入口文件是src/lib.rs
- 默认的可执行程序入口文件是src/main.rs
- 其他可选的可执行文件位于src/bin/*.rs(这里每一个rs文件均对应一个可执行文件)。
- 外部测试源代码文件位于tests目录下。
- 示例程序源代码文件位于examples
- 基准测试源代码文件位于benches目录下。

好了,大家一定谨记这些默认规则,最好按照这种模式来组织自己的rust项目。

toml解析

cargo.toml是cargo特有的项目数据描述文件,对于猿们而言,cargo.toml文件存储了项目的所有信息,它直接面向rust猿,猿们如果想让自己的rust项目能够按照期望的方式进行构建、测试和运行,那么,必须按照合理的方式构建'cargo.toml'

cargo.lock文件则不直接面向猿,猿们也不需要直接去修改这个文件。lock文件是cargo工具根据同一项目的toml文件生成的项目依赖详细清单文件,所以我们一般不用不管他。

[package]
name = "rsPrj"
version = "0.1.0"
authors = ["xin.zhang <xin.zhang@xxxx.com>"]

[dependencies]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

package段落

[package]段落描述了软件开发者对本项目的各种元数据描述信息,例如[name]字段定义了项目的名称,[version]字段定义了项目的当前版本,[authors]定义了该项目的所有作者,当然,[package]段落不仅仅包含这些字段,[package]段落的其他可选字段详见cargo参数配置章节。

定义项目依赖

使用cargo工具的最大优势就在于,能够对该项目的各种依赖项进行方便、统一和灵活的管理。这也是使用cargo对rust 的项目进行管理的重要目标之一。在cargo的toml文件描述中,主要通过各种依赖段落来描述该项目的各种依赖项。toml中常用的依赖段落包括一下几种:

  • 基于rust官方仓库crates.io,通过版本说明来描述:
  • 基于项目源代码的git仓库地址,通过URL来描述:
  • 基于本地项目的绝对路径或者相对路径,通过类Unix模式的路径来描述:

这三种形式具体写法如下:

[dependencies]
typemap = "0.3"
plugin = "0.2*"
hammer = { version = "0.5.0"}
color = { git = "https://github.com/bjz/color-rs" }
geometry = { path = "crates/geometry" }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上述例子中,2-4行为方法一的写法,第5行为方法二的写法,第6行为方法三的写法。这三种写法各有用处:
1. 如果项目需要使用crates.io官方仓库来管理项目依赖项,推荐使用第一种方法。
2. 如果项目开发者更倾向于使用git仓库中最新的源码,可以使用方法二。方法二也经常用于当官方仓库的依赖项编译不通过时的备选方案。
3. 方法三主要用于源代码位于本地的依赖项。

定义集成测试用例

cargo另一个重要的功能,即将软件开发过程中必要且非常重要的测试环节进行集成,并通过代码属性声明或者toml文件描述来对测试进行管理。

其中,单元测试主要通过在项目代码的测试代码部分前用#[test]属性来描述,而集成测试,则一般都会通过toml文件中的[[test]]段落进行描述。

例如,假设集成测试文件均位于tests文件夹下,则toml可以这样来写:

[[test]]
name = "testinit"
path = "tests/testinit.rs"

[[test]]
name = "testtime"
path = "tests/testtime.rs"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上述例子中,name字段定义了集成测试的名称,path字段定义了集成测试文件相对于本toml文件的路径。 看看,定义集成测试就是如此简单。 需要注意的是:

  • 如果没有在Cargo.toml里定义集成测试的入口,那么tests目录(不包括子目录)下的每个rs文件被当作集成测试入口.
  • 如果在Cargo.toml里定义了集成测试入口,那么定义的那些rs就是入口,不再默认指定任何集成测试入口.

定义项目示例和可执行程序

上面我们介绍了cargo项目管理中常用的三个功能,还有两个经常使用的功能:example用例的描述以及bin用例的描述。其描述方法和test用例描述方法类似。不过,这时候段落名称’[[test]]’分别替换为:’[[example]]’或者’[[bin]]’。例如:

[[example]]
name = "timeout"
path = "examples/timeout.rs"

[[bin]]
name = "bin1"
path = "bin/bin1.rs"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

对于'[[example]]''[[bin]]'段落中声明的examples和bins,需要通过'cargo run --example NAME'或者'cargo run --bin NAME'来运行,其中NAME对应于你在name字段中定义的名称。

5. 表达式

rust是一个基于表达式的语言,不过它也有语句。rust只有两种语句:声明语句和表达式语句,其他的都是表达式。基于表达式是函数式语言的一个重要特征,表达式总是返回值。

5.1 声明语句

rust的声明语句可以分为两种,一种为变量声明语句,另一种为Item声明语句。

  • 变量声明
let a = 8;
let b: Vec<f64> = Vec::new();
let (a, c) = ("hi", false);

let b = (let a = 8); // 错误,由于let是语句,所以不能将let语句赋给其他值
  • 1
  • 2
  • 3
  • 4
  • 5
  • Item声明

    指函数(function)、结构体(structure)、类型别名(type)、静态变量(static)、特质(trait)、实现(implementation)或模块(module)的声明。这些声明可以嵌套在任意块(block)中。

5.2 表达式语句

表达式语句,由一个表达式和一个分号组成,即在表达式后面加一个分号就将一个表达式转变为了一个语句。所以,有多少种表达式,就有多少种表达式语句。

// 字面表达式(literal expression)
();        // unit type
"hello";   // string type
'1';       // character type
15;         // integer type

// 元组表达式(Tuple expression):
(0.0, 4.5);
("a", 4usize, true);

//通常不使用一个元素的元组,不过如果你坚持的话,rust也是允许的,不过需要在元素后加一个逗号:
(0,); // single-element tuple
(0); // zero in parentheses

// 结构体表达式(structure expression) 由于结构体有多种形式,所以结构体表达式也有多种形式。
Point {x: 10.0, y: 20.0};
TuplePoint(10.0, 20.0);
let u = game::User {name: "Joe", age: 35, score: 100_000};
some_fn::<Cookie>(Cookie);

// 结构体表达式一般用于构造一个结构体对象,它除了以上从零构建的形式外,还可以在另一个对象的基础上进行构建:
let base = Point3d {x: 1, y: 2, z: 3};
Point3d {y: 0, z: 10, .. base};

// 块表达式(block expression): 块表达式就是用花括号{}括起来的一组表达式的集合,表达式间一般以分号分隔。块表达式的值,就是最后一个表达式的值。
let x: i32 = { println!("Hello."); 5 };

// 如果以语句结尾,则块表达式的值为():
let x: () = { println!("Hello."); };

// 范围表达式(range expression): 可以使用范围操作符..来构建范围对象(variant of std::ops::Range):
1..2;   // std::ops::Range
3..;    // std::ops::RangeFrom
..4;    // std::ops::RangeTo
..;     // std::ops::RangeFull

// if表达式(if expression):
let a = 9;
let b = if a%2 == 0 {"even"} else {"odd"};

// 除了以上这些外,还有许多,如:
path expression
mehond-call expression
field expression
array expression
index expression
unary operator expression
binary operator expression
return expression
grouped expression
match expression
if expression
lambda expression
... ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

这里无法详细展开,读者可以到Rust Reference去查看。

6. Match和模式

match所罗列的匹配,必须穷举出其所有可能。当然,你也可以用 _ 这个符号来代表其余的所有可能性情况,就类似于switch中的default语句。
match的每一个分支都必须是一个表达式,并且,除非一个分支一定会触发panic,这些分支的所有表达式的最终返回值类型必须相同。

模式

模式,是Rust另一个强大的特性。它可以被用在let和match表达式里面。相信大家应该还记得我们在复合类型中提到的关于在let表达式中解构元组的例子,实际上这就是一个模式。

let tup = (0u8, 1u8);
let (x, y) = tup;
  • 1
  • 2

而且我们需要知道的是,如果一个模式中出现了和当前作用域中已存在的同名的绑定,那么它会覆盖掉外部的绑定。比如:

let x = 1;
let c = 'c';

match c {
    x => println!("x: {} c: {}", x, c),
}

println!("x: {}", x);

// 它的输出结果是:
x: c c: c
x: 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在以上代码中,match作用域里的x这个绑定被覆盖成了'c',而出了这个作用域,绑定x又恢复为1。这和变量绑定的行为是一致的。

更强大的解构

在上一节里,我们初步了解了模式匹配在解构enum时候的便利性,事实上,在Rust中模式可以被用来对任何复合类型进行解构——struct/tuple/enum。现在我们要讲述一个复杂点的例子,对struct进行解构。

首先,我们可以对一个结构体进行标准的解构:

struct Point {
    x: i64,
    y: i64,
}
let point = Point { x: 0, y: 0 };
match point {
    Point { x, y } => println!("({},{})", x, y),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最终,我们拿到了Point内部的值。有人说了,那我想改个名字怎么办? 很简单,你可以使用 :来对一个struct的字段进行重命名,如下:

struct Point {
    x: i64,
    y: i64,
}
let point = Point { x: 0, y: 0 };
match point {
    Point { x: x1, y: y1} => println!("({},{})", x1, y1),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

另外,有的时候我们其实只对某些字段感兴趣,就可以用..来省略其他字段。

struct Point {
    x: i64,
    y: i64,
}

let point = Point { x: 0, y: 0 };

match point {
    Point { y, .. } => println!("y is {}", y),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

忽略和内存管理

总结一下,我们遇到了两种不同的模式忽略的情况——_..。这里要注意,模式匹配中被忽略的字段是不会被move的,而且实现Copy的也会优先被Copy而不是被move

说的有点拗口,上代码:

let tuple: (u32, String) = (5, String::from("five"));
let (x, s) = tuple;
// 以下行将导致编译错误,因为String类型并未实现Copy, 所以tuple被整体move掉了。
// println!("Tuple is: {:?}", tuple);
let tuple = (5, String::from("five"));
// 忽略String类型,而u32实现了Copy,则tuple不会被move
let (x, _) = tuple;
println!("Tuple is: {:?}", tuple);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

范围和多重匹配

模式匹配可以被用来匹配单种可能,当然也就能被用来匹配多种情况:

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

let x = 1;
match x {
    1 ... 10 => println!("一到十"),
    _ => println!("其它"),
}
let c = 'w';
match c {
    'a' ... 'z' => println!("小写字母"),
    'A' ... 'Z' => println!("大写字母"),
    _ => println!("其他字符"),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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

let x = 1;
match x {
    1 | 2 => println!("一或二"),
    _ => println!("其他"),
}
  • 1
  • 2
  • 3
  • 4
  • 5

ref 和 ref mut

前面我们了解到,当被模式匹配命中的时候,未实现Copy的类型会被默认的move掉,因此,原owner就不再持有其所有权。但是有些时候,我们只想要从中拿到一个变量的(可变)引用,而不想将其move出作用域,怎么做呢?答:用ref或者ref mut。

let mut x = 5;

match x {
    ref mut mr => println!("mut ref :{}", mr),
}
// 当然了……在let表达式里也能用
let ref mut mrx = x;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

变量绑定

在模式匹配的过程内部,我们可以用@来绑定一个变量名,这在复杂的模式匹配中是再方便不过的,比如一个具名的范围匹配如下:

let x = 1u32;
match x {
    e @ 1 ... 5 | e @ 10 ... 15 => println!("get:{}", e),
    _ => (),
}
  • 1
  • 2
  • 3
  • 4
  • 5

如代码所示,e绑定了x的值。
当然,变量绑定是一个极其有用的语法,下面是一个来自官方doc里的例子:

struct Person {
    name: Option<String>,
}

let name = "Steve".to_string();
let x: Option<Person> = Some(Person { name: Some(name) });
match x {
    Some(Person { name: ref a @ Some(_), .. }) => println!("{:?}", a),
    _ => {}
}
// 输出:
Some("Steve")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

后置条件

一个后置的if表达式可以被放在match的模式之后,被称为match guards。例如如下代码:

let x = 4;
let y = false;

match x {
    4 | 5 if y => println!("yes"),
    _ => println!("no"),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

猜一下上面代码的输出?

答案是no。因为guard是后置条件,是整个匹配的后置条件:所以上面的式子表达的逻辑实际上是:

// 伪代码表示
IF y AND (x IN List[4, 5])
  • 1
  • 2

总结

Rust的语法兼顾C++和Golang一类语言的优点,集合了自己的特点,从语法层次来讲,做的非常不错。但是强大的同时带来的是复杂性,作为一个和C++比肩的语言,学习难度还是相当高的。

虽然这里已经了解了很多了,但是还有很多细节没有涉及到,在后续的文章中继续。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/249044
推荐阅读
相关标签
  

闽ICP备14008679号