不安全Rust
不安全超能力
wow!看来我们在C++中习以为常的事情在Rust这里变成了不安全超能力
在代码块前使用unsafe
关键字来切换到不安全模式,获得以下的能力
- 解引用裸指针(raw pointer)
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问 union 的字段
解引用裸指针
我们熟悉的指针回来辣!!
不可变的裸指针*const T
,可变的裸指针*mut T
裸指针与引用和智能指针的区别在于(在C++中我们可太熟悉了):
- 允许忽略借用规则(可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针)
- 不能保证自己总是指向了有效的内存地址
- 允许为空
- 没有实现任何自动清理机制
可以在安全的代码块中创建裸指针,使用as
关键字将正常的智能指针或引用转换成裸指针
但是只能在unsafe块中对裸指针进行解引用
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
我们将在接下来的小节中给出一个裸指针有用武之地的例子
调用不安全的函数或方法
不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe
unsafe fn dangerous() {}
unsafe {
dangerous();
}
创建不安全代码的安全抽象
在这里,我们展示如何实现一个安全的函数split_as_mut
,该函数接受一个切片,返回在指定索引切割后获得的两个可变切片。该函数使用了不安全的Rust,但是函数本身是安全的
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))
}
}
这里的原理是,切片是由一个长度和一个裸指针组合成的,我们先将其长度和裸指针取出来,再重新组合成两个切片
这里面涉及到了对裸指针的操作,所以需要在unsafe块中进行
但是我们这里通过assert!保证了其裸指针的操作一定是有效的,也就是说我们知道这个函数运行起来没有安全上的问题,只是使用了一些“不安全超能力”。所以整个函数并没有打上unsafe的标记
这就是为不安全代码创建了一个安全抽象
使用 extern 函数调用外部代码
extern,有助于创建和使用 外部函数接口(Foreign Function Interface, FFI)
extern 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
这里暂且不做深入的讨论
访问或者修改一个可变静态变量
全局变量在 Rust 中被称为 静态(static)变量
- 以关键字
static
开头 - 通常使用全部大写,以下划线为连接的命名方式
STATIC_VAR
- 必须标注变量类型
- 只能存储生命周期是
'static
的引用 - 静态变量的值在内存中有固定的地址
- 访问和修改可变静态变量是不安全的
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
实现不安全trait
当某个tarit中存在至少一个方法拥有编译器无法校验的不安全因素时,整个trait就是不安全的
实现不安全的trait也是不安全的行为
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
高级trait
使用关联类型指定占位类型
关联类型是trait中类型的占位符,这个占位符所代表的类型将由后续的实现所决定
稍微偏题,讲讲Self
在Rust编程语言中,Self
关键字是一个特殊的类型名称,代表正在实现的类型。这在实现结构体(Struct)或枚举(Enum)的方法时非常有用。
Self
关键字的主要用途包括:
- 在实现类型的方法中,
Self
用来表示实例的类型。例如:
struct MyStruct;
impl MyStruct {
fn new() -> Self {
MyStruct
}
}
在上述代码中,new
函数返回一个MyStruct
类型的实例,但是我们使用Self
来指代这个类型,而不是直接用MyStruct
。这样做的好处是,如果我们改变了结构体的名称,我们并不需要在每一个方法中都去修改它,只需在结构体定义的地方修改即可。
- 在特性(Trait)的实现中使用
Self
。例如:
trait MyTrait {
fn new() -> Self;
}
struct MyStruct;
impl MyTrait for MyStruct {
fn new() -> Self {
MyStruct
}
}
在这个例子中,MyTrait
特性定义了一个new
方法,这个方法返回一个实现这个特性的类型的实例。然后,我们为MyStruct
实现了这个特性,所以在new
方法中,Self
代表的就是MyStruct
。
含有关联类型的trait
比如我们之前举的例子
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
- 我们先使用
type Item
来对类型占位 - 之后在通过语法
Self::Item
指代,“实现该trait的那个具体类型中,Item被定义的类型”
与泛型的区别:
如果把这个占位符替换成泛型参数,合不合适呢?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 Counter 的 next 方法时,必须提供类型标注来表明希望使用 Iterator 的哪一个实现
通过关联类型,则无需标注类型,因为不能多次实现这个 trait
默认泛型参数和运算符重载
可以在定义泛型时通过语法<T=默认类型>
来为泛型指定默认类型
这个技术通常用于运算符重载
Rust没有像C++一样的运算符重载,Rust是通过为std::ops
中的trait实现,来重载对应的运算符
比如重载+
就需要重载Add 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 });
}
而Add trait的定义里就是用了默认泛型参数,这因为一般来说我们都是将两个类型相同的东西相加
trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
用于消除歧义的完全限定语法:调用相同名称的方法或关联函数
同名方法
为一个类型实现多个trait时,不同的trait中可能有同名的方法,这个类型自身还可能有同名的方法
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*");
}
}
进行调用时
- 直接通过
实例.方法名
默认调用到该类型自己拥有的方法 - 通过
trait名::方法名(&实例名,...)
调用到具体哪个tarit中的方法
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
同名关联函数
与方法一样,为一个类型实现多个trait时,不同的trait中可能有同名的关联函数,这个类型自身还可能有同名的关联函数
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")
}
}
此时调用关联函数时,就需要使用完全限定语法<类型 as trait名>
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
用于在tarit中附带另一个trait功能的超trait
如果一个trait是基于另一个trait实现的,我们就像对泛型指定trait约束一样指定依赖
trait 依赖: 被依赖
比如我们基于Display trait实现一个OutlinePrint,它会打印出带有星号框的值
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));
}
}
newtype 模式用以在外部类型上实现外部 trait
使用元组结构体创建一个新类型,该结构体只有一个字段。
我们称这个为瘦封装
通过这样我们可以巧妙地为定义于其它包的类型实现定义于其他包的trait:将种种行为委托与self.0
即可
比如我们为Vec<T>
实现Display
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);
}
这样的做法留下的问题是,新类型Wrapper
并不拥有Vec<T>
的其它方法,除非我们手动通过将各种行为委托于self.0
将其重新暴露出来;如果我们确实需要Vec<T>
的所有方法,那就为Wrapper
实现Deref trait即可
高级类型
为了类型安全和抽象而使用 newtype 模式
newtype模式是我们上一节提到的使用单一字段的元组结构体
- 可以用来静态的保证各种值之间不被混淆
- 标明值使用的单位
- 为类型的某些细节提供抽象能力
- 隐藏内部实现
使用类型别名来创建同义类型
只是为类型取个新名字,类似于C++中的typedef
主要用途是为那些名字过于复杂的复合类型取个别名,更加清晰
type Thunk = Box<dyn Fn() + Send + 'static>;
或是为泛型指定部分具体化的别名
type Result<T> = std::result::Result<T, std::io::Error>;
永不返回的never类型
该特殊类型名为!
,
-
类型
!
可以被强制转化为任意类型-
panic!就会返回一个
!
类型,这使得我们可以在match的任何分支中使用panic!。比如实现unwrap
方法impl<T> Option<T> { pub fn unwrap(self) -> T { match self { Some(val) => val, None => panic!("called `Option::unwrap()` on a `None` value"), } } }
-
-
该类型永不返回,所以也叫never类型
- 比如
loop
就是以!
为返回类型
- 比如
动态大小类型和 Sized trait
动态大小类型(dynamically sized types,DST),或者unsized types,这些类型允许我们处理只有在运行时才知道大小的类型。
必须将动态大小类型的值置于某种指针之后。
有几个我们一直在使用的DST,比如str
,我们总是通过&str
使用;再比如trait
,我们总是以trait对象的形式使用
为了处理 DST,Rust 有一个特定的 trait 来确定一个类型的大小是否在编译时可知:这就是 Sized trait。
实现这个 trait 的类型,编译器在编译时就知道其大小。
另外,Rust 隐式的为每一个泛型函数增加了 Sized bound。也就是说,对于如下泛型函数定义:
fn generic<T>(t: T) {
// --snip--
}
实际上被当作如下处理:
fn generic<T: Sized>(t: T) {
// --snip--
}
泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized
trait bound 与 Sized
相对;也就是说,它可以读作 “T 可能是也可能不是 Sized 的”。这个语法只能用于 Sized ,而不能用于其他 trait。
另外注意我们将 t 参数的类型从 T 变为了 &T:因为其类型可能不是 Sized 的,所以需要将其置于某种指针之后。在这个例子中选择了引用。
高级函数与闭包
函数指针
函数是可以作为参数传递的
函数指针的参数类型是fn(参数类型列表)->返回值类型
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
再谈闭包参数
函数指针fn实现了所有闭包trait,所以接受一个闭包参数的地方总是可以接受一个函数指针
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string)
.collect();
更奇特的,元组结构体、元组结构体枚举变体,其初始化语法()与调用函数类似
其构造器真的被实现为了函数,这类“函数”接受参数,返回的是一个实例
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> =
(0u32..20)
.map(Status::Value)
.collect();
上面的例子中,通过Status::Value
调用所谓的“构造器函数”,该函数接受一个u32为参数,返回一个Value(u32)
的Status
实例
事实证明,这种“构造器函数”也能作为参数传给接受闭包的位置
返回闭包
当我们需要返回闭包时,我们返回闭包trait对象
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
宏
这部分实在难以整理归纳,主要是很复杂,无法用三言两语说清。原书也只是给了几个具体的例子
这里给出学习资源
Rust程序设计语言:宏
The Little Book of Rust Macros