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

news2024/9/23 20:17:53

本文是阅读《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/341050.html

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

相关文章

libxlsxwriter条件格式

今天来看一个libxlsxwriter的高级用法&#xff1a;一个条件格式的示例。 说它“高级”&#xff0c;也是基于非Excel专家的小白们的视角。对&#xff0c;没错&#xff0c;本小白正是这样的小白。 1 一个简单的问题 来看我们今天的场景问题&#xff1a;有一列数据&#xff0c;有…

操作系统(一): 进程和线程,进程的多种状态以及进程的调度算法

文章目录前言一、进程和线程1. 进程2. 线程二、进程和线程的区别(面试常问)三、进程调度算法3.1. 批处理系统3.2. 交互式系统3.2.1 时间片轮转3.2.2 优先级调度3.2.3 多级别反馈队列3.3. 实时系统四、进程的状态五、进程同步5.1 什么是进程同步5.2 进程同步应该遵循的几点原则前…

Qt 学习(四) —— QGridLayout栅格布局

目录一、QGridLayout布局规则二、创建QGridLayout三、成员函数1. 控件间距2. 可拉伸控件&#xff08;弹簧&#xff09;3. 最小行高/列宽4. 行数和列数5. 锁定纵横比6. 添加控件7. 添加布局8. 设置栅格布局原点位置9. 操作布局项9.1 访问布局项9.2 删除布局项9.3 通过索引获取布…

Git教程个人分享:如何将一个本地项目上传至远程仓库的流程

前言&#xff1a; 今天来分享一下&#xff0c;关于Git的一些教程&#xff0c;同时这也是我自己曾今学习Git时候的笔记&#xff0c;之所以更&#xff0c;也是方便后期自己可以去回顾&#xff0c;当然后面也会出一部分关于Git其他操作方面的内容。 这次我们分享的是&#xff0c…

基于JavaScript的Web端股票价格查看器——大道

&#x1f436; 基于JavaScript的Web端股票价格查看器——大道 一、项目背景 当下互联网发展迅速&#xff0c;互联网已经不断向传统金融领域渗透。在互联网上有大量金融领域的数据&#xff0c;如何利用好这些数据&#xff0c;对于投资者来说是十分重要的一件事情。股票价格实时…

JavaSE学习day4_01 循环for,while,do...while

1. 循环高级 1.1 无限循环 for、while、do...while都有无限循环的写法。 最为常用的是while格式的。 因为无限循环是不知道循环次数的&#xff0c;所以用while格式的 代码示例&#xff1a; while(true){} 1.2 跳转控制语句&#xff08;掌握&#xff09; 跳转控制语句&…

MySQL 插入数据

数据库与表创建成功以后&#xff0c;需要向数据库的表中插入数据。在 MySQL 中可以使用 INSERT 语句向数据库已有的表中插入一行或者多行元组数据。 你可以通过 mysql> 命令提示窗口中向数据表中插入数据&#xff0c;或者通过PHP脚本来插入数据。 语法 以下为向MySQL数据表…

51单片机——步进电机实验,小白讲解,相互学习

步进电机简介&#xff1a; 步进电机是将电脉冲信号转变为角位移或多线位移的开源控制元件。在非超载的情况下&#xff0c;电机的转速&#xff0c;停止的位置只取决于脉冲信号的频率和脉冲数&#xff0c;而不受负载变化的影响&#xff0c;即给电机加一个脉冲信号&#xff0c;电机…

Android - 自动系统签名

一、系统签名 以下是两类应用开发场景&#xff1a; 普通应用开发&#xff1a;使用公司自定义 keystore 进行签名&#xff0c;如&#xff1a;微信、支付宝系统应用开发&#xff1a;使用 AOSP 系统签名或厂商自定义 keystore 进行签名&#xff0c;如&#xff1a;设置、录音 系…

数学建模拓展内容:卡方检验和Fisher精确性检验(附有SPSS使用步骤)

卡方检验和Fisher精确性检验卡方拟合度检验卡方独立性检验卡方检验的前提假设Fisher精确性检验卡方拟合度检验 卡方拟合度检验概要&#xff1a;卡方拟合度检验也被称为单因素卡方检验&#xff0c;用于检验一个分类变量的预期频率和观察到的频率之间是否存在显著差异。 卡方拟…

第一部分:简单句——第二章:简单句的补充

简单句的核心构成&#xff1a;一主一谓 主语/宾语/表语 可以变成名词/代词/doing/to do 谓语动词有四种核心变化&#xff1a;三态 一否 时态语态情态否定 简单句的核心&#xff1a;将简单句给写对 简单句的补充&#xff1a;将简单句给写的更好、更充分 简单句的补充 1、限定…

计算机网络之HTTP04ECDHE握手解析

DH算法 离散读对数问题是DH算法的数学基础 &#xff08;1&#xff09;计算公钥 &#xff08;2&#xff09;交换公钥&#xff0c;并计算 对方公钥^我的私钥 mod p 离散对数的交换幂运算交换律使二者算出来的值一样&#xff0c;都为K k就是对称加密的秘钥 2. DHE算法 E&#…

DNS 原理入门指南(一)

DNS 是互联网核心协议之一。不管是上网浏览&#xff0c;还是编程开发&#xff0c;都需要了解一点它的知识。 本文详细介绍DNS的原理&#xff0c;以及如何运用工具软件观察它的运作。我的目标是&#xff0c;读完此文后&#xff0c;你就能完全理解DNS。 一、DNS 是什么&#xff1…

记录每日LeetCode 1138.字母板上的路径 Java实现

题目描述&#xff1a; 我们从一块字母板上的位置 (0, 0) 出发&#xff0c;该坐标对应的字符为 board[0][0]。 在本题里&#xff0c;字母板为board ["abcde", "fghij", "klmno", "pqrst", "uvwxy", "z"]&#x…

Linux重定向符、管道符讲解

目录 重定向 将命令与文件进行互动 输出重定向 输入重定向 管道符 将命令与命令互动起来 重定向 将命令与文件进行互动 重定向分类 一般情况下&#xff0c;Linux命令运行时都会打开一下三个文件 标准输入文件&#xff1a;stdin文件&#xff0c;文件描述符为0&#xff0c;Li…

自定义ESLint规则开发与使用

自定义eslint及使用 项目结构 |-eslint-plugin-demo //自定义eslint插件项目 | |-demo-app // 使用自定义eslint的测试应用 |-README.md 项目效果&#xff1a; github项目地址 自定义ESLint环境准备 安装脚手架 执行下列命令来安装开发eslint的脚手架。 yo(y…

中小学信息学相关编程比赛清单及报名网站汇总(C++类)

1、NOI系列比赛(CSP-J CSP-S NOIP NOI APIO CTSC IOI ISIJ等) NOI官网 NOI全国青少年信息学奥林匹克竞赛https://www.noi.cn/ 2、蓝桥杯青少年创意编程大赛 https://www.lanqiaoqingshao.cn/home 3、中国电子协会考评中心

数据结构 - Set 与 Map 接口介绍(TreeMap,HashMap,TreeSet,HashSet类)

文章目录前言1. Set / Map接口2. TreeSet类3. TreeMap 类4. HashSet 与 HashMap4.1 HashSet / HashMap 底层哈希表4.2 解决哈希冲突总结✨✨✨学习的道路很枯燥&#xff0c;希望我们能并肩走下来&#xff01; 编程真是一件很奇妙的东西。你只是浅尝辄止&#xff0c;那么只会觉得…

神经网络基础知识

神经网络基础知识 文章目录神经网络基础知识一、人工神经网络1.激活函数sigmod函数Tanh函数Leaky Relu函数分析2.过拟合和欠拟合二、学习与感知机1.损失函数与代价函数2. 线性回归和逻辑回归3. 监督学习与无监督学习三、优化1.梯度下降法2.随机梯度下降法(SGD)3. 批量梯度下降法…

JavaScript系列之new运算符

文章の目录一、什么是new&#xff1f;二、new经历了什么过程&#xff1f;三、new的过程分析四、其他作用参考写在最后一、什么是new&#xff1f; 众所周知&#xff0c;在JS中&#xff0c;new的作用是通过构造函数来创建一个实例对象。 像下面这样&#xff1a;&#xff08;和普…