《Rust权威指南》 第19章 高级特性

不安全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关键字的主要用途包括:

  1. 在实现类型的方法中,Self用来表示实例的类型。例如:
struct MyStruct;

impl MyStruct {
    fn new() -> Self {
        MyStruct
    }
}

在上述代码中,new函数返回一个MyStruct类型的实例,但是我们使用Self来指代这个类型,而不是直接用MyStruct。这样做的好处是,如果我们改变了结构体的名称,我们并不需要在每一个方法中都去修改它,只需在结构体定义的地方修改即可。

  1. 在特性(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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Rust权威指南是一本深入介绍Rust编程语言权威性书籍。这本书主要介绍Rust语言的基本语法和特性,包括Rust的所有关键字、数据类型、函数、模块、宏、”生命周期”、所有权等重要概念的详细解释和使用方法。 这本书由Rust社区知名的开发者Steve Klabnik和Carol Nichols撰写,是Rust开发者及其他想要学习Rust编程语言的程序员的必读之作。书中详细介绍了Rust的语法规则和特性,如何编写高质量、高性能的Rust代码,以及如何使用Rust来进行并发编程和系统级编程。 通过阅读本书,读者能够了解Rust的核心概念,如所有权、生命周期和借用,以及如何使用Rust的工具链进行开发。在本书的后半部分,作者通过实践案例和示例向读者展示了如何使用Rust来编写高效、可靠的程序。 总的来说,Rust权威指南是一本非常实用且详尽的Rust编程语言指南,对于想要学习Rust编程语言的读者非常有益。不仅提供了基础知识方面的介绍,还给读者提供了诸如性能优化等高级内容的指导。同时,本书也适合那些已经有一定Rust编程经验的读者,它们可以通过本书深入了解Rust语言内部之间的联系。 ### 回答2: 《Rust权威指南》是一本针对Rust编程语言的详细讲解和指导的权威指南。该书由Rust的核心开发人员编写,涵盖了Rust语言的基本概念、语法、数据类型、内存管理、并发编程、错误处理、泛型、宏等方面,旨在帮助读者全面深入地学习和理解Rust语言。 该书内容广泛,详细全面,适合初学者和有经验的开发人员。书中每一都有大量的代码示例,并附有解释和注释,易于理解。书中还介绍了Rust的生态系统和常用开发工具,如包管理器Cargo、测试框架等。 此外,《Rust权威指南》还强调了Rust语言的安全性和性能优势。Rust通过静态类型检查、所有权系统、借用规则等机制,大大减少了程序运行时的内存安全问题。同时,Rust的设计和实现也使得其具有与C++类似的性能优势。 总的来说,《Rust权威指南》是一本权威性强、内容深入的Rust编程指南。对于想要学习Rust编程语言开发人员来说,这本书绝对是一本值得购买和阅读的好书。 ### 回答3: 《Rust权威指南》是一本全面介绍Rust语言的书籍,该书由Rust中文社区的翻译小组翻译而成。这本书详细讲解了Rust语言的语法、基础知识、数据类型、控制流、函数、模块、生命周期、所有权系统等内容,涵盖了Rust语言的各个方面。 这本书的特点在于其权威性和全面性。作为Rust语言权威指南,该书不仅涵盖了Rust语言的基础知识,还深入讲解了Rust语言高级特性,如内存管理、安全性、并发等方面。此外,书中还包括了大量的实例和案例,可以帮助读者更深入地理解Rust语言。 对于初学者来说,该书可以作为Rust语言的入门指南。对于已经掌握了Rust语言基础知识的读者来说,本书也可以作为进一步深入学习和实践Rust语言指南。此外,该书还提供了完整的API文档和Rust语言的标准规范,方便读者进行参考和查阅。 总之,《Rust权威指南》是一本全面、详细、权威Rust语言指南,适合Rust语言的初学者和进阶者。读者可以通过该书深入了解Rust语言并掌握其应用,提高自己的编程能力和实践经验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值