Rust语言入门

目录

  • 一、Rust概述
  • 二、环境搭建
    • [2.1 Windows 系统安装步骤](#2.1 Windows 系统安装步骤)
    • [2.2 macOS 系统安装步骤](#2.2 macOS 系统安装步骤)
    • [2.3 Linux 系统安装步骤](#2.3 Linux 系统安装步骤)
    • [2.4 安装过程常见问题及解决方案](#2.4 安装过程常见问题及解决方案)
  • 三、入门指南
    • [3.1 hello world](#3.1 hello world)
    • [3.2 包管理器和构建工具------Cargo](#3.2 包管理器和构建工具——Cargo)
    • [3.3 简单demo入门------猜数字游戏](#3.3 简单demo入门——猜数字游戏)
  • 四、基础语法知识
    • [4.1 变量与可变性](#4.1 变量与可变性)
    • [4.2 变量遮蔽](#4.2 变量遮蔽)
    • [4.3 数据类型](#4.3 数据类型)
      • [4.3.1 标量类型](#4.3.1 标量类型)
        • [4.3.1.1 整数类型](#4.3.1.1 整数类型)
        • [4.3.1.2 浮点类型](#4.3.1.2 浮点类型)
        • [4.3.1.3 布尔类型](#4.3.1.3 布尔类型)
        • [4.3.1.4 字符类型](#4.3.1.4 字符类型)
      • [4.3.2 复合类型](#4.3.2 复合类型)
        • [4.3.2.1 元组类型](#4.3.2.1 元组类型)
        • [4.3.2.2 数组类型](#4.3.2.2 数组类型)
    • [4.4 函数](#4.4 函数)
  • 五、所有权(Ownership)
    • [5.1 所有权规则](#5.1 所有权规则)
    • [5.2 可变范围](#5.2 可变范围)
    • [5.3 内存和分配](#5.3 内存和分配)
    • [5.4 变量与数据交互的方式](#5.4 变量与数据交互的方式)
      • [5.4.1 移动(Move)](#5.4.1 移动(Move))
      • [5.4.2 克隆(Clone)](#5.4.2 克隆(Clone))
      • [5.4.3 涉及函数的所有权机制](#5.4.3 涉及函数的所有权机制)
      • [5.4.4 函数返回值的所有权机制](#5.4.4 函数返回值的所有权机制)
      • [5.4.5 引用(Reference)](#5.4.5 引用(Reference))
      • [5.4.6 租借(Borrow)](#5.4.6 租借(Borrow))
      • [5.4.7 垂悬引用(Dangling References)](#5.4.7 垂悬引用(Dangling References))
  • 六、参考资料

由于之前没听过Rust语言,故记录一下从0到1的学习过程,帮助需要学习Rust语言入门的同学,Rust语言定义可参看Rust语言(百度百科),于2015年发布稳定版本,是一个专注于并发安全的语言

一、Rust概述

Rust 是一种系统级编程语言,由 Mozilla 研究院开发,专注于安全性、并发性和性能。其设计目标是为开发者提供接近 C/C++ 的运行效率,同时避免内存安全漏洞(如缓冲区溢出、空指针解引用等)。Rust 通过所有权(ownership)、借用(borrowing)和生命周期(lifetimes)等机制,在编译阶段即可检测出大多数内存错误,无需依赖垃圾回收(GC)。

官方网站:rust-lang.org

Rust特点

  • 内存安全无需牺牲性能:Rust 的编译时检查机制(如所有权系统)可消除数据竞争和内存安全问题,而无需运行时开销。这使得它适合开发操作系统、游戏引擎、高频交易系统等对性能要求苛刻的领域。

  • 高效的并发编程:Rust 的并发模型基于"无畏并发"(fearless concurrency),通过类型系统和所有权规则,编译器能直接阻止数据竞争问题。开发者可以编写安全的并发代码,无需依赖复杂的运行时检查。

  • 跨平台与嵌入式支持:Rust 支持交叉编译,可轻松生成针对不同平台的二进制文件。其对嵌入式系统的支持(如无标准库模式、精确内存控制)使其在物联网(IoT)和微控制器开发中表现优异。

  • 活跃的生态系统:Rust 拥有强大的工具链(如包管理器 Cargo、格式化工具 rustfmt)和丰富的开源库(crates.io)。社区注重文档质量,官方提供的《Rust 程序设计语言》(The Book)和标准库文档均为学习提供了便利。

适用场景

  • 需要高性能且安全的系统软件(如操作系统、数据库)。
  • 高并发服务(如网络服务器、分布式系统)。
  • 资源受限的嵌入式开发。
  • 对现有 C/C++ 代码库进行渐进式安全重构。

行业应用

  • 微软:用于 Windows 系统组件的安全重构。
  • 亚马逊:AWS 的底层服务(如 Lambda、S3)部分采用 Rust。
  • 区块链项目:Solana、Polkadot 等使用 Rust 开发核心协议。

二、环境搭建

2.1 Windows 系统安装步骤

  • 下载安装exe安装程序:rust-lang.org/tools/install
  • 运行安装程序:rustup-init.exe,按照安装向导提示进行操作。在安装过程中,选择默认的安装选项。安装程序会自动配置系统环境变量,将 Rust 的二进制文件路径添加到PATH中。不过,有时可能需要手动重启终端才能使环境变量生效。
  • 验证安装:打开命令提示符或 PowerShell,输入rustc --version,如果安装成功,你将看到类似于rustc x.y.z (abcabcabc yyyy-mm-dd)的版本信息输出,其中x.y.z是具体的版本号。

2.2 macOS 系统安装步骤

  • 打开终端:在 "应用程序" 文件夹中找到 "终端" 并打开。

  • 运行安装脚本:在终端中执行以下命令来下载并运行 Rust 安装脚本:
    curl https://sh.rustup.rs -sSf | sh

    这个命令会下载一个安装脚本并自动运行,安装过程中如果权限不够可以在命令前面添加sudo并输入密码。另外Rust支持自动添加环境变量到Path,下面直接按照默认的方式安装(直接按enter键)

  • 配置环境变量:安装完成后,需要将 Rust 的二进制文件路径添加到PATH环境变量中。在终端中执行以下命令使配置立即生效:source $HOME/.cargo/env

    如果你希望每次打开终端时都自动加载这个环境变量,可以将上述命令添加到你的.bashrc或.zshrc文件中。

  • 验证安装:在终端中输入rustc --version,若安装成功,会显示 Rust 的版本信息。

2.3 Linux 系统安装步骤

不同的 Linux 发行版安装步骤略有差异,但总体思路一致,这里以 Ubuntu 为例:

  • 打开终端:通过快捷键或在应用程序列表中找到终端并打开。
  • 运行安装脚本:执行以下命令下载并运行 Rust 安装脚本:curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh,按照提示选择默认安装选项。
  • 安装 C 编译器(可选):如果你的系统中没有安装 C 编译器,某些 Rust 项目在编译时可能会出错。在 Ubuntu 上,可以通过以下命令安装 GCC 编译器:sudo apt-get install build-essential
  • 配置环境变量:和 macOS 一样,安装完成后需要将 Rust 的二进制文件路径添加到PATH中,执行:source $HOME/.cargo/env,为了每次打开终端都自动加载环境变量,将上述命令添加到.bashrc.zshrc文件中。
  • 验证安装:在终端输入`rustc --version,检查是否安装成功并显示版本信息。

2.4 安装过程常见问题及解决方案

网络问题导致安装失败

如果没有翻墙,可以参看我之前的文章,使用国内镜像站

在 Linux 和 macOS 系统中,可以在运行安装脚本前设置环境变量:

复制代码
export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

command not found

安装时,需要确保有必要的依赖包,如 Python、curl、gnu 等。请确保系统已安装这些依赖项。可以使用下面命令检查是否已经安装ok,

在 Ubuntu/mac 上,可以使用以下命令安装:
sudo apt-get install python3 curl,同时,安装完后检查系统环境变量是否正常,可以使用echo系统命令echo $PATH输出环境变量配置,如下图

编译器环境缺失

在编译 Rust 代码时,可能会出现编译器环境缺失的错误。这通常是因为缺少 C 编译器,如 GCC 或 MSVC。对于 Windows 系统,如果遇到 "note: the MSVC targets depend on the MSVC linker but link.exe was not found" 错误,确保系统中已经安装了 MSVC 工具链。可以通过安装 Microsoft Visual Studio 或者独立的 Build Tools 来获取它,安装时确保勾选 C++ 组件。也可以使用以下命令安装 Rust 编译器的 GNU 版本:

rustup install stable-x86_64-pc-windows-gnu

对于 Linux 系统,可以使用包管理器安装 GCC,如在 Ubuntu 上:

sudo apt-get install build-essential

三、入门指南

3.1 hello world

输入下面命令,在指定路径下创建rust目录,新建一个hello_world项目目录,再创建一个main.rs文件,

bash 复制代码
mkdir rust
cd rust
mkdir hello_world
cd hello_world
touch main.rs

使用vi命令编辑文件,输入下面main函数打印,

rust 复制代码
fn main() {
    println!("Hello, world!");
}

使用rustc命令对main.rs文件进行编译,如下图,再输入./main执行(注意在 Windows 系统中,请输入命令.\main而不是./main

另外,如果你想直接用在线编辑器测试一下也可以,play.rust-lang.org/?version=stable&mode=debug&edition=2024

3.2 包管理器和构建工具------Cargo

如果是按照官方的安装包进行的安装,Cargo是随Rust一起安装的,在终端中输入cargo --version检查是否已安装,

创建一个hello_cargo的新项目,可以看到通过cargo命令创建的项目目录是初始化了一个新的git本地仓库,

bash 复制代码
cargo new hello_cargo
cd hello_cargo

cargo new命令会生成下面两个东西:

  • Cargo.toml 文件,TOML(Tom's Obvious, Minimal Language)格式的文件是 Cargo 的配置格式;
  • 包含main.rs文件的src目录;

其中,

  • package\]:节标题,表明接下来的语句正在配置一个包,里面包含Cargo编译时的基本信息:程序名称、版本以及要使用的 Rust 版本;

打开src/main.rs 文件,发现Cargo 已经为你生成了一个"Hello, world!"程序

如果你创建的项目没有使用 Cargo,你可以将项目代码移动到src目录,并创建一个相应的Cargo.toml文件,使用cargo install cargo.toml将自动为你创建该文件。

输入cargo build进行构建,会在我的目录下(/kaizhang/workspace/rust/hello_cargo)目录中新增一个Cargo.lock(该文件记录了项目中依赖项的确切版本)和target目录(target/debug/hello_cargo为前面编译生成的可执行文件)

我们也可以直接通过cargo run命令编译然后使用生成的执行文件,注意这里Cargo 检测到文件没有更改,因此没有重新构建,而是直接运行了二进制文件。

如果您修改了源代码,Cargo 会在运行项目之前重新构建项目,

另外cargo check命令可以快速检查您的代码,确保其能够编译,但不会生成可执行文件,

3.3 简单demo入门------猜数字游戏

工作原理:程序会生成一个介于 1 到 100 之间的随机整数,然后提示玩家输入猜测的数字。玩家输入后,程序会判断猜测的数字是偏小还是偏大。如果猜测正确,游戏会打印一条祝贺信息并退出。

建立新项目

bash 复制代码
cargo new guessing_game
cd guessing_game

使用vi命令编辑main.rs文件,输入:%d全删原来的数据,然后粘贴下面的代码,比如下面use std::io使用标准库中的io库

c 复制代码
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

如果直接构建的话,会报错无法解析rand模块,

所以需要编辑将依赖的版本信息添加到Cargo.toml文件

json 复制代码
[dependencies]
rand = "0.8.5"

最后效果如下,系统随机生成一个1~100的随机数,你输入你猜测的数字,它会提示你太大或者太小,只有相等才会输出you win

有其他语法基础,看rust代码应该也不会很难,主要是需要熟悉对应的库函数:比如array

四、基础语法知识

与其他语言类似, Rust 语言也拥有一组仅供该语言内部使用的关键字(比如Java的package,import等)。请注意,您不能将这些关键字用作变量名或函数名。大多数关键字都有特殊含义,您将在 Rust 程序中使用它们来执行各种任务;少数关键字目前没有关联的功能,但它们已被保留,用于未来可能添加到 Rust 中的功能。比如前面小程序里的useletfnloop等关键字,

基础关键字

  • as:执行原始类型转换,消除包含项目的特定特性歧义,或重命名use语句中的项目
  • async:返回一个 aFuture而不是阻塞当前线程
  • await:暂停执行,直到 a 的结果Future准备就绪
  • break:立即退出循环
  • const:定义常量项或常量原始指针
  • continue:继续进行下一次循环迭代
  • crate:在模块路径中,指的是 crate 根目录
  • dyn:动态分发到 trait 对象
  • else:回退机制if和if let控制流结构
  • enum:定义枚举
  • extern:链接外部函数或变量
  • false:布尔假字面值
  • fn:定义函数或函数指针类型
  • for:遍历迭代器中的元素、实现特性或指定更高级别的生命周期
  • if:根据条件表达式的结果进行分支
  • impl:实现固有或特征功能
  • infor:循环语法的一部分
  • let:绑定变量
  • loop:无条件循环
  • match:将值与模式匹配
  • mod:定义一个模块
  • move:使闭环对其所有捕获物拥有所有权
  • mut:表示引用、原始指针或模式绑定中的可变性
  • pub:表示结构体字段、impl代码块或模块中的公共可见性
  • ref:通过引用绑定
  • return:从函数返回
  • Self:我们正在定义或实现的类型的类型别名
  • self:方法主题或当前模块
  • static:全局变量或生命周期贯穿整个程序执行过程
  • struct:定义结构
  • super:当前模块的父模块
  • trait:定义一个特征
  • true:布尔真字面值
  • type:定义类型别名或关联类型
  • union:定义联合体;仅在联合体声明中使用时才是一个关键字
  • unsafe:表示不安全的代码、函数、特性或实现
  • use:将符号纳入作用域;为通用和生命周期边界指定精确捕获。
  • where:表示约束类型的子句
  • while:根据表达式的结果进行条件循环

关键字预留作将来使用 ,以下关键字目前尚无任何功能,但 Rust 已将其保留以备将来可能使用。比如Java中gotoconstnull

  • abstract
  • become
  • box
  • do
  • final
  • gen
  • macro
  • override
  • priv
  • try
  • typeof
  • unsized
  • virtual
  • yield

4.1 变量与可变性

默认情况下,变量是不可变的。这是 Rust 鼓励你以充分利用其安全性和易于并发的方式编写代码的众多机制之一。不过,你仍然可以选择使变量可变。接下来,我们将探讨 Rust 如何以及为何鼓励你使用不可变性,以及为何有时你可能需要选择可变性。

创建一个名为variables的新项目。cargo new variables

编辑src/main.rs文件

rust 复制代码
fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

保存并运行程序cargo run。您应该会收到一条关于不可变性错误的错误消息,如下所示:

收到此错误消息cannot assign twice to immutable variable 'x'是因为我们试图为不可变变量赋第二个值x。但可变性非常有用,可以让代码编写起来更方便。虽然变量默认是不可变的,但我们可以添加关键字mut

rust 复制代码
fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

添加mut关键字后,x是可变变量了,

4.2 变量遮蔽

可以重复使用let关键字来对相同的变量名进行"遮蔽"。

rust 复制代码
fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}

花括号创建的内部作用域内,第三条语句也对进行遮蔽,并创建一个新变量,将之前的值乘以2,当该作用域结束时,内部遮蔽结束,变量恢复为6。

当我们let再次使用关键字时,实际上是创建了一个新变量,因此我们可以更改值的类型,但重用相同的名称。那假设遮蔽的是不同的类型,比如第一个spaces变量是字符串类型,第二个spaces变量是数字类型,

rust 复制代码
fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
}

那么编译时会报错提示我们无权更改变量的类型,

4.3 数据类型

Rust 中的每个值都具有特定的数据类型,这告诉 Rust 所指定的数据类型,以便它知道如何处理这些数据。我们将介绍两种数据类型子集:标量和复合。let有点类似于Java的var关键字,但是你去定义变量时一定要清楚它的数据类型,所以Java中一般还是建议定义清楚封装类型,

下面先看一个例子,比如下面定义guess为u32的32位无符号整型(基础数据类型),将42字符串转换为整形,

rust 复制代码
let guess: u32 = "42".parse().expect("Not a number!");

其中parse用于将字符串解析为特定的数据类型。这个方法通常是通过为类型实现 std::str::FromStr trait 来提供的。当字符串的格式符合预期的数据类型时,parse 方法能够安全地转换字符串为该类型。如果字符串不符合格式要求,parse 方法会返回一个 Result 类型,其中包含一个错误信息(Err),或者转换成功的值(Ok),下面是其定义:

rust 复制代码
	pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
        FromStr::from_str(self)
    }

如果上面示例代码不加: u32,即不定义类型,会有下面的报错,

这个错误可以使用rustc --explain E0284查看更多信息,

4.3.1 标量类型

标量类型表示单个值。Rust 有四种主要的标量类型:整数、浮点数、布尔值和字符。

4.3.1.1 整数类型

整数是不包含小数部分的数字。有符号数使用二进制补码表示法存储。

长度(Length) 有符号(Signed) 无符号(Unsigned)
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
架构相关类型(architecture dependent) isize usize

整型字面量(Integer Literals in Rust)

数字字面量 例子
小数(Decimal) 98_222
十六进制(Hex) 0xff
八进制(Octal) 0o77
二进制(Binary) 0b1111_0000
字节(Byte,仅限u8) b'A'

整数溢出

假设你有一个类型为 int 的变量u8,其值范围为 0 到 255。如果你尝试将该变量的值更改为超出此范围的值,例如 256,则会发生整数溢出,这可能导致两种情况之一。在调试模式下编译时,Rust 会检查整数溢出,如果发生这种情况,程序会在运行时崩溃。Rust 使用术语"panicking "来描述程序因错误而退出的情况;我们将在第 9 章的"使用 int 处理不可恢复的错误 panic!"部分更深入地讨论 panic。

rust 复制代码
fn main() {
    let x = 2.0; // f64
    println!("The value of x is: {x}");
    
    let y: f32 = 3.0; // f32
    println!("The value of y is: {y}");
    
    let u: u8 = 256;
}

比如上面的代码,就会报错overflowing_literalsu8范围是0..=255

4.3.1.2 浮点类型

浮点数按照 IEEE-754 标准表示,

rust 复制代码
fn main() {
    let num = 3.14159;
    let num_str = format!("{}", num);
    println!("浮点数: {}", num_str);
}

前面的代码示例中,整数类型中如果直接打印输出小数点后的精度展示不出来,使用format函数(本质上是转换成字符串)才可以正确的打印出结果,保留精度,

4.3.1.3 布尔类型

与大多数其他编程语言一样,Rust 中的布尔类型只有两个可能的值:true真和假false。布尔值占用一个字节。Rust 中的布尔类型使用 boolean 来指定bool。例如:

rust 复制代码
fn main() {
    let t = true;
    println!("布尔类型真: {}", t);
    
    let f: bool = false; // with explicit type annotation
    println!("布尔类型假: {}", f);
}
4.3.1.4 字符类型

Rust 的char类型是该语言最原始的字母类型,char类型大小为 4 个字节,表示一个 Unicode 标量值,这意味着它可以表示的内容远不止 ASCII。带重音符号的字母、中文、日文和韩文字符、表情符号在 Rust 中都是有效的值。声明方式如下,

rust 复制代码
fn main() {
    let c = 'z';
    println!("字符类型: {}", c);
    let z: char = 'ℤ'; // with explicit type annotation
    println!("字符类型: {}", z);
    let heart_eyed_cat = '😻';
    println!("字符类型: {}", heart_eyed_cat);
}

比如上面的表情符号也可以表示出来,因为使用字符串可以存储UTF-8编码文本

4.3.2 复合类型

复合类型可以将多个值组合成一个类型。Rust 有两种基本复合类型:元组和数组。

4.3.2.1 元组类型

元组是一种将多个不同类型的值组合成一个复合类型的通用方法。元组的长度是固定的:一旦声明,其大小就不能增加或减少。

下面的示例中,变量tup绑定到整个元组,因为元组被视为一个单一的复合元素。要从元组中获取各个值,我们可以使用模式匹配来解构元组值,

rust 复制代码
fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

还可以通过使用句点 ( .) 后跟要访问的值的索引来直接访问元组元素。

rust 复制代码
fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;

    println!("The value of five_hundred is: {five_hundred}");
}
4.3.2.2 数组类型

与元组不同,数组中的每个元素必须具有相同的类型。

rust 复制代码
fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];

    println!("The value of first is: {first}");
}

可以使用索引访问数组元素,

4.4 函数

函数在 Rust 代码中非常普遍。你已经见过该语言中最重要的函数之一:main函数,它是许多程序的入口点。Rust 代码使用蛇形命名法作为函数和变量名的规范风格,其中所有字母均为小写,单词之间用下划线分隔。

rust 复制代码
fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

基本上和其他语言类似,主要是需要熟悉语法规则,详细的请看官网:function,注释和控制流在不在这里赘述了,直接看官网。

五、所有权(Ownership)

所有权是一套规则,用于管理 Rust 程序的内存使用方式。所有程序在运行时都必须管理其对计算机内存的使用。有些语言有垃圾回收机制,会在程序运行时定期检查不再使用的内存;而另一些语言则要求程序员显式地分配和释放内存。Rust 采用了第三种方法:通过所有权系统来管理内存,该系统包含一系列编译器会检查的规则。如果违反了任何规则,程序将无法编译。所有权机制的任何特性都不会降低程序的运行速度。

前面是基础,此小节是重点,

5.1 所有权规则

划重点划重点划重点,所有权有以下三条规则

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

5.2 可变范围

变量范围是变量的一个属性,其代表变量的可行域,默认从声明变量开始有效直到变量所在域结束。

rust 复制代码
fn main() {
    {
    // 在声明以前,变量 s 无效
    let s = "hello";
    // 这里是变量 s 的可用范围
    }
    // 变量范围已经结束,变量 s 无效
    println!("{s}");
}

比如下面在主函数的大括号内声明的变量s,超过作用域则无效,

5.3 内存和分配

对于字符串字面量,我们在编译时就知道其内容,因此文本直接硬编码到最终可执行文件中。这就是字符串字面量快速高效的原因。但这些特性仅仅源于字符串字面量的不可变性。遗憾的是,我们无法为每一段在编译时大小未知且在程序运行过程中大小可能发生变化的文本都分配一块内存到二进制文件中。

String为了支持可变、可扩展的文本内容,我们需要在堆上分配一定量的内存(编译时未知)来保存其内容。这意味着我们需要做下面两件事情:

  • 1.在运行时向内存分配器请求内存
  • 2.操作后将这部分内存归还给分配器

比如下面这段代码,String::from会申请所需内存,当s变量超出作用域时。当变量超出作用域时,Rust 会为我们调用一个特殊的函数。这个函数叫做 drop,对应c++的析构函数,

rust 复制代码
fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }                                  // this scope is now over, and s is no
                                       // longer valid
}

5.4 变量与数据交互的方式

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

5.4.1 移动(Move)

多个变量可以在 Rust 中以不同的方式与相同的数据交互:

rust 复制代码
fn main() {
    let x = 5;
    let y = x;
    println!("the value of x is {}", x);
    println!("the value of y is {}", y);
}

这个程序将值 5 绑定到变量 x,然后将 x 的值复制并赋值给变量 y。这是基础数据类型,且不需要存储到堆中,仅在栈中的数据的"移动"方式是直接复制。

采用直接复制的方式移动的基础数据类型有下面这些:

  • 所有整数类型,例如 i32 、 u32 、 i64 等。
  • 布尔类型 bool,值为 true 或 false 。
  • 所有浮点类型,f32 和 f64。
  • 字符类型 char。
  • 仅包含以上类型数据的元组(Tuples)。

如果是字符串类型,则完全不一样,

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; 
    println!("{}, world!", s1); // 错误!s1 已经失效
}

当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 和 s2 都被释放的话堆区中的 "hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。

Rust will never automatically create "deep" copies of your data.

其中s1变量的字符串"hello"其实是有三部分组成,这三部分是存在栈上,数据"hello"是存在堆上,

  • 指向存储字符串内容的指针
  • 长度
  • 容量

5.4.2 克隆(Clone)

Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,那就可以用clone函数,

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

本质上就是下面这种效果,

5.4.3 涉及函数的所有权机制

如果将一个变量当作函数的参数传给其他函数,怎样安全的处理所有权呢?下面这段程序描述了这种情况下所有权机制的运行原理:

rust 复制代码
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 是基本类型, 无需释放

如果将变量当作参数传入函数,那么它和移动的效果是一样的。

5.4.4 函数返回值的所有权机制

返回值也可以转移所有权。被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。

rust 复制代码
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 被当作返回值移出函数
}

5.4.5 引用(Reference)

引用(Reference)是 C++ 开发者较为熟悉的概念。如果你熟悉指针的概念,你可以把它看作一种指针。实质上"引用"是变量的间接访问方式。

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

变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值,

5.4.6 租借(Borrow)

引用不会获得值的所有权。引用只能租借(Borrow)值的所有权。

引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权:

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s2);
}

s2 租借的 s1 已经将所有权移动到 s3,所以 s2 将无法继续租借使用 s1 的所有权。

如果需要使用 s2 使用该值,必须重新租借:

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s1;
    s2 = &s3; // 重新从 s3 租借所有权
    println!("{}", s2);
}

另外,如果尝试利用租借来的权利来修改数据会被阻止:

rust 复制代码
fn main() {
    let s1 = String::from("run");
    let s2 = &s1;
    println!("{}", s2);
    s2.push_str("oob"); // 错误,禁止修改租借的值
    println!("{}", s2);
}

当然,也存在一种可变的租借方式,就像你租一个房子,如果物业规定房主可以修改房子结构,房主在租借时也在合同中声明赋予你这种权利,你是可以重新装修房子的:

rust 复制代码
fn main() {
    let mut s1 = String::from("hello");
    // s1 是可变的

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

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

可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以:

rust 复制代码
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
}

5.4.7 垂悬引用(Dangling References)

这是一个换了个名字的概念,如果放在有指针概念的编程语言里它就指的是那种没有实际指向一个真正能访问的数据的指针(注意,不一定是空指针,还有可能是已经释放的资源)。它们就像失去悬挂物体的绳子,所以叫"垂悬引用"。下面是垂直引用的示例,

rust 复制代码
fn main() {
    let reference_to_nothing = dangle();
}

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

    &s
}

"垂悬引用"在 Rust 语言里不允许出现,如果有,编译器会发现它。

后面的部分:切片、结构体、枚举、模式匹配、模块管理、错误处理、生命周期、迭代器、闭包、智能指针、无畏并发、异步编程、高级功能在后面章节补充,本章只为入门。

六、参考资料

相关推荐
csdn_wuwt3 小时前
前后端中Dto是什么意思?
开发语言·网络·后端·安全·前端框架·开发
JosieBook3 小时前
【Rust】基于Rust 设计开发nginx运行日志高效分析工具
服务器·网络·rust
print(未来)3 小时前
C++ 与 C# 的性能比较:选择合适的语言进行高效开发
java·开发语言
JosieBook3 小时前
【Rust】 基于Rust 从零构建一个本地 RSS 阅读器
开发语言·后端·rust
云边有个稻草人3 小时前
部分移动(Partial Move)的使用场景:Rust 所有权拆分的精细化实践
开发语言·算法·rust
一晌小贪欢3 小时前
Pandas操作Excel使用手册大全:从基础到精通
开发语言·python·自动化·excel·pandas·办公自动化·python办公
松涛和鸣4 小时前
11.C 语言学习:递归、宏定义、预处理、汉诺塔、Fibonacci 等
linux·c语言·开发语言·学习·算法·排序算法
IT痴者5 小时前
《PerfettoSQL 的通用查询模板》---Android-trace
android·开发语言·python
2501_941111246 小时前
C++与自动驾驶系统
开发语言·c++·算法