Skip to content

语言基础

基本语法

数据类型

操作符

操作符的本质是一些内置函数的语法糖,允许开发者使用简洁的符号调用一个全局可见的、常用的函数。

流程控制

if-else

while/for/loop

模式匹配 match

模式匹配是一种高级的 if-else 语句和 switch 的语法糖,它支持匹配一个变量的类型、结构,并提取和解构一个变量,相较于 if-else 更加方便,同时简化了丑陋的 switch 语句。

函数

面向数据 + 方法绑定

面向对象是一个火爆的特性,常见的语言几乎都或多或少地支持。Rust 中采取的是函数式编程语言中的思路,提供一种使用面向数据和方法绑定的机制来实现面向对象的封装,但是不支持继承。这种将数据和行为分离的书写方式,相较于直接在类里面定义数据和方法的写法,更加强调数据面和管理面的分离,同时也易于扩展某个数据类型的行为,同时默认将面向对象中的组合概念进行深刻地贯彻。

C 语言中不支持面向对象的特性,但是提供基础的面向数据和面向过程的能力,在 C 语言中,允许用户定义 struct 结构来封装一个复杂的纯数据,并且 C 的开发这会书写一系列以该结构体为第一个参数的函数,从而来模拟方法绑定的效果。

Go 语言中,也提供了类似 Rust 的面向数据 + 方法绑定的方式来管理一些复杂的数据类型

结构体

在 rust 中,使用 struct 关键字定义一个纯数据结构,同时可以使用 enum 关键字定义一类纯数据结构。struct 可以是字典结构体,也可以是一个元组结构体,

rust
struct MyDataStruct {
  name: String,
  age: u32
}

struct MyPointTuple(i32, i32, i32);

struct MyDataEmpty;

enum MyEnum {
  TypeEmpty,
  TypeTuple(i32, i32, i32),
  TypeStruct {
    name: String,
    age: u32
  },
}

impl 伴生结构体

trait(特性)vs interface(接口)

在 Rust 中,提供一种名叫 trait 的机制来实现类似面向对象中的 interface 的功能,

内置 trait

内置的 trait 作为语言的基础设施,广泛用于实现大家经常遇到的功能和需求,规定语言中的一些标准,甚至能够影响编译器的编译行为。

泛型

泛型是一个迫切求值类型函数,它可以通过泛型函数自动生成多种组合类型,其表现为一个数据类型的参数,通过 Type<T> 中的尖括号进行声明和调用。

数据类型包括简单类型和复杂类型,如 i32、f64,复杂类型使用 struct、enum、union 进行定义;

rust 具有强大的泛型系统,它支持类型修饰、泛型组合、关联类型等强大特性。

类型修饰

在数据类型的基础上对类型进行额外修饰,这些修饰包括:

  • 可变引用修饰与不可变引用修饰,&、&mut
  • 可变裸指针修饰和不可变裸指针修饰,*mut、*const
  • 生命周期修饰,'a、'static
  • 动态派发修饰,dyn trait_name

泛型组合 trait bound

关联类型

在 Haskell 等函数式的语言中,存在一种名叫高阶类型的特性,高阶类型对标的是高阶函数,普通的函数接受值然后输出值,高阶函数接受函数并且可以输出新函数,高阶类型接受高阶类型并可以输出新的高阶类型。

值空间类型空间
类型
二等函数泛型
一等函数+高阶函数一等类型+高阶类型
闭包?

二等函数和一等函数区别在于,一等函数可以被看做数据一样被传递,二等函数不能,因为二等函数是迫切求值的,它必须以调用的形式出现,而不能被当作值来使用和传递;闭包意味着能够动态生成一个函数,并且引用局部作用域中的变量。泛型意味着可以使用泛型生成一个新的类型,但是泛型本身不能作为类型来传递,因为泛型是迫切求值的。

在 rust 中,没有选择实现高阶类型,但是选择实现了关联类型,来在一定程度上解决泛型迫切求值的问题。

属性标记和宏

rust 中使用 #[] 来对一个目标进行编译时的元数据定义,从而影响编译器的行为,它可以被放在结构体、枚举、函数、模块等目标的声明处。它相当于其他语言中的装饰器。

宏是指生成代码的代码,它们的工作原理是:编译器会将传入宏的代码块作为输入,经过宏处理后生成新的代码。和 C 语言的宏相比较起来,rust 的宏有更多的规则限制,更加现代,不像 C 语言那样纯粹地基于字符串进行替换。

C 语言的宏是一个简陋的系统,基于纯粹的字符串替换,在 C 的预处理器进行预处理的期间被展开。C 宏使用起来可以非常地自由放纵,这将使得宏展开的过程难以控制,给代码带来非常大的不确定性。另外,展开的宏和内联函数等特性,会导致代码调试的时候,调试器无法找到目标函数和代码,导致定位困难。但是,碍于 C 语言的语言特性太弱,宏又是 C 语言项目中不可缺失的一部分,这就要求 C 语言开发者必须明确自己的宏在做什么,保持良好的编码规范,要求项目管理者对项目进行严格管理和明确约束。

宏类型定义方式使用位置示例
声明式宏macro_rules!任何代码位置(如函数内、模块内、全局)println!vec!
自定义派生宏#[proc_macro_derive]结构体、枚举定义上,结合 derive 宏一起使用#[derive(Debug)]
属性宏#[proc_macro_attribute]项(如模块体、函数、结构体)定义上#[test]
函数式过程宏#[proc_macro]类似函数调用位置my_macro!()

声明宏

声明宏使用的时候就像一个函数一样进行调用,通过模式匹配的方式生成代码,使用 macro_rules! 宏来定义。

rust
macro_rules! say_hello {
    () => {
        println!("Hello, world!");
    };
}

fn main() {
    say_hello!();  // 输出: Hello, world!
}

过程宏

过程宏是通过解析 Rust 代码并生成新的代码来工作的,它在使用的时候拥有多种表现形式,它在定义的时候通过编写一个合法的 rust 函数来解析输入的 rust 代码,从而生成更多的代码,同时需要结合元宏 proc_macro_xxx 来定义。

  • 派生宏 一个派生宏是指能够放在 #[derive(MyDeriveMacro)] 这个高阶宏 derive 中的宏,该宏需要以独立 crate 的方式进行定义。派生宏通常用于为结构体或枚举自动实现 trait。使用 proc_macro_derive 进行定义。

    rust
    extern crate proc_macro;
    use proc_macro::TokenStream;
    
    #[proc_macro_derive(MyDebug)]
    pub fn my_debug(input: TokenStream) -> TokenStream {
        let input = syn::parse_macro_input!(input as syn::DeriveInput);
    
        // 我们为结构体生成一个简单的 Debug 实现
        let name = &input.ident;
        let gen = quote::quote! {
            impl std::fmt::Debug for #name {
                fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                    write!(f, "{} {{}}", stringify!(#name))
                }
            }
        };
    
        gen.into()
    }
  • 属性宏 一个派生宏是指能够放在 #[derive(MyDeriveMacro)] 这个高阶宏 derive 中的宏,该宏需要以独立 crate 的方式进行定义。派生宏通常用于为结构体或枚举自动实现 trait。使用 proc_macro_derive 进行定义。属性宏允许我们为各种元素(如结构体、函数等)添加自定义行为。这类宏通常用于注解代码,提供额外的功能。

    rust
    // lib.rs
    use proc_macro::TokenStream;
    
    #[proc_macro_attribute]
    pub fn my_attribute(_attr: TokenStream, item: TokenStream) -> TokenStream {
      // 修改 item 或返回新代码
      item
    }
  • 函数宏

所有权和借用机制

所有权

在 rust 中,所有权是指一个保存在内存中的数据,需要有且仅有一个所有者。所谓所有者,本质就是一个指针,通常表现为一个变量名,或者是一个实例化对象的名字符号;

  • 每个值都有且只有一个所有者,但是一个值的所有者可以改变,也就是移动语义;
  • 当所有者离开作用域时,其拥有的值会被释放;
  • 赋值操作具有默认的移动语义,即旧变量赋值给新变量时,旧变量不再拥有该值;

借用机制意味着,一个值可以被指针 A 所有,但却可以被 B 借用,其核心规则是任意时刻,一个值只能有一个可变指针指向该值,或者同时有多个不可变指针指向该值,两种情况选择其一;

  • 一个可变指针
  • 多个不可变指针

移动和借用

移动,说明一个值的所有权发生了转移,原先的指针失效了,不能再通过原指针访问数据;借用,意味着新建了一个新的指针指向了该值,但是这个指针是一个引用,引用类型和原类型相差一个 & 符号,他们都属于指针,不带 & 是所有者,带 & 是借用者。

隐式(所有权)移动

在 rust 中,除了赋值符号 =,当对函数传入参数的时候、还有函数结束并返回值的时候、还有声明闭包函数的时候,同样也会发生所有权的移动,除非将函数的参数或者返回值声明为一个 & 符号的引用,这表明函数只是希望借用,本质上是一个指针。

智能指针

rust 中的默认指针,即引用,需要受到严格的借用检查的约束限制,在一些需要灵活的场景下无法满足业务需求,因此需要使用一些智能指针来管理内存,从而同时获得自由和安全,但是需要一定的开销

智能指针特性所有权共享线程安全性备注
Box<T> Deref堆分配递归结构、大对象
Rc<T> Deref共享所有权单线程共享数据
Arc<T> Deref共享所有权多线程共享数据
Weak<T> Deref防止循环引用适用于树结构
Cell<T> ?Deref轻量内部可变适用于 Copy 类型
RefCell<T> ?Deref内部可变性运行时可变借用
Mutex<T>内部可变性运行时可变借用
RwLock<T>内部可变性运行时可变借用
Atomic<T>原子操作运行时可变借用

作用域和生命周期

rust 中有块作用域、函数作用域和全局作用域,作用域会随着程序的执行而展开,同时随着程序的执行结束而关闭。一个变量往往在一个作用域中被声明,此时,我们称这个变量属于这个作用域,当这个作用域被关闭的时候,从属于该作用域的变量将会被清除,rust 将会自动调用这些变量的 drop 方法释放资源。

生命周期泛型

生命周期是对一个引用类型的描述,它表现为一个类型的泛型

unsafe

unsafe 是 rust 特性子集,通过使用 unsafe 关键可以将一个函数或者 trait 中方法的标记为 unsafe,从而开启 unsafe 特性:

  • 解引用裸指针
  • 解析 union 的字段

异步编程

在 IO 密集型的程序中,程序的瓶颈出现在程序执行无法避免的 IO 等待,这将导致程序因为进行阻塞 IO 而被操作系统挂起,从而使得程序不能充分占用 CPU,出现了资源利用不充分的情况。

模块系统和包管理

rust 的模块系统有三个主要抽象,package, crate, mod。理解模块是建构大型应用和复用社区生态的前提。