Rust语法基础
本文源自观看B站Rust编程语言入门教程记录下来的重点笔记
视频链接:Rust编程语言入门教程(Rust语言/Rust权威指南配套)
文章目录
第三章
变量与可变性
shadow:定义了一个变量后,用let再给同名变量赋值即为shadow,可改变原值(原值不一样的类型也可以)。
如果用let mut定义了一个变量类型后,这个变量不能赋值给不同类型。
数据类型
rust是静态编译语言,在编译时必须知道所有变量的类型。有时可能的类型比较多,编译器无法自动推断,就必须添加类型标注,否则编译会报错(如把String转为整数的parse方法)
标量类型:整数、浮点、布尔、字符
- 整数字面值 ex:十进制:98_222,十六进制:0xff,八进制:0o77,二进制:0b1111_000,字节:b’A’。允许使用类型后缀如57u8;整数的默认类型是i32(速度较快)
- 整数溢出:调试模式下会检查整数溢出,如果溢出程序在运行时会panic;在发布模式下(-- release)编译,rust会执行“环绕”操作比如u8是0-255,256会变成0···但程序不会panic
- 浮点类型:f64为默认类型,因为在现代cpu上f64(双精度)和f32(单精度)速度差不多,而且精度更高。
- 布尔类型:1字节,true与false
- 字符类型:rust中的字符类型char是Unicode标量值的表示,使rust在处理字符串时能准确地表示Unicode字符,使用单引号,4字节,Unicode标量值能表示比ASCII多得多的字符:甚至emoji;Unicode没有“字符”概念,因为它是基于Unicode标量的(由一个或多个Unicode代码单元组成)
复合类型:
可以将多个值放在一个类型里,rust提供了两种基础的复合类型:tuple和数组。
Tuple:
- tuple中每个位置对应一个类型,各元素类型不必相同;长度固定,声明后无法改变
- 可以用模式匹配来解构一个tuple来获取元素的值
- 用.索引号访问tuple里的元素
- ex:
let tup(i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; println!(tup.1);
数组:
- 可以将多个值放在一个类型里,每个元素类型相同,数组长度是固定的
- 放在栈上而不是堆上,保证有固定数量的元素。(vector更灵活)
- 表示形式:
let a:[i32; 5] = [1, 2, 3, 4, 5]
或let a = [3(初始值); 5(元素个数)];
- 访问:数组是栈上分配的单个块的内存,可以使用索引访问数组的元素
- 如果访问越界,那么编译时会通过,但运行时会报错(panic),不允许继续访问相应地址的内存
函数
声明使用fn关键字,针对函数和变量名rust都适用snake case命名规范:所有字母都是小写,单词之间使用下划线分开。并且不像C++函数生命位置与调用顺序无关(没有前置声明)
- 参数:行参(parameters)实参(arguments),在函数签名里,必须声明每个参数的类型
- 函数体中的语句与表达式:函数体由一系列语句组成,可选的由一个表达式结束;rust是基于表达式的语言;语句不返回值所以不能将let将一个语句赋值给一个变量
- 函数的返回值:在->符号后声明函数返回值的类型,但不可以为返回值命名;函数体中最后一个表达式的值为返回值
控制流
if,else
循环:
- loop:用break 加表达式的值退出
- while
- for:
for i in elements.iter()
类似于迭代器,更安全且简洁 - range:指定一个开始数字和一个结束数字,Range可以生成他们之间的数字(不包含结束,左开右闭),rev方法可以反转Range。
for number in (1..4).rev() {}
输出321
第四章
所有权
所有权是rust最独特的特性,让rust无需垃圾回收器(GC)就可以保证内存安全。
- 所有程序在运行时都需要必须管理它们使用计算机内存的方式
- 有些内存有垃圾收集机制,在程序运行时,它们会不断地寻找不再使用的内存(如C#和java)
- 在其他语言中,程序必须显示的分配内存和释放内存
- 但rust采用了第三种方式:
- 内存是通过一个所有权系统管理的,其中包含一组编译器在编译时进行检查的规则
- 当程序运行时,所有权不会减慢程序运行的速度(因为内存管理相关的工作都发生在编译时)
Statck vs Heap:
在像rust这样的系统级编程语言下,一个值在stack还是在heap上有重大影响;在代码运行时这两者都是你可用的内存,但它们的结构很不相同。
-
Stack按值的接受顺序来存储数据(压入栈),按相反的顺序将它们移除(弹出栈)(后进先出)
-
所有存储在栈上的数据都必须拥有已知的固定的大小
- 编译时大小未知的数据或者运行时大小发生改变的数据都需要存放在堆上
-
把值压到栈上不叫分配
-
因为指针是固定大小的所以可以存放在栈上
- 但如果想要实际数据,必须使用指针来定位(使用栈中的指针指向堆中的数据)
-
Heap内存组织差一些,当把数据放入heap时会请求一定数量的空间
-
操作系统在堆中找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址;这个过程叫做在heap上进行分配
-
把数据压到栈上要比在heap上分配快得多:因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在栈的顶端
-
然而在heap上分配空间需要做更多的工作:操作系统首先找到一块儿足够大的空间存放数据,然后要做好记录方便下次分配
访问数据:
- 访问堆上的数据要比在栈上更慢,因为需要通过指针才能找到堆中的数据
- 对于现代的处理器来说,由于缓存的缘故,当指令在内存中跳转的次数越少,访问速度越快
- 如果数据存放的比较近,访问速度就快一些(栈)
- 如果数据之间的距离比较远,访问速度就慢一些(堆)
- 在堆上分配大量的空间也需要时间
函数调用:
当代码调用函数时,值被传入到函数(包括指向堆区的指针);函数本地的变量被压入到栈上,当函数结束后,这些值会从stack上弹出
所有权存在的原因:
解决存放在堆中数据的内存浪费问题;加入一个结构体中存放了一个字符串类型(动态数据类型,在堆上分配)和一个整形(固定类型大小,在栈上分配),如果多次复制该结构体,会造成结构体中的字符串在堆上分配多次造成内存空间浪费,这时rust凭借所有权可以将字符串字段的所有权转移给结构体的所有者,当所有者离开作用域时,字符串字段的内存将被自动释放,从而避免重复占用堆上的内存。 ex:let s = String::from("hello"); let p = Person { name: s, age: 30 };
在这个例子中,一个字符串值"hello"被动态分配在堆上,并将其所有权赋值给变量s。然后,将s的值赋值给Person结构体的name字段,这意味着字符串的所有权被转移给了Person结构体变量p。当p变量离开作用域时,字符串的内存将被自动释放。
所有权规则、内存与分配
所有权规则:
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能拥有一个所有者
- 当所有者超出作用域时,该值将被删除
String类型:
- 一个String由三部分组成:
- 一个指向存放字符串内容的内存的指针
- 一个长度len(存放字符串内容所需要的字节数)
- 一个容量(从操作系统总共获得内存的总字节数)
- 上面这些东西都存放在栈上,存放字符串内容的部分在堆上
- 创建:
let s = String::from("Hello");
- String类型的值可以修改,而字符串字面值不可以修改,因为它们处理内存的方式不同
- 为了支持可变性,需要在堆上分配内存(通过调用String::from)来保存编译时未知的文本内容;而字符串字面值在编译时就知道其内容了,其文本内容直接被硬编码到最终的可执行文件里了(速度快,高效,因为其不可变性)
内存与分配:
rust中对某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给系统(内存释放);即自动调用drop
函数
变量和数据交互的方式:
- 移动(Move):s2复制s1,s1失效(相当于将s1移动到s2);rust隐含的一个设计原则:不会自动创建数据的深拷贝;就运行时的性能而言,任何自动赋值的操作都是廉价的
- 克隆(Clone):如果真想对堆上的String数据进行深度拷贝,而不仅仅是栈上的数据,可以使用克隆方法
Stack上的数据:复制(Copy)
- Copy trait,一般存放在栈上的数据类型自动默认有该接口
- 如果一个类型实现了这个trait,那么旧的变量在赋值后仍然可用
- 如果一个类型或者该类型的一部分实现了Drop trait,那么rust就不允许它再去实现Copy trait了
- ex:
let x = 5;
let y = x;
println!("adda {:?}", x); // Output:5
// 这段代码中的变量 x 拥有整数值 5 的所有权,然后将其赋值给变量 y。由于 i32 类型是 Copy trait 的实现类型,因此这个赋值操作不会移交整数值的所有权,而是复制一份新的值给 y,因此 x 和 y 都拥有整数值 5 的所有权。
所有权与函数
在语义上将值传递给函数跟把值赋给变量是类似的:将发生移动或复制(取决于不同的变量类型)
引用与借用
把引用&作为函数参数的行为叫做借用
可变引用(&mut):
- 有一个重要的限制:在特定作用域内,对某一块数据只能有一个可变的引用,但可以有多个不可变的引用
- 这样的好处是可在编译时防止数据竞争
- 以下三种行为下会发生数据竞争:
- 两个或多个指针访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
- 可以通过创建新的作用域,来允许非同时的创建多个可变引用
- 不能同时存在可变引用和不可变引用,因为这时如果再对引用的变量操作会报错
悬空引用:
- 悬空指针:一个指针引用了内存中的某个地址,而这块内存可能已经释放了
- rust可以保证引用永远不是悬空引用,编译器会保证引用在离开作用域之前数据不会离开作用域
字符串切片:
- 指向字符串中一部分内容的引用
&s[..]
- 字符串切片在 Rust 中是不可变引用类型。字符串切片是
&str
类型的变量,它是对原始字符串的一个不可变引用。因为它是一个引用类型,所以它本身不拥有所引用的数据的所有权,而是借用了原始字符串的所有权。同时,由于它是不可变引用类型,所以它不能修改原始字符串中的数据
第五章
struct定义和实例化
- 使用struct关键字,并为整个struct命名
- 要想使用struct需要创建struct的实例:
- 为每个字段指定具体的值,不能遗漏某一字段
- 无需按照声明的顺序指定
- 使用点标记法访问
- 一旦struct实例是可变的,那么实例中所有的字段都是可变的
- 字段初始化可以简写;当字段名与字段值对应变量名相同时,就可以使用字段初始化简写的方式
- stuct更新语法
- tuple struct
- struct里可以存引用,但需要生命周期
输出格式化
因为简单的类型变量自带Display这个trait所以用{}可以直接输出,但是像struct这种类型则需要加上#[derive(Debug)]
来显示打印调试的功能,这样就可以使用{:#?}
来格式化输出
struct方法
- 方法和函数类似:fn 关键字、名称、函数、返回值
- 方法与函数不同之处:
- 方法是在sturct(或enum、trait对象)的上下文中定义
- 第一个参数是self,表示方法被调用的struct实例
ex:
struct Rectangle {
width: u32,
length: u32,
}
impl Rectangle {
fn area(&self) -> u32{
self.width * self.length
}
}
方法调用的运算符
- Rust没有指针用的->运算符
- Rust会自动引用或解引用;在调用方法时就会发生这种行为
- 在调用方法时自动添加&、&mut或*,以便object可以匹配方法的签名
关联函数
- 可以在impl块里定义不把self作为第一个参数的函数,他们叫关联函数(不是方法) 例如:
String::from()
- 关联函数通常用于构造器
- :: 符号
impl块
每个struct允许拥有多个impl块
第六章
枚举
枚举允许我们列举所有可能的值来定义一个类型
// 定义枚举
enum IpAddr{
V4,
V6,
}
// 枚举值
let four = IpAddr::V4;
// 枚举的变体都位于标识符的命名空间下,使用两个冒号::进行分隔
允许数据附加到枚举的变体中:
优点:
- 不需要额外使用struct
- 每个变体可以拥有不同的类型以及关联的数据量
ex:
enum IpAddr{
V4(u8, u8, u8, u8), // 无论是数字、字符串、结构体还是枚举类型都能嵌入
V6(String),
}
为枚举定义方法:也使用impl关键字
Option枚举
定义与标准库中,在预导入模块中,描述了某个值可能存在(某种类型)或不存在的情况
Rust没有Null但是它提供了一个类似Null概念的枚举 - Option<T>
标准库中的定义:
enum Option<T> {
Some(T),
None,
}
// 这三者都包含在预导入模块中,均可直接使用
这样Option<T>
比Null好在哪儿呢?
Option<T>
与T是不同的类型,不能把Option<T>
直接当成T来用(比如相加减)
Match
强大的控制流运算符,允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码;模式可以是字面值、变量名、通配符…
ex:
enum Car{
laosilaisi,
benchi,
aodi,
hamadi,
}
fn trans(car: Car) -> String {
// 必须要穷举所有的模式
match car {
Car::laosilaisi => String::from("劳斯莱斯"),
Car::benchi => String::from("奔驰"),
Car::aodi => String::from("奥迪"),
// Car::hamadi => String::from("哈马迪"),
_ => (),// 但使用_通配符可以替代其余没列出来的值
}
}
fn main() {
let c:Car = Car::aodi;
let x = trans(c);
println!("{}", x);
}
if let
- 处理只关心一种匹配而忽略其他匹配的情况
- 更少的代码,更少的缩进
- 放弃了穷举的可能
- 相当于match的语法糖
- 搭配else
ex:
if let Car::aodi = car {
String::from("奥迪")
} else {
String::from("其他车")
}
第七章
路径
私有边界:
- 模块不仅可以组织代码,还可以定义私有边界
- 如果想把函数或struct等设为私有,可以把它放到某个模块当中
- Rust中所有条目(函数、模块、方法、enum、struct、常量)默认是私有的
ex:
mod front_of_house {
// 注意以下模块和方法如果不加pub在下方调用时都会报错
pub mod hosting {
pub fn add_to_house() {}
}
}
pub fn eat_at_res() {
crate::front_of_house::hosting::add_to_house(); // 绝对路径
front_of_house::hosting::add_to_house(); // 相对路径
}
super关键字:
- 用来访问父级模块路径中的内容,类似文件系统中的…
比如上面的例子:
pub fn add_to_house() {
// 因为上级有两个mod所以要使用两次super
super::super::eat_at_res(); // 相对路径
crate::eat_at_res(); // 绝对路径
}
pub struct:
- pub放在struct前:
- struct是公共的
- struct字段默认是私有的
- struct字段要单独设置pub来变成公有
pub enum:
- pub放在enum前:
- enum是公共的
- enum的变体也都是公共的
use的习惯用法:
- 函数:将函数的父级模块引入到作用域(指定到父级)
- struct,enum等指定到完整路径(指定到本身)
- 同名条目:指定到父级;或使用
as
创建别名
将模块内容移动到其他文件:
- 模块定义时,如果模块后边是“;”,而不是代码块:
- Rust会从与模块同名的文件中加载内容
- 模块树的结构不会变化
- 随着模块逐渐变大,该技术可以让你把模块的内容移动到其他文件中
第八章
Vector
创建Vector:
let v: V<i32> = Vec::new();
let v = vec![1, 2, 3]; // 初始值创建方式用vec!宏
// 即使初始化时没有指明存储类型,rust可以上下文推断出类型
let mut v = Vec::new();
// 更新vector
v.push(3);
v.push(4);
读取Vector中的元素:
- 索引(类似字符串切片)
&v[0]
- 数组越界时发生panic
- get方法(返回的事Option值)
- 数组越界时返回None
所有权和借用规则
- 不能在同一作用域内同时拥有可变和不可变引用
ex:
let v = vec![1, 2, 3];
let first = &v[0];
v.push(6); // 报错
// 试想一下vector工作原理,vector是动态分配的连续容器,如果push之后容器找了一块扩充的新的内存地址,而first还指向原内存地址,那么就会发生内存泄漏(所以借用规则防止该情况发生)
vector遍历
ex:
let v = vec![1, 2, 3];
// 注意是引用,如果要修改v里的值就用&mut,然后用解引用符号*
for i in &v{
println!("{}", i);
}
println!("{:?}", v);
vector还可以与枚举enum配合使用
String
更新String:
push_str()
方法:把一个字符串切片附加到Stringpush
方法:把单个字符附加到String+
:连接字符串;相当于add(self, s: &str)format!
:连接多个字符串- 和
println!
类似,但返回字符串 - 不会获得参数的所有权
- 和
Rust不允许对String进行索引
切割String:
- 可以使用
[]
和一个范围来创建字符串的切片- 必须谨慎使用
- 如果切割时跨越了字符边界,程序就会panic
遍历String
- 对于标量值:
chars()
方法 - 对于字节:
bytes()
方法
HashMap
- HashMap用的比较少,不在预导入模块(Prelude)中
- 标准库对其支持较少,没有内置的宏定来创建HashMap
- 数据存储在heap上
- 同构的,在一个HashMap中:
- 所有的K必须是同一种类型
- 所有的V必须是同一种类型
HashMap和所有权:
- 对于实现了Copy trait的类型类如i32,值会被复制到HashMap中
- 对于拥有所有权的值例如String,值会被移动,所有权转移给HashMap
- 如果将值的引用插入到HashMap,值本身不会移动
更新HashMap:
- entry方法:检查指定的HashMap中Key是否对应一个V;如果不存在V则使用
or_insert()
方法插入
第九章
Result枚举
enum Result<T, E>{
Ok(T), // T:操作成功时Ok变体里返回的数据类型
Err(E), // E:操作失败时Err变体里返回的数据类型
}
处理Result的一种方式:match表达式
和Option一样,Result及其变体也是由prelude带入作用域的
ex:
use std::{fs::File};
fn main() {
let file = File::open("hello.txt");
let _f = match file {
Ok(file) => file,
Err(e) => {
panic!("Error opening file {:?}", e)
}
};
}
unwrap:match表达式一个快捷方法
- 如果Result结果是Ok,返回Ok里面的值
- 如果Result结果是Err,调用panic!宏
expect:和unwrap类似,但可指定错误信息
传播
?运算符:
传播错误的一种快捷方式
- 如果Result是Ok,Ok中的值就是表达式的结果,然后继续运行程序
- 如果Result是Err,Err就是整个函数的返回值,就像使用了return
- 可以链式调用
?与from函数:
用于针对不同的错误原因返回同一种错误类型,只要每个错误类型实现了转换为所返回的错误类型的from函数
?与main函数:
- main函数的返回类型是:()
- main函数的返回类型也可以是:Result<T, E>
- Box是trait对象
第十章
泛型
- 提高代码复用能力,处理重复代码的问题
- 泛型是具体类型或其他属性的抽象代替:
- 你编写的代码相当于一个模版,里面有一些占位符
- 编译器在编译时将“占位符”替换为具体类型
- 例如:`fn largest(list: &[T]) -> T {…}
- 类型参数:
- 很短,通常就一个字母
- 驼峰命名
- T:type的缩写
函数定义中的泛型
- 参数类型
- 返回类型
Struct定义中的泛型
- 可以使用多个泛型的类型参数
#[derive(Debug)]
struct Car<T, U> {
door: T,
people: U,
}
fn main() {
let x = Car {door:3, people: "sdad"};
println!("{:#?}", x);
}
Enum定义中的类型
- 可以让枚举的变体持有泛型数据类型
- 例如
Option<T>, Result<T, E>
- 例如
方法定义中的泛型
- 为struct或enum实现方法的时候,可以在定义使用泛型
- 注意:
- 把T放在impl关键词后,表示在类型T上实现方法
- ex:
impl<T> Point<T>
- ex:
- 只针对具体类型实现方法(其余类型没实现方法):
- ex:
impl Point<f32>
- ex:
- 把T放在impl关键词后,表示在类型T上实现方法
- struct里的泛型类型参数可以和方法的泛型类型参数不同
泛型代码的性能:
- 使用泛型的代码和使用具体类型的代码运行速度是一样的
- 单态化:
- 在编译时将泛型替换成具体类型的过程
trait
格式
pub trait Summary{
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub baseline: String,
}
impl Summary for NewsArticle{
fn summarize(&self) -> String {
format!("{}, {}", self.headline, self.baseline)
}
}
Trait作为参数
- impl Trait语法:适用于简单情况
- Trait bound语法:可用于复杂情况
- 使用+指定多个Trait bound
- Trait bound使用where子句
- 在方法签名后指定where子句