searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

rust 顶层设计

2023-11-21 01:31:19
53
0


rust起源


任何一门语言的兴起,都是为了解决以往其他语言所面临的问题或挑战 -- 鲁迅

自操作系统诞生以来,系统级主流编程语言,从汇编语言发展到C和C++, 已经发展了近50年。但仍然存在两个难题:

  • 很难编写内存安全的代码
  • 很难编写线程安全的代码

这两个问题本质的原因是:C/C++属于类型不安全的语言。

因此,需要一个可以提供开发效率高、代码容易维护、性能还能与C/C++媲美,同时还得保证安全性的语言。

rust应运而生!

由此可见,rust之所以安全,是因为从设计上就是解决传统编程不安全的问题的,这是他的内在灵活。

 

rust核心解决的问题


内存安全


在编程中,内存管理是一项具有挑战性的任务。错误的内存管理可能导致诸如数据竞争,空指针引用,内存泄漏等问题,这些问题常常导致程序崩溃或者安全漏洞。传统的编程语言如C和C++虽然给予了开发者对内存的高度控制权,但同时也使得开发者需要对内存管理负责,而内存管理的错误往往导致严重的后果。

类似新兴的编程语言golang,引入了垃圾回收机制,通过语言自动处理内存分配和回收,实现了内存安全。但是,垃圾回收会有较大的性能损失,并且存在 “stop the world” 的问题!

Rust在这方面采取了一种全新的方法,它引入了所有权(ownership)、借用(borrowing)和生命周期(lifetime)的概念,用以保证内存安全而无需垃圾回收

Rust的这种设计让编译器在编译阶段就能捕捉到许多常见的内存错误,从而极大地提高了程序的安全性和稳定性。

并发安全

并发编程是另一项具有挑战性的任务,尤其是在多线程环境中。并发程序中的数据竞争问题是导致程序错误的主要原因之一。传统的编程语言往往需要开发者自行使用锁等同步机制来避免数据竞争,而这对于开发者来说是一项非常繁琐且容易出错的任务。

Rust通过其所有权系统,配合可变借用限制条件(同一作用域内,可以多个入口度读变量;同一作用域内,只允许一个入口修改变量;同一作用域内,不允许同时存在读入口和写入口),提供了一种在编译时检测数据竞争的机制。

这种设计使得在Rust中写并发程序变得更加安全且容易。通过在编译阶段就消除数据竞争,Rust让并发编程变得更加简单和安全。

零成本抽象

在许多高级编程语言中,语言提供的抽象往往会导致运行时的性能损失。例如,虚函数、动态类型、垃圾收集等特性在提供便利的同时,也可能导致程序的性能下降。为了提高性能,开发者往往需要做出妥协,放弃一些便利的抽象。

Rust提供了“零成本抽象”的承诺。

在Rust中,抽象不会导致运行时的性能损失。这是因为Rust的设计哲学是:让那些在编译阶段就能解决的问题,在编译阶段就解决掉。这种设计使得开发者可以在不牺牲性能的前提下,使用高级的抽象来编写代码。

trait 和 宏是rust 零成本抽象的基石!

 

跨平台开发

在现代软件开发中,跨平台性成为了一项重要的需求。开发者们希望他们编写的代码能够在各种不同的平台上运行,而不需要进行大量的修改。然而,不同的平台往往有不同的系统调用和硬件接口,这使得跨平台开发变得非常复杂。

Rust为跨平台开发提供了强大的支持。它的标准库提供了一系列的抽象,可以在不同的平台上进行一致的系统调用。

此外,Rust还支持WebAssembly,使得Rust代码可以在浏览器中运行。

性能优化

性能是任何编程语言都需要考虑的问题。

Rust通过零成本抽象、精确的内存管理和高效的并发处理,提供了卓越的性能。

Rust的代码执行效率可以与C++相媲美,而且由于其内存和并发安全的设计,开发者可以更加集中精力在业务逻辑上,而不需要过多地担心性能优化。

 

内存安全补充

常见的内存访问问题有如下几个:

  • 引用空指针
  • 使用未初始化内存
  • 访问已释放的内存
  • 内存访问越界
  • 重复释放
  • 内存泄漏

为了保证内存安全,Rust语言建立了严格的安全内存管理模型。


无空值设计+强制初始化

(1)rust 并不支持‘空值’,不存在null、nil、NULL等,取而代之的是Option。 而Option 是一个枚举类型,如果要使用其中的值,就必须进行模式匹配,这样就强制编程者去关注是否为空。进而避免了空值引用(引用空指针)。

(2)rust 在定义变量时,不允许定义未初始化的变量。举例而言,在c语言中,申请一个结构体空间,那么得到一个结构体指针,但结构体指针所指向的内容依然还是空值,此时你便可以读写在实例;在rust中,申请一个结构体实例,就必须要求先对结构体所有字段进行初始化,才可以使用。

以上两点,就可以保证rust中内存引用不会出现空指针引用,或访问到未初始化内存!

 

所有权系统 

(1)rust 中,每个内存分配的实例,都会绑定到一个变量x上,则x拥有该实例的所有权。一个实例,任意时刻只能被一个变量拥有所有权。

(2)当x离开其作用域时,x将被自动销毁;同时,x所指向的内存也会自动被释放。

(3)在x作用域内,x可以将其实例的所有权移交给另一个变量y。

rust的所有权系统,可以确保一个实例由唯一的变量持有所有权,并将其生命周期与持有所有权的变量绑定;从而避免重复释放,避免内存泄漏。

 

借用和借用检查(生命周期)

rust的所有权系统,虽然为内存分配和释放提供了有力保障,但对编程过程中数据共享却形成和非常大的阻碍。为了解决这个问题,rust设计了借用机制。借用,和其他语言中的‘引用’概念相似,是一个指向实例的指针。

rust中,通过借用,可以对实例进行读取和修改,但不会持有该数据所有权;那么当该借用变量离开其作用域时,则只回收该变量本身,而不会释放其指向的内存分配。

rust 通过借用解决了数据共享问题,同时避免了重复释放。

但借用会引发另一个问题:如果在借用过程中,持有实例所有权的变量先走出了作用域,那么实例内存空间就会被释放,此时引用变量再去访问这块内存,就会形成 ‘访问已释放的内存’ 的错误。为了解决这个问题,rust设计了借用检查机制 。借用检测机制的核心是:通过一种机制,确保借用变量的生命周期内,实例的生命周期不能提前结束。

而借用检查的实现方式:生命周期检测。 编程中,每一个变量都有一个生存期(也称生命周期);rust 的引用的生命周期,关联着 实例拥有者变量的生存期(记为‘o),和 引用变量的生存期(记为 ‘p);那么,要求 'p 是包含于 'o的(即实例变量的生存周期一定要比引用变量的生存期要长)。

{
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}


上述的一段代码,rust编译就会报错,提示x生存不够长。我们将r和x的生存期标注出来:

    let r;                 

    {
        let x = 5;         // 'x start
        r = &x;            // 'r start 
    }                      // 'x end

    println!("r: {}", r);  // 'r end
}


如上代码标注,借用检测器,在确定引用时,就会比较引用变量本身和实例拥有者变量的生命周期长短,并确保引用的生存期不能长于实例的生存期。

 

另一方面,当引用在函数中传递时(或从函数中返回引用),此时就丢失了引用所绑定的实例变量的生存期信息,此时编译器在确认引用生存期时将面临挑战。有如下示例函数:

fn longer(p1:&String, p2:&String) -> &String {

    if pl.len() >= p2.len() {
        p1
    }
    p2
}

longer函数返回两个字符串中更长的一个。

... 
// p1 start
// p2 start 

let rp = longer(p1, p2);  // rp start 

// p1 end 

println!( "the longer pointer is {}", rp); // rp end

// p2 end 

有如上示例代码,通过longer函数决策,rp是p1、p2 其中的一个,但具体是哪一个,却不知道。当rust编译器,尝试判断p1 和p2 的实例变量的生命周期与rp的生命周期的长短时,无法选择比较目标,而选择报错。

此时,编译器需要程序员的协助,帮它指定一个比较标准。而实现该目标的方法,就是“生命周期标注”!

生命周期标注,是用来告诉编译器,一个可以评估引用与其关联的实例生存期长短的依据。值得注意的是,生命周期标注,只是引导编译器进行评估比较,并没有实际改变被标准的变量的生命周期!

fn longer<'a>(p1: &'a String, p2: &'a String) -> &'a String {
      if p1.len() >= p2.len() {
          return p1;
      }
      p2
  }

对longer函数进行如上修改。该函数签名表示:longer函数接收入参p1,p2,返回值生存期关系为:p1 p2的实例变量的生存期,不能短于该函数返回值的生存期。

let p1 = String::from("123456");
let p2 = String::from("789");

let rp = longer(&p1, &p2);

drop(p2);

println!("{}", rp);

 

上述代码编译会报错,其中drop(p2)处,在rp引用未结束前尝试销毁p2(缩短p2的生存期),导致p2的生存期无法满足rp的引用需求,该问题会被编译器执行rp生命周期检测是发现并暴露出来。

 

0条评论
0 / 1000