Rust快速入门(二)

三个指令:

  • cargo run 执行

    • --release:

      由于使用run命令rust默认为debug模式,代码中很多debug数据就会打印,于是我们使用relsase参数就可以不输出debug的代码。

  • cargo check 校验是否能够通过编译

  • cargo build 打包为可执行文件

Cargo.toml & Cargo.lock

Cargo.toml:

是cargo特有的项目数据描述文件,存储了项目所有元配置信息。

Cargo.lock

是cargo工具根据同一项目的toml文件生成的项目依赖详细清单

所以一般情况下我们只需要修改Cargo.toml即可(这里可以联想到golang的mod和sum包管理文件)

所以当项目为一个可运行项目时,就上传Cargo.lock,如果是一个依赖库项目,那么就把它添加到 .gitignore 中。

项目依赖定义

Cargo.toml 中,主要通过各种依赖段落来描述该项目的各种依赖项:

  • 基于Rust官方仓库 crates.io,通过版本说明来描述

  • 基于项目源码的git仓库来描述

  • 基于本地项目的绝对路径或者相对路径,通过类 Unix 模式的路径来描述

为什么手动设置变量可变性

既要安全性,又要灵活性。一切选择皆是权衡。这样一来也可以减少runtime过程中多余的检查。

变量绑定

举个栗子:

rust 复制代码
let a = "hello"

这里就是把 "hello" 绑定到了 a。为什么不是赋值而是绑定呢?这里就要涉及到rust的核心原则------所有权,即任何内存对象都有主人,如果将 "hello" 绑定给 a 那么之前的主人就失去了 "hello" 所有权。

如果我们希望该变量可变那么我们需要作如下操作:

rust 复制代码
let mut a = "hello"

这样一来,我们就可以对a进行多次赋值。

当我们希望提前定义一些变量

使用下划线开头忽略未使用的变量。因为如果声明了变量却不使用,rust就会报warning,所以我们需要作如下操作:

rust 复制代码
let _a = "haha"

变量遮蔽

在rust中,允许声明相同的变量名,在后面的声明的变量会遮蔽掉前面声明的。

这样做的好处是:如果你在某个作用域内无需再使用之前的变量(在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。

基础类型

Rust分为两类:基本类型和复合类型

基本类型如下:

  • 数值类型:有符号整数(i8, i16,i32, i64, isize)、无符号整数(u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数

  • 字符串:字符串字面量和字符串切片 &str

  • 布尔类型:truefalse

  • 字符类型:表示单个 Unicode 字符,存储为 4 个字节

  • 单元类型:即 () ,其唯一的值也是 ()

注意:浮点类型虽然可以比较,但是在rust中 0.1+0.2 != 0.3 我们需要注意,当类型为f32相加时不会报错,但是如果为f64就会导致程序崩溃。

语句和表达式

在Rust中函数体是由一系列语句(statement)组成,最后由一个表达式(expression)来返回值:

rust 复制代码
fn main() {
    let res = add_with_extra(1, 1);
    println!("the result is {}", res)
}
​
fn add_with_extra (x:i32, y:i32) -> i32 {
    let x = x+1;
    let y = y+5;
    x+y
}

我们需要注意的是区别rust与其他语言,基于表达式是函数式语言的重要特征表达式总要返回值。

  • 语句:

    完成了一个具体的操作,但是并没有返回值,如:

    rust 复制代码
    let a = 8;
    let b: Vec<f64> = Vec::new();
    let (a, c) = ("hello", 1);

    由于let是语句,不是表达式,所以不能将let语句赋值给其他值

  • 表达式:

    表达式会进行求值,例如:

    rust 复制代码
    5+6
    let y = 6 // 6是一个表达式,求值后返回一个6

    注意:表达式后面不能跟分号(;),如果跟了分号表达式就不返回值了

    调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式。

函数

举例:

rust 复制代码
fn add(i:i32, j:i32) -> i32 {
    i+j
}

也可以直接放在main函数中,如:

rust 复制代码
fn main() {
    let res = add_with_extra(1, 1);
    println!("the result is {}", res);
​
    fn add(i:i32, j:i32) -> i32 {
        i+j
    }
    
    let res1 = add(1, 2);
    println!("the result1 is {}", res1);
}

所以综上,我们给出三条函数结论:

  • 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}

  • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可

  • 每个函数参数都需要标注类型(Rust 是静态类型语言,因此需要你为每一个函数参数都标识出它的具体类型)

特殊返回类型

  • 无返回值()

    单元类型()是一个零长度的元组,可以用于表示一个函数没有返回值:

    • 函数没有返回值,返回一个()

    • 通过 ; 结尾的语句返回一个()

    举例:

    rust 复制代码
    fn report<T: Debug>(item: T) {
      println!("{:?}", item);
    ​
    }
    ​
    //或
    ​
    fn clear(text: &mut String) -> () {
      *text = String::from("");
    }
  • 永不返回的发散函数

    当用 ! 作函数返回类型的时候,表示该函数永不返回( diverge function ),特别的,这种语法往往用做会导致程序崩溃的函数:

    rust 复制代码
    fn dead_end() -> ! {
      panic!("你已经到了穷途末路,崩溃吧!");
    }

所有权

在内存管理中,程序如何从内存空间申请内存,在不需要的时候如何安全释放内存是十分重要的部分,目前存在三种流派:

  1. 垃圾回收机制(GC):在运行时不断寻找不再使用的内存,如 Golang,Java。

  2. 手动管理内存分配与释放:在程序运行过程中,通过函数调用的方式来申请和释放内存,如Cpp。

  3. 通过所有权来管理内存:编译器在编译时会进行一系列检查。

所有权原则

  • Rust中每一个值都被一个变量所拥有,改变量称为值的所有者

  • 一个值同时只能被一个变量所拥有(一个值只能有一个所有者)

  • 当所有者(变量)离开作用范围时,这个值会被丢弃

举例说明:

rust 复制代码
let x = 1;
let y = x;

在上述语句中,由于int为基础类型在栈上分配,所以拷贝会快于所有权移动,所以如果打印输出的话,上述x,y都为1,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的。

然而在下述代码中:

rust 复制代码
let s1 = String::from("hello");
let s2 = s1;

则需要注意了,因为String是复合类型,所以s1 被赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了

和浅拷贝很类似,但是为了避免二次释放 前者失去了指针,所以我们称之为移动

Rust 永远也不会自动创建数据的 "深拷贝"

Copy特征

Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。

那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,这里可以给出一个通用的规则: 任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 。如下是一些 Copy 的类型:

  • 所有整数类型,比如 u32

  • 布尔类型,bool,它的值是 truefalse

  • 所有浮点数类型,比如 f64

  • 字符类型,char

  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是

  • 不可变引用 &T但是可变引用 &mut T 是不可以 Copy的

引用与借用

如果仅仅通过转移所有权的方式获取一个值,程序将会变得非常复杂。于是Rust采用了 借用(Borrowing) 这个概念来减少复杂度。

引用与解引用

常规引用是一个指针类型,指向了对象存储的内存地址,如:

rust 复制代码
let x = 5;
let y = &x;
​
assert_eq!(5,x);
assert_eq!(5,*y);

不可变引用

举例说明:

rust 复制代码
fn main() {
    let s1 = String::from("hello");
​
    let len = calculate_length(&s1);
​
    println!("The length of '{}' is {}.", s1, len);
}
​
fn calculate_length(s: &String) -> usize {
    s.len()
}

这里 s1 所有权传给了 calculate_length() 函数,但是我们无法修改s1。

可变引用

我们可以通过修改s1为可变变量,calculate_length() 接收可变引用来达到我们希望修改s1的目的。

rust 复制代码
fn main() {
    let mut s = String::from("hello");
​
    change(&mut s);
}
​
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

**但是!同一作用域,特定数据只能有一个可变引用!**这样做使Rust在编译期间就避免了数据竞争。

导致数据竞争的行为:

  • 两个或更多的指针同时访问同一数据

  • 至少有一个指针被用来写入数据

  • 没有同步数据访问的机制

可变引用和不可变引用不可以同时存在

  • 这里就和读写锁很类似,不希望造成脏读。

  • 在新编译器中,引用作用域的结束位置从花括号变成了最后一次使用的位置。

  • 对于上述这种编译器优化行为(找到某个引用在作用域(})结束前就不再被使用的代码位置),Rust称其为------Non-Lexical Lifetimes(NLL)

悬垂引用

也叫做悬垂指针,意为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向内存可能不存在或者已被其他变量重新引用。

在rust中,编译器可以确保引用永远也不会变成悬垂状态:当你获取数据引用后,编译器可以确保数据不会在引用结束前被释放,想要释放数据,必须先停止其引用的使用。

所以总结一下,借用规则如下:

  • 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用

  • 引用必须总是有效的

相关推荐
抽风侠2 分钟前
qt实现窗口的动态切换
开发语言·qt
liuweni12 分钟前
Next.js系统性教学:深入理解缓存交互与API缓存管理
开发语言·前端·javascript·经验分享·缓存·前端框架·交互
潘多编程20 分钟前
Spring Boot性能提升:实战案例分析
java·spring boot·后端
m0_7482561420 分钟前
Spring Boot 整合 Keycloak
java·spring boot·后端
AI人H哥会Java27 分钟前
【JAVA】Java高级:多数据源管理与Sharding:在Spring Boot应用中实现多数据源的管理
java·开发语言
Thomas_YXQ29 分钟前
Unity3D项目为什么要使用FairyGUI
开发语言·unity·游戏引擎·unity3d·游戏开发
uyeonashi41 分钟前
【C++】刷题强训(day14)--乒乓球匡、组队竞赛、删除相邻数字的最大分数
开发语言·c++·算法·哈希算法
AskHarries1 小时前
如何利用EasyExcel导出带有选择校验框的excel?
java·后端·spring cloud·excel
egekm_sefg1 小时前
【Golang】——Gin 框架中的模板渲染详解
开发语言·golang·gin
小码编匠1 小时前
C#上位机实现高效示波器功能
后端·c#·.net