31_rust_高级特性
高级特性
- 不安全rust
- 高级Trait
- 高级类型
- 高级函数和闭包
- 宏
不安全rust
隐藏这第二个语言,其未强制内存安全保证:Unsafe rust(不安全的rust);其和普通rust一样,但提供了额外的“超能力”。
unsafe rust存在的原因:
- 静态分析是保守的,使用unsafe rust,表示开发者知道自己在做什么,并承担相应风险。
- 计算机硬件本身就不安全,rust需要能够进行底层系统编程。
unsafe的超能力:
使用unsafe
关键字来切换到unsafe rust,开启一个块,块内为unsafe代码。
unsafe rust里可执行四个动作(超能力):
- 解引用原始指针
- 调用unsafe函数或方法
- 访问或修改可变的静态变量
- 实现unsafe trait
注意:
- unsafe并未关闭借用检查或停用其他安全检查,如果在里边使用引用,依然会被检查。所以即便在unsafe代码块中依然可获得一定的安全性。
- 任何内存安全相关的错误必须保留在unsafe块里。
- 尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全的API。
很多标准库里也含有unsafe代码,但使用安全接口封装,防止不安全代码泄露到调用处。
解引用原始指针
原始指针分类:
- 可变的:*mut T
- 不可变的:*constT。意味着指针在解引用后不能直接对其进行赋值
注意:这里的
*
不是解引用符号,它是类型名的一部分。
与引用的区别点,原始指针:
- 允许通过同时具有不可变和可变指针,或多个指向同一位置的可变指针,并忽略借用规则
- 无法保证能指向合理的内存
- 允许为 null
- 不实现任何自动清理
- 放弃保证的安全,换取更好的性能/与其它语言或硬件接口的能力
fn main() {
let mut n = 5;
let r1 = &n as *const i32; // 不可变原始指针
let r2 = &mut n as *mut i32; // 可变原始指针
// 可在非安全代码块外创建原始指针,但只能在不安全代码块内进行解引用
// 在非安全代码块内解引用这两个必定有效的指针
unsafe {
println!("r1 {}", *r1);
println!("r2 {}", *r2);
}
// 创建一个无法确定有效性的原始指针
let addr = 0x0123456usize; // 一个内存地址,可能有数据也可能无
let r = addr as *const i32; // 创建一个该地址的原始指针,编译不会报错
unsafe {
println!("r {}", *r); // 解引用不确定指针,编译不报错,需开发者自己保证安全性
}
}
/* 运行输出:
r1 5
r2 5
thread 'main' panicked at 'misaligned pointer dereference: address must be a multiple of 0x4 but is 0x123456', src\main.rs:15:7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread caused non-unwinding panic. aborting.
*/
用原始指针的作用:与c语言进行接口,构建借用检查器无法理解的安全抽象。
调用unsafe函数或方法
unsafe函数或方法,在定义前加上unsafe关键字。
- 调用前需开发者确认满足要求或条件(通常由提供方提供文档说明),因为rust无法对这些条件进行验证。
- 需要在unsafe块里进行调用。
unsafe fn danger() {}
fn main() {
unsafe {
danger(); //正确调用
}
danger(); // 编译报错 call to unsafe function is unsafe and requires unsafe function or block
}
创建unsafe代码的安全抽象
函数包含unsafe代码并不意味着需要将整个函数标记为unsafe,将unsafe代码包裹在安全函数中是一个常见的抽象。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let r = &mut v[..]; // 建立一个完整切片
// split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
// 分割成两个切片,分别是1 2 3, 4 5两组
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5]);
}
split_at_mut是库函数,使用了不安全函数,为了理解,这里进行手动定义如下:
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid], &mut slice[mid..]) // 试图返回两个切片,以mid作为切分点
// 但这里会报无法对切片进行两次借用的错误,因为这是rust语言的规则
// 但实际情况是可以的切分借用的,因为这里借用的是前后两个无交叉部分,切分后无关联,但rust规则限定,无法识别这种情况
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let r = &mut v[..]; // 建立一个完整切片
// split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
// 分割成两个切片,分别是1 2 3, 4 5两组
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5]);
}
编译报错:cannot borrow *slice
as mutable more than once at a time,所以这里需要使用unsafe代码块来实现,进行安全抽象,代码如下:
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr(); //返回原始指针,类型是*mut i32,指向slice
assert!(mid <= len);
unsafe {
(slice::from_raw_parts_mut(ptr, mid), // 创建切片
slice::from_raw_parts_mut(ptr.add(mid), len - mid))
} //由于使用了原始指针和偏移量,代码块不安全,调用需要在unsafe块里完成
// 虽然split_at_mut调用了不安全代码,但自身未标记为不安全函数,它就是不安全代码的安全抽象
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let r = &mut v[..]; // 建立一个完整切片
// split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
// 分割成两个切片,分别是1 2 3, 4 5两组
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5]);
}
如果在main中不是调用安全抽象函数,而是直接调用非安全函数:
fn main() {
let addr = 0x0123456usize;
let r = addr as *const i32;
let slice: &[i32] = unsafe {
slice::from_raw_parts_mut(r, 50);
};
// 如果这种方式调用,编译不会报错,但运行时可能崩溃,因为无法保障切片是有效的
}
使用extern函数调用外部代码
extern关键字:简化创建和使用外部函数接口(FFI)的过程
外部函数接口(FFI,Foreign Function Interface):允许一种编程语言定义函数,并让其它编程语言能调用这些函数。
extern "C" {
fn abs(in: i32) -> i32;
}
fn main() {
unsafe {
println!("call c abs:{}", abs(-3));
}
}
应用二进制接口(ABI,Application Binary Interface):定义函数在汇编层的调用方式。
“C” ABI是最常见的ABI,它遵循C语言的ABI。
从其它语言调用rust函数
可使用extern创建接口,其它语言通过它们可调用rust的函数。
- 在fn前添加extern关键字,并指定ABI
- 还需添加
#[no_mangle]
注解(这是一个编译阶段),避免rust在编译时改变它的名称
#[no_mangle]
pub extern "C" fn call_func() {
println!("func in rust");
}
且无需使用unsafe。
访问或修改一个可变静态变量
rust支持全局变量,但因所有权机制可能产生某些问题,比如数据竞争。
在rust里,全局变量叫静态(static)变量。声明时必须指明类型。其生命周期能被编译器推断出,只存储'static
的引用。
static HELLO: &str = "hello";
fn main() {
println!("{}", HELLO);
}
静态变量
- 静态变量与常量类似
- 命名:SCREAMIN_SNAKE_CASE,全大写+下划线分割
- 必须标注类型
- 静态变量只能存储
'static
生命周期的引用,无需显示标注 - 访问不可变静态变量是安全的
常量和不可变静态变量的区别
- 静态变量有固定的内存地址,使用它的值总会访问同样的数据
- 常量允许使用它们的时候对数据进行复制
- 静态变量可以是可变的,访问和修改静态可变变量是不安全的(unsafe)
static mut CNT: u32 = 0;
fn add(inc: i32) {
unsafe {//访问和修改静态是unsafe的,所以要放在unsafe块中
CNT += inc;
}
}
fn main() {
add(5);
unsafe {//如果多线程的情况可能出现数据竞争
println!("{}", CNT);
}
// 多线程的情况下,很难保障数据竞争不发生,所以是不安全的
}
实现不安全(unsafe)trait
当某个trait中存在至少一个方法拥有编译器无法校验的不安全因素时,就称之为trait是不安全的。
声明unsafe trait:在定义前加unsafe关键字,该trait只能在unsafe代码块中实现。
unsafe trait Ts {
// methods go here
}
unsafe impl Ts for i32 {
// method implementations go here
}
何时使用 unsafe 代码
- 编译器无法保证内存安全,保证 unsafe 代码正确并不简单
- 有充足理由使用 unsafe 代码时,就可以这样做
- 通过显式标记 unsafe,可以在出现问题时轻松的定位
高级trait
在 Trait 定义中使用关联类型来指定占位类型,
关联类型(associated type)
是 Trait 中的类型占位符,它可以用于 Trait 的方法签名中: 可以定义出包含某些类型的 Trait,而在实现前无需知道这些类型是什么。
pub trait Iter {
type Item; // Item是关联类型,类型占位符
// 在迭代的过程中,用item来替代具体的值,与泛型有点类似
fn next(&mut self) -> Option<Self::Item>;
}
fn main() {
}
关联类型与泛型的区别
- 泛型
每次实现Trait 时标注类型;可以为一个类型多次实现某个 Trait(不同的泛型参数)。 - 关联类型
无需标注类型, 无法为单个类型多次实现某个Trait。
默认泛型参数和运算符重载
- 可以在使用泛型参数时为泛型指定一个默认的具体类型
- 语法:
<PlaceholderType=ConcreteType>
- 这种技术常用于运算符重载(operator overloading)
- Rust不允许创建自己的运算符及重载任意的运算符
- 但可以通过实现 std::ops 中列出的那些 trait 来重载一部分相应的运算符
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Pt {
x: i32,
y: i32,
}
impl Add for Pt {
type Output = Pt;
fn add(self, other: Pt) -> Pt {
Pt { x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(Pt {x: 1, y: 1} + Pt {x: 2, y: 3}, Pt{x: 3, y: 4});
}
默认泛型参数被指定类型的例子
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Mm(u32);
#[derive(Debug, PartialEq)]
struct M(u32);
impl Add<M> for Mm {
type Output = Mm;
fn add(self, other: M) -> Mm {
Mm (self.0 + (other.0 * 1000))
}
}
fn main() {
assert_eq!(Mm(1) + M(1), Mm(1001));
}
默认泛型参数的主要应用场景:
- 扩展一个类型而不破坏现有代码
- 允许在大部分用户都不需要的特定场景下进行自定义
完全限定语法(Fully Qualified Syntax)
如何调用同名方法
trait P1 {
fn func(&self);
}
trait P2 {
fn func(&self);
}
struct H;
impl P1 for H {
fn func(&self) {
println!("p1 func");
}
}
impl P2 for H {
fn func(&self) {
println!("p2 func");
}
}
impl H {
fn func(&self) {
println!("H's func");
}
}
fn main() {
let h = H;
h.func();// 默认是自身的func
P1::func(&h);//通过传入的h区分是哪个实现,类似于多态
P2::func(&h);
}
/*
H's func
p1 func
p2 func
*/
而对于没有参数的场景,则需要采用完全限定语法。
完全限定语法(Fully Qualified Syntax)
完全限定语法格式:<Type as Trait>::function(receiver_if_method, netx_arg, ...);
- 可在任何调用函数或方法的地方使用
- 允许忽略从其它上下文能推导出来的部分
- 当rust无法区分期望调用具体哪个函数实现时才使用这种方式
trait A {
fn func() -> String;
}
struct D;
impl D {
fn func() -> String {
String::from("from D")
}
}
impl A for D {
fn func() -> String {
String::from("from A for D")
}
}
fn main() {
println!("{}", D::func());
println!("{}", <D as A>::func());
}
/*
from D
from A for D
*/
使用supertrait要求trait附带其它trait功能
使用supertrait要求trait附带其它trait的功能,就相当于一个trait继承于另外一个trait。
需要一个trait中使用其它trait的功能,需要被依赖的trait也被实现,被间接依赖的trait就是当前trait的supertrait。
use std::fmt;
trait O: fmt::Display {//要求实现display trait
fn o_print(&self) {
let o = self.to_string(); //要求实现to_string方法
let len = o.len();
println!("{}, {}", len, o);
}
}
struct P {
x: i32,
y: i32,
}
impl O for P {} // 因为有默认实现,则可不用再实现o_print
// 但依然会报错,因为O trait要求实现display trait
impl fmt::Display for P {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{} {}]", self.x, self.y)
}
}
fn main() {
let p1 = P {x: 3, y: 6,};
p1.o_print();
}
/*输出
5, [3 6]
*/
使用 newtype 模式在外部类型上实现外部 trait
孤儿规则:只有当 trait 或类型定义在本地包时,才能为该类型实现这个trait
可以通过 newfype 模式来绕过这一规则,利用 tuple struct(元组结构体)创建一个新的类型的。
use std::fmt;
// 如果想对vec实现display trait,但这两个都实现在外部包中,无法像自定义struct那样实现
// 就可以将vec放在一个struct里,对该struct进行实现
struct Tv(Vec<String>); // 创建一个本地tuple struct,包含Vec
impl fmt::Display for Tv {// 为本地struct实现display trait
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", ")) //由于是tuple struct,通过self.0获取vec
}
}
fn main() {
let w = Tv(vec![String::from("a"), String::from("b"), String::from("c"),]);
println!("{}", w);
}
/*
[a, b, c]
*/
高级类型
使用newtype模式实现类型安全和抽象
newtype 模式可以:
- 用来静态的保证各种值之间不会混淆并表明值的单位
- 为类型的某些细节提供抽象能力
- 通过轻量级的封装来隐藏内部实现细节
使用类型别名创建类型同义词
Rust提供了类型别名的功能:
- 为现有类型生产另外的名称(同义词)
- 但并不是一个独立的类型
- 使用 type 关键字实现该功能
- 主要用途:减少代码字符重复
type MyI32T = i32; //为i32定义别名
fn main() {
let x: i32 = 5;
let y: MyI32T = 6;
println!("{}", x + y);
}
针对类型很长的场景:
type BTy = Box<dyn Fn() + Send + 'static>;
fn func1(f: BTy) {}
fn func2() -> BTy {
Box::new(|| println!("hi"))
}
fn main() {
let f: BTy = Box::new(|| println!("f"));
}
在标准库里的场景
use std::io::Error;
use std::fmt;
// pub trait Write {
// fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
// fn flish(&mut self) -> Result<usize, Error>;
// fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
// }
// 在std::io下的大部分函数都会返回Result<usize, Error>类型,这个Result<T, E>中T不同,E都是Error
// 所以在std::io下定义了一个如下别名,下边表达式声明在std::io里
// type Result<T> = Result<T, std::io::Error> // 实际不能再重复声明
// 这样定义后,就只剩T对外呈现了,E写死为std::io::Error了
// 上面trait就可改成如下格式
type Result<T> = std::io::Result<T>;
pub trait Write2 {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flish(&mut self) -> Result<usize>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
}
fn main() {}
Never类型
有一个名为!
的特殊类型:
- 它没有任何值,行话称为空类型(empty type)
- 倾向于叫它 never 类型,因为它在不返回的函数中充当返回类型
- 不返回值的函数也被称作发散函数(diverging function)
fn func() -> ! {
// 这里什么都不返回和返回!类型是不同的,这段代码编译报错:
// expected `!`, found `()`
// 如果返回值为! 则表示该函数永远不会返回值,但实际无法创建出来这样的never值
}
fn main() {}
continue表达式
fn main() {
let g = "";
loop {
let g: u32 = match g.trim().parse() {
Ok(n) => n,
Err(_) => continue, // 这里返回值是continue,continue类型就是never类型,这里将被强制转成u32类型
// never类型会被强制转成其它任意类型
}
}
}
panic!的返回值也是nerver类型
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(v) => v, // panic!返回值!,因为其会中断程序,不会返回值
None => panic!("called Option::unwrap() on a None value"),
// 所以在None的时候,不会为unwrap返回一个值,所以定义是合理的
}
}
}
loop循环
fn main() {
loop {// loop表达式永远不会结束,所以返回类型是nerver类型
print!("forever");
}
}
动态大小和Sized Trait
Rust需要在编译时确定为一个特定类型的值分配多少空间。
动态大小的类型(Dynamically Sized Types,DST)的概念:
- 编写代码时使用只有在运行时才能确定大小的值
- str是动态大小的类型(注意不是 &str):只有运行时才能确定字符串的长度
下列代码无法正常工作:
let s1: str = "Hello there!";
let s2: str = "How's it going2";
使用&str解决:因为字符串切片存放的是str的地址和长度。
Rust 使用动态大小类型的通用方式:
- 附带一些额外的元数据来存储动态信息的大小
- 使用动态大小类型时总会把它的值放在某种指针后边
另外一种动态大小的类型:trait
- 每个 trait 都是一个动态大小的类型,可以通过名称对其进行引用
- 为了将 trait 用作 trait 对象, 必须将它放置在某种指针之后,
例如放在&dyn Trait 或 Box<dyn Trait>(Rc<dyn Trait>)
之后
Sized trait
为了处理动态大小的类型,Rust 提供了一个 Sized trait 来确定一个类型的大小在编译时是否已知:
- 编译时可计算出大小的类型会自动实现这一 trait
- Rust 还会为每一个泛型函数隐式的添加 Sized 约束
fn ge<T>(t: T) {}
//会被隐式转成如下
fn ge<T: Sized>(t: T) {}
?Sized
trait约束
默认情况下,泛型函数只能被用于编译时已经知道大小的类型,可以通过特殊语法解除这一限制
该语法只能用在sized语法上,不能用于其他。
fn ge<T>(t: T) {}
//会被隐式转成如下
fn ge<T: Sized>(t: T) {}
//?Sized表示T可能为sized也可能不是sized
fn ge<T: ?Sized>(t: &T) {} //这里变成了&T,因为T可能运行时才能确定大小,只能为指针
高级函数和闭包
函数指针
可将函数传递给其它函数,函数在传递过程中会被强制转换成fn类型,fn类型就是“函数指针(function pointer)”。
fn func1(x: i32) -> i32 {
x + 1
}
fn func2(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let a = func2(func1, 5);
println!("{}", a);
}
函数指针与闭包的不同
- fn是一个类型,不是一个trait,可直接指定fn为参数类型,不用声明一个Fn trait为约束的泛型参数。
- 函数指针实现了全部3种闭包trait(Fn, FnMut, FnOnce),总是可把函数指针用作参数传递给一个链接闭包的函数,所以倾向于搭配闭包trait的泛型编写函数,可同时接收闭包和普通函数。
- 某些情况下,只接收fn而不接收闭包,与外部不支持闭包的代码交互,如c函数。
fn main() {
let l1 = vec![1, 2, 3];
// 传入闭包
let l_str: Vec<String> = l1.iter().map(|i| i.to_string()).collect();
let l1 = vec![1, 2, 3];
// 传入函数指针
let l_str: Vec<String> = l1.iter().map(ToString::to_string).collect();
}
另一个枚举构造的例子:
fn main() {
enum St {
V(u32),
Ss,
}
let v = St::V(5);// 这个构造器被实现成了函数,所以下边map可直接传入构造器函数指针
let l: Vec<St> = (0u32..20).map(St::V).collect();
}
返回闭包
闭包使用trait进行表达,无法在函数中直接返回一个闭包,可将一个实现了该trait的具体类型作为返回值。
// fn re_closure() -> Fn(i32) -> i32 {
// |x| x + 1 //试图返回一闭包,但报错trait objects must include the `dyn` keyword
// }
fn re_closure2() -> Box<dyn Fn(i32) -> i32> { //正确写法
Box::new(|x| x + 1)
}
fn main() {}
宏macro
宏在 Rust 里指的是一组相关特性的集合称谓,主要有:
- 使用 macro_rules!构建的声明宏(declarative macro)
- 3种过程宏
- 自定义#[derive] 宏,用于 struct 或 enum,可以为其指定随 derive 属性添加的代码
- 类似属性的宏,在任何条目上添加自定义属性的
- 类似函数的宏,看起来像函数调用,对其指定为参数的 foken 进行操作
函数与宏的差别
- 本质上,宏是用来编写可以生成其它代码的代码(元编程,metaprogramming)
- 函数在定义签名时,必须声明参数的个数和类型,宏可处理可变的参数
- 编译器会在解释代码前展开宏
- 宏的定义比函数复杂得多,难以阅读、理解、维护
- 在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域
- 函数可以在任何位置定义并在任何位置使用的
macro_rules!声明宏(可能弃用)
rust中最常见的宏形式:声明宏,类似于match模式匹配,需要使用macro_rules!。
let v: Vec<u32> = vec![1, 2, 3];
// 上面的vec!宏,其简化的定义如下:
#[macro_export] //标注,表示宏所在的包只有被引入作用域后才可被使用,缺少这个标注则不能被引入作用域
macro_rules! vec { // 使用macro_rules!声明了一个名字为vec的宏
//以下内容有点像match的模式匹配,这里只有一个分支,匹配的是rust的代码结构
/*这是一个模式,$x:expr表示任意表达式expr,被命名为$x。
后面的,号表示紧跟表达式后可能是,号作为分隔符,再后面的*号表示*号前的语句“( $x:expr ),”可能被匹配0次或多次。
比如vec![1, 2, 3]表示$x会匹配1 2 3三次
*/
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
//匹配后大概如下:
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
macro_rules!关键字有些奇怪的技术细节,rust团队正在推第二种声明宏的方式,修复了当前宏声明可能的极端情况,后会将本宏声明的方式标注为弃用。
基于属性来生成代码的过程宏
这种形式更像函数(某种形式的过程)一些,接收并操作输入的 Rust 代码,生成另外一些 Rust 代码作为结果。
三种过程宏:
- 自定义派生
- 属性宏
- 函数宏
创建过程宏时:一宏定义必须单独放在它们自己的包中,并使用特殊的包类型。
use proc_macro;
#[some_attribute] //指定过程宏的占位符
// 定义过程宏的函数,参数和返回值都是TokenStream(定义于proc_macro包中)
pub fn func(input: TokenStream) -> TokenStream {}
自定义 derive 宏
类似属性的宏
- 属性宏与自定义 derive 宏类似
- 允许创建新的属性
- 但不是为 derive 属性生成代码
属性宏更加灵活:
- derive 只能用于 struct 和 enum
- 属性宏可以用于任意条目,例如函数
类似函数的宏
函数宏定义类似于函数调用的宏,但比普通函数更加灵活
函数宏可以接收 TokenStream 作为参数
与另外两种过程宏一样,在定义中使用 Rust 代码来操作 TokenStream