Rust基础

174 阅读13分钟

官方网站

www.rust-lang.org/zh-CN/learn

教程

kaisery.github.io/trpl-zh-cn/

www.runoob.com/rust/rust-t…

最高效的老师:freegpt

环境搭建(类linux系统)

安装

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Cargo

Cargo 是 Rust 的构建系统和包管理器。

创建工程cargo new myapp
构建cargo build
运行cargo run
格式化cargo fmt

基础语法

变量、不可变量、常量

rust变量的两个特性:

  • 强类型
  • 不允许精度有损失的自动数据类型转换
let a = 1;

a = "abc"; // 错,类型不一致
a = 4.56;  // 错,精度损失
a = 456; // 错,a不是可变变量

Rust 语言为了高并发安全而做的设计:在语言层面尽量少的让变量的值可以改变。所以 a 的值不可变。但这不意味着 a 不是"变量"(英文中的 variable),官方文档称 a 这种变量为"不可变变量"(不是常量!不是const!)。

let mut a = 1; // mutable
a = 456; // 正确

常量和不可变量的区别

变量的值可以"重新绑定",但在"重新绑定"以前不能私自被改变,这样可以确保在每一次"绑定"之后的区域里编译器可以充分的推理程序逻辑。

// a是变量
let a = 123;   // 可以编译,但可能有警告,因为该变量没有被使用
let a = 456;

// a是常量
const a: i32 = 123;
let a = 456; // 报错:patterns `i32::MIN..=122_i32` and `124_i32..=i32::MAX` not covered. missing patterns are not covered because `a` is interpreted as a constant pattern, not a new variable. help: introduce a variable instead: `a_var`。
// Rust 编译器发现有两个范围的模式没有被匹配到,即 i32::MIN..=122_i32 和 124_i32..=i32::MAX。 这是因为 a 在这种情况下被解释为常量模式而不是新变量模式,因此缺少的模式未被匹配到。这个报错还提供了一个建议,即引入一个新变量来解决问题,例如使用 a_var 代替 a。

"重新绑定" —— 重影 Shadowing

重影就是指变量的名称可以被重新使用的机制。重影与可变变量的赋值不是一个概念,重影是指用同一个名字重新代表另一个变量实体,其类型、可变属性和值都可以变化。但可变变量赋值仅能发生值的变化。

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
}
// 结果
The value of x is: 12

fn main() {
    let x = 5;
    let x = "hehe"; // 重新绑定可以换类型
    println!("{}", x)
}
// 结果
hehe

数据类型

整型

  • i8 / u8
  • i32 / u32 : 默认i32
  • i64 / u64
  • i128 / u128:搜了一下i128最大是340282366920938463463374607431768211455
  • isize / usize: 位长度取决于所运行的目标平台,如果是 32 位架构的处理器将使用 32 位位长度整型。

整型进制

进制说明
十进制98_222数字中间可以插入下划线,更容易辨别大小
十六进制0xff
八进制0o77
二进制0b1111_0000
字节(只能表示 u8 型)b'A'

浮点型

rust有两种浮点型——32 位浮点数(f32)和 64 位浮点数(f64),默认是双精度。

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}

布尔型

用 bool 表示,值只能为 true 或 false。

字符型

Rust的 char 类型大小为 4 个字节,代表 Unicode标量值,这意味着它可以支持中文,日文和韩文字符等非英文字符甚至表情符号和零宽度空格在 Rust 中都是有效的 char 值。

由于中文文字编码有两种(GBK 和 UTF-8),所以编程中使用中文字符串有可能导致乱码的出现,这是因为源程序与命令行的文字编码不一致,所以在 Rust 中字符串和字符都必须使用 UTF-8 编码,否则编译器会报错。

数组

数组用一对 [] 包括的同类型数据:

let a = [1, 2, 3, 4, 5];
// a 是一个长度为 5 的整型数组

println!("{}", a.len())
// 使用a.len()获取数组长度

let b = ["January", "February", "March"];
// b 是一个长度为 3 的字符串数组

let c: [i32; 5] = [1, 2, 3, 4, 5];
// c 是一个长度为 5 的 i32 数组

let d = [3; 5];
// 等同于 let d = [3, 3, 3, 3, 3];

let first = a[0];
let second = a[1];
// 数组访问

a[0] = 123; // 错误:数组 a 不可变
let mut a = [1, 2, 3];
a[0] = 4; // 正确

复合类型(元组)

元组用一对 () 包括的一组数据,可以包含不同种类的数据:

let tup: (i32, f64, u8) = (500, 6.4, 1);
// tup.0 等于 500
// tup.1 等于 6.4
// tup.2 等于 1
let (x, y, z) = tup;
// y 等于 6.4

println!("{}", tup.0) // 元组元素的访问方式不一样

函数

rust函数的结构:

fn 函数名( 参数 ) -> 返回值 {
    函数体
}

实例

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

// 数组作为参数
fn main() {
    let arr = [1,2,3];
    println!("{}", arr_len(&arr));
}

fn arr_len(arr : &[i32]) -> usize {
    return arr.len();
}

表达式

当语句不以分号结尾,就是表达式。语句没有返回值,表达式有返回值。

let a = 6; // 没有返回值,是语句
let a = (let b = 2); // 错误!因为let b = 2没有返回值

// 以下都是表达式
a = 7
b + 2
c * (a + b)

表达式块

{}包裹的多条语句,最后一个步骤是表达式,此表达式的结果值是整个表达式块所代表的值。这种表达式块叫做函数体表达式。

fn main() {
    let x = 5;

    let y = {
        let x = 3;
        x + 1 // 注意没有分号结尾
    };

    println!("x 的值为 : {}", x);
    println!("y 的值为 : {}", y);
}
// 结果
x 的值为 : 5
y 的值为 : 4

可以用表达式作为函数返回值,注意必须是最后一句,因为表达式块不是return。

fn main() {
    fn five() -> i32 {
        5 // 没有分号,它是表达式
    }
    println!("five() 的值为: {}", five());
}
// 结果
five() 的值为: 5

为什么println!()调用时会使用感叹号,其他函数没有?

println!函数使用感叹号(!)是因为它是一个宏(Macro),而不是普通的函数。

宏与函数相似,可以带参数并执行操作,但是它们的行为更为复杂。宏的主要功能是在编译时生成代码,而不是在运行时执行代码。在生成代码时,宏可以访问类型和结构等编译器内部的元信息,从而允许执行比函数更高级的操作。

结构体

定义、初始化

初始化结构体必须定义所有成员。

struct Site {
    domain: String,
    name: String,
    nation: String,
}

let bytedance = Site {
    domain: String::from("www.douyin.com"),
    name: String::from("yanyu"),
    nation: String::from("China"),
};

当你想用另一个实例的一部分成员定义一个实例:

struct Site {
    domain: String,
    name : String,
    nation: String,
}
fn main() {
    let s1 = Site{
        domain: String::from("hehe"),
        name: String::from("yanyu"),
        nation: String::from("China"),
    };
    let s2 = Site{
        nation: String::from("US"),
        ..s1 // 其他属性用s1的
    };
}

元组结构体

当你要处理那些需要定义类型(经常使用)又不想太复杂的简单数据,可以这样简单的定义struct:

fn main() {
    struct Color(u8, u8, u8);
    struct Point(f64, f64);

    let black = Color(0, 0, 0);
    let origin = Point(0.0, 0.0);

    println!("black = ({}, {}, {})", black.0, black.1, black.2);
    println!("origin = ({}, {})", origin.0, origin.1);
}

结构体方法

结构体方法的第一个参数必须是 &self,不需声明类型。定义一个矩形,并计算面积、周长:

struct Rectangle {
    width: u32,
    height: u32,
}

impl  Rectangle {
 fn  area (& self ) ->  u32 {
 self .width * self .height
}
 fn  perimeter (& self ) ->  u32 {
 self .width * 2 + self .height * 2
}
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    println!("rect1's area is {}", rect1.area());
    println!("rect1's perimeter us {}", rect1.perimeter())
}

结构体关联函数

关联函数不用于结构体方法,定义时也不需要self参数,调用它不需要实例,使用::进行调用:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn rect(w: u32, h: u32) -> Rectangle {
        return Rectangle {
            width: w,
            height: h,
        };
    }
}

fn main() {
    println!("{:?}", Rectangle:: rect ( 100 , 50 ) )
}

条件语句

if-else

和golang的语法基本类似。条件不需要括号包括、必须是bool型。

fn main() {
    let a = 12;
    let b;
    if a > 0 {
        b = 1;
    } else if a < 0 {
        b = -1;
    } else {
        b = 0;
    }
    println!("b is {}", b);
}

用表达式和if-else构成三元条件运算:

fn main() {
    let a = 3;
    let number = if a > 0 { 1 } else { -1 };
    println!("number 为 {}", number);
}

match

fn main() {
    let number = 3;
    match number {
        1 => println!("The number is one."), // 精准匹配
        2 | 3 => println!("The number is either two or three."), // 或
        4..=10 => println!("The number is between four and ten."), // 范围匹配
        _ => println!("The number is something else."), // default
    }
}

循环语句

for

for在rust里面用于遍历集合,因此没有Go / C那种条件判断能力。

fn main() {
    // 遍历数组,可以控制步长
    let arr = [1, 2, 3, 4, 5];
    for element in arr.iter().step_by(1) {
        println!("{}", element);
    }
    // 遍历数字区间
    for i in (0..10).step_by(2) {
        println!("{}", i);
    }
}

while

可以用while实现条件退出。rust没有do-while语法。

fn main() {
    let mut number = 1;
    while number != 4 {
        println!("{}", number);
        number += 1;
    }
}

loop

一个while true的平替

fn main() {
    let mut number = 1;
    loop {
        println!("{}", number);
        number += 1;
        if number >= 4 {
            break;
        }
    }
}

可以配合break+返回值,实现简易的循环查找

fn main() {
    let s = ['y', 'a', 'n', 'y', 'u'];
    let mut i = 0;
    let location = loop {
        let ch = s[i];
        if ch == 'u' {
            break i; // 返回值
        }
        i += 1;
    };
    println!(" 'u' 的索引为 {}", location);
}
// 结果
 'u' 的索引为 4

所有权

所有权对大多数开发者而言是一个新颖的概念,它是 Rust 语言为高效使用内存而设计的语法机制。所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念。

关于所有权的三条规则:

  1. Rust 中的每个值都有一个变量,称为其所有者。
  2. 一次只能有一个所有者。
  3. 当所有者不在程序运行范围时,该值将被删除。

变量范围:

{
    // 在声明以前,变量 s 无效
    let s = "yanyu";
    // 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效

内存和分配

C/C++手动管理内存,Go/Java依赖垃圾回收。

如果我们定义了一个变量并给它赋予一个值,这个变量的值存在于内存中。如果无法在定义时明确数据长度,也就无法在编译阶段令程序分配固定长度的内存空间供数据储存使用,这就需要提供一种在程序运行时程序自己申请使用内存的机制——堆。本文所讲的所有"内存资源"都指的是堆所占用的内存空间。

有分配就有释放,程序不能一直占用某个内存资源。因此决定资源是否浪费的关键因素就是资源有没有及时的释放。

一个C++的例子:

#include <iostream>
int main() {
    std::string * str = new std::string("hello world!");
    std::cout << *str << std::endl;
    delete str;
    return 0;
}

Rust 中不需要调用 delete 来释放字符串 str 的资源。Rust 之所以没有明示释放的步骤是因为在变量范围结束的时候,Rust 编译器自动添加了调用释放资源函数的步骤

变量和数据的交互

变量与数据交互方式主要有移动(Move)和克隆(Clone)两种。

移动

基本类型(所有整型、浮点、布尔、char以及只有基本类型的元组)的变量是存在栈上的,栈上数据的传递方式是复制。

let x = 5; // 将值 5 绑定到变量 x
let y = x; // 将 x 的值复制并赋值给变量 y

但是如果数据在堆上,就不是复制。以下例子看起来像是两个String对象指向了同一个值(堆上),但是 s1 和 s2 都被释放的话堆区中的 "hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用,这就是移动。

let s1 = String::from("hello"); // 产生一个 String 对象,值为 "hello"。对象在栈上,指向堆中的数据
let s2 = s1; // 不是复制栈上的对象!而是把值的所有权移动给新对象,s1从此失效
println!("{}, world!", s1); // 错误!s1 已经失效
复制对象(错误的)移动对象

克隆

真的把值复制一份,两个变量绑定两个独立的值。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}", s1, s2);
}

所有权转换实战

传参时的所有权

fn main() {
    let s = String::from("hello");
    // s 被声明有效

    takes_ownership(s);
    // s 的值被当作参数传入函数
    // 所以可以当作 s 已经被移动,从这里开始已经无效

    let x = 5;
    // x 被声明有效

    makes_copy(x);
    // x 的值被当作参数传入函数
    // 但 x 是基本类型,依然有效
    // 在这里依然可以使用 x 却不能使用 s
} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放

fn takes_ownership(some_string: String) {
    // 一个 String 参数 some_string 传入,有效
    println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放

fn makes_copy(some_integer: i32) {
    // 一个 i32 参数 some_integer 传入,有效
    println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放

函数返回值的所有权

fn main() {
    let s1 = gives_ownership();
    // gives_ownership 移动它的返回值到 s1

    let s2 = String::from("hello");
    // s2 被声明有效

    let s3 = takes_and_gives_back(s2);
    // s2 被当作参数移动, s3 获得返回值所有权
} // s3 无效被释放, s2 被移动, s1 无效被释放.

fn gives_ownership() -> String {
    let some_string = String::from("hello");
    // some_string 被声明有效

    return some_string;
    // some_string 被当作返回值移动出函数
}

fn takes_and_gives_back(a_string: String) -> String {
    // a_string 被声明有效

    a_string // a_string 被当作返回值移出函数
}

引用和租借

引用可以看作是指针。&可以取变量的引用,当一个变量的值被引用时,变量本身不会被认定无效。因为引用并没有在栈中复制变量的值。

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 is {}, s2 is {}", s1, s2);
}

也可以函数传参:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

原变量被转移,引用自动失效:

// 转移带来的引用失效
fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s2); // 错误!因为s1已经转移给s3,原s2引用失效
}
// 解决引用失效
fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1; // 声明s2是可变的
    let s3 = s1;
    s2 = &s3; // 重新从 s3 租借所有权
    println!("{}", s2);
}

租借方默认无权修改变量,除非使用可变引用:

// 租客修改原变量
fn main() {
    let s1 = String::from("run");
    let s2 = &s1;
    println!("{}", s2);
    s2.push_str("oob"); // 错误!禁止修改租借的值
    println!("{}", s2);
}
// 解决租客无权修改
fn main() {
    let mut s1 = String::from("run");
    // s1 是可变的

    let s2 = &mut s1;
    // s2 是可变的引用

    s2.push_str("oob");
    println!("{}", s2);
}

可变引用只能借一次,不可变引用可以借多次。

Rust 对可变引用的这种设计主要出于对并发状态下发生数据访问碰撞的考虑,在编译阶段就避免了这种事情的发生。由于发生数据访问碰撞的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。

// 有了可变引用,不允许再有其他引用
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s; // 错!不许再次借出
    let r3 = &s; // 错!不许再次借出

    println!("{}, {}, {}", r1, r2, r3);
}

// 不可变引用不受限制
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = & s;
    let r3 = &s;

    println!("{}, {}, {}", r1, r2, r3);
}

当一个借用指向的对象被释放时,如果这个借用还存在,就会出现悬垂引用的问题。rust编译器会禁止悬垂引用:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s // 错!s在函数结束后失效
}

组织管理

rust有三种组织概念,箱(crate)、包(package)和模块(mudule)。从包含关系来看:包 > 箱 > 模块。

箱(crate)

箱有两种形式:可以编译出二进制的文件,或者是库文件。箱存在于“包”中。

包(package)

cargo new出来的工程就是一个包。每个包有一个Cargo.toml文件,里面声明了包名、箱和依赖。看个例子:

image.png

每一个包可以有多个箱,但是仅允许存在一个库箱。src/lib.rs和src/main.rs两个文件无需额外的声明,他们是与包同名的库箱和二进制箱。此外,src/bin目录里面的rs文件也无需声明,cargo约定他们是二进制箱。

模块(module)

Rust语言的模块是一种代码组织方式,可以将代码划分为独立的、可重用的单元,以实现代码的模块化和封装。模块可以包含许多不同的项(例如结构体、枚举、函数和常量等),这些项可以在模块外部使用,也可以在模块内部使用。模块可以嵌套,形成层次结构。

在Rust中,模块形成了Crate的基础,每个Crate都有至少一个模块,任何项都必须定义在一个模块中。模块可以使用关键字mod来定义,这样就可以将一个文件作为一个独立的模块,也可以在一个文件中定义多个模块。模块之间可以使用关键字use来引用,从而使得一个模块可以使用其他模块中的项。

在lib.rs中定义模块(lib.rs是一个库箱):

pub mod root_mod { // public根模块
    pub mod mod1 { // public模块
        pub fn tool1() { // 模块内public函数
            crate::root_mod::mod1::internal();
        }
        fn internal() { // 模块内私有(private)函数
            println!("root->mod->internal");
        }
    }
}

使用use引用其他文件的模块:

use modlib::root_mod; // 箱::模块名

fn main(){
    root_mod::mod1::tool1();
}

待续...

  • 枚举
  • attribute
  • 面向对象
  • 泛型
  • 错误处理
  • 集合/标准库
  • 并发编程
  • 性能探索