Rust 基础入门
rust 基础总结
1
2023-08-29 初始化
导语
工作原因,需要抱紧 Rust, 那来吧,那个传说中的入门曲线….名不虚传…😂😒
这并非教程,是个人刷 Rust 圣经的记录,以备回看.
开胃菜
1 | // Rust 程序入口函数,还是 main,该函数目前无返回值 |
还有另外几个需要注意的点
- 字符串是双引号. 字符是单引号;
- 占位符只有
{}
,具体类型 编译器会知道,无需再写%d
啥的;
编译器行为
一些调试编写代码常用 trick
可以以注解形式,控制一些编译器行为, 这些行为方便了 原型时候 调试代码;
1 | 允许未使用变量 |
_x
下划线开头的变量将被 rust 编译器忽略,方便编写初始代码;
derive 派生特征: 类似注解,为对象实现默认特征
#[derive(Debug)]
: 见过很多了,println!("{:?}", s)
可以打印整个结构体;Copy
(插入) 复杂结构的打印输出
1 | struct Rectangle { |
下面这个结构体是打印不出来的, 编译器: 结构体没有实现 Display.. 难怪,结构体完全不同,能默认实现 Display 才怪.
1 | `Rectangle` doesn't implement `std::fmt::Display` |
按照提示换用 {:#?}
,编译器继续
1 | `Rectangle` doesn't implement `Debug` |
手动 impl Debug or 添加 #[derive(Debug)]
, 那就添加; 能输出了;
1 | rect1 is Rectangle { |
如果默认格式不满意就只能自行实现 Display 了;
还有另一种是 dbg!(xxx)
: 打印出 表达式值 文件名 行号 等信息;
- 输出到标准错误输出
stderr
,而println!
输出到标准输出stdout
。
1 | dbg!(rect1); |
变量绑定与解构
let a = 1
语法上等价于 赋值语句,这里意味着 a 与 1 的值绑定
- 涉及到了 所有权 的概念
默认 rust 变量不可变, 可变 -> let mut a = 2
.
- 不可变带来了内存安全和性能的提升
和 kotlin 的不可变一点也不像val
有些类似
_x
下划线开头的变量将被 rust 编译器忽略,方便编写初始代码;
变量解构: 与 py 中相同的概念,从变量中直接拆出部分内容;
1 | // 与 py 类似的语法 |
常量… rust 居然还有这个概念..
1 | // const 声明, 命名习惯上 全大写字母 下划线 |
- 不可变 变量 和 常量还是有点区别,
后面再详聊;- 常量 作用域是整个程序 or 模块. 常量值 和 类型 必须在编译时就确定;
变量遮蔽: 允许多个 let 声明同名变量, 后面会顶替前面; 前后是完全不同的变量,只是同名而已.
- 还有个作用域的事,
{ }
内继承外部定义的变量,但内部定义变量作用域仅在 内部; 最后 println! 输出的还是 let x = x+1 定义的 x;
1 | fn main() { |
对应 练习
基本类型
rust 不是动态语言,且因为所有权等概念的引入导致 类型相当相当重要;;;
- 必须指明变量类型,大部分情况下编译器足够聪明,不需要操心..但是总有二般
let guess = "42".parse().expect("Not a number!");
从字符串"42"
解析出整数, 编译器要能推断出这个,都能自己写程序了.- 这里必须显式补充上,要不然直接报错:
let guess: i32 = "42".parse().expect("Not a number!");
有 3 点 rust 与 c/c++ 完全不同
- 数值类型相当多,且泾渭分明. 数字后面可以跟着类型
let a = 8i8
- 不存在隐式类型转换,所有转换必须是显式;
- 数值可以直接调用方法…这一点又非常像 py..
a.is_nan
基本类型
- 数值类型:
- 有符号整数 (
i8
,i16
,i32
,i64
,isize
) - 无符号整数 (
u8
,u16
,u32
,u64
,usize
) - 浮点数 (
f32
,f64
) - 有理数、复数
- 有符号整数 (
- 字符串:字符串字面量 和 字符串切片
&str
- 布尔类型:
true
和false
- 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
- 单元类型: 即
()
,其唯一的值也是()
^21ffc3- 这是最新鲜的…似乎是类似 c/c++ 的
void
- 这是最新鲜的…似乎是类似 c/c++ 的
整数
(i8
, i16
, i32
, i64
, isize
) / (u8
, u16
, u32
, u64
, usize
) 没了
- 长度直接写在脸上了, 除了 isize / usize 是架构决定
- 默认 i32 一把梭子
十进制 98_200
十六进制 0x
八进制 0o
二进制 0b
整数溢出问题: 编译器 –debug 模式会检查,但 –release 就直接按照补码溢出来操作了…
- 处理这些情况,使用标准库的拓展即可:
wrapping_*
一切按照补码溢出操作;checked_*
溢出直接 None (rust 居然也有这个值);overflowing_*
是否溢出,返回 bool;saturating_*
值达到最大/最小值;
浮点数
f32 f64 类似于 float double
- 不能当作 hashmap 的 key,这个还是有点不一样
3 倍注意: 浮点数精度问题, C 中也常见,但 rust 要求更加严格;
1 | fn main() { |
都是 0.1+0.2 与 0.3 比较, f32 就相等, f64 就不相等;
- f32 下.加完都是
3e99999a
- f64 下精度高太多了,
3fd3333333333334
和3fd3333333333333
的比较.
防御性编程: (0.1_f64 + 0.2 - 0.3).abs() < 0.00001
是最好的解决办法;
NaN
数学上未定义结果统一 NaN… NaN != 0,无法用于比较,比较直接 painc;
x.is_nan()
防御性编程;
运算
普通运算 + - * / %
位运算 & | ^ ! << >>
序列
1..5
生成 1 到 4; 1..=5
生成 1 到 5;
字符同理 'a'..'z'
和 'a'..='z'
- 这是 py 吗…
有理数 复数
rust 标准库并没有支持有理数 复数,而是需要第三方库 num.
1 | cargo add num |
字符 布尔 单元
来个 rust 震惊: 所有的 Unicode
值都可以作为 Rust 字符, 包括单个的中文 日文 韩文 emoji 表情符号等等,都是合法的字符类型.
- 👽 我也是
4 字节. ''
单引号
bool: true false, 1 字节 (不是 1bit,看清楚)
单元类型; ()
有且仅有这个 ()
^e508df
- 看似无返回的函数返回的就是
()
- 占位但不占用任何内存, map 不关注值 只关注 key 时候,
()
可以作为值; (类似 go 的struct{}
)
语句 表达式
1 | fn add_with_extra(x: i32, y: i32) -> i32 { |
一般编程语言中并没有特别区分 语句 和 表达式, rust 则是分的很开.
表达式是要返回值; 语句是纯执行没有返回值;
- 函数就是表达式
- 表达式不能包含分号
这一块的概念触及了更深层次,新东西太多,作者省略了一些细节.具体请看 语句和表达式
- 似乎概念上 表达式的写法更加简洁,暂时没有更多实感.
函数
语法上 rs 的函数与带类型标注的 py 很像.
1 | fn add(i: i32, j: i32) -> i32 { |
- 返回可以不同 (表达式); plus plus2 等价;
- 关键词
fn
不同
一般是蛇形命名 (全小写 下划线);
类似 c 中的 void 无返回的函数,实际上返回的是 [[#^21ffc3|单元类型]]
发散函数
类似 void 的还是会返回 ()
,但 发散函数 是真的一去不回….
使用 !
作为函数的返回类型
1 | // 真 dead 函数 |
所有权 与 借用
堆 和 栈 的区别, 入栈要求固定大小,速度快; 堆则较为无序,速度慢;
粗看下来,有些类似 指针 + 所有权; 作用域 = 生命周期 ; 有些类似 面向对象语言中, 对象 生命周期的概念..通过编译器 严格遵守 生命周期 和 所有权, 避免了 对象 (?) GC..
感觉有点像 对象生命周期管理 的思想 用来管理 内存里的一切;;
先看一段非常典型的 c 内存错误..类似的炸弹会存在在任何地方..
1 | int* foo() { |
^fb6442
- 程序返回了一个指向局部变量的指针,局部变量销毁后就成了 野指针;
所有权和借用 还涉及到了 堆 和 栈
- 栈 出栈入栈要求长度类型确定,速度很快,长度确定因此找某个值也很快;
- 堆 不定长,不定类型,存入访问速度比栈慢,但能处理类型多很多.
c/c++ 中堆上数据跟踪非常复杂,但 rust 要全管理起来 -> 所有权 和 生命周期
所有权
3 个规则
- 每个值只有一个所有者;
- 一个值的所有者只能是 一个;
- 所有者离开作用域,值就 drop (drop!)
1 | // x 拥有所有权 |
let 会将 x 的所有权转移到 y,因此 x 不再有效, println 就会报错; –> 所有者仅 1 个
1 | // 但基本类型无碍 |
同样的逻辑下, 基本类型却没事… –> 基本类型是在 栈 中, 栈内复制非常快,因此 let 并没有发生所有权转移,而是复制了一份 x 的副本给 y;
- 其实是 Copy 特征
1 | let x: &str = "hello, world"; |
但是字符串不是在堆中吗 😵… 这里引出了 引用 的概念.. x 是指向字符串的引用,也在栈中,因此可以直接复制..
rust 永远不会自行创建 深拷贝,但又 x.clone()
手动执行深拷贝 (重复执行,对性能影响很大)
上面说的基本类型可以自动拷贝 (栈上的复制), 并不限于基本类型; –> 任何 不需要分配内存或某种形式的资源 就可以 自动拷贝 (也叫实现了 Copy);
- 基本类型
- 元组, 当且 包含类型也只有 基本类型时;
- 不可变 引用
变量传递给函数,函数返回值赋予变量 也有所有权的转移; // 这真没想到;
1 | let s = String::from("hello"); // s 进入作用域 |
这样来回传递当然非常麻烦 –> 借用和引用
- 非常类似 权限控制的指针,或者说就是;
let 的操作,对 复杂类型是 移动所有权, 对简单类型就是完整拷贝;
引用 和 借用
语法和 c/c++ 指针完全相同 & *
;
为了安全性, 默认引用是不能修改值的; –> 可变引用; let r = &mut s
还是为了完全性, 类似读者写者问题的解决方案:
- 不可变引用 与 可变引用 只能同时存在一个; ^xlg6a4
- 不可变引用数量 可以有多个, 可变引用 只能有一个;
引用的作用域 呃, 非常独特..(⊙﹏⊙)..
最开始时候, 引用作用域判定规则和变量一致,这没啥..
但是叠加上: 变量释放时必须没有任何引用; 要了命了… 写代码时候要时刻注意 变量 + 引用的范围,随时加中括号..//这谁受得了///
最新编译器中: 引用作用域等于最后一次使用该引用的位置 == 解放
1 | fn main() { |
对于这样的 引用作用域,还有个专门的名称: Non-Lexical Lifetimes(NLL)
悬垂引用/悬垂指针 类似 [[#^fb6442]] 的情况, 变量被 drop 了,引用/指针 还存在; rust 编译器会阻止这一切;
复合类型
复合类型这里会提到 字符串/切片 元组 结构体 枚举 还有 数组, 大都见过,但是 rust 中会非常不一样.
字符串 (和 Go 很像)
语法层面上 rust 只有一种字符串类型: str
,但 str
不可修改,所有权变化很乱, –> 字符串的切片 &str
(跟 go 一模一样), 可变就交给了 标准库的 String
类型
- 标准库还有更多类型 (
xxStr
),但常用就上面俩
rust 中字符是 unicode 4 字节,那么 字符串 肯定也是 4*字符量
的长度吧…并不是, 字符串是 utf-8.. 字符串中字符的长度是 1-4 变长…
- 这个特性使得字符串处理与其他语言完全不同 –> 禁止字符串索引访问,虽然有切片..
- 同时所有权等概念, 让这一切更加复杂
切片 (go 基本相同): 本质上不可变引用
let hello = &s[0..5];
&s[0..2];
和&s[..2];
|&s[4..len]
和&s[4..];
&s[0..len]
和&s[..];
- 特别的一点:
let s = "Hello, world!";
字符串字面量 s 是切片&str
类型
String
类型
1 | let s1 = String::new(); // 空的 String 对象 |
String
和 &str
转换
- 上面新建
String
里就是String::from("hello,world")
和"hello,world".to_string()
- 转换回去就直接取引用 (切片);
- 这里有隐式的类型转换 Deref 解引用
禁止字符串索引访问, 即使切片也得注意索引范围;
- utf8 变长,索引取到中间值无意义,直接报错;
- 性能问题,期望性能始终为 O(1) 但始终难以保证
字符串操作
s.push('!');
s.push_str("rust");
s.insert(5, ',');
s.replace_range(7..8, "R");
- 操作原字符串, 需要 mut; 仅适用于
String
s.replace("rust", "RUST")
s.replacen("rust", "RUST", 1)
- 返回新字符串,不需要 mut; 适用于
String
&str
删除: 都需要 mut ,仅适用于 String
- pop 删除最后一个字符;
- remove(5): 删除单个字节
- truncate(3): 删除索引到尾部 (字节)
- clear(): 清空
连接:
+
+=
: 相当于标准库的std::sting
的 add 方法; 要求第二个必须是&str
类型,返回String
类型;format
: 类似 print, 适用于String
和&str
:format!("{} {}!", s1, s2);
小坑
字符串内: 所有权的坑
调用方法 == 使用了引用, 非常容易出现 可变不可变 同时存在
1 | fn main() { |
代码看似挺正常的… 但是 s.clear()
就会出错….
- 传入 first_word 的是一个不可变引用, println! 调用的 2
- 但是但是,
s.clear()
是个方法,对自身修改,需要的 可变引用 - 可变引用 + 不可变引用 同时存在 + ![[Rust 基础入门#^xlg6a4]] == bong !
任何直接消费 非实现 copy 类型,都有所有权的转移
1 | fn main() { |
s1 在 let s3 = s1 + &s2;
就失效了
+
相当于 add 函数, s1 又是 String 非基本类型,所有权转移到了 add 内部 (相当于给函数传递了 变量 本身,所有权转移进去了)- add 执行完, s1 就被 drop 了…
其他
转义
1 | \x73 直接转成 ascII or unicode 输出 |
utf-8
1 | // 以 unicode 遍历字符串 |
以 utf-8 获取子串,标准库无能为力 –> utf8_slice
元组
任意类型 ()
常用于返回值 (这一点挺像 py)
访问: 解构 or x.2
1 | let tup = (500, 6.4, 1); |
结构体
语法和 C 很像, 但没有指针和分号; struct
,
分割 声明类型;
1 | struct User { |
创建实例时,每个字段都得初始化, 但 顺序无所谓.
1 | let user1 = User { |
访问结构体成员 x.a
; 修改成员要求 实例必须是 mut 才行
1 | // user1 必须声明成 mut |
结构体初始化和创建,每一个字段都得精心维护,太繁琐了,有一些简便写法
1 | // 函数参数与字段名 重名,可省略一个声明; |
已有实例创建新实例 (可能有 所有权转移)
- 所有权一旦转移, use1 就可能 一部分字段可访问,一部分无效;
1 | // ..展开已有实例; |
rust 结构体肯定不止 C 那样,还有几类特殊的结构体;
1 | // 省略字段名, 按照索引号访问 -> 也叫元组结构体 |
以上都没涉及到一个情况: 字段取到 引用 类型…. 在 学习生命周期前暂时不涉及
- 应该 引用 要在 变量本身生命周期 内, 而又套上 结构体 就更复杂了…
枚举
将一类的值全包起来,不限制类型;
- rust 枚举 可以关联其他类型 (这一点拓展了很多使用场景)
1 | enum PokerSuit { |
组织数据结构,屡试不爽…
1 | // 枚举嵌套结构体... 有点干了泛型的活.. |
Null 处理
java 中最恨这个,其次是 getter/setter; 一不留神就程序没了; 所以 rust 直接不要 null 了..
rust 中任何可能为 null 的类型,得套上 Option 枚举, Option<T>
和 T
不能直接运算,得配合 null 的处理才行. 相当于强制用户必须处理 null ..(习惯了,习惯了..吸氧..)
1 | enum Option<T> { |
^bce572
数组 (不可变数组 array)
前排提醒 这里数组特指 不可变数组 array; rust 中 可变数组 是 Vector ;
array 中括号 []
; 不可变; 定长; 类型一致; –> 在栈中;
- 定长,所以能 索引 访问了; 越界会直接 pannic…
- 快捷声明仅对 栈上 能直接 copy 的类型生效..
1 | let a = [1,1,1,1,1] |
不能直接 Copy 的类型有要用 –> std::array::from_fn
1 | use std::array::from_fn; |
当然集合类型都有切片
- array 类型是
[T;n]
, 切片是[T]
这俩千万别混淆了. - 切片创建代价非常小
- 切片类型
[T]
大小不固定, 切片引用类型&[T]
大小固定 (只是个引用), 因此引用更常见.
1 | let a: [i32; 5] = [1, 2, 3, 4, 5]; |
流程控制
if - else while loop 以及 continue 和 break
- 关键字还都蛮熟悉的, loop 是个死循环,在哪里见过呢,似乎是 vb..
- 关键是 rust 融合了很多高级语言的特性,本身定位又是系统编程..高低混合..这个乱啊..
continue
或break
多了一个标签 控制 (另类的goto
?)
1 | // 最常见 |
for 循环有个大坑 (所有权), 迭代集合 (非 copy 类型) 一定要传入引用, 除非你之后不打算再使用这个集合了..
1 | for x in XS {} // 集合迭代 |
1 | while true { } // 一切如常 |
标签: 标签名 :
流程, break or continue 可以指定 标签
- 这可比只能控制当前流程 好太多了
1 | 'outer: for i in 0. { |
模式匹配
Match / if Let
match 类似 switch,但更像 py 中的 switch;
- 模式绑定: 取出枚举类绑定的值; –> [[#null 处理]] 中 None 值的实际处理,一般就是配合 match ,在分支中进行.
1 | match target { |
解模式绑定
1 | enum Coin { |
当只想匹配一个条件时候 match –> if let
1 | let v = Some(3u8); |
matches!(value, pattern => epression)
value 值, pattern 是模式. 最后返回 bool 值.
1 | let foo = 'f'; |
变量遮蔽: 模式和模式匹配后的表达式是一个新的代码块, 在这个作用域内 同名变量 会发生 变量遮蔽 ^8963d3
解构 Option
1 | fn plus_one(x: Option<i32>) -> Option<i32> { |
Some(i)
会匹配到 [[#^bce572|Option]] 的 Some(T)
并且将 i 赋予具体的绑定值;
Some(4)
有具体值的,就只能匹配到确切的值;
模式适用场景
这一节是模式匹配,终于谈到 模式了 2333 😂
模式本身就是 rust 的特殊的语法, 用于匹配和解构数据结构的语法; 一般可由下面内容混合
- 字面值 | 解构的数组 枚举 结构体 或 元组 | 变量 | 通配符 | 占位符
哪里用到了模式
match 的分支, 每个分支就是一个模式.最后用 _
兜底
if let
单一模式
while let
循环单一模式 使用 loop + iflet or match 更繁琐;
1 | // Vec是动态数组 |
for (index, value) in v.iter().enumerate()
也是一种匹配模式,元组匹配迭代器
甚至 let a = 123
也是一种模式匹配… 匹配的值 绑定到 变量上…
- 就说闹不闹
类似的解构元组 let (x,y,z) = (1,2,3);
也是模式匹配….这里模式还包括了 变量个数.
函数的参数也是模式..我去, 就是个筐是吧..
if let
这里特殊一点, if let
允许不完全的匹配条件. -> 可驳模式匹配
1 | let Some(x) = some_option_value; // 可能还有 None,因此匹配失败 |
全模式列表
纯罗列..
字面值: 很简单 match 当 switch 来用;
命名变量: 前面提到的 [[#^8963d3|变量遮蔽]] 就是匹配命名变量时候遇到的.
- 命名变量就是
Some(y)
这样…值被赋予了 y,要是前面还有同名变量,就会有变量遮蔽;
1 | Some(y) => println!("Matched, y = {:?}", y), |
单分支多模式: 多个模式 |
序列匹配模式: 模式直接使用 1..=5
'a'..='j'
- 只有 数字 和 字符 可以这样用, 适当使用节省了大量模板代码
解构
前文中使用 解构直接拆分 结构体 元组 数组 引用等等,但是还不够,得加大药量.
1 | let Point { x: a, y: b } = p; // 解构结构体, 变量名 其实可以和结构体不一致 |
解构也没有层级限制,唯一的限制是你能不能看的下去..
1 | enum Color { |
解构数组: 定长不定长..
#todo
方法
又到了另一个与 go 相似的地方 /o(* ̄▽ ̄*)ブ
方法隶属于 object, 封装了对 属性的操作. rust 中
- object 定义 属性与方法分离,
impl
关键词, 多个impl
可组合. - 方法内访问自身
&self
(第一个参数,又很像 py) - object 可以是: 结构体 / 枚举 / 特征 (类似接口)
1 |
|
关联函数: 方法本身可以算带 *self
的关联函数, 关联函数调用上 观感与类方法更接近.
*self
其实是语法糖, 方法也是函数,也有所有权 生命周期 的问题
self
&self
&mut self
3 种, 多用&self
- 执意使用
self
那么所有权直接转入,然后实例本身会被释放掉…这一点很坑,千万注意.
还有一种用法是 方法与结构体字段名相同,实现 getter…/(ㄒoㄒ)/~~
- 结构体字段可设置为私有,增加安全性.
1 | impl Rectangle { |
泛型和特征
泛型
和 go 的泛型有得一拼.. 但 rust 又是那种精心设计而非为了兼容妥协的,又比 c++ 精简多了..所以现在就是 👽👽👽…
几个印象
- rust 泛型范围要比其他语言更广,结构体枚举都能用,甚至有 const 泛型.
- 0 成本,完全不会牺牲性能,总用代价吧? –> 单态化,为每种可能类型都实现一遍,程序大小 ++
一个示例: 动态数组最大值
- 符号还是熟悉的
T
,要提前声明在 函数名<T>
- 面向对象中的 extend 和 super 限制 T 的种类,rust 中则基于 特征 (类似于接口) 限定.
- 其他 T 的使用,没有太多差别.
1 | fn lagest<T: std::cmp::PartialOrd + Copy>(list: &[T]) -> T { |
结构体泛型
- 结构体名
<T>
,一样得提前声明; T
只能代表一种类型, 多种类型 就声明多个呗. 即使是这样 `struct Woo<T,U,V,W,X>
1 | struct Point<T, U> { |
枚举泛型: 大明湖畔的 Option
已经见过了
- 语法与结构体泛型基本一致
1 | enum Option<T> { |
方法泛型: 方法也是函数,自然也能用泛型.
- 一个声明中泛型的来源就是
impl
后面的<>中,Point<xx>
都是具体的类型 - 方法泛型中还可夹带 函数泛型,互不影响;
- 重点: 方法可以限定具体类型, 仅针对该类型实现方法; ^pkn22t
1 | impl<T> Point<T, T> { // Point<T, T> 是使用 T 不是声明 |
Const 泛型
: 值的泛型
- 上面种种泛型,T 代指的总是某个类型, 但在 rust 中还有一点特殊情况…需要待指某个具体值;
数组 [i32; 2]
和 [i32; 5]
是不同的类型, 这一点仅在 rust,其他语言似乎都是同一个类型.这是个数组类型就好了..
这个问题导致了,明明都是 i32 的数组,只是长度不同,就得编写基本相同的函数处理..导致大量重复代码;
- 改用数组切片,传入引用可以解决大部分,但并不是全部.
- 以前有的数组库限定长度 32,就是为每个长度实现一遍..简直…
值的泛型: 加上关键词 const
; 仅支持 整数 bool 和 char 类型
- 这个实际上在编译时就能确认 N 的具体值, 归根结底算是个语法糖.
- 类型的泛型,编译时无法确定到底是什么类型; 只能为每个可能都实现一遍;
1 | // N 是一个常量 |
还有 const 表达式 和 const 函数指针; 但书里暂时没有更多介绍
特征
特征 (类似 接口) 定义一组方法 的集合 // 会有类似 面向对象 鸭子类型的感觉…
- 真正定义的是一组 方法的签名 // 非常非常的 java 接口… 也会有类似 java 接口的用法
定义 trait X
特征名
1 | pub trait Summary { |
实现 impl X for m
, 调用 实例.x
1 | impl Summary for M { |
允许定义默认实现 (和 实现一个方法 没有区别)
1 | pub trait Summary { |
孤儿规则?? #todo ^bd02aa
特征也能用作 函数 入参 or 返回值 // 这不就是 java 接口吗…
入参
也叫特征约束
fn notify(item: &impl Summary)
和fn notify2<T: Summary>(item: &T)
等价,前者是语法糖,但是异常好读.
1 | // &impl Summary 异常好读 |
多重约束: 几个特征一起来
1 | // 两者等价 |
要是泛型参数再多几个,每个的特征约束也再来几个…一行不敢看了.. –> Where 约束,换个写法
1 | // 将泛型的约束 另起一行 |
当特征 遇到 [[Rust 基础入门#^pkn22t|方法泛型]] = 为指定类型 指定特征 实现 方法
- 这样的方式要比 java 接口更细致
1 | // 只有 T 同时实现 Display 和 PartialOrd 特征时才可以调用 cmp_display |
返回
当作 返回值
- 有一个限制: 即使 M N 都实现了 Summary,但返回两个类型时候 编译器还是提示报错;
- 这里得用 –> [[#特征对象]]
1 | fn return_test(flag: bool) -> impl Summary { |
其他
derive 派生特征: 类似注解,为对象实现默认特征
#[derive(Debug)]
: 见过很多了,println!("{:?}", s)
可以打印整个结构体;Copy
- 更多的参考 –> 派生特征 trait
调用方法时,需要特定 特征, as 又有很大限制 –> TryInto
- 尝试将类型转换为另一个类型, 返回
Result
类型; - 转换成功就包含值,转换失败就包含失败信息;
例子
特征这里和其他概念交叉太多了,还是例子更好理解;
1 | use std::ops::Add; |
特征对象
动态分发 / 动态分配
特征对象: 是一个引用, 指向 实现了 特征的 实例
- 参数的关键词是
dyn
实际上是动态分发关键词 - 真正创建时: 对实例取
&
orBox<T>
Box<T>
涉及智能指针, 被包裹的 T 会强制分配在 堆 上.
1 | trait Draw { // 特征 |
特征对象数组调用: components 元素仅实现了 Draw 特征,嗯 接口数组…
1 | pub struct Screen { |
动态分发
在运行时才能确定到底是那个实例的实现 –> 动态分发
- 相对的是静态分发, 类似于泛型的处理,编译时就完成了;
具体就不展开了, 有点搞不懂…
Self and Self
Self 代指 特征 or 方法类型 (class) | self 代指 实例
1 | // Self 代表的是 Button 类型 |
能够实现特征对象的 特征 == 要求 特征是 对象安全; 要求所有方法 (除了方法还有别的要求):
- 方法返回类型不能是
Self
- 方法没有任何泛型参数
特征 2
特征内容实在太多了,这是第二个章节, 其实是很多其他特性 与 特征 的结合使用
关联类型
特征中叠加进 类型; 比泛型声明更加精简;
1 | pub trait Iterator { |
关联类型 在多个参数时候, 能比泛型节约模板代码
1 | // A B 泛型必须深入到每个地方... |
似乎关联类型可以使用泛型了…已经是稳定版了..
默认泛型类型参数
这个和特征无关,任何用到泛型的地方都能用.
给泛型一个默认值 (T=i32), 在未指定泛型时,编译器使用默认值;
1 | trait Add<RHS=Self> { // 给 RHS 来个默认值 |
节省了很多模板代码… 仅此而已.
完全限定语法 (调用同名函数/方法)
结构体 实现 多个特征 和 方法, 这里每个结构都带有一个签名完全相同的函数..(闲的…)
- 调用顺序是 结构体的方法
- 其他特征的方法需要显式调用 –> 完全限定语法
完全限定语法: <Path>::<Function>(args)
显式指明函数/方法/关联函数的完整路径
- 特征的方法
Pilot::fly(&person);
- 多个特征的关联函数
<Dog as Animal>::baby_name()
(注意 as 关键词)
大部分情况下无需如此, rust 的编译器完全值得信赖.
特征的特征约束
泛型中能用 特征 作为约束条件, 那 特征也能作为 特征 的约束条件…// 直接说 类似 接口继承就完事了…
1 | use std::fmt::Display; |
OutlinePrin 依赖于 Display 特征, 任何实现 OutlinePrin 的 类型 必须先实现 Display
Newtype
^d732f3
没有什么是加一层解决不了的,如果不行那就再加一层.. from 鲁迅没说过
[[#^bd02aa|孤儿规则]] 的存在使得未在本作用域 定义的 特征 和 类型, 没法做啥文章, 但 newtype 绕过了这个限制.. –> 如直面意思,再包一层 (类型)
Vec<T>
和Display
均定义在标准库,无法为Vec<T>
实现Display
- 那可以 Wrapper 包含成员
Vec<T>
, 为 Wrapper 实现Display
啊.
1 | use std::fmt; |
- 这样的麻烦就是访问时候,还得
x.a
访问成员才行 –> Deref 解引用
集合类型
动态数组 Vector
这个才是其他编程语言中的数组…
Vetor: 单一类型; 动态扩容; 关键词 Vec
;
声明: Vec::new()
vec![]
- 已知数组大小 ->
Vec::with_capacity(capacity)
可以避免动态扩容的损耗
1 | let v1: Vec<i32> = Vec::new(); // 最 rust 的声明 |
单个访问: 索引 和 get
get
返回Option<&T>
还需要 match 解才行, 索引倒是直接返回值,但遇空 可能会 挂掉- rust 意思是,反正我都给了,爱用那个用那个.
1 | let third: &i32 = &v3[2]; |
- 这里有个新的格式化输出,
{third}
嗯又来 python 了 →_→
更新: mut 还是必须的
1 | let mut v = Vec::new(); |
多个数组元素访问
1 | let mut v = vec![1, 2, 3, 4, 5]; |
- 这里肯定报错了 first 作用域内,出现了
v.push(6);
可变引用; - 一方面是语法上限制,令一方面 Vec 动态数组 可能会动态扩容: 拷贝 再更新地址; 这样 first 可能指向无效内存….
遍历: 自然是可以通过索引 来, 也能通过 迭代方式来,不用每次检查索引 还更快.
1 | let v = vec![1, 2, 3]; |
存储不同类型: Vec 还是只能存储单一类型 –> 枚举 or 特征对象
- 特征对象要繁琐一些, 但更加灵活 更为常见
1 | // 枚举 |
排序
sort_unstable
稳定 sort_unstable_by
非稳定; 非稳定更快, 稳定还需要额外空间
1 | let mut vec = vec![1, 5, 10, 2, 15]; |
结构体: 可以传入比较函数 or 实现 Ord
特征
Ord
特征依赖于Ord
Eq
PartialEq
PartialOrd
–>derive
注解- 注解要求 结构体所有属性 均已经实现了
Ord
特征,否则报错
1 | struct Person { |
HashMap
HashMap: 不在 prelude 中,因此需要 use xxx
1 | use std::collections::HashMap; |
#todo
生命周期
→_→ →_→ 终于到了号称最难的部分了 ←_← ←_←
- 其实也还好, 类似于对象生命周期概念, 所有权 引用 作用域 搞清楚了,就没啥了..
生命周期 = 实例的有效作用域,大部分情况下 rust 编译器已经足够了,用不到我们操心..但总有二般..
- 多种类型存在,编译器会犯糊涂,此时就需要 手动标注,以方便编译器理解.
生命周期标注 就是糊弄编译器,让其给代码开个通关文牒. 不会改变任何引用的实际作用域 !
- 标注:
'a
(单引号 +a 开始) &'a str'
在&
后面- 结构体和方法内 与 泛型位置基本一致
1 | struct ImportantExcerpt<'a> { |
一般情况下编译器会智能推断生命周期 == 自动加上生命周期标记 -> 生命周期消除 的规则 (第一条输入, 23 条是输出) ^17576a
- 每个输入参数 标注上独立的生命周期:
fn foo(x: &i32, y: &i32)
->fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
- 只有一个输入,其生命周期自动赋予所有输出:
fn foo(x: &i32) -> &i32
仅有一个输入,则fn foo<'a>(x: &'a i32) -> &'a i32
x 的生命周期赋予 返回值
- 输入中存在
&self
or&mut self
,所有输出默认与&self
一致- 方法狂喜,狂喜.
编译器按照生命周期消除规则 加上 标注, 还不过就报错.
实例:
1 | fn longest(x: &str, y: &str) -> &str { |
生命周期标注也有类似 泛型 约束的写法, 包括 where
1 | impl<'a: 'b, 'b> ImportantExcerpt<'a> { // 'a: 'b 代表 'a 必须比 'b 活得久 |
总有些无解的生命周期吧 –> static
我会活的和程序一样长
- 存在即合理吧, 最后的解决就这个
&'static
: 字符串常量 特征对象T: 'static
: 泛型 or 一个奇迹
最后来个例子
1 | use std::fmt::Display; |
错误处理
这里就和 go 没啥关系了;; 放心没有 if err != nil {}
Panic
做内核相关肯定不想看到这个…(⓿_⓿)
panic!
直接输出恐慌
1 | fn main() { |
- 恐慌信息: 代码的位置; 那个函数; 很正常
$env:RUST_BACKTRACE=1 ; cargo run
带 backtrace 展开调用的堆栈,方便追踪到底哪里出错了.
1 | fn main() { |
panic 后,程序有两种方式善后:
- 栈展开 (默认): 追踪哪里出错了,并打印详细信息.
- 直接终止: 直接退出,善后交给操作系统…
Cargo.toml
中[profile.release] panic = 'abort'
善后完成后, panic 的线程就终止了,但不影响其他线程, 因此不要在 main 线程 堆积过多逻辑;
panic 的包装 unwrap
和 expect
- 两者都会解析返回值 Result 类型, 遇到错误直接 panic
expect
可以自定义错误信息,unwrap
则抛出默认错误信息- 这俩一般仅在原型 / 调试 时候使用,有更好的办法处理 Result 类型
1 | enum Result<T, E> { |
Result 和 ?
unwrap
和 expect
比直接 panic 好了一点,但还是会 panic, 配合 match
1 | use std::fs::File; |
更好的做法 –> match 匹配,传播错误; error 别 panic 犯不上.. 但是..繁琐..繁琐..
1 | use std::fs::File; |
那么 ?
来了/ 嘿嘿 / 少了一大半代码… 作用完全一致.
1 | use std::fs::File; |
?
是一个宏定义, 配合 Result 类型, ok 就立刻返回, 错误就向上抛出.
- 一般在返回值是 Rsult 的 函数/方法 中使用 (Option 其实也行)
- 可以处理 错误类型转换 和 链式调用
1 | use std::fs::File; |
- 始终注意
?
的限制条件,需要一个变量来承载正确值; (不行就展开成 match 看看对不对)
包与模块
有不同的等级
- Packages \ Crate \ Module
Packages 顶级
- 独享的
Cargo.toml
文件 - 3 种类型
- Binary Package: 即使 Package 嵌套也仅一个,入口一般是
src/main.rs
,编译后是一个二进制文件. - Library Package: Package 嵌套时可以有多个,不能独立运行,只能供其他调用. 创建时会在
src/package_name.rs
- Tool Package: 辅助,代码生成,自动化测试等等.
- Binary Package: 即使 Package 嵌套也仅一个,入口一般是
Crate 层级更小一点,常见 pub (crate)
仅在包内公开
Module 更像 py 的 __int__.py
mod xxx
创建新模块,其他地方能引用的.
1 | // 绝对路径引用 |
rust 中子 Module 都是对 父 Module 隐藏的…
- pub 结构体 字段还是隐藏的///
- pub 枚举 成员就全部公开…
文件夹作为模块:
- 目录下创建 mod.rs,再指定 哪些模块是公开暴露的.
- 另一种方式是 创建于文件夹名.rs ,内容同上, 不过可以避免大量的 mod.rs.
Use
不管那个文件,先 use
1 | // 绝对路径 | 模块 |
1 | // 导入后的模块/函数 都是私有的,如果需要二次暴露, 得 |
可见性 pub
1 | pub 意味着可见性无任何限制 |
注释 / 文档
按照分级
- 代码注释:
//
or/* */
- 文档注释:
///
or/** */
甚至包括测试用例 (和 py 很像) - 包和模块注释
文档注释
- 需要位于
lib
类型的包中,例如src/lib.rs
中 - 被注释的对象需要使用
pub
对外可见 - 可以使用
markdown
- 文档注释是给用户看的,内部实现细节不应该被暴露出去
包和模块注释
文档测试
#todo