- 没有内存安全保证:Unsafe Rust。
- 和普通Rust一样,提供了额外能力。
- 存在原因:
- 静态分析时保守的:unsafe rust 我知道自己在做什么 并承担风险。
- 计算机本身就是不安全的,需要进行底层编程。
Unsafe超能力:
用unsafe关键字创建一个块儿:
- 解引用原始指针
- 调用unsafe 函数或者方法
- 访问或者修改常量变量
- 实现unsafe trait
注意:
- 没有关闭rust安全特性
- 错误必须留在unsafe里
- 尽可能隔离unsafe代码,对外提供安全api
解引用原始指针
- 原始指针:
- 可变的:*mut T
- 不可变的:*const T
- 与引用不同:
- 允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则。
- 无法保证指向合理内存
- 允许为null
- 不实现任何自动清理
-
放弃安全:换取性能或者一些其他语言和接口的硬件能力。
- 为什么要用?
- 与C语言进行接口
- 构造借用检查器无法理解的安全抽象
调用unsafe函数或者方法
- 定义前加上unsafe,调用前需要手动满足一些条件
- unsafe块里调用
创建unsafe代码的安全抽象
函数包含unsafe并不一定将整个函数标记为unsafe。
将unsafe代码块包裹在函数中是常见抽象。
使用extern函数调用代码
- extern默认就是不安全的
- 简化创建和使用外部函数接口的过程。
- 允许其他编程语言调用本语言函数。
- 应用二进制接口,汇编层面接口。
-
C ABI最常见ABI遵循C语言:
从其他语言调用Rust接口
- 可以使用extern创建接口,其他语言可以调用Rust函数
- 在fn前添加extern并制定ABI
-
需要添加#[no_mangle]注解,避免Rust编译时改变其名称
访问或者修改一个可变静态变量
- 支持全局变量:在rust里叫做静态变量
静态变量:
- 静态变量和常量类似
- SCREAMING_SNAKE_CASE
- 必须标注类型
- 不需要标明声明周期
- 访问不可静态变量是安全的
常量和不可变静态变量的区别
-
静态变量:
- 有固定的内存地址,使用他的值总会放问同样的数据。
-
可变的:访问和修改是unsafe的:
常量:允许使用他们的时候对数据进行复制。
实现不安全的trait
- 当某个trait中存在一个方法拥有编译器无法校验的不安全因素的时候,就称这个trait是不安全的。
-
在trait前加unsafe:
何时使用unsafe代码
- 编译器无法保证内存安全
- 有充分理由
- 显示标记unsafe,快速定位问题
高级Trait
在Trait定义中使用关联类型来指定占位类型
- 关联类型:Trait中类型的占位符,用在方法签名中
-
可以定义出包含某些类型的Trait,而在实现前无需知道这些类型是什么:
-
和泛型的区别
泛型:
- 实现Trait时需要标注类型
-
可以实现多次Trait(不同泛型参数)
关联类型:
- 无需标注类型
-
无法为单个类型多次实现某个Trait
默认泛型参数和运算符的重载
- 指定默认泛型具体类型
- <T = u32>
- 常用于运算符重载
- Rust不允许创建自己的运算符和重载任意运算符
-
但是可以重载std:ops列举出来的一部分运算符
原来:
运用场景
- 扩展一个类型而不破坏现有代码
- 允许在大部分用户都不需要的特定场景下进行自定义
完全限定语法(fully qualified syntax)
指定对应的triat,然后传入引用。
但是上面的例子得以实现时因为fly的参数是self,但是如果没有参数,怎么能调用其他trait的方法?
这个时候就用到了完全限定语法: <Type as Trait>::function(xx, xxx);
使用supertrait来调用其他trait
- 被依赖的trait也被实现
-
被依赖的trait就是当前trait的supertriat
使用newtype模式在外部类型上实现外部trait
- 孤儿规则:定义在本地包,才能实现trait
-
利用newtype绕过该规则:tuple struct
trait和vec都在外部,但是我们要为vec实现trait,实际上就是创建了一个新的struct来包了一层对应的类型。
高级类型
使用newtype模式实现类型安全和抽象
newtype可以:
- 用来静态保证各种值不被混淆
- 为类型的某些细节提供抽象能力
- 通过轻量级的封装来隐藏内部实现细节
使用类型别名来创建同义词
- rust提供了类型别名功能:type关键字,并非独立类型。
-
主要用来减少代码字符重复。
Never类型
- 有一个名为!的特殊类型:
- 它没有任何值,行话成为空类型
-
不返回值的函数充当返回类型
不写返回值不代表没有返回值,所以报错。
Never类型可以强制转化为其他类型:
panic!也是never。
无限循环也是never。
动态大小和Sized Trait
- Rust需要在编译时,确定为一个特定类型的值分配多少空间。
- 动态大小类型(DST):编写代码是使用只有在运行才能确定大小的值。
-
str是动态大小类型(不是&str):运行时才能确定长度:
Rust使用动态大小类型的通用方式
- 附带一些元数据来存储动态信息的大小。
- 使用动态大小类型时总会把它的值放在某种指针后边
另一种动态大小的类型:Trait
- 每个trait都是一个动态大小的类型,通过名称进行引用。
-
为了将trait用作trait对象,必须把它放置在某种指针之后。
Sized Trait
来确定一个类型的大小是否在编译时已知。
- 编译时计算出大小的类型会自动实现该trait
-
Rust还会为每一个泛型函数隐式的添加sized约束
泛型函数只能被用于编译时已经知道大小的类型,但是特殊语法可以解除这一限制:
?Sized Trait约束
高级函数和闭包
函数指针
- 可以将函数传递给其他函数
- 函数在传递的过程中被强制转化为fn类型
-
fn类型就是函数指针(function pointer)
函数指针和闭包的不同
- fn是一个类型,不是一个trait:可以指定fn为参数,而不用以Fn Trait为约束的泛型参数
- 函数指针实现了全部3种闭包的trait(Fn FnMut FnOnce):总是可以把一个函数指针作为参数传递给一个接收闭包的函数
- 所以,倾向于搭配闭包Trait的泛型来编写函数:可以同时接收闭包和普通函数
-
某些场景只想接收fn 而不想接收闭包:和其他语言交互
返回闭包
-
闭包使用trait表示,无法直接返回一个闭包,可以将实现了该trait的具体类型作为返回值:
宏
- 相关特性的集合称谓
- 使用macro_rules!来构建声明宏
- 3种过程宏
- 定义#[derive]宏,用于struct或者enum,可为其指定随derive属性添加的代码
- 类似属性的宏,在任何条目上添加自定义属性
- 类似函数的宏,看起来像函数调用,对其指定为参数的token进行操作
函数和宏的差别
- 本质上,宏是用来编写可以生成其他代码的代码(元编程,metaprogramming)
- 函数在定义签名时,必须声明参数的个数和类型,宏可处理可变参数
- 编译器会在解释代码前,把宏展开
- 宏的定义比函数复杂得多,难以阅读、理解、维护
- 在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域
- 函数可以在任何位置定义并在任何位置调用
macro_rules! 声明宏
- Rust最常见的宏形式:声明宏
- 类似match的模式匹配
- 需要使用macro_rules!
例子:
基于属性来生成代码的过程宏
- 像函数:
- 接受一段rust代码
- 生成另外一些rust代码为结果
- 三种过程宏:
- 自定义派生
- 属性宏
- 函数宏
- 创建过程宏时:
-
宏定义必须单独放在他们自己的包中,并使用特殊的包类型
-
自定义derive宏
- 需求:
- 创建一个hello_macro包,定义一个拥有关联函数hello_macro的HelloMacro trait
- 我们提供一个能够自动实现trait的过程宏
- 在他们上标明#[derive(HelloMacro)],进而得到hello_macro的默认实现。
一个trait往往需要默认实现,如果我有9个struct都需要使用一个trait,那么我的默认实现可能就要写9次,而往往默认实现都非常的相似,我们不想写这么多次,那么怎么办?
首先我们先创建了一个工作空间,底下有3个项目:
- hello_macro
- hello_macro_derive (过程宏)
-
hell_macro的使用方:Pancakes
hello_macro:
Pancakes希望最终能写成:
如果没有hello_macro_derive,它只能写成:
就得写默认实现。
那么hello_macro_derive怎么实现:
他的依赖:
- syn:rust代码变成ast
-
quote:将ast还原为字符串
固定写法:
主要是impl_hello_macro:
stringify!:将表达式变成字面字符串:比如stringify!(1 + 2) 输出 "1+2";
类似属性的宏
- 属性宏和自定义derive宏类似
- 允许创建新的属性
- 但不是为derive属性生成代码
-
属性宏更灵活:运用在任何条目
类似函数的宏
- 类似函数调用的宏,但是比普通函数更加灵活
- 可以接收TokenStream作为参数
-
可以操作TokenStream