目录
二、VS code 开发插件:rust-analyzer编辑
这是我观看杨旭老师的Rust教程的笔记,笔记中省略了一些细节部分,如有不理解的请大家观看杨旭老师的视频。
视频链接:杨旭老师B站的视频
Rust官网:Rust 语言 中文网 官方网站
《Rust 程序设计语言》简体中文版:Rust 程序设计语言 - Rust 程序设计语言 简体中文版
一、开发环境配置
安装rust管理工具链
下载链接: https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe
安装步骤:
运行安装的exe文件:
因为我用的是gcc编译器,所以要先选择自定义安装进行设置
设置完成后还会有一些其他配置如果没有特殊需求直接回车就行(默认选择)
然后在选择1进行安装
安装完成后可进行版本配置查看
rustc --version
rustc是对程序较小的时候进行调试的,如果是项目的调试我们需要用到cargo
安装完rustc也会自动安装cargo,也可以查看cargo相关版本信息
cargo --version
PS:至此rust环境配置完成
二、VS code 开发插件:rust-analyzer
三、rust项目基本构建
使用cargo 创建rust项目:
cargo new 项目名称
会生成如下结构:(附说明)
其中 Cargo.toml是项目配置信息,类似于json
PS: 在Rust里面,代码包称为crate
构建项目
cargo build
PS1:构建项目后会在当前目录下生成可执行exe文件
PS2:第一次运行cargo build 会在顶层目录下生成cargo.lock文件
- 该文件负责追踪项目依赖的精确版本
- 不需要手动修改该文件
编译并执行可执行文件
cargo run
检查代码
cargo check
PS:不会产生执行文件
PS:cargo check 比 cargo build 快得多
发布打包
cargo build --release
PS:编译时会进行优化(代码会运行更快,但编译时间更长)
PS:会在target/release下生成可执行文件而不是 target/debug
四、 变量与可变性
变量
- 声明变量使用let关键字
- 默认情况下,变量是不可变的(需要在变量前面加mut)
注意:以上变量的修改会报出如下警告(目前未找到合适的解决办法)
常量
- 可以在main函数外声明
- 也可以在main函数内声明
- 常量在绑定值以后也是不可变的,但是它与不可变的变量有很多区别
注意:
- 不可以使用mut,常量永远不可变
- 声明const,它的类型必须标注
- 常量可以在任何作用域内进行声明,包括全局作用域
- 常量只可以绑定常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值
- 在程序运行期间,常量在其声明的作用域内一直有效
- 命名规范:Rust里常量使用全大写字母,每个单词之间用下划线分开
例如:const MAX_COUNT: u32 = 100_000;
分别输出结果
shadowing(隐藏)
- 可以使用相同的名字声明新的变量,新的变量就会shadow(隐藏)之前声明的同名变量
注意:在后续的代码中这个变量名代表就是新的变量
五、数据类型
标量和复合类型
- Rust是静态编译语言,在编译时必须知道所有变量的类型
- 基于使用的值,编译器通常能够推断出它的具体类型
- 但是如可能的类型比较多(例如把String转为整数的parse方法),就必须添加类型的标注,否则编译会报错
标量类型:
- 一个标量类型代表一个单个的值
- Rust有四个主要的标量类型:整数类型、浮点类型、布尔类型、字符类型
整数类型
- 整数类型没有小数部分
- 例如u32就是一个无符号的整数类型,占据32位的空间
- 无符号整数类型以u开头
- 有符号整数类型以i开头
Rust的整数类型列表 所占位数 有符号整数 无符号整数 8字节 i8 u8 16字节 i16 u16 32字节 i32 u32 64字节 i64 u64 128字节 i128 u128 arch isize usize - 每种都分i和u,以及固定的位数
- 有符号范围:
到 - 无符号范围:
0到 - isize和usize类型
isize和usize类型的位数由程序运行的计算机的架构所决定:
例如:如果是64位计算机,则isize就是i64,usize是u64
使用isize或usize的主要场景是对某种集合进行索引操作
整数字面值
字面值 | 说明 |
Decimal(十进制) | _222 |
Hex(十六进制) | 0xff |
Octal(八进制) | 0o77 |
Binary(二进制) | 0b1111_0000 |
Byte(u8 only) | b'A' |
注意:
- 除了byte类型外,所有的数值字面值都允许使用类型后缀。
例如 57u8 - 如果不清楚使用哪些类型,可以使用Rust默认类型i32
整数溢出
假设操作:u8的范围是0-255,这时却把一个u8变量的值设为256
- 调试模式下编译:Rust会检查出整数溢出,如果发生溢出,程序在运行时就会panic
- 发布模式下编译(加--release):Rust不会检查可能导致panic的整数溢出
如果溢出则会执行“环绕”操作:256变成0,257变成1,258变成2......
浮点类型
- Rust有两种基础的浮点类型,也就是含有小数部分的类型
f32,32位,单精度
f64,64位,双精度 - Rust的浮点类型使用了IEEE-754标准来表述
- f64是默认类型
数值操作
运行符号 | 说明 |
+ | 加 |
- | 减 |
* | 乘 |
/ | 除 |
% | 取余 |
布尔类型
- Rust的布尔类型也有两个true和false
- 占一个字节大小
字符类型
- Rust语言中char类型被用来描述语言中最基础的单个字符
- 字符类型的字面值使用单引号
- 占用4字节大小
- 是Unicode标量,可以表示比ASCll多的拼音、中日韩文、零长度空白字符、emoji表情
范围:U+0000 ~ U+D7FF、U+E000 ~ U+10FFFF
复合类型
- 复合类型可以将多个值放在一个类型里
- Rust提供了两种基础的复合类型:元组(Tuple)、数组
Tuple(元组)
- Tuple可以将多个类型的多个值放在一个类型里
- Tuple的长度是固定的:一旦声明就无法改变
创建Tuple
-
在小括号里,将值用逗号分开
-
Tuple中的每个位置都对应一个类型,Tuple中各元素的类型不必相同
获取Tuple的元素值
- 可以使用模式匹配来解构一个Tuple来获取元素的值
访问Tuple的元素
- 在tuple变量使用点标记,后接元素的索引号
数组
- 数组也可以将多个值放在一个类型里
- 数组中每个元素的类型必须相同
- 数组的长度也是固定的
声明数组
数组的用处
- 如果想让你的数据存放在栈上而不是堆上,或者想保证有固定数量的元素,这时使用数组更有好处
- 数组没有Vector灵活
Vector和数组类型,它由标准库提供
Vector的长度可以改变
如果你不确定应该用数组还是Vector,那么估计你应该用Vector
例如:月份是固定的所有可以使用数组
数组的类型
- 数组的类型以这种形式表示:[类型;长度]
- 另一种声明数组的方法:如果数组的每个元素的值都相同,那么可以在中括号里指定初始值,然后是一个“;”,最后是数组长度
访问数组的元素
- 数组是栈上分配的单个块内存
- 可以使用索引来访问数组的元素
- 如果访问的索引超出了数组的范围,那么编译会通过,但是运行会报错(runtime时会panic)
String类型
- 一个String由3部分组成
ptr:指向存放字符串内容的内存指针
len:长度(字符串的字节数)
capacity:容量 - 长度和容量存放在stack上
- ptr存放在heap上
创建String
- 可以使用from函数从字符串字面值创建出String类型
- 使用String::new()
- 使用to_String()
fn main() {
// 创建String
let mut s = String::new();
// 使用to_string
let s1 = "hello";
println!("s1:{}", s1.to_string());
// 使用from函数创建字符串
let s = String::from("hello");
}
更新String
- push_str()方法:把一个字符串切片附加到string
// 使用from函数创建字符串
let mut s2 = String::from("hello");
s2.push_str(" world");
println!("s2:{}", s2);
- push()方法:把单个字符附加到String
fn main() {
// 创建String
let mut s = String::new();
// 使用to_string
let s1 = "hello";
println!("s1:{}", s1.to_string());
// 使用from函数创建字符串
let mut s2 = String::from("hello");
s2.push_str(" world");
// 把单个字符附加到String
s2.push('!');
println!("s2:{}", s2);
}
遍历String的方法
- 对于标量值:chars()方法
- 对于字节:bytes()方法
- 对于字形簇:很复杂,标准库未提供
六、函数
函数定义
- 声明函数使用fn关键字
- 依照惯例,针对函数和变量名,Rust使用snake case命名规范:
所有的字母都是小写的,单词之间使用下划线分开
函数的参数
- parameters(形参),arguments(实参)
- 在函数签名里,必须声明每个参数的类型
函数体中的语句与表达式
- 函数体是由一系列语句组成,可选的由一个表达式结束
- Rust是一个基于表达式的语言
- 语句是执行一些动作的指令
- 表达式会计算产生一个值
- 函数的定义也式语句
- 语句不返回值,所以不可用使用let将一个语句赋给一个变量
函数的返回值
- 在->符号后边声明函数返回值的类型,但是不可用为返回值命名
- 在Rust里面,返回值就是函数体里面最后一个表达式的值
- 若想提前返回,需要使用return关键字,并指定一个值
七、控制流
if表达式
使用else if处理多重条件
- 如果使用了多于一个else if,那么最好使用match来重构代码
Rust循环
- Rust提供了3中循环:loop,while和for
loop循环
- loop关键字告诉Rust反复的执行一块代码,直到叫停
- 可以在loop循环中使用break关键字来告诉程序何时停止循环
while条件循环
- 每次执行循环体之前都要判断一次条件
for循环
- 可以使用while或loop来遍历集合,但是易错且低效
- 使用for循环更简洁紧凑,它可以针对集合中的每个元素来执行一些代码
Range
- 标准库提供
- 指定一个开始数字和一个结束数字,Range可以生成它们之间的数字(不含结束)
- rev方法可以反转Range
match
- 允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码
- 模式可以是字面值、变量名、通配符...
- match匹配必须穷举所有的可能(可以使用“_”通配符替代其余没有列出的值)
if let
- 处理只关心一种匹配而忽略其它匹配的情况
- 不需要穷举所有可能
- 可以把if let 看作match的语法糖
八、所有权
所有权是Rust最独特的特性,它让Rust无须GC就可以保证内存安全
什么事所有权
- Rust的核心特性就是所有权
- 所有程序在运行时都必须管理它们使用计算机内存的方式
有些语言有垃圾收集机制,在程序运行时,它们会不断地寻找不再使用的内存
在其他语言中,程序员必须显式地分配和释放内存 - Rust采用了第三种方式
内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则
当程序运行时,所有权特性不会减慢程序的运行速度
函数调用
- 当你的代码调用函数时,值被传入到函数(也包括指向heap的指针)。函数本地的变量被压到stack上。当函数结束后,这些值会从stack上弹出
所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
变量作用域
- Scope就是程序中一个项目的有效范围
借用
- 一个函数中的变量传递给另外一个函数作为参数暂时使用,函数参数离开自己的作用域的时候将所有权还给当初递给它的变量
九、Slice(切片)
- let 切片变量 = &变量[起始位置..结束位置]
- 起始位置最小是0
- 结束位置是数组、向量、字符串的长度
十、结构体
基本结构体:
元组结构体:
空结构体:
注意: 一旦struct的实例是可变的,那么实例中所有的字段都是可变的
定义结构体中的方法
- 在impl块里定义方法
- 方法的第一个参数可以是&self,也可以获得其所有权或可变借用。和其他参数一样
十一、枚举
-
枚举允许我们列举所有可能的值来定义一个类型
十二、Rust模块系统
- Package(包):Cargo特性,让你构建、测试、共享crate
- Crate(单元包):一个模块树,它可产生一个library或可执行文件
- Module(模块):让你控制的代码组织、作用域、私有路径
Crate
- Crate的类型:binary、library
- Crate Root:这是源码文件、Rust编译器从这里开始,组成Crate的根Module
Package
- 包含1个Cargo.toml,它描述了如何构建这些Crates
- 只能包含0-1个library crate
- 可以包含任意数量的binary crate
- 但必须至少包含一个crate(library或binary)
Cargo的惯例
- src/main.rs:binary crate 的crate root,crate名与package名相同
- src/lib.rs:package包含一个library crate,library crate的crate root,crate名与package名相同
- Cargo 会把crate root 文件交给rustc来构建library或binary
Module
- 在一个crate内,将代码进行分组
- 增加可读性,易于复用
- 控制项目的私有性。public、private
建立module:在src/lib.rs
module树:
crate
|--front_of_house
|--hosting
| |--add_to_waitlist
| |--seat_at_table
|--serving
|--take_order
|--serve_order
|--take_payment
私有边界
- 模块不仅可以组织代码,还可以定义有边界
- 如果想把函数或struct等设为私有,可以将它放到某个模块中
- Rust中所有条目默认是私有的
- 父级模块无法访问子模块中的私有条目
- 子模块里可以使用所有祖先模块中的条目
- 使用pub关键字可以将某些条目标记为公共的
十三、各种关键字
pub关键字
- 使用pub关键字可以将某些条目标记为公共的
super关键字
-
用来访问父级模块路径中的内容,类似文件系统中的“..”
use关键字
- 可以使用use关键字将路径导入到作用域内(仍遵循私有性规则)
as关键字
- as关键字可以引入的路径指定本地的别名
十四、使用外部包(package)
- Cargo.toml添加依赖的包(package)
- use将特定条目引入作用域
十五、常用的集合
Vector
- 由标准库提供
- 可存储多个值
- 只能存储相同类型的数据
- 值在内存中连续存放
fn main() {
// 创建Vector
let v:Vec<i32> = Vec::new();
// 使用vec!宏初始化Vector
let mut v1 = vec![1,2,3,4,5];
// 添加元素
v1.push(6);
// 通过索引获取元素
let value1 = v1[0];
println!("value1:{:?}",value1);
// 通过get方法获取元素
let value2 = v1.get(0).unwrap();
// 通过索引修改元素
v1[0] = 100;
// 通过索引删除元素
v1.remove(0);
// 通过索引插入元素
v1.insert(0,1000);
println!("v1:{:?}",v1);
// 索引 vs get处理访问越界
// 索引会返回panic
// get返回None
}
十六、HashMap<K,V>
- 键值对的形式存储数据,一个键对应一个值
- Hash函数:决定如何在内存中存放K和V
- 数据存储在heap(堆)上
- HashMap不再Prelude(预编译库)中
创建HashMap
方法一:new()函数
- 创建空HashMap:new()函数
- 添加数据:insert()方法
// 首先要导入HashMap
use std::collections::HashMap;
fn main() {
// 创建HashMap
// 如果不指明数据类型则会报错
let mut scores:HashMap<String,i32> = HashMap::new();
// 或者直接初始化赋值,利用Rust的类型推导
scores.insert(String::from("tom"), 100);
}
方法二:collect方法
- 在元素类型为Tuple的Vector上使用collect方法,可以组建一个HashMap:
- 要求Tuple有两个值:一个作为K,一个作为V
- collect方法可以把数据整合成很多种集合类型,包括HashMap(返回值需要显示指明类型)
fn main() {
let teams = vec![String::from("Blue"),String::from("Yellow")];
let intial_score = vec![10,50];
// 使用下划线,HashMap会自动推导
let scores:HashMap<_,_> = teams.iter().zip(intial_score.iter()).collect();
}
HashMap和所有权
- 对于实现了Copy trait的类型,值会被复制到HashMap中
- 对于拥有所有权的值,值会被移动,所有权会转移给HashMap
- 如果将值的引用插入到HashMap,值本身不会移动
use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// 因为String类型的所有权已经发生改变所以无法使用field_value和field_name了
println!("{}:{}",field_name,field_value);//---->error
// 如果插入的是引用则可以继续使用
map.insert(&field_name, &field_value);
println!("{}:{}",field_name,field_value) //----->OK
}
访问HashMap中的值
- get方法(参数:Key,返回:Option<&V>)
use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(&field_name, 10);
// 获取HashMap中的值
let score = map.get(&field_name);
match score {
Some(s) => println!("{}",s),
None => println!("None"),
}
}
遍历HashMap
- for循环
// 遍历HashMap
for (key,v) in &map {
println!("{}:{}",key,v);
}
更新HashMap<K,V>
- HashMap大小可变
- 每个K同时只能对应一个V
- Key存在,对应一个Value:替换现有的Value,保留现有的Value,忽略新的Value,合并现有的Value和新的Value
- K不存在:添加一对Key,Value
替换现有的Value
- 如果向HashMap插入一对key,value,然后再插入同样的key,但是不同value,那么原来的value会被替换掉
// 替换现有的Value
let mut scores= HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.insert(String::from("Alice"), 25);
println!("{:?}",scores);
- 只在Key不对应任何值的情况下,才插入Value
let mut scores = HashMap::new();
scores.insert(String::from("Bob"), 10);
// entry方法:检查指定的K是否对应一个V,参数为K,返回一个enum Entry代表值是否存在
let e = scores.entry(String::from("Bob"));
println!("{:?}",e);
// Entry的or_insert方法:如果K不存在则插入V,否则返回V
e.or_insert(50);
scores.entry(String::from("Bob")).or_insert(50);
println!("{:?}",scores);
基于现有的V来更新V
let text = "hello world wonderful world";
let mut map = HashMap::new();
// split_whitespace方法将字符串以空格分割成一个Vec
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:#?}",map);
Hash函数
- 默认情况下,HashMap使用加密功能强大的Hash函数,可以抵抗拒绝服务(DoS)攻击。
- 不是可用的最快的Hash算法,但是具有更好的安全性
- 可以指定不同的hasher来切换到另一个函数,hasher是实现BuildHasher trait的类型
十七、Rust错误处理概述
panic时可展开(默认)或中止调用栈
- 默认情况下,panic发生程序展开调用栈(工作量大),Rust沿着调用栈往回走,清理每个遇到的函数中的数据,或立即终止调用栈,不进行清理,直接停止程序,内存需要OS进行清理
- 将panic设置从展开设置为中止:在Cargo.toml中的profile设置panic=‘abort’
#cargo.toml
# 在生产环境中如果发生panic,那么就中止执行,然后让OS来清理内存
[profile.release]
panic = 'abort
使用panic!产生的回溯信息
- panic!可能出现在我们写的代码中或我们所依赖的代码中
- 通过设置环境变量Rust_BACKTRACE可得到回溯信息
具体流程:
在mian.rs中编写如下代码
fn main() {
let v = vec![1, 2, 3, 4, 5];
v[99];
}
运行后会产生如下提示panic
根据提示获取panic产生的回溯信息
注意:一定要在cmd终端命令下而不是powershell
use std::fs::File;
fn main(){
let f = File::open("foo.txt");
let f = match f {
Ok(file)=>file,
Err(error)=>{
panic!("{:?}",error);
}
};
}
Result枚举处理错误
- 基本用法
fn main() {
enum Result<T,E> {
OK(T), //操作成功的情况下,OK变体里返回的数据类型
Err(E), //操作失败的情况下,Err变体里返回的数据类型
}
}
- 用match表达式处理Result:和Option枚举一样,Result及其变体也是由prelude带入作用域
use std::fs::File;
fn main(){
let f = File::open("foo.txt");
let f = match f {
Ok(file)=>file,
Err(error)=>{
panic!("{:?}",error);
}
};
}
错误信息
匹配不同的错误
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("{:?}",e),
},
other_error => panic!("{:?}",other_error),
},
};
}
unWrap:match表达式的一种快捷方法
fn main() {
// 正常的match方法
use std::{error, fs::File};
let f = File::open("hello.txt");
let f = match f {
Ok(file)=>file,
Err(error)=>{
panic!("{:?}",error)
}
};
// unwrap方法:等同于上面的match
let f = File::open("hello.txt").unwrap();
}
expect:unWrap的补充
- unWrap无法自定义错误信息,expect可以指定错误信息
let f = File::open("hello.txt").expect("该文件无法打开");
传播错误
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(error) => return Err(error),
};
// 如果File::open操作成功的话,就创建一个string用于获取文件内容
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),//如果操作成功则s是match的返回值,而match的返回值就是这个函数的返回值,所以s就是这个函数的返回值
// 如果操作失败则返回从上面传递的错误信息
Err(error) => Err(error),
}
}
fn main() {
let Result= read_username_from_file();
}
- “?”运算符作为传播错误的快捷方式
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(error) => return Err(error),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s)
Err(error) => Err(error),
}
}
//等价于“?”运算符
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
}
?与from函数
- Trait std::convert::From上的from函数:用于错误之间的转换
- 被?所应用的错误,会隐式的被from函数处理
- 当?调用from函数时:它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型
- from函数用于针对不同错误原因,返回同一种错误类型,只要每个错误类型实现了转换为所返回的错误类型的from函数
- ?运算符只能用于返回Result的函数
?运算符链式调用
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
let mut f = File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
?运算符与main函数
- main函数返回类型是()
- main函数的返回值类型也可以是Result<T,E>
- Box<dyn Error>是trait对象:可以理解为“任何可能错误的类型”
use std::fs::File;
use std::error::Error;
fn main()->Result<(),Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
十八、泛型
- 提高代码复用能力
- 处理重复代码问题
- 泛型是具体类型或其他属性的抽象代替:你编写的代码不是最终的代码,而是一种模版,里面有一些“占位符”,编译器在编译时将“占位符”替换为具体的类型
- 类型参数:T
fn largest<T>(list:&[T])->T{...}
函数定义中的泛型
- 泛型函数:参数类型,返回类型
fn largest<T>(list:&[T])->T{
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
Struct定义中的泛型
- 可以使用多个泛型的类型参数(太多类型参数:你的代码需要重组为多个更小的单元)
struct Point<T>{
x: T,
y: T
}
fn main() {
let integer = Point{x: 5, y: 10};
let float = Point{x: 1.0, y: 4.0};
}
Enum定义中的泛型
- 可以让枚举的变体持有泛型数据类型:Option<T>,Result<T,E>
enum Option<T> {
Some(T),
None,
}
enum Result<T,E> {
Ok(T),
Err(E),
}
fn main() {
}
方法定义中的泛型
- 为struct或enum实现方法的时候,可在定义中使用泛型
- 注意:
把T放在impl关键字后,表示在类型T上实现方法:impl<T> Point<T>
只针对具体类型实现方法(其余类型没实现方法):impl Point<f32> - struct 里面的泛型类型参数可以和方法的泛型类型参数不同
struct Point<T,U>{
x: T,
y: U,
}
impl<T,U> Point<T,U> {
fn mixup(V,M)(self,other:Point<V,W>)->Point<T,W> {
Point{
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point{x: 5, y: 10};
let p2 = Point{x: "Hello", y: 'c'};
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
十九、Trait
- Trait告诉Rust编译器:某种类型具体有哪些并且可以与其他类型共享的功能
- Trait:抽象的定义为共享行为
- Trait bounds(约束):泛型类型参数指定为实现了特定行为的类型
定义Trait
- 定义:把方法签名放在一起,来定义实现某种目的的所必需的一组行为
- 关键字:trait
- 只有方法签名,没有具体实现
- trait可以有多个方法:每个方法签名占一行,以“;”结尾
- 实现trait的类型必须提供具体的方法实现
pub trait Summary { // 方法的签名也就是行为,可以有多个 fn summarize(&self) -> String; fn summarize1(&self) -> String; } fn main() {}
在类型上实现trait
- 与类型实现方法类似
在lib.rs里定义trait
//lib.rs
// 定义一个trait
pub trait Summary {
// 方法的签名也就是行为,可以有多个
fn summarize(&self) -> String;
}
pub struct NewsArticle{
pub headline:String,
pub author:String,
pub location:String,
pub content:String,
}
// 实现该NewsArticle的trait
impl Summary for NewsArticle{
fn summarize(&self) -> String{
format!("{}, by {} ({})",self.headline,self.author,self.location)
}
}
// 推特的推文
pub struct Tweet{
pub username:String,
pub content:String,
pub reply:bool,
pub retweet:bool,
}
// 实现该Tweet的trait
impl Summary for Tweet{
fn summarize(&self) -> String{
format!("{}, {}",self.username,self.content)
}
}
在main.rs中调用
use demo24::Tweet;
use demo24::Summary; //必须有,否则会报错summarize,因为summarize只有在Summary所在的当前作用域下才能使用,且Summary必须是pub公共的
fn main() {
let tweet = Tweet{
username: String::from("tweet"),
content: String::from("tweet content"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
实现trait的约束
- 可以在某个类型上实现某个trait的前提条件是这个类型或这个trait是在本地crate里定义的
- 无法为外部类型来实现外部的trait(一致性,孤儿规则),此约束确保其他人的代码不能破坏您的代码,反之亦然,如果没有这个约束,两个crate可以为同一类型实现同一个trait,Rust就不知道应该使用哪个实现了
默认实现
- 默认实现的方法可以调用trait中其它的方法,及时这些方法没有默认实现。
注意:无法从方法的重写实现里面调用默认的实现
还是上面的例子
// 定义一个trait
pub trait Summary {
// 方法的签名也就是行为,可以有多个
//设置了一个默认实现
fn summarize(&self) -> String{
String::from("(Read more...)")
}
}
pub struct NewsArticle{
pub headline:String,
pub author:String,
pub location:String,
pub content:String,
}
// 去掉了NewsArticle具体的实现,变成空实现
impl Summary for NewsArticle{
}
// 推特的推文
pub struct Tweet{
pub username:String,
pub content:String,
pub reply:bool,
pub retweet:bool,
}
// 实现该Tweet的trait
//而Tweet的实现则会对默认实现进行重写
impl Summary for Tweet{
fn summarize(&self) -> String{
format!("{}, {}",self.username,self.content)
}
}
Trait作为参数
- 第一种方法:使用impl Trait语法(Trait bound语法的语法糖)
// item代表传入的参数实时实现Summary的具体实现,在上面的例子中是Tweet或NewsArticle
pub fn notify(item:impl Summary){
println!("{}",item.summarize());
}
- 第二种方法:使用Trait bound语法
pub fn notify<T:Summary>(item:T){
println!("{}",item.summarize());
}
指定多个Trait bound
- 第一种方法:使用+指定
use std::fmt::Display;
// impl Trait语法
pub fn notify1(item:impl Summary+Display){
println!("{}",item.summarize());
}
// Trait bound语法
pub fn notify<T:Summary+Display>(item:T){
println!("{}",item.summarize());
}
- 第二种方法:使用where子句(在方法签名后指定where子句)
pub fn notify2<T,U>(a:T,b:U)->String
where
T:Summary+Display,
U:Clone+Debug,
{
format!("Breaking news! {}",a.summarize())
}
实现Trait作为返回类型
- impl Trait语法
注意:impl Trait 只能返回确定的同一种类型,返回可能不同类型的代码会报错
// 实现Trait作为返回类型
pub fn notify3(flag: bool) -> impl Summary {
if flag {
NewsArticle {
headline: String::from("Penguins win again"),
content: String::from("The penguin team scored again in a contest against the sharks"),
author: String::from("Iceburgh"),
location: String::from("New York"),
}
}else {
// 由于这个函数返回的类型不止一个,因此会报错
Tweet{
username: String::from("Penguin1"),
content: String::from("Penguin1: I am a penguin"),
reply: false,
retweet: false
}
}
}
- Trait Bound方法
// 如果导入的是数字vec,则要+Copy
// 如果是str类型的vex,因为String存储在堆内存上所以要+Clone
fn largest<T:PartialOrd +Clone>(list:&[T])->T{
let mut largest = list[0].clone();//并且如果传入的是str类型,对于获取也要加Clone
for item in list.iter(){
if item > &largest{ //>符号对应的方法std::cmp::PartialOrd(存在预导入模块中)
largest = item.clone();
}
}
largest
}
fn main() {
// let number_list = vec![1,2,3,4,5];
// let result = largest(&number_list);
// println!("largest number is {}", result);
let str_list = vec![String::from("hello"),String::from("world")];
let result = largest(&str_list);
println!("largest number is {}", result);
}
使用trait Bound有条件的实现方法
- 在使用泛型类型参数的impl块上使用Trait bound,我们可以有条件的为实现了特定Trait的类型来实现方法