rust 程序设计语言入门(一)

news2024/9/22 15:51:18

本文是阅读《Rust程序设计语言》的学习记录,配合视频《Rust编程语言入门教程》食用更佳

环境搭建

  • windows下载rustup_init.exe,点击安装,默认选择msvc的toolchain,一路default即可

  • 解决下载慢的问题,在powershell中修改环境变量

    $ENV:RUSTUP_DIST_SERVER='https://mirrors.ustc.edu.cn/rust-static'
    $ENV:RUSTUP_UPDATE_ROOT='https://mirrors.ustc.edu.cn/rust-static/rustup'
    
  • 使用vscode作为ide开发,安装rls或ra插件,推荐ra(rust编程语言服务器,承担语法检查等工作)

  • 使用原生的cargo进行new、build和run的操作

  • 下载第三方包速度慢的问题,在~/.cargo/config中换源

    [source.crates-io]
    registry = "https://github.com/rust-lang/crates.io-index"
    replace-with = 'ustc'
    [source.ustc]
    registry = "git://mirrors.ustc.edu.cn/crates.io-index"
    ## registry = "https://mirrors.ustc.edu.cn/crates.io-index"
    
  • 配置文件,包名等信息,添加依赖,由ra自动拉取

    [package]
    name = "hello"
    version = "0.1.0"
    edition = "2018"
    [dependencies]
    rand = "0.3.14"
    
  • vscodetask.jsonlaunch.json配置,参考cargo-tutorial

Hello world

Rust 程序设计语言能帮助你编写更快、更可靠的软件。开发者正在使用Rust在系统和工具领域对当前已有的项目进行重构。Rust的编译器将内存和代码的检查聚焦在程序逻辑方面。不可否认的是,Rust是一门面向未来的语言,在操作系统,嵌入式,音视频分析等领域大展身手。同时,Rust吸收了前辈的诸多优势,正在不断修补并增加新的特性来完善自身。

本文在学习Rust编程指南过程中的一些记录,参考资料为

  • rust-lang/book

  • Rust程序设计语言(中文版)

Rust 是一门预编译(ahead-of-time compiled)语言,可以直接分享和运行编译后的可执行文件

通常使用cargo工具创建Rust项目,具备完善易用的工具链(区别于C/CPP),cargo是Rust的构建系统和包管理器,可以胜任构建代码、下载依赖库,以及编译这些库等任务。

$ cargo new rusttour # 自动创建git项目
$ ll -al
drwxr-xr-x 1 Administrator 197121   0 Feb  7 10:01 .git/
-rw-r--r-- 1 Administrator 197121   8 Feb  7 10:01 .gitignore
-rw-r--r-- 1 Administrator 197121 152 Feb  7 10:01 Cargo.lock
-rw-r--r-- 1 Administrator 197121 177 Feb  7 10:01 Cargo.toml
drwxr-xr-x 1 Administrator 197121   0 Feb  7 10:01 src/
drwxr-xr-x 1 Administrator 197121   0 Feb  7 10:01 target/

Cargo.toml为cargo项目的配置文件,和golang非常类似,使用TOML格式进行配置

$ cargo check # 快速编译检查,比build快得多
$ cargo build
$ cargo run
$ cargo build --release #优化编译

cargo使得不同平台下Rust项目的管理方式统一,便于项目的开发和维护

通用概念

参考官方文档给出的示例程序学习基本的语法概念。如果对其他语言的语法有所接触,会发现导入依赖包和c#相似,而语法和cpp类似。

语法的细节不用过多的纠结(和熟练度相关),只需要关注一些比较特殊的语法即可

  • 导入外部模块(crate)的方式为use,通过::区分名称空间。库 crate 可以包含任意能被其他程序使用的代码,但是不能独自执行。rand为外部依赖,需要在Cargo.tomldependencies中添加
  • 使用println!打印到标准输出
  • 变量的初始化,涉及到Rust的静态强类型系统,具备自动类型推断。
  • 由于是强类型语言因而需要考虑类型转换。类型通过:标识,和python类似的类型标注
  • 创建新的变量类型时可以复用已有的变量名,即类型遮蔽(shadow)
  • Rust的错误处理的语法比较特殊,流式写法的风格
  • Rust的模式匹配语法
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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;
            }
        }
    }
}

Rust语法中较为特殊的部分

  • 默认变量不可变,可变需要用mut修饰,但是变量终究和常量(const)不同。

    let x = 10;
    let x = x + 1; // 遮蔽,创建了新的变量
    let mut x = 10; 
    x = x + 1; // 可变
    const y = 10; // 常量
    
  • 数组在栈上分配的已知固定大小的单个内存块,此处可能会产生索引越界错误,panicked at 'index out of bounds'

  • 函数签名中,必须明确声明每个参数的类型。返回值可以是表达式(没有分号)

    fn plus_one(x: i32) -> i32 {
        x + 1
    }
    
  • 三种循环语句的写法,break的特殊用途(标签跳转,循环语句返回值)

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

所有权

Rust的所有权系统,使得无需垃圾回收器(garbage collector)即可保证内存安全。“没有手动管理内存的经历可能无法体会Rust所有权系统的良苦用心”

编程语言对于内存的管理无非以下三种

  • 垃圾回收机制
  • 手动分配和释放内存
  • 编译器检查内存管理

Rust采用第三种方式(通过所有权系统管理内存),考虑堆和栈内存的区别。所有权系统的目的是最大限度对堆内存进行管理。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针pointer),即分配堆内存。

所有权规则

(1)Rust 中的每一个值都有一个被称为其 所有者owner)的变量。

(2)当所有者(变量)离开作用域,这个值将被丢弃

永远记得在alloc之后使用free释放内存,这通常是由gc来完成的。程序编译过程中可能需要将申请一段未知大小的内存,这段内存使用可能随着程序运行而变化,因而必须在堆中申请。

Rust通过作用域判断变量是否继续使用(自动释放这部分内存),在变量离开作用域是调用drop函数释放内存

(3)值在任一时刻有且只有一个所有者。

以下代码在堆中申请字符串内存,并将指针赋值给变量s1,之后将s1赋值给s2。在main函数执行完毕后,s1和s2失效被丢弃,因而这段内存可能会被连续释放两次(二次释放

Rust 永远也不会自动创建数据的 “深拷贝”,Rust的以上行为被成为移动操作(而非浅拷贝)

let s1 = String::from("hello");
let s2 = s1; // 移动 move
println!("{}, world!", s1); // 报错

如果需要深拷贝,可以使用clone

let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝
println!("s1 = {}, s2 = {}", s1, s2);

Rust 有一个叫做 Copy trait 的特殊标注,如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。实现了 Drop trait 的类型就不能再使用 Copy trait。

实现了copy trait的类型有:整型,布尔,浮点,字符和元组(当且仅当元素为前四种)

let x = 5;
let y = x; // 在栈上拷贝数据
println!("x = {}, y = {}", x, y);

当变量为函数的参数时,以上的结论仍旧有效

fn main() {
  let s = String::from("hello");
  takes_ownership(s); // s没有实现copy trait,s移动到函数中,之后不可用
}
fn takes_ownership(some_string: String) {
  println!("{}", some_string);
  // drop 方法,释放some_string内存
}

当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。以下函数中所有权在函数内外转移,如果在函数后仍旧需要使用变量,则必须将变量作为返回值转移出来(否则失效)。这种操作显然很繁琐

fn takes_and_gives_back(a_string: String) -> String {
  a_string  // 返回 a_string 并移出给调用的函数
}

引用和借用

使用引用,可以使用值但不获取其所有权(意味着变量不会失效),这种操作称为借用

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // 传引用
    println!("The length of '{}' is {}.", s1, len); // string仍可用
}
fn calculate_length(s: &String) -> usize {
    s.len()
    // string没有转移
}

如果需要修改引用的值,需要将引用标注为可变的。有以下原则

  • 在同一时间,只能有一个对某一特定数据的可变引用(避免数据竞争产生的未定义行为)

    let r1 = &mut s;
    let r2 = &mut s;
    
  • 无法在拥有不可变引用的同时拥有可变引用(避免写行为影响读操作)

    let mut s = String::from("hello");
    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    let r3 = &mut s; // 大问题
    
  • 一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。意味着只要使用过一次,上面的两个引用即离开作用域,规则刷新。

    let mut s = String::from("hello");
    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    println!("{} and {}", r1, r2); // 此位置之后 r1 和 r2 不再使用
    let r3 = &mut s; // 没问题
    
  • Rust编译器会检测悬垂指针(指向的内存可能已经被分配给其它持有者)

    fn dangle() -> &String {
        let s = String::from("hello");
        &s // 返回字符串 s 的引用,报
    }
    

切片

同样是为了避免已经存在的索引失效,通过使用切片让编译器主动检查

字符串切片时String 中部分值的引用,“字符串 slice” 的类型声明写作 &str,实际上字符串的字面值就是切片。切片也是引用(指向部分数据),默认不可变。

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s); // 返回切片 &str
    s.clear(); // s.clear()会获取s的可变引用,从而报错
    println!("the first word is: {}", word);
}

结构体和枚举

结构体

不得不说各种语言的数据结构都是类似的,Rust的结构体类似于golang的结构体。结构体是一种类型,每部分可以分别对应不同类型。

结构体不允许只将某个字段标记为可变,想要修改成员必须将整个结构体声明为可变

fn main() {
    let mut user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername"),
        active: true,
        sign_in_count: 1,
    };
    user1.email = String::from("anotheremail@example.com");
}

复用其他的结构体,.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值

fn main() {
    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

为了方便调试,直接使用println!宏需要结构体实践display trait。但是可以指定debug模式显示更多信息

#[derive(Debug)] // 指定结构体实现Deubg trait
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    println!("rect1 is {:?}", rect1);
    // println!("rect1 is {:#?}", rect1);
}

继续看结构体的方法实现,类似于golang和python

impl Rectangle {
    fn area(&self) -> u32 { // &self 实际上是 self: &Self 的缩写
        self.width * self.height
    }
    fn can_hold(&self, other: &Rectangle) -> bool { // 多个参数
        self.width > other.width && self.height > other.height
    }
}

在vscode中借助插件能够看到结构体目前实现了两个implementation

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7OUh1nt7-1676200228390)(assets/image-20230208145057388.png)]

不指定self为第一个参数的称为关联函数,使用结构体名和 :: 语法来调用这个关联函数

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

枚举

枚举类型能够增强代码的可读性,咋一看和结构体类似。但是枚举能够在同一种类型中定义不同的结构,尽管可以用4个结构体(4种类型)实现和以下相同的逻辑,但是无疑增加了复杂度。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

同样可以为枚举实现方法

impl Message {
    fn call(&self) {
        // 在这里定义方法体
    }
}

在标准库中Option枚举类型处理空值(空指针)。空值Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。

Option枚举类型被包含在了 prelude(预导入模块),可以不需要 Option:: 前缀来直接使用 SomeNone

在对 Option<T> 进行 T 的运算之前必须将其转换为 T确保该变量不为空,确保代码的安全性

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

包管理和crate

Rust的包管理主要涉及到以下模块系统

  • Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate。
  • Crates :一个模块的树形结构,它形成了库或二进制项目。
  • 模块Modules)和 use: 允许你控制作用域和路径的私有性。
  • 路径path):一个命名例如结构体、函数或模块等项的方式

回顾下cargo new 创建的新项目结构,如果src/main.rs 存在则是二进制crate,如果 src/lib.rs 存在则是库crate。根crate和包的名称相同

paakage的要点

  • 包含一个cargo.toml,描述如何构建crate
  • 只能包含0或1个库crate
  • 可以包含任意数量binary create(在src/bin路径下)

惯例

  • binary create 的 create root 为 /src/main.rs,编译的入口,和package 名称相同
  • library create 的 create root 为 /src/lib.rs,编译的入口,和package 名称相同
image-20230208161159032

将mod拆分为多个文件

file not found for module `hosting`
to create the module `hosting`, create file "src\front_of_house\hosting.rs" or "src\front_of_house\hosting\mod.rs"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pufesco9-1676200228396)(assets/image-20230208163540664.png)]

错误处理

Rust将错误分为可恢复的错误(Result)与panic

默认情况下Rust会进行调用栈回溯,清理调用栈数据会有性能损耗,也可以 配置不需要清理调用栈(交给os清理)

[profile.release]
panic = 'abort'

当主动触发panic时,报错会提示开启backtrace,例如宏 panic!("crash and burn");,只有在debug模式下才能看到栈的回溯信息

thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

其他语言中通过try…catch捕获异常,而Rust则具备Result枚举类型(同样是预导入的)对结果进行模式匹配。Result有两个成员Ok和Err例如

let f = File::open("hello.txt");
let f = match f {
    Ok(file) => file,
    Err(error) => match error.kind() {
        ErrorKind::NotFound => match File::create("world.txt") {
            Ok(fc) => fc,
            Err(e) => panic!("Problem creating the file: {:?}", e),
        },
    	other_error => panic!("Problem opening the file: {:?}", other_error),
	}
};

当异常嵌套时,match语法会变得难以理解,因而有unwrap和expect的写法(实际上和match结果相同)

  • 如果 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!

  • expect和unwrap的用法一致,但是在触发panic!可以指定error信息

let f = File::open("hello.txt").unwrap();
let f = File::open("hello.txt").expect("Failed to open hello.txt");

此外,错误可以不在当前作用域处理,而是向上传播(将Result作为函数返回值),仍旧可以用?简写

  • 如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。
  • 如果值是 ErrErr 将作为整个函数的返回值,就好像使用了 return 关键字一样
let mut f = match f {
    Ok(file) => file,
    Err(e) => return Err(e),
};
let mut f = File::open("hello.txt")?; // 和上面的代码一致

泛型和trait

泛型太常见了,主要目的是使用泛型来编写不重复的代码,听说隔壁golang也要支持泛型了.

主要考虑在以下方式使用的泛型

  • 函数
  • 结构体
  • 枚举
  • 方法

Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失

Rust 通过在编译时进行泛型代码的 单态化monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程

trait 告诉Rust编译器某种类型具有哪些可以与其他类型共享的功能

  • trait bounds 指定泛型是任何拥有特定行为的类型
  • trait和接口(interface)的概念类似但是有所区别

不同类型可能具有相同的方法,称为共享相同行为,原则如下

  • 只有方法签名没有实现
  • trait有多个方法,每个独占一行
  • 实现trait的类型需要提供方法实现(在trait中没有默认实现时),如果有默认实现会进行方法重载
  • 类型或trait必须至少有一个是本地crate中定义的
  • 无法为外部类型实现外部trait(为标准库vec实现标准库的display trait)

以上的定义实际上和接口一致,下面的各种概念可以直接联系接口理解

// src/lib.rs
pub trait Summary {
    fn summarize1(&self) -> String;
    fn summarize2(&self) -> String { // 默认实现
        String::from("(Read more...)")
    }
}
impl Summary for NewsArticle {
    fn summarize1(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

有了trait我们可以实现多态了,方法的类型为实现的trait,参数可以将实现该trait的类型传入

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

trait实现适用于较为简单的情景,较为复杂的情景可以使用语法糖(trait boundary)简化,以下代码和上面的代码意义相同

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

对于实现多个trait的情形,trait boundary 的写法如下,参数同时实现了Summary和Display

pub fn notify(item: impl Summary + Display) {
pub fn notify<T: Summary + Display>(item: T) {
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {

对于上面的第三行中写法较为冗长,可以继续通过语法糖简化,类似C#的写法

fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
	  U: Clone + Debug
{

将trait作为参数返回 ,函数的返回类型只能为同一种(即使实现了同样的trait )

fn returns_summarizable() -> impl Summary {

甚至可以在有条件指定泛型方法的实现,以下只有实现了Display + PartialOrd的类型才有cmp_display方法

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {

总结一下,trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。

在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误

生命周期

https://rustwiki.org/zh-CN/book/ch10-03-lifetime-syntax.html

生命周期的概念时Rust与众不同的地方之一,较为难以理解(需要更多例子说明),Rust每个引用都有自己的生命周期(在作用域内保持有效)。总的来说感觉是通过复杂的语法标注减少安全隐患

大多数情况下生命周期都能够隐式推断,在特定情况下需要手动标注生命周期

生命周期存在的目的是避免悬垂引用(指向已经释放的内存)

Rust借用检查器会判断引用的生命周期(作用域范围)是否小于被借用的对象,如果不小于说明对象已经释放但是借用仍然存在,出现悬垂引用。

let r: &i32;
{
    let x = 5;
    r = &x;
} // r的生命周期大于x,出现悬垂引用
println!("r: {}", r);

更复杂的例子例如,函数返回参数借用,但是不确定返回x和y,x和y的生命周期不一定是相同的,所以无法推断需要手动标注

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

生命周期标注并不会改变引用的生命周期,只是描述多个引用之间的生命关系

// 泛型生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { 
    // 表示参数和返回值的生命周期一致
    // 泛型中的'a, 表明任何标注了'a的引用,其生命周期都不能短于'a
    // 任何不满足这个约束条件的值都将被借用检查器拒绝
    // 借用检查器并不知道具体引用的生命周期,只是检查是否符合约束条件
    // 整个的检查都是在编译阶段完成的
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

对于以下示例,尽管string1的生命周期满足result的要求,但是string2的生命周期在打印result之前就已经结束,由于生命周期会取两者重叠的部分(较小值)作为返回引用的生命周期,因此借用检查器不允许编译通过,显示错误string2 does not live long enough,borrowed value does not live long enough

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

因此,逻辑上只对x参数进行标注即可,但是由于y同样是返回引用,因此编译器需要标注生命周期,编译器报错^ lifetime 'a required。修改为以下可以通过编译

fn longest<'a>(x: &'a str, y: &str) -> &'a str { 
	x
}

对于结构提来说,引用可以是成员,为了避免悬垂需要保证成员的生命周期不小于结构体本身

struct ImportantExcerpt<'a> {
    part: &'a str,
}

生命周期省略的规则在一定程度上减轻了编码者的心智负担。例如对于方法定义来说,self参数的生命周期会自动赋给所有输出参数的生命周期。规则如下

  • 每一个是引用的参数都有它自己的生命周期参数
  • 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
  • 如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,那么所有输出生命周期参数被赋予 self 的生命周期

静态生命周期('static)是一个特殊的生命周期,在程序的持续时间内都有效,例如字符串生命值,但是这个要慎用

下面有一个综合泛型,trait和生命周期的例子

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

最终总结就是

  • 返回的引用必须和某个参数的生命周期匹配
  • 生命周期标注不影响生命周期,只是静态的检查
  • 函数生命周期的目的在于描述参数和返回值之间的关系,一旦有联系那么必须通过明确标注的方式进行描述
  • 结构体引用成员的生命周期不小于结构体实例本身

可见为了避免悬垂引用的问题,编译器将压力给到了编码者,尽管有生命周期省略的规则,但是负担仍旧很重

单元测试

测试函数的三个操作

  • 准备数据
  • 运行被测试的代码
  • 断言结果

Rust 中的测试就是一个带有 test 属性标注的函数。属性(attribute)是关于 Rust 代码片段的元数据

使用assert!宏,可以额外指定自定义信息

assert! 
assert_eq! // 失败时自动使用debug格式打印参数,要求参数实现了PartialEq和Debug trait
assert_ne!

示例如下

pub fn add_tow(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use crate::add_tow;

    #[test]
    fn if_add_twos() {
        assert_eq!(4, add_tow(2));
    }
    
    #[test]
    fn if_add_twos_info() {
        let result =  add_tow(2);
        // assert!(5 == result,"failed to add two for 3, get wrong value {}",result);
        assert_eq!(5,result,"failed to add two for 3, get wrong value {}",result);
    }
}

测试代码预期的panic行为

#[test]
#[should_panic]
// #[should_panic(exepcted = "should include this txt")] // 检测发生panic具体的panic内容匹配
fn test_panic() {
    panic!("shoule panic");
    // panic!("should include this txt");
}

在测试中,也可以通过返回Result枚举的方式作为返回类型编写测试

  • 返回OK,测试通过
  • 返回Err,测试失败
#[test]
fn test_result() -> Result<(),String>{
	if 1 == 1 {
		Ok(())
	} else {
		Err(String::from("error info"))
	}
}

控制测试的运行逻辑

cargo test会构建一个Test Runner的可执行文件,逐个运行测试函数并报告结果

Cargo测试的默认行为

  • 并行测试
  • 执行所有测试
  • 捕获(不显示)所有输出
cargo test -- --test_threads=1 // 并行测试的线程数
cargo test -- --show-output // 默认通过的测试不会打印pringln!宏的内容
cargo test 

运行忽略的测试

cargo test -- --ignored // 执行被忽略标记的测试
// lib.rs
#[test]
#[ignore] // 忽略耗时测试
fn test_panic() {
    panic!("shoule panic");
    // panic!("should include this txt");
}

#[cfg(test)] 单元测试标注测试mod,只有执行test操作才会编译此部分

测试模块的#[cfg(test)] 标注告诉 Rust 只在执行 cargo test 时才编译和运行测试代码,而在运行 cargo build 时不这么做

Rust允许测试私有函数,未导出

对于集成测试,在项目根目录下创建test文件夹,每个文件都被视作单独的crate

可以创建common文件夹作为集成测试的公用逻辑,不在测试范围内

注意:

  • 如果是binary create ,则不能在 tests 目录下创建集成测试,无法把main.rs导入作用域
  • 只有 library crate 才能暴露函数给其他crate用

最后,总结下,Rust将程序安全相关的问题从运行时提前到了编译期间,迫使开发者思考程序中可能存在的不安全因素

  • 默认不可变即只读的变量,写需要单独声明
  • 尽可能避免无效,未定义内存的使用
  • 尽可能避免数据争夺和未定义行为(悬垂指针,空指针
  • 主动触发panic(可恢复错误),避免缓冲区溢出(例如索引越界)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/341627.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2.7、进程调度的时机、切换与过程、方式

1、进程调度的时机 进程调度\color{red}进程调度进程调度&#xff08;低级调度&#xff09;&#xff0c;就是按照某种算法从就绪队列中选择一个进程为其分配处理机 进程在操作系统内核程序临界区\color{red}操作系统内核程序临界区操作系统内核程序临界区中不能\color{red}不能…

网络连通性测试(ping/tcp)

网络连通性首先要看ping是否能够通其次&#xff0c;测试TCP是否能通方式一、iperf3/iperf2可以测试tcp的连接一方作为server&#xff0c;一方作为client来连接&#xff0c;但是iperf3和iperf2不兼容server&#xff1a;iperf3/iperf2 -s 172.20.0.36client: iperf3/iperf2 -c 17…

亚马逊、沃尔玛卖家自养号退款经验和测评技术

今天给大家介绍下在做亚马逊、沃尔玛退款自养号中的经验&#xff0c;众所周知&#xff0c;自养号最重要的是养号的环境&#xff0c;包括系统的纯净度&#xff0c;下单的信用卡以及其他的一些细节。 环境系统市面上有很多&#xff0c;鱼龙混杂&#xff0c;比如什么lumi&#xf…

Java测试——junit的使用(2)

排序 我们同一个类下的多个用例的执行顺序是不确定的&#xff0c;如果需要指定固定的顺序&#xff0c;则需要在类上加这个注解 TestMethodOrder(MethodOrderer.OrderAnnotation.class)然后在想要第一个执行的用例上加上 Order(1)第二个执行的用例上注解&#xff1a; Order(…

爬虫圈,常见的加密手段,你应该了解一下

常见加密手段 实验介绍 本实验为大家介绍一下常见的加密技术&#xff0c;掌握之后&#xff0c;可以在反爬时加入各加密算法&#xff0c;从而提高爬虫采集难度&#xff0c;本实验为大家介绍三类加密&#xff0c;其一是消息摘要算法/签名算法&#xff0c;其二是对称加密&#x…

十四、MySQL 约束详解

文章目录一、定义完整性约束1.1 实体完整性1.1.1 主键约束1.1.2 自增列&#xff1a;AUTO_INCREMENT1.1.3 候选键约束或唯一约束(UNIQUE KEY)1.2 用户定义完整性1.2.1 非空约束1.2.2 CHECK约束1.2.3 DEFAULT约束1.3 参照完整性1.3.1 FOREIGN KEY 约束的作用1.3.2 主表(父表)从表…

趣味三角——第10章——(sinx)/x

第10章 函数(sinx)/x I call our world Flatland, not because we call it so, but to make its nature clear to you, my happy readers, who are privileged to live in Space. (我称我们的世界为平面国&#xff0c;这样称呼它并不是我的本意&#xff0c;而是为了让你们明…

【Linux】Rsync基于rsync-daemon认证的使用(rsync服务端与客户端发配置、排错思路、使用示例、优缺点及使用场景)

一、Rsync基于rsync-daemon认证的使用与 ssh 认证不同&#xff0c;rsync 协议认证不需要依赖远程主机的 sshd 服务&#xff0c;但需要远程主机开启 rsyncd 服务&#xff0c;本地 rsyncd 服务可不必开启。另外&#xff0c;rsync 协议认证不是直接使用远程主机的真实系统账号&…

【C++之容器篇】造轮子:list的模拟实现与使用

目录前言一、关于list1. 简介2. 成员类型二、默认成员函数1. 构造函数1. list()2. list(size_t n,const T& val T())和list(InputIterator first,InputIterator last)2. 拷贝构造函数3. 析构函数4. 赋值运算符重载函数三、迭代器1. 普通对象的正向迭代器2. const对象的正向…

多线程

标题创建多线程方式一&#xff1a;继承线程中常用方法和优先级多窗口卖票创建多线程方式二&#xff1a;实现Runnable接口多窗口卖票&#xff08;使用Runable方式&#xff09;进程的生命周期同步代码块解决实现Runable的线程安全问题方法一方法二使用同步方法处理实现Runable的线…

结构体熟练掌握--实现通讯录

魔王的介绍&#xff1a;&#x1f636;‍&#x1f32b;️一名双非本科大一小白。魔王的目标&#xff1a;&#x1f92f;努力赶上周围卷王的脚步。魔王的主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王.&#x1f525;&#x1f525;&#x1f525; ❤️‍&#x1…

帮助中心在线制作工具推荐这4款,很不错哟!

根据用户咨询问题是否解决的情景&#xff0c;分为三个部分&#xff0c;首先帮助中心恰好有用户需要咨询的问题&#xff0c;用户可以通过点击相关问题即可解决自己的问题&#xff0c;其次&#xff0c;用户第一眼没有在帮助中心解决问题&#xff0c;有个搜索框&#xff0c;用户的…

为什么开发人员应该在 2023 年学习 Docker 和 Kubernetes

开发者你好&#xff0c;如果你想在 2023 年学习新的工具和技术&#xff0c;那么你应该考虑学习 Docker 和 Kubernetes&#xff0c;这是在这个微服务和云计算时代创建和管理容器的两个最重要的工具。随着微服务和云计算的兴起&#xff0c;Docker 和 Kubernetes 已经成为软件开发…

gg又来深圳

我们都喜欢的DGGgg是我在TCL的朋友&#xff0c;刚毕业的我们在TCL度过了一段非常欢快的时光&#xff0c;gg也是我们几十人中在TCL呆的特别久的&#xff0c;先是在深圳&#xff0c;然后转去了惠州&#xff0c;后面在惠州买房、结婚、定居、生娃。前几年举家回了西安、也从TCL离职…

ChatGPT到底是个啥 - 它甚至会和狗说话

写在前面&#xff1a;博主是一只经过实战开发历练后投身培训事业的“小山猪”&#xff0c;昵称取自动画片《狮子王》中的“彭彭”&#xff0c;总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域&#xff0c;如今终有小成…

2023.2.12(总结)

今天主要就是下午进行了一个测试&#xff0c;有三个困难版的题目我没有写出来&#xff0c;打算今天晚上好好磨磨&#xff0c;这里主要就只放一个题目。 C - Don’t be cycle Editorial / Time Limit: 2 sec / Memory Limit: 1024 MB Score : 300300 points Problem Statemen…

Spring缓存指定Redis做为缓存中间件Demo笔记

文档地址D:/Test10/redisdemo和springcachedemo E:/FTPshangchuang/smbms 一下是SpringBoot整合Redis的初略配置,引入Redis依赖 想自己测试的话 链接&#xff1a;https://pan.baidu.com/s/14hdBzdjtFu0lYmZUhy_DuA 提取码&#xff1a;j0m8 Redis配置文件 package com…

总览 Java 容器--集合框架的体系结构

前言 我们在讲 Java 的数据类型的时候&#xff0c;单独介绍过数组&#xff0c;数组也确实是开发程序中常用的内存类型之一&#xff0c;不过 Java 内置的数组限制颇多&#xff0c;所以此后扩展出了List这种结构&#xff0c;与之类似的Set、Queue 这些内存中的容器都被放在了 Co…

浅谈明暗水印

前言 水印&#xff08;Watermark&#xff09;是一种能让人识别纸上图案的技术&#xff0c;当光线照射纸张时&#xff0c;纸张上会显现出各种不同阴影&#xff0c;这些阴影组成的图案就是水印。 水印常常起到验证货币、护照、邮票、政府文件或者其他纸制文件的真实性的作用。 …

什么是热迁移?90%的企业都理解错误

科技的发展&#xff0c;新冠的冲击&#xff0c;让市场竞争愈发激烈。尽管云计算服务为企业免除了基础硬件的建设和维护成本&#xff0c;当企业需要进行业务跨架调整、升级维护、环境测试等场景而进行云迁移&#xff0c;其过程中所带来的停机时间&#xff0c;就变得尤为头疼了。…