探索Rust语言类型系统 - Part 1

发布时间 2023-12-04 20:26:49作者: 啊哈哈哈哈哈h

理解Rust语言类型系统中的Ownership(所有权), Resource Management(资源管理), Aliasing(别名), Mutation(可变性), 和the Borrow Checker(借用检查器)

目录

  1. Ownership and Move Semantics(所有权与移动)
  2. Aliasing and Mutation(别名与可变)
  3. Lifetime(生命周期)
  4. Region Based Resource Management(基于区域的资源管理)

编程语言中的类型系统

在底层,计算机仅关心bytes(字节,由0和1组成的序列)。由于bytes缺乏额外的结构,人类难以理解;在更高的级别,根据我们所在的领域,类型系统提供了对这些字节的额外解释,以便于我们理解。

直接与计算机硬件交互本质上是不安全的。除了实验量子计算机之外,硬件只能理解0或1。无论编程语言是静态类型还是动态类型,都存在类型系统。静态类型还是动态类型区别在于类型何时已知。如果没有类型,我们就无法有效快速地将我们的意图传达给计算机且不会在某些时候遇到错误,或者在与技术堆栈的最低层(硬件)交互时不格外小心。

类型系统是计算机编程和软件工程中的基本概念。它根据值的行为、结构和用途将值分为不同类型。为了确保程序的正确性、安全性和效率,类型系统对各种类型的值如何交互建立并执行规则和约束。它有助于在编译过程中检测错误,并提供一种无需执行代码即可理解代码行为的方法。

类型仅在抽象级别才有意义,这是理想的。例如,单词 a 的二进制表示与整数97的二进制表示相同。虽然它们在 CPU 级别上看起来相同,但在抽象级别上通过类型来区分。这种保护措施可以保护我们免受无效操作的影响,在生成汇编代码之前捕获类型不匹配(尽管汇编是无类型的,但类型化汇编也存在)。

如果一种语言避免隐式类型强制转换(implicit type coercion)、确保值始终在使用前初始化、防止野指针并消除类型混淆,则该语言被视为强类型。因此,强类型程序结构良好,不会遇到与类型相关的问题。以下面的 Rust 函数为例:

fn add(x: u8, y: u8) -> u8 {
    x + y
}

对于这个函数,我们可以做出以下观察:

  1. 该函数接受两个 u8 类型的参数。需要注意的是,这些类型在传递给函数之前必须进行初始化。由于类型要求是 u8,因此它确保不接受负值。此外,除了组合两个 u8 值之外,该函数不会造成任何副作用,因为 Rust 函数无法捕获其自身之外的变量。
  2. 添加两个 u8 值将产生另一个 u8 值,遵循 Rust 不允许隐式转换的严格类型系统。
  3. 如果发生溢出,例如结果超过 u8 的最大值,Rust 的行为取决于编译模式。在debug mode下,程序将出现紧急情况,停止执行并提供详细的错误消息。在release mode下,溢出被环绕,程序继续执行,可能会导致意外结果。release mode下的此行为旨在提高开发期间调试时的性能。

静态类型意味着编译器在编译时拥有有关所有变量及其类型的信息,并且在编译时执行大部分检查。这使得运行时的类型检查非常少,例如边界检查和整数溢出处理。 Rust 支持类型推断系统,允许我们在许多情况下省略显式类型注释。

静态类型系统还有助于维护大型软件并向现有代码添加新功能,而不会破坏代码的其他部分。 Rust 的类型系统在更改或更新代码时支持您,只要它编译成功。

Rust 的类型系统不仅仅是防止类型上的无效操作。内存安全和并发错误也可以通过类型系统解决。这意味着我们可以防止内存错误,就像在执行代码之前检测类型不匹配错误一样。这引入了编程范式的另一个维度。虽然 Vale、Idris 和 Pony 等其他语言也具有与 Rust 类似的类型系统,但它们并未得到广泛采用。

凭借富有表现力和健壮的类型系统,Rust 可以在编译时消除或捕获更多错误,甚至是逻辑错误,例如不完整的案例覆盖、控制流和循环中整数的不当使用以及在持有读锁时尝试写入数据。这就是为什么 Rust 的学习曲线比其他常用语言更陡峭的原因。然而,我们在改进语言的人体工程学、提供学习资源和提供全面的文档方面付出了巨大的努力。

其他具有表达类型系统的语言包括 Swift、Haskell 和 OCaml。有些概念在 Rust 中很容易表达,但在其他语言中则不然,反之亦然。选择正确的语言取决于上下文和权衡。例如,在为新项目编写 Web 应用程序时选择纯 JavaScript 可能会导致更多运行时错误。 NoRedink 使用 Elm 构建他们的 Web 应用程序,即使在长时间的生产使用后,也很少或没有出现运行时错误。 Elm 类型系统专为创建 Web 应用程序而设计,从而简化了学习过程。”

单一所有权与移动

在 Rust 中,每个值(不包括引用)都拥有其数据,这意味着所有者负责内存清理。与具有垃圾收集器(GC)的语言不同,Rust 缺乏这样的机制(即:无GC)。Rust 中的所有权规则结合了自动内存管理(如垃圾收集器-GC)的优点和 C/C++ 等语言中手动内存管理的性能。编译器在已知点处理内存释放,这消除了对内存清理的担忧。堆内存分配和释放的抽象对程序员来说是透明的。编译器本质上执行 C++ 程序员手动执行的内存管理任务,以避免内存泄漏(memory leak)和双重释放(double free)等问题。

标记为“Copy(Trait)”的类型在分配给新变量时会隐式克隆(let a = 10; let b = a; // 这个是复制值)。然而,标记为“Clone(Trait)” (并不是调用了clone()方法)的类型会被移动(let a = String::from("hello"); let b = a; // 发生了移动),这意味着原始所有者失去了对数据的访问权限,并且数据变得未初始化。 Rust 会阻止进一步使用该变量,除非在移动后重新初始化它。在这种情况下,“移动”仅指存储在堆栈上的指针,而不是实际的堆数据。 Rust 中的这种移动过程非常高效,无论是在单线程还是多线程代码中。如果所有权转移到不同的线程,则该值不能在最初创建它的线程中使用。

Copy与Clone的定义如下:

pub trait Clone: Sized {
   fn clone(&self) -> Self;

   fn clone_from(&mut self, source: &Self) {
       *self = source.clone()
   }
}
// Trait bounds; 不是继承
pub trait Copy: Clone {
    // Empty.
}

使用线性类型(linear types)或仿射类型(affine types)表达单一所有权非常简单,并且可以防止双重释放和释放后使用等问题,而无需运行时检查。线性类型仅使用一次,这可能会限制语言,而仿射类型最多使用一次。仿射类型提供与线性类型相同的安全性,但提供了更多实用的灵活性,可以在 Rust 等语言中使用来表达各种模式。

这里,“value”指的是类型 T,前面没有任何 & 或 &mut。

  • 如果 T 实现了“Copy”,则在赋值时隐式克隆它。由于这些类型驻留在内存中并且缺乏堆分配,因此当它们超出范围时没有特殊行为。复制语义在 Rust 中没有所有权概念,是使用复制标记类型实现的,允许移动和复制语义在语言中共存。

  • 如果 T 实现了“Clone”,则在分配时会隐式移动它。对于实现“Drop”特征的类型,编译器调用 drop 函数来释放内存。它还确保没有其他引用可以超出它们的使用范围。数据的所有权并不意味着能够修改数据,除非以 mut 为前缀。值的使用不能超出其原始范围,从而防止释放后使用和悬空内存。单一所有权可以防止双重释放。如果我们尝试两次清理资源,编译器将生成一条错误消息,指示“使用了已移动的值”。

所有权可以通过赋值、参数传递,从函数或闭包返回来转移。仅Clone类型(移动类型)上的克隆会创建一个独立的副本,允许不同的变量独立拥有其数据。当作用域结束时,将为每个拥有的类型独立调用“Drop”(析构函数)特征,因为所有权不能使用别名。值得注意的是,此规则有一个例外,本系列的第 3 部分将介绍该例外。

fn main() {
    //Drops immediately; 立即Drop
    let _ = String::from("Not bind to anything");
    
    //Copy types; 整型实现了Copy trait
    let a =10;
    //implicitly cloned; 隐式cloned
    let b = a;
    //explicitly cloned; 显式cloned
    let c = b.clone();
    //This code wouldn't compile if they were move types
    //如果是move类型的变量,则代码不能被成功编译
    println!("{a} {b} {c}");

    //Each variable owns its data i.e not aliased
    let uqe_owner1 = vec![14, 5, 78];

    //The explicit clone on Move types will cause dynamic allocation
    let uqe_owner2 = uqe_owner1.clone();

    let mut first = String::from("A type that implements Clone and Drop");
    //First moved to second
    let second = first;
    //here the variable first is uninitialized and statically can't be accessible

    //But the type information is still there so we can initialize again only
    //if it's mutable and used again
    first = "Reinitialized after moved".to_string();

    //both the First and second are owned by the variable vec_of_string
    //Each String is own its data and Vec owns its buffers
    let vec_of_string = vec![
        first,
        second,
        String::from("One"),
        String::from("Two"),
        String::from("Three"),
    ];
    //The if-else expression returns the ownership
    //Conditional Moving, no duplicates
    //If it's true x takes ownership
    let returned = if true{
                 //Ownership is transferred
                     x(vec_of_string)
    }
    //Otherwise y takes ownership
                 else {
                     y(vec_of_string)
    };
    //variable vec_of_string won't reach here
    //println!("{vec_of_string:?}")
    //returned variable has the ownership now
    println!("{returned:?}"); //Value dropped here

}
//Ownership received and returned to the caller
//the signature/type is T not &T or &mut T
fn x(x: Vec<String>) -> Vec<String> {
    x
}
//No value is destroyed in x or y
fn y(y: Vec<String>) -> Vec<String> {
    y
}

单线程代码不仅会阻止我们在移动数据后使用数据,而且相同的移动语义也适用于多线程代码。

借用检查器和生命周期

单一所有权更具限制性,因为当我们想要读取数据时,我们必须来回传递所有权,即使所有权对于读取/写入数据来说不是必需的。这就是借用检查器发挥作用的地方,它放宽了单一拥有类型的限制,以提供更大的使用灵活性,类似于 C/C++ 中的指针。然而,Rust 中的引用与 C/C++ 中的指针并不完全相同。 Rust 引用在几个方面是不同的:它们总是在使用前初始化、具有大小、永远不为空、遵循受限制的别名模型,并且具有生命周期和对齐方式

在 Rust 中,存在与 C/C++ 中的原始指针等效的概念,用 *const T 表示不可变,用 *mut T 表示可变。这些仅在不安全块中使用。 Rust 参考有两种变体:

  1. 不可变引用(Immutable Reference) - &T
  2. 可变引用(Mutable Reference) - &mut T

顾名思义,引用并不拥有它们所指向的数据。相反,它们提供对它们指向的内存的临时访问。这种方法在处理大堆内存时非常有效,因为它避免了仅仅为了访问而克隆数据的需要。在 64 位架构上,引用仅占用 8 个字节或 64 位,使其成为轻量级选项。值得注意的是,并非所有参考文献都是单词实体;切片和特征对象是胖指针的示例,占用两个字的内存。这个概念与 C++ 一致。

Rust 的独特之处在于它对引用及其施加的限制之间的明确区分。这些限制有助于减少通常与指针使用相关的错误。

别名与可变

Aliasing和Mutation可能会导致单线程和多线程代码出现问题。在本节中,我们将重点关注单线程代码。通过可变操作增长或收缩的类型可能会导致不正确的读取或写入。Rust 中可以通过多种方式发生这种情况:

    //for the purpose of showing that this will grow
    //after pushing more elements than 24
    let mut string = String::with_capacity(24);
    string.push_str("A mutable data structure");
    //storing different regions of data for read-only
    //For single-byte character range integer indexing is okay
    //but for multi-byte characters this will may panic
    let sub_str1 = &string[0..5];
    let sub_str2 = &string[5..];

    //Here the length is 24 since we push
    //elements upto 24
    println!("{}", string.len());

    //pushing more elements to the String data cause
    //them to grow so allocating more space
    //to move all elements to that place
    for char in ('a'..'z').into_iter() {
        string.push(' ');
        string.push(char);
    }

    //This is safe to read the owner
   //Owner is responsible for changing the pointer to the newly allocated data
   //So it should be there.
    println!("{string}");

    //But reading the references is not because
    //the references point to memory where the string initially
    //there but after mutating string may not there
    //So borrow checker forbid this
    //println!("{sub_str1} {sub_str2}");

同时写入可能会导致问题。对同一数据有两个可变引用也会导致问题。例如,假设我们有两个对同一数据的可变引用。如果我们在第二个引用之后取消引用第一个可变引用,则可能会读取或写入不正确的数据。这是因为第二个可变引用可能会修改数据,导致数据所有者为其分配更多空间。如果允许这样做,第一个可变引用可能会写入不应该写入的数据。由于这些潜在的问题,Rust 对引用进行了限制,以确保代码的安全。

不可变引用

  • &T 类型的引用被视为Copy,因为从另一个借用者借用的权限只是复制与先前借用者相同的权限。这意味着我们可以对数据有多个不可变引用,从而产生自由别名引用。这种情况没有问题,因为可变引用在我们读取数据时不能修改数据,不可变引用也不能写入数据。
  • 它是不可变的,因为我们无法更改不可变引用背后的数据。
    //A type of T
    let referent:bool = true;
    //A type of &T
    let borrower1:&bool = &referent;
    //copied
    let borrower2 = borrower1;
    //still create a reference from the referent
    let borrower3 = &referent;

    //Can read any of reference and referent itself
    println!("{referent} {borrower1} {borrower2} {borrower3}");

可变引用

  • &mut T 类型的引用是唯一的,因为我们不能仅通过复制可变借用来创建多个可变引用,而不可变借用则可以。
  • 可变引用不能使用别名,类似于所有权的移动语义。然而,与所有权不同的是,可变引用并不拥有数据;他们授予修改所拥有数据的专有权限。
  • 当存在可变引用时,数据所有者本身无权访问数据。
  • 这个概念类似于多线程代码中的 ReadWriteLock 或 XOR 模式,区别在于它是静态验证的,并且在单线程代码中使用时不会产生任何开销。
    //A type of T
    let mut referent: bool = true;
    //A type of &mut T
    let borrower1: &mut bool = &mut referent;
    //moved
    let borrower2 = borrower1;
    //borrower1 moved so we can't use it here
    //println!("{borrower1}");

可变引用和不可变引用都不拥有它们指向的数据,因此当它们超出范围时,析构函数不会运行,只会在其使用范围内运行。引用更类似于请求访问数据的权限;它们允许临时使用,最终,当它们的范围结束时,它们必须释放它。我们可以对数据有多个不可变引用或单个可变引用,但不能对同一数据同时有可变引用和不可变引用。这个特性可以防止我们的代码中发生死锁,尽管智能指针可能会出现这种死锁。

可变类型和不可变类型之间的区别以及多重不可变性或唯一可变性的限制是静态防止迭代器无效 (II) 的原因,而不会导致 C/C++ 中的未定义行为、Java 中的运行时异常或 Python 中的无限循环。由于签名不同,接受可变引用的函数也不能接受不可变引用。然而,反之亦然,因为 Rust 自动将可变引用强制为不可变引用,从而在不违反内存安全的情况下保持内存安全。

这些限制使我们的代码更容易推理,也使编译器能够更有效地优化代码,因为不存在不受限制的别名允许编译器做出假设,否则在存在此类别名的情况下会导致错误使用。

虽然对引用的限制可以防止某些错误的发生,但它们也会导致借用检查器不允许实际上安全的代码模式。非词汇生命周期(NLL)的引入改善了这种情况。 RwLock 模式对于单线程代码中的数组、整数甚至切片等基本类型来说可能有点过分了,但它在多线程代码中仍然带来了挑战。这是因为这些类型不会动态增长或收缩,但与其他动态可变数据相比,它们可以以不会导致问题的方式进行变异。值得注意的是,只有可变和可增长类型才会导致单线程代码中并发别名/突变问题。

     //A copy type 
    let mut a:i32 = 10;
    let ref1 = &a;
    let ref2 = &a;
    let mut_ref1 = &mut a;
    let mut_ref2 = &mut a;
    //we can't previous mutable borrow to modify
    *mut_ref1+=1; 
    //nor access read-only references
    //Even though it's completely memory safe
    println!("{} {}",ref1,ref2);

不幸的是,Rust 并没有区分可变、不可增长类型和可变、可增长类型的借用检查器,就​​像所有权类型不适用于复制类型一样。然而,这些限制已通过使用单元类型得到缓解,而不是使借用检查器变得复杂。

当您创建对集合的可变引用时,借用检查器会假定您正在创建对整个数据集的可变引用,即使情况可能并非如此。我们可以利用在这些类型上定义的方法来安全地改变它们,而不是诉诸不安全的块来阐明我们的意图。集合(例如切片slice)提供了独立安全地改变非重叠区域的方法。

   let mut vector = vec![1,3,5,67,78,9];
   //borrowing non-overlapping regions
   let mut_ref_to_1 = &mut vector[0];
   let mut_ref_to_2 = &mut vector[5];
   // This is safe but borrow a checker forbid this
   //*mut_ref_to_1+=10;          
   //*mut_ref_to_2+=23;
   
   //Mutate different elements through methods
   //Instead of directly
   if let Some(last) = vector.last_mut(){
         *last=11;
    }
    if let Some(first) = vector.first_mut(){
        *first=10;
    }

验证引用的生命周期和范围:

顾名思义,生命周期用于确定引用可以生存并保持有效的时间。 Rust 中的生命周期仅作为编译时注释存在,因为 Rust 没有垃圾收集器。创建引用时,编译器会隐式地用生命周期对其进行标记。

不同的引用会使用不同的生命周期标签,并且引用可以嵌套,即一个引用可能指向另一个引用,这可能涉及到多个生命周期。范围是指创建引用或引用对象的区域。由于这些因素,定义和使用某些内容的顺序变得很重要。

生命周期不仅与引用(如 &T&mut T 等类型)相关,还与所指对象(T 类型)相关。在全局范围之外创建的动态分配的数据甚至堆栈分配的数据的生命周期较短,仅限于创建它们的范围。只有声明为 static 或 const 的类型才有静态生命周期。但是,存在一些限制。我们不能在静态或 const 上下文中使用动态数据结构或可变引用,并且 const 值根本不能是可变的。非常量类型可以与标准库或 crates.io 的库提供的延迟初始化一起使用。较长的寿命可以被强制缩短,但反之则不可能。创建字符串时,它最初具有静态生命周期,当我们在特定范围内引用它时,该生命周期就会受到限制。

生命周期使用撇号后跟小写字母来声明,例如 fn、impl 块、结构、枚举和具有关联类型的特征的通用上下文中的 'a、'env、'lifetime。静态生命周期被声明为“static”。如果 T 是非常量(在编译时不求值),则不可能有 'static T 类型,但 &'static T 和 &'static mut T 是有效的。静态生命周期意味着数据在程序运行时就存在,但其可访问性取决于它创建的范围。如果在全局范围内定义,则可以在整个程序中访问它,而如果在函数体内(例如在主函数中)定义,则不能在该函数的范围之外引用它。然而,数据仍然存在于二进制文件中;它只是无法超出其定义的范围进行访问。

    use std::collections::HashMap;
    let mut hashmap = HashMap::new();
    {
        //static created inside the lexical scope
        static VALUE: i32 = 10;
        hashmap.insert("Key", &VALUE);
        // or
        // hashmap.insert("Key",VALUE);
    }
    //The static VALUE can't be accessed here but
    //through hashmap
    println!("{}", hashmap.get("Key").unwrap());

任何引用都不能比其所指对象更久的存在;这是使用生命周期进行静态验证的。所指对象必须比参考对象生命周期更长;否则,我们可能会有悬空指针。当引用对象被销毁时,访问引用将无效。

   {
       // Both referent and reference created in this scope 
       // Thus destroyed in this scope too
       let vector = vec![56,89,34];
       let reference = &vector;
       println!("{reference:?}");
       
       //Here both vector and reference freed
    }  
   //This is Use After Free for both
   //variable reference and referent vec
   //println!("{vector} {reference}");

``
```rust
   //vector created here
   let vector; //'a
    {  
       //Vector initialized in the inner scope
       //but still the same lifetime as the outer scope
       vector = vec![1]; 
       //But reference created in the inner scope
       //thus can't exist beyond the scope
       let reference = &vector; 
   }
   //Vector still accessible here 
   println!("{vector:?}");
   //but references don't
   println!("{reference}");
    let referent = vec![1]; 
    //even though the reference is defined in the outer scope
    //but it references the data that created inside in this 
    //scope so we can't use beyond this scope.
    reference = &referent; 
    }
    
    // The variable reference is invalidated when 
    //above scope is ended so we can't use it here
    
    //println!("{reference:?}");
    
     //The type of variable reference is preserved when initialized 
     //Reference variable initialized in above scope meaning 
    //we can't initialize other than &vec<i32> in outer scope
    //reference =10;
    
    // But we can shadow with different types using let
    let reference = 12;
let mut referent = vec![1];
let reference = &mut referent;
 {
     //we can read either referent or reference or in other words
     //we can either read through the referent or write through the //reference but not both at the same time
     println!("{reference:?}");
 }
    //the scope {} own the data so we can't return a reference but
    //only the data itself i.e moving to the outer scope
        let reference = {
        //we can't use the reference of data created in this scope in the outer //scope or we can't return the reference of data created in this scope
              let string = String::from("Created in this scope");
              //Returns the reference to String due to & operator
              //which returns the string slice
              &string
        };

生命周期可以是有条件的,根据条件,他们可以存在得更长或更短。

    let mut x = 10;
    let y = &mut x;
    let bool_ = true;
    if bool_ {
    //if true then y can't be used after this
        x = 11;
    } else {
    //else the lifetime is valid until this scope.
    //so we can modify and print it
       *y+=11;
       println!("{y}");
    }
    //We can't use y after the if-else because 
    //we don't know which block will execute 
    //so borrow checker refuses to compile
        
    //println!("{y}");
        
    //But the referent can be read no matter which block is run
    //because either way x is valid till here with a value of if or else branch
    println!("{x}");

变量本身是不可变的,但它存储对数据的可变引用,以便我们可以通过取消引用引用变量来修改原始数据(引用)。如果引用变量是可变的并且它存储对数据的不可变引用,则不可能通过引用变量改变所指对象。变量前面的 mut 和右侧的 mut 根据类型的不同具有不同的语义。

区域可以嵌套;内部作用域可以引用外部作用域中的数据,因为外部作用域在内部作用域('a,'b:'a)之后失效。外部作用域的寿命不能比它借用数据的所有者的寿命长。

 
   let a= 10;
   let b : &i32= &a; //'a
   let c : &&i32 = &b; //&'b &'a i32 
   {
        let d = &a; // 'c: 'a, a scope of c is bound to the scope of a, or in other words variable a outlives the variable d. 
   }

除了在通用上下文中之外,我们不必担心生命周期,因为编译器大多数时候会自动推断生命周期。在某些情况下,生命周期错误不符合人体工程学调试。有些生命周期隐藏在图书馆中或从我们这里抽象出来。

简单的生命周期示例并不能掩盖生命周期分析的复杂性。由于我在项目中缺乏对这些类型的使用,这里没有讨论更高种类的类型、子类型以及它们在使用中所依赖的差异类型。

类型之间的区别使得 Rust 可以拥有不同的 API 来满足借用检查器和所有权规则。要了解哪些方法借用(可变/不可变)以及哪些方法移动所有权,可以参考集合类型和迭代器的文档。如果没有这些 API,我们在 Rust 生态系统中的体验将充满挑战。

基于区域/范围的资源管理

这种方法涉及一种静态的内存管理方法,可以解决以下几个问题:

  1. 它可以防止手动内存管理中常见的错误。
  2. 它避免了对垃圾收集器的需要。
  3. 它消除了程序员直接干预的需要。

此方法不是自由分配内存并在不同位置释放内存,而是将内存分配和释放限制在特定的词法范围或区域。内存或资源在区域内分配,并在范围结束时释放。借用检查器确保区域内的引用不会在该范围之外使用。因此,内存泄漏、内存保留时间延长和临时内存错误(例如“用户释放后(UAF)”、“悬空指针(DP)”和“双重释放(DF)”等问题)都被消除了。

数组、字符串、向量等数据结构及其切片变体包含关联的长度信息。这可以启用运行时边界检查,或者在编译器可以推断出边界检查是不必要的情况下消除它们。这解决了空间记忆错误,例如“越界(OOB)”。

然而,Rust 提供了一种使用 Box::leak 来泄漏内存的显式方法。在其他语言可能导致崩溃的情况下,Rust 更喜欢在编译时捕获错误。例如,不会因“释放后的用户”、“悬空指针”、“双重释放”、“空指针异常 (NPE)”或使用未初始化的值而崩溃,意味着可以避免导致应用程序崩溃的运行时意外。

Rust 的方法涉及权衡。虽然它可能会导致开发过程中的崩溃,但这些崩溃通常有助于尽早发现错误,防止它们进入生产并导致不可预测的行为。

 let vector: Vec<i32> = vec![1, 5, 7, 87, 231];
    //0 to 3 and 4 are exclusive
    accept_sub_slice(&vector[..4]);
    //0 to 4 because it's inclusive
    accept_sub_slice(&vector[..=4]);
    
    fn accept_sub_slice(slice: &[i32]) {
    //even though the vector has a length of 5
    //but we can't access beyond the ranges we specified
    //when calling
    println!("{}", slice[4]);
}

该函数接受向量的子切片。在函数体内,我们无法访问超出调用时指定范围的元素,即使向量有更多要索引的元素。如果界限大于或等于长度,索引运算符将会出现恐慌,因为集合是零索引的。 get 和 get_mut 方法返回一个 Option,允许我们使用显式处理来安全地索引数据而不是恐慌(panic)。

RAII 模式:

资源包括堆内存、数据库句柄、锁、套接字和文件等系统资源,或在作用域结束时使用 Drop 特征自动清理的任何类型。这反映了 C/C++ 中手动内存管理的确定性和可预测性能,同时还具有安全性的额外优势,并且无需程序员干预。

use std::net::TcpListener;
fn main() {
    {
        let tcp = TcpListener::bind("127.0.0.1:8090").unwrap();
        //std::mem::forget(tcp);
    }
    {
        let socket = TcpListener::bind("127.0.0.1:8090").unwrap();
    }
}

在上面的代码中,我演示了我们不必显式关闭套接字。为了说明这一点,我创建了另一个套接字,在不同范围内侦听同一端口。如果第一个套接字未关闭,程序将出现紧急情况,因为除非程序完成使用该端口,否则操作系统不允许使用该端口。如果是这种情况,我们将收到一条错误,指示 AddrInUse。当您取消注释上面的行时,就会发生这种情况,因为 mem::forget() 取得所有权但不执行任何操作,这意味着套接字仍在使用中。因此,当我们在第二个套接字绑定上调用 unwrap() 时,会导致恐慌。这与 Rust 中处理其他资源的方式一致。

以下是一些展示基于所有权的系统的好处的用例:

基于硬件的隔离会产生不小的性能成本,而基于软件的隔离则不会产生与基于硬件的隔离相同的费用。然而,在具有不受限制的别名的编程语言(如 C)中实现基于软件的隔离将很难有效实现,因为我们的假设可能会在存在别名的情况下失败,从而使静态分析变得复杂。由于 Rust 的单一所有权和受限别名模型,《Rust 中的系统编程:超越安全》一文表明,Rust 能够实现基于零开销的基于软件的隔离,而无需显着的运行时开销或依赖于特定于硬件的功能。

尽管基于区块链的应用程序更安全,但编程语言本身是安全的并且能够有效地表达他们希望在应用程序中维护的模式也至关重要。这就是为什么基于区块链的应用程序是使用 Solidity 和 Obsidian 等语言来开发的,而不是 Java 甚至 C++。这些语言的类型系统可以表达契约并在编译时防止许多其他语言无法做到的错误。然而,情况正在发生变化,Solana 和 Coswasm 等平台使用 Rust 结合 WebAssembly 来开发 web3 平台。 Rust 的高级类型系统用途广泛,足以用于任何安全性和可靠性至关重要的应用程序。 Rust 是一种混合语言,不仅与其他编程语言相比,而且还具有其自身的功能。它包含所有权类型和非所有权类型、安全(在借用检查器的雷达下)和不安全(程序员的责任)Rust,以及安全和非安全线程数据结构,同时遵守 Rust 的安全原则和可用性。这种多功能性扩展到特定领域的语言。

理解所有权和借用对于理解和使用其他语言功能(例如特征、泛型、闭包、模式匹配、结构和枚举实现以及并发原语)编写代码至关重要。所有这些都是根据所有权和借贷规则设计的。

原文链接