Rust 高级特性

打印 上一主题 下一主题

主题 852|帖子 852|积分 2556

本文有删减,原文链接高级特性

目次

不安全 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 的保证并不实用。
通过引用创建裸指针:
  1. let mut num = 5;
  2. let r1 = &num as *const i32;
  3. let r2 = &mut num as *mut i32;
复制代码
注:可以在安全代码中 创建 裸指针,只是不能在不安全块之外 解引用 裸指针。
创建指向任意内存地址的裸指针:
  1. let address = 0x012345usize;
  2. let r = address as *const i32;
复制代码
在 unsafe 块中解引用裸指针:
  1. let mut num = 5;
  2. let r1 = &num as *const i32;
  3. let r2 = &mut num as *mut i32;unsafe {    println!("r1 is: {}", *r1);    println!("r2 is: {}", *r2);}
复制代码
为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口,另一个场景是构建借用检查器无法理解的安全抽象
调用不安全函数或方法

不安全函数和方法与通例函数方法十分雷同,除了其开头有一个额外的 unsafe。
一个没有做任何操作的不安全函数 dangerous 的例子:
  1. unsafe fn dangerous() {}
  2. unsafe {
  3.     dangerous();
  4. }
复制代码
必须在一个单独的 unsafe 块中调用 dangerous 函数,否则会得到一个错误。不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块。
创建不安全代码的安全抽象

函数包含不安全代码并不意味着整个函数都必要标记为不安全的,如标准库中的函数 split_at_mut:它获取一个 slice 并从给定的索引参数开始将其分为两个 slice,用法如下:
  1. let mut v = vec![1, 2, 3, 4, 5, 6];
  2. let r = &mut v[..];
  3. let (a, b) = r.split_at_mut(3);
  4. assert_eq!(a, &mut [1, 2, 3]);
  5. assert_eq!(b, &mut [4, 5, 6]);
复制代码
尝试只使用安全 Rust 来实现 split_at_mut:
  1. //这段代码无法通过编译!
  2. fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  3.     let len = values.len();
  4.     assert!(mid <= len);
  5.     (&mut values[..mid], &mut values[mid..])
  6. }
复制代码
"C" 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如安在汇编语言层面调用此函数
也可以使用 extern 来创建一个答应其他语言调用 Rust 函数的接口,在 fn 关键字之前增加 extern 关键字并为相关函数指定所用到的 ABI,还需增加 #[no_mangle] 注解来告诉 Rust 编译器不要 mangle 此函数的名称。
一旦其编译为动态库并从 C 语言中链接,call_from_c 函数就可以或许在 C 代码中访问:
  1. use std::slice;
  2. fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  3.     let len = values.len();
  4.     let ptr = values.as_mut_ptr();
  5.     assert!(mid <= len);
  6.     unsafe {
  7.         (
  8.             slice::from_raw_parts_mut(ptr, mid),
  9.             slice::from_raw_parts_mut(ptr.add(mid), len - mid),
  10.         )
  11.     }
  12. }
复制代码
访问或修改可变静态变量

全局变量在 Rust 中被称为 静态(static)变量,一个拥有字符串 slice 值的静态变量的声明和应用:
  1. use std::slice;
  2. let address = 0x01234usize;
  3. let r = address as *mut i32;
  4. let values: &[i32] = unsafe { slice::from_raw_parts_mut(r,10000) };
复制代码
通常静态变量的名称采用 SCREAMING_SNAKE_CASE 写法,静态变量只能储存拥有 'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注,访问不可变静态变量是安全的
常量与不可变静态变量的区别:

  • 静态变量中的值有一个固定的内存地址,使用这个值总是会访问相同的地址,常量则答应在任何被用到的时候复制其数据。
  • 静态变量可以是可变的,访问和修改可变静态变量都是 不安全 的。
读取或修改一个可变静态变量是不安全的:
  1. extern "C" {
  2.     //希望能够调用的另一个语言中的外部函数的签名和名称
  3.     fn abs(input: i32) -> i32;
  4. }
  5. fn main() {
  6.     unsafe {
  7.         println!("Absolute value of -3 according to C: {}", abs(-3));
  8.     }
  9. }
复制代码
使用 mut 关键来指定可变性,任何读写 COUNTER 的代码都必须位于 unsafe 块中。
实现不安全 trait

当 trait 中至少有一个方法中包含编译器无法验证的稳固式(invariant)时 trait 是不安全的:
  1. //extern 的使用无需 unsafe
  2. #[no_mangle]
  3. pub extern "C" fn call_from_c() {
  4.     println!("Just called a Rust function from C!");
  5. }
复制代码
访问联合体中的字段

union 和 struct 雷同,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,由于 Rust 无法保证当前存储在联合体实例中数据的类型。
何时使用不安全代码

当有来由使用 unsafe 代码时,是可以这么做的,通过使用显式的 unsafe 标注可以更容易地在错误发生时追踪问题的源头。
高级 trait

关联类型在 trait 定义中指定占位符类型

关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型,trait 的实现者会针对特定的实现在这个占位符类型指定相应的具体类型。
Iterator trait 的定义中带有关联类型 Item,它用来替代遍历的值的类型:
  1. static HELLO_WORLD: &str = "Hello, world!";
  2. fn main() {
  3.     println!("name is: {}", HELLO_WORLD);
  4. }
复制代码
关联类型看起来像一个雷同泛型的概念,由于它答应定义一个函数而不指定其可以处理的类型。
在一个 Counter 结构体上实现 Iterator trait ,指定了 Item 的类型为 u32:
  1. static mut COUNTER: u32 = 0;
  2. fn add_to_count(inc: u32) {
  3.     unsafe {
  4.         COUNTER += inc;
  5.     }
  6. }
  7. fn main() {
  8.     add_to_count(3);
  9.     unsafe {
  10.         println!("COUNTER: {}", COUNTER);
  11.     }
  12. }
复制代码
一个使用泛型的 Iterator trait 假想定义:
  1. //在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe
  2. unsafe trait Foo {
  3.     // methods go here
  4. }
  5. // trait 的实现也必须标记为 unsafe
  6. unsafe impl Foo for i32 {
  7.     // method implementations go here
  8. }
  9. fn main() {}
复制代码
假如使用泛型就可以有多个 Counter 的 Iterator 的实现,当使用 Counter 的 next 方法时,必须提供类型注解来表明希望使用 Iterator 的哪一个实现。
通过关联类型,则无需标注类型,由于不能多次实现这个 trait。当调用 Counter 的 next 时不必每次指定 u32 值的迭代器。
关联类型也会成为 trait 契约的一部分:trait 的实现必须提供一个类型来替代关联类型占位符
默认泛型类型参数和运算符重载

当使用泛型类型参数时,可以为泛型指定一个默认的具体类型,为泛型类型指定默认类型的语法是在声明泛型类型时使用 。
Rust 并不答应创建自定义运算符或重载任意运算符,不过 std:ps 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载
实现 Add trait 重载 Point 实例的 + 运算符:
  1. pub trait Iterator {
  2.     //占位符类型,trait 的实现者会指定 Item 的具体类型
  3.     type Item;
  4.     fn next(&mut self) -> Option<Self::Item>;
  5. }
复制代码
默认泛型类型位于 Add trait 中,一个带有一个方法和一个关联类型的 trait:
  1. impl Iterator for Counter {
  2.     type Item = u32;
  3.     fn next(&mut self) -> Option<Self::Item> {
  4.         // --snip--
复制代码
尖括号中的 Rhs=Self 语法叫做 默认类型参数(default type parameters),Rhs 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add 方法中的 rhs 参数。
在 Millimeters 上实现 Add,以便可以或许将 Millimeters 与 Meters 相加:
  1. pub trait Iterator<T> {
  2.     fn next(&mut self) -> Option<T>;
  3. }
复制代码
默认参数类型主要用于如下两个方面:

  • 扩展类型而不粉碎现有代码。
  • 在大部分用户都不必要的特定情况进行自定义。
第一个目的是相似的,但过程是反过来的:假如必要为现有 trait 增加类型参数,为其提供一个默认类型将答应在不粉碎现有实当代码的基础上扩展 trait 的功能
完全限定语法与消歧义:调用相同名称的方法

Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。
两个 trait 定义为拥有 fly 方法,并在直接定义有 fly 方法的 Human 类型上实现这两个 trait:
  1. use std::ops::Add;
  2. #[derive(Debug, Copy, Clone, PartialEq)]
  3. struct Point {
  4.     x: i32,
  5.     y: i32,
  6. }
  7. impl Add for Point {
  8.     type Output = Point;
  9.     fn add(self, other: Point) -> Point {
  10.         Point {
  11.             x: self.x + other.x,
  12.             y: self.y + other.y,
  13.         }
  14.     }
  15. }
  16. fn main() {
  17.     assert_eq!(
  18.         Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
  19.         Point { x: 3, y: 3 }
  20.     );
  21. }
复制代码
当调用 Human 实例的 fly 时,编译器默认调用直接实现在类型上的方法:
  1. trait Add<Rhs=Self> {
  2.     type Output;
  3.     fn add(self, rhs: Rhs) -> Self::Output;
  4. }
复制代码
指定希望调用哪一个 trait 的 fly 方法:
  1. use std::ops::Add;
  2. struct Millimeters(u32);
  3. struct Meters(u32);
  4. //指定 impl Add<Meters> 来设定 Rhs 类型参数的值而不是使用默认的 Self
  5. impl Add<Meters> for Millimeters {
  6.     type Output = Millimeters;
  7.     fn add(self, other: Meters) -> Millimeters {
  8.         Millimeters(self.0 + (other.0 * 1000))
  9.     }
  10. }
复制代码
运行这段代码会打印出:
  1. trait Pilot {
  2.     fn fly(&self);
  3. }
  4. trait Wizard {
  5.     fn fly(&self);
  6. }
  7. struct Human;
  8. impl Pilot for Human {
  9.     fn fly(&self) {
  10.         println!("This is your captain speaking.");
  11.     }
  12. }
  13. impl Wizard for Human {
  14.     fn fly(&self) {
  15.         println!("Up!");
  16.     }
  17. }
  18. impl Human {
  19.     fn fly(&self) {
  20.         println!("*waving arms furiously*");
  21.     }
  22. }
复制代码
不是方法的关联函数没有 self 参数,当存在多个类型大概 trait 定义了相同函数名的非方法函数时,Rust 无法计算出期望的类型,除非使用 完全限定语法(fully qualified syntax)。
一个带有关联函数的 trait 和一个带有同名关联函数并实现了此 trait 的类型:
  1. fn main() {
  2.     let person = Human;
  3.     person.fly();
  4. }
  5. //会打印出 *waving arms furiously*
复制代码
尝试调用 Animal trait 的 baby_name 函数,不过 Rust 并不知道该使用哪一个实现:
  1. fn main() {
  2.     let person = Human;
  3.     Pilot::fly(&person);
  4.     Wizard::fly(&person);
  5.     person.fly();
  6. }
复制代码
由于 Animal::baby_name 没有 self 参数,同时这可能会有其它类型实现了 Animal trait,Rust 无法计算出所需的是哪一个 Animal::baby_name 实现。
使用完全限定语法来指定希望调用的是 Dog 上 Animal trait 实现中的 baby_name 函数:
  1. This is your captain speaking.
  2. Up!
  3. *waving arms furiously*
复制代码
在尖括号中向 Rust 提供了类型注解,并通过在此函数调用中将 Dog 类型看成 Animal 对待,来指定希望调用的是 Dog 上 Animal trait 实现中的 baby_name 函数。
通常,完全限定语法定义为:
  1. trait Animal {
  2.     fn baby_name() -> String;
  3. }
  4. struct Dog;
  5. impl Dog {
  6.     fn baby_name() -> String {
  7.         String::from("Spot")
  8.     }
  9. }
  10. impl Animal for Dog {
  11.     fn baby_name() -> String {
  12.         String::from("puppy")
  13.     }
  14. }
  15. fn main() {
  16.     println!("A baby dog is called a {}", Dog::baby_name());
  17. }
  18. //会打印出: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 会表现如下:
  1. //会得到一个编译错误
  2. fn main() {
  3.     println!("A baby dog is called a {}", Animal::baby_name());
  4. }
复制代码
实现 OutlinePrint trait,它要求来自 Display 的功能:
  1. fn main() {
  2.     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
  3. }
  4. //会打印出:A baby dog is called a puppy
复制代码
尝试在一个没有实现 Display 的类型上实现 OutlinePrint :
  1. <Type as Trait>::function(receiver_if_method, next_arg, ...);
复制代码
一旦在 Point 上实现 Display 并满足 OutlinePrint 要求的限定,则能成功编译:
  1. **********
  2. *        *
  3. * (1, 3) *
  4. *        *
  5. **********
复制代码
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:
  1. use std::fmt;
  2. //指定了 OutlinePrint 需要 Display trait
  3. //否则会报错:在当前作用域中没有找到用于 &Self 类型的方法 to_string
  4. trait OutlinePrint: fmt::Display {
  5.     fn outline_print(&self) {
  6.         let output = self.to_string();
  7.         let len = output.len();
  8.         println!("{}", "*".repeat(len + 4));
  9.         println!("*{}*", " ".repeat(len + 2));
  10.         println!("* {} *", output);
  11.         println!("*{}*", " ".repeat(len + 2));
  12.         println!("{}", "*".repeat(len + 4));
  13.     }
  14. }
复制代码
此方法的缺点是必须直接在 Wrapper 上实现 Vec 的全部方法,这样才可以代理到self.0 上。假如不希望封装类型拥有全部内部类型的方法,只必要自行实现所需的方法。
高级类型

为了类型安全和抽象而使用 newtype 模式

newtype 模式也可以用于一些其他还未讨论的功能:

  • 静态简直保某值不被混淆,和用来表现一个值的单位,如 Millimeters 和 Meters 结构体。
  • 用于抽象掉一些类型的实现细节,如暴露出与直接使用其内部私有类型时所差别的公有 API
  • 隐藏其内部的泛型类型,如封装了 HashMap 的 People 类型。
类型别名用来创建类型同义词

Rust 提供了声明 类型别名(type alias)的能力,使用 type 关键字来给予现有类型另一个名字:
  1. //这段代码无法通过编译!
  2. struct Point {
  3.     x: i32,
  4.     y: i32,
  5. }
  6. impl OutlinePrint for Point {}
复制代码
类型别名的主要用途是减少重复,例如可能会有这样很长的类型:
  1. use std::fmt;
  2. impl fmt::Display for Point {
  3.     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  4.         write!(f, "({}, {})", self.x, self.y)
  5.     }
  6. }
复制代码
引入类型别名 Thunk 来减少重复:
[code]type Thunk = Box
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

涛声依旧在

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表