本文有删减,原文链接高级特性。
目次
不安全 Rust
Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rust(unsafe Rust)。
不安全 Rust 之所以存在,是由于静态分析本质上是保守的。可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。”
另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。假如 Rust 不答应进行不安全操作,那么有些使命则根本完成不了。
不安全的超能力
可以通过 unsafe 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力(unsafe superpowers)” :
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问 union 的字段
unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:假如在不安全代码中使用引用,它仍会被检查。unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能。
unsafe 不意味着块中的代码就一定是危险的大概一定导致内存安全问题:其意图在于作为程序员会确保 unsafe 块中的代码以有效的方式访问内存。
解引用裸指针
不安全 Rust 有两个被称为 裸指针(raw pointers)的雷同于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 *const T 和 *mut T。这里的星号不是解引用运算符;它是类型名称的一部分。
裸指针与引用和智能指针的区别在于:
- 答应忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 答应为空
- 不能实现任何主动清理功能
通过去掉 Rust 强加的保证,可以放弃安全保证以调换性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不实用。
通过引用创建裸指针:- 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;
复制代码 在 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);}
复制代码 为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口,另一个场景是构建借用检查器无法理解的安全抽象。
调用不安全函数或方法
不安全函数和方法与通例函数方法十分雷同,除了其开头有一个额外的 unsafe。
一个没有做任何操作的不安全函数 dangerous 的例子:- unsafe fn dangerous() {}
- unsafe {
- dangerous();
- }
复制代码 必须在一个单独的 unsafe 块中调用 dangerous 函数,否则会得到一个错误。不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块。
创建不安全代码的安全抽象
函数包含不安全代码并不意味着整个函数都必要标记为不安全的,如标准库中的函数 split_at_mut:它获取一个 slice 并从给定的索引参数开始将其分为两个 slice,用法如下:- let mut v = vec![1, 2, 3, 4, 5, 6];
- let r = &mut v[..];
- let (a, b) = r.split_at_mut(3);
- assert_eq!(a, &mut [1, 2, 3]);
- assert_eq!(b, &mut [4, 5, 6]);
复制代码 尝试只使用安全 Rust 来实现 split_at_mut:- //这段代码无法通过编译!
- fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
- let len = values.len();
- assert!(mid <= len);
- (&mut values[..mid], &mut values[mid..])
- }
复制代码 "C" 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如安在汇编语言层面调用此函数。
也可以使用 extern 来创建一个答应其他语言调用 Rust 函数的接口,在 fn 关键字之前增加 extern 关键字并为相关函数指定所用到的 ABI,还需增加 #[no_mangle] 注解来告诉 Rust 编译器不要 mangle 此函数的名称。
一旦其编译为动态库并从 C 语言中链接,call_from_c 函数就可以或许在 C 代码中访问:
- use std::slice;
- fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
- let len = values.len();
- let ptr = values.as_mut_ptr();
- assert!(mid <= len);
- unsafe {
- (
- slice::from_raw_parts_mut(ptr, mid),
- slice::from_raw_parts_mut(ptr.add(mid), len - mid),
- )
- }
- }
复制代码 访问或修改可变静态变量
全局变量在 Rust 中被称为 静态(static)变量,一个拥有字符串 slice 值的静态变量的声明和应用:- use std::slice;
- let address = 0x01234usize;
- let r = address as *mut i32;
- let values: &[i32] = unsafe { slice::from_raw_parts_mut(r,10000) };
复制代码 通常静态变量的名称采用 SCREAMING_SNAKE_CASE 写法,静态变量只能储存拥有 'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注,访问不可变静态变量是安全的。
常量与不可变静态变量的区别:
- 静态变量中的值有一个固定的内存地址,使用这个值总是会访问相同的地址,常量则答应在任何被用到的时候复制其数据。
- 静态变量可以是可变的,访问和修改可变静态变量都是 不安全 的。
读取或修改一个可变静态变量是不安全的:- extern "C" {
- //希望能够调用的另一个语言中的外部函数的签名和名称
- fn abs(input: i32) -> i32;
- }
- fn main() {
- unsafe {
- println!("Absolute value of -3 according to C: {}", abs(-3));
- }
- }
复制代码 使用 mut 关键来指定可变性,任何读写 COUNTER 的代码都必须位于 unsafe 块中。
实现不安全 trait
当 trait 中至少有一个方法中包含编译器无法验证的稳固式(invariant)时 trait 是不安全的:- //extern 的使用无需 unsafe
- #[no_mangle]
- pub extern "C" fn call_from_c() {
- println!("Just called a Rust function from C!");
- }
复制代码 访问联合体中的字段
union 和 struct 雷同,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,由于 Rust 无法保证当前存储在联合体实例中数据的类型。
何时使用不安全代码
当有来由使用 unsafe 代码时,是可以这么做的,通过使用显式的 unsafe 标注可以更容易地在错误发生时追踪问题的源头。
高级 trait
关联类型在 trait 定义中指定占位符类型
关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型,trait 的实现者会针对特定的实现在这个占位符类型指定相应的具体类型。
Iterator trait 的定义中带有关联类型 Item,它用来替代遍历的值的类型:- static HELLO_WORLD: &str = "Hello, world!";
- fn main() {
- println!("name is: {}", HELLO_WORLD);
- }
复制代码 关联类型看起来像一个雷同泛型的概念,由于它答应定义一个函数而不指定其可以处理的类型。
在一个 Counter 结构体上实现 Iterator trait ,指定了 Item 的类型为 u32:- static mut COUNTER: u32 = 0;
- fn add_to_count(inc: u32) {
- unsafe {
- COUNTER += inc;
- }
- }
- fn main() {
- add_to_count(3);
- unsafe {
- println!("COUNTER: {}", COUNTER);
- }
- }
复制代码 一个使用泛型的 Iterator trait 假想定义:- //在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe
- unsafe trait Foo {
- // methods go here
- }
- // trait 的实现也必须标记为 unsafe
- unsafe impl Foo for i32 {
- // method implementations go here
- }
- fn main() {}
复制代码 假如使用泛型就可以有多个 Counter 的 Iterator 的实现,当使用 Counter 的 next 方法时,必须提供类型注解来表明希望使用 Iterator 的哪一个实现。
通过关联类型,则无需标注类型,由于不能多次实现这个 trait。当调用 Counter 的 next 时不必每次指定 u32 值的迭代器。
关联类型也会成为 trait 契约的一部分:trait 的实现必须提供一个类型来替代关联类型占位符。
默认泛型类型参数和运算符重载
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型,为泛型类型指定默认类型的语法是在声明泛型类型时使用 。
Rust 并不答应创建自定义运算符或重载任意运算符,不过 std: ps 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。
实现 Add trait 重载 Point 实例的 + 运算符:- pub trait Iterator {
- //占位符类型,trait 的实现者会指定 Item 的具体类型
- type Item;
- fn next(&mut self) -> Option<Self::Item>;
- }
复制代码 默认泛型类型位于 Add trait 中,一个带有一个方法和一个关联类型的 trait:- impl Iterator for Counter {
- type Item = u32;
- fn next(&mut self) -> Option<Self::Item> {
- // --snip--
复制代码 尖括号中的 Rhs=Self 语法叫做 默认类型参数(default type parameters),Rhs 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add 方法中的 rhs 参数。
在 Millimeters 上实现 Add,以便可以或许将 Millimeters 与 Meters 相加:- pub trait Iterator<T> {
- fn next(&mut self) -> Option<T>;
- }
复制代码 默认参数类型主要用于如下两个方面:
- 扩展类型而不粉碎现有代码。
- 在大部分用户都不必要的特定情况进行自定义。
第一个目的是相似的,但过程是反过来的:假如必要为现有 trait 增加类型参数,为其提供一个默认类型将答应在不粉碎现有实当代码的基础上扩展 trait 的功能。
完全限定语法与消歧义:调用相同名称的方法
Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。
两个 trait 定义为拥有 fly 方法,并在直接定义有 fly 方法的 Human 类型上实现这两个 trait:- use std::ops::Add;
- #[derive(Debug, Copy, Clone, 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 }
- );
- }
复制代码 当调用 Human 实例的 fly 时,编译器默认调用直接实现在类型上的方法:- trait Add<Rhs=Self> {
- type Output;
- fn add(self, rhs: Rhs) -> Self::Output;
- }
复制代码 指定希望调用哪一个 trait 的 fly 方法:- use std::ops::Add;
- struct Millimeters(u32);
- struct Meters(u32);
- //指定 impl Add<Meters> 来设定 Rhs 类型参数的值而不是使用默认的 Self
- impl Add<Meters> for Millimeters {
- type Output = Millimeters;
- fn add(self, other: Meters) -> Millimeters {
- Millimeters(self.0 + (other.0 * 1000))
- }
- }
复制代码 运行这段代码会打印出:- 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*");
- }
- }
复制代码 不是方法的关联函数没有 self 参数,当存在多个类型大概 trait 定义了相同函数名的非方法函数时,Rust 无法计算出期望的类型,除非使用 完全限定语法(fully qualified syntax)。
一个带有关联函数的 trait 和一个带有同名关联函数并实现了此 trait 的类型:- fn main() {
- let person = Human;
- person.fly();
- }
- //会打印出 *waving arms furiously*
复制代码 尝试调用 Animal trait 的 baby_name 函数,不过 Rust 并不知道该使用哪一个实现:- fn main() {
- let person = Human;
- Pilot::fly(&person);
- Wizard::fly(&person);
- person.fly();
- }
复制代码 由于 Animal::baby_name 没有 self 参数,同时这可能会有其它类型实现了 Animal trait,Rust 无法计算出所需的是哪一个 Animal::baby_name 实现。
使用完全限定语法来指定希望调用的是 Dog 上 Animal trait 实现中的 baby_name 函数:- This is your captain speaking.
- Up!
- *waving arms furiously*
复制代码 在尖括号中向 Rust 提供了类型注解,并通过在此函数调用中将 Dog 类型看成 Animal 对待,来指定希望调用的是 Dog 上 Animal trait 实现中的 baby_name 函数。
通常,完全限定语法定义为:- 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
复制代码
- 对于不是方法的关联函数,其没有一个 receiver,故只会有其他参数的列表。
- 可以选择在任何函数或方法调用处使用完全限定语法。
- 答应省略任何 Rust 可以或许从程序中的其他信息中计算出的部分。
父 trait 用于在另一个 trait 中使用某 trait 的功能
对于一个实现了第一个 trait 的类型,希望要求这个类型也实现了第二个 trait。如此就可使 trait 定义使用第二个 trait 的关联项,这个所需的 trait 是我们实现的 trait 的 父(超)trait(supertrait)。
创建一个带有 outline_print 方法的 trait OutlinePrint,它会将给定的值格式化为带有星号框。给定一个实现了标准库 Display trait 的并返回 (x, y) 的 Point,当调用以 1 作为 x 和 3 作为 y 的 Point 实例的 outline_print 会表现如下:- //会得到一个编译错误
- fn main() {
- println!("A baby dog is called a {}", Animal::baby_name());
- }
复制代码 实现 OutlinePrint trait,它要求来自 Display 的功能:- fn main() {
- println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
- }
- //会打印出:A baby dog is called a puppy
复制代码 尝试在一个没有实现 Display 的类型上实现 OutlinePrint :- <Type as Trait>::function(receiver_if_method, next_arg, ...);
复制代码 一旦在 Point 上实现 Display 并满足 OutlinePrint 要求的限定,则能成功编译:- **********
- * *
- * (1, 3) *
- * *
- **********
复制代码 newtype 模式用以在外部类型上实现外部 trait
孤儿规则(orphan rule):只要 trait 或类型对于当前 crate 是当地的话就可以在此类型上实现该 trait,一个绕开这个限定的方法是使用 newtype 模式(newtype pattern)。
假如想要在 Vec 上实现 Display,而孤儿规则阻止我们直接这么做,由于 Display trait 和 Vec 都定义于我们的 crate 之外。可以创建一个包含 Vec 实例的 Wrapper 结构体,接着可以如列表 19-23 那样在 Wrapper 上实现 Display 并使用 Vec 的值:
创建 Wrapper 类型封装 Vec 以便可以或许实现 Display:- use std::fmt;
- //指定了 OutlinePrint 需要 Display trait
- //否则会报错:在当前作用域中没有找到用于 &Self 类型的方法 to_string
- 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));
- }
- }
复制代码 此方法的缺点是必须直接在 Wrapper 上实现 Vec 的全部方法,这样才可以代理到self.0 上。假如不希望封装类型拥有全部内部类型的方法,只必要自行实现所需的方法。
高级类型
为了类型安全和抽象而使用 newtype 模式
newtype 模式也可以用于一些其他还未讨论的功能:
- 静态简直保某值不被混淆,和用来表现一个值的单位,如 Millimeters 和 Meters 结构体。
- 用于抽象掉一些类型的实现细节,如暴露出与直接使用其内部私有类型时所差别的公有 API
- 隐藏其内部的泛型类型,如封装了 HashMap 的 People 类型。
类型别名用来创建类型同义词
Rust 提供了声明 类型别名(type alias)的能力,使用 type 关键字来给予现有类型另一个名字:- //这段代码无法通过编译!
- struct Point {
- x: i32,
- y: i32,
- }
- impl OutlinePrint for Point {}
复制代码 类型别名的主要用途是减少重复,例如可能会有这样很长的类型:- use std::fmt;
- impl fmt::Display for Point {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "({}, {})", self.x, self.y)
- }
- }
复制代码 引入类型别名 Thunk 来减少重复:
[code]type Thunk = Box |