目录
- 一、不安全的Rust
- 二、不安全的超能力
- 2.1 概念
- 2.2 解引用裸指针
- 2.3 调用不安全的函数或方法
- 2.3 创建不安全代码的安全抽象
- 2.4 使用extern函数调用外部代码
- 2.5 访问或修改可变静态变量
- 2.6 实现不安全trait
- 2.7 访问联合体中的字段
- 三、高级trait
- 3.1 关联类型在trait定义中指定占位符类型
- 3.2 关联类型与泛型的区别
- 3.3 默认泛型类型参数和运算符重载
- 3.4 完全限定语言与消歧义:如何调用相同名称的方法
- 3.5 使用supertrait来要求trait附带其它trait的功能
- 3.6 使用newtype模式用于在外部类型上实现外部trait
一、不安全的Rust
- Rust隐藏有第二种语言,它没有强制内存安全保证,这被称为不安全Rust(unsafe Rust);
- 不安全Rust存在的原因:
- 静态分析是保守的,使用unsafe Rust则是告诉编译器自己知道在做啥;
- 计算机硬件本身是不安全的, Rust需要能够进行底层系统编程;
二、不安全的超能力
2.1 概念
- 可以通过
unsafe
关键字将Rust切换到不安全Rust,开启一个块,存放不安全代码; - Unsafe Rust里执行的五种操作
- 解引用裸指针;
- 调用不安全的函数或方法;
- 访问或修改可变静态变量;
- 实现不安全trait;
- 访问
union
的字段;
- unsafe 并不会关闭借用检查器或禁用任何其他安全检查;
- unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能;
- 隔离unsafe代码,最好将它们封装进一个安全的抽象并提供安全API;
2.2 解引用裸指针
- 可变的原始指针: *mut T;
- 不可变的原始指针: *const T;
- 不可变意味着指针解引用之后不能直接赋值;
*
不是解引用运算符,它是类型名称的一部分;- 裸指针与引用和智能指针的区别:
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针;
- 不保证指向有效的内存;
- 允许为空;
- 不能实现任何自动清理功能;
fn main() {let mut num = 5;let r1 = &num as *const i32;let r2 = &mut num as *mut i32;let address = 0x012345usize;let r = address as *const i32;
// println!("r1 is: {}", *r1);
// println!("r2 is: {}", *r2);
}
- 代码同时创建不可变和可变裸指针;
- 可以在安全代码中创建裸指针,但是解引用必须在
unsafe
代码块里; - 使用
as
将不可变和可变引用强制转换为对应的裸指针类型; - address是一个不能确定其有效性的裸指针;
- 放开最后两行注释而直接进行解引用会产生错误;
- 使用
unsafe {}
把他们包裹起来就能正确运行了; - 为什么要用原始指针?
- 与C语言进行交互;
- 构建借用检查器无法理解的安全抽象;
2.3 调用不安全的函数或方法
- unsafe函数或方法:在定义前加上unsafe关键字;
- 调用unsafe函数要先满足条件(看文档),Rust无法对这些条件进行验证;
- 需要在unsafe块里调用;
unsafe fn dangerous() {}fn main() {dangerous();
}
- 编译错误
- 在main函数里,用
unsafe{}
将dangerous函数调用包裹起来就能通过了;
2.3 创建不安全代码的安全抽象
- 函数包含不安全代码并不意味着整个函数都需要标记为不安全;
- 将不安全代码封装进安全函数是一个常见的抽象;
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..])
}fn main() {let mut v = vec![1, 2, 3, 4, 5, 6];let (a, b) = split_at_mut(&mut v[..], 3);println!("a = {:?}", a);println!("b = {:?}", b);
}
- 上述代码中
split_at_mut
函数将传入的可变切片进行分割,返回两个可变切片; - 编译报错
- Rust的借用检查器任何代码将同一个变量借用了两次;
- 代码里明确了两个切片不会重叠,这就要触及不安全代码了;
- 这就需要用
unsafe
块,裸指针和一些不安全函数调用来修改split_at_mut
;
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {let len = slice.len();let ptr = slice.as_mut_ptr();assert!(mid <= len);unsafe {(slice::from_raw_parts_mut(ptr, mid),slice::from_raw_parts_mut(ptr.add(mid), len - mid))}
}
- 使用
len
方法获取切片的长度,使用as_mut_ptr
获取访问切片的裸指针;- slice变量是一个
i32
类型的可变切片,因此as_mut_ptr
返回一个*mut i32
类型的裸指针,储存在 ptr 变量中;
- slice变量是一个
slice::from_raw_parts_mut
函数获取一个裸指针和一个长度来创建一个切片;- 无需将
split_at_mut
函数标记为unsafe
,且该函数可以在安全的Rust中被调用;
下面这段代码使用slice时会崩溃
use std::slice;fn main() {let address = 0x01234usize;let r = address as *mut i32;let slice: &[i32] = unsafe {slice::from_raw_parts_mut(r, 10000)};println!("slice = {:#?}", slice);
}
编译正确,运行崩溃
2.4 使用extern函数调用外部代码
extern
函数可以创建和使用外部函数接口;extern
块中声明的函数在 Rust 代码中总是不安全的;- 外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
extern "C" {fn abs(input: i32) -> i32;
}fn main() {unsafe {println!("Absolute value of -3 according to C: {}", abs(-3));}
}
- 上述代码展示了如何集成C标准库中的
abs
函数; - 在
extern "C"
块中,列出了要调用的C语言中的外部函数;
从其它语言调用Rust函数
- 使用
extern
创建一个允许其他语言调用 Rust 函数的接口; - 还需增加
#[no_mangle]
标注;- Mangling是指当编译器将代码中指定的函数名进改时会增加一些额外的信息;
- 每一个编程语言的编译器都会以稍微不同的方式mangle函数名;
#[no_mangle]
pub extern "C" fn call_from_c() {println!("Just called a Rust function from C!");
}
- 将上述代码编译为动态库并从C语言中链接,
call_from_c
函数就能够在 C 代码中访问; - extern 的使用无需使用unsafe标注;
2.5 访问或修改可变静态变量
- 如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争;
- 全局变量在 Rust 中被称为 静态(static)变量 ;
- 通常静态变量的名称采用
SCREAMING_SNAKE_CASE
写法; - 静态变量只能储存拥有
static
生命周期的引用,因此静态变量的生命周期可以被Rust自动计算; - 访问不可变静态变量是安全的,访问和修改可变静态变量都是不安全的;
static mut COUNTER: u32 = 0;
static HELLO_WORLD: &str = "Hello, world!";fn add_to_count(inc: u32) {unsafe {COUNTER += inc;}
}fn main() {add_to_count(3);unsafe {println!("COUNTER: {}", COUNTER);}println!("name is: {}", HELLO_WORLD);
}
- 代码展示了不可变静态变量
HELLO_WORLD
的声明和使用; - 任何访问或修改可变静态变量
COUNTER
的代码都必须位于unsafe
中;
2.6 实现不安全trait
- 当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的;
- 在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也应该标记为unsafe;
2.7 访问联合体中的字段
参考文档:https://rustwiki.org/zh-CN/reference/items/unions.html
三、高级trait
3.1 关联类型在trait定义中指定占位符类型
- 关联类型(associated types) 是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法参数中就可以使用这些占位符类型;
- trait的实现者会针对特定的实现在这个类型的位置指定相应的具体类型;
- 标准库提供的
Iterator trait
就是一个带有关联类型的trait,内部的关联类型Item替代遍历的值的类型;
pub trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;
}
- Item是占位符,next的返回值说明它返回
Option<Self::Item>
类型的值; - trait的实现者指定Item的具体类型;
- 用起来像泛型;
3.2 关联类型与泛型的区别
泛型 | 关联类型 |
---|---|
每次实现Trait时标注类型 | 无需标注类型 |
可以为一个类型多次实现某个Trait(不同的泛型参数) | 无法为单个类型多次实现某个 Trait |
pub trait Iterator2<T>{fn next(&mut self) -> Option<T>;
}struct Counter{}impl Iterator2<String> for Counter{fn next(&mut self) -> Option<String> {None}
}impl Iterator2<u32> for Counter{fn next(&mut self) -> Option<u32> {None}
}
- 上述代码为 Counter实现了Iterator2的trait,返回值为String和u32;
pub trait Iterator{type Item;fn next(&mut self) -> Option<Self::Item>;
}struct Counter{}impl Iterator for Counter{type Item = u32;fn next(&mut self) -> Option<Self::Item> {None }
}
- 上述代码实现了关联类型,只能写一个,如果再写一遍
impl Iterator for Counter{
,里面的Item
写成String,就会报错;
3.3 默认泛型类型参数和运算符重载
- 可以在使用泛型参数的时候为泛型指定一个默认的具体类型;
- 语法为:
<PlaceholderType=ConcreteType>
- 这种技术通常用于运算符重载;
- Rust不允许创建自己的运算符及重载任意的运算符;
- 但可以通过
std::ops
中所列出的运算符和相应的 trait重载一部分相应的运算符;
use std::ops::Add;#[derive(Debug, PartialEq)]
struct Point {x: i32,y: i32,
}impl Add for Point {type Output = Point;fn add(self, other: Point) -> Point {Point {x: self.x + other.x,y: self.y + other.y,}}
}fn main() {assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },Point { x: 3, y: 3 });
}
- 上述代码在
Point
结构体上实现Add trait来重载+
运算符; - Add trait定义如下
#[doc(alias = "+")]
#[const_trait]
pub trait Add<Rhs = Self> {/// The resulting type after applying the `+` operator.#[stable(feature = "rust1", since = "1.0.0")]type Output;#[must_use = "this returns the result of the operation, without modifying the original"]#[rustc_diagnostic_item = "add"]#[stable(feature = "rust1", since = "1.0.0")]fn add(self, rhs: Rhs) -> Self::Output;
}
Rhs = Self
是默认类型参数;- RHS是一个泛型类型参数,它用于定义 add 方法中的 rhs 参数;
- 如果实现 Add trait 时不指定 RHS 的具体类型,RHS 的类型将是默认的 Self 类型,也就是Point;
- 下面是一个实现Add trait时使用自定义类型而不是默认类型的例子;
use std::ops::Add;struct Millimeters(u32);
struct Meters(u32);impl Add<Meters> for Millimeters {type Output = Millimeters;fn add(self, other: Meters) -> Millimeters {Millimeters(self.0 + (other.0 * 1000))}
}
- 这是将毫米和米相加的例子;
- 所以用
Add<Meters>
指明是米,相加的时候传进来的是米,所以乘以1000再相加; - 返回值是毫米;
默认泛型参数的主要应用场景
- 扩展一个类型而不破坏现有代码;
- 允许在大部分用户都不需要的特定场景下进行自定义;
3.4 完全限定语言与消歧义:如何调用相同名称的方法
trait Pilot {fn fly(&self);
}trait Wizard {fn fly(&self);
}struct Human;impl Pilot for Human {fn fly(&self) {println!("This is your captain speaking.");}
}impl Wizard for Human {fn fly(&self) {println!("Up!");}
}impl Human {fn fly(&self) {println!("*waving arms furiously*");}
}fn main() {let person = Human;person.fly();
}
- Pilot和Wizard都有
fly
方法; - Human又定义了一个
fly
; - 几个方法的参数是完全相同的,那么调用哪个?
- 所以调用的是Human本身的
fly
方法; - 如下代码演示了调用Pilot和Wizard的fly方法;
fn main() {let person = Human;Pilot::fly(&person);Wizard::fly(&person);person.fly();
}
- 当同一作用域的两个类型都实现了同一trait,Rust就不能明确的知道调用哪个函数;
- 使用完全限定语法(fully qualified syntax) 可以解决这个问题;
- 语法为:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
- 可以在任何调用函数或方法的地方使用;
- 允许忽略那些从其它上下文能推导出来的部分;
- 当Rust无法区分代码编写人员期望调用哪个具体实现时,才需要使用这种语法;
trait Animal {fn baby_name() -> String;
}struct Dog;impl Dog {fn baby_name() -> String {String::from("Spot")}
}impl Animal for Dog {fn baby_name() -> String {String::from("puppy")}
}fn main() {println!("A baby dog is called a {}", Dog::baby_name()); //A baby dog is called a Spot//println!("A baby dog is called a {}", Animal::baby_name());
}
- Animal trait有关联函数baby_name,结构体 Dog 实现了 Animal,都是一些关联方法(没有self);
- baby_name直接在Dog之上,因此使用
Dog::baby_name
直接调用就可以; - 放开最后一个
println!
,则无法编译通过;
- 使用完全限制语法解决它:
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
3.5 使用supertrait来要求trait附带其它trait的功能
- 需要在一个trait中使用其它trait的功能;
- 需要被依赖的trait也被实现
- 那个被间接依赖的trait就是当前trait的supertrait;
use std::fmt;trait OutlinePrint: fmt::Display {fn outline_print(&self) {let output = self.to_string();let len = output.len();println!("{}", "*".repeat(len + 4));println!("*{}*", " ".repeat(len + 2));println!("* {} *", output);println!("*{}*", " ".repeat(len + 2));println!("{}", "*".repeat(len + 4));}
}struct Point {x: i32,y: i32,
}impl OutlinePrint for Point {}
- OutlinePrint trait里的
outline_print
函数要被使用,则必须实现fmt::Display
trait; - 结构体Point实现了OutlinePrint,但它没有实现
Display
trait,所以会报错;
- 在Point上实现了Display后就会通过编译;
impl fmt::Display for Point {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "({}, {})", self.x, self.y)}
}
3.6 使用newtype模式用于在外部类型上实现外部trait
- 孤独规则: 只有当trait或类型定义在本地包时,才能为该类型实现这个trait;
- 可以通过
newtype
模式绕过这个规则;- 利用
tuple struct
元组结构体创建一个新的类型;
- 利用
use std::fmt;struct Wrapper(Vec<String>);impl fmt::Display for Wrapper {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "[{}]", self.0.join(", "))}
}fn main() {let w = Wrapper(vec![String::from("hello"), String::from("world")]);println!("w = {}", w);
}
- 想在
Vec<T>
上实现Display确被孤独规则阻止(Display trait和Vec<T>
都定义于外面的包中); - 可以创建一个包含
Vec<T>
实例的 Wrapper 结构体, 在它之上实现 Display 并使用Vec<T>
的值;