不安全的 Rust
编译器的检查是非常保守的, 哪怕有些代码可以执行, 他也不允许你这样做
unsafe 关键字就是告诉编译器, 我知道自己在做什么, 让我自己来承担风险(可能导致空指针之类的问题)
unsafe 代码块超能力:
- 解引用原始指针(裸指针)
raw-pointers
- 代用 unsafe 函数/方法
- 访问/修改可变的静态变量
- 实现 unsafe trait
- 解引用原始指针(裸指针)
注意:
- unsafe 并不会在编译时关闭检查(就算在 unsafe 代码块中也还是会检查)
- 任何内存安全相关的错误必须留在 unsafe 代码块中
- 尽可能不使用/隔离 unsafe 代码, 将其分装在安全的抽象里, 对外提供安全的API
解引用裸指针(原始指针)
- 可变的:
*mut T
, 此处的*
不是解引用, 而是类型的一部分 - 只读的:
*const T
, 所谓只读的就是不可变的
与Rust中的引用不同的是, 原始指针有这些特性:
- 允许同时有多个不可变和多个可变的指针指向同一内存空间的可变指针(不遵循Rust的借用规则)
- 无法保证能指向合理的内存空间
- 允许为 NULL
- 不实现任何自动清理, 也就是说需要手动释放内存
放弃内存安全的保证, 使用不安全的Rust可以换取更好的与其他语言/硬件接口交互的能力
获取裸指针
use std::ptr::{addr_of_mut, addr_of};
fn main() {
let mut num = 5;
// 此时 r1/r2/r3/r4 原始指针指向的是同一个内存空间
// 如果我通过原始指针修改值, 那么 r1/r2/r3/r4 对应内存空的值都改变
// 1. 用宏来获取裸指针
let r1 = addr_of_mut!(num); // 获取可变的裸指针
let r2 = addr_of!(num); // 获取只读的裸指针
// 2. 用强制转类型的方式来获取裸指针
let r3 = &mut num as *mut i32; // 获取可变的裸指针
let r4 = &num as *const i32; // 获取只读的裸指针
unsafe {
println!("befre update, r1 is: {:?}", *r1);
println!("befre update, r2 is: {:?}", *r2);
println!("befre update, r3 is: {:?}", *r3);
println!("befre update, r4 is: {:?}", *r4);
*r1 = 10;
println!("r1 is: {:?}", *r1);
println!("r2 is: {:?}", *r2);
println!("r3 is: {:?}", *r3);
println!("r4 is: {:?}", *r4);
}
}
调用不安全的函数/方法
use std::ptr::addr_of_mut;
static mut SLOGAN: &str = "C is the best program language!";
// 被 unsafe 关键字修饰的函数, 都是不安全的函数
unsafe fn dangerous() {
unsafe {
// 使用宏直接获取静态变量的裸指针, 并重新修改值
let slogan_ptr = addr_of_mut!(SLOGAN);
*slogan_ptr = "Rust is the best program language!";
}
}
fn main() {
// dangerous();
// 不能在 unsafe 块外部调用 unsafe 关键字修饰的函数/方法
// error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe function or block
unsafe {
// 必须在 unsafe 内部调用 unsafe 关键字修饰的函数/方法
println!("before update, SLOGAN is: {:?}", SLOGAN);
dangerous();
println!("after update, SLOGAN is: {:?}", SLOGAN);
}
}
extern 使用外部ABI接口
ABI: application binary interface, 用于指定编译器如何调用外部接口
了解即可, 要这样操作这个还是比较麻烦的
// Rust调用其他语言暴露的接口
extern "C" {
fn meanof(a:i32, b:i32, c:i32) -> i32;
}
// Rust暴露接口给其他语言去调用
// 注意必须加 #[no_mangle] 注解
pub extern "C" fn hi_rust() {
println!("hi rust!");
}
fn main() {
unsafe {
// 外部的函数接口都是不安全的
let x = meanof(1, 3, 5);
println!("x: {}", x);
}
}
高级类型
使用类型别名创建类型同义词
为现有数据类型定义别名
// type keyword alias like c lang style
type int = i32;
type long = i64;
type Thunk = Box<dyn Fn() + Send + 'static>;
fn main() {
// int 不是一个新的类型, 他的本质还是 i32, int只是个别名
let x: int = 10;
let y: long = 1000_0000;
println!("{} {}", x, y);
// 使用别名的好处是方便使用特别长的类型标注
fn f1(handler: Thunk) {}
fn f2() -> Thunk {}
}
never 类型
学习了解过 TypeScript 的应该都不陌生了, 它表示空类型, 它可以转为其他任何类型
fn main() {
let values: Vec<Option<i32>> = vec![
Some(1),
None,
Some(2),
];
for value in values {
// 将 num 手动标注 i32 类型的值, 可以通过编译, 那么也就说明:
// never 类型是空类型, 空类型可以转为其他任何类型
let num: i32 = match value {
Some(n) => n, // 这个匹配项返回 i32 类型的值
None => continue, // 这个匹配项返回 never 类型的值
};
println!("{}", num);
}
}
动态大小类型和 Sized 特性
Rust 需要知道应该为特定类型的值分配多少内存, 同时所有同一类型的值必须使用相同数量的内存
但是有些数据类型可能需要在程序运行时才能确定大小, 这就是 动态大小类型(dynamically sized types)
简称 DST
// 举个例子: 直接返回一个闭包, 此时会报错:
// 编译器报错: Fn(i32) doesn't have a size known at compile-time
// 在编译的时候, 无法知道这个闭包需要多大空间的内存,
// 必须要运行的时候才能知道, 这个就可以称之为 动态大小类型
fn get_a_clourse() -> Fn(i32) {
|x| println!("{}", x)
}
此时就有一个问题: 泛型为什么可以在编译的时候就知道该分配多少内存呢
为了处理 DST
, Rust 有一个特定的 trait
来确定一个类型的大小是否能够在编译时就推断出来
fn f1<T>() {}
// 实际上编译器会自动的将代码处理为
fn f1<T: Sized>{}
那么应该如何返回一个类似函数功能的东西呢? 既然闭包的大小是不确定的, 那就返回一个能够确定大小 的东西, 且这个东西必须符合三点要求:
- 能够在编译时确定需要分配多大内存空间
- 能够实现和函数类似的功能
答案很明显了, 就是指针
- 能够确定大小
- 能够实现类似函数的功能(通过函数指针来执行代码)
// 1) 返回 Box 智能指针而不是直接返回闭包, 因为 Box 指针是可以确定大小的
// 2) dyn Fn(i32) 表示这个类型是动态类型大小
// 3) 'static 表示这个Box指针里面的内容的生命周期是整个程序运行期间有效
// 否则 get_a_clourse 执行完后, 会被销毁掉, 这个标注省略掉
// 因为你都返回闭包了,肯定是希望在其他地方执行它, 而不是随着 get_a_clourse
// 完就销毁掉, 那就没有意义了, 我在这里手动标注是为了记录笔记
fn get_a_clourse() -> Box<(dyn Fn(i32) + 'static)> {
Box::new(|x| println!("run closure: {}", x))
}
fn main() {
let handler = get_a_clourse();
handler(11);
}
高级函数与闭包
函数指针
建议学习C语言基础, 先明白什么是指针, 什么叫 函数指针
注意: 函数指针不是和闭包trait(Fn/FnMut/FnOnce)是有区别的
函数指针能够做什么?
能够通过指针的方式直接执行代码, 这个和普通的执行函数的过程可能有点区别
具体在 Rust 代码中的体现就是: 能够指定一个参数的类型, 让这个类型既可以传入函数, 也可以传入闭包
// 这个方法的 handler 参数, 既可使用闭包也可使用函数
fn each(items: &Vec<i32>, handler: fn(i32)) {
for item in items {
handler(*item);
}
}
fn main() {
let nums = vec![1, 2, 3, 4, 5];
// 闭包
let c = |x| println!("closure:{}", x);
each(&nums, c);
// 函数
fn f(x: i32) {
println!("function:{}", x);
}
each(&nums, f);
}
宏
宏是什么?
是能够生成其他Rust代码的特殊代码, 所谓的元编程(metaprogramming)
我们在学习的过程中一直在用, 比如: println!()
vec![]
声明宏
英文: declarative macros
也叫 macro_rules 宏
由于我们可能更多是使用, 而不是编写他, 所以暂时先了解下即可
fn main() {
println!("hello");
}
过程宏
英文: procedural macros
, 他们很像另外一个维度的函数, 只不过它操作的是Rust源代码
比如学习单元测试时用的 #[cfg(test)]
和 #[test]
都属于过程宏
#[cfg(test)]
将一个模块标记为测试模块#[test]
将一个测试模块中的函数标记为测试用例
#[cfg(test)]
pub mod some_unit_test_module {
#[test]
fn test_case() {}
}
编写宏
主要是编写过程宏, 简化一些常用的操作
- 类似属性的宏: 比如
#[cfg(test)]
标注在其他代码上 - 类似函数的宏: 比如
println!("hello")
直接和调用函数一样使用
常用宏说明
- 标准库宏文档 注意:文档中宏的颜色都是浅绿色的
断言系列宏 assert
fn main() {
let is_ok = true;
assert!(is_ok); // 参数结果必须是 true, 否则断言失败
let x = 11;
let y = 11;
assert_eq!(x, y); // 两个参数必须相等, 否则断言失败
let x = 11;
let y = 22;
assert_ne!(x, y); // 两个参数必须不等, 否则断言失败
}
输出系列宏 print
fn main() {
let s = Some(2);
dbg!(s);
// 控制台输出
// [src/main.rs:3:5] s = Some(2,)
// 输出到标准输出
print!("stdout: {:?}", s); // 不换行
println!("stdout: {:?}", s); // 换行
// 输出到标准错误
print!("stderr: {:?}", s); // 不换行
println!("stderr: {:?}", s); // 换行
}
文件信息 file
fn main() {
let main_file = file!(); // 当前文件路径
let curr_line = line!(); // 当前文件所在行
println!("[{}:{}]", main_file, curr_line); // [src/main.rs:3]
}
格式化字符串 format
fn main() {
let x = 10;
let y = 20;
// 格式化字符串
let s = format!("{} + {} = {}", x, y, x+y);
println!("{}", s); // 10 + 20 + 30
// 对其参数进行字符串化
let s = stringify!(x + y);
println!("{}", s); // x + y
}
恐慌 panic
fn f1() {
panic!("手动让代码恐慌");
}
fn f2() {
todo!();
// not yet implemented
// 主动让代码恐慌, 表示还未实现, 仅做个标记用
}
fn main() {
f2();
f1();
}
创建 Vec
fn main() {
let nums = vec![1, 2, 3];
println!("{:?}", nums);
// 代码等同于
let mut nums = Vec::new();
nums.push(1);
nums.push(2);
nums.push(3);
println!("{:?}", nums);
}
写入缓冲区 write
use std::io::Write;
fn main() -> std::io::Result<()> {
let mut w = Vec::new();
writeln!(&mut w, "hello")?; // 会换行
write!(&mut w, "rust {}", "world")?; // 不换行
// hello\nrust world, 注意\n是换行
println!("{:?}", String::from_utf8(w).unwrap());
Ok(())
}