一些地址

rust语言圣经

变量绑定与解构

使用下划线开头忽略未使用的变量

let _x = 15;

这里x即便没有使用也不会报错.

之所以报错是因为有时候可能成为一个BUG.

未使用变量修复报错

代表匹配一个值

只用一个 _, 可以代表我们不关心具体的值是什么.

方法一:

fn main(){
    let _y = 1;
}

方法二:

#[allow(unused_variables)]
fn main() {
    let x = 1;
}

常量

常量用const而非let关键字声明.

变量遮蔽shadowing

对变量作用域内进行遮蔽.

在某个作用域内无需再使用之前的变量, 就可以用重复的变量名字, 不用在费劲心思去想更多的名字.

基本类型

整数类型

iinteger, 表示的是有符号数.

uunsigned, 表示的是无符号数.

类型定义的形式为:i or u + 位数.

整型溢出

使用debug构建时, 可以看到整型溢出, 但是 --release 无法看到, 该模式下, 会按照补码循环溢出来处理, 如 u8 下, 256会变成0, 257会变成1.

浮点数陷阱

  1. 避免在浮点数上测试相等性
  2. 当结果在数学上可能存在未定义时,需要格外的小心

NaN

NaN无法比较, 可以使用is_nan()的方法来判断一个数字是否是NaN.

如何进行类型标注

fn main(){
    //类型标注
    let a:i32 = 21;
    //类型后缀进行标注
    let b = 22i32;
    //通过编译器, 这个有可能报错
    let c = 23;
}

序列

for i in 1..5{
    println!("{}",i);
}

1..5, 可以生成从1到4的连续数字.

for i in 1..=5{
    println!("{}",i);
}

1..=5, 可以生成从1到5的连续数字.

序列只允许数字或字符类型, 因为他们是连续的.

函数要点

  • 函数名和变量名使用蛇形命名法(snake case),例如fn add_two() -> {}
  • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
  • 每个函数参数都需要标注类型

特殊返回类型

  1. 无返回值
    • 函数没有返回值,那么返回一个 ()
    • 通过 ; 结尾的语句返回一个 ()
    • 十分重要的是, 语句与表达式的区分
  2. 永不返回的发散函数
    • !作为函数返回类型, 也就是fn dead()-> !{}
    • 这个作用是导致程序崩溃.

所有权

栈和堆

栈上存储数据大小已知且固定大小的值. 堆上存储大小未知或者可能变化的数据. 放入堆上的数据返回该地址的指针, 指针大小是确定且固定的. 堆上的数据缺乏组织. 并且效率低.

&str和String

&str 是被硬编码进程序里的字符串值. 是不可变的.

String是动态的.

什么是 ::

这个是个调用符. 表示用了什么操作.

栈上的值的所有权问题

fn main(){
    let a = 1;
    let b = a;
}

这里a是栈上的值, 把a赋值给b, a在栈上, 使用的是深拷贝, 不涉及所有权的转移.

不过其实在栈上不涉及深浅拷贝, 深浅拷贝是在堆上的操作.

复杂类型所有权的转移

如果是复杂类型, 不存放在栈上, 而是存放在堆上, 栈上存放的只是指向堆位置的指针, 那rust的赋值操作看似是浅拷贝, 实际是move. 涉及到了所有权的转移, 当所有权转移后, 原本的指针不指向任何数据, 所以直接就释放了, 无法再使用.

深拷贝

使用.clone().

函数的传递与返回

往函数里面传值传递的不仅仅只是值, 依旧有所有权的转移, 函数的返回, 也会返回所有权.

引用与解引用

这个就跟c语言一样, &是引用, *是解引用. 不过这里只是借用, 不能进行更改.

可变引用

使用 &mut xxx 来进行可变引用, 这里的引用就可以进行更改.

但是同一作用域下面只能有一个可变的引用.

fn main(){
    let s = String::from("永恒至尊");
    let s1 = &mut s;
    let s2 = &mut s;
}

这里是不可以的, 同一作用域内部只能存在一个可变引用.

这个特性避免了编译时期的数据竞争.

避免方法:

let mut s = String::from("hello");
 
{
    let r1 = &mut s;
 
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
 
let r2 = &mut s;

可变引用与不可变引用可以同时存在吗

fn main(){
    let mut s = Stirng::from("bbb");
    let r1 = &s;
    let r2 = &s; //不可变引用可以同时使用多个, 因为是只读的.
    //只读就是比可写好处理很多.
    let r3 = &mut s;
    println!("{}, {}, and {}", r1, r2, r3);
}

不能同时存在.

fn main() {
   let mut s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束
    // 这里是因为编译器太厉害的原因, r1, r2之后不再使用了, 这个rust编译器可以知道, 所以作用域就到此为止.
 
    let r3 = &mut s;
    println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
  // 新编译器中,r3作用域在这里结束

复合类型

字符串与切片

rust里面的切片与python差不多.

需要注意的是, 字符串的字面量&str是String的切片.

是对某一部分的引用.

这里其实也就解释了为什么字面量是不可变的, 因为它是String的不可变引用.

rust中的字符串具体解析

字符用的是Unicode类型, Unicode使用的是4个字节一个字符, 但是使用的是utf-8编码, 编码使用的是可变字节, 1-4个字节, 比如中文就用了三个字节, 所以引用中文要特别注意.

fn main(){
    let a ="永恒至尊";
    let s = &a[0..2];
    println!("{}",s);//这里就是错的, 因为一个中文占三个字节.
}

所以字符串切片是非常危险的操作.

rust从语言上说其实只有 str 类型, 这个通常以 &str 引用这种类型出现. 不过标准库里面有许多字符串类型, 其中使用最广泛的就是 String.

unicode与utf-8的区别

unicode是一个字符集的标准, 而utf-8是编码的方法, 也就是说unicode是一个接口, 而utf-8是具体的实现方法.

&str与String互相转换

&str转String

fn main(){
    let s1 = String::from("xxxx");
    let s2 = "xxx".to_string();
}

String转&str

fn main(){
    let s = String::from("xx");
    let s1 = &s //这里就是把string转为了&str
}

rust允许索引字符串吗

不允许.

a是一个字符串.

a[1] 这个就是不允许的. 因为字符串使用utf-8编码, 变长的, 你还得确定边界, 所以无法索引.

python与rust的辨析

python也是使用utf-8编码, 但是它通过抽象字符串为unicode字符序列, 每个字符对应的是一个完整的unicode代码点. 索引基于字符而非字节.

但是rust使用的是utf-8的字节序列.

这个的好处就是开销十分小. 不需要额外的结构.

''"" 的辨析

如果要使用push, 就用s.push('!'), 而不是s.push("!"), 这个就是字符串了, 需要用`s.push_str()“.

insert()在中文中的插入边界问题

fn main() {
    let s = "永恒至尊";
    let mut s1 = String::from(s);
    s1.push_str(",August");
    println!("{}", s1);
 
    s1.push('!');
    println!("{}", s1);
 
    s1.insert(3, 'A'); //这里如果第一个参数是2就会出现边界问题.
    println!("{}", s1);
}

结构体

结构体只能整体可变, 不能某个字段可变.

当一个结构体的实例给另一个实例赋值的时候, 原来的实例就无法使用了, 但是有Copy特征的原实例字段仍然可以使用, 只是整体以及无Copy特征的字段发生了所有权的变更, 导致无法使用.

元组结构体

就是结构体的字段无名字.

标准结构体:

struct Color1{
    a:i32,
    b:i32,
    c:i32,
}

元组结构体:

struct Color2(i32, i32, i32);

单元结构体

无字段, 也没有属性的结构体. 当我们只关注行为的时候使用.

struct A;
impl SomeTrait for A(){}

打印结构体

使用#[derive(Debug)]来打印结构体的信息. 不加无法打印, 当然还有别的方法.

流程控制

暂时不想看

模式匹配

match的穷尽匹配

match必须匹配所有的分支, 可以使用_来作为default, 匹配省下的, 没匹配到的. 用个变量也行.

if let匹配

遇到只有一个模式的值需要进行处理, 可以直接使用if let来解决.

如果使用match匹配:

let v = Some(3u8);
match v {
    Some(3) => println!("three"),
    _ => (),
}

可以直接 if let 匹配:

let v = Some(3u8);
let if Some(3) = v{
    println!("three");
}

只处理一个值, 并且忽略其他的, 就使用if let.

方法

方法往往和结构体, 枚举, 特征一起使用.

如何定义一个方法

使用 impl 来定义方法.

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}
 
impl Circle {
    // new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
    // 这种方法往往用于初始化当前结构体的实例
    fn 也就是所有返回值的生命周期都等于该输入生命周期new(x: f64, y: f64, radius: f64) -> Circle {
        Circle {
            x: x,
            y: y,
            radius: radius,
        }
    }
 
    // Circle的方法,&self表示借用当前的Circle结构体
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

rust方法与其他语言中的类的方法不同, 其他语言中类将数据和方法放在一起, 而rust则将方法和数据进行分离.

接下来的内容非常重要,请大家仔细看。在 area 的签名中,我们使用 &self 替代 rectangle: &Rectangle,&self 其实是 self: &Self 的简写(注意大小写)。在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例,换句话说,self 指代的是 Rectangle 结构体实例,这样的写法会让我们的代码简洁很多,而且非常便于理解:我们为哪个结构体实现方法,那么 self 就是指代哪个结构体的实例。

需要注意的是,self 依然有所有权的概念:

  • self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
  • &self 表示该方法对 Rectangle 的不可变借用
  • &mut self 表示可变借用

关联函数

参数中不包含 self 就是关联函数, 这个是函数不是方法. 所以无法通过 . 的方法去调用. 这个关联函数就相当于rust的构造器.

但是可以使用::来调用. ::语法用于关联函数和模块创建的命名空间.

约定俗成用new来作为构造器的名称.

方法名与字段名

可以相同, 如果有 () 则说明调用的是方法, 如果没有则说明是字段.

一般来说, 方法和字段同名, 往往适用于实现getter访问器.

rust中的私有与公有

默认私有.

最前面加上 pub 就是公有的.

泛型

定义一个泛型函数

fn largest<T>(list: &[T]) -> T{}

这个前面的<T>定义了这是一个泛型函数, 参数列表使用了泛型, 而->后面的T表示返回了一个泛型.

不是所有都能比较

T可以是任何类型, 但是不是所有类型都能进行比较. 需要使用特征对T进行限制, 才能进行比较.

结构体使用泛型

首先要先定义:

struct point<T>{
    x:T,
    y:T,
}

然后实例化的时候, 泛型所赋的值必须是相同类型的, 例如都是整数或都是浮点数, 不能一个是整数一个是浮点数.

如果需要不同的数据类型, 那就需要定义不同的泛型:

struct point<T,U>{
    x:T,
    y:U,
}

这个时候就可以用不同的类型.

方法中使用泛型

struct Point<T>{
    x: T,
    y: T,
}
impl<T> Point<T>{
    fn x(&self) -> &T {
        &self.x
    }
}

这里的impl<T>声明这个方法使用了泛型, 后面的Point<T>, 不是泛型的声明, 而是我们使用的是一个完整的结构体类型.

const泛型

也就是针对值泛型, 可以处理数组长度问题.

fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
}
fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);
 
    let arr: [i32; 2] = [1, 2];
    display_array(arr);
}

常量函数 const fn

普通的函数运行实在运行时被调用和执行的, 而const fn是在编译期执行的, 将计算结果直接嵌入生成的代码中.

不能将随机数生成器写成const fn. 因为要确保值的安全.

const fn可以减少运行时的计算开销.

泛型的性能的解析

rust中的泛型是零成本的抽象, 无需担心性能上的问题, 但是编译速度和文件大小变大了. 使用的是单态化.

特征Trait

其实就是接口.

定义特征

不同类型具有相同的行为就可以定义一个特征.

pub trait Summary {
    fn summarize(&self) -> String;
}

特征不关注具体行为.

实现特征

pub trait Summary {
    fn summarize(&self) -> String;
}
pub struct Post {
    pub title: String, // 标题
    pub author: String, // 作者
    pub content: String, // 内容
}
impl Summary for Post {
    fn summarize(&self) -> String {
        format!("文章{}, 作者是{}", self.title, self.author)
    }
}
 
pub struct Weibo {
    pub username: String,
    pub content: String
}
 
impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("{}发表了微博{}", self.username, self.content)
    }
}

impl后面加上特征, 然后必须实现特征里面定义的行为.

孤儿规则

关于特征实现与定义的位置,有一条非常重要的原则:如果你想要为类型 A 实现特征 T, 那么 A 或者 T 至少有一个是在当前作用域中定义的! 例如我们可以为上面的 Post 类型实现标准库中的 Display 特征,这是因为 Post 类型定义在当前的作用域中。同时,我们也可以在当前包中为 String 类型实现 Summary 特征,因为 Summary 定义在当前作用域中。

但是你无法在当前作用域中,为 String 类型实现 Display 特征,因为它们俩都定义在标准库中,其定义所在的位置都不在当前作用域,跟你半毛钱关系都没有,看看就行了。

该规则被称为孤儿规则,可以确保其它人编写的代码不会破坏你的代码,也确保了你不会莫名其妙就破坏了风马牛不相及的代码。

默认实现

可以在特征里面直接写好行为.

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

这个就是如果没有实现的时候的默认行为.

使用特征作为函数参数

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

可以传递impl该特征的结构体实例作为参数.

不过上面的其实是一个语法糖, 省略写法, 真正的是特征约束.

特征约束

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

其实就是泛型的做法, 规定泛型的类型为特征, 我们就可以传递实现了该特征的结构体的实例了.

特征约束可以适应更加复杂的场景.

pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
//不同类型可以, 相同类型无法强制限制.
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
//不同类型可以, 相同类型可以强制限制.

多重约束

pub fn notify(item: &(impl Summary + Display)) {}
//语法糖形式
pub fn notify<T: Summary + Display>(item: &T) {}
//特征约束形式

其实就是多用个+, 来连接多个特征.

where约束

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
//原本的, 这个函数签名太复杂了.
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}
//这里就简化了. 其实也不是简化了, 就是代码更加清晰了.

返回特征

fn a() -> impl Summary{
    Weibo {
    }
}

这里就是返回一个实现了Summary特征的类型.

这种 impl Trait 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait 的方式简单返回。

derive派生特征

被derive标记的对象会自动实现对应的默认特征代码.

特征对象

用来解决返回特征时无法返回不同类型的问题.

dyn关键字只用在特征对象的类型声明上, 在创建时无需使用dyn.

rust可以通过特征对象, 来变成动态类型语言, 也符合鸭子类型.

静态分发与动态分发

泛型实在编译期完成的, 为每一个泛型参数的具体类型生成一份代码, 这种是静态分发, 对性能没有影响.

特征对象使用的是动态分发, 关键字dyn强调了动态这一特点.

Self与self

self指的是实例, Self指的是实例的类型.

trait Draw {
    fn draw(&self) -> Self;
}
 
#[derive(Clone)]
struct Button;
impl Draw for Button {
    fn draw(&self) -> Self {
        return self.clone()
    }
}
 
fn main() {
    let button = Button;
    let newb = button.draw();
}

特征对象的限制

必须是安全的特征才能有特征变量.

安全的特征:

  1. 方法返回类型不能是Self, 也就是不能返回一个类型.
  2. 方法没有任何泛型参数.

todo

特征的第四节没看, 看不下去了.

集合类型

创建动态数组

let mut v: Vec<i32> = Vec::new(); //1
let mut v = Vec::new(); //2
v.push(1);

rust编译器根据上下文判断的, 所以解释代码要看上下文.

这里第一个之所以要显示声明类型, 就是因为没下文了, 不知道vec里面装什么类型.

而第二个则有下文, 编译器可以自己判断.

vec![]

这是一个宏, 与Vec::new不同的是可以在创建的时候就给予初始化.

vector中读取元素

  1. 通过下标索引
  2. 使用 get 方法

get方法返回的是Option枚举. 需要match来匹配.

get方法不存在数组越界的问题, 因为无值的时候返回none. 但是写起来太麻烦了, 所以还保留了下标索引.

元组结构体的访问

因为元组结构体的字段没有名字, 所以使用 .0, .1 来访问, 表示第一个字段, 第二个字段.

rust的生命周期

主要作用是避免悬垂引用.

rust的生命周期与引用参数紧密相关.

借用检查

为了保证rust的所有权和借用的正确性, 使用了一个借用检查器.

生命短的引用生命长的, 没问题, 生命长的引用生命短的有问题.

生命周期标注语法

生命周期标注并不会改变任何引用的实际作用域

生命周期是给编译器看的.

生命周期语法以'开头, 名称是一个小写字母. 一般用'a来作为生命周期的名称.

命周期'a不代表生命周期等于'a,而是大于等于'a.

Rust 编译器在调教上是非常保守的: 当可能出错也可能不出错时, 它会选择前者, 抛出编译错误.

生命周期的使用

能用在函数上面, 也可以用在结构体上面.

只有这样结构体的字段才能使用引用类型.

引用字段的声明周期应大于结构体的声明周期.

生命周期消除

对于编译器来讲, 每一个引用类型都有一个生命周期. 但是有些简单的生命周期, 编译器会自己标注, 这种标注是隐式的.

注意的事情:

  1. 消除规则不是万能的, 若编译器不能确定某件事是正确时, 会直接判为不正确, 那么你还是需要手动标注生命周期.
  2. 函数或者方法中, 参数的生命周期被称为输入生命周期, 返回值的生命周期被称为输出生命周期.

必然出现悬垂引用的情况

就是一个函数返回一个引用, 这个引用来自与函数体内部新创建的变量. 这种情况一定会出现悬垂引用.

三条消除规则

  1. 每一个引用参数都会获得独自的生命周期.
  2. 若只有一个输入生命周期(也就是函数参数中只有一个引用类型), 该声明周期会被赋给所有的输出生命周期, 也就是所有返回值的生命周期都等于该输入生命周期.
  3. 若有多个输入生命周期, 且其中一个是&self或者&mut self, 则&self的生命周期被赋给所有的输出生命周期. 拥有&self形式的参数, 说明这个函数是一个方法.

生命周期约束语法

'a: 'b, 说明 'a 必须比 'b 活得久.

可以直接写在 <> 中, 也可以使用 where, 返回值后面.

静态生命周期

'static, 标注了这个的引用类型, 能够活得跟程序一样长.

rust 的错误

rust的错误分类

  1. 可恢复错误
  2. 不可恢复错误

Result<T, E> 用于可恢复错误.

panic!用于不可恢复错误.

panic的触发

被动触发

数组越界直接就paniced.

主动调用

使用panic!, 并且只有不可恢复的错误才用.

backtrace栈展开

使用RUST_BACKTRACE=1 cargo run, 来运行程序. 我们就能够看到函数调用的顺序, 按照逆序排列, 也就是先调用的排后面, 后调用的排前面.

panic的两种终止方式

  1. 栈展开
  2. 直接终止

默认是栈展开.

如果想要直接终止, 在Cargo.toml文件中修改:

[profile.release]
panic = 'abort'

线程panic问题

如果main线程出现panic, 则程序直接终止.

如果是其他子线程, 线程终止, 而不影响main线程.

Result枚举类型

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

无错误的时候返回一个 Result 类型包裹的 Ok(T), 出错误返回包裹的 Err(E).

Result类型的处理方法

  1. unwrap
  2. expect
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();

先使用parse()函数解析成IpAddr类型, 这个返回一个Result<IpAddr, E>类型. 如果成功返回里面的值, 失败直接panic.

unwrap就是成功返回值, 失败直接panic, 不进行任何错误处理.

而expect会返回expect参数列表中的值.

传播错误

出错了就层层上传, 让调用链的上游函数去处理问题.

传播界的大明星 ?

它可以自动进行类型提升(转换), 相当于从子类变成父类.

?除了用于Result中, 还可以用在Option中, 通过?来返回None.

rust抽象层次的最高错误

std::error:Error, 其他标准库中的错误都实现了该特征.

可以用这个代表一切错误.

包和模块

组织管理

  1. 项目 Packages
  2. 工作空间 WorkSpace 多个包联合起来
  3. 包 Crate 多个模块组成的树形结构
  4. 模块 Module 一个文件多个模块, 也可以一个文件一个模块

Package的区分

存在两种

  1. 二进制Package: 正常的cargo new 名字生成的就是二进制package
  2. 库Package:使用cargo new 名字 --lib生成的就是库package.

包只是一个编译单元.

package是一个项目工程.

一个典型的package结构

.
├── Cargo.toml
├── Cargo.lock
├── src
│   ├── main.rs  //默认二进制包
│   ├── lib.rs   //唯一库包
│   └── bin      //其余的二进制包
│       └── main1.rs
│       └── main2.rs
├── tests
│   └── some_integration_tests.rs
├── benches
│   └── simple_bench.rs
└── examples
    └── simple_example.rs

模块Module

使用 mod 关键字创建新模块, 后面紧跟着模块名称.

所有模块均在一个文件中.

模块可以嵌套.

用路径引用模块

绝对路径: 从包根开始, 路径名以包名crate开头.

相对路径: 从当前模块开始, 以selfsuper当前模块的标识符作为开头.

如果无法确定哪个好, 优先使用绝对路径.

代码可见性

模块可以定义代码的私有化边界.

默认下, 所有类型都是私有化的, 连模块本身也是私有化的.

父模块无法访问子模块的的私有项, 子模块却可以访问父模块, 以及之上的私有项.

pub关键字

需要将最底层的也都pub. 才能公开.

使用super引用模块

就是使用父模块.

使用self引用模块

就是引用自身模块中的项.

结构体和枚举的可见性

结构体: 将结构体设置为pub, 字段也是私有的.

枚举: 将枚举设置为pub, 所有字段对外可见.

use的使用

可以减少代码.

可以使用{}简化引用方式.

简化前:

use std::collections::HashMap;
use std::collections::BTreeMap;
use std::collections::HashSet;
 
use std::cmp::Ordering;
use std::io;
use std::io;
use std::io::Write;

简化后:

use std::collections::{HashMap,BTreeMap,HashSet};
use std::{cmp::Ordering, io};
use std::io::{self, Write};

也可以使用*引用模块下的所有公共项. 当然这个要格外小心.

受限的可见性

没看.

注释和文档

代码注释

八字原则: 围绕目标, 言简意赅.

  1. 行注释: //
  2. 块注释: /* */

文档注释

文档注释需要位于lib类型的包中.

可以使用md语法.

被注释的对象需要pub对外可见.

  1. 文档行注释: ///
  2. 文档块注释: /** **/

使用cargo doc可以直接生成HTML文件, 使用cargo doc --open自动打开浏览器.

包和模块注释

需要放在包, 模块的最上方.

  1. 行注释: //!
  2. 块注释: /*! */

文档测试

/// `add_one` 将指定值加1
///
/// # Examples11
///
/// ```
/// let arg = 5;
/// let answer = world_hello::compute::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

使用cargo test就可以运行测试.

保留测试, 隐藏文档

使用 #.

/// ```
/// # // 使用#开头的行会在文档中被隐藏起来,但是依然会在文档测试中运行
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// #    try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Divide-by-zero"))
    } else {
        Ok(a / b)
    }
}

还有更多内容

我实在是懒得看了, 疲惫了.

格式化输出

格式化输出常用的

  1. print! 将格式化文本输出到标准输出, 不带换行符
  2. println! 带换行符. 用于调试输出.
  3. format! 将格式化文本输出到String字符串. 用于生成格式化字符串.
  4. eprint! 输出到标准错误输出
  5. eprintln! 同上, 多个换行

{}与{:?}的选择

写代码需要调试的时候用{:?}, 其他情况用{}.

{:?}实现了debug特征.

{}实现了display特征.

{:#?}

相比起{:?}更具有格式, 更加优美.

// {:?}
[1, 2, 3], Person { name: "sunface", age: 18 }
// {:#?}
[
    1,
    2,
    3,
], Person {
    name: "sunface",
}