hello rust.
Rust 快速入门,参考 《Rust程序设计语言》。
i8, i16, i32, i64, i128, isize // 有符号整型
u8, u16, u32, u64, u128, usize // 无符号整型
f32, f64 // 浮点型,默认f64
bool // 布尔类型,true/false
char // 字符类型,4字节,Unicode标量值
// 数组索引 / 容器长度几乎都用 usize(和指针大小相同,64位系统为64位)
Rust 变量使用 let 关键字声明,默认是不可变的,但可以使用mut关键字声明可变变量。如果声明的是不可变变量,一旦值被绑定一个名称上,你就不能改变这个值。
const关键字声明,必须显式指定类型,且只能绑定到常量表达式;固定长度的有序集合,可以包含不同类型的值,常用于函数返回多个值:
let tup: (i32, f64, bool) = (500, 6.4, true);
let (x, y, z) = tup; // 使用模式匹配来解构赋值
let five_hundred = tup.0; // 通过下标访问
不带任何值的元组叫做 单元(unit) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。
固定长度的同类型元素集合,栈分配 ,长度是数组类型的一部分例如[i32; 5]:
let a: [i32; 5] = [1, 2, 3, 4, 5];
let first = a[0]; // 访问元素
let b = [3; 5]; // 创建含5个3的数组,[初始值; 长度]
Vec<T>动态大小的同类型元素集合,堆分配:
// let v = vec![1, 2, 3]; // 宏创建
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
let third = &v[2]; // 访问元素
match v.get(2) {
Some(x) => println!("{}", x),
None => println!("None"),
}
经常使用的字符串类型有:String,&String, &str。
String动态大小字符串,可以用来声明变量指向堆上分配的数据(使用起始地值+长度+容量实现)。&String是其引用类型,借用而不获取所有权。
str系统类型,具有动态大小,所以不能直接用来声明变量(只能通过引用或指针访问)。代表的是一段 UTF-8 编码的字节序列。
&str字符串切片,可以用来声明变量,也是一种引用类型,指向一块字符串字节数组。(起始地值+长度实现,没有所有权)。字符串字面量创建的变量默认声明为&str,即引用分配在静态存储区的常量字符串。必须符合utf-8字符的边界。
&str,更加通用(String类型容易转换为切片,&String 可通过解引用强制转换为 &str,或者使用切片)字符串String类型底层就是Vec<u8>字节数组,字符串使用 UTF-8 编码,并提供字节-文本解析方法。String 类型来自标准库而非核心语言,可增长,可修改和获得所有权的类型。
在核心语言层面,Rust只有一个字符串类型 str (大小可变的字符串),通常使用 &str 来表示字符串切片。字符串切片:引用类型,对存储在其它地方的utf-8编码字符串的引用。
常用方法:
// 创建String
let s = String::from("hello");
let s = String::new();
let s = "hello".to_string();
// 添加字符串/字符
s.push_str(", world!");
s.push('!');
// 拼接字符串
let s3 = s1 + &s2; // fn add(self, s: &str) -> String
let s3 = format!("{}-{}", "hello", s); // format!
拼接字符串的+操作,注意获取了s1的所有权(后续失效),参数是&str;解引用强制转换(deref coercion)可以将 &String 转换为 &str 类型。
使用format!宏拼接字符串,不会获取所有权,方便于多个字符串的拼接。
内部表示:
String 底部是u8字节数组,使用 UTF-8 编码存储 Unicode 标量值,主要特性:
不支持整形下标索引(保证字符索引安全);
使用 [..] 创建字符串切片时,也必须符合 utf-8 字符边界,否则出现运行时错误;
自定义类型,封装多个字段。
构造 struct 时可以使用字段初始化简写语法:
struct User{
name: String,
active: bool,
}
let name = String::from("alice");
let user1 = User{
name, // 简写,相当于 name: name, 使用变量 name 的值初始化
active: true,
};
let user2 = User{
active = false, // 不同的字段
// 使用结构体更新语法从其他实例创建实例
// .. 语法指定了剩余未显式设置值的字段应与给定实例对应字段相同的值
..user1
};
结构体调试打印:
#[derive(Debug)] // 自动实现 Debug trait
struct User{
name: String,
active: bool,
}
fn main(){
let user = User{
name: String::from("alice"),
active: true,
};
// 使用 {:?} 或 {:#?} 进行调试打印
println!("{:#?}", user); // println!("{user:#?}");
}
dbg! 宏打印到标准错误控制台流println! 接收的是引用,不会获取所有权。而 dbg! 宏会获取所有权并返回该值的所有权。元组结构体没有具体的字段名,只有字段的类型,但整个类型是具名类型。
使用上与元组类似:
. 后跟索引来访问单独的值struct Point(i32, i32, i32);
let origin = Point(0, 0, 0);
// 解构
let Point(x, y, z) = origin;
// 访问
let x = origin.0;
没有任何字段的结构体,类似于单元类型(),主要用于在某个类型上实现 trait 但不需要在类型中存储数据:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual; // 只需要使用名称
}
方法(method)在结构体的上下文中(impl 块)被定义(或者是枚举或 trait 对象的上下文),并且第一个参数总是 self,它代表调用该方法的结构体实例。
#[derive(Debug)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
//构造函数-关联函数
// 关键字 Self 在函数的返回类型和函数体中,都是对 impl 关键字后所示类型的别名
pub fn new(width: i32, height: i32) -> Self {
Self {
//同名可以省略
width,
height,
}
}
// 方法,&self参数; self: &Self 的缩写
fn erea(&self) -> i32 {
self.width * self.height
}
//关联函数
fn erea2(width: i32, height: i32) -> i32 {
width * height
}
fn drop(mut self) {
println!("drop myself");
}
}
使用impl块来定义方法和关联函数,每个结构体允许有多个块;
如果块中函数第一个参数为self | &self | &mut self,函数类型是该类型的方法;其他就数就是(非方法的)关联函数,使用 structName::funcName() 调用;(非方法的)关联函数经常被用作返回一个结构体新实例的构造函数,例如 new 函数。
枚举类型,定义了同类型的多个变体(同属该类型),定义的变体名字同时也是构建枚举实例的函数。
每个枚举变体可以关联一组不同类型的数据,关联的数据可以通过match模式匹配来获取:
enum IpAddrKind {
V4(u32,u32,u32,u32),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
标准库枚举 Option<T> 用来解决空值问题(编码存在或不存在):
enum Option<T> {
None,
Some(T),
}
let some_num = Some(5);
let absent_num: Option<i32> = None;
Option<T> 枚举以及两个变体被包含在了 prelude 之中,无需将其显式引入作用域匹配 Option<T> 枚举:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1), // i 绑定了 Some 中包含的值
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
_ 通配符来匹配任意值而不绑定到该值。结合 if 和 let,来处理只匹配一个模式的值而忽略其他模式的情况。
// 工作方式与 match 相同
// if let 模式 = 表达式{...}
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
} else {
// 与 match 表达式中的 _ 分支块中的代码相同
println!("The maximum is not configured.");
}
let...else 应用的场景:解构+提前返回,如果某个值存在,就对它做一些操作;如果不存在,就返回一个默认值。
fn describe_state_quarter(coin: Coin) -> Option<String> {
// 左侧是模式,右侧是要匹配的值
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
函数只要在模块作用域范围可见就可以使用,不需要先声明后使用。
函数使用 fn 声明,使用尾置形式返回类型:
fn add(a: i32, b: i32) -> i32{
a+b
}
条件表达式必须是 bool 类型,判断条件不需要小括号。
if 表达式可以作为其他语句的一部分,例如赋值:
// 代码块的值是其最后一个表达式的值
let number = if condition { 5 } else { 6 };
loop {
number -= 1;
if number == 0 {
break; // break number; // 可以返回值
}
}
while number > 0 {
number -= 1;
}
for i in 0..10{ // 使用 Range 循环特定次数
println!("{}", i);
}
for element in a { // 遍历集合
println!("the value is: {element}");
}
loop无限循环,可以使用break跳出循环,可以带一个值作为循环表达式的值;Rust 通过所有权机制来管理内存,避免垃圾回收带来的性能开销。每个值在任意时刻都有一个唯一的所有者,当所有者超出作用域时,值会被自动释放。
Rust 的所有权规则作用在“值”上,不关心它在栈还是堆上分配。类型没有实现 Copy trait 的值在赋值或者作为函数参数传递时会被移动(move),否则会被复制(copy)。
基础标量类型、原始指针、类型实现了 Copy trait。 如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然有效。任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。
Rust 永远也不会自动创建数据的 “深拷贝”;如果确实需要深度复制堆上的数据,可以使用类型的 clone() 方法。
引用的场景:不转移所有权,可访问储存于该地址的属于其他变量的数据,引用在其生命周期内保证指向某个特定类型的有效值。
创建一个引用的行为称为 借用(borrowing)
创建可变引用(mutable reference)例如 &mut T,才可以修改引用指向的数据。可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。(避免数据竞争)
切片(slice)引用集合中一段连续的元素序列,它是一种引用,不拥有所有权。
字符串 slice 是 String 中一部分值的引用,字符串切片的类型声明使用 &str:
let s = String::from("hello world");
let hello = &s[0..5]; // 可省略起始或结束索引
let world = &s[6..11];
// 索引必须位于有效的 UTF-8 字符边界内
&str,它是一个指向二进制程序特定位置的 slice。它是不可变引用。&str 而不是 &String 作为参数,以提高灵活性和性能。(&str 参数兼容字符串 slice,也兼容 &String)包 package 是一个 Cargo 项目,由 Cargo.toml 描述。一个包可以包含多个二进制 crate 项和一个可选的库 crate(根文件是 src/lib.rs)。
crate 是 Rust 编译器的编译单元,可以是二进制 crate 或库 crate。
Module 是 crate 内部的代码组织与命名空间机制,解决命名冲突问题,以及控制代码的可见性(私有/公有)。
模块用于将相关代码组织在一起,形成命名空间。一般是内联模块(在同一文件中定义)或者文件模块(在单独的文件中定义)。
mod 关键字,作用是:把一段代码(文件 / 内联模块)挂到 crate 的模块树上(模块树的根模块名叫做 crate,它是一个隐式模块),编译器会在下列路径中寻找模块代码:
可见性:一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用 pub mod 替代 mod。为了使一个公用模块内部的成员公用,应当在声明前使用pub。
引入类型/函数等到作用域:使用use关键字,可以将路径引入作用域,简化访问。
Rust 使用两种路径类型来引用代码:绝对路径和相对路径。
crate 开头;对于外部 crate(例如外部库),使用外部 crate 名开头。self(当前模块)、super(父模块)或者当前模块的某个标识符开始(例如子模块名,use 引入的模块名)。引用路径和模块树紧密相关,因为模块树定义了路径的结构。mod 声明不同于 C++ include,只需在模块树中的某处使用一次 mod 声明就可以加载这个文件/模块,然后就可以在模块树中的任何位置使用该模块的路径来引用它。
pub 模块允许其父模块引用它,但是不允许访问内部代码,如果需要访问内部项(结构体、枚举、函数),还需要将这些项声明为 pub。
结构体和枚举的可见性:结构体定义使用了 pub,该结构体会变成公有的,但是这个结构体的字段仍然是私有的。可以根据情况决定每个字段是否公有。但是将枚举设为公有,则它的所有变体都将变为公有。
习惯:
// 导入模块
use crate::front_of_house::hosting;
// 导入结构体
use std::collections::HashMap;
std 标准库是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们。
使用 as 在导入时重命名以避免名称冲突:
use std::fmt::Result;
use std::io::Result as IoResult;
使用 pub use 语法将导入的项重新导出,使得其他模块可以通过当前模块访问该项。
作用:创建公共 API,简化外部代码对内部复杂模块结构的访问。
可以使用嵌套路径将相同的项在一行中引入作用域。
use std::{cmp::Ordering, io};
// use std::io;
// use std::io::Write;
// =>
use std::io::{self, Write}; // self 代表 io 模块本身
使用 * 通配符将模块中的所有公有项引入作用域,常用于测试模块 tests 中:
use std::collections::*;
常用的集合数据类型:Vec<T>,String,HashMap<K, V>(HashSet<K>, LinkedList<T>, VecDeque<T>)。https://doc.rust-lang.org/std/collections/index.html
Vec<T>Vec<T> (vector) 是一个动态数组,可以存储同类型的多个值,大小可变,存储在堆上。使用:
// 创建一个空的 Vec: 构造函数 new
let mut v1: Vec<i32> = Vec::new();
// 使用宏 vec! 创建并初始化(一系列初始值)
let mut v2 = vec![1, 2, 3]; // 类型推断为 Vec<i32>
let mut v = Vec::new();
// 追加元素
v.push(1);
v.push(2);
// 弹出末尾元素
let x = v.pop(); // Some(2)
// 索引赋值
v[0] = 10; // 注意:如果索引越界会 panic
// get_mut 获取可变引用
if let Some(x) = v.get_mut(0) {
*x = 20; // 解引用赋值,不会越界
}
// insert 插入元素
v.insert(0, 99) ; // 在索引0位置插入99,后续元素后移
// remove 删除元素并返回
let y = v.remove(0); // 删除索引1位置元素,后续元素前移
// swap_remove 删除元素并返回,但不保持顺序
// 使用末尾元素替换被删除位置
let z = v.swap_remove(0);
let v = vec![1, 2, 3, 4, 5];
// 1.通过索引访问,越界会 panic
let third: &i32 = &v[2];
// 2.get 方法返回 Option<&T>
// get_mut 返回 Option<&mut T>
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
let v = vec![100, 32, 57];
for i in &v { // (&v)的不可变引用遍历
println!("{i}");
}
for i in &mut v { // (&mut v)的可变引用遍历
// i: &mut i32
*i += 50; // 解引用赋值
}
// 也可以直接遍历元素(模式匹配 let &i = ...)
// &v: 迭代器 Item 类型是 &i32,
// 使用 &i 解构得到 i32 类型的值(必须可复制)
for &i in &v {
// i: i32
println!("{i}");
}
结合 Vec 和枚举,可以创建储存不同类型值的集合(枚举的成员都被定义为相同的枚举类型,但可以携带不同数据)。
// 表格一行的列包含数字,浮点值,字符串
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
在编写程序时不能完全知道运行时会储存进 vector 的所有类型时,可以使用 trait 对象。
vector 在其离开作用域时会被释放,所有其内容也会被丢弃。
字符串(String)类型由 Rust 标准库提供,是一种可增长的、可修改的、拥有所有权的 UTF-8 编码字符串(字符串是作为字节集合外加一些方法实现)。
// 创建String
let mut s = String::new(); // 同 Vec<T>
// 任何实现了 Display trait 的类型都可以使用 to_string() 方法来创建 String
let s = "initial contents".to_string(); // 比如字符串字面值
// 使用 String::from() 函数创建
let s = String::from("initial contents");
let mut s = String::from("foo");
s.push_str("bar"); // push_str() 方法追加字符串切片 &str
s.push('!'); // push() 方法追加单个字符
拼接字符串:
// 使用 + 操作符拼接字符串
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
说明:+ 运算符的第一个参数是 String 类型,第二个参数是字符串切片 &str 类型(通过解引用强制转换将 &String 转换为 &str,效果就是 &s2 转换为 &s2[..])。+ 运算符会获取第一个参数的所有权并返回一个新的 String。Add trait 为 + 运算符重载,内部调用 push_str 方法。
级联多个字符串:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
// let s = s1 + "-" + &s2 + "-" + &s3;
let s = format!("{s1}-{s2}-{s3}"); // format! 宏拼接字符串,使用引用不获取所有权
Rust 的字符串不支持索引。
String 内部实现是一个 Vec<u8> 的封装,字符串中每个 Unicode 标量值使用 utf-8 编码。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。(字符串的长度以字节为单位)
字符串不允许索引,但可以使用切片语法来获取部分字符串。
字符串 slice 是对 String 或字符串字面值的一部分的引用,类型是 &str;但是切片的起始和结束索引必须位于有效的 UTF-8 字符边界内,否则会引发运行时错误:
let hello = "Здравствуйте";
// s类型: &str
let s = &hello[0..4]; // 正确,前四个字节是有效的 UTF-8
遍历字符串需要明确表示是字符还是字节:
// 遍历字符串的字符
for c in "Зд".chars() {
println!("{c}");
}
// 遍历字符串的字节
for b in "Зд".bytes() {
println!("{b}");
}
需要显示导入 use std::collections::HashMap; 使用:
// 创建HashMap,可以 insert 方法中推断 HashMap 范型类型
use std::collections::HashMap;
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("{:#?}", scores);
// collect 方法从一个迭代器创建一个 HashMap
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
// 或者
let vec = vec![(String::from("Blue"), 10), (String::from("Yellow"), 50)];
let scores: HashMap<_, _> = vec.into_iter().collect();
insert() 默认会替换相同 key 的旧值,即覆盖旧值;
只在键不存在时插入键值对,用 map.entry(key).or_insert(0);entry() 方法根据键返回一个枚举 Entry,表示键在哈希映射中对应的值是否存在;使用 or_insert() 方法在不存在时插入新值,返回值的可变引用(插入后的值或原值):
use std::collections::HashMap;
let mut scores = HashMap::new(); // 类型推断
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}");
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
// 返回一个由空格分隔 text 值子 slice 的迭代器
for word in text.split_whitespace() {
// *map.entry(word).or_insert(0) += 1;
// =>
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{map:?}");
使用 HashMap 计数时常用的标准写法:
for word in text.split_whitespace() {
// 累加器的标准写法:
map.entry(word)
.and_modify(|v| *v += 1)
.or_insert(1);
}
Option<&V> 类型的值;get_mut() 方法返回一个 Option<&mut V> 类型的值。map[key] 语法获取值的引用,如果 key 不存在会 panic。let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
// copied 方法来获取一个 Option<i32> 而不是 Option<&i32>
遍历键值对:
for (key, value) in &scores {
// key: &String, value: &i32
println!("{key}: {value}");
}
// 可变引用遍历
for (key, value) in &mut scores {
// key: &String, value: &mut i32
*value += 10;
}
插入数据时:
对于实现了 Copy trait类型例如i32,会复制到 HashMap中;对于拥有所有权的类型会被移动;
如果将引用插入到HashMap中,值不会移动,但是要保证被引用指向值的有效性;
HashMap 默认使用一种叫做 SipHash 的哈希函数,hasher 是一个实现了 BuildHasher trait 的类型。
错误分为可恢复错误和不可恢复错误两种情况;可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误,比如数组越界访问,通常是程序逻辑中的 bug,程序无法继续运行下去。
Rust 没有异常机制,使用 Result 和 panic! 宏来处理错误。
Result<T, E> 枚举用于可恢复错误处理;panic! 宏用于不可恢复错误处理。panic! 宏实践中有两种方法会造成 panic: 执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)或者自己显式调用 panic! 宏。
panic!("crash and burn");
release 模式中 panic 时直接终止程序,不会展开栈,程序体积更小。在 Cargo.toml 的 [profile] 部分增加:
[profile.release]
panic = 'abort'
Result<T, E> 枚举Result<T, E> 枚举用于可恢复错误处理,定义在标准库中:
// T: 成功时返回的值类型(Ok变体中的数据类型)
// E: 失败时返回的错误类型(Err变体中的数据类型)
enum Result<T, E> {
Ok(T),
Err(E),
}
注意:与 Option 枚举一样,Result 枚举和其变体也被导入到了 prelude 中,所以不需要指定 Result:: 模块路径就可以使用。
在处理(嵌套)Result 时,可以使用闭包方式,避免大量 match 语句嵌套:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
// unwrap_or_else 方法接收一个闭包作为参数(解包否则处理错误)
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
Result 常用方法:
unwrap():如果是 Ok,返回包含的值;如果是 Err,调用 panic! 宏。expect(msg: &str):与 unwrap 类似,如果是 Ok, 返回包含的值;但是在 panic 时提供自定义错误消息(通常是期望的上下文消息)。Err 值通常需要传播给调用者,让调用者决定如何处理错误。rust 提供了 ? 运算符来简化错误传播(简化使用 match 匹配来提前返回 Err 值):
?运算符被定义为从函数中提早返回一个值,可以理解是提前返回的语法糖。
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
// 直接对 Result 使用 ? 运算符
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
//
username_file.read_to_string(&mut username)?;
Ok(username)
}
// 上面的读取文件 std 库提供了函数:
// fs::read_to_string("hello.txt") // Result<String, io::Error>
Result 值之后的 ? 运算符,等价与同功能的 match 匹配:
Ok<T>,则提取出值并继续执行后续代码(解包);Err<E>,则立即返回该 Err 值,结束函数执行(将错误值传递给调用者)。错误转换:如果底层函数返回不同的错误类型,需要顶层函数返回一个统一的错误类型,可以使用 From trait 来定义不同错误类型之间的转换关系,? 运算符会自动调用 From::from 方法来进行转换。
类似与:
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
// 自定义错误类型
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
// 为不同错误实现 From
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::Parse(err)
}
}
// 只返回一种错误类型
fn read_and_parse_number(path: &str) -> Result<i32, AppError> {
let mut file = File::open(path)?; // io::Error → AppError
let mut contents = String::new();
file.read_to_string(&mut contents)?; // io::Error → AppError
let number: i32 = contents.trim().parse()?; // ParseIntError → AppError
Ok(number)
}
? 之后允许直接使用链式调用,其中遇到错误会自动转换并返回(File::open("hello.txt")?.read_to_string(&mut username)?;);
? 使用条件:能用 ? 的返回类型,必须实现了 std::ops::FromResidual trait,例如 Result<T,E>,Option<T> 类型。注意提前返回时:是把失败值变形返回,例如自动调用 From::from 方法进行转换。
对于 struct,enum,方法/普通函数都可以定义为泛型:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<i32> {
fn origin() -> Point<i32> {
Point { x: 0, y: 0 }
}
}
Rust泛型实现与 C++类型,编译时都会进行具体类型替换-单态化;
struct 方法可以定义其他的类型参数,与struct是否为泛型无关;
对泛型struct的某个具体类型,例如Point<i32>定义方法是对该类型添加的,其他类型参数的Point不具有(与C++偏特化/特化不同)
泛型可以扩展代码的通用性,常见类型都定义为泛型,Option<T>, Result<T, E>等
类似与接口概念,告诉编译器某种类型具有哪些特定行为/功能,用来抽象地定义共享/公共行为。
一个主要作用就是为泛型类型参数进行约束Trait bounds,指定为实现特定行为的类型。
定义与为类型实现Trait:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Tweet {
pub username: String,
pub content: String,
}
// impl
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Trait可以定义默认实现。
类型实现Trait必须实现 trait 定义的所有没有默认实现的方法,对于有默认实现的,可以选择重写该方法;trait 定义中,(默认)方法可以调用没有默认实现的方法,类型会保证实现这些方法。
类型实现Trait的条件:
类型或者该 Trait 是在本地 crate定义的;
即无法为外部类型实现外部的 trait(孤儿规则)
使用impl Trait或者Trait bound语法:
// impl Trait 修饰参数
pub fn notify(item: impl Summary+Display) {
println!("Breaking news! {}", item.summarize());
}
// 使用泛型 + trait约束
pub fn notify<T: Summary+Display>(item: T) {
println!("Breaking news! {}", item.summarize());
}
// where
pub fn notify2<T, U>(a: T, b: U)
where
T: Summary + Display,
U: Clone + Debug,
{
println!("Breaking news! {}", a.summarize());
}
使用impl trait语法:
pub fn news() -> impl Summary {
Tweet {
username: String::from("ebooks"),
content: String::from("people"),
}
}
限制:
Trait 主要用来表示类型约束,其他用法: 在泛型类型的 impl 块上使用 Trait bound,可以为类型参数实现了特定 Trait 的泛型类型有条件地实现某些方法; 为实现特定 Trait 的任意类型有条件地实现另一个 Trait (覆盖实现)
// 1. 有条件地实现某些方法
impl<T: Display+PartialOrd> Pair<T>{
fn cmp_display(&self){
...
}
}
// 2. 有条件实现另一个 Trait
impl<T: fmt::Display+?Sized> ToString for T{
...
}
生命周期目的:避免悬垂引用 dangling reference.
Rust中每个引用都有自己的生命周期(保持有效的作用域),当生命周期以不同的方式互相关联,需要手动标注生命周期,使用泛型声明来规范生命周期的名称。
例如函数签名中使用泛型生命周期参数:
可以理解为返回引用的生命周期至少是x,y中较短的生命周期(交集)
fn long_str<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
生命周期标注不会实际改变引用的生命周期长度;
只是描述了多个引用的生命周期的关系,不影响其生命周期;因此单个生命周期的标注没有意义;
'static特殊生命周期标识,整个程序的运行时间(例如字符串字面量)
注意:函数返回引用类型,返回类型的生命周期参数需要与一个参数的生命周期匹配(即跟输入参数相关,否则就是悬垂引用)
Rust引用分析中考虑了一些特定模式/生命周期省略规则,符合该模式的代码无需显示标注生命周期。(输入生命周期: 参数为引用的生命周期;输出生命周期:返回值是引用的生命周期)
规则如下:
每个输入**参数(引用类型)**如果省略生命周期,则具有不同的生命周期参数(例如'a,'b,'c);
如果只有一个输入生命周期参数,该生命周期被赋给所有的输出生命周期参数;
如果有多个输入生命周期参数,但是其中有一个是&self,&mut self(适用于方法中),那么 self 的生命周期被赋给所有的输出生命周期参数。
应用上述规则后如果不能确定签名中所有引用的生命周期编译器会报错。或者出现不匹配,例如返回值的生命周期与返回类型生命周期不同(示例见impl块和方法)。
struct 定义中字段除了基本类型和自拥有类型,其引用类型需要使用生命周期标注:
struct Stu<'a> {
name: &'a str, // 至少比Stu实例生命周期长
age: u8,
}
// main
fn main(){
let name = String::from("xiaoming");
let stu = Stu {
name: name.as_str(),
age: 18,
};
println!("{:#?}", stu);
}
对于字段的生命周期需要在 imp 块中显示标注:
struct Stu<'a> {
name: &'a str,
age: u8,
}
// 对于struct 字段的生命周期标注显示指明/语法类似泛型
// 方法中的生命周期参数可以使用字段声明的,也可以自定义
// &self 方法可以有默认规则
impl<'a> Stu<'a> {
fn get_age(&self) -> u8 {
self.age
}
fn get_name(&self) -> &str {
self.name
}
//fn get_name2(&self, other: &str) -> &str{
// other // error 引用规则3返回类型的声明周期与 self相同
//}
}
本文链接: Rust基础(一)
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
发布日期: 2022-05-28
最新构建: 2026-01-13
欢迎任何与文章内容相关并保持尊重的评论😊 !