杜克大学-Rust-编程基础-全-
杜克大学 Rust 编程基础(全)
001:认识你的课程讲师 🎓

在本节课中,我们将认识本课程的讲师,并了解这门Rust基础课程的目标与结构。

大家好,我是Alfredo Deza,欢迎来到这门Rust课程。我们将探讨Rust编程语言的基础知识。
我们将学习你需要掌握的基础Rust知识。我们的目标不是深入最复杂的Rust特性,而是在课程结束时,应用所学概念来构建一些可操作、实用的项目。这样,你学到的所有概念在开始动手构建时都会变得有意义。用刚学到的知识来构建项目,是无可替代的学习方式。
我拥有超过10年的软件工程经验。
我曾在小型公司、大型公司以及大型企业工作过。在本课程中,你将看到我在这些年软件工程师生涯中学到的一些概念,我会尝试将这些经验融入即将讲解的概念中。
学习一门新的编程语言可能有些令人却步。
但我总是尝试从最实用和务实的角度来教授这些内容。希望你能在课程中感受到,这些概念是细小且易于理解的。你将能够跟上课程内容的节奏。最后,你将能够综合运用所学,从零开始使用Rust编程语言构建项目。
我认为Rust编程语言非常有用。它拥有极快的速度,并具备许多不同的积极特性。它有一个编译器和类型检查器,编译器会为你进行类型检查,捕捉你可能未意识到的错误。与其他一些编程语言不同,它不会让你陷入“Google搜索错误”的境地。此外,它还具备惊人的运行速度和更低的资源消耗。我非常兴奋。
让我们开始学习Rust吧。
本节课中,我们一起认识了讲师,并了解了本Rust基础课程旨在通过实践项目来巩固所学概念,让学习过程更加直观和有意义。
002:关于本课程 📚

在本节课中,我们将介绍本课程的整体结构、你将学习到的内容,以及课程中将使用的主要工具和环境。了解这些信息将帮助你更好地规划学习路径,并充分利用课程资源。
课程内容与结构
本课程将采用多种教学形式来帮助你掌握Rust编程。你将看到概念讲解与实际演示相结合。
以下是课程的主要组成部分:
- 阅读材料:提供额外的阅读资料,帮助你深入探索已学概念。
- 演示环节:通过实际代码演示来讲解核心概念。
- 实验练习:提供大量实验,让你能够应用所学概念,并结合阅读材料与演示内容进行实践。
开发工具与环境
上一节我们介绍了课程内容,本节中我们来看看课程中将使用的主要工具。这些工具旨在提升你的学习效率和编程体验。
我将主要使用 Visual Studio Code 作为文本编辑器。它是一个免费的编辑器。使用它并非绝对要求,但如果你使用它,将能更好地理解我给出的示例,并且更容易完成课程中的文本编辑任务。你可以自由选择任何文本编辑器。
除了编辑器,我们还需要安装 Rust 语言环境。课程中将展示如何将其安装到系统中。
此外,我们还将使用两款高度推荐的GitHub工具。它们同样不是强制要求,但能极大提升学习效率。
以下是这两款工具的介绍:
- GitHub Copilot:这是一款AI编程助手。它能在你编写代码时提供辅助,例如自动补全。课程中将详细解释如何使用并发挥其优势。请注意,它不是完全免费的工具,但学生可以申请使用,非学生也有免费试用期。
- GitHub Codespaces:这是一个云端开发环境。它每月提供免费的额度,每30天重置。Codespaces内置了Visual Studio Code,因此如果你习惯使用VSCode,会感到非常熟悉。你可以在网页上运行它,也可以在本地通过VSCode连接使用。
关于这些工具的具体使用方法,我们将在后续课程中详细说明。
课程特色与总结
本节课中我们一起学习了本课程的概览。本课程的核心特色是提供了大量的实验练习,让你能在实践中巩固概念。同时,课程也将探索使用上述那些可选但高度推荐的工具,以辅助你的学习过程。
通过结合理论讲解、演示、阅读和大量实践,本课程旨在为你打下坚实的Rust编程基础。
003:Rust安装与环境配置概述 🛠️

在本节课中,我们将学习如何为Rust编程设置开发环境。这包括配置文本编辑器、安装必要的扩展插件,以及安装和配置Rust工具链本身。我们将以Visual Studio Code为主要编辑器进行讲解,但其中涉及的概念同样适用于其他编辑器。
环境设置概述
配置开发环境是我们首先要完成的任务。这个过程主要包含两个部分:文本编辑器的配置与Rust本身的安装。
上一节我们介绍了课程的整体目标,本节中我们来看看如何搭建一个高效的Rust开发环境。
文本编辑器配置
我们将重点讲解Visual Studio Code的配置,因为它是我推荐且在本课程中全程使用的编辑器。不过,我们将讨论的许多概念,例如扩展插件的使用,同样可以应用到其他文本编辑器上。
以下是配置文本编辑器时需要完成的几个核心步骤:
- 安装Visual Studio Code编辑器。
- 安装与Rust开发相关的扩展插件,以增强代码补全、语法高亮和调试等功能。
Rust工具链安装
除了文本编辑器,我们还需要安装Rust本身。我们将使用Rust官方的标准工具进行安装和配置。
我们将使用rustup这一官方工具来安装Rust。rustup是Rust的工具链安装器,它能管理多个Rust版本和相关工具。在终端中运行以下命令即可开始安装:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装完成后,你可以通过运行rustc --version来验证安装是否成功。此命令会输出已安装的Rust编译器版本。
环境验证与总结
完成上述步骤后,你的开发环境就已准备就绪。你将能够自信地借助功能强大的文本编辑器开始Rust开发。
本节课中我们一起学习了如何搭建Rust开发环境。我们了解了配置Visual Studio Code编辑器、安装有用的扩展插件,以及通过rustup安装和配置Rust工具链的核心步骤。现在,你的机器已经具备了编写和运行Rust程序的一切条件。
004:安装Rust 🛠️

在本节课中,我们将学习如何在您的计算机上安装Rust编程语言。我们将以Linux系统为例,演示一个简单直接的安装过程,该方法同样适用于Mac系统。如果您使用的是Windows系统,也可以找到相应的安装程序。
概述
安装Rust的过程非常直接。本示例将使用Linux系统进行演示。如果您使用的是Mac电脑,在终端中执行相同的命令同样有效。如果您既不使用Linux也不使用Mac,可以查看其他支持的安装程序,例如为Windows系统下载可执行的Rust安装程序。因此,您有多种不同的选择。
安装步骤
以下是安装Rust的具体步骤。我们将使用一个命令,该命令会下载一个脚本并通过shell执行。
首先,我切换到终端。这里有一个正在运行的终端。让我们先查看一下当前系统的类型。在这个例子中,我运行的是Linux操作系统。为了确认,我可以查看/etc/os-release文件的内容。这里显示是Ubuntu 20.04,这是一个长期支持版本。所以,这是一个基于Debian的Linux操作系统,没有问题。
现在,清空屏幕并粘贴安装命令。命令中使用了curl工具,协议是HTTPS,这没问题。它会使用TLS版本,并带有一些额外的标志。这个命令将下载脚本并执行文件。如果您不熟悉管道符|,它的作用是获取内容然后执行它们。现在,我按下回车键。
接下来会发生的是,安装程序被下载,并且我看到了几个不同的选项。让我向上滚动一点。我看到了“欢迎使用Rust”的提示。安装程序会为我动态创建一些东西,例如像cargo这样的实用工具会有其单独的目录。我们稍后会深入探讨cargo是什么以及它如何工作。但这个安装过程会设置好一切,非常方便。一些将被更新的内容会放在这些配置文件中,这些文件将帮助我为安装的工具获取正确的路径。
让我们继续查看选项。在Linux上,这将安装默认版本的Rust。默认配置文件是没问题的。修改PATH环境变量吗?是的,我希望修改PATH变量。这意味着我终端中可用的命令将被更新,以包含Rust及其所有其他工具。
所以,我选择继续并完成安装。在绝大多数情况下,这都是您想要使用的选项。在某些特殊情况下,您可能希望进行一些自定义安装和调整,这当然也可以。但对于本课程,我们将继续进行常规安装。
我输入1然后按回车。这需要一点时间,让我们等待它完成。很好,Rust现在已经安装完成,一切似乎都成功完成了。
验证安装
那么,如何验证安装是否成功呢?您可能想做的第一件事就是输入rustc命令。但如果您立即这样做,可能会得到“命令未找到”的错误。这是为什么呢?因为我还没有更新我的PATH环境变量。
如果我执行echo $PATH,输出会很长,因为它指向了许多包含可执行文件的不同路径,但这些路径中没有一个包含实际存放Rust二进制文件和所有额外工具的路径。
因此,我需要运行source $HOME/.cargo/env这个命令。让我们执行它。
现在,如果我再次输入rustc,您将看到完整的帮助菜单。如果我执行which rustc来查看这个命令来自哪里,您会看到它来自我主目录下的.cargo/bin目录。了解这一点很有用,这也是我们验证Rust可执行文件位置的方式。
我向上滚动,可以看到我得到了帮助菜单以及rustc的许多不同选项。其他工具也已经被安装,例如cargo。所以,如果我输入which cargo,它会指向我主目录下的.cargo子目录。如果我输入cargo --help,清屏后再运行一次,我们也会得到许多输出选项。
总结

总而言之,这是一种非常直接的安装Rust的方法,非常简洁,非常容易遵循,特别是如果您不进行额外调整,只是按照提示操作。请记住,我必须在命令行中调用source命令,这确保了Rust工具链中安装的可执行文件在我的系统中可用。
本节课中,我们一起学习了如何通过官方脚本在Linux/Mac系统上安装Rust,如何通过修改PATH环境变量使命令生效,以及如何使用rustc和cargo --version等命令来验证安装是否成功。
005:Visual Studio Code 🛠️

在本节课中,我们将学习如何设置和使用 Visual Studio Code 作为 Rust 开发的主要编辑器。我们将介绍其核心界面、关键功能以及如何配置以优化 Rust 编程体验。
虽然你几乎可以使用任何支持 Rust 的文本编辑器,但本课程将主要使用 Visual Studio Code。大部分示例都将在此编辑器中演示。它非常方便,几乎支持所有主流操作系统,并且拥有良好的社区支持。因此,本课程中使用的所有工具和配置都将围绕 Visual Studio Code 展开。如图所示,它会自动检测操作系统(例如 Mac),但同样为 Windows 和 Linux 用户提供了相应版本,这为开发者提供了灵活性。安装源可能不同,但最终环境是一致的,所以无论你使用哪个系统,所有示例、扩展和配置都将正常工作。
这就是 Visual Studio Code。接下来,让我们深入了解它的一些核心组件。
界面概览与基本操作
我已经启动了 Visual Studio Code,这是你初次打开时可能看到的界面。如果你从未使用过它,本节将快速介绍其布局以及我们将如何使用它进行 Rust 开发。
我在这里有一些代码,这是一个 Rust 的 CI 示例。文件内容本身不重要,重要的是理解界面。左侧是资源管理器,这里列出了工作空间中的所有文件。通常,你的项目文件都会在这里。你看到的是欢迎页面,在底部有一个“启动时显示欢迎页面”的选项。如果你取消勾选,下次启动时就不会再显示。
欢迎页面提供了快速概览和一些有用的入门指南。如果你从未使用过 Visual Studio Code,我强烈建议你浏览这些指南并尝试操作。不过,对于我们的 Rust 学习目标,从 Jupyter 笔记本开始可能不那么相关,但了解 Visual Studio Code 的基础知识仍然很有价值。
好的,我关闭了欢迎页面。界面变空了,但我仍然有示例项目。任何时候你想关闭这些面板,只需点击产生它的图标即可。例如,点击资源管理器图标可以隐藏或显示文件列表。
如果我点击这些文件,比如 README.md,它会在编辑区打开。注意,它的标签是斜体,这表示它是临时打开的。这意味着如果我打开 LICENSE 文件,它会替换掉 README.md。如果我希望文件固定打开,可以双击标签页,这样它就不再是斜体,变成了永久标签。此时再打开 LICENSE,它会作为一个新标签页打开。如果你曾因文件被替换而感到困扰,现在你知道如何解决了。
核心功能面板
除了资源管理器,界面左侧还有其他重要图标:
- 搜索:你可以在这里搜索项目中的文件。虽然我们不会使用大量文件,但这个功能很有用。
- 源代码管理:如果你在系统中安装了 Git,这里会显示你的版本控制状态。我强烈建议你预先安装并配置好 Git。虽然本课程不深入讲解 Git,但它是现代开发的必备工具。
- 运行和调试:这是我们后续会详细讲解的功能。你可以在这里运行和调试 Rust 应用程序。通常需要为项目创建一个自定义的
launch.json配置文件,里面包含了如何启动应用并进入调试模式的指令。
扩展:增强你的编辑器
接下来,我想重点介绍扩展。这是 Visual Studio Code 最强大的功能之一,你可以通过安装扩展来极大地增强编辑器的能力,优化 Rust 开发体验。
在扩展面板中,你可以搜索和安装各种工具。如图所示,我已经安装了许多扩展。例如,如果我搜索“license”,会出现像“Choose a License”或“License Injector”这样的扩展。点击任何一个,你会看到详细的扩展信息页面,这相当于 Visual Studio Code 扩展市场的网站版。你可以查看更新日志、详细信息、发布信息以及使用文档。
安装更多扩展后,你可能会在活动栏看到新的图标出现。例如,我安装了 Azure、GitHub、远程资源管理器等相关扩展,它们为我提供了更多功能。虽然我们可能不会用到所有高级功能,但扩展、运行和调试以及源代码管理是我们将频繁使用的核心部分。
如果我点击源代码管理图标,所有文件的更改都会显示在这里。如果我修改了文件,更改会立即出现。你可以在这里提交更改,当然,你也可以选择在终端中完成这些操作。
命令面板:快速操作的入口
另一个我想展示的重要功能是命令面板。有时我们需要快速更改设置或与特定扩展交互,命令面板就是为此而生的。
在我的系统上,我使用快捷键 Cmd + Shift + P(在 Windows/Linux 上通常是 Ctrl + Shift + P)来打开它。这被称为命令面板。如果你想更改设置或与不同扩展交互,可以在这里进行。你也可以通过菜单栏的“查看”菜单找到“命令面板”选项,旁边会显示对应的快捷键(虽然字体很小)。通过这个面板,你可以快速执行各种命令和修改设置。

本节课中,我们一起学习了如何设置 Visual Studio Code 作为 Rust 开发环境。我们快速浏览了其用户界面,包括资源管理器、搜索、源代码管理和运行调试面板。我们重点介绍了如何通过安装扩展来增强编辑器功能,并演示了如何使用强大的命令面板进行快速操作。掌握这些基础知识,将为后续的 Rust 编程实践打下坚实的基础。
006:启用Rust分析器 🛠️

在本节课中,我们将学习如何在Visual Studio Code中安装和启用Rust分析器(Rust Analyzer)。这是一个强大的扩展,能为Rust开发提供智能代码补全、类型提示和代码导航等功能,极大地提升编程效率。
安装Rust分析器
我们已经安装并打开了Visual Studio Code。接下来,让我们安装Rust分析器扩展。
请转到扩展面板,搜索“rust”。搜索结果可能会非常多,容易让人眼花缭乱。例如,这里显示了大量与Rust相关的结果。我们需要的是名为“rust-analyzer”的扩展。
为了更精确地查找,你可以直接搜索“analyzer”来过滤结果。但毫无疑问,我们需要的扩展就是“rust-analyzer”。我们可以把窗口调大一些,以便更清楚地查看。这个扩展拥有超过170万的安装量和五星好评,这正是我们需要的。
我将点击这个扩展来查看其详情。它提供了一些基本功能,例如代码补全,这是现代代码编辑器应具备的功能。此外,它还提供了一些更高级的、与IDE相关的增强功能。
代码补全功能非常出色,同时它还提供了代码提示和高亮,这些功能运行良好。我个人非常喜欢,并且对于正在学习Rust或希望更精通Rust的开发者来说非常有用的一点是:它可以显示所有定义、类型及其解释说明。尤其是在你将鼠标悬停在代码上时,这些信息会显示出来。在本课程中,我们将全程看到这一点,因为我们会安装并启用Rust分析器。
现在,让我们开始安装。我将点击这里的“安装”按钮。这需要一点时间。安装完成后,一切就基本准备就绪了,但你可能不会立即看到与仅安装编辑器时有太大不同。
验证Rust分析器是否工作
你可能会疑惑:我怎么知道它真的在工作呢?
首先需要了解的是,当前这个扩展标签页显示的是关于Rust的信息,但这本身并不是Rust代码。因此,Rust分析器在这里不会有什么动作。
在开始之前,最重要的一步是确保Rust本身已经安装在你的系统上。否则,这个扩展将无法正常工作,因为它在后台依赖于已安装的Rust环境。这就是为什么我们在进行这些步骤之前,已经提前安装好了Rust。
我将关闭这些标签页。很巧,我现在已经在一个包含Rust代码的项目中。让我打开main.rs文件。你可以看到,Rust分析器在这里出现了,速度非常快。状态栏上立即显示了一些信息。
为什么现在它出现了呢?因为Visual Studio Code足够智能,能够识别出这是一个Rust代码文件,因此会触发相关功能。我们在这里看到的“运行和调试”按钮非常有用,所有这些功能之所以对我们可用,正是因为我们使用了Rust分析器。
现在它已经启用,你可以在这里再次看到它。如果我点击它,它会重新运行,你可以看到它正在索引和处理一些内容。这就是启用它的方法:安装它,并确保它正在运行。
如果我切换到其他文件,Rust分析器会继续保持活动状态。现在它已经理解这是一个Rust项目。
常见问题与提示
在刚开始使用时,如果看不到Rust分析器出现,可能会有点困惑,不明白原因。一个简单有效的方法是:只需点击打开一个Rust代码文件(例如.rs文件),Rust分析器就会被激活并开始工作,之后一切就会正常了。

本节课中我们一起学习了如何在VS Code中安装和配置Rust分析器扩展。我们了解了它的核心功能,如代码补全和类型提示,并掌握了验证其是否正常工作的基本方法。确保Rust环境已安装是扩展生效的前提。现在,你的开发环境已经具备了更强大的Rust编程支持。
007:使用Rust分析器 🛠️

在本节课中,我们将学习如何在实际编程中使用Rust分析器(Rust Analyzer)。我们将通过几个简单的演示,了解它如何提供代码补全、错误诊断和依赖管理等功能,从而提升你的开发效率。
上一节我们介绍了如何安装Rust分析器。本节中,我们来看看它的具体使用方法及其带来的好处。
现在,让我们启动一个项目。如果你之前从未接触过Rust,其中的一些代码语句可能看起来有些奇怪,这完全没关系。我们假设自己对这些代码一无所知。
当我将鼠标悬停在代码上时,会获得一些信息提示。例如,这里显示有9种实现。这提供了额外的上下文信息,非常有用。
如果我点击“实现”,它会告诉我这些功能来自哪里。所有这些功能都是为了给你提供额外的代码上下文,非常实用。
关闭这个窗口后,另一个有用的功能是,当我需要查找某个东西的来源时,可以获得更多上下文。例如,假设我想使用clap库。
以下是自动补全功能的工作方式。你会看到这里有很多选项可供选择,并且当你双击某个选项时,会直接显示其文档和示例。这能为你提供关于想使用功能的额外背景信息。
例如,如果我想使用Arg,它是命令行参数的抽象表示。clap实际上是一个库,一个帮助我们构建命令行工具的包。具体细节目前不重要,但你可以看到,这已经非常酷了,因为我在这里获得了大量有用信息。这一切都得益于Rust分析器。
除了代码补全,另一个非常有用的功能是实时错误提示。
例如,如果我在这里打了一个错字,将help写成大写的HELP并保存,编辑器会立即告诉我这可能有问题,并提供诊断信息。它提示有一个名称相似的方法_help。这背后是Rust分析器在调用rustc等工具,为我提供关于错误的上下文信息。
这是一个很好的错误提示,我知道自己打错了字。让我修正它并保存,红色的下划线就会立刻消失。
如果我想在这里做点别的,比如输入一个未知的值true并保存,我会开始看到多条红色下划线。这是因为情况变得混乱了。
编译器(我们稍后会详细学习编译器)在抱怨,但我不需要切换上下文去运行编译器或其他外部工具来获取这些信息。在编辑器中直接获得反馈,这正是我对一个优秀扩展的期望。在这方面,Rust分析器表现出色,因为我们不一定需要回到终端或命令提示符去执行命令并解析输出。
当然,如果我们想查看详细的输出,可以点击这里的编译器消息。我们将在后面学习如何与编译器消息和错误交互。现在,只需知道Rust分析器非常棒,因为它能帮助我们将代码调整到正常工作状态。
接下来,我想向你展示依赖管理功能。我们转到Cargo.toml文件,这里可以放置项目依赖。
例如,clap是库的名称,后面是它的版本号。如果我将版本改为一个非常旧的版本,比如2.0并保存,你会立即在编辑器右下角看到一些输出。
Rust分析器开始工作,获取元数据、建立索引。突然之间,我的代码中出现了大量红色错误提示。
让我们看看是什么问题。我再次悬停,会看到“未解析的导入”错误。为什么?因为我更改了库的版本,突然之间这些功能都不存在了,我使用的是旧版本的库。
如果我快速将版本改回之前的4.2,一切就会恢复正常。你可以看到依赖正在被拉取,所有东西重新构建,一切又恢复工作了。
显然,Rust分析器拥有大量功能。我刚刚向你展示了其中两三个高层次但非常实用的特性,它们无疑会在你学习Rust的旅程中提供巨大帮助。


再次提醒,现在不必过于担心代码的具体细节,我们将循序渐进地学习。本节课的重点是,我们学会了使用VS Code的Rust分析器扩展,它将帮助我们更高效地进行Rust开发。
008:同步你的设置 🔄

在本节课中,我们将学习如何在不同的 Visual Studio Code 实例之间同步你的设置、配置和扩展。这个功能对于确保你在任何地方工作都能获得一致的开发体验至关重要。
上一节我们介绍了 Visual Studio Code 的基本界面,本节中我们来看看如何通过设置同步功能,让你的开发环境保持一致。
我推荐启用设置同步功能。它允许你在不同的 Visual Studio Code 实例之间同步你的设置、配置以及所有可以在 VS Code 中更改的内容。你可以选择同步哪些项目,但我强烈建议启用此功能。
你可能会问,我只有本地桌面上的一个 VS Code,为什么要启用同步?因为在某些情况下,例如我们稍后将使用的 Codespaces,它允许你在云端使用 VS Code。这意味着你本地的环境可能与云端的版本不匹配,因此你会希望启用配置同步功能。
现在,我将引导你完成操作。让我们从我的本地 VS Code 版本开始。
这是我的本地 VS Code。我需要进入的菜单在这里。由于视频录制,菜单可能被截断,但如果你点击左下角的齿轮图标,你会看到最底部显示“设置同步已开启”。点击此处,我可以进行配置或查看同步设置。
让我们看看配置界面是什么样子。我选择同步所有内容。但你可能不想同步某些代码片段或快捷键,你完全可以在这里进行配置。
一个需要注意的重要选项是“扩展”同步。这意味着,我在这里本地安装的任何扩展,当我在其他地方安装 VS Code 时,它都会自动拉取并安装这些扩展。这无疑是确保我的扩展始终可用的好方法。
那么,如果我在其他地方有另一个 VS Code 实例,这意味着什么?这需要你登录账户。让我们看看这个非本地的 VS Code 实例。
这是另一个 VS Code 实例。我需要在这里点击“管理”按钮,然后系统会提示我开启设置同步。
让我们看看点击后会发生什么。系统会询问是否确认开启。我选择“是,登录并开启”。
当你尝试登录时,你可以选择要使用的账户。我使用 GitHub 账户,因为它对我来说很常用。点击登录后,你可以看到设置同步正在开启,并开始尝试获取我所有的配置设置。
接下来,扩展将开始安装,你会看到蓝色的下载图标,这表示正在安装。这就是因为我启用了设置同步,它非常有用。
如果我查看正在安装的内容,我可能会有 Rust 分析器。再看看我的设置,可能有一些 Markdown 相关的配置。我肯定安装了很多不同的东西,可能还有 Python 相关的扩展。你可以看到 Python 扩展也安装了,因为我偶尔会写 Python 代码。
因此,这就是为什么这个功能会很有用,尤其是在本课程中。我们有时会使用本地 VS Code,但也会在云端通过 Codespaces 使用 VS Code。
我知道我们还没有介绍 Codespaces,这将在后续课程中涉及。但你现在就应该启用设置同步,以便之后无论你在何处使用 VS Code,你的设置和配置都已准备就绪。

本节课中我们一起学习了如何启用和配置 Visual Studio Code 的设置同步功能。通过登录账户,你可以在不同设备或云端环境(如 Codespaces)中保持编辑器设置、快捷键和扩展的一致性,从而提升开发效率和体验。这是一个简单但强大的工具,能确保你的 Rust 开发环境随时随地都处于熟悉和高效的状态。
009:Rust与Visual Studio Code安装总结 🛠️
在本节课中,我们将总结如何为Rust编程设置开发环境,特别是使用Visual Studio Code编辑器。我们将回顾关键步骤和工具,确保你拥有一个坚实的起点,以便更自信地开始Rust开发。
概述
开始学习Rust编程,首要且最重要的步骤之一是设置你的开发环境。我们已经了解了如何使用一些推荐的工具,特别是文本编辑器。虽然你不必强制使用Visual Studio Code,但我们强烈推荐它。现在,你将知道如何设置它。如果你选择使用其他工具,你也可以参考我们介绍过的组件(例如Rust Analyzer),以了解你可能需要哪些额外的帮助。希望到现在为止,你已经有了一个良好的开端,能够在正确的环境中更自信地开始Rust开发。
环境设置的核心步骤
以下是设置Rust开发环境的关键组成部分和步骤。
1. 安装Rust工具链
首先,你需要安装Rust编程语言本身。这通常通过rustup工具完成,它是Rust的版本管理器和安装器。
安装命令(在终端中执行):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
执行此命令会下载一个脚本并启动rustup的安装过程。安装完成后,你需要重启终端或运行source $HOME/.cargo/env来将Cargo(Rust的包管理器和构建工具)添加到你的系统路径中。
2. 安装Visual Studio Code
Visual Studio Code(VS Code)是一个轻量级但功能强大的源代码编辑器。它是开始Rust开发的绝佳选择,因为它拥有丰富的扩展生态系统。
- 下载与安装:访问Visual Studio Code官网,根据你的操作系统(Windows、macOS、Linux)下载并安装编辑器。
3. 配置VS Code的Rust扩展
为了让VS Code更好地支持Rust开发,你需要安装一些扩展。其中最重要的扩展是 Rust Analyzer。
- 安装Rust Analyzer:
- 打开VS Code。
- 点击侧边栏的“扩展”图标(或按
Ctrl+Shift+X/Cmd+Shift+X)。 - 在搜索框中输入“rust-analyzer”。
- 找到由“rust-lang”发布的“rust-analyzer”扩展,并点击“安装”。
Rust Analyzer 为Rust提供了强大的语言服务器支持,包括代码补全、跳转到定义、类型提示和错误检查等功能,能极大提升开发效率。
4. 验证安装
设置完成后,最好验证一切是否正常工作。
验证步骤:
- 打开VS Code。
- 创建一个新文件,例如
hello.rs。 - 输入以下简单的Rust程序:
fn main() { println!("Hello, world!"); } - 打开VS Code的集成终端(
Ctrl+或View -> Terminal`)。 - 在终端中,导航到文件所在目录,然后运行:
如果终端输出“Hello, world!”,则说明你的Rust编译器和基本环境已配置成功。rustc hello.rs ./hello # 在Linux/macOS上 # 或 .\hello.exe # 在Windows上
使用其他编辑器
如果你决定不使用Visual Studio Code,上述核心概念仍然适用。你需要:
- 安装Rust工具链(通过
rustup),这是必不可少的。 - 为你的编辑器寻找Rust语言支持。大多数现代编辑器(如Vim、Emacs、Sublime Text等)都通过插件或语言服务器协议(LSP)支持Rust。关键是要寻找能够集成 Rust Analyzer 或提供类似功能的插件。这能确保你获得高质量的代码分析、补全和提示。

总结
本节课中,我们一起学习了如何为Rust编程设置一个高效的开发环境。我们总结了几个核心步骤:通过rustup安装Rust工具链,安装并配置Visual Studio Code编辑器,以及为其安装关键的Rust Analyzer扩展来获得智能编程辅助。我们还简要提到了,如果选择其他编辑器,核心任务同样是安装Rust并为其配置类似Rust Analyzer的语言支持功能。现在,你的开发环境已经准备就绪,可以自信地开始编写和运行Rust代码了。
010:AI结对编程与GitHub Copilot概述 🧠🤖

在本节课中,我们将学习AI结对编程的概念,并重点介绍如何使用GitHub Copilot这一工具来辅助Rust编程。我们将了解其安装、配置方法,并初步体验其工作方式。
什么是AI结对编程?
上一节我们介绍了Rust编程的基础环境。本节中,我们来看看一种提升编程效率的新方法——AI结对编程。
AI结对编程指的是利用人工智能工具,在编写代码时实时提供建议和辅助。这类似于一位经验丰富的开发者坐在你身旁,在你尝试实现某个已有大致思路的功能时,给予你提示和指导。
聚焦GitHub Copilot
我们将具体使用GitHub Copilot来实现AI结对编程。GitHub Copilot是GitHub提供的一项服务,同时它也是一个Visual Studio Code编辑器扩展。
这个扩展能够在你键入代码时提供建议,甚至可以根据你给出的指令生成准确的代码片段,帮助你更快地实现目标。
辅助学习而非替代学习
你可能会想,在学习像Rust这样的编程语言时,依赖代码建议是否会削弱学习效果?事实恰恰相反,它将增强你的学习过程。
GitHub Copilot能让你更快地推进项目,其作用可以理解为:拥有一位精通Rust的“伙伴”,在你明确知道要做什么但不确定具体如何实现时,帮助你抵达终点。
本节课实践内容
以下是本节课我们将要完成的具体步骤:
- 尝试安装GitHub Copilot扩展。
- 尝试对其进行基本配置。
- 尝试观察并体验与Copilot协同工作的实际效果。
所有这些操作都将在Visual Studio Code环境中完成。
本节课中,我们一起学习了AI结对编程的概念,认识了GitHub Copilot这一强大工具,并明确了它将如何辅助而非替代我们的Rust学习之旅。接下来,我们将进入实践环节,开始安装和配置Copilot。
011:注册GitHub Copilot 🚀

在本节课中,我们将学习如何在Visual Studio Code中启用GitHub Copilot。我们将详细介绍注册流程、学生优惠验证方法,以及如何在账户设置中确认Copilot已成功激活。
概述
在开始使用GitHub Copilot处理文本数据或在Visual Studio Code中编程之前,你需要先启用这项服务。这通常需要购买订阅,但学生可能有资格免费获取。本节将指导你完成整个启用和验证过程。
启用GitHub Copilot服务
首先,你需要访问GitHub Copilot的官方页面以了解定价并启动服务。
- 访问
github.com/features/copilot查看定价详情和工作原理。 - 如果你不是学生,可以在此处开始免费试用。

完成购买或启动试用后,你需要在账户的账单设置中确认服务已生效。
学生免费获取指南
如果你是一名学生,有可能通过验证流程免费获得GitHub Copilot。
- 搜索“GitHub学生开发包”或类似关键词,可以找到详细的申请指南页面。
- 该流程通常需要几个步骤来完成身份验证。
- 核心要求是使用你的学校邮箱地址,验证你当前正在进行的学习课程或项目。
- 虽然存在一些限制条件,但只要按步骤完成,就能顺利设置Copilot。
即使无法免费获取,你也可以尝试免费试用。本课程的内容量很可能在免费试用期内完成,让你能在Copilot的辅助下学习。
验证Copilot激活状态
完成上述设置后,务必验证Copilot是否已在你的账户中激活并可用。
以下是在账户设置中查看的方法:
- 进入你的个人账户设置中的“账单”部分。
- 在账单摘要中,你可以看到已启用的服务。
- 滚动到“附加功能”区域,GitHub Copilot 会显示在此处。
- 如果尚未接受,你可能会看到一个写着“是的,我想启用Copilot”的验证按钮。
- 一旦启用并准备就绪,它将显示在这里,你就可以正常使用了。
组织设置与高级选项
除了个人账户,如果你是某些组织的成员,可能还需要关注组织层面的设置。
- 在“代码规划与自动化”设置中,你可能会找到 Copilot 的选项。
- 点击进入后,你可以为所属组织自定义一些设置(例如,是否允许匹配公共代码的建议)。
- 本教程不深入探讨这些高级设置,但它们为你提供了调整选项。
- 关键是要确保回到
settings/billing页面,确认Copilot已列出并激活。
在编辑器中启用
一旦通过上述步骤在GitHub账户中激活了Copilot,最后一步是在你的代码编辑器中启用它。
- 在你的Visual Studio Code中,通过GitHub进行身份验证登录。
- 登录后,你就能在文本编辑器中启用GitHub Copilot功能了。
总结

本节课我们一起学习了注册和启用GitHub Copilot的完整流程。我们了解了如何访问服务页面、学生获取免费权限的验证方法、如何在GitHub账户的账单设置中确认激活状态,以及最终在Visual Studio Code中登录并启用该工具。确保完成所有步骤,你就能开始体验Copilot带来的编程辅助了。
012:在Visual Studio Code上安装并启用Copilot 🚀
概述
在本节课中,我们将学习如何在Visual Studio Code中安装并启用GitHub Copilot。我们将逐步完成注册服务、安装扩展、验证其运行状态以及体验初步的代码建议功能。
注册与准备
上一节我们介绍了GitHub Copilot的背景,本节中我们来看看具体的安装步骤。首先,你需要完成GitHub Copilot的注册。无论是通过学生身份获取优惠,还是使用免费试用,你都需要先完成服务注册。
在VS Code中安装扩展
注册完成后,你可以在文本编辑器(本例中使用Visual Studio Code)中开始使用它。以下是安装步骤:
- 打开Visual Studio Code。
- 进入扩展视图。
- 在搜索框中输入“GitHub Copilot”并按下回车键。
- 在搜索结果中,点击“GitHub Copilot”扩展旁的“安装”按钮。
点击安装后,会跳转到一个内容详尽的页面。这个页面与扩展市场中的页面相同,提供了关于GitHub Copilot的丰富信息。


你可以在此查看更新日志、功能介绍等详细信息。建议你定期浏览更新日志,以了解最新的发布说明。
验证扩展运行状态
安装完成后,一个常见的问题是:如何确认Copilot已成功启用并正在运行?

扩展安装并运行后,你会在Visual Studio Code底部状态栏看到一个特定的图标。

这个图标就是Copilot的徽标。状态栏中出现这个图标,是判断Copilot正在运行的最直观方式。
如果你点击这个图标,会弹出菜单。你可以选择“禁用”它(当然我们不需要),或者选择“管理”进入设置页面。在设置页面,你可以进行一些配置,尽管目前可配置的选项并不多。
体验代码建议功能
至此,你已经完成了安装并确认其可用性。现在,让我们来体验一下它的核心功能——代码建议。
你可以打开任意文件进行测试,例如一个README.md文件。当你开始输入时,Copilot就会提供建议。
有时,你甚至还没开始输入,建议就可能出现,这同样是Copilot正在工作的一个信号。不过,具体情况可能略有不同。
例如,如果你删除部分内容后重新输入,建议会随着你的输入实时出现。就像这样,刚才的建议消失了,但当我输入字母“c”时,自动补全建议又出现了。
// 例如,当你输入 “c” 时,可能会看到类似以下的建议
c
此时的自动补全并非基于项目上下文,而是完全由Copilot根据其理解,预测你接下来可能想输入的内容。我们将在后续课程中深入探讨其工作原理。
总结
本节课中,我们一起学习了在Visual Studio Code中安装和启用GitHub Copilot的全过程。关键步骤包括:在扩展市场中搜索并安装Copilot,通过状态栏图标验证其运行状态,以及在编辑文件中初步体验其智能代码建议功能。现在,你的开发环境已经配备了强大的AI编程助手。
013:使用Copilot进行编程 🚀
在本节课中,我们将学习如何利用GitHub Copilot的代码建议功能来辅助Rust编程。我们将通过一个简单的示例,演示如何通过引导Copilot生成代码,并理解其工作原理。

概述
GitHub Copilot是一个强大的AI编程助手,能够根据上下文提供代码自动补全和建议。本节将展示如何在Rust编程中利用这一工具,即使你对Rust的细节尚不熟悉,也能快速生成代码框架。
启用Copilot与基础设置
首先,确保你的开发环境中已安装并启用了GitHub Copilot扩展。你可以通过编辑器扩展面板进行确认。我们将在一个名为main.rs的空白文件中开始演示。
基础函数与自动补全
上一节我们确认了环境准备就绪,本节中我们来看看如何开始编写代码并接收建议。
在Rust中,使用fn关键字定义函数。当你开始输入时,Copilot会提供自动补全建议。
fn main() {
println!("Hello, world!");
}
输入fn main()后,Copilot可能会自动补全花括号{}和println!("Hello, world!");语句。按下Tab键即可接受该建议。
引导Copilot生成复杂代码
仅仅接受建议可能不够,我们常常需要引导Copilot生成更符合我们意图的代码。以下是引导Copilot的几个步骤:
-
定义新函数:我们尝试定义一个计算平均价格的函数。
fn average_price() { // Copilot可能会在此处生成一些示例代码 } -
指定变量名:当建议的变量名(如
item)不符合需求时,我们可以通过继续输入来引导。例如,输入Banana,Copilot可能会将变量名修正为banana。 -
修改变量类型与值:同样,我们可以通过输入来改变建议的变量类型和初始值。例如,将
price改为quantity,或将数值进行修改。
Copilot的行为模式与限制
将Copilot的行为类比为引导一个学步的幼儿是恰当的。它不会总是给出完美的结果,需要你通过持续的“输入反馈”来将其引导至正确的方向。有时它的建议完全正确,你可以直接使用Tab键补全,这能极大提升编码速度。有时则需要进行手动调整。
添加注释与文档
我们还可以利用Copilot来生成注释。在Rust中,使用双斜杠//进行单行注释。
// 这个文件中的函数用于演示Copilot的使用。
输入//后,Copilot也可能会提供相关的注释补全建议,帮助我们快速编写文档。
总结

本节课中我们一起学习了如何使用GitHub Copilot辅助Rust编程。我们演示了如何通过简单的输入触发代码补全,并通过持续输入来引导AI生成更符合预期的代码。Copilot是一个强大的工具,能够帮助初学者快速构建代码框架,但理解其建议并适时进行手动修正同样重要。随着编写的Rust代码增多,你会更加熟练地利用这个工具来提升开发效率。
014:使用Copilot提示编程 🚀

在本节课中,我们将学习如何利用GitHub Copilot进行基于提示(Prompt)的编程。我们将看到,通过编写描述性的注释,可以引导Copilot生成我们想要的代码,而不仅仅是依赖它提供的自动补全建议。
从建议编程到提示编程
上一节我们介绍了如何使用Copilot的建议进行编程。现在,我们来看看当建议不完全符合需求时,如何主动引导Copilot生成代码。
假设我们正在编写一个函数,但遇到了问题。我们确切地知道自己想要什么,但当前的代码并不完全正确。例如,我们想要一个计算平均价格的函数,它可以处理多个项目并计算平均值。
与其像之前那样手动输入代码,我们可以使用Copilot进行基于提示的编程。具体做法是,先删除现有代码,然后从一个注释开始。通过注释,我们可以告诉Copilot我们想要什么。
以下是具体步骤:
- 首先,删除有问题的函数代码。
- 然后,输入一个描述性的注释。例如:
// 创建一个函数,计算多个数字的平均值并返回。 - 等待片刻,Copilot会根据注释生成相应的函数代码。
- 检查生成的代码,如果正确,按
Tab键接受。
通过这种方式,我们不再需要手动编写所有代码,而是通过注释“提示”Copilot生成我们想要的功能。这种方法生成的代码通常是正确的,并且不会出现之前遇到的错误下划线提示。
在函数内部使用提示
基于提示的编程不仅限于创建新函数,在函数内部同样有效。例如,假设我们有一个数字列表,我们想在函数内部添加一个循环来打印这些数字。
我们可以这样做:
- 在需要添加循环的地方,输入一个描述性的注释。
- 例如,输入:
// 循环遍历数字并打印它们。 - Copilot会根据这个注释,生成一个
for循环代码块。 - 按
Tab键接受生成的代码,然后保存文件。
这样,Copilot就会生成一个遍历列表并打印每个数字的循环。目前,我们无需担心具体的语法细节或代码是否完全符合我们的最终项目结构。这个例子主要展示了Copilot如何超越简单的命令补全,允许我们通过注释提示来生成有用的代码片段,并将其应用到程序中。
总结


本节课中,我们一起学习了GitHub Copilot的提示编程功能。我们了解到,当自动建议不满足需求时,可以通过编写清晰的注释来主动引导Copilot生成特定功能的代码。这种方法不仅适用于创建新函数,也适用于在现有代码块中添加复杂逻辑。通过结合注释提示,我们可以更高效地利用Copilot构建应用程序。
015:Copilot X与基于聊天的学习 🧠💬

在本节课中,我们将学习GitHub Copilot X,这是一组由GitHub Copilot推出的新功能。它将AI结对编程的能力扩展到更多场景。我们将通过实际操作演示,了解如何利用基于聊天的界面来学习、调试和理解代码。
上一节我们介绍了Copilot的基本代码补全功能。本节中,我们来看看Copilot X如何通过聊天交互,将学习与编程过程深度融合。
Copilot X是GitHub Copilot的一系列增强功能。它允许你在几乎任何地方实现AI结对编程。这意味着什么?我们已经看到Copilot本身能提供代码建议,而Copilot X在此基础上更进一步。它集成了类似GPT-4的模型,使你能够进行各种交互,而不仅仅是基于提示生成代码或获取智能建议。
如下图所示,你可以选中一段代码,然后通过一个类似聊天机器人的界面与之互动,尝试实现我们想要完成的任务。

让我们看看它的实际应用。在制作本视频时,该功能处于技术预览阶段。但当你观看此视频时,它可能已经结束预览并广泛可用。因此,这绝对是一个值得关注的功能,你可能会直接体验到它。
那么它具体是什么样子呢?我现在切换到了Visual Studio Code的另一个会话。这是我之前构建的内容,你可以看到界面略有变化。首先,一个明显的标志是会出现一个聊天图标。你的设置可能看起来不同,但Copilot已启用,一切准备就绪。
如果我点击那个聊天图标,会收到一条欢迎消息:“你好,我是你的Copilot,我在这里为你提供帮助和建议。”这很好。
假设我是一个Rust新手,不理解当前的代码。我想了解发生了什么。我可以输入:“请解释高亮的代码。”Copilot会进行思考,然后针对我的代码上下文给出解释。这是一种极佳的学习方式。因为当我被Rust(或任何编程语言)的问题困住时,我能够获得一些解释。
例如,假设我犯了一个错误。我把代码中的 f64 改成了 u64,然后保存。接着,我写 let a = 30.2; 并保存。这时会出现红色的波浪线错误分析。我知道这些更改不会工作,但作为一个新开发者,你可能会疑惑:这段代码看起来没问题,为什么不行?为什么会出现这个错误?我不明白发生了什么。
我可以选中整个函数,然后输入:“这个函数没有按预期工作。”好处是,GitHub Copilot可能会返回信息,即使最初没有直接给出你想要的答案。在这种情况下,你从编译器得到了类型不匹配的错误。Copilot会提供一个详尽解释,说明哪里出了问题。
这是一个有趣的功能,因为我现在在这里获得了输出。我可以说:“哇,这实际上很好,它修复了问题。我理解它所说的,这对我来说很有道理。它解释了更新后的版本在做什么。”然后我可以点击“在光标处插入”按钮。执行后,注释可能消失了,但这没关系。现在代码完美运行了。
这超越了仅仅通过打字获取建议。你能够与生成输出的Copilot模型进行更好的交互,几乎就像在聊天一样。这肯定需要一些时间来适应,但你可以学到很多,产出大量优质、完整的代码,并避免陷入困境。每当你被卡住时,你都可以提问并获得答案,甚至得到关于你可能做错了什么的非常非常好的解释。
即使是我自己,拥有超过11、12年的专业Python开发经验,现在也做很多Rust开发,我肯定会犯一些错误。有时当我从一种编程语言切换到另一种时,我可能会犯诸如漏掉分号这样的错误。这在某些语言中不是问题,但在Rust中就是问题,这确实发生在我身上。
除此之外,我实际上可以说:“我不明白为什么这会发生在我身上。”然后你可以询问GitHub Copilot并获得很好的解释,从而提升你的学习效果。




本节课中我们一起学习了GitHub Copilot X的聊天交互功能。我们看到了如何通过选中代码并提问来获得解释、调试错误以及理解代码逻辑。这种基于聊天的学习方式,将AI辅助编程从简单的代码补全提升到了交互式教学和问题解决的层面,能有效帮助初学者和资深开发者更高效地学习和编写代码。
016:AI结对编程总结 🧠🤖

在本节课中,我们将总结AI结对编程的核心要点。你将了解其重要性、基本配置方法以及如何在学习Rust的过程中有效利用它。
AI结对编程是一项新颖且非常有趣的技术。我很高兴能将其纳入本课程。到目前为止,你不仅应该能够利用它,还应该知道如何安装和配置它,或许还能与它进行一些互动。
现在,在你的开发环境中以及在整个课程学习过程中使用AI结对编程工具至关重要。虽然这不是100%必需的,但它绝对能帮助你在学习不同课程内容时尝试掌握Rust。因此,到目前为止,你应该已经能够在你的环境中完成安装、配置,并希望已成功启用它。
本节课中,我们一起学习了AI结对编程的总结。你了解了它的价值、基本设置步骤,以及如何将其作为学习Rust的辅助工具。掌握这些将有助于你更高效地进行后续的编程学习与实践。
017:Codespaces概述 🚀

在本节课中,我们将学习GitHub Codespaces这一服务。它是一个云端开发环境,能帮助我们快速启动一个标准化的开发环境,从而专注于代码本身,而无需花费大量时间配置本地环境。
什么是GitHub Codespaces? ☁️
GitHub Codespaces是GitHub提供的一项服务,它允许我们在云端拥有一个标准化的开发环境。
这听起来可能并不特别,但关键在于,通过少量配置,我们就能在云端使用Visual Studio Code,并拥有一个标准化的环境。在这个环境中,我们可以安装、编译程序,并且只需简单的配置就能预装特定的扩展。
Codespaces的核心优势 ⚡
上一节我们介绍了Codespaces的基本概念,本节中我们来看看它的具体优势。这项服务能让你快速上手,无需过多担心环境设置问题,因为环境已经为你预先配置好了。
我们将看到它是如何工作的。此外,如果你有协作者,或者想与他人一起工作,Codespaces能确保环境的一致性。想象一下,一个团队成员使用Linux,另一个使用Windows,还有人用macOS,这中间必然存在细微的差异。使用Codespaces这样的服务,就能为所有人提供统一的环境。
便捷的开发体验 💻
以下是使用Codespaces带来的便捷开发体验:
- 跨平台一致性:无论团队成员使用何种操作系统,都能获得完全相同的开发环境,避免了“在我机器上能运行”的问题。
- 快速启动:环境预配置意味着你可以跳过繁琐的安装和设置步骤,直接开始编码。
- 基于浏览器的开发:你甚至可以在网页浏览器中直接使用Codespaces,体验就像在本地运行Visual Studio Code一样。这能让你更快地开始开发,快速搭建环境,从而将精力集中在重要的部分。
总结 📝
本节课中我们一起学习了GitHub Codespaces。它是一个云端标准化开发环境服务,通过预配置的环境和基于浏览器的访问方式,极大地简化了开发环境的搭建过程,确保了团队协作环境的一致性,并让开发者能更专注于编写代码。
018:Codespaces基础 🚀

在本节课中,我们将学习如何使用GitHub Codespaces来创建一个云端开发环境。我们将从登录GitHub开始,逐步演示如何为一个仓库启动Codespace,并探索其基本功能和配置选项。
概述
GitHub Codespaces提供了一个完整的、基于云的开发环境。它允许你直接在浏览器中使用Visual Studio Code,而无需在本地安装任何工具。这对于确保开发环境的一致性,尤其是在团队协作或教学场景中,非常有用。
上一节我们介绍了云端开发环境的概念,本节中我们来看看如何实际创建并使用一个GitHub Codespace。
启动你的第一个Codespace
要开始使用Codespaces,你需要确保已登录GitHub账户。
登录后,访问你的任意一个仓库(特别是你自己账户下的仓库)。在仓库主页,你会看到一个绿色的“Code”按钮。
在Codespaces功能发布前,点击这里主要看到的是本地克隆选项。现在,“Codespaces”标签页已经可用,并且在大多数情况下是默认选项。
点击“Codespaces”标签页,你会看到当前没有活跃的Codespace,并可以“在main分支上创建Codespace”。这意味着GitHub将在云端为main分支创建一个开发环境。
在创建之前,我想向你展示“新建Codespace”按钮旁边的三个点菜单。这个菜单提供了几个不同的选项,例如设置预构建(我们暂不涉及)或配置开发容器(稍后会看到)。这里我们重点关注“新建并附带选项”。
点击“新建并附带选项”。
配置Codespace选项
在弹出的窗口中,你可以进行以下配置:

- 分支:你可以选择除
main之外的其他分支。本例中项目只有main分支,但如果你想基于其他分支的代码创建开发环境,可以在这里选择。 - 区域:系统会自动选择离你地理位置最近的区域。例如,我所在的位置显示为“美国东部(USE)”。
- 机器类型:默认是2核CPU和4GB内存。这个配置对大多数情况都足够。但你也可以选择更高配置,最高可达16核CPU、32GB内存和128GB存储空间。
对于初学者,默认配置通常就足够了。
让我们回到仓库主页,直接点击“在main上创建Codespace”。
进入Codespace环境
点击创建后,你会进入一个加载页面。稍等片刻,一个完整的Visual Studio Code界面将在你的浏览器中加载完成。
本质上,你获得了一个运行在云端的VS Code。这个过程可能需要几分钟,具体时间取决于需要安装的扩展和依赖。
在后台,系统正在异步管理并更新扩展。你可能会看到一个欢迎消息,提示环境正在准备中,你可以先随意浏览。
与此同时,你已经可以立即访问你的代码了。我使用了一个包含Rust项目的示例GitHub仓库,里面已经有一些文件。
你可以看到界面正在忙碌地安装各种扩展。虽然后台在运行,但你现在就可以开始探索这个环境了。
让我调整一下界面,以便更清楚地查看。我把终端字体调大了一些。
探索云端环境
让我们看看正在使用的是什么系统。默认情况下,Codespace运行在Linux系统上,具体是Ubuntu LTS发行版。
这明确不是我的本地机器,因为我正在浏览器中工作。
需要注意的一点是复制粘贴操作。由于在浏览器中,使用Ctrl+C和Ctrl+V可能会触发浏览器的默认行为。你可能需要允许页面访问剪贴板,之后就可以正常粘贴了。
这个环境功能强大,我可以与操作系统交互。例如,我可以运行sudo apt update来获取最新的软件包列表。
在其他系统中,命令可能略有不同。这之所以重要,是因为我们能在一个标准化的环境中工作。
这意味着,如果你访问(或Fork)这个仓库,并通过Codespaces打开它,你将拥有与我完全相同的环境。这避免了在macOS、Windows或不同Linux发行版上配置环境时可能遇到的兼容性问题。
因为环境是标准化的,所以我们不会遇到这些问题。
在这里,我们可以像在本地环境一样进行所有设置和操作,只不过一切都在云端。
关闭欢迎页面,打开我常用的视图。我来查看一下main.rs文件。
我可以看到一些功能已经就绪。我可以查看已安装的扩展,甚至可以安装Rust分析器的预发布版本。
不过,这可能会遇到问题,因为我还没有安装Rust工具链。这引出了一个重要问题:为什么要使用Codespaces?
Codespaces的优势与自定义
我们稍后会看到如何自定义环境。关键在于,我们可以预先配置一些扩展甚至系统依赖。
这意味着,任何人(包括我自己)打开这个项目的Codespace时,都不需要手动安装像Rust分析器这样的扩展或系统依赖,因为它们已经预先配置好了。
目前我们所做的都很直接。最后,我想展示如何查看分配给这个Codespace的CPU核心数和内存大小。
再次打开终端。我将使用free -m命令查看内存信息。可以看到,总内存接近4GB,当前使用了大约1GB。
我们再用htop工具看看进程和资源使用情况。这是一个查看系统进程的工具。
可以看到有两个CPU核心,资源使用情况良好。内存使用了大约1GB,占总可用4GB的一部分。
这样我们就知道当前环境的资源配置了。我们之后可以在此基础上进行交互和配置。
管理你的Codespace
现在,Codespace已经成功运行,并且会列在“Codespaces”标签页下。它有一个自动生成的名称,状态显示为“活跃”和“运行中”。
你可以对它进行其他操作,例如“停止”或“删除”。在后台,Codespace实际上是一个容器,因此你可以像管理容器一样启动和停止它。
如果我点击“停止”,然后切换到其他标签页,可能会看到提示说你的Codespace正在停止。这就像一个可以随时启停的容器。
总结


本节课中我们一起学习了GitHub Codespaces的基础操作。我们从登录GitHub开始,演示了如何为仓库创建Codespace,探索了其配置选项(如分支、区域和机器类型),并进入了云端VS Code环境进行体验。我们看到了它是一个标准化的Linux环境,允许我们安装工具和运行命令。最后,我们还了解了如何查看资源使用情况以及管理(停止/删除)Codespace。这为在任何GitHub仓库上快速获得一个一致的开发环境奠定了基础。
019:理解使用量与配额 💻

在本节课中,我们将学习如何理解和管理 GitHub Codespaces 的使用量与配额。这对于避免触及服务限制至关重要。
概述
我们将探讨 GitHub Codespaces 的配额系统,包括核心小时和存储空间的计算方式,并学习如何通过设置来有效管理配额,防止意外超限。
访问配额信息
要查看您的配额使用情况,您需要访问 GitHub 的设置页面。
以下是具体步骤:
- 前往
github.com网站。 - 导航至 Settings(设置) > Billing(账单)。
- 在账单摘要页面,向下滚动直至找到 Codespaces 部分。
在此部分,您可以查看已包含的核心小时数、当前使用量以及配额重置周期。例如,信息可能显示“包含 180 核心小时,当前使用 12.46 小时,包含的配额每 30 天重置”。
理解核心小时
核心小时是 Codespaces 计算使用量的单位。它考虑了您所选虚拟机的核心数。
其计算逻辑可以用以下公式表示:
可用机器时间(小时) = 包含的核心小时总数 / 所选机器的核心数
例如,如果您有 180 个包含的核心小时,并选择一个 2 核的机器,那么您可以使用该机器 90 小时。如果您选择 4 核机器,则只能使用 45 小时。在 Codespaces 创建页面,您可以看到不同核心数选项(如 2、4、8、16、32 核)及其对应的每小时价格。
理解存储配额
除了计算资源,存储空间也有月度配额。
存储配额的计算方式如下:
- 1 GB-月 表示 1 GB 的存储空间被持续占用整整一个月。
- 如果您有 20 GB 的月度包含存储,那么持续占用 20 GB 一个月后,配额将用尽。
您可以在账单页面的 Codespaces 部分查看当前的存储使用情况,例如“已使用 0.32 GB / 包含 20 GB”。
管理配额与防止超限
了解配额后,我们可以通过配置设置来主动管理使用量,防止触及限制。
上一节我们介绍了配额的计算方式,本节中我们来看看如何进行有效管理。关键设置位于 Settings(设置) > Codespaces 页面底部。
以下是两个重要的配置项及其作用:
- 默认保留期:此设置决定停止 Codespace 后,它被自动删除前的保留天数。最大值是 30 天。建议设置为较短的时间(例如 2 天),并在停止工作后及时将代码提交并推送(
commit and push)到 GitHub 仓库。这样可以避免闲置的 Codespace 长期占用存储配额。 - 默认空闲超时:此设置决定 Codespace 无操作(空闲)多长时间后会自动停止。设置为一个合理的值(例如 30 分钟),可以确保在您离开时不会持续消耗核心小时。您可以根据自身需求调整此时间。
通过合理配置这些选项,您可以有效管控核心小时和存储空间的消耗,从而避免超出免费或包含的配额限制。
总结

本节课中我们一起学习了 GitHub Codespaces 配额的核心概念。我们了解了如何查看核心小时和存储空间的使用情况,掌握了配额的计算方式,并学会了通过配置“默认保留期”和“默认空闲超时”来优化使用习惯,防止资源超限。请记住,养成及时提交代码并停止不使用的 Codespace 的习惯,是高效利用该服务的关键。
020:开发容器基础 🐳

在本节课中,我们将学习开发容器(Dev Containers)的基础知识。开发容器是一种通过代码来配置开发环境的方式,它允许我们为项目创建一个标准化、可复现的编程环境。我们将通过实际操作,了解如何在项目中添加和配置开发容器。
什么是开发容器?
开发容器是一种通过配置文件来定义开发环境的方法。它允许我们使用代码来指定项目所需的所有工具、依赖和设置,确保每个开发者都能在完全相同的环境中工作。
添加开发容器配置
现在,让我们看看如何为一个Rust项目添加开发容器配置。
首先,我们有一个默认的代码空间,其中只包含Rust应用程序的文件,没有任何特殊配置。为了添加开发容器,我们可以非常方便地通过命令面板进行操作。
以下是添加开发容器配置文件的步骤:
- 打开命令面板。
- 输入“Dev Containers”并选择“Add Dev Container Configuration Files”。
- 选择“Create a new configuration”,因为我们还没有任何活动配置。
- 从列表中选择我们需要的环境。在本例中,我们选择“Rust”。
- 选择操作系统版本,通常保持默认即可。
- 可以选择性地添加一些额外的功能或工具,但本例中我们不需要。
- 点击“OK”完成创建。

完成上述步骤后,系统会生成一个名为 .devcontainer 的文件夹,其中包含一个 devcontainer.json 配置文件。

重建开发环境
配置文件创建后,系统会立即提示我们环境发生了变化,并询问是否要重建容器。我们选择“Rebuild”。在重建过程中,系统会在后台拉取一个预装了Rust工具链的Docker镜像。
重建完成后,我们可以关闭配置日志,回到代码编辑器。此时,如果打开终端并运行 which cargo 和 which rustc 命令,会发现Cargo和Rust编译器已经成功安装在这个新的容器环境中了。
配置文件详解
那么,这个配置文件具体做了什么呢?让我们来看一下 .devcontainer/devcontainer.json 文件的内容。

本质上,这是一个JSON格式的配置文件,它定义了开发容器所使用的镜像、扩展、设置等。通过这个文件,我们可以精确地定制项目的开发环境,确保所有必需的开发工具和依赖都已就位。
除了使用默认的 devcontainer.json 文件,我们还可以更进一步,使用一个 Dockerfile 来构建完全自定义的容器镜像,并在 devcontainer.json 中引用它,以实现更复杂的环境配置。
总结

本节课中,我们一起学习了开发容器的基础知识。我们了解了开发容器的概念,并实际操作了如何为一个Rust项目添加和配置开发容器。通过使用 devcontainer.json 文件,我们可以轻松地为项目创建标准化、可移植的开发环境,这极大地简化了团队协作和环境搭建的流程。
021:自定义编辑器 🛠️
在本节课中,我们将学习如何在开发容器中自定义Visual Studio Code编辑器,特别是如何通过配置自动安装扩展和设置,以确保每次创建新的开发环境时都能获得一致且个性化的体验。
概述
上一节我们介绍了开发容器的基本概念。本节中,我们来看看如何通过修改开发容器配置文件,来自动化安装像Github Copilot这样的编辑器扩展,并统一其他编辑器设置,从而实现开发环境的标准化。
自定义编辑器扩展
我们希望定制编辑器环境。具体操作是,对现有项目进行一些配置更改。这个项目与我们之前使用的项目相同,已经配置了开发容器,这没有问题。

我推荐的做法之一是安装Github Copilot扩展。以下是具体步骤:
- 打开VSCode的扩展面板。
- 搜索“Github Copilot”。
- 点击安装按钮。
安装完成后,扩展会启用并开始工作。你可以在编辑器界面上看到它已激活的提示。

实现配置自动化
然而,为每一个新的代码空间手动重复此安装过程并不是一个好主意。我们的目标是实现自动化,让环境配置标准化。我们将通过修改开发容器配置文件来实现这一点。
操作方法是,在已安装的Github Copilot扩展旁边,点击齿轮图标,然后选择“添加到devcontainer.json”。
点击后,系统会提示配置已更改。我们可以先不立即重建容器,而是查看具体更改了哪些内容。
关闭当前提示,打开文件资源管理器,找到并打开 .devcontainer/devcontainer.json 文件。你会看到文件中新增了一个 customizations 部分,其中 vscode 的 extensions 列表里现在包含了Github Copilot。
{
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot"
]
}
}
}
理解配置的重要性
这个改动非常重要,因为现在我可以将此配置文件提交到版本库中。下次我打开一个新的代码空间时,Github Copilot扩展将自动安装并可供使用。这就是实现标准化设置的关键,它能确保我每次都能获得完全一致的开发环境。
就像我们在本地VSCode中管理扩展一样,在开发容器配置中,我们不仅可以管理扩展,还可以统一其他设置。
自定义编辑器设置
除了扩展,我们还可以配置编辑器设置。例如,如果我想针对Rust语言进行一些特定设置,比如调整rust-analyzer的行为,我可以在配置文件中添加 settings 部分。
以下是添加自动重载设置的示例:
{
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot"
],
"settings": {
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.checkOnSave.extraArgs": ["--", "-D", "warnings"]
}
}
}
}
通过这种方式,我可以开始为我的环境进行各种定制,并确保每次使用代码空间打开这个特定项目时,所有设置都是统一应用的。
保存配置文件后,我可以选择重建容器,这些设置就会立即生效。此后,每次启动环境,这些配置都会自动应用。

总结

本节课中我们一起学习了如何配置你的文本编辑器环境。我们主要掌握了通过修改 devcontainer.json 文件来自动安装VSCode扩展和统一编辑器设置的方法。这样就能确保团队协作或个人在不同机器上都能获得完全一致的开发体验。接下来,我们将看看如何对环境本身进行一些修改。
022:自定义环境 🛠️

在本节课中,我们将学习如何通过自定义Docker文件来修改开发容器的底层系统环境。这允许我们安装额外的系统包或对操作系统进行特定配置,以满足项目需求。
上一节我们介绍了如何通过JSON配置文件来管理开发容器。本节中我们来看看,当需要修改操作系统或底层系统时,应该如何操作。
自定义Docker文件
如果需要对操作系统或底层系统进行更改,例如暴露特定文件、添加文件或修改系统配置,仅查看当前的JSON配置文件可能不够清晰。
以下是我们可以采取的自定义步骤:
- 使用Docker文件:我们可以自定义Docker文件。开发容器指南中提供了Docker文件或Docker Compose文件的使用方法,路径始终相对于开发容器配置文件。
- 执行系统命令:在Docker文件中,可以运行如
apt-get update和apt-get install等命令来安装所需的软件包。

实施自定义步骤
现在,让我们具体实施这些更改。
首先,修改开发容器配置文件。我们将指定一个构建配置,并打开一个新部分。原始配置中可能指定了基础镜像,但我们现在要将其改为使用自定义的Docker文件进行构建。
修改后的配置关键部分如下:
{
"build": {
"dockerfile": "Dockerfile"
}
}
我们移除了直接指定镜像的行和相关的参数,使配置指向我们即将创建的Docker文件。
接下来,在开发容器配置文件的同级目录中,创建一个新的Dockerfile文件。
在这个Docker文件中,我们仍然希望基于之前预配置的镜像。然后,我们可以在其中添加系统级的操作指令。
一个基础的Docker文件内容如下:
FROM mcr.microsoft.com/devcontainers/rust:1
RUN apt-get update && apt-get install -y \
<你的软件包名> \
&& rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=noninteractive
其中,FROM指令指定了基础镜像。RUN指令用于执行更新和安装命令。ENV DEBIAN_FRONTEND=noninteractive环境变量可以防止在安装过程中出现交互式提示。
保存Docker文件后,即可尝试重建容器。


重建并验证容器
在VS Code中,执行“重建容器”操作。这将根据新的Docker文件指令触发完整的容器重建过程。
重建完成后,之前通过JSON配置文件添加的扩展(如GitHub Copilot和Rust Analyzer)会被重新安装。同时,在终端中运行sudo apt-get update命令会非常快,因为该命令已在容器构建过程中通过Docker文件执行过了。
通过这种方式,devcontainer.json文件管理了大多数开发环境更改。当需要进行系统级修改时,则需添加并配置自定义的Docker文件。


本节课中我们一起学习了如何通过创建和配置自定义Docker文件来扩展开发容器的系统功能。这为满足特定的项目依赖和系统配置需求提供了灵活的方法。
023:GitHub Codespaces总结 🚀
在本节课中,我们将对GitHub Codespaces这一工具进行总结。我们将回顾它的核心价值、适用场景以及在本课程中的作用,帮助你理解如何利用这个标准化的开发环境来学习和实践Rust编程。
概述
上一节我们介绍了如何在Codespaces中配置和运行Rust代码。本节中,我们将对GitHub Codespaces进行全面的总结,梳理其优势和对学习者的意义。
GitHub Codespaces让我感到非常兴奋,它是一个标准化的环境。希望你现在对查看Codespaces、与Codespaces交互以及其中的一些配置已经更加熟悉了。
当然,这个工具不仅允许你完成本课程中的一些课程甚至一些实验,而且在未来,它还能帮助你为自己的Rust项目设置个性化的配置。它甚至也适用于Rust之外的其他项目,但这绝对是一个很好的起点。
再次强调,这门课程并不100%要求使用它,但它是一个非常实用的工具。你会获得一定的免费额度,允许你每月免费使用数小时。

核心优势与应用
以下是GitHub Codespaces的几个关键优势:
- 标准化环境:它提供了一个统一、预配置的开发环境,消除了“在我机器上能运行”的问题,确保所有学习者起点一致。
- 开箱即用:对于Rust学习,它通常预装了必要的工具链(如
rustc、cargo),无需在本地进行复杂的安装和配置。 - 个性化配置的起点:你可以基于课程提供的配置,进一步定制属于自己的开发环境,并将此配置应用于未来的个人Rust项目或其他类型的项目。
- 低成本入门:GitHub提供免费的月度使用额度,对于完成课程学习和进行小型实验来说,通常是足够的。
对本课程的意义
虽然在本课程中并非强制要求使用GitHub Codespaces,但它极大地降低了学习Rust的入门门槛。你无需担心系统兼容性或依赖冲突,可以直接专注于编程语言本身的学习和实践。它尤其适合以下情况:
- 你的本地环境配置遇到困难。
- 你想在不同的设备上无缝继续学习。
- 你希望体验一个干净、专为Rust学习配置的环境。
总结

本节课中我们一起学习了GitHub Codespaces的总结。我们了解到它是一个强大且便捷的云端开发环境,为学习Rust提供了标准化的起点,并能扩展到个人项目。记住,你可以利用其免费额度来辅助完成本课程,并将其作为未来开发工作的一个有力工具。
024:Rust入门介绍

在本节课中,我们将学习如何开始使用Rust。我们将从项目文件组织、变量定义等最基础的内容入手,并辅以伪代码和布局说明,帮助你快速上手编写Rust代码。课程还将涵盖条件语句的基础知识,以及变量遮蔽(即重新赋值变量)的概念。
项目组织与起步 🚀

开始使用Rust可能会有些棘手。
上一节我们介绍了课程概述,本节中我们来看看如何组织文件并启动项目,以便你能立即开始编写Rust代码。
我们将学习如何创建基本的项目结构。
以下是创建和运行一个简单Rust程序的步骤:
- 使用
cargo new命令创建一个新项目。 - 进入项目目录。
- 编辑
src/main.rs文件。 - 使用
cargo run命令编译并运行程序。
通过以上步骤,你可以建立一个基础的Rust开发环境。
变量定义基础 📦
现在我们已经有了项目结构,接下来看看如何在Rust中定义变量。这是编写任何程序的基础。
Rust使用 let 关键字来声明变量。变量默认是不可变的。
例如,定义一个不可变的整数变量:
let x = 5;
要定义一个可变的变量,需要使用 mut 关键字:
let mut y = 10;
y = 15; // 这是允许的
理解变量的可变性是掌握Rust所有权系统的第一步。
条件语句与流程控制 🔀
在定义了变量之后,我们通常需要根据不同的条件执行不同的代码块。本节中我们来看看Rust中的条件语句。
Rust中的条件判断使用 if、else if 和 else 关键字,其语法与其他类C语言相似。
以下是一个条件语句的基本示例:
let number = 7;
if number < 5 {
println!("条件为真");
} else {
println!("条件为假");
}
条件表达式必须产生一个布尔值(true 或 false)。使用条件语句可以控制程序的执行路径。
变量遮蔽 🔄
最后,我们来探讨Rust中一个独特的概念:变量遮蔽。这与我们之前讨论的可变变量重新赋值不同。
变量遮蔽允许你使用相同的变量名来声明一个新变量。新变量会“遮蔽”前一个变量,并且它可以具有不同的类型或可变性。
以下是变量遮蔽的示例:
let spaces = " ";
let spaces = spaces.len(); // 第一个spaces是字符串,第二个spaces是整数,这是允许的
这与可变变量重新赋值有本质区别:
let mut spaces = " ";
// spaces = spaces.len(); // 错误!不能将整数赋值给字符串类型的变量
变量遮蔽是一个强大的特性,它能在不污染命名空间的情况下转换值的类型。
总结 📝
本节课中我们一起学习了Rust编程的入门知识。
我们首先了解了如何组织Rust项目并运行第一个程序。接着,我们学习了如何使用 let 和 mut 关键字来定义不可变与可变变量。然后,我们探讨了如何使用 if 语句进行基本的流程控制。最后,我们介绍了Rust特有的变量遮蔽概念,并区分了它与变量重新赋值的不同。
掌握这些基础概念是进一步学习Rust复杂特性(如所有权、借用和生命周期)的坚实基础。
025:创建新的Rust项目 🚀

在本节课中,我们将学习如何使用Cargo工具创建新的Rust项目。Cargo是Rust的官方包管理器和构建工具,它能帮助我们快速初始化项目结构,管理依赖,并执行构建、测试等任务。
概述
我们将探索使用Cargo创建Rust项目的几种不同方式,包括在当前目录创建项目和创建独立子目录项目,并区分创建二进制应用包和库包。
使用Cargo创建项目
首先,确保你的系统已经安装了Cargo。如果你已经按照Rust的官方安装步骤操作,那么Cargo应该已经准备就绪。虽然不使用Cargo,手动创建文件也是可行的,但使用工具可以自动设置正确的项目结构,从而更高效地推进工作。
以下是创建项目的几种方法。
在当前目录创建项目
有两种主要方式可以在当前工作目录中初始化项目。
第一种方式是使用 cargo init . 命令。这个命令会利用当前目录的名称作为项目名来初始化项目。
cargo init .
运行此命令后,默认会创建一个二进制应用包。执行 ls 或 tree 命令查看目录,你会发现生成了 Cargo.toml 文件、src 目录以及 src/main.rs 文件。
查看 Cargo.toml 文件,可以看到项目名被设置为当前目录名(例如“example”),edition 字段设置为“2021”。Cargo.toml 是项目的清单文件,我们稍后会详细讨论其内容。这里的关键点是,因为它创建了包含 main 函数的 src/main.rs 文件,所以这是一个二进制应用包。
如果你想创建一个库包,可以使用 --lib 选项。
cargo init . --lib
运行此命令后,项目结构会发生变化:不再有 src/main.rs 文件,取而代之的是 src/lib.rs 文件。查看 src/lib.rs,可以看到它定义了一个模块和测试,以及一个名为 add 的公共函数。这为编写可复用的代码库提供了基础。
创建独立子目录项目
上一节我们介绍了在当前目录创建项目,本节我们来看看如何创建一个位于独立子目录中的新项目。
要实现这一点,我们需要使用 cargo new 命令,而不是 cargo init。cargo new 命令允许我们指定一个路径参数,该路径将成为新项目的目录名。
例如,要创建一个名为 my_project 的二进制应用包,可以运行:
cargo new my_project
这个命令会创建一个名为 my_project 的子目录,并在其中初始化项目。使用 tree 命令查看,可以看到所有项目文件(如 Cargo.toml 和 src/main.rs)都包含在这个子目录内。这种方式有助于将项目内容组织在独立的文件夹中。
同样,你也可以使用 --lib 选项来创建一个库包。
cargo new my_lib --lib
执行后,会在 my_lib 目录下创建一个库包,其中包含 src/lib.rs 文件,而不是 src/main.rs 文件。
总结
本节课中,我们一起学习了使用Cargo创建Rust项目的四种方法:
- 在当前目录创建二进制应用包:
cargo init . - 在当前目录创建库包:
cargo init . --lib - 在新子目录创建二进制应用包:
cargo new <project_name> - 在新子目录创建库包:
cargo new <project_name> --lib

二进制应用包主要用于生成可执行文件,而库包则用于封装可供其他应用程序使用的函数、方法和结构体。掌握这些创建项目的方法是开始Rust编程之旅的第一步。
026:项目文件概览 📁

在本节课中,我们将一起探索一个典型的Rust项目包含哪些文件。了解这些文件的结构和用途,是管理和构建Rust项目的基础。
概述
一个Rust项目包含多种不同的文件,不同的项目会使用不同的文件和结构。我们已经初步接触过其中一些文件。本节将通过一个名为“re_split”的小型命令行工具项目和一个库项目作为例子,详细介绍常见的项目文件及其作用。
项目文件详解
1. README.md 文件
README.md是一个Markdown格式的文件,用于书写项目的初始描述,说明项目是关于什么的。这通常是了解项目的第一步。
2. 许可证文件
项目通常会包含一个许可证文件。虽然不是每个人都使用相同格式的许可证,但包含许可证文件是相当常见的做法。
3. Cargo.toml 文件
正如之前所见,Cargo工具会生成一些文件,Cargo.toml就是其中之一。它是项目的清单文件,定义了项目的元数据和依赖关系。
以下是Cargo.toml文件的一个示例片段:
[package]
name = "re_split"
version = "0.1.0"
authors = ["Your Name"]
edition = "2021"
[dependencies]
clap = { version = "0.3", features = ["derive"] }
在这个文件中,你可以看到我的名字、使用的Rust版本、包的版本和名称,以及我已有的一些依赖项。我定义了一个名为clap的依赖,并指定了其版本和使用的特性。
我们不会深入探讨Cargo.toml的所有细节,但依赖项总是以这种方式在这里定义。这个文件包含了关于我的包的详细信息和我想要的一些设置。其中一些内容是预先生成的,但你可以阅读更多关于Cargo.toml以及所有可以实际添加的字段的信息,所有这些都可以在清单文档中找到。
4. Cargo.lock 文件
Cargo.lock文件我们尚未详细探讨,它的作用是锁定我的库或包(在本例中)所使用的所有库及其确切版本。
以下是一个Cargo.lock文件的简化视图:
[[package]]
name = "anystream"
version = "0.3.0"
dependencies = [
"clap-builder",
"clap-derive",
"lex",
]
你可以看到这里有一个名为anystream的包,版本是0.3.0,它有几个依赖项。这个文件的作用是收集依赖项的依赖项。我只有一个名为clap的依赖,但实际上,如果我们搜索它,会发现它就在这里。它会拉取各种其他依赖,如clap-builder、clap-derive和lex等。所有这些都会被拉取进来,这使得任何想要从头开始重建项目的人都能做到。
关于此文件的一个注意事项是:对于二进制包(如本例),通常包含此文件很常见。但如果你正在开发一个库,你可能不希望进行版本锁定并包含Cargo.lock文件。这一点需要记住,因为对于库来说,用Cargo.lock锁定版本通常不太合适,只包含带有依赖项的Cargo.toml会更灵活。
5. src 源代码目录
src目录在Rust中非常常见,实际上是预期的结构。我们之前看到,如果我们想扩展一些模块,会有一个特殊的文件叫lib.rs。
对于命令行工具、二进制包或可执行文件,main.rs将是你要使用的文件。同样,如果这只是一个库,那么我们就不会有main.rs,而只会有lib.rs。
现在这是一个非常小的项目。让我们看看另一个来自someund(他写了很多Rust代码)的小项目。在这种情况下,你可以看到文件非常相似。这是一个库,不是一个命令行工具,但你可以看到它包含了README.md,它确实有一个我们尚未介绍的Makefile,还有一个许可证文件。
让我们看看它的Cargo.toml。这里我们看到了更多的字段,唯一的依赖是一个叫slab的库。它没有包含Cargo.lock文件,正如我们从我的命令行工具(我的二进制可执行文件)中看到的那样。这是一个区别:这是一个库。
6. examples 示例目录
库项目通常包含一个examples目录,你可以查看其中的示例,这些示例能让你了解如何使用这个库。
7. 模块化与源文件组织
在src目录中,会有lib.rs(而没有main.rs),并且会有所有其他文件,这些文件允许你在开始模块化时分离关注点。我们还没有研究模块及其工作原理,但在这种情况下,这足以让我们了解包的概貌。
8. Makefile 文件
我们稍后肯定会看看Makefile,这绝对是一个你在Rust项目中也会看到的非常常见的模式。它使你能够将某些命令组合在一起。例如,component rustfmt确保格式化工具会被安装和存在;类似的还有format-check、lint和testing。所有这些都会存在,这是一种在整个项目中规范操作的方式。这绝对是值得关注的东西。
总结


本节课中,我们一起学习了Rust项目的常见文件概览。我们介绍了README.md、许可证文件、Cargo.toml(项目清单)、Cargo.lock(依赖锁定)、src源代码目录(包含main.rs或lib.rs)、examples示例目录以及Makefile等文件的作用和区别。理解这些文件是有效管理和构建Rust项目的重要基础。
027:Rust代码基本组件 🧱

在本节课中,我们将学习Rust代码的基本组成部分。通过分析一个示例项目,我们将了解关键字、语句、作用域、函数定义、变量声明以及返回值的语法。这些是理解任何Rust程序的基础。
使用 use 关键字导入模块
首先,我们来看代码顶部的 use 关键字。它用于导入模块或库,并将其引入当前文件的作用域。
use std::env;
在上面的例子中,std::env 模块被导入。这使得我们可以在文件后续的代码中直接使用 env 模块中的功能,例如在第6行。双冒号 :: 用于表示路径分隔,类似于其他编程语言中的点 . 或斜杠 /。每个 use 语句都以分号 ; 结束,表示该语句的终止。
函数定义与作用域
接下来,我们看看如何定义函数。在Rust中,使用 fn 关键字来声明函数。
fn main() {
// 函数体
}
这里的函数名为 main,它不接受任何参数(括号 () 内为空)。函数体由一对花括号 {} 包裹,这定义了一个作用域。花括号内的所有代码都处于这个局部作用域中,变量和逻辑通常不会影响到外部。虽然我们暂时不深入嵌套作用域,但理解基本作用域概念很重要。
使用 let 声明变量
在函数体内,我们使用 let 关键字来声明变量。
let args: Vec<String> = env::args().collect();
let 用于将值绑定到一个变量名上。我们会在后续课程中详细学习变量和类型。注意,这里再次使用了双冒号 :: 来调用特定模块下的函数。
结构体与属性
现在,让我们转向 lib.rs 文件,这里展示了更复杂的结构。除了 use 语句,我们还看到了以 # 开头的属性。
#[derive(Debug)]
struct Cli {
pattern: String,
path: std::path::PathBuf,
}
属性为代码提供了额外的元数据或指令。例如,#[derive(Debug)] 告诉Rust编译器自动为 Cli 结构体实现 Debug 特性,以便于打印调试信息。结构体本身是一种组织相关数据的方式。它的内容同样被包裹在花括号 {} 定义的作用域内。
可见性修饰符 pub
在结构体定义中,我们遇到了 pub 关键字。
pub struct Cli {
pub pattern: String,
pub path: std::path::PathBuf,
}
pub 代表“公开”。在Rust中,默认所有项(函数、结构体、字段等)都是私有的,只能在当前模块内访问。使用 pub 关键字可以将其暴露给外部模块使用。这对于构建库和定义公共API至关重要。
函数返回值与分号规则
最后,我们探讨函数返回值的一个关键语法点。观察以下函数:
pub fn read_stdin() -> String {
let mut buffer = String::new();
std::io::stdin().read_line(&mut buffer).unwrap();
buffer
}
这个函数声明返回一个 String 类型。请注意函数体的最后一行 buffer 没有以分号 ; 结尾。在Rust中,函数的返回值由最后一个表达式的值决定,如果该表达式后没有分号,则它就成为返回值。
如果我们在最后一行加上分号,像这样:
buffer;
那么这行代码就变成了一个语句,它没有值,函数将隐式返回一个空元组 (),这与声明的 -> String 返回类型冲突,会导致编译错误。因此,有意返回值时,必须省略末尾的分号。

本节课中我们一起学习了Rust代码的基本组件。我们了解了如何使用 use 导入模块,如何使用 fn 和花括号定义函数与作用域,如何使用 let 声明变量,初步认识了结构体、属性以及 pub 可见性修饰符,并掌握了通过省略分号来返回函数值的关键规则。理解这些基础元素是阅读和编写Rust程序的第一步。
028:变量赋值与不可变性 🦀

在本节课中,我们将学习Rust中变量赋值的基本概念,特别是其独特的默认不可变性规则。我们将通过编写代码、观察编译器错误并修复它们,来理解如何声明变量、为何默认不可变,以及如何显式地声明可变变量。
变量赋值基础
上一节我们介绍了课程概述,本节中我们来看看Rust中如何进行基本的变量赋值。
在Rust中,使用 let 关键字来声明一个变量。其基本语法非常直接:let 后跟变量名,然后是赋值表达式。
let name = "Alfredo";
let weight = 190;
在上面的代码中,我们声明了两个变量 name 和 weight。Rust编译器会自动推断它们的类型(分别是字符串和整数)。每个语句以分号 ; 结尾。
类型推断与运算
当我们对变量进行运算时,需要注意操作数的类型必须匹配。Rust是一门强类型语言,编译器会严格检查。
以下是一个常见的错误示例:
let weight = 190;
let kilos = weight / 2.2; // 错误:整数除以浮点数
运行这段代码会产生编译器错误:“cannot divide integer by float”。这是因为 weight 是整数类型,而 2.2 是浮点数。Rust没有为“整数除以浮点数”这个操作提供默认实现。
要修复这个错误,我们需要确保操作数类型一致。可以将其中一个操作数转换为浮点数:
let weight = 190.0; // 将 weight 声明为浮点数
let kilos = weight / 2.2; // 现在可以正确计算
或者,也可以在运算时进行类型转换。修复后,程序可以正常运行并输出结果。
变量的默认不可变性
这是Rust一个核心的安全特性。与许多其他语言不同,Rust中所有通过 let 声明的变量默认都是不可变的。这意味着一旦给变量赋值,就不能再改变它的值。
请看以下代码:
let message = String::from("Hello");
message.clear(); // 错误:尝试修改不可变变量
在这段代码中,我们尝试调用 .clear() 方法来清空 message 字符串的内容。由于 message 默认是不可变的,编译器会报错:“cannot borrow message as mutable”。
如何声明可变变量
如果你确实需要一个其值可以改变的变量,必须使用 mut 关键字来显式声明。
以下是修复上述错误的方法:
let mut message = String::from("Hello"); // 使用 mut 声明可变变量
message.clear(); // 现在可以成功清空字符串
通过在 let 后添加 mut,我们告诉编译器这个 message 变量是可变的,允许后续修改。
关于变量重新赋值,还有一点需要注意:在同一个作用域内,如果变量已经用 let 声明过,后续重新赋值时不需要再次使用 let 关键字。但是,如果变量是不可变的,重新赋值仍然会出错。
let height = 180;
height = 185; // 错误:不能给不可变变量赋值两次
let mut height = 180; // 声明为可变
height = 185; // 正确:可以修改可变变量的值
总结

本节课中我们一起学习了Rust变量赋值的核心概念:
- 使用
let关键字进行变量声明,编译器支持类型推断。 - 进行运算时需确保操作数类型匹配,否则编译器会报错。
- Rust变量默认是不可变的,这是其保障内存安全和并发安全的重要机制。
- 如果需要修改变量的值,必须使用
mut关键字显式声明变量为可变的。 - 理解默认不可变性有助于编写更安全、更易于推理的Rust代码。
029:控制流基础 🚦

在本节课中,我们将要学习Rust中控制流的基础知识。控制流是编程的核心概念之一,它允许程序根据条件执行不同的代码块。我们将通过简单的示例来理解if、else if和else语句的使用方法。
基础语法与结构
上一节我们介绍了Rust的基本结构,本节中我们来看看控制流语句的语法。Rust中的控制流语句使用if、else if和else关键字,其基本结构如下:
if condition {
// 条件为真时执行的代码
} else if another_condition {
// 另一个条件为真时执行的代码
} else {
// 所有条件都不满足时执行的代码
}
第一个示例:布尔条件
以下是使用布尔变量进行条件判断的示例。我们首先声明一个布尔变量,然后根据其值执行不同的代码块。
fn main() {
let proceed = true;
if proceed {
println!("This is proceeding");
} else {
println!("Nope, not proceeding");
}
}
在这个例子中,变量proceed被赋值为true。由于条件if proceed评估为真,程序将输出“This is proceeding”。如果将proceed的值改为false,程序将输出“Nope, not proceeding”。
第二个示例:数值比较
接下来,我们看看如何使用比较运算符进行条件判断。以下示例根据身高值输出不同的分类。
fn main() {
let height = 190;
if height > 180 {
println!("That's tall");
} else if height > 170 {
println!("Average");
} else {
println!("Short");
}
}
程序首先检查height > 180。如果为真,输出“That's tall”。如果为假,则检查else if height > 170。如果这个条件为真,输出“Average”。如果所有条件都不满足,则执行else块,输出“Short”。
常见错误与调试
在编写控制流语句时,初学者常会忘记语句结束的分号。例如,以下代码会导致编译错误:
let proceed = true // 缺少分号
if proceed {
println!("This is proceeding");
}
编译器会给出明确的错误信息,指出在特定行和列期望一个分号。按照提示添加分号即可修复错误。
总结

本节课中我们一起学习了Rust控制流的基础知识。我们了解了if、else if和else语句的基本语法,并通过布尔条件和数值比较两个示例实践了它们的用法。我们还讨论了常见的语法错误及其调试方法。掌握这些基础控制流语句是编写逻辑清晰程序的重要一步。
030:变量遮蔽演示

概述
在本节课中,我们将要学习Rust语言中的一个特性:变量遮蔽。我们将了解什么是变量遮蔽,如何在Rust中使用它,以及在使用过程中需要注意的一些重要事项,特别是与变量可变性和类型系统相关的细节。
变量遮蔽的概念
变量遮蔽是许多编程语言中常见的特性,Rust语言也支持这一特性。
变量遮蔽指的是定义一个变量后,重新声明同一个变量名并赋予它新的值。这个新值可以与之前的值类型相同,也可以不同。
基础遮蔽演示
以下是变量遮蔽的一个基础示例。
let height = 190;
let height = height - 20;
在这个例子中,我们首先定义了一个名为height的变量,其值为190。随后,我们使用let关键字再次声明height,将其重新赋值为原值减去20的结果。
可变性与遮蔽的区别
上一节我们介绍了基础的变量遮蔽。本节中我们来看看变量遮蔽与可变变量赋值之间的一个重要区别。
如果我们尝试直接修改一个不可变变量的值,编译器会报错。
let height = 190;
height = height - 20; // 错误:不能给不可变变量赋值
错误信息提示我们正在尝试修改一个不可变变量。为了使上述代码正常工作,我们需要将变量声明为可变的。
let mut height = 190;
height = height - 20; // 正确:可变变量可以重新赋值
请注意,使用mut关键字是重新赋值,而使用let进行遮蔽是重新声明一个新变量,只是名字相同。这是两个不同的操作。
结合条件表达式的遮蔽
Rust中的if-else块可以用作表达式并返回一个值,这个特性可以与变量遮蔽结合使用。
以下是结合条件表达式进行变量遮蔽的示例。
let height = 170;
let result = if height > 180 {
"tall"
} else if height > 160 {
"average"
} else {
"short"
};
在这段代码中,if-else表达式根据height的值计算出结果字符串(“tall”、“average”或“short”),然后将这个结果赋值给新声明的变量result。注意,if和else分支的代码块末尾没有分号,这表明它们是一个表达式,其值是代码块中最后一行语句的值。
遮蔽时改变变量类型
变量遮蔽的一个独特之处在于,重新声明变量时,可以将其更改为完全不同的类型。
以下是改变变量类型的遮蔽示例。
let health = if height < 180 { "good" } else { "unknown" };
let health = true; // 将字符串类型的 health 遮蔽为布尔类型
我们首先将health定义为一个字符串类型(“good”或“unknown”)。随后,我们再次使用let声明health,并将其赋值为布尔值true。Rust编译器允许这样的操作。
重要注意事项
虽然Rust允许通过遮蔽更改变量类型,但开发者需要谨慎使用这一特性。
Rust是一门强类型语言,其类型系统是保证代码安全性和正确性的重要基石。随意更改变量类型可能会破坏类型安全带来的好处,使代码难以理解和维护。除非有充分的理由(例如在某些循环控制结构中),否则应避免仅仅为了便利而更改变量的类型。保持变量类型的清晰和一致,是编写高质量Rust代码的良好实践。
总结

本节课中我们一起学习了Rust的变量遮蔽特性。我们了解到变量遮蔽允许我们使用let关键字重新声明同名变量,并可以赋予其新的值,甚至可以是不同的类型。我们区分了变量遮蔽与使用mut进行重新赋值的不同。同时,我们也探讨了如何将遮蔽与条件表达式结合使用,并强调了在强类型语言的上下文中,虽然可以更改变量类型,但应当非常谨慎,并有充分的理由。理解这些概念有助于我们更灵活且安全地使用Rust进行编程。
031:Rust入门总结 🎉
在本节课中,我们将总结Rust编程的入门知识,涵盖环境设置、项目创建、基础变量赋值以及核心工具Cargo的使用。通过本总结,你将巩固Rust的基础概念,为后续深入学习做好准备。
环境设置与项目创建
上一节我们介绍了Rust的基础概念,本节中我们来看看如何开始一个新项目。在Rust中,创建新项目通常是设置好环境后的首要步骤。


Cargo工具入门
我们初步了解了如何使用Cargo这一Rust的核心工具。Cargo是Rust的包管理器和构建系统,能极大地简化项目的创建、构建和管理过程。
以下是使用Cargo创建新项目的基本命令:
cargo new project_name
此命令会生成一个新的Rust项目目录,包含基本的项目结构和配置文件。
基础变量赋值
我们也学习了一些入门级的基础变量赋值知识。这是Rust编程的基石,为后续更复杂的概念打下基础。
在Rust中,变量默认是不可变的,使用let关键字进行声明:
let x = 5;
若要创建可变变量,需使用mut关键字:
let mut y = 10;
y = 15;
学习动力与下一步
希望你对继续学习感到兴奋。当我开始学习Rust时,设置第一个环境并看到像Cargo这样的工具如何使过程变得简单直接,这让我觉得非常有趣。
现在,你应该准备好进入下一步,超越基础变量赋值,探索Rust更强大的功能。
本节课中我们一起学习了Rust的入门总结,包括环境设置、Cargo工具的使用、基础变量赋值以及如何保持学习动力。掌握这些基础知识是成为熟练Rust开发者的关键第一步。
032:循环与控制流介绍 🔄
在本节课中,我们将要学习Rust中的循环与控制流。上一节我们介绍了变量与赋值,本节中我们来看看如何通过循环结构来控制程序的执行流程。

Rust提供了不同类型的循环,包括while循环和for循环。此外,还有一个名为loop的构造。如果你来自Java或Python等语言,可能会觉得有些不同,因为Rust的循环有其独特之处。我们将看到这些差异,并学习如何使用控制语句来跳出循环或匹配特定条件,从而有效地控制执行流程。你将能够编写代码进入循环,并精细地调整它,以决定何时跳出循环或继续执行更多代码。
循环类型
以下是Rust中主要的循环类型:
loop循环:这是一个无限循环,除非使用break关键字明确中断,否则会一直执行。while循环:只要给定的条件表达式求值为true,就会重复执行代码块。for循环:主要用于遍历一个集合(如数组、范围或迭代器)中的每个元素。
控制流语句
为了有效地管理循环,Rust提供了以下控制流语句:
break:立即终止当前循环。continue:跳过当前循环迭代的剩余代码,直接开始下一次迭代。
代码示例
让我们通过一些简单的代码示例来理解这些概念。
使用 loop
loop关键字会创建一个无限循环。
let mut count = 0;
loop {
count += 1;
println!("Count is: {}", count);
if count >= 5 {
break; // 当 count 达到 5 时跳出循环
}
}
使用 while
while循环在条件为真时持续运行。
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("Liftoff!");
使用 for
for循环是遍历集合的首选方式。
let arr = [10, 20, 30, 40, 50];
for element in arr.iter() {
println!("The value is: {}", element);
}
// 使用范围(Range)
for number in 1..4 { // 不包含4
println!("{}!", number);
}
本节课中我们一起学习了Rust中的循环与控制流。我们了解了三种主要的循环:loop、while和for,并掌握了如何使用break和continue语句来控制循环的执行。理解这些结构是编写能够重复执行任务并根据条件做出决策的程序的基础。
033:Rust循环入门 🌀
在本节课中,我们将要学习Rust语言中实现循环的几种不同方式。我们会逐一介绍它们,并解释在何种情况下你可能希望使用其中某一种循环方式。
循环关键字:loop
如果你来自Python或其他一些没有特定循环关键字的编程语言,loop关键字可能看起来有些新奇。你可能会疑惑,在什么情况下需要使用这样一个关键字来创建循环。
loop关键字的核心思想是,你可能希望一个代码块能够无限期地持续执行下去。
在像Python这样的编程语言中,你会使用类似while True:的写法。while True意味着一个永远不会改变的条件,它始终为真,因此循环会一直执行。在Rust中,我们使用loop关键字来达到同样的目的。它标志着其后的代码块将无限期执行。
当然,我们有一个尚未介绍但在此处会提前用到的条件:break。通过本课的学习,我们将能够从这个看似会永远运行的循环中“跳出”。
让我们来剖析一下这个将要运行循环的函数中的一些组成部分。
以下是该循环函数的一个示例:
fn main() {
let mut x = 1;
loop {
println!("{}", x);
x += 1;
if x > 5 {
break;
}
}
}
首先,我们定义了一个变量x并初始化为1。它被声明为mut(可变),因为我们在第10行将会改变x的值。由于我们要修改它,所以必须用mut来声明,这个特性允许我们使其可变。

运行这段代码,我们会看到输出从1到5。这很直观。
在这个例子中,println!用于打印,其后的x += 1;是一种累加并重新赋值的方式,这涉及“遮蔽”的概念,我们之前已经简单了解过,你应该对这个概念比较熟悉。这里不需要再次使用let,因为x仍在作用域内。
现在,如果x大于5,那么程序就会执行break,循环不再继续。这就是为什么这个if条件需要大括号,以便我们能在其中执行break。
以上就是loop关键字。当你想要无限期地运行一段代码的迭代循环,或者你实际上并不确切知道何时应该停止循环时,可以使用它。本例中的停止条件非常明确,但在你编写代码时,这个条件可能并不那么显而易见。在这种情况下,我们明确地定义了它。但在某些你并不确定何时停止的情况下,使用loop提供了一种对代码进行无限循环的方式。
上一节我们介绍了最基本的无限循环loop,本节中我们来看看另一种更常见的循环方式:while循环。
while循环
while循环在条件为真时重复执行代码块。它非常适合在循环开始前就知道循环条件的情况。
以下是while循环的一个基本结构:
while condition {
// 要执行的代码
}
当condition(条件)评估为true时,花括号{}内的代码就会执行。每次循环迭代前都会检查条件。如果条件为false,循环停止,程序继续执行循环之后的代码。
让我们看一个具体的例子,它实现了与之前loop示例相同的功能:
fn main() {
let mut x = 1;
while x <= 5 {
println!("{}", x);
x += 1;
}
}
在这个例子中,只要x小于或等于5,循环就会继续。每次迭代打印x的当前值,然后将其增加1。当x变成6时,条件x <= 5变为false,循环终止。
while循环提供了一种在已知条件下进行迭代的清晰、可读的方式。
了解了基于条件的while循环后,接下来我们看看如何对集合中的每个元素进行遍历,这就是for循环。
for循环
for循环用于迭代一个集合(如数组、向量或范围)中的每个元素。它是Rust中最常用、最安全的循环方式,因为它能避免越界错误。
以下是for循环遍历一个范围的基本语法:
for item in collection {
// 对每个item执行的操作
}
让我们用for循环重写之前的计数示例:
fn main() {
for x in 1..=5 {
println!("{}", x);
}
}
这里,1..=5是一个包含范围,表示从1到5(包含5)。for循环依次将x绑定到范围中的每个值(1, 2, 3, 4, 5),并执行循环体内的代码(打印x)。当范围中的所有值都被迭代过后,循环自动结束。
for循环简洁、安全,并且意图明确,是遍历已知集合的首选方法。
我们已经介绍了三种主要的循环方式。为了帮助你更好地理解和应用,以下是它们各自特点的总结。
循环方式对比与选择指南
以下是三种循环方式的关键区别和使用场景:
-
loop- 用途:创建无限循环,直到内部显式调用
break。 - 适用场景:当你需要循环持续运行,直到满足某个在循环内部才能确定的复杂条件时(例如,等待用户输入特定指令、监听网络事件)。当你不确定循环需要执行多少次时,它也很有用。
- 示例:游戏主循环、服务器监听循环。
- 用途:创建无限循环,直到内部显式调用
-
while- 用途:只要给定条件为
true,就重复执行代码块。 - 适用场景:当循环次数未知,但继续循环的条件在每次迭代前可以明确检查时。它比
loop更具可读性,因为停止条件在循环开头就声明了。 - 示例:读取文件直到末尾、处理用户输入直到输入“退出”。
- 用途:只要给定条件为
-
for- 用途:遍历集合(如数组、范围、向量)中的每个元素。
- 适用场景:当你需要按顺序处理集合中的每一项,并且确切知道要迭代的集合时。这是最常用、最安全的循环,能避免索引错误。
- 示例:计算数组元素之和、打印列表中的所有名称。
选择哪种循环取决于你的具体需求:需要无限循环吗?循环取决于一个可变条件吗?还是你只是在遍历一组已知的元素?

本节课中我们一起学习了Rust中三种主要的循环结构:用于无限循环的loop,用于条件循环的while,以及用于遍历集合的for循环。每种循环都有其特定的用途和优势。理解它们之间的区别将帮助你在编写Rust代码时做出更清晰、更高效的选择。记住,for循环通常是最安全、最惯用的选择,尤其是在处理集合时。
034:Rust条件语句

在本节课中,我们将学习Rust中一种特殊的条件语句用法,它允许我们在if let表达式中同时进行模式匹配和变量绑定。我们将通过一个具体的例子来演示这种语法,并理解其工作原理。
从变量定义开始
首先,我们定义一个变量maybe_number,它是Psalm 42的结果。实际上,maybe_number将是一个Option<i32>类型的值。
let maybe_number = Some(42);
理解 if let 语法
上一节我们定义了一个Option类型的变量。本节中我们来看看如何使用if let语法来处理它。
if let允许我们检查maybe_number是否是Some变体,并且如果是,则将其内部的值绑定到一个新变量上。
if let Some(number) = maybe_number {
println!("number is {}", number);
}
在这段代码中,number是一个在if let分支内即时创建的变量。如果maybe_number是Some(42),那么number将被赋值为42,并且代码块内的语句会执行。
运行示例代码
以下是运行上述代码的步骤和预期结果。
我们快速运行这段代码,可以看到输出是number is 42。这符合我们的预期。
处理 None 情况
现在,让我们修改代码,将maybe_number改为None,看看会发生什么。
let maybe_number: Option<i32> = None;
if let Some(number) = maybe_number {
println!("number is {}", number);
}
当我们尝试运行这段代码时,会遇到一个错误。错误信息指出Option<i32>没有实现std::fmt::Display trait,因此无法直接用于println!宏。
解决编译错误
为了解决这个错误,我们需要遵循编译器的建议,使用?操作符或者明确处理None的情况。但在这个例子中,我们遇到了另一个问题:需要类型注解。
编译器在maybe_number为None时,无法推断出其具体类型。因此,我们需要显式地告诉编译器maybe_number的类型。
let maybe_number: Option<i32> = None;
通过添加类型注解Option<i32>,我们明确了变量的类型,使得编译器能够正确处理后续的if let表达式。
最终代码与总结
修改后的完整代码如下所示:
fn main() {
let maybe_number: Option<i32> = None;
if let Some(number) = maybe_number {
println!("number is {}", number);
}
}
运行这段代码,程序将不会打印任何内容,因为maybe_number是None,if let分支不会执行。

本节课中我们一起学习了Rust中if let条件语句的用法。这是一个将模式匹配和变量赋值结合在一起的语法糖。你可能会在其他语言中看到不同的写法,但在Rust中,这是一种常见且实用的模式,特别是在处理Option或Result枚举时。通过本节的学习,你应该能够理解并开始使用这种简洁的条件赋值方式。
035:Rust中的while循环 🌀

在本节课中,我们将学习Rust编程语言中的while循环。我们将通过两个具体的例子来演示其基本用法和一个更高级的应用场景。while循环是一种控制流结构,只要给定的条件为真,它就会重复执行一段代码块。
基础while循环示例
首先,我们来看一个基础的while循环示例。这个例子展示了如何使用一个计数器变量来控制循环的执行次数。
fn main() {
let mut i = 0;
while i < 5 {
println!("i 的值为:{}", i);
i += 1;
}
}
以下是上述代码的逐步解释:
- 我们声明了一个名为
i的可变变量,并将其初始值设为0。 - 循环的条件是
i < 5。只要i的值小于5,循环就会继续执行。 - 在循环体内,我们首先打印出
i的当前值。 - 然后,我们使用
i += 1;将i的值增加1。这是通过变量遮蔽和加法赋值操作完成的。 - 当
i的值增加到5时,条件i < 5不再为真,循环终止。
运行这段代码,输出结果将是:
i 的值为:0
i 的值为:1
i 的值为:2
i 的值为:3
i 的值为:4
正如预期,循环从0开始,一直执行到4。花括号 {} 之间的代码块会重复执行,其行为与其他编程语言中的 while 循环非常相似。
进阶while循环示例
上一节我们介绍了基础的计数器循环,本节中我们来看看一个更贴近实际应用的例子。这个例子会持续读取用户的输入,直到用户输入特定的命令才停止。
use std::io;
fn main() {
let mut input = String::new();
while {
println!("请输入一个单词(输入‘stop’退出):");
input.clear();
io::stdin().read_line(&mut input).expect("读取失败");
input.trim() != "stop"
} {
println!("你输入的是:{}", input.trim());
}
println!("再见!");
}
以下是这个进阶示例的关键点说明:
- 我们首先导入了
std::io库,以便处理控制台输入。 - 创建了一个可变的
String变量input来存储用户输入。 - 循环的条件是一个代码块,它执行以下操作:
- 提示用户输入。
- 清空
input字符串,准备接收新输入。 - 从标准输入读取一行文本到
input变量中。 - 检查去除首尾空格后的输入是否不等于字符串
"stop"。如果不等于,条件为真,循环继续。
- 只要条件为真(即用户没有输入“stop”),循环体就会执行,打印出用户输入的内容。
- 一旦用户输入“stop”,条件变为假,循环结束,程序打印“再见!”并退出。
这个例子涉及一些我们尚未深入讲解的概念,例如:
io::stdin().read_line(&mut input):从标准输入读取一行。&mut input:以可变引用的方式传递input变量。.expect(“读取失败”):处理可能出现的错误。
不过,这些细节目前不重要,我们会在后续课程中详细讲解。这个例子的核心在于展示了如何在 while 循环中使用更复杂的条件逻辑,这里我们使用了条件表达式的否定形式(!= “stop”)来控制循环的继续执行。
运行这个程序,交互过程可能如下:
请输入一个单词(输入‘stop’退出):
hello
你输入的是:hello
请输入一个单词(输入‘stop’退出):
rust
你输入的是:rust
请输入一个单词(输入‘stop’退出):
stop
再见!
总结

本节课中我们一起学习了Rust语言中while循环的两种用法。我们首先通过一个计数器循环了解了其基本语法和工作原理。接着,我们探索了一个更高级的示例,它通过读取用户输入并检查特定条件来动态控制循环的终止。这两个例子是学习while循环非常实用和基础的入门方式。
036:Rust中的for循环 🔄

在本节课中,我们将要学习Rust编程语言中的for循环。for循环是遍历集合(如范围或向量)中元素的常用方式。我们将通过三个具体的例子来理解其语法和用法,包括如何创建范围、如何反向遍历以及如何遍历向量。
范围循环
上一节我们介绍了for循环的基本概念,本节中我们来看看如何使用for循环遍历一个数字范围。
在Rust中,范围由起始数字、两个点..和结束数字定义。例如,1..10表示从1开始(包含)到10结束(不包含)的范围。
以下是使用范围进行for循环的代码示例:
for i in 1..10 {
println!("{}", i);
}
运行这段代码,会打印数字1到9。这是因为范围1..10包含起始值1,但不包含结束值10。这可能导致“差一错误”,如果你希望包含结束值,就需要调整语法。
为了包含结束值,我们可以使用..=语法。以下是修改后的代码:
for i in 1..=10 {
println!("{}", i);
}
现在运行代码,会打印数字1到10。括号()的使用是可选的,不影响功能。
反向循环
了解了基本的范围循环后,我们来看看如何实现反向遍历。
Rust允许我们轻松地反向遍历一个范围。我们可以使用rev()方法来实现这一点。
以下是反向遍历范围的代码示例:
for i in (1..=5).rev() {
println!("{}", i);
}
运行这段代码,会按顺序打印数字5、4、3、2、1。这展示了for循环在处理范围对象时的灵活性。
范围对象提供了许多方法,例如clone()、count()和ref()等,可以通过在变量后加.来查看和使用这些方法。
遍历向量
最后,我们来看看如何使用for循环遍历一个向量(Vector)。
向量是Rust中一种常见的集合类型,类似于其他语言中的数组或列表。它可以存储一组相同类型的值。
以下是创建并遍历一个向量的代码示例:
let numbers = vec![1, 2, 3, 4, 5];
for number in numbers {
println!("{}", number);
}
在这段代码中,vec!是一个宏(macro),用于创建向量。宏在Rust中是一种强大的元编程工具,println!也是一个常用的宏。虽然它们看起来像函数,但本质上是宏。
运行这段代码,会依次打印向量中的每个数字:1、2、3、4、5。这与遍历范围的行为非常相似。
总结
本节课中我们一起学习了Rust中for循环的三种常见用法。
我们首先学习了如何使用for循环遍历一个数字范围,包括包含和不包含结束值的语法。接着,我们探讨了如何反向遍历一个范围。最后,我们了解了如何遍历向量这种集合类型。

for循环是Rust中处理迭代任务的基础工具,通过结合范围、方法和集合,可以高效地处理各种数据遍历需求。
037:Rust中的break与continue语句 🚦

在本节课中,我们将学习Rust中两个用于控制循环流程的关键字:break和continue。它们能让我们更灵活地管理循环的执行,无论是for循环、while循环还是其他循环结构。
概述
break和continue是控制流语句,它们允许我们在循环内部根据特定条件改变程序的执行路径。continue用于跳过当前迭代的剩余部分,直接进入下一次循环迭代;而break则用于立即终止整个循环。
上一节我们介绍了Rust中的基本循环结构,本节中我们来看看如何使用break和continue来更精细地控制循环行为。
continue语句:跳过当前迭代
continue关键字的作用是跳过当前循环迭代中剩余的代码,并立即开始下一次迭代。这在需要过滤掉某些不符合条件的值时非常有用。
以下是continue语句的一个典型应用场景,用于在循环中仅处理奇数:
for number in 1..=10 {
if number % 2 == 0 {
continue;
}
println!("{}", number);
}
在这段代码中,for循环遍历数字1到10(包含10)。if条件检查数字是否为偶数(即number % 2 == 0)。如果条件为真,continue语句会跳过当前迭代的剩余部分(即println!语句),直接进入下一次循环迭代。因此,只有奇数(1, 3, 5, 7, 9)会被打印出来。
break语句:终止循环
break关键字用于立即终止整个循环,无论循环条件是否仍然满足。这在找到所需结果或满足特定条件后提前退出循环时非常有用。
以下是break语句的一个示例,它在找到数字7后停止循环:
for number in 1..=10 {
if number == 7 {
break;
}
println!("{}", number);
}
在这段代码中,循环同样遍历1到10。当number等于7时,break语句被执行,整个循环立即终止。因此,程序只会打印数字1到6。
综合示例:结合使用break与continue
现在,让我们看一个结合使用break和continue的完整示例,以更好地理解它们如何协同工作。
for number in 1..=10 {
if number % 2 == 0 {
continue;
}
if number == 7 {
break;
}
println!("{}", number);
}
以下是这段代码的执行步骤分析:
- 循环从数字1开始。
- 检查
number % 2 == 0。对于偶数,continue语句跳过打印步骤。 - 对于奇数,检查
number == 7。当遇到数字7时,break语句终止整个循环。 - 因此,只有数字1、3、5会被打印出来。循环在遇到7时停止,不会处理数字9。
运行此代码,输出结果为:
1
3
5
应用场景与注意事项
break和continue语句可以应用于所有Rust循环结构,包括while循环和loop循环。它们为循环控制提供了额外的灵活性。
使用这些语句时,需要注意逻辑清晰,避免创建难以理解的循环逻辑。确保break条件最终能够被满足,以防止无限循环。
总结

本节课中我们一起学习了Rust中break和continue语句的用法。continue用于跳过当前迭代,直接进入下一次循环;break用于立即终止整个循环。通过合理使用这两个关键字,我们可以更有效地控制循环流程,编写出更清晰、更高效的Rust代码。
038:Rust中的match控制流语句 🔍

在本节课中,我们将要学习Rust编程语言中一个非常强大且独特的控制流结构——match语句。match语句提供了一种清晰、安全的方式来处理多种可能的情况,它比传统的if-else链更强大,尤其适用于枚举类型和模式匹配。我们将通过一个简单的例子来理解它的基本语法和工作原理。
概述
match控制流是一种处理不同场景的有趣方式。在其他编程语言中,你可能会看到case语句或关键字。但在Rust中,它被称为match,而不是case。它的核心思想是尝试将一个值与一系列模式进行匹配,并为第一个匹配成功的模式执行相应的代码块。
match语句的基本结构
match语句的基本语法如下:
match value_to_match {
pattern1 => expression1,
pattern2 => expression2,
// ...
_ => default_expression,
}
它的工作方式是:将需要匹配的值(写在match关键字后)与左侧列出的每一种可能性(即模式)进行比较。每一行代表一种独立的可能性。如果值匹配某个模式,则执行该模式右侧由=>符号引导的代码(可以是一个表达式或一个代码块)。
一个简单的match示例
让我们来看一个具体的例子。假设我们有一个变量name,我们想根据它的值打印不同的问候语。
以下是代码示例及其工作流程:
let name = "hello";
match name {
"goodbye" => println!("Sorry to see you go."),
"hello" => println!("Hi, nice to meet you!"),
_ => println!("I can't find a greeting: {}.", name),
}
- 变量
name被赋值为"hello"。 - 程序进入
match块,开始将name的值与各个分支进行匹配。 - 它首先与
"goodbye"比较,不匹配。 - 接着与
"hello"比较,匹配成功。 - 因此,程序会执行对应的代码块,打印出
"Hi, nice to meet you!"。 - 匹配成功后,
match表达式就会结束,不会继续检查后面的分支。
通配模式 _
在上面的例子中,最后一个分支使用了下划线_。这是一个通配模式,用于“捕获”所有前面未匹配到的情况。这在此类控制流结构中非常常见,它确保了程序的完备性,即使输入不符合任何预设条件,也有一个默认的处理方式。
结合用户输入
为了使程序更有趣,我们可以将match与从终端读取用户输入结合起来。这样,程序就能动态地响应用户的输入。
以下是增强版的代码示例:
use std::io;
fn main() {
println!("Enter a greeting:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");
let trimmed_input = input.trim(); // 使用`.trim()`移除输入首尾的空白字符和换行符
match trimmed_input {
"goodbye" => println!("Sorry to see you go."),
"hello" => println!("Hi, nice to meet you!"),
_ => println!("I can't find a greeting: {}.", trimmed_input),
}
}
在这个版本中:
- 程序提示用户输入一个问候语。
- 使用
io::stdin().read_line(...)读取用户的输入到字符串input中。 - 使用
.trim()方法处理输入字符串,移除可能因终端输入而产生的多余空格或换行符,使匹配更精确。 - 最后,使用
match语句根据处理后的输入来决定输出哪条消息。
通过这种方式,我们可以构建一个非常健壮的方式来处理终端输入。例如,你可以很容易地将这段逻辑放入一个while循环中,持续询问用户并做出响应。
总结
本节课中我们一起学习了Rust的match控制流语句。我们了解到:
match是Rust中用于模式匹配的核心语法,它比if-else链更清晰、更安全。- 其基本结构是将一个值与多个模式进行比较,并执行第一个匹配成功的分支对应的代码。
- 通配模式
_用于处理所有其他未匹配的情况,确保逻辑的完备性。 - 通过将
match与用户输入(如io::stdin().read_line)结合,并辅以字符串处理(如.trim()),我们可以构建出能够优雅处理多种情况的交互式程序。

match语句是Rust强大表达能力的体现,它允许你以非常简洁和可读的语法处理许多不同的情况,而这些情况在其他语言中可能需要冗长的if和else if条件链来完成。
039:循环与控制流总结 🎯

在本节课中,我们将对Rust语言中的循环与控制流概念进行总结。控制流是编程的核心,它决定了程序执行的路径。通过掌握条件判断和循环,你将能够为程序构建复杂的逻辑。
核心概念回顾
上一节我们介绍了各种循环和控制结构,本节中我们来总结它们的关键点。处理逻辑不仅是Rust,也是所有编程语言的核心。这是你未来使用Rust进行任何工作的基础模块。
以下是控制流的核心组成部分:
-
条件判断 (
if,else if,else):根据布尔表达式的值决定执行哪段代码。if condition { // 条件为真时执行 } else if another_condition { // 另一个条件为真时执行 } else { // 以上条件都不为真时执行 } -
循环 (
loop,while,for):用于重复执行代码块,直到满足特定条件。loop:无限循环,直到显式break。while:当条件为真时循环。for:遍历集合或范围。
-
循环控制 (
break,continue,return):用于改变循环的正常执行流程。break:立即退出当前循环。continue:跳过当前循环的剩余部分,直接开始下一次迭代。return:从当前函数返回值并退出。
你现在应该对所有这些if、else条件判断,以及跳出循环或应用其他逻辑感到更加得心应手。
重要性与应用
将逻辑应用于Rust程序(事实上是任何程序)同样是一个基础部分。掌握这些知识将使你能够在未来自己的程序中,就如何处理决策做出更好的判断。
本节课中我们一起学习了Rust中控制流的核心机制,包括条件执行和多种循环结构。理解并熟练运用这些基础构件,是构建更复杂、更智能程序的第一步。
040:函数基础介绍 🧩

在本节课中,我们将学习Rust中函数的基础知识。我们将了解如何构建函数,区分不返回值的“单元函数”和返回值的函数,并初步探讨panic!宏的用途与适用场景。
函数概述
当我们之前在Rust中添加循环、条件和变量赋值时,我们已经在使用一个特殊的函数——main函数。main函数是Rust可执行程序的入口点。现在,我们将从不同的视角来学习函数:我们将能够自己构建它们。
单元函数
首先,我们将看到一类被称为“单元函数”的函数。这类函数仅执行一些代码,不返回任何值。它们被调用,完成一些工作,然后结束。
以下是单元函数的一个简单示例:
fn greet() {
println!("Hello, world!");
}
在这个例子中,greet函数被调用时会打印一条消息,但它不向调用者返回任何结果。
返回值函数
上一节我们介绍了不返回值的单元函数,本节中我们来看看能够返回值的函数。这类函数在执行完工作后,会向调用者返回一个结果。
以下是一个返回值的函数示例:
fn add(a: i32, b: i32) -> i32 {
a + b
}
在这个例子中,函数add接收两个i32类型的参数,并返回一个i32类型的值,即两者之和。注意,在Rust中,最后一个表达式的值(不加分号)会隐式作为返回值。
认识 panic! 宏
最后,我们将简要了解panic!。它是Rust中的一个特殊宏,用于停止程序的所有执行。虽然你可能不会在生产代码中频繁看到它,但理解其适用场景很重要。
以下是panic!宏的调用方式:
panic!("Something went terribly wrong!");
当这行代码执行时,程序会立即终止,并打印出提供的错误信息。
何时使用 panic!
我们需要理解在什么情况下使用panic!是合理的,而在什么情况下可能不是好主意。
以下是需要考虑的几点:
- 在程序遇到不可恢复的错误时(例如,访问超出数组边界),使用
panic!是合理的。 - 在可以优雅处理的预期错误场景中(例如,文件未找到),使用
Result枚举进行错误处理通常是更好的选择。 - 在原型开发或测试代码中,
panic!可以用于快速标记未实现的功能或无法处理的情况。
课程总结
本节课中我们一起学习了Rust函数的基础知识。我们了解了作为程序入口的main函数,如何定义和执行不返回值的单元函数,以及如何编写能够返回值的函数。最后,我们初步探讨了panic!宏的作用,并讨论了其适用的场景与局限性。掌握这些概念是构建更复杂Rust程序的重要一步。
041:简单单元函数

在本节课中,我们将学习Rust中的函数,特别是被称为“单元函数”的类型。我们将了解什么是单元函数,如何定义和调用它们,并通过一个简单的例子来演示其工作流程。
概述
Rust是一门支持函数式编程的语言,函数在其中扮演着核心角色。本节我们将重点介绍一种特殊的函数——单元函数。这类函数执行某些操作,但不返回任何值。我们将通过一个具体的代码示例来理解其概念和用法。
单元函数的概念
上一节我们介绍了Rust中函数的基本角色。本节中我们来看看什么是单元函数。
在Rust中,当你调用一个函数,而该函数不返回任何内容时,这类函数被称为单元函数。它们的主要目的是执行某些处理或操作,而不是计算并返回一个结果。你可以将其理解为“调用即忘”的模式:你调用函数,它完成工作,但你并不期待它返回一个值,即使你可能向它传递了参数。
代码演示与分析
以下是本节课将要演示的代码示例。它展示了一个main函数如何调用一个名为process_numbers的单元函数。
fn main() {
let numbers = [1, 2, 3];
process_numbers(&numbers);
}
fn process_numbers(nums: &[i32]) {
let sum: i32 = nums.iter().sum();
println!("数字之和是 {}", sum);
if sum % 2 == 0 {
println!("和是偶数。");
} else {
println!("和是奇数。");
}
}
在这个例子中:
main函数是程序的入口点。process_numbers是一个我们定义的单元函数。- 我们向
process_numbers函数传递了一个切片(&[i32]),它看起来类似于其他编程语言中的列表。 - 该函数计算切片中所有数字的总和,并打印出该和以及它是奇数还是偶数。
工作流程解析
现在,让我们详细解析一下这个“调用即忘”的工作流程。
- 函数调用:在
main函数中,我们创建了一个包含数字1, 2, 3的数组,然后调用process_numbers函数,并将这个数组的引用(切片)传递给它。 - 执行过程:
process_numbers函数接收这个切片,计算其元素的总和(1+2+3=6),然后执行打印操作。 - 输出结果:运行程序后,我们将在控制台看到执行结果:“数字之和是 6”和“和是偶数。”。这些是函数内部处理工作的执行细节。
- 无返回值:关键点在于,
process_numbers函数完成了所有工作(计算和打印),但并没有使用return关键字向调用者(main函数)返回任何值。它的返回类型是(),即单元类型,这标志着它是一个单元函数。
总结

本节课中我们一起学习了Rust中的单元函数。我们了解到,单元函数是那些执行某些操作但不返回任何值的函数。它们遵循一种“调用即忘”的模式。我们通过一个传递切片、计算总和并打印结果的简单例子,演示了从主函数调用单元函数的完整且直接的工作流程。虽然Rust中还有更复杂的函数交互方式,但理解单元函数这一基础概念非常有用。
042:返回值 📝

概述
在本节课中,我们将学习Rust函数中返回值的重要性,并通过一个具体的例子来理解如何处理可能返回Option类型的函数,以确保我们的代码能够正确编译和运行。
返回值的重要性 🔑
在Rust这样的语言中,明确指定函数的返回类型至关重要。函数必须返回其声明中指定的类型,否则会导致编译错误。
例如,split_string函数被声明为返回一个String类型。这意味着无论函数内部逻辑如何,它最终都必须返回一个String。如果不满足这个条件,程序将无法通过编译。
问题演示与分析 🔍
上一节我们介绍了返回值的基本概念,本节中我们来看看一个具体的代码示例及其遇到的问题。
我们有一个main函数,其中调用了split_string。我们向该函数传递了三个参数。虽然参数的具体细节在此不那么重要,但核心问题是:我们期望chunk变量是一个String。
let chunk = split_string(...);
我们可以利用编辑器(如VS Code)的功能来查看类型提示。将鼠标悬停在chunk上,可以看到它被推断为Option<&str>类型,而不是我们期望的String。这导致了类型不匹配的错误。
错误信息可能看起来复杂,例如:“method to_string exists for Option<&str>...”。对于初学者,一个很好的建议是:先尝试运行代码,看看具体的错误上下文。
运行后,编译器给出了更清晰的建议:它提示我们考虑使用.expect()或.unwrap()方法来处理Option类型,或者检查值是否为None。
理解 Option 枚举 🧩
那么,问题的根源是什么?关键在于split_string函数(或类似函数)返回的类型是Option。
Option是Rust中的一个枚举(enum),它用于表示一个值可能存在(Some)也可能不存在(None)的情况。其定义大致如下:
enum Option<T> {
Some(T),
None,
}
这里的T是一个泛型参数,可以代表任何类型。对于Option<&str>,T就是&str(字符串切片)。
所以,result变量可能是Some(&str),也可能是None。直接对它调用.to_string()方法是行不通的,因为.to_string()是&str类型的方法,而不是Option类型的方法。
解决方案:使用 expect 方法 ✅
编译器建议我们使用.expect()方法。这个方法的作用是:如果Option是Some(value),它就取出这个value;如果是None,则使程序恐慌(panic)并输出我们提供的错误信息。
以下是修改后的代码:
let chunk = split_string(...).expect("Oops, something went wrong here!").to_string();
通过添加.expect("..."),我们做了两件事:
- 安全地从
Option中提取出内部的&str值(如果是None则程序终止)。 - 然后,对这个
&str值调用.to_string(),将其转换为函数签名要求的String类型。
修改后代码可以成功编译运行,例如输出分割后的字符串“word”。
无返回值函数:unit 类型 📦
之前我们讨论了有返回值的函数。那么,像main这样没有显式返回值的函数呢?
在Rust中,不返回任何值的函数实际上返回一个特殊的类型,称为单元(unit)类型,表示为()。它类似于其他语言中的void,但在Rust中它是一个具体的、唯一的类型。
fn main() -> () { // `-> ()` 通常可以省略
// 函数体
}
总结 🎯
本节课中我们一起学习了:
- 返回值的约束:Rust函数必须严格返回其声明的类型。
Option枚举:它是一个表示“有值(Some)”或“无值(None)”的枚举,常用于处理可能失败的操作。- 处理
Option:使用.expect()方法可以安全地从Option中提取值,并在值为None时提供明确的错误信息。 - 单元类型:没有显式返回值的函数实际上返回
()单元类型。


理解并正确处理返回值,特别是像Option和Result这样的枚举类型,是编写健壮、可编译Rust代码的基础。
043:使用参数 📚

在本节课中,我们将学习如何在Rust函数中使用参数,特别是如何处理可变数量的参数。我们将通过一个具体的例子来理解Rust中参数传递的类型系统和设计思路。
函数参数基础
在之前我们接触过的许多不同示例中,我们已经见过函数的参数。在本例中,main函数不接受任何参数,这由此处可见的空括号()表示。
然而,main函数依赖并实际调用了另一个名为sum的函数。sum函数有一个参数,这个参数本质上需要一个名为numbers的实参,并且该实参具有类型。类型由冒号:指示。在本例中,它使用了&符号,表示借用该值。
参数类型详解
它实际上使用了一个i32类型的切片,这就是实际的类型。该函数将返回一个i32类型的结果,即一个32位整数。这是一种表示整数的方式,我们使用的整数是32位长的。因此,我们对于要执行的操作非常明确。
这个函数要做的事情就是将所有的项相加,最后返回最终的总和,即所有项相加的结果。
Rust的参数设计哲学
这些细节本身并不那么重要,我们想要传入一些数字。在其他一些语言中,比如Python,你实际上可以传递任意数量的参数,其函数签名会与此非常不同。
但在Rust中,我们没有可变参数。这意味着所有参数都必须被明确定义,包括它们的类型。
实现“可变参数”功能
一种在Rust本身不支持的情况下实现类似可变参数支持的方法是,使用类似我们这里展示的方式:我们将多个不同的值传递到一个数据结构中。在本例中,我们传递的是一个数字切片。这绝对是处理该问题的一种方式,也是你可能需要习惯的方式。
你也可以使用向量,虽然我们还没有详细讨论向量,但将多个不同的值封装在一个数据结构中传递的能力,绝对是处理此问题的一种方法。在本例中,我们完全遵循了这一点。
代码执行流程
我们在此处借用切片,然后进行一些计算。它实际上并没有修改那个值,这是正确的,然后返回一个i32类型的结果。这样代码就能编译通过。
总结与对比
这就是我们如何在Rust中使用“可变参数”的方法,尽管Rust本身并不直接支持可变参数。可变参数意味着参数数量可以是零个或多个。归根结底,在像Python这样的语言中,这最终会转化为某种可迭代对象(在Python中我认为是元组),但在这里本质上是相同的事情。
因此,你需要更多地思考你传递的是什么,以及支持和使用它的方法。使用切片绝对是一种有效的方式。

本节课中我们一起学习了Rust函数参数的基本用法,理解了Rust严格类型系统下参数传递的特点,并通过切片数据结构探索了模拟“可变参数”功能的实现方式。关键在于将多个值封装到单一数据结构(如切片或向量)中进行传递。
044:借用概念演示 🧠

在本节课中,我们将学习Rust语言中一个核心且独特的概念:借用。我们将通过具体的代码示例,理解所有权、借用、移动和拷贝之间的区别,并学习如何避免常见的编译错误。
概述
Rust通过一套严格的所有权系统来保证内存安全,无需垃圾回收。借用是这套系统的关键组成部分,它允许你临时地引用一个值,而不获取其所有权。理解借用对于编写正确且高效的Rust代码至关重要。
上一节我们介绍了Rust的基本语法和所有权概念,本节中我们来看看借用的具体规则和实际应用。
变量定义与所有权
首先,我们定义几个变量作为示例:
let mut vector = vec![1, 2, 3]; // 一个可变的向量(动态数组)
let my_int = 10; // 一个整数
let my_string = String::from("Hello World"); // 一个字符串
这些定义都能正常编译,没有问题。
整数与拷贝行为
让我们从一个简单的整数函数开始:
fn own_integer(x: i32) {
println!("{}", x + 1);
}
fn main() {
let my_int = 10;
own_integer(my_int);
println!("{}", my_int); // 这里仍然可以访问 my_int
}
own_integer 函数接收一个 i32 类型的参数 x,并打印 x + 1 的结果。当我们调用 own_integer(my_int) 时,程序可以正常运行,并且在函数调用后,我们仍然可以打印 my_int 的值。
这是因为对于像整数、布尔值这样的简单类型(实现了 Copy trait),Rust会在传递时自动进行值拷贝。 拷贝的成本很低,所以 my_int 的所有权并没有被移动,原变量依然有效。
字符串与移动行为
现在,让我们对字符串进行类似的操作:
fn own_string(s: String) {
println!("{}", s);
}
fn main() {
let my_string = String::from("Hello World");
own_string(my_string);
println!("{}", my_string); // 这里会导致编译错误!
}
当我们保存这段代码时,编译器会立即报错。函数 own_string 只是打印了字符串,并没有修改它,为什么不行呢?
原因在于:字符串(String)类型的大小在编译时是未知的,进行深拷贝的成本可能很高。因此,Rust默认不会拷贝它,而是进行“移动”。
当我们将 my_string 传递给 own_string 时,所有权从 main 函数中的变量 my_string 移动到了函数 own_string 的参数 s 中。移动之后,原来的 my_string 就失效了,不能再被使用。这就是编译器报错“value borrowed here after move”的原因。
引入借用:使用引用
那么,如何让函数能够读取字符串而不获取所有权呢?答案是使用引用,也就是“借用”。
以下是修改方法:
fn own_string(s: &String) { // 参数类型改为 &String,表示一个不可变引用
println!("{}", s);
}
fn main() {
let my_string = String::from("Hello World");
own_string(&my_string); // 传递一个引用(借用)
println!("{}", my_string); // 现在可以正常访问了
}
我们在函数参数类型前加上 & 符号,表示接收一个引用。在调用函数时,我们也使用 & 符号来传递变量的引用。
&my_string 的含义是“我将 my_string 借给你用一下”。 函数 own_string 只是临时借用这个值来打印,用完后所有权仍然归 main 函数中的 my_string 所有。因此,在函数调用后,我们依然可以访问 my_string。
向量与可变借用
向量的行为与字符串类似。尝试直接传递所有权也会导致移动:
fn own_vector(v: Vec<i32>) {
// ... 对向量进行操作
}
fn main() {
let mut vector = vec![1, 2, 3];
own_vector(vector);
// 此后不能再使用 vector
}
如果我们希望函数能修改向量,则需要使用可变引用:
fn modify_vector(v: &mut Vec<i32>) { // 参数类型为 &mut Vec<i32>
v.push(10);
}
fn main() {
let mut vector = vec![1, 2, 3];
modify_vector(&mut vector); // 传递可变引用
println!("{:?}", vector); // 输出: [1, 2, 3, 10]
}
注意,要使用可变引用,变量本身必须用 mut 声明为可变的,并且在传递引用时使用 &mut。
替代方案:返回新值
有时,为了避免复杂的借用和所有权转移,一个更简单的策略是创建并返回新的数据,而不是修改传入的参数。
例如,我们想实现一个向向量添加元素但不修改原向量的函数:
fn add_to_vector(v: &Vec<i32>) -> Vec<i32> { // 接收不可变引用,返回新的 Vec<i32>
let mut new_vector = v.clone(); // 克隆原向量的数据
new_vector.push(10);
new_vector // 返回新的向量
}
fn main() {
let vector = vec![1, 2, 3];
let new_vector = add_to_vector(&vector);
println!("Original: {:?}", vector); // 输出: [1, 2, 3]
println!("New: {:?}", new_vector); // 输出: [1, 2, 3, 10]
}
在这个例子中:
- 函数
add_to_vector接收一个向量的不可变引用&Vec<i32>。 - 在函数内部,我们通过
.clone()方法创建了原向量的一个完整拷贝(这需要消耗额外的内存)。 - 我们对拷贝进行修改,然后将其返回。
- 这样,原向量
vector完全没有被触动,所有权清晰,避免了借用带来的复杂性。

总结
本节课中我们一起学习了Rust的核心概念——借用。我们通过对比整数、字符串和向量的不同行为,理解了以下关键点:
- 拷贝:适用于实现了
Copytrait 的简单类型(如i32,bool),传递时自动复制值,原变量保持不变。 - 移动:对于复杂类型(如
String,Vec),默认传递会转移所有权,原变量随之失效。 - 借用:通过引用(
&)允许函数临时访问数据而不获取所有权。这分为:- 不可变借用 (
&T):允许多个同时存在,但不能修改数据。 - 可变借用 (
&mut T):同一时间只能有一个,并且可以修改数据。
- 不可变借用 (
- 替代策略:当所有权和借用规则使代码变得复杂时,可以考虑通过克隆数据并返回新值的方式来简化逻辑。
理解这些概念是掌握Rust内存安全模型的基础。最好的学习方法是多写代码,并仔细阅读编译器给出的错误信息,尝试用本节课学到的概念去理解它们。
045:使用panic停止程序 🚨

在本节课中,我们将学习Rust中一个名为panic的核心概念。panic是一种立即停止程序执行的机制,通常用于处理程序无法或不应继续运行的严重错误情况。
概述
在JavaScript等语言中,你可以使用throw来抛出错误或异常。在Python中,你可以引发这些异常。而在Go和Rust这类语言中,你可以调用panic。panic会立即停止程序的执行,并输出一条消息,导致程序提前退出,同时可以选择性地提供回溯信息。
理解panic的基本用法
上一节我们介绍了panic的概念,本节中我们来看看它的基本用法。
我们可以在代码的最顶部直接调用panic!宏。例如,我们可以这样写:
panic!("Error: We are panicking or we're crashing the program");
现在,文本编辑器会立即知道这行代码之后的语句将不会被执行。如果我们运行这段代码,会看到类似“unreachable statement”的警告,因为panic!之后的任何代码都是不可达的。执行程序会输出“thread ‘main’ panicked”,并显示我们提供的错误信息,然后程序崩溃退出。
在条件逻辑中使用panic
直接调用panic可能不是最佳实践。更常见的做法是在检测到特定错误条件时触发panic。
以下是使用panic处理错误条件的一个示例:
fn main() {
let numbers = vec![1, 2, 3, 4, -5];
for &number in &numbers {
if number < 0 {
panic!("Negative number found: {}", number);
}
println!("{}", number);
}
}
在这个例子中,我们遍历一个数字向量。当遇到负数(例如-5)时,程序会调用panic!并停止执行。这是一种处理你希望不惜一切代价让程序崩溃并停止执行的场景的方法,因为程序无法合理地继续运行。
何时使用panic?
那么,为什么要使用panic呢?panic在Rust中确实可用,但在生产环境的优秀Rust代码中,除非在非常特定的情况下,通常不鼓励使用它。
一般来说,你不会用panic来处理错误条件。panic适用于示例代码或演示中,当你希望立即停止执行时。就像我们之前在循环前做的那样。
当然,也存在一些你可能想要实现panic的情况,例如当某个条件是100%禁止的,违反它意味着程序必然陷入麻烦。但你必须非常小心地使用它。
谨慎使用panic的示例
为了更清晰地说明,让我们看一个可能意外引发panic的例子。我们还没有深入讨论向量或字符串,但这里有一个简单的示例:
fn main() {
let s = String::from("hello world");
println!("{}", s);
// 尝试访问不存在的索引会导致panic
// println!("{}", s.chars().nth(100).unwrap());
}
如果我们尝试访问字符串s的索引100(例如使用s.chars().nth(100).unwrap()),这很可能会引发panic,因为“hello world”没有那么多字符。
因此,请谨慎使用panic。只有在某些你必须使用panic的情况下,当你确信在你编写的程序中,某种情况是绝对不可想象的,并且除了panic别无他法时,才使用它。我们在Rust标准库的许多不同部分都能看到这种用法,这使其成为可接受的,但它并不是处理错误最常见或最推荐的方式。
总结

本节课中我们一起学习了Rust中的panic机制。我们了解到panic是一种立即终止程序执行的工具,适用于处理不可恢复的严重错误。我们演示了它的基本用法、如何在条件逻辑中触发它,并重点讨论了应谨慎使用panic的原因。在大多数情况下,更推荐使用Rust中提供的Result等错误类型来优雅地传递和处理错误。
046:使用match进行基础错误处理 🛠️

在本节课中,我们将学习如何使用 match 表达式来处理Rust程序中的错误。与直接让程序崩溃(panic)不同,match 提供了一种更可控、更灵活的方式来应对可能出现的各种问题,例如文件不存在或权限不足等情况。
概述
在之前的课程中,我们接触过使用 panic 来处理错误。本节将介绍另一种更优雅的错误处理方式——match 表达式。我们将通过一个打开并读取文件的常见场景,演示如何利用 match 来区分操作成功与失败,并对不同类型的错误进行针对性的处理。
使用match处理文件打开错误
以下是一个使用 match 处理文件打开操作的示例。文件操作可能因多种原因失败,例如文件不存在、路径错误或权限不足,match 表达式允许我们优雅地处理这些情况。
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() {
let file_result = File::open("example.txt");
match file_result {
Ok(file) => {
// 文件成功打开,创建缓冲阅读器并逐行打印内容
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(content) = line {
println!("{}", content);
}
}
}
Err(error) => {
// 文件打开失败,进一步匹配错误类型
match error.kind() {
std::io::ErrorKind::NotFound => {
panic!("文件未找到: {}", error);
}
_ => {
println!("无法打开文件: {}", error);
}
}
}
}
}
上一节我们介绍了 match 的基本结构,本节中我们来看看代码的具体逻辑。
代码逻辑解析
以下是代码各部分的详细解释:
- 尝试打开文件:
File::open("example.txt")返回一个Result<File, std::io::Error>类型。Result是一个枚举,要么是包含成功值的Ok,要么是包含错误信息的Err。 - 匹配Result:外层
match表达式对file_result进行匹配。Ok(file)分支:如果文件成功打开,变量file被绑定到打开的文件句柄。随后,程序会创建一个BufReader来高效地读取文件,并逐行打印其内容。这是程序的“成功路径”。Err(error)分支:如果文件打开失败,变量error被绑定到具体的错误信息。为了进行更精细的处理,我们使用了嵌套的match。
- 嵌套匹配错误类型:内层
match针对error.kind()返回的错误类型进行匹配。NotFound分支:如果错误原因是“文件未找到”,我们选择让程序panic并终止,同时打印出错误信息。这对于某些关键文件缺失的情况是合理的。_(下划线)分支:这是一个通配符模式,匹配所有其他类型的错误(如权限错误)。对于这些错误,我们选择不崩溃程序,而是简单地打印一条错误信息并继续执行。
关于类型一致性的重要说明
在使用 match 处理 Result 并赋值给变量时,必须确保所有分支返回的类型一致。在上面的初始示例中,file_result 被匹配并期望得到一个 File 类型。因此,Ok 分支返回 File,而 Err 分支也必须返回一个能兼容的类型(或者通过 panic! 使该分支不返回任何值)。
如果我们在 Err 分支中错误地尝试返回其他类型(例如直接使用 println!,它返回 () 单元类型),编译器会报类型不匹配的错误。
另一种模式:不绑定变量直接处理
我们也可以不将 match 的结果绑定到一个变量,而是直接在匹配分支中执行操作。这种方式提供了更大的灵活性,特别是在错误处理分支中,我们可以自由选择是 panic、打印日志还是尝试恢复,而无需担心所有分支的返回值类型必须统一。
以下是修改后的示例:
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() {
match File::open("example.txt") {
Ok(file) => {
// 成功路径:直接在此作用域内处理文件
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(content) = line {
println!("{}", content);
}
}
println!("文件读取完成。");
}
Err(error) => {
// 错误路径:直接处理错误,无需返回特定类型
match error.kind() {
std::io::ErrorKind::NotFound => {
println!("错误:指定的文件未找到。");
}
_ => {
println!("警告:打开文件时发生错误 - {}", error);
}
}
// 程序可以继续执行其他逻辑
println!("程序继续运行...");
}
}
}
在这个版本中,match 表达式直接包裹了 File::open 的调用。每个分支都是一个独立的代码块,执行相应的操作。由于没有将结果赋值给一个需要特定类型的变量,每个分支可以自由地执行任何操作(包括不返回任何有意义的值的操作),这使错误处理逻辑更加灵活。
总结
本节课中我们一起学习了如何使用 match 表达式进行基础的错误处理。
match是处理Result枚举的强大工具,可以清晰地区分成功和失败情况。- 通过嵌套
match,我们可以对错误进行更细粒度的分类和处理。 - 在使用
match并赋值时,必须注意所有分支的返回值类型必须一致。 - 另一种常见模式是直接在
match分支中执行操作,而不绑定到变量,这在进行错误处理时提供了更大的灵活性,允许我们根据错误类型决定是终止程序、记录日志还是尝试恢复。

通过 match,我们能够以结构化和可预测的方式构建程序的错误处理逻辑,从而编写出更健壮、更易于维护的Rust代码。
047:函数基础总结


在本节课中,我们将对Rust语言中的函数基础知识进行总结。我们将回顾如何创建和使用函数,理解值的传递与返回,并初步探讨Rust的核心概念之一——借用。
Rust是一门函数式编程语言,函数在其中扮演着核心角色。我们已经学习了如何处理函数以及与函数协作。上一节我们介绍了控制流,本节中我们来看看如何对函数相关的知识进行梳理和总结。
函数的创建与使用
我们学习了如何创建函数以及如何使用它们。这包括定义函数签名、参数和函数体。
以下是定义函数的基本语法:
fn function_name(parameter: Type) -> ReturnType {
// 函数体
}
值的输入与输出
我们探讨了传入函数和从函数返回的一些值。有时函数会显式地返回一个值,有时则不会。
例如,一个不返回值的函数:
fn print_message(msg: &str) {
println!("{}", msg);
}
而一个返回值的函数:
fn add(a: i32, b: i32) -> i32 {
a + b
}
借用概念初探
除了函数本身,你还初步接触了借用的概念。这是Rust编程语言的核心概念之一,正是它使得Rust如此高效。
借用在Rust中通过引用(&)来实现,它允许你使用值但不获取其所有权。
fn calculate_length(s: &String) -> usize {
s.len()
}
通过确保你能够熟练处理Rust中的借用概念,你本质上为学习Rust中更高级的概念铺平了道路。后续的所有内容都将与这个借用概念打交道。
所有权转移
我们看到了当传递一个值,其所有权被转移到另一个函数或另一段代码时会发生什么。Rust会防止你在后续代码中犯下潜在的灾难性错误。
例如,所有权转移后,原变量将失效:
let s1 = String::from("hello");
let s2 = s1; // s1的所有权转移到s2
// println!("{}", s1); // 这里编译会报错,因为s1不再有效
本节课中我们一起学习了Rust函数的基础知识,包括创建与使用函数、处理值的输入输出,并初步认识了借用和所有权这两个核心概念。现在,你应该对函数、控制流以及作为Rust语言基石的借用概念感到更加熟悉了。掌握这些是深入理解Rust高级特性的关键。
048:结构化数据介绍 🏗️

在本节课中,我们将要学习Rust中用于组织相似数据的核心概念——结构体(struct)。我们将了解如何定义结构体、如何使用它们,并初步接触与之相关的便捷功能。
结构体的概念
Rust通过结构体(struct)来组织和归类相似的数据。如果你有Java背景,可以将其类比为Java中的对象;如果你熟悉Python,则可以将其想象为字典(dict)或哈希映射。虽然不完全相同,但其核心思想都是以结构化的方式组织数据。
定义与使用结构体
上一节我们介绍了结构体的基本概念,本节中我们来看看如何具体定义和使用一个结构体。
以下是一个定义结构体的基本语法示例:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
定义好结构体后,我们可以创建它的实例:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
结构体的关联函数
Rust为结构体提供了一种特殊的实现,允许我们创建类似于构造函数的关联函数。这使我们能够便捷地创建结构体实例,而无需每次都从头手动构建。
以下是如何为结构体定义关联函数的示例:
impl User {
fn new(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
}
// 使用关联函数创建实例
let user2 = User::new(String::from("another@example.com"), String::from("anotheruser"));
结构体的常见操作模式
创建、定义结构体以及与它交互的方式有很多种。我们将尝试涵盖你在开发Rust程序时最常见的一些模式。
以下是几种常见的结构体使用模式:
- 字段初始化简写:当变量名与字段名相同时,可以简化初始化语法。
- 结构体更新语法:使用
..语法基于一个实例快速创建另一个实例。 - 元组结构体:没有具体字段名,只有字段类型的结构体。
- 类单元结构体:没有任何字段的结构体,常用于在泛型中标记类型。
总结
本节课中我们一起学习了Rust中结构体(struct)的基础知识。我们了解了结构体是用于组织相似数据的结构化方式,学习了如何定义结构体、创建其实例,并初步接触了通过impl块定义关联函数来便捷地构造实例。掌握结构体是理解Rust数据建模的关键第一步。
049:定义结构体 🏗️

在本节课中,我们将要学习如何在Rust中定义结构体。结构体是一种将多个相关数据项组合在一起的方式,类似于其他编程语言中的对象。
概述
Rust提供了一种定义结构化数据的方法,你可以将其视为相关项的集合。这类似于在Python、Ruby或Java等语言中创建对象并为其定义一些属性。本节我们将从创建一个结构体开始。
定义结构体
要定义一个结构体,我们使用关键字 struct。下面我们来定义一个名为 Person 的结构体。
struct Person {
first_name: String,
last_name: String,
age: u8,
}
这是定义结构体最基本的方式。这里我们创建了一个名为 Person 的结构化数据类型,它包含了与个人相关的属性数据。这是一种组织数据的简洁方法。
使用 #[derive(Debug)] 属性
现在,我们不会深入探讨如何实例化或创建结构体,这将在后续课程中介绍。但让我们先尝试一个简单的操作:打印整个结构体。
如果我们尝试直接使用 println! 宏打印结构体,会遇到错误。这是因为Rust默认不知道如何格式化结构体以进行调试输出。
// 这会导致编译错误
println!("{:?}", person);
为了能够打印整个结构体,我们需要为结构体添加 #[derive(Debug)] 属性。这个属性允许我们使用调试格式打印结构体。
#[derive(Debug)]
struct Person {
first_name: String,
last_name: String,
age: u8,
}
添加此属性后,我们就可以使用 println!("{:?}", person); 来打印结构体了。
结构体字段与类型
在结构体定义中,左边的部分称为字段,冒号右边指定的是字段的类型。例如,在上面的 Person 结构体中:
first_name字段的类型是String。last_name字段的类型也是String。age字段的类型是u8,即一个8位无符号整数,这限制了我们可以使用的整数范围。
值得注意的是,我们在这里使用冒号来分隔字段名和类型,而不是等号。这种方式与某些语言中的哈希映射或字典定义类似,例如Python的字典(尽管Python没有类型注解)。
示例代码与输出
以下是一个完整的示例,展示了如何定义结构体并打印它:
#[derive(Debug)]
struct Person {
first_name: String,
last_name: String,
age: u8,
}
fn main() {
let person = Person {
first_name: String::from("John"),
last_name: String::from("Doe"),
age: 25,
};
println!("{:?}", person);
}
运行此代码,输出将类似于:
Person { first_name: "John", last_name: "Doe", age: 25 }
你可以看到输出清晰地展示了结构体名称及其字段和对应的值。
总结

本节课中我们一起学习了Rust结构体的基础知识。我们了解了如何使用 struct 关键字定义结构体,如何通过 #[derive(Debug)] 属性使结构体能够被打印,以及结构体字段和类型注解的语法。记住,为了能够使用 println! 宏的调试格式打印结构体,添加 #[derive(Debug)] 属性是必要的,否则编译器会报错。结构体是组织相关数据的强大工具,我们将在后续课程中继续探索其用法。
050:创建结构体实例 🏗️

在本节课中,我们将学习如何创建结构体(struct)实例,并为其字段赋值。我们还会探讨如何访问这些字段,以及如何处理可选字段。
概述
上一节我们介绍了结构体的定义。本节中,我们来看看如何实际创建一个结构体实例,为其填充数据,并访问其内部字段。
创建结构体实例
首先,让我们创建一个结构体实例。这类似于我们之前为Person结构体添加字段值的操作。
以下是创建Person结构体实例的步骤:
let alfredo = Person {
first_name: String::from("Alfredo"),
last_name: String::from("Sanchez"),
age: 25,
};
注意,字段first_name和last_name的类型是String,而不是字符串切片&str。因此,我们必须使用String::from()或.to_string()方法将字符串字面量转换为String类型,否则会导致编译错误。
访问结构体字段
创建实例后,我们可以访问其字段。访问方式很简单,使用点号(.)即可。
例如,如果我们想打印一个人的名字:
println!("The person's first name is: {}", alfredo.first_name);
当你将鼠标悬停在first_name上时,集成开发环境(如Visual Studio Code)会显示其类型信息,这有助于理解结构体实例的构成。
处理必需字段
结构体的所有字段在创建实例时都是必需的。如果你注释掉age字段,即使只想使用first_name和last_name,编译器也会报错,提示缺少结构体字段。
使用Option类型处理可选字段
有一种方法可以部分解决这个问题,即使用Option类型。Option类型表示一个值可能存在(Some),也可能不存在(None)。
以下是使用Option类型定义age字段的方法:
struct Person {
first_name: String,
last_name: String,
age: Option<u32>, // age 现在是可选的
}
现在,创建实例时,你可以将age设置为None:
let alfredo = Person {
first_name: String::from("Alfredo"),
last_name: String::from("Sanchez"),
age: None,
};
或者,如果你想为age提供一个值,必须使用Some包装:
let alfredo = Person {
first_name: String::from("Alfredo"),
last_name: String::from("Sanchez"),
age: Some(23), // 使用 Some 包装值
};
这样,age字段就可以灵活地表示“有值”或“无值”的状态。
总结

本节课中我们一起学习了如何创建和初始化结构体实例,访问其字段,以及使用Option类型来处理可选字段。这些是使用Rust结构体的基础,掌握它们对后续学习至关重要。
051:关联函数与构造函数演示 🏗️

在本节课中,我们将学习Rust中一个非常实用的概念:关联函数。关联函数是定义在类型(如结构体)上下文中的函数,它不需要一个具体的实例(即不需要self参数)。我们将重点探讨如何使用关联函数来创建构造函数,这是一种自动化创建结构体实例并设置默认值的强大方式。
结构体定义
首先,我们从一个简单的结构体定义开始。假设我们正在构建一个用户系统,需要一个User结构体来存储用户信息。
struct User {
username: String,
email: String,
uri: String,
active: bool,
}
这个结构体包含用户名、邮箱、URI以及一个表示用户是否活跃的布尔值字段。这种简单的定义方式对于直接创建实例是可行的。
实现块与关联函数
上一节我们介绍了User结构体的基本定义。本节中,我们来看看如何使用impl(实现)块来为结构体扩展功能。impl关键字用于为特定类型(如结构体)实现功能。
impl User {
// 关联函数将在这里定义
}
通过impl User,我们为User类型添加了新的能力。实现块内可以定义两种主要类型的函数:关联函数和方法。我们首先关注不需要实例的关联函数。
构造函数:new 函数
以下是创建构造函数的具体步骤。构造函数通常命名为new,这是一个约定俗成的做法,用于创建结构体的新实例。
- 在
impl块中,定义一个名为new的函数。 - 这个函数不接收
self参数,因为它是一个关联函数,而不是方法。 - 函数的参数通常对应结构体的字段。
- 在函数体内,返回一个构建好的结构体实例。
impl User {
fn new(username: String, email: String, uri: String) -> User {
User {
username,
email,
uri,
active: true, // 设置默认值
}
}
}
注意,我们在构造函数中做了一个重要的抽象:将active字段默认设置为true。这基于一个合理的假设:新创建的用户默认是活跃的。这样做避免了每次创建实例时都需要手动设置该字段,简化了调用过程。
使用构造函数
了解了如何定义构造函数后,现在让我们看看如何调用它来创建User实例。
let new_user = User::new(
String::from("my_username"),
String::from("pretodesa@example.com"),
String::from("https://doc.rust-lang.org"),
);
我们使用结构体名User、双冒号::和函数名new来调用这个关联函数。通过传递必要的参数,我们就获得了一个active字段默认为true的新用户实例。
实例方法:接收 self 参数
除了构造函数,我们还可以在impl块中定义方法。方法与关联函数的区别在于,它的第一个参数是self,代表调用该方法的实例本身。这允许我们修改或查询实例的内部状态。
以下是一个实例方法的例子,它用于停用用户账户:
impl User {
// ... 之前的 new 函数 ...
fn deactivate(&mut self) {
self.active = false;
println!("Account status for {} is now: {}", self.username, self.active);
}
}
这个方法接收一个可变的self引用(&mut self),因此它可以修改active字段的值。
要调用这个方法,你需要一个可变的User实例:
let mut new_user = User::new(...); // 创建可变实例
println!("Account status is: {}", new_user.active); // 输出: true
new_user.deactivate(); // 调用方法修改状态
println!("Account status is now: {}", new_user.active); // 输出: false
当deactivate方法被调用时,它操作的是new_user这个具体的实例,将其active状态改为false。
总结
本节课中我们一起学习了Rust中impl块的两个核心用途。
- 关联函数:例如构造函数
new,它不依赖于实例(没有self参数),通过StructName::function_name()的语法调用。它非常适合用于执行初始化逻辑和提供默认值。 - 实例方法:它第一个参数是
self(或&self、&mut self),代表当前实例。通过instance.method_name()的语法调用,用于执行与特定实例相关的操作,如修改字段或计算属性。

通过impl关键字,我们可以有组织地为结构体添加各种功能,使代码更加模块化和易于维护。构造函数帮助我们简化对象的创建过程,而实例方法则封装了对对象状态的操作。
052:其他结构体用途 🧱

在本节课中,我们将学习Rust中定义和创建结构体实例的几种不同方法。我们将探讨字段初始化简写语法和元组结构体的使用,这些技巧能让你的代码更简洁、更灵活。
结构体实例化的不同方式
上一节我们介绍了如何通过逐一指定字段来创建结构体实例。本节中我们来看看其他更便捷的方法。
字段初始化简写语法
当变量名与结构体字段名相同时,Rust提供了一种简写语法来初始化字段。
以下是使用传统方式创建User结构体实例的示例:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
let username = String::from("john_doe");
let email = String::from("john@sample.com");
let sign_in_count = 1;
let active = true;
let user = User {
username: username,
email: email,
sign_in_count: sign_in_count,
active: active,
};
当变量名与字段名完全匹配时,可以使用简写语法:
let user = User {
username,
email,
sign_in_count,
active,
};
这种简写语法使代码更简洁。条件是变量名必须与结构体字段名完全相同。在某些情况下,如果字段名发生变化,可能需要重构代码。使用Visual Studio Code等编辑器并安装Rust扩展,可以帮助你重命名变量以保持一致性。
元组结构体
Rust还支持元组结构体,它结合了元组和结构体的特性。元组结构体有名称,但其字段没有名称,只有类型。
以下是定义和使用元组结构体的示例:
struct Point(i32, i32, i32);
let my_point = Point(10, 20, 30);
访问元组结构体的字段需要使用索引,因为字段没有名称:
println!("Point x: {}", my_point.0); // 输出 10
println!("Point y: {}", my_point.1); // 输出 20
println!("Point z: {}", my_point.2); // 输出 30
在这个例子中,my_point.0对应第一个值10,my_point.1对应20,my_point.2对应30。运行代码会输出Point x: 10。
总结

本节课中我们一起学习了Rust结构体的两种高级用法。我们探讨了字段初始化简写语法,它能在变量名与字段名匹配时简化代码。我们还学习了元组结构体,它适用于不需要命名字段的情况。这些方法各有其适用场景,能够帮助你编写更高效、更清晰的Rust代码。
053:结构化数据总结 🎯

在本节课中,我们将总结Rust中结构化数据的使用,特别是struct类型。我们将回顾如何通过struct为数据赋予结构和顺序,并探讨其强大的扩展功能。
核心概念回顾
在之前的课程中,我们学习了如何使用struct类型在Rust中组织数据。这不仅仅是创建一个简单的键值对映射,类似于Python中的字典或JavaScript中的对象。
struct允许我们定义数据的结构,并为每个字段指定名称和类型。例如,定义一个表示点的结构体:
struct Point {
x: i32,
y: i32,
}
扩展功能与方法
上一节我们介绍了struct的基本定义,本节中我们来看看如何为其添加额外功能。Rust允许我们通过impl块为结构体扩展方法,提供辅助功能。
这是一个非常强大的概念,并非所有编程语言都具备。它允许你将希望融入结构体的所有额外辅助函数和功能整合进来。
以下是为Point结构体实现方法的示例:
impl Point {
// 一个关联函数,用于创建新的Point实例
fn new(x: i32, y: i32) -> Self {
Point { x, y }
}
// 一个实例方法,计算到原点的距离
fn distance_from_origin(&self) -> f64 {
((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
}
}
结构体的优势
因此,struct不仅关乎数据本身,还允许你添加所有这些额外行为,例如用于数据转换,这可以使得结构体变得非常、非常强大和实用。
以下是使用struct和其方法的主要优势列表:
- 数据组织:清晰地将相关数据字段组合在一起。
- 类型安全:每个字段都有明确的类型,编译器会进行检查。
- 行为封装:通过
impl块将与数据相关的操作(方法)与数据定义放在一起。 - 灵活性:可以定义关联函数(如构造函数)和实例方法。
- 可扩展性:可以随时为结构体添加新的方法来增强其功能,而无需修改使用它的现有代码。
总结
本节课中我们一起学习了Rust中struct类型的总结。我们回顾了如何使用struct为数据赋予结构,更重要的是,我们探讨了如何通过impl块为其扩展方法和行为,这使得Rust的结构体不仅仅是数据的容器,更是功能强大的自定义类型的基础。掌握struct及其相关特性是有效使用Rust进行编程的关键一步。
054:字符串与向量介绍 🧵📦
在本节课中,我们将学习Rust中两种最常用、最重要的数据结构:字符串和向量。我们将了解它们的不同类型、核心概念以及基本操作方法。
字符串与向量介绍
字符串和向量是你在Rust编程中可能会大量见到,或者说最常遇到的两种数据结构。
字符串类型
现在,字符串经常被交替使用,但主要有两种类型。
一种是字符串切片,我们稍后会看到它的样子。
另一种直接被称为字符串。有时,Rust开发者会交替使用“字符串”这个定义来指代这两种不同的字符串表示形式。
接下来,我们将具体看看它们之间的区别,何时该使用其中一种而非另一种,以及如何实际使用这两种类型。
向量简介
然后,我们将看看向量,它实际上是一个项目的集合,几乎就像一个列表。如果你来自Java或Python,它是一个集合,是一个我们可以存放许多项目的数据结构。


向量可以增长,并允许我们对其进行修改。我们将学习如何操作向量,包括:
- 如何向其中添加值。
- 如何修改向量。
- 以及如何从向量中检索值,特别是当我们想查找向量中包含的特定值时。
总结
本节课中,我们一起学习了Rust中两种核心数据结构:字符串和向量。我们了解了字符串的两种主要类型(字符串切片和String类型)及其区别,并初步认识了向量作为动态集合的基本概念和操作。在接下来的课程中,我们将深入探讨它们的具体用法。
055:理解String与str 🧠

在本节课中,我们将要学习Rust中两种重要的字符串类型:String 和 &str(字符串切片)。我们将通过具体的代码示例来探讨它们之间的核心区别,包括可变性、所有权以及各自的使用场景。
概述
Rust中的字符串处理是一个核心概念。String 是一个可增长、可变的、拥有所有权的UTF-8编码字符串类型。而 &str 是一个字符串切片,它是对存储在别处的UTF-8编码字符串数据的不可变引用。理解这两者的区别对于编写高效、安全的Rust代码至关重要。
字符串切片(&str)的特性
上一节我们介绍了两种字符串类型的基本概念,本节中我们来看看字符串切片 &str 的具体特性。
字符串切片通常以 &str 的形式出现。& 符号意味着它几乎总是一个指向由他人拥有的现有字符串数据的引用。
&str 的一个关键特性是不可变性。你无法修改一个字符串切片的内容。
let s: &str = "hello world";
// s.push_str(" new text"); // 这行代码会编译错误,因为 &str 不可变
以下是创建和使用字符串切片的示例:
fn print_str(s: &str) {
println!("{}", s);
}
fn main() {
let my_slice: &str = "hello world";
print_str(my_slice); // 传递 &str 类型,无需额外添加 &
}
在这个例子中,my_slice 的类型是 &str。当我们将其传递给 print_str 函数时,因为参数类型就是 &str,所以直接传递即可。
字符串类型(String)的能力
了解了不可变的字符串切片后,本节我们来看看功能更强大的 String 类型。
与 &str 不同,String 类型是可变的,并且拥有其数据的所有权。这意味着你可以修改其内容。
如果你有一个 &str 但需要修改它,一个常见的做法是将其转换为 String。
以下是转换和操作 String 的几种方法:
-
使用
String::from或to_string进行转换
你可以从一个字符串切片创建新的String。let s1: &str = "hello world"; let mut s2: String = String::from(s1); // 或者 s1.to_string() -
使用
push_str方法修改字符串
String类型提供了push_str方法来追加内容。注意,变量必须声明为mut(可变的)。let mut s2 = String::from("hello world"); s2.push_str(" some other string"); println!("{}", s2); // 输出:hello world some other string -
使用
format!宏创建新字符串
format!宏是一个灵活的工具,它可以格式化文本并返回一个新的String,而不需要改变原始数据。let s1: &str = "hello world"; let new_string = format!("{} - other stuff here", s1); // new_string 的类型是 String
可变性与函数参数
当在函数中处理字符串时,可变性的规则同样适用。如果你想在函数内部修改一个 String 参数,必须将其标记为可变引用 &mut String。
fn modify_string(s: &mut String) {
s.push_str(" has been modified");
}
fn main() {
let mut greeting = String::from("Hello");
modify_string(&mut greeting);
println!("{}", greeting); // 输出:Hello has been modified
}
你需要密切关注Rust的所有权和借用规则。如果你在函数中修改了字符串并可能需要返回它,必须根据所有权是否转移来设计你的函数签名。
核心区别与使用场景总结
本节课中我们一起学习了 String 和 &str 的主要区别。以下是核心要点的总结:
- 所有权与可变性:
String拥有数据且可变;&str是借用数据且不可变。 - 内存布局:
String在堆上分配内存;&str是对内存中某处字符串的引用。 - 使用场景:
- 当你需要一个可以动态增长、收缩或修改的字符串时,使用
String。例如,从文件或网络读取的数据,或用户输入。 - 当你只需要一个字符串的只读视图,或者字符串字面量(在编译时已知)时,使用
&str。这常用于函数参数,以接受更灵活的类型(既能接受&str,也能接受&String,因为String可以解引用为&str)。
- 当你需要一个可以动态增长、收缩或修改的字符串时,使用
一个重要的实践原则是:在函数签名中,优先使用 &str 作为参数类型,除非你明确需要在函数内部获取所有权(用 String)或修改内容(用 &mut String)。这能使你的API更通用、更灵活。
// 好的实践:接受字符串切片,调用者可以传递 String 或 &str
fn good_function(input: &str) {
// ...
}
// 更具体的需求:需要修改或获取所有权
fn needs_mutability(input: &mut String) {
// ...
}
fn takes_ownership(input: String) {
// ...
}

通过掌握 String 和 &str 的区别,你就能更好地利用Rust的类型系统来管理内存和确保数据安全。
056:Rust字符串操作演示

在本节课中,我们将学习Rust中字符串的基本操作。我们将通过几个示例来探索如何对字符串进行切片、格式化、迭代、分割和反转。这些操作是处理文本数据的基础。
字符串切片操作
上一节我们介绍了字符串的基本概念,本节中我们来看看如何获取字符串的一部分。
我们可以使用切片语法来获取字符串的前几个字符。在Rust中,字符串切片使用范围索引。
let sentence = String::from("The quick brown fox jumps over the lazy dog");
let first_three = &sentence[0..3];
println!("{}", first_three);
运行这段代码会输出字符串的前三个字符。需要注意的是,范围索引可以是包含的,使用..=语法。
let first_four = &sentence[0..=3];
println!("{}", first_four);
使用format!宏进行字符串拼接
接下来,我们看看如何使用format!宏来拼接字符串。这是一种灵活且安全的方式。
let formatted = format!("{} - formatted", sentence);
println!("{}", formatted);
format!宏可以接受字符串切片(&str)或字符串(String)类型,并返回一个新的String。这使得它在处理不同类型字符串时非常方便。
迭代字符串中的字符
以下是遍历字符串中每个字符的方法。这在需要检查或处理每个字符时非常有用。
for c in sentence.chars() {
match c {
'a' | 'e' | 'i' | 'o' | 'u' => println!("Got a vowel: {}", c),
_ => continue,
}
}
这段代码会遍历句子中的每个字符,并打印出所有的元音字母。这是一个简单的例子,展示了如何基于条件处理字符。
按空白分割字符串
在处理文本时,经常需要将句子分割成单词。以下是按空白字符分割字符串并将其收集到向量中的方法。
let words: Vec<&str> = sentence.split_whitespace().collect();
println!("{:?}", words);
split_whitespace方法返回一个迭代器,我们可以使用collect方法将其转换为向量。向量是一种集合类型,类似于其他语言中的数组或列表。
反转字符串
作为额外内容,我们还可以反转字符串。虽然在实际应用中不常用,但它展示了字符串操作的另一种可能性。
let reversed: String = sentence.chars().rev().collect();
println!("{}", reversed);
这段代码首先将字符串转换为字符迭代器,然后使用rev方法反转迭代顺序,最后收集成一个新的字符串。


本节课中我们一起学习了Rust字符串的几种基本操作:切片、格式化拼接、字符迭代、按空白分割以及反转。这些操作是文本处理的基础,掌握它们将帮助你更有效地处理字符串数据。
057:向量与切片基础 🧩

在本节课中,我们将学习Rust中向量(Vector)和切片(Slice)的基础知识,特别是它们之间的关系、所有权规则以及可变性。我们将通过代码示例来理解这些核心概念。
向量和切片在Rust中的关系,类似于字符串(String)和字符串切片(&str)的关系。接下来,我们来看看它们之间的一些区别。
所有权示例
以下是关于所有权的一个基础示例。我们首先定义一个向量,然后从中创建一个切片。
fn main() {
// 定义一个包含三个整数的向量
let numbers = vec![1, 2, 3];
// 从向量创建一个包含所有元素的切片
let slice = &numbers[..];
// 打印切片内容
println!("Slice: {:?}", slice);
}
运行上述代码,切片将包含 [1, 2, 3],与原始向量内容一致。创建切片时,我们使用了 &numbers[..] 语法,其中的 .. 表示“全范围”,即包含向量中的所有元素。
可变性示例
现在,我们来看看如何使切片可变。在Rust中,变量默认是不可变的,要修改它们需要显式声明。
fn main() {
// 定义一个可变的向量
let mut numbers = vec![1, 2, 3];
// 创建一个可变的切片引用
let slice = &mut numbers[..];
// 修改切片中的第一个元素
slice[0] = 10;
// 打印修改后的切片
println!("Modified Slice: {:?}", slice);
}
运行此代码,切片将变为 [10, 2, 3]。我们通过 &mut 借用了向量的可变引用,从而能够修改其数据。
借用规则冲突
Rust的所有权系统有严格的借用规则。以下示例展示了一个常见的冲突情况。
fn main() {
let mut numbers = vec![1, 2, 3];
// 第一个可变借用
let slice1 = &mut numbers[..];
// 尝试第二个不可变借用(此行会导致编译错误)
// let slice2 = &numbers[..];
println!("Slice1: {:?}", slice1);
}
如果取消注释 let slice2 = &numbers[..]; 这一行,编译器会报错。错误信息指出:无法将 numbers 作为不可变借用,因为它已经被作为可变借用。在Rust中,同一时间只能有一个可变引用,或者多个不可变引用,但不能同时存在。
解决此类问题的一种策略是,在可变借用的作用域结束后再进行其他借用,或者通过创建新的向量副本等方式来管理数据访问。

本节课中我们一起学习了Rust向量与切片的基础操作、所有权规则以及可变性的管理。记住,切片通常被视为大小固定且不可变的视图,而向量则是可增长的数据结构。理解并遵守Rust的借用规则,是写出安全、高效代码的关键。
058:从向量检索值 🧮

在本节课中,我们将学习如何从Rust的向量(Vector)中检索值。向量是一种可存储多个相同类型值的集合。我们将探讨几种不同的检索方法,包括通过索引直接访问、使用安全的方法获取首尾元素,以及如何处理可能出现的错误情况。
从向量中检索值的几种方法
有多种方法可以从向量中检索值。让我们具体看看如何操作。
首先,我们定义一个不可变的向量。这个向量在定义后不会被修改。
let vector = vec![1, 2, 3, 4, 5];
接下来,我们将学习如何从这个向量中检索值。
通过索引检索特定值
我们可以通过索引来检索向量中的特定值。请记住,索引从0开始。
例如,第三个值的索引是2。
let third_value = vector[2];
println!("向量中的第三个值是:{}", third_value);
这意味着这里的数字3是第三个值。因为索引2对应的是第三项。
检索最后一个值
向量提供了一个便捷的方法.last()来获取最后一个元素。由于向量可能为空,此方法返回一个Option类型,因此我们需要调用.unwrap()来获取实际值。
let last_value = vector.last().unwrap();
println!("向量中的最后一个值是:{}", last_value);
.last()方法返回Option类型的原因是:它可能返回一个值,也可能返回None(如果向量为空)。这是一种安全的检索方式,可以避免程序出错。我们使用.unwrap()是因为我们确信向量不为空,并希望直接获取其中的值。
使用match表达式处理检索结果
另一种有趣的方式是使用match表达式。例如,我们可以检索第一个元素,并根据结果执行不同的操作。
match vector.first() {
Some(value) => println!("向量的第一个值是:{}", value),
None => println!("向量为空"),
}
.first()方法同样返回Option类型。通过match表达式,我们可以优雅地处理有值和无值(即向量为空)两种情况。
运行上述代码,我们将依次得到:3、5和1。
处理空向量的情况
如果向量是空的,会发生什么情况呢?
让我们定义一个空向量:
let empty_vector: Vec<i32> = vec![];
此时,如果我们尝试通过索引[2]访问元素,程序将会恐慌(panic),因为索引超出了向量的长度。
// 这行代码会导致 panic!
// let value = empty_vector[2];
错误信息会是:thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 2'。向量完全为空,我们无法访问索引2处的元素。
同样,尝试对空向量调用.last().unwrap()也会导致恐慌,因为我们在一个None值上调用了.unwrap()。
// 这也会导致 panic!
// let last = empty_vector.last().unwrap();
在这种情况下,使用match表达式处理.first()方法则不会恐慌,因为它能妥善处理None的情况。
match empty_vector.first() {
Some(value) => println!("值为:{}", value),
None => println!("向量为空,没有第一个值。"),
}
当Rust无法从上下文推断空向量的类型时,我们需要显式声明其类型,如Vec<i32>。
创建检索函数并理解索引类型
上一节我们介绍了基础的检索操作,本节中我们来看看如何将其封装成函数,并注意一个关键的索引类型细节。
让我们定义一个函数,它接收一个索引并返回向量中对应位置的值。
fn get_item(index: usize) -> Option<&i32> {
let v = vec![1, 2, 3, 4, 5];
v.get(index)
}
这个函数使用.get(index)方法,它安全地返回一个Option<&T>,而不是直接恐慌。
我们可以这样调用它:
println!("索引3处的值是:{:?}", get_item(3));
输出将是Some(4)。为了直接得到数字4,我们可以使用.unwrap()。
let value = get_item(3).unwrap();
println!("索引3处的值是:{}", value);
现在,如果我们想让函数更通用,允许传入索引参数,需要注意索引的类型。
你可能会尝试使用u8作为索引类型:
// 错误的尝试
// fn get_item(index: u8) -> Option<&i32> { ... }
// get_item(3);
但这会导致错误:the type \Vec
关键在于,用于索引向量的类型必须是usize。 usize是一个指针大小的无符号整数类型,其大小取决于目标平台的内存地址空间(例如,在32位系统上是4字节,64位系统上是8字节)。这是Rust用于索引集合类型的标准类型。
因此,正确的函数签名应该是:
fn get_item(index: usize) -> Option<&i32> {
let v = vec![1, 2, 3, 4, 5];
v.get(index)
}
现在,调用get_item(3)就能正常工作了。
总结
本节课中我们一起学习了从Rust向量中检索值的多种方法:
- 通过索引:使用
vector[index],但需注意越界会导致恐慌。 - 安全方法:使用
.get(index)返回Option,或使用.first()、.last()。 - 错误处理:使用
match表达式或.unwrap()(在确定有值时)来处理Option结果。 - 关键类型:向量的索引必须是
usize类型。 - 空向量:操作空向量时需要格外小心,优先使用返回
Option的安全方法。

理解这些不同的检索方式及其安全性,是有效使用Rust向量的基础。
059:向向量添加元素 🧩

在本节课中,我们将学习如何向Rust的向量(Vector)中添加元素。向量是一种可动态增长的数据结构,了解如何有效地添加元素是使用它的基础。我们将介绍几种核心方法,包括 push、extend、append 和 insert。
概述
向量是Rust中一个非常重要的集合类型。要修改向量,例如添加元素,首先需要将其声明为可变的。本节将演示几种向向量末尾或特定位置添加元素的方法。
使用 push 方法添加单个元素
上一节我们介绍了向量的可变性,本节中我们来看看如何向向量末尾添加单个元素。push 方法是最直接的方式。
以下是使用 push 方法的示例:
let mut vec = vec![1, 2, 3]; // 定义一个可变向量
vec.push(4); // 将元素4添加到向量末尾
执行上述代码后,向量 vec 将包含元素 [1, 2, 3, 4]。push 方法接收一个元素,并将其追加到集合的最后。
使用 extend 方法添加多个元素
除了添加单个元素,我们还可以一次添加多个元素。extend 方法可以将一个迭代器(如另一个向量或切片)中的所有元素添加到当前向量的末尾。
以下是使用 extend 方法的示例:
let mut vec = vec![1, 2, 3, 4];
let more_numbers = vec![5, 6];
vec.extend(more_numbers); // 将 more_numbers 中的所有元素添加到 vec 末尾
运行代码后,向量 vec 将变为 [1, 2, 3, 4, 5, 6]。extend 方法会遍历给定迭代器的每个元素,并将它们逐个追加。
使用 append 方法合并向量
extend 方法处理的是迭代器,而 append 方法则专门用于将另一个向量的所有元素“整体”添加到当前向量末尾。需要注意的是,append 会消耗掉源向量,因此源向量必须是可变的。
以下是使用 append 方法的示例:
let mut vec = vec![1, 2, 3, 4, 5, 6];
let mut another_vec = vec![7, 8];
vec.append(&mut another_vec); // 将 another_vec 的所有元素移动到 vec 末尾
执行后,vec 将包含 [1, 2, 3, 4, 5, 6, 7, 8],而 another_vec 将变为空。这是因为 append 取得了另一个向量元素的所有权。
使用 insert 方法在指定位置插入元素
最后,我们来看如何在向量的任意位置插入元素,而不仅仅是末尾。insert 方法允许我们在指定的索引位置插入一个元素。
以下是使用 insert 方法的示例:
let mut vec = vec![1, 2, 3, 4, 5, 6, 7, 8];
vec.insert(0, 0); // 在索引0的位置(即开头)插入元素0
运行后,向量将变为 [0, 1, 2, 3, 4, 5, 6, 7, 8]。insert 方法的第一个参数是索引,第二个参数是要插入的值。
总结
本节课中我们一起学习了向Rust向量添加元素的四种主要方法:
push:在向量末尾添加一个元素。extend:将一个迭代器中的所有元素添加到向量末尾。append:将另一个可变向量的所有元素移动到当前向量末尾。insert:在向量的指定索引位置插入一个元素。

这些操作与其他编程语言(如Java、TypeScript、Python)中的类似。理解这些方法及其细微差别(例如对可变性的要求),将帮助你更高效地使用和处理Rust中的向量。你同样可以使用循环来添加元素,但这些内置方法是更直接和高效的选择。
060:字符串与向量总结

概述
在本节课中,我们将总结Rust编程中字符串(String)和向量(Vector)的核心概念。我们将回顾它们各自的工作原理、如何协同工作,以及在编写Rust程序时需要注意的关键点。理解这些数据结构对于处理数据和程序逻辑至关重要。
字符串:两种类型与操作
上一节我们介绍了向量的基本操作,本节中我们来看看字符串。字符串和向量在Rust中都是常用的集合类型,但字符串有其独特的特性和两种不同的表现形式。
字符串的处理,特别是两种字符串类型之间的差异,在Rust程序中会频繁出现。您需要了解这些差异,以及何时该使用其中一种而非另一种。
Rust中主要有两种字符串类型:
String: 这是可增长、可修改、拥有所有权的字符串类型,存储在堆上。&str: 这是字符串切片(string slice),通常是对String或字符串字面量的不可变引用。
以下是操作字符串的一些基本方式:
- 创建字符串:
let mut s = String::from("hello"); - 追加字符串:
s.push_str(" world"); - 字符串拼接:
let s3 = s1 + &s2;(注意s1的所有权会被移动) - 使用
format!宏:let s = format!("{}-{}-{}", a, b, c);
向量:动态数组的使用
理解了字符串后,我们转向向量。向量是一个在堆上分配的、可动态增长的同类型数据集合。在处理程序中的数据逻辑时,您绝对会经常用到向量。
您已经学习了如何向向量添加元素。在使用向量时,代码需要保持一些条件和一致性,例如明确向量的可变性。
以下是向量的核心操作:
- 创建向量:
let v: Vec<i32> = Vec::new();或let v = vec![1, 2, 3]; - 添加元素(需要可变引用):
v.push(4); - 读取元素:
- 通过索引:
let third: &i32 = &v[2];(可能 panic) - 使用
get方法:let third: Option<&i32> = v.get(2);(返回Option类型,更安全)
- 通过索引:
关于向量的可变性、添加项和检索项的方式,是您必须掌握的内容。当您从向量中检索元素时,需要特别注意所有权的规则和引用的生命周期。
总结
本节课中我们一起学习了Rust中字符串和向量的关键知识。我们回顾了两种字符串类型(String和&str)的区别与操作,以及向量(Vec<T>)的基本使用方法,包括添加和检索元素。这些集合类型是构建Rust程序数据逻辑的基础,您将在未来的编程实践中反复应用它们。现在,您已经掌握了使用它们的基础知识,并可以开始在实践中运用了。
061:枚举与变体介绍 🧩
在本节课中,我们将学习Rust编程语言中一个非常强大的特性:枚举(Enums)与变体(Variants)。我们将了解如何定义枚举、理解变体的概念,并探索如何为变体关联数据。最后,我们将看到如何在解决实际问题时使用这些类型。
枚举和变体是Rust编程语言中一个非常强大的特性。我本人非常喜欢使用枚举和变体。这在其他编程语言中也很常见,例如Go语言。但如果你不是来自Go语言背景,或者从未接触过它,处理枚举的方式可能会让你感觉有些陌生和不寻常。不过不用担心,我们将学习如何创建和定义枚举,了解什么是变体,以及如何有时为变体填充少量数据或关联数据。

我们还将探讨在实际逻辑中实现和使用枚举与变体是怎样的,在什么情况下你可能需要枚举和变体,以及如何通过使用Rust中的这些类型来解决实际问题。我希望在本课结束时,你也能像我一样喜欢它们。一旦你习惯了它们,它们绝对能让你以一种非常具有描述性的方式来定义和解决Rust中的问题和逻辑问题,最终会变得非常有意义。
核心概念
上一节我们概述了枚举与变体的重要性,本节中我们来看看其核心定义。
枚举是一种自定义数据类型,它允许你通过列举所有可能的变体来定义一个值。每个变体可以是一个简单的标识符,也可以关联不同类型的数据。
以下是定义一个简单枚举的代码示例:
enum IpAddrKind {
V4,
V6,
}
在这个例子中,IpAddrKind 是一个枚举,它有两个变体:V4 和 V6。
为变体关联数据
我们已经看到了简单的枚举,现在让我们看看如何为枚举的变体关联更丰富的数据。

枚举的变体可以关联数据,这使得枚举变得非常灵活和强大。你可以将数据直接嵌入到变体中。
以下是为枚举变体关联数据的示例:
enum IpAddr {
V4(String),
V6(String),
}
在这个例子中,V4 和 V6 变体都关联了一个 String 类型的值,用于存储IP地址。
枚举的实际应用
了解了枚举的基本定义和如何关联数据后,本节我们来看看枚举在实际编程中如何帮助我们清晰地表达逻辑。
枚举非常适合用于表示一组固定的、互斥的可能性。它们使代码更安全、更易读,因为编译器可以确保你处理了所有可能的情况。
以下是枚举在实际问题中的应用场景列表:
- 状态机:例如,表示一个网络连接的状态(
Connecting,Connected,Disconnected)。 - 选项类型:Rust标准库中的
Option<T>枚举用于处理可能存在或不存在的值。 - 错误处理:Rust标准库中的
Result<T, E>枚举用于处理可能成功或失败的操作。 - 自定义消息类型:在图形用户界面或事件驱动编程中,可以用枚举来表示不同类型的用户输入或系统事件。
总结
本节课中我们一起学习了Rust中的枚举与变体。我们了解了枚举是一种通过列举所有可能变体来定义类型的强大工具。我们学习了如何定义简单的枚举,如何为变体关联数据以增强其表达能力,并探讨了枚举在表示固定选项集、处理可选值及错误等实际场景中的应用。掌握枚举将帮助你写出更清晰、更安全、更具表达力的Rust代码。
062:定义枚举 🧩

在本节课中,我们将学习Rust中一个强大的特性——枚举(enum)。我们将了解如何定义枚举、如何为枚举的变体关联数据,以及如何使用match表达式来比较和处理枚举值。通过一个磁盘类型的例子,我们将看到枚举如何帮助我们清晰地组织和处理不同类型的数据。
枚举的定义与基本概念
枚举(enum)是Rust中一种用于定义一组命名值的数据类型。它允许你将一个值限定为几个可能的变体之一。与结构体(struct)不同,枚举的每个变体可以持有不同类型和数量的数据。
以下是定义一个枚举的基本语法:
enum EnumName {
Variant1,
Variant2(Type1, Type2),
Variant3 { field1: Type1, field2: Type2 },
}
在上一节我们了解了枚举的基本概念,本节中我们来看看如何具体定义一个枚举。
定义磁盘类型枚举
让我们通过一个具体的例子来学习。假设我们要表示两种类型的磁盘:固态硬盘(SSD)和机械硬盘(HD)。我们可以定义一个名为Disk的枚举。
enum Disk {
SSD,
HD,
}
这里,Disk是一个枚举类型,它有两个变体:SSD和HD。每个变体目前不关联任何额外数据。
使用枚举值
定义了枚举之后,我们可以创建该类型的变量。例如,我们可以声明一个Disk类型的变量并为其赋值。
let disk_type = Disk::SSD;
这行代码创建了一个名为disk_type的变量,其值为Disk枚举的SSD变体。注意,我们使用枚举名::变体名的语法来指定具体的变体。
比较枚举值
在像Python或JavaScript这样的语言中,你可能会尝试使用==运算符来比较枚举值。但在Rust中,直接比较枚举变体是不允许的,除非为枚举类型实现了PartialEq等特质。
以下尝试会导致编译错误:
if disk_type == Disk::SSD {
println!("This is an SSD.");
}
编译器会提示类似“binary operation == cannot be applied to type Disk”的错误。这是因为我们没有为Disk类型实现相等性比较。
使用Match表达式处理枚举
在Rust中,处理枚举值的标准方式是使用match表达式。match允许你根据枚举的变体执行不同的代码分支。
以下是使用match表达式处理Disk枚举的示例:
match disk_type {
Disk::SSD => println!("This type is an SSD."),
Disk::HD => println!("This type is a spin drive."),
}
这段代码的意思是:检查disk_type的值。如果它是Disk::SSD,则打印“This type is an SSD.”;如果它是Disk::HD,则打印“This type is a spin drive.”。
运行这段代码,如果disk_type是SSD,输出将是:
This type is an SSD.
为枚举变体关联数据
枚举的强大之处在于,它的变体可以关联数据。例如,我们可以修改Disk枚举,让SSD变体关联一个表示容量的无符号32位整数。
enum Disk {
SSD(u32),
HD,
}
现在,SSD变体可以携带一个u32类型的值。我们可以这样创建一个带数据的SSD实例:
let disk_with_data = Disk::SSD(128);
这表示一个容量为128GB的固态硬盘。我们可以使用match表达式来提取并打印这个数据:
match disk_with_data {
Disk::SSD(size) => println!("This is an SSD with {} GB.", size),
Disk::HD => println!("This is a spin drive."),
}
运行这段代码,输出将是:
This is an SSD with 128 GB.
使用Debug特质打印枚举
与结构体一样,你也可以为枚举派生Debug特质。这允许你使用println!("{:?}", enum_value)来方便地打印枚举值及其关联的数据。
#[derive(Debug)]
enum Disk {
SSD(u32),
HD,
}
fn main() {
let disk = Disk::SSD(256);
println!("{:?}", disk);
}
输出:
SSD(256)
枚举变体的命名
在枚举定义中,SSD和HD被称为变体,而不是字段或属性。当编译器报告错误时,它可能会提到“变体”。例如,如果你在match表达式中漏掉了一个变体,编译器会提示“non-exhaustive patterns”(模式未覆盖所有情况),并指出缺少哪个变体。
总结
本节课中我们一起学习了Rust中枚举的定义和使用。我们了解到:
- 枚举使用
enum关键字定义,用于将值限定为一组可能的命名变体。 - 枚举变体可以关联不同类型和数量的数据。
- 不能直接使用
==运算符比较枚举值,而应使用match表达式进行模式匹配和处理。 - 可以为枚举派生
Debug特质以便于打印调试信息。 - 枚举是Rust中表达数据分类和状态的强大工具,在错误处理、状态机等场景中非常有用。

通过掌握枚举,你能够更清晰、更安全地组织和处理程序中的多种可能性。
063:使用枚举作为类型 🍷

在本节课中,我们将学习如何在Rust中使用枚举(enum)作为一种自定义数据类型。我们将看到如何将枚举用作结构体字段的类型,以及如何将其作为函数参数的类型,从而编写出更安全、更清晰的代码。
枚举是什么?
枚举是Rust中的一种自定义数据类型。它可以容纳多种不同的信息。在本例中,我们将使用一个表示葡萄酒产区的枚举。
以下是WineRegions枚举的定义:
enum WineRegions {
Bordeaux,
Burgundy,
Champagne,
Tuscany,
Rioja,
NapaValley,
}
从第10行到第20行,我们定义了这个枚举,它包含多个不同的变体(如Bordeaux、Tuscany等)。代码中的花括号下划线提示我们正在使用所有变体,目前这没有问题。
在结构体中使用枚举类型
上一节我们介绍了枚举的定义,本节中我们来看看如何将枚举用作结构体字段的类型。
首先,我们将在结构体中使用它。以下两个例子都涉及结构体,但请注意,我们传入了一个名称,然后使用了region字段。
枚举允许我们做什么?让我们回头看看这里的Wine结构体:
struct Wine {
name: String,
region: WineRegions, // 使用WineRegions枚举作为类型
}
你可以看到我们有两个字段:第一个是name字段,它接受一个String;第二个是region字段,它要求使用WineRegions类型。这意味着什么?如果我们将其注释掉,并说这也是一个字符串,那就需要你输入各种不同的字符串,比如“Napa Valley”。这容易出错,可能会出现拼写错误。
这就是枚举非常有用的原因。它允许我们非常明确地指定结构体字段将拥有什么类型的数据或描述。在本例中,我们声明任何来自WineRegions枚举的值都是有效的。
回到我们的main函数,这意味着我们可以用非常符合Rust习惯的方式来表达。这是一种非常地道的说法:“嘿,这是Wine结构体,这是名称,产区是Bordeaux。”这很有意义。另一款酒将是来自意大利的“Barolo”,产区是Tuscany。这个变体让我们能够很好地描述我们所拥有的数据。
如果我们运行以下代码:
fn main() {
let wine1 = Wine {
name: String::from("Chateau Margaux"),
region: WineRegions::Bordeaux,
};
let wine2 = Wine {
name: String::from("Barolo"),
region: WineRegions::Tuscany,
};
println!("Wine 1 is a {} from {:?}", wine1.name, wine1.region);
println!("Wine 2 is a {} from {:?}", wine2.name, wine2.region);
}
我们将在底部看到输出:
Wine 1 is a Chateau Margaux from Bordeaux
Wine 2 is a Barolo from Tuscany
这非常棒。我真的很喜欢这个功能,它允许我们在结构体中使用类型。
将枚举作为函数参数类型
除了在结构体中使用,我们还可以将枚举作为函数参数的要求。这意味着我们可以声明一个函数,它接受一个特定的枚举类型作为参数。
以下是一个非常简单的函数,它接受一个WineRegions枚举类型的参数w:
fn supported_regions(w: WineRegions) {
match w {
WineRegions::Tuscany => println!("Tuscany is supported!"),
WineRegions::Rioja => println!("Rioja is supported!"),
_ => println!("{:?} is not supported.", w),
}
}
让我们调用这个函数,看看能得到什么。我们不再打印所有内容,而是实际调用我们的函数supported_regions。
我们可以这样调用:
supported_regions(WineRegions::Bordeaux);
或者:
supported_regions(WineRegions::Rioja);
以这种方式做有什么问题吗?其实没有问题。只是我这里有额外的部分,我需要保存它。你绝对可以像这样传递参数。
现在,如果我运行它,我们会看到输出:
Bordeaux is not supported.
Rioja is supported!
你可以使用match关键字进行一些混合匹配,使其工作得非常完美。
总结

本节课中我们一起学习了如何在Rust中使用枚举作为类型。我们看到了如何将其作为结构体的一部分添加,使其在描述数据结构时非常清晰和具有描述性。我们还学习了如何在函数内部要求枚举作为参数,并使用match表达式对其进行处理。这些技巧能帮助我们编写更健壮、更易维护的代码。
064:Option枚举 🧩

在本节课中,我们将深入学习Rust中一个非常重要的概念——Option枚举。我们将探讨它的定义、用途,以及如何通过它来优雅地处理可能缺失的值或潜在的错误情况。
概述
Option枚举是Rust标准库的核心部分,用于表示一个值可能存在(Some)或不存在(None)的情况。它提供了一种类型安全的方式来处理可能失败的操作,避免了空指针异常等常见问题。本节我们将通过一个具体的除法函数示例,来演示Option的工作原理和实际应用。
Option枚举的定义与理解
上一节我们介绍了枚举的基本概念,本节中我们来看看Option这个特殊的枚举。
Option是一个枚举,它有两个变体:Some(T) 和 None。这里的 T 是一个泛型参数,意味着Some可以包装任何类型的值。其核心定义可以理解为:
enum Option<T> {
Some(T),
None,
}
Some是一个枚举变体,它包装了一个类型为T的值。None则表示没有值。对于从JavaScript或Python等语言转来的开发者来说,这个概念可能最初会感觉有些抽象,但它本质上就是一个简单的枚举,用于表示“有值”或“无值”两种状态。
示例:一个返回Option的函数
让我们通过一个具体的函数来理解Option的用法。我们有一个函数divide,它接收两个i32类型的参数,并返回一个Option<i32>。
fn divide(x: i32, y: i32) -> Option<i32> {
if y == 0 {
None
} else {
Some(x / y)
}
}
函数签名 -> Option<i32> 意味着这个函数肯定会返回一个Option类型。更具体地说,它要么返回None,要么返回一个包装了i32整数值的Some。这种设计非常强大,因为它抽象了我们处理错误和边界情况(如除数为零)的方式。
在代码块中,我们检查除数y。如果y等于0,我们返回None。这里可能看起来有点奇怪,因为None本身并不是一个i32,但由于None是Option枚举的一个有效变体,所以这样返回是完全合法的。否则,如果y不为0,我们就执行除法运算,并用Some包装结果x / y,从而构造出一个Option<i32>值。
使用match处理Option的结果
定义了函数后,我们需要一种方式来安全地处理它的返回值。以下是使用match表达式处理Option的典型模式:
let a = 10;
let b = 2;
let result = divide(a, b);
match result {
Some(x) => println!("结果是: {}", x),
None => println!("错误:除数为零"),
}
当a=10, b=2时,divide函数返回Some(5)。match表达式会匹配到Some(x)分支,并将内部值5绑定到变量x,然后打印出结果。如果我们将b改为0,函数将返回None,match则会执行None分支,打印出错误信息。
谨慎使用unwrap方法
除了match,Option还提供了一个名为unwrap的方法。这个方法会直接取出Some中的值,但如果遇到None,则会导致程序恐慌(panic)。
// 如果result是Some(5),这会打印出5
println!("直接解包: {}", result.unwrap());
// 如果result是None,这行代码会导致程序崩溃
// println!("直接解包: {}", result.unwrap()); // 危险!
以下是使用unwrap的示例输出:
- 当结果为
Some(5)时,result.unwrap()会安全地返回5。 - 当结果为
None(例如除数为零时)时,调用unwrap会立即引发恐慌,并产生类似“thread ‘main’ panicked at ‘calledOption::unwrap()on aNonevalue’”的错误信息。
理解这些错误信息并学会查看堆栈跟踪,是调试和解决Rust程序中问题的关键技能,尤其是在你熟悉了所操作的类型(如Option)之后。
总结
本节课中我们一起学习了Option枚举。我们了解到:
Option<T>是一个用于表示可选值的枚举,包含Some(T)和None两个变体。- 它是处理可能缺失的值或潜在操作失败(如除数为零)的类型安全方式。
- 我们可以使用
match表达式来安全地检查和提取Option中的值。 unwrap方法可以快速获取Some中的值,但在值为None时会引发程序恐慌,因此需要谨慎使用。

通过掌握Option,你为编写健壮、无错误的Rust程序奠定了重要基础。
065:应用枚举 📂

在本节课中,我们将学习如何在实际场景中应用Rust的枚举(enum)。我们将创建一个表示文件大小的枚举,并编写一个函数来将其格式化为人类可读的形式(例如,字节、千字节、兆字节、千兆字节)。通过这个例子,你将看到枚举如何帮助组织代码,以及如何为枚举实现关联函数(方法)。
概述
我们将定义一个名为 FileSize 的枚举,它有四个变体:Bytes、Kilobytes、Megabytes 和 Gigabytes。然后,我们将编写一个函数,该函数接收一个以字节为单位的大小值,并根据其数值范围,将其匹配到相应的 FileSize 变体,最后格式化为易读的字符串。
定义枚举
首先,我们定义 FileSize 枚举。它代表文件大小的不同单位。
enum FileSize {
Bytes(u64),
Kilobytes(f64),
Megabytes(f64),
Gigabytes(f64),
}
这里,Bytes 变体存储一个无符号64位整数,而其他变体存储浮点数,以便进行除法运算。
实现格式化函数
接下来,我们实现一个函数 format_size。该函数接收一个字节数,通过匹配其数值范围,决定使用哪个 FileSize 变体,然后进行格式化。
以下是 format_size 函数的实现步骤:
- 使用
match表达式根据字节数的大小范围选择FileSize变体。 - 对于每个变体,进行相应的计算(例如,将字节转换为千字节需要除以1024.0)。
- 使用第二个
match表达式根据具体的变体生成格式化的字符串。
fn format_size(size: u64) -> String {
let filesize = match size {
0..=999 => FileSize::Bytes(size),
1000..=999_999 => FileSize::Kilobytes(size as f64 / 1024.0),
1_000_000..=999_999_999 => FileSize::Megabytes(size as f64 / (1024.0 * 1024.0)),
_ => FileSize::Gigabytes(size as f64 / (1024.0 * 1024.0 * 1024.0)),
};
match filesize {
FileSize::Bytes(bytes) => format!("{} bytes", bytes),
FileSize::Kilobytes(kb) => format!("{:.2} KB", kb),
FileSize::Megabytes(mb) => format!("{:.2} MB", mb),
FileSize::Gigabytes(gb) => format!("{:.2} GB", gb),
}
}
在 main 函数中测试
现在,我们可以在 main 函数中测试这个 format_size 函数。
fn main() {
let size = 2500; // 示例字节数
println!("{}", format_size(size));
}
运行这段代码,对于输入 2500,输出应为 "2.44 KB"。你可以尝试不同的字节数值,观察输出如何变化。
为枚举实现方法
上一节我们介绍了如何使用独立函数处理枚举。本节中,我们来看看如何将格式化逻辑更紧密地与枚举本身耦合,即为其实现一个方法。
在Rust中,就像为结构体(struct)实现方法一样,我们也可以使用 impl 关键字为枚举实现关联函数和方法。这样做可以使代码组织得更清晰,功能与数据结合得更紧密。
以下是 FileSize 枚举的实现(impl)块,其中定义了一个 format 方法:
impl FileSize {
fn format(&self) -> String {
match self {
FileSize::Bytes(bytes) => format!("{} bytes", bytes),
FileSize::Kilobytes(kb) => format!("{:.2} KB", kb),
FileSize::Megabytes(mb) => format!("{:.2} MB", mb),
FileSize::Gigabytes(gb) => format!("{:.2} GB", gb),
}
}
}
现在,更新 main 函数,直接使用枚举变体并调用其 format 方法:
fn main() {
let files = vec![
FileSize::Bytes(150),
FileSize::Kilobytes(2.5),
FileSize::Megabytes(100.0),
FileSize::Gigabytes(5.2),
];
for f in files {
println!("{}", f.format());
}
}
这种方法使得 format 逻辑成为 FileSize 类型的一部分,调用起来更加直观:filesize.format()。
总结
本节课中我们一起学习了Rust枚举的两个重要应用方式:
- 使用
match表达式和独立函数来处理枚举变体,并根据输入值动态创建和格式化枚举实例。 - 使用
impl关键字为枚举实现关联方法,将行为与数据类型紧密绑定,从而写出更模块化、更易维护的代码。

通过文件大小格式化的实际例子,你看到了枚举如何使代码意图更清晰,以及如何通过两种不同的代码组织方式(独立函数 vs 关联方法)来达到相似的目标。在实际开发中,为枚举实现方法通常是更受推荐的做法,因为它遵循了将数据与操作封装在一起的原则。
066:在向量中使用枚举 🧩

在本节课中,我们将学习如何将枚举(Enums)与向量(Vectors)结合使用。我们将创建一个包含不同形状(圆形和正方形)的向量,并计算所有形状的总面积。通过这个例子,你将看到如何利用枚举的变体(Variants)来存储不同类型的数据,并使用迭代器(Iterator)和map方法进行高效的数据处理。
枚举定义与向量创建
首先,我们定义一个名为Shape的枚举。它有两个变体:Circle和Square。每个变体都关联一个f64类型的值。对于Circle,这个值代表半径;对于Square,则代表边长。
enum Shape {
Circle(f64),
Square(f64),
}
接下来,我们创建一个Shape类型的向量。这个向量包含两个元素:一个半径为5.0的圆形和一个边长为3.0的正方形。
let shapes = vec![Shape::Circle(5.0), Shape::Square(3.0)];
计算总面积
现在,我们来计算这个向量中所有形状的总面积。我们将定义一个函数total_area,它接收一个Shape类型的向量,并返回一个f64类型的总面积。
以下是实现步骤的分解:
- 迭代向量:我们使用
.iter()方法获取向量的迭代器。 - 映射与匹配:使用
.map()方法处理迭代器中的每个元素。在map的闭包中,我们使用match表达式来匹配每个Shape变体。 - 计算面积:
- 如果匹配到
Circle(radius),则使用圆的面积公式 π * radius² 进行计算。 - 如果匹配到
Square(length),则使用正方形的面积公式 length² 进行计算。
- 如果匹配到
- 求和:最后,使用
.sum()方法将所有计算出的面积累加起来。
以下是完整的代码实现:
fn total_area(shapes: Vec<Shape>) -> f64 {
shapes
.iter()
.map(|shape| match shape {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Square(length) => length * length,
})
.sum()
}
让我们运行这段代码。对于半径为5.0的圆和边长为3.0的正方形,计算出的总面积约为87.53平方单位。
扩展性与灵活性
这种方法的优势在于其扩展性。如果我们想向向量中添加更多形状,例如再添加一个半径为2.0的圆,只需将其推入向量即可:
let shapes = vec![Shape::Circle(5.0), Shape::Square(3.0), Shape::Circle(2.0)];
再次运行total_area函数,总面积会相应更新,例如增长到约100平方单位。这演示了如何轻松地处理动态数据集合。
总结
本节课中,我们一起学习了如何将枚举与向量结合使用。我们定义了一个带有关联数据的枚举,创建了该枚举类型的向量,并利用迭代器的.map()方法和match表达式,高效地计算了集合中所有元素的总面积。
核心要点包括:
- 枚举变体可以存储数据,使类型更加丰富。
- 向量可以存储同一枚举类型的不同变体。
- 迭代器与
map、match的组合,是处理此类集合数据的强大且优雅的模式。

通过掌握这些概念,你可以在Rust中更灵活地组织和处理复杂的数据结构。
067:穷尽匹配 🍇
在本节课中,我们将学习Rust中一个非常重要的概念:穷尽匹配。当使用 match 表达式处理枚举(enum)时,Rust编译器会强制要求你处理所有可能的情况。我们将通过一个关于葡萄酒葡萄品种的枚举示例,来理解为什么需要这样做,以及如何使用通配符 _ 来简化代码。
问题引入:一个编译错误

上一节我们介绍了枚举和模式匹配的基本用法。本节中我们来看看,如果在匹配枚举时遗漏了某些情况会发生什么。
观察以下代码,在 match 表达式下,grapes 下方有一条红色的波浪线,这表明代码中存在一个错误。
enum WineGrapes {
CabernetFranc,
Tannat,
Merlot,
}
fn taste_wine(grapes: WineGrapes) {
match grapes {
WineGrapes::CabernetFranc => println!("This is a Cabernet Franc wine."),
// 这里故意注释掉了 Tannat 和 Merlot 的处理分支
// WineGrapes::Tannat => println!("This is a Tannat wine."),
// WineGrapes::Merlot => println!("This is a Merlot wine."),
}
}
为什么编译器会报错呢?一个像 WineGrapes 这样的枚举定义了若干变体。如果我们定义了三个变体,那么Rust会要求我们在 match 表达式中处理所有这三种变体。我们目前只处理了其中一种,所以编译器会提示错误。
理解穷尽性要求
Rust的设计目标是安全,它希望确保你的代码不会因为未处理的枚举变体而崩溃。因此,match 表达式必须是穷尽的,即覆盖所有可能的值。
当我们注释掉 Tannat 和 Merlot 的分支时,保存代码,红色波浪线依然存在。编译器给出的错误信息类似于:
missing match arm: Tannat and Merlot not covered 或 non-exhaustive patterns。
这意味着 match 没有覆盖 WineGrapes::Tannat 和 WineGrapes::Merlot 这两种情况。如果我们取消这些注释,让 match 覆盖所有三个变体,错误就会消失,代码可以正常工作。
fn taste_wine(grapes: WineGrapes) {
match grapes {
WineGrapes::CabernetFranc => println!("This is a Cabernet Franc wine."),
WineGrapes::Tannat => println!("This is a Tannat wine."),
WineGrapes::Merlot => println!("This is a Merlot wine."),
}
}
fn main() {
let my_wine = WineGrapes::CabernetFranc;
taste_wine(my_wine); // 输出:This is a Cabernet Franc wine.
}
使用通配符简化匹配
那么,是否每次都必须列出每一个变体呢?并非如此。我们之前已经见过一种例外情况:通配符模式。
当我们注释掉部分分支再次出现错误时,我们可以使用下划线 _ 来捕获所有未被明确列出的情况。
以下是具体做法:
fn taste_wine(grapes: WineGrapes) {
match grapes {
WineGrapes::CabernetFranc => println!("This is a Cabernet Franc wine."),
_ => println!("This wine is fine, but I don't care about the specific grape."),
}
}
你会发现,红色波浪线消失了。这是因为 _ 代表了“除了 CabernetFranc 之外的所有其他情况”。这样,match 表达式依然是穷尽的。
这种方法非常有用,特别是当枚举有很多变体,而你只关心其中少数几个的时候。你不需要显式地列出200个变体,只需要处理你关心的,然后用 _ 处理剩下的即可。
我们可以混合使用具体匹配和通配符。运行修改后的代码,当传入 WineGrapes::CabernetFranc 时,会输出对应的信息;如果传入 WineGrapes::Tannat 或 WineGrapes::Merlot,则会执行 _ 分支的代码。
fn main() {
taste_wine(WineGrapes::CabernetFranc); // 输出关于Cabernet Franc的信息
taste_wine(WineGrapes::Tannat); // 输出通配符分支的信息
taste_wine(WineGrapes::Merlot); // 输出通配符分支的信息
}
总结
本节课中我们一起学习了Rust中的穷尽匹配规则。核心要点是:
- 规则:使用
match处理枚举时,必须覆盖其所有变体。 - 目的:这是Rust保障代码安全、避免遗漏情况的重要手段。
- 简化方法:使用通配符模式
_可以捕获所有未被明确列出的情况,从而满足穷尽性要求,同时简化代码。

通过这个关于葡萄酒葡萄的例子,你应该理解了为什么编译器有时会“抱怨”,以及如何通过完整列出变体或使用 _ 通配符来让编译器满意。这是编写健壮Rust代码的关键一步。
068:枚举与变体总结 🧩

在本节课中,我们将总结Rust中枚举(Enums)和变体(Variants)的核心概念。我们将回顾如何定义和使用枚举,以及如何利用match表达式来处理不同的变体,从而构建更清晰、更强大的程序控制流。
枚举类型简介
上一节我们介绍了枚举的基本概念。枚举类型(简称Enum)允许你定义一种可以表示多种可能值的数据类型。这在处理“是A、B、C还是D”这类逻辑时非常有用,它提供了一种其他编程语言可能不具备的模式化处理逻辑的方式。
使用match表达式处理枚举
当你看到枚举时,它开启了处理这类逻辑的可能性。由于你使用了枚举,你可以避免使用一连串的if、else if、else语句来条件性地处理不同情况,转而使用match表达式。我们必须使用match来处理枚举的不同变体。
以下是使用match表达式处理枚举变体的基本语法:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("退出程序"),
Message::Move { x, y } => println!("移动到坐标 ({}, {})", x, y),
Message::Write(text) => println!("文本消息: {}", text),
Message::ChangeColor(r, g, b) => println!("颜色变更为 RGB({}, {}, {})", r, g, b),
}
}
枚举与变体的结合应用
除了枚举本身,我们还探讨了变体以及如何将所有这些概念结合到Rust程序中使用。这无疑是一个强大的概念,值得深入理解。
以下是枚举变体可以携带数据的几种形式:
- 单元变体:不关联任何数据,例如
Message::Quit。 - 元组变体:关联一个元组,例如
Message::Write(String)。 - 结构体变体:关联一个匿名结构体,例如
Message::Move { x: i32, y: i32 }。
总结
本节课中我们一起学习了Rust枚举类型及其变体的定义与使用方法。我们了解到枚举如何扩展程序的控制流逻辑,以及如何通过match表达式优雅地匹配和处理不同的变体。掌握枚举和match是编写健壮、清晰Rust代码的关键步骤。
069:库与Cargo介绍 🧰

在本节课中,我们将学习如何使用Rust解决一个现实世界的问题。我们将从零开始构建一个库,并在这个过程中介绍调试器、Makefile等实用工具和概念,帮助你像一个真正的Rust开发者一样工作。
概述
上一节我们学习了Rust的基础知识和环境配置。本节中,我们来看看如何综合运用这些知识来解决实际问题。
使用Rust解决现实世界问题是最终的目标步骤。我们已经掌握了基础知识,了解了多种类型,并配置了环境、安装了Rust。现在我们对Rust有了良好的理解,接下来要解决一个实际的工作问题。
我们将从零开始构建一个库,并利用调试器等工具来排查可能存在的问题。我们还会接触Makefile,了解它的意义、作用以及为何你可能希望在Rust项目中使用它。
我们将从头开始构建这个包含不同组件的库,准备好所有文件,并逐步添加代码,就像你已经是一名Rust开发者一样,去解决一个现实问题。
我的建议是,在学习本课时,尝试思考一个你想自己解决和实现的问题,并尝试应用这些相同的概念,同时为自己构建一些东西。这样你可以开始尝试在Rust中构建库,并应用这些概念。这种练习将使你更自如地运用我们已经学过的所有概念。
核心概念与工具
以下是本教程将涉及的核心部分:
-
构建库:我们将创建一个基本的库结构。在Rust中,库通常被定义为
lib类型的Cargo项目。# Cargo.toml 示例 [package] name = "my_library" version = "0.1.0" edition = "2021" [lib] name = "my_library" crate-type = ["lib"] -
使用调试器:调试是开发的关键环节。我们将使用Rust内置的调试支持,例如在代码中插入
println!宏或使用更高级的调试器(如LLDB或GDB集成)来检查变量状态。// 简单的调试输出 println!("Debug: The value of x is {}", x); -
理解Makefile:Makefile是一个自动化构建工具。虽然Cargo是Rust的主要构建系统,但Makefile可以用于编排更复杂的任务流,例如运行测试、构建文档和发布。
# 一个简单的Makefile示例 build: cargo build test: cargo test doc: cargo doc --open
总结
本节课中,我们一起学习了Rust项目开发的进阶步骤。我们从解决实际问题的角度出发,规划了如何从零构建一个库,并介绍了调试器和Makefile这两个在实战中非常有用的工具。记住,将所学概念应用于个人项目是巩固知识的最佳方式。现在,你可以开始构思并着手创建你的第一个Rust库了。
070:使用Cargo创建库 📚

在本节课中,我们将学习如何将一个现有命令行工具中的核心功能提取出来,创建一个独立的库。我们将使用Cargo来初始化和管理这个库项目,这是Rust生态中代码复用和项目组织的常见做法。
项目背景与目标
上一节我们介绍了代码复用的概念。本节中我们来看看一个具体的例子。我们有一个名为resplit的命令行工具,其功能类似于Linux的cut命令,可以按指定分隔符(如逗号)分割输入字符串并选择特定字段。目前,其核心的分割逻辑直接写在命令行工具的代码中。
我们的目标是:将resplit工具中的核心分割功能提取出来,创建一个独立的、可复用的库。这样,resplit工具可以依赖这个库,未来其他项目也能方便地使用这个功能。
创建库项目
以下是使用Cargo创建新库项目的步骤。
首先,我们使用cargo init命令,并加上--lib标志来指明我们要创建的是一个库,而不是一个二进制(命令行)项目。
cargo init --lib cli_utils
这个命令会在cli_utils目录下创建一个新的库项目。与创建二进制项目不同,库项目的src目录下会有一个lib.rs文件,而不是main.rs文件。lib.rs是库的根模块文件。
初始化项目结构
执行命令后,我们得到了一个基本的项目结构。让我们查看一下生成的文件。
Cargo.toml: 项目的配置文件,用于管理元数据和依赖。src/lib.rs: 库的源代码入口文件。.gitignore: Git版本控制的忽略文件。LICENSE和README.md: 许可证和项目说明文件(初始内容可能为空)。
打开src/lib.rs文件,我们会看到Cargo生成的示例代码。由于我们要从现有项目中移植代码,所以需要清空这个文件的内容,准备写入我们自己的库代码。
总结

本节课中我们一起学习了如何启动一个库项目。我们使用cargo init --lib命令创建了一个名为cli_utils的库项目骨架,并了解了库项目与二进制项目在初始结构上的主要区别(lib.rs vs main.rs)。在接下来的课程中,我们将把现有命令行工具中的功能代码移植到这个新建的库中,并逐步完善它,包括编写文档和发布。
071:向库中添加代码 📚
在本节课中,我们将学习如何将代码从一个项目移动到另一个项目的库(lib)文件中。我们将重点关注如何从标准输入读取数据,并处理相关的导入和类型问题。
概述
我们将回顾 re_split 项目,并将 read_standard_in 函数的代码复制到另一个 CLI 项目的库文件中。在此过程中,我们将解决导入问题,理解标准输入的概念,并确保代码类型正确。

复制代码与导入问题
上一节我们介绍了项目结构,本节中我们来看看如何将现有代码整合到库中。
首先,我们从 re_split 项目中复制 read_standard_in 函数的代码,并将其粘贴到新项目的库文件(lib.rs)中。粘贴后,代码会立即出现一些问题,主要是 BufReader 未被正确导入。
为了解决这个问题,我们需要使用 use 语句导入必要的模块。如果只导入 BufReader,我们会遇到错误。
use std::io::BufReader;
将鼠标悬停在错误提示的红线上,会看到信息:“no method named read_line found for struct BufReader in the current scope”。这意味着在 Rust 中,有时需要导入特定的 trait 才能使某些方法可用。
错误信息明确告诉我们该怎么做:需要导入 BufRead trait。这是从标准输入读取数据时的常见操作。
use std::io::{BufReader, BufRead};
理解函数功能
接下来,我们专注于 read_standard_in 函数本身,看看它具体做了什么。
该函数定义如下:它不接受任何参数,并返回一个 String。函数内部最终会返回一行经过修剪(trim)的字符串。
fn read_standard_in() -> String {
// ... 函数体
}
标准输入(stdin)的概念
在第五行,我们定义了标准输入:
let stdin = std::io::stdin();
如果你从未在终端中使用过命令行工具,标准输入是一种可以向工具传递输入的方式,通常使用管道(|)操作符。
以下是一个快速示例,说明如何在终端中使用标准输入:
echo "some text" | cut -d ' ' -f 1
在这个例子中,echo 命令的输出通过管道传递给了 cut 命令。cut 命令将接收到的文本(“some text”)作为标准输入,然后根据空格分隔符(-d ' ')提取第一个字段(-f 1),最终输出“some”。
这就是标准输入的基本概念,也是我们函数试图实现的功能。
读取并处理输入
回到我们的函数,我们需要定义一个可变变量来从标准输入读取数据。
以下是实现步骤:
-
创建缓冲读取器:我们创建一个
BufReader的新实例来读取标准输入,并使用.lock()方法。锁(lock)可以确保在读取期间独占标准输入,直到离开作用域,这是一种非常标准且有用的做法。let mut reader = BufReader::new(stdin.lock()); -
定义可变字符串变量:我们定义一个名为
line的可变String变量,用于存储读取的内容。let mut line = String::new(); -
读取一行数据:我们调用
reader.read_line方法,并将可变引用&mut line传递给它。如果读取失败,我们使用.expect方法处理错误,这会导致程序恐慌(panic)。对于这个简单的示例,这种处理方式是可以接受的。reader.read_line(&mut line).expect("Failed to read the input line"); -
修剪并返回字符串:最后,我们修剪
line两端的空白字符,并使用.to_string()方法将其转换为拥有的String类型返回。这一步是必需的,因为.trim()返回的是一个字符串切片(&str),而函数签名要求返回String。line.trim().to_string()
代码整合与总结
通过以上步骤,我们成功将 read_standard_in 函数整合到了库文件中。目前 main.rs 中还没有内容,我们正在逐步构建我们的 CLI 实用程序库项目。
本节课中我们一起学习了:
- 如何将代码移动到库文件中。
- 如何解决 Rust 中的导入和 trait 范围问题。
- 理解了标准输入(stdin)的概念及其在命令行工具中的应用。
- 实现了从标准输入读取一行数据、处理错误并返回修剪后字符串的完整函数。

这个基础函数为我们后续构建更复杂的 CLI 工具功能打下了良好的基础。
072:代码文档化 📚

在本节课中,我们将学习如何在Rust中为代码编写文档。我们将了解如何使用特定的注释语法来生成美观的HTML文档,以及如何为函数和整个库添加描述与示例。
概述
代码文档化不仅有助于他人理解你的代码,也能帮助你做出更好的设计决策。如果你发现很难用文档解释某个函数的功能,这可能意味着该函数过于复杂,需要考虑重构。
Rust内置了强大的文档工具cargo doc,它能将代码中的特殊注释自动生成为格式良好的网页文档。
生成基础文档
首先,我们来看如何为未添加任何注释的代码生成基础文档。
我们可以在项目根目录下运行以下命令:
cargo doc
这个命令会编译你的项目(例如库文件),然后在target/doc目录下生成HTML文档。生成的文档结构与Rust官方文档网站(docs.rs)完全一致,包含模块、结构体、枚举和函数等信息的自动排版。
为函数添加文档
上一节我们看到了自动生成的基础文档,本节中我们来看看如何为具体的函数添加详细的文档注释。
在Rust中,文档注释使用三个斜杠///。以下是为一个函数添加文档和示例的方法:
/// 该函数从标准输入读取一行,并将其作为字符串返回。
/// 如果读取失败,它会以一个有意义的消息(“failed to read input line”)触发panic。
///
/// # 示例
///
/// ```
/// use cli_utils::read_stdin;
/// let input = read_stdin();
/// println!("你输入了:{}", input);
/// ```
pub fn read_stdin() -> String {
// ... 函数实现
}
添加文档后,再次运行cargo doc并刷新浏览器,你会看到函数的描述和“示例”部分已按Markdown格式优雅地呈现出来。
为库或包添加文档
除了为函数添加文档,我们还可以为整个库或包添加概述性文档。
以下是添加库级文档的方法,注意这里使用的是//!注释:
//! 这是一个为命令行工具提供实用功能的库。
//!
//! # 示例
//!
//! ```
//! use cli_utils::read_stdin;
//! let input = read_stdin();
//! println!("用户输入:{}", input);
//! ```
这种文档会显示在生成的文档页面的最顶部,用于描述整个包的目的和基本用法。
文档注释与普通注释的区别
理解不同类型的注释至关重要,这决定了哪些内容会出现在最终的用户文档中。
以下是Rust中注释的主要类型:
- 文档注释 (
///或//!): 用于生成公共API文档。///用于注释紧随其后的项(如函数、结构体),//!用于注释包含它的项(如模块、crate)。 - 普通注释 (
//): 仅作为代码内部的说明,不会被cargo doc提取到公开文档中。
通过合理使用这两种注释,你可以控制文档中向用户暴露的信息,并将内部实现细节保留为普通注释。
总结

本节课中我们一起学习了Rust代码文档化的核心方法。我们了解了如何使用cargo doc命令生成文档,如何使用///为函数添加包含描述和示例的文档,以及如何使用//!为整个库添加概述。同时,我们也明确了用于生成公开API的文档注释与普通代码注释之间的区别。良好的文档是创建可维护、易理解软件的关键一步。
073:使用调试器 🐛

在本节课中,我们将学习如何使用调试器来诊断和修复Rust程序中的问题。我们将通过一个具体的示例,演示如何设置断点、单步执行代码以及观察变量状态,从而定位并解决一个常见的逻辑错误。
上一节我们介绍了调试的基本概念,本节中我们来看看如何在Visual Studio Code中实际使用调试器来解决问题。
我在准备课程示例时遇到了一个问题。虽然这个程序与我们课程开始时看到的库不完全相同,但它仍然很有价值,并且更容易操作,因为它有一个主函数,并且目前没有任何用于CI工具或Cargo包的测试。
以下是程序的核心逻辑:
// 这是一个简化的示例,用于演示调试过程
fn main() {
loop {
// 从标准输入读取一行
let mut input = String::new();
std::io::stdin().read_line(&mut input).expect("Failed to read line");
// 如果用户输入“stop”,则退出循环
if input.trim() == "stop" {
println!("Goodbye!");
break;
}
// 否则,打印输入的内容
println!("You entered: {}", input);
}
}
我们首先运行程序。如果输入“stop”,程序会打印“Goodbye”并正常退出。但是,如果我们想继续输入其他内容,然后再次尝试输入“stop”退出时,程序却没有按预期停止。问题似乎出在输入字符串没有被正确清理。
我尝试在循环开始时调用input.clear()来清空字符串,但问题依然存在。程序似乎陷入了某种状态,无法响应“stop”命令。这正是需要调试器介入的时刻。
为了进行调试,我们需要在Visual Studio Code中安装一个调试器扩展。我强烈推荐安装“CodeLLDB”扩展,它是一个由LLDB驱动的原生调试器,非常适合调试Rust等编译型语言。
安装完成后,我们就可以使用调试功能了。与直接点击“运行”不同,我们现在可以点击“调试”按钮。但仅仅启动调试器可能还不够直观,我们需要设置断点来暂停程序执行,以便观察状态。
以下是设置和使用调试器的关键步骤:
- 设置断点:在代码编辑器的行号左侧点击,会出现一个红点,这表示一个断点。当调试器运行到这一行时,程序会暂停。
- 启动调试:点击调试按钮启动程序。程序会在你设置的断点处暂停。
- 观察变量:在暂停状态下,侧边栏的“变量”窗口会显示当前作用域内所有变量的值。
- 控制执行:使用调试控制栏的按钮可以控制程序执行:
- 单步跳过(Step Over):执行当前行,然后跳到下一行。
- 单步进入(Step Into):如果当前行是一个函数调用,会进入该函数内部。
- 继续(Continue):从当前断点继续执行,直到遇到下一个断点或程序结束。
在我的例子中,我在read_line之后和条件判断之前设置了断点。启动调试并输入“stop”后,程序在断点处暂停。我观察到input变量的值是"stop\n"。然后我使用“单步跳过”执行trim()和比较操作,程序正确地进入了if分支并退出。
然而,当我修改代码,在循环末尾添加了input.clear()后,问题出现了。重新调试时,我发现:输入“stop”后,程序在判断input.trim() == "stop"时条件为真,打印了“Goodbye”。但紧接着,执行流回到了循环开头,并立即执行了input.clear()。这时,如果我再次输入“stop”,input变量在判断之前就已经被清空了,因此条件判断失败,程序无法退出。这清楚地揭示了bug的位置:clear调用放错了地方。
发现问题后,我停止了调试器,将input.clear()语句从循环末尾移到了循环开始、read_line之前。这样,每次循环都会从一个干净的字符串开始读取新输入,而不会影响上一次循环中已存储并用于判断的值。
重新运行调试器验证,问题得到解决。输入“stop”后,程序能正确识别并退出。

本节课中我们一起学习了如何使用Visual Studio Code和LLDB调试器来诊断Rust程序。我们了解了如何设置断点、单步执行代码、观察变量变化,并利用这些工具定位了一个因变量清理时机不当导致的逻辑错误。调试器是开发过程中极其强大的工具,能帮助我们深入理解程序运行状态,高效地找到并修复问题。
074:使用Makefile自动化构建 🛠️

在本节课中,我们将学习如何使用Makefile来简化和标准化Rust项目的构建与管理流程。Makefile可以帮助我们抽象出常用的命令,使得项目构建过程更加一致和自动化,特别是在团队协作或持续集成环境中。
概述
上一节我们介绍了基本的Cargo工具链使用。本节中,我们来看看如何通过创建一个Makefile来封装这些命令,从而建立一个标准化的项目构建流程。
创建基本Makefile目标
首先,我们创建一个简单的Makefile,定义一些基本的目标(targets)。目标是Makefile中的可执行单元,类似于子命令。
以下是两个基本目标示例:
clean: 清理项目构建产物。clean: cargo cleanbuild: 构建项目。build: cargo build
在终端中,你可以通过运行 make clean 或 make build 来执行这些目标。执行 make clean 会运行 cargo clean 并删除 target 目录;执行 make build 则会运行 cargo build 进行项目编译。
改进:添加帮助目标
目前,我们可能不清楚Makefile中有哪些可用的目标。为了解决这个问题,我们可以设置一个默认目标并创建一个帮助(help)目标。
首先,我们指定使用的Shell并设置一个默认的“伪目标”(.PHONY),这样当不提供任何参数运行 make 时,会执行默认操作。
SHELL := /bin/bash
.PHONY: help
help:
我们希望 make 或 make help 能列出所有目标及其描述。以下是一个实现此功能的 help 目标:
help: ## 显示此帮助信息
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
这个命令会解析Makefile本身,寻找以 ## 注释作为描述的目标,并以彩色格式打印出来。现在,我们需要为其他目标添加描述。
为目标添加文档
为了使帮助信息生效,我们需要在每个目标后面使用 ## 添加描述。
以下是添加了描述的目标:
clean: 使用Cargo清理项目。clean: ## 使用Cargo清理项目 cargo cleanbuild: 使用Cargo构建项目。build: ## 使用Cargo构建项目 cargo buildclippy: 使用Clippy进行代码检查。clippy: ## 使用Clippy进行代码检查 cargo clippyformat: 使用Cargo格式化代码。format: ## 使用Cargo格式化代码 cargo fmt
现在,运行 make help 或直接运行 make,就会看到一个清晰、文档化的目标列表。
增强健壮性:处理依赖
某些工具(如 rustfmt 或 clippy)可能没有预先安装。在自动化环境(如CI/CD管道)中,我们希望命令能自动处理这些依赖。
例如,我们可以改进 format 目标,确保 rustfmt 组件已安装:
format: ## 使用Cargo格式化代码
rustup component add rustfmt 2>/dev/null || true
cargo fmt
2>/dev/null 将错误输出重定向到空设备,|| true 确保即使组件已安装,命令也不会失败。这样,无论在哪种系统上运行 make format,它都能“正常工作”。
你可以对 clippy 目标进行类似的增强。
高级自动化:版本号管理
Makefile的强大之处在于可以封装更复杂的逻辑。例如,我们可以创建一个目标来自动更新项目的版本号。
假设我们想更新 Cargo.toml 中的版本。以下是一个 bump 目标的示例:
bump: ## 交互式地提升项目版本号
@echo "Current version is $$(cargo pkgid | cut -d# -f2)"
@read -p "New version: " new_version; \
old_version=$$(cargo pkgid | cut -d# -f2); \
sed -i.bak "s/version = \"$$old_version\"/version = \"$$new_version\"/" Cargo.toml; \
echo "New version is $$(cargo pkgid | cut -d# -f2)"
这个脚本会:
- 显示当前版本。
- 提示用户输入新版本。
- 使用
sed命令在Cargo.toml中替换版本号,并自动创建一个备份文件(Cargo.toml.bak)。 - 显示更新后的版本。
运行 make bump 即可交互式地完成版本升级,这比手动编辑文件更可靠,尤其适合自动化流程。
总结
本节课中我们一起学习了如何为Rust项目创建和使用Makefile。我们从一个简单的命令封装开始,逐步添加了帮助文档、依赖处理以及复杂的版本管理自动化。
关键要点包括:
- Makefile可以标准化你的构建命令(
build,test,clean)。 help目标能极大提升Makefile的可用性。- 通过处理工具链依赖(如
rustup component add),可以使Makefile在各种环境(包括CI/CD)中更健壮。 - Makefile能封装复杂操作(如版本号更新),实现一键自动化。

即使你只实现一个带帮助的简单Makefile,也能为你管理Rust项目(乃至其他语言的项目)带来一致性,并为后续集成到持续集成/持续交付系统打下良好基础。
075:库与Cargo总结


在本节课中,我们将总结使用库和Cargo工具链的关键概念与实践。我们将回顾如何构建、测试和链接Rust项目,并探讨这些技能在真实开发场景中的重要性。
真实场景中的Rust应用
将Rust应用于真实世界的场景非常有用。你已经学习了如何实现某些功能,例如调试器。
调试器的作用与使用
当Rust程序出现问题时,除了添加大量println!语句,我们如何解决?你当然可以那么做。但拥有调试器为何有用?如何使用它?以及如何配置你的文本编辑器来实现?这些都是至关重要的技能。
掌握这些技能后,你不仅能构建Rust程序,还能在程序运行不符合预期时,理解如何调试它们。
构建工具:Makefile
使用Makefile非常有用,因为它以一种方式进行了抽象和标准化,规定了你的Rust程序将如何构建、测试和链接。
链接的最佳实践
使用链接无疑是一种最佳实践。你可以利用这种抽象,在后续尝试自动化操作时将其集成起来。我们当然会在后续课程中涵盖自动化。
总结
本节课中,我们一起学习了库管理与Cargo工具的核心总结。这些内容非常实用,希望你能在后续的Rust编程实践中应用它们。
076:模块介绍 🧩
在本节课中,我们将要学习Rust中的模块系统。你已经创建了一个Rust库项目,并向lib.rs文件添加了代码。随着项目增长,将所有代码都放在lib.rs文件中会变得难以管理。这时,模块系统就能帮助你更好地组织代码结构。
上一节我们介绍了如何创建库项目并编写初始代码,本节中我们来看看如何使用模块来扩展和组织你的项目。
什么是模块? 📦
模块是Rust中用于组织代码的核心特性。它允许你将相关的函数、结构体和其他项分组,并控制它们的可见性(即哪些部分可以被外部代码访问)。使用模块的主要目的是提高代码的可读性、可维护性和复用性。
当你的项目开始扩展时,你可能会考虑使用模块来组织代码,因为将所有内容都添加到lib.rs文件中并不是一个长久的、清晰的组织方式。
如何添加模块
在Rust中,有多种方式可以添加模块。我们将重点介绍通过创建新文件并将其作为库的一部分暴露出来的方法。以下是几种常见的模块定义方式:
- 内联模块:直接在
lib.rs或任何其他文件中使用mod关键字定义。mod my_module { // 模块内容 } - 文件模块:创建一个与模块同名的
.rs文件,然后通过mod关键字声明它。 - 目录模块:创建一个与模块同名的目录,并在其中放置一个
mod.rs文件来定义模块内容。
我们将主要探讨第二种方式,因为它最常用于将代码拆分到不同文件中。
模块的交互与可见性
模块之间可以相互调用,但你需要理解Rust的可见性规则。默认情况下,模块内的所有项(函数、结构体等)都是私有的,只能在模块内部访问。要使它们对其他模块或外部代码可见,需要使用pub关键字。
例如,以下代码定义了一个公共函数:
pub fn public_function() {
println!("这个函数可以被外部访问。");
}
当你将代码拆分到不同文件并通过模块组织起来后,你需要使用use关键字将其他模块的路径引入当前作用域,或者使用完全限定路径来调用其他模块中的项。

总结
本节课中我们一起学习了Rust模块系统的基础知识。我们了解到模块是组织代码、管理作用域和可见性的强大工具。通过将代码拆分到不同的模块和文件中,你可以让项目结构更加清晰,更易于维护和扩展。记住,使用mod关键字来声明模块,使用pub来控制可见性,是构建模块化Rust程序的关键步骤。
077:使用Cargo管理依赖 📦

在本节课中,我们将学习如何使用Cargo来管理项目的依赖。我们将通过一个实际例子,演示如何将一个本地开发的库作为依赖项引入到另一个项目中,而无需先将其发布到公共仓库。
概述
我们已经为我们的库 ciu 取得了很大进展。我们添加了一个名为 read_stdin 的函数,并且项目组织得很好。我们有一个基础的 Makefile,但目前还没有任何外部依赖。现在,我们希望在另一个项目中使用我们刚刚创建的 read_stdin 函数。这个函数是从我之前另一个包中提取出来的。那么,具体该如何操作呢?
我们将回到另一个项目,并将 ciu 库作为依赖引入。我知道 ciu 库的本地路径,接下来就让我们看看如何实现。
项目背景
这是 re_split 项目。如果我查看 lib.rs 文件,会发现 read_stdin 函数原本定义在这里。但现在,我们不打算直接调用它。让我们检查一下它在哪里被使用。实际上,它是在 main.rs 中被调用的。
在 main.rs 中,我们可以看到 read_stdin 的调用。为了进行测试,我们先将这行代码注释掉。保存文件后,我们会看到很多红色错误提示。这是因为 read_stdin 函数不再能从 re_split 这个 crate 中获取了。这很正常。
引入本地依赖
那么,我们如何从另一个项目(即我们刚刚创建的 ciu 库)中获取 read_stdin 函数呢?我们甚至已经有了如何使用它的示例。
让我们查看 Cargo.toml 文件。在这个文件中,我们不仅可以设置包的详细信息(如名称和版本),还可以设置依赖项。目前,re_split 项目正在使用 clap(一个用于轻松构建命令行工具的命令行框架),版本是4,并启用了一些特性。
但是,对于尚未发布的库,我们有不同的方式来定义依赖。我还没有发布我的 ciu 库。我可以在 Cargo.toml 中这样定义:
[dependencies]
ciu = { path = "../applied_rust/examples/ciu" }
我使用了一个相对路径来指向 CIU 项目的位置。我知道它在父目录的 applied_rust/examples/ciu 路径下。保存文件后,Rust Analyzer 会进行大量工作,一切看起来都正常了。
让我再确认一下发生了什么:我将 ciu 定义为一个依赖项。因为它尚未发布,所以我使用了 path 关键字,并传入了一个指向 CIU 项目目录的相对路径。路径是 ../applied_rust/examples/ciu,这就是我的 CIU crate。
使用依赖项
依赖项设置看起来没问题。下一步,我可以回到 main.rs 文件。read_stdin 的调用仍然被注释着,main.rs 也还是一团糟。现在,我要做的是从 ciu 库中获取 read_stdin 函数。
我需要使用 ciu::read_stdin 来引入它,然后重新定义它。这样,read_stdin 函数就从我们刚刚创建、构建并编写了文档的库项目中引入了,并由 Cargo 负责管理。
其他依赖管理方式
这是一种非常酷的方式,你可以在开发库时,从本地路径引入它们,尤其是在你还没有发布它们的时候。
另一种更深入的方式是使用 Git 仓库作为标识符,Cargo 会从代码仓库中拉取依赖。你甚至可以指定特定的分支。
此外,如果你希望引入已发布的依赖,比如 clap,你完全可以通过指定版本和一些特性来实现。
总结

本节课中,我们一起学习了如何使用 Cargo 管理项目依赖。我们重点演示了如何通过 Cargo.toml 文件的 path 字段,将一个本地开发的 Rust 库作为依赖引入到另一个项目中。这种方法在库尚未发布到公共仓库(如 crates.io)时非常有用。我们还简要提到了使用 Git 仓库或指定版本来管理依赖的其他方式。掌握这些技能,将帮助你更灵活地组织和构建复杂的 Rust 项目。
078:使用模块扩展

概述
在本节课中,我们将学习如何在Rust项目中通过创建新模块来扩展库的功能。我们将以添加终端颜色输出功能为例,演示如何将代码组织到独立的文件中,并在主库文件中声明和暴露该模块。
模块化扩展的必要性
上一节我们介绍了基础库的构建。本节中我们来看看如何通过添加新模块来扩展库的功能,同时保持代码的整洁。
CI U 库已经非常有用,我们现在想尝试扩展它。CI utilities 旨在为我想要构建的命令行工具提供实用程序,这也是我们这里的用例。到目前为止,我们只有一个函数,我想添加更多功能,但我不想污染 lib.rs 文件,而是希望使用一个单独的文件。
创建颜色模块
对于命令行实用程序,一个常见的模式和需求是为终端输出添加颜色。我将转到 src 目录,添加另一个名为 colors.rs 的文件。
以下是创建颜色函数的步骤:
- 在
colors.rs文件中,创建一个函数。 - 这个函数将命名为
red。 - 函数参数
s的类型是字符串切片&str。 - 函数返回一个带有颜色输出的
String。
这个函数的作用是格式化字符串,使其包含ANSI转义代码。它使用一个变量,并在特定的颜色开始和结束转义码之间放置字符串,这是为输出内容着色的基础。
让我们快速开始记录这个函数。我们将说明它返回一个包裹在红色中的字符串,并提供一些使用示例。
/// 返回一个包裹在红色ANSI代码中的字符串。
///
/// # 示例
/// ```
/// use ci_utils::colors::red;
/// let colored_text = red("Hello");
/// ```
pub fn red(s: &str) -> String {
format!("\x1b[31m{}\x1b[0m", s)
}
代码保存后看起来很不错。这里需要考虑的一点是,与Python等其他语言不同,在Rust中仅仅创建一个名为 colors.rs 的文件本身并没有特殊意义。
在库中声明模块
对Rust有特殊意义的特定文件是库的 lib.rs 和可执行文件的 main.rs。lib.rs 是所有内容的入口点。因此,我们需要回到 lib.rs 文件进行操作。
我们将在导入语句下方添加模块声明。
pub mod colors;
我们这样做是因为否则 colors 模块将不可用。让我们再看一下 colors.rs 中的示例。第5行的 ci_utils::colors::red 意味着我期望该函数以那种形式可用。通过将代码放在不同的文件中,我命名该文件为 colors,并使该模块可用并暴露 red 函数,否则这些将无法工作。为了让这些功能可用,我必须到我的 lib.rs 文件中进行声明。
代码组织策略
这是一种非常简单、直接的方式,我们可以开始暴露功能并添加更多内容,而不必污染或全部放入 lib.rs 中。你当然可以将 red 函数直接放在 lib.rs 中,并在那里处理颜色功能。
选择将其分离到单独的文件中是一种组织决策。何时应该将内容放在单独的文件中?这只是一个组织上的决定,并没有硬性规定你必须将内容放在单独的文件中。你完全可以仍然在 lib.rs 中添加内容,但将内容分开可能是一个好主意,以便于解析、阅读和理解代码的不同部分试图实现的功能。
总结

本节课中我们一起学习了如何通过创建新模块来扩展Rust库。我们创建了一个独立的 colors.rs 文件来存放颜色输出函数,并在 lib.rs 中通过 pub mod colors; 声明将其暴露给库的使用者。这种方式有助于保持代码结构清晰,便于维护和扩展。记住,模块化是一种代码组织策略,旨在提高项目的可读性和可管理性。
079:使用文档测试验证代码 📝

在本节课中,我们将学习Rust中一个强大的功能——文档测试。我们将了解如何通过编写文档注释来同时生成文档和可执行的测试用例,从而确保代码示例的正确性,并验证模块的公开接口是否按预期工作。
概述
上一节我们介绍了如何为代码添加文档注释。本节中我们来看看如何利用这些注释来验证代码功能。当我们在库项目中编写了大量函数和模块,却没有一个可执行的 main 函数时,文档测试提供了一种便捷的方式来确保一切工作正常。
文档测试的基本原理
我们通过在代码上方添加三个反斜杠 /// 来编写文档注释。这种注释不仅能生成项目文档,其内部的代码示例还可以被自动执行和测试。这就是所谓的“文档测试”。
例如,以下是一个文档注释,它包含了一个使用 utils::colors::red 函数的示例:
/// 这个函数返回红色字符串。
/// # 示例
/// ```
/// use my_crate::utils::colors::red;
/// let red_str = red();
/// assert_eq!(red_str, "red");
/// ```
请注意示例中的 use 语句。它是必需的,因为它告诉测试运行器如何找到被测试的函数。当我们运行 cargo test 时,Rust会提取这些注释中的代码块,编译并运行它们,以此验证示例是否仍然有效。
运行文档测试
在IDE中,你可能会在文档注释旁边看到一个“Run doctest”的按钮或链接。点击它,测试结果会输出在控制台中,显示“test result: ok”或具体的失败信息。
文档测试实际上在执行我们写在注释里的例子。它验证了文档中的代码片段在当前代码环境下能够正确编译和运行。
文档测试的验证作用
文档测试的核心价值在于它能自动验证文档与代码的一致性。假设我们在 lib.rs 中公开了模块 utils::colors,文档示例才能正常工作。
如果我们忘记公开模块(例如,注释掉 pub mod colors;),文档测试将立即失败,并给出类似“file not included in module tree”的错误。这能让我们立刻发现问题。
同样,如果文档示例中调用的函数名或返回值与代码实际不符(例如,示例中写的是 red(),但函数返回的是 "blue"),文档测试也会编译失败,提示“no red in colors”。这确保了我们的文档始终是准确、可运行的。
模块级文档注释
除了为函数添加文档,我们还可以为整个模块添加顶层的文档注释。这使用 //! 符号,并放置在模块文件的开头。
以下是在 colors.rs 模块文件顶部添加文档的示例:
//! 颜色模块,提供终端输出用的颜色字符串。
//! 包含一些辅助函数。
//! # 示例
//! ```
//! use my_crate::utils::colors::{red, blue};
//! let r = red();
//! let b = blue();
//! assert_eq!(r, "red");
//! assert_eq!(b, "blue");
//! ```
模块级文档适合放置一些更综合性的示例,展示模块中多个功能的协同使用。添加模块级文档后,在 lib.rs 中导入该模块时,也会出现“Run doctest”的选项,点击它可以运行该模块内所有的文档测试。
文档测试的优势
以下是文档测试带来的主要好处:
- 确保文档准确性:代码变动时,过时的文档示例会导致测试失败,迫使开发者更新文档。
- 提供可执行示例:用户可以直接复制文档中的代码并运行,降低了学习成本。
- 辅助开发验证:在编写正式的单元测试或集成测试之前,开发者可以通过文档测试快速验证接口的基本功能。
- 增强信心:在重构或移动代码时,运行文档测试可以快速确认公开API是否仍然可用,包括在
lib.rs主模块中定义的内容。
总结

本节课中我们一起学习了Rust的文档测试功能。我们了解到,通过编写包含代码示例的 /// 注释或 //! 模块注释,我们不仅能生成漂亮的文档,还能创建一套可自动执行的测试用例。这套机制强制保证了文档示例的正确性,并在开发过程中为我们提供了快速的反馈,是编写高质量、可维护Rust库的重要工具。
080:定义公有与私有模块 🧱

在本节课中,我们将学习Rust中模块(module)的公有(public)与私有(private)访问控制。理解这个概念对于构建结构良好、封装性强的库至关重要。我们将通过代码示例,清晰地展示pub关键字的作用以及默认的私有行为。
概述
之前我们已经学习了如何验证和使用模块。本节我们将重点探讨如何控制模块、函数和数据结构的可见性。通过使用pub关键字,我们可以决定哪些部分可以被外部代码访问,哪些部分只能在模块内部使用。默认情况下,Rust中的所有项(item)都是私有的。
公有与私有的基本概念
在Rust中,pub是一个关键字,用于使一个模块、函数或数据结构对外部公开。这意味着它可以在定义它的模块之外被使用。
公式/概念:
- 公有:
pub关键字修饰。 - 私有:默认状态,无
pub关键字。
如果一个项没有pub关键字,那么它就是私有的,只能在定义它的模块及其子模块内部访问。
演示:函数可见性
让我们通过一个具体的例子来理解。假设我们有一个公开的函数red_string。
pub fn red_string() -> String {
String::from("red")
}
因为它是pub的,所以可以在模块外部被调用。
现在,如果我们移除pub关键字:
fn red_string() -> String { // 移除了 pub
String::from("red")
}
然后运行测试(cargo test),测试将会失败。错误信息会指出red_string现在是一个私有函数。虽然我们没有显式声明它为私有,但移除pub关键字后,它就变成了私有的。这是Rust的默认行为:所有项默认都是私有的。
演示:模块可见性
同样的规则也适用于模块本身。如果一个模块没有被声明为pub,那么外部代码就无法使用它。
例如,在lib.rs中:
mod colors; // 这是一个私有模块
如果colors模块是私有的,那么任何尝试从lib.rs模块外部(例如另一个crate)通过use crate::colors来引入它的操作都会失败。错误信息会提示“私有模块”。
模块内部的可见性规则
模块内部的代码可以自由访问该模块内的所有私有项。这是一个关键点。
考虑以下在colors模块内部的代码:
// 在 colors 模块内部
fn red() -> String { ... } // 私有函数
fn blue() -> String { ... } // 私有函数
fn example() { // 这也是一个私有函数
let r = red(); // 可以访问私有的 red
let b = blue(); // 可以访问私有的 blue
}
example函数可以调用同模块内的私有函数red和blue,因为它们都在同一个作用域(colors模块)内。私有性主要是针对模块外部的访问限制。
测试与可见性
这引出了一个关于测试的常见问题。如果我们想从模块外部(例如在测试代码中)测试一个私有函数,应该怎么办?因为测试通常写在单独的tests目录或使用#[cfg(test)],它们被视为模块外部代码。
有以下几种常见的解决方法:
- 将需要测试的函数设为
pub:这是最简单直接的方法。 - 将测试写在模块内部:使用
#[cfg(test)]在模块内部编写测试,这样测试代码就可以访问私有项。 - 通过公有接口测试私有逻辑:只测试对外公开的函数,这些函数内部会调用私有逻辑。
在当前的演示中,为了确保测试通过,我们将恢复pub关键字。
库设计的考量
在构建库时,你需要仔细考虑哪些模块和函数应该公开。例如,你可能希望colors模块仅用于lib.rs内部的代码组织,而不想暴露给库的使用者。
你可以这样组织:
// 在 lib.rs 中
mod colors; // 私有模块,仅内部使用
pub fn some_public_api() {
// 可以在内部使用 colors 模块
let red = colors::red();
// ...
}
这样,库的使用者无法直接访问crate::colors,但你的库内部代码可以自由使用它。这有助于保持清晰的公共API边界。
总结
本节课我们一起学习了Rust中公有与私有访问控制的核心机制:
- Rust中所有项(模块、函数、结构体等)默认都是私有的。
- 使用
pub关键字 可以使一项变为公有,从而允许在定义它的模块之外被访问。 - 模块内部的代码可以访问该模块的所有项,无论其是公有还是私有。
- 在设计库时,需要仔细规划公有API,通过有选择地使用
pub关键字来暴露必要的功能,同时隐藏实现细节,这有助于构建稳定、易于维护的代码库。

理解并正确运用可见性规则,是编写高质量、模块化Rust代码的关键一步。
081:结构体中的私有与公有字段 🏗️

在本节课中,我们将学习如何控制Rust结构体中字段的访问权限,即区分私有字段与公有字段。我们将通过创建一个配置模块来演示这一概念,并了解如何通过文档测试来验证我们的代码。
概述
上一节我们介绍了如何使用文档测试以及如何控制模块和项的公开与私有范围。本节中,我们将实际创建一个新的模块,并定义一个包含私有和公有字段的结构体,以此来演示Rust中的访问控制机制。
创建配置模块
首先,我们创建一个名为 config.rs 的新模块。这个模块将包含应用程序的配置选项。
pub mod config {
// 配置内容将在这里定义
}
定义枚举
在配置模块中,我们首先定义两个枚举,用于表示日志级别和日志输出目标。
以下是日志级别枚举的定义:
pub enum LogLevel {
Debug,
Info,
Error,
}
接下来,我们定义日志输出目标枚举:
pub enum LogOutput {
StdOut,
StdErr,
File(String),
}
定义结构体
现在,我们定义一个名为 Logging 的结构体,它将包含日志配置的具体字段。
pub struct Logging {
enabled: bool,
level: LogLevel,
destination: LogOutput,
}
为结构体实现方法
为了能够方便地创建 Logging 结构体的实例,我们为其实现一个构造函数 new。
impl Logging {
pub fn new() -> Self {
Logging {
enabled: false,
level: LogLevel::Info,
destination: LogOutput::StdOut,
}
}
}
添加文档和示例
为了验证我们的代码,并为使用者提供参考,我们为结构体添加文档注释和示例。
以下是结构体的文档注释和一个使用示例:
/// 该结构体包含用于控制日志的配置选项。
///
/// # 示例
///
/// ```
/// use crate::config::Logging;
///
/// let config = Logging::new();
/// ```
pub struct Logging {
enabled: bool,
level: LogLevel,
destination: LogOutput,
}
我们还可以添加另一个示例,展示如何使用自定义值创建结构体实例:
/// # 使用自定义值创建的示例
///
/// ```
/// use crate::config::{Logging, LogLevel, LogOutput};
///
/// let config = Logging {
/// enabled: true,
/// level: LogLevel::Debug,
/// destination: LogOutput::File("log.txt".to_string()),
/// };
/// ```
理解访问控制问题
当我们运行文档测试时,可能会遇到编译错误,提示结构体的字段是私有的。这是因为在Rust中,结构体的字段默认是私有的,只能在定义它们的模块内部访问。
错误信息可能如下所示:
fieldenabledof structLoggingis privatefieldlevel... is privatefielddestination... is private
解决方案:使用 pub 关键字
为了使结构体的字段能够在模块外部被访问和构造,我们需要使用 pub 关键字将它们标记为公有。
修改后的结构体定义如下:
pub struct Logging {
pub enabled: bool,
pub level: LogLevel,
pub destination: LogOutput,
}
进行此更改后,文档测试应该能够顺利通过。这演示了如何通过 pub 关键字来控制对结构体字段的访问。
灵活控制访问权限
Rust的访问控制非常灵活。你可以根据需求,只将部分字段设为公有,而将其他字段保持私有。例如,如果你不希望使用者直接修改 destination 字段,可以将其保持为私有,并通过提供公有方法来间接修改或访问它。
pub struct Logging {
pub enabled: bool,
pub level: LogLevel,
destination: LogOutput, // 保持私有
}
impl Logging {
pub fn set_destination(&mut self, dest: LogOutput) {
self.destination = dest;
}
}
枚举的访问控制
需要注意的是,枚举变体(如 LogLevel::Debug)的访问控制与结构体字段类似。如果枚举本身是公有的,但其变体在模块外部被用于构造一个结构体(该结构体在模块外部),那么这些变体也必须是公有的,或者通过其他公有接口来使用。
总结
本节课中,我们一起学习了Rust结构体中字段的访问控制。我们了解到:
- 结构体字段默认是私有的。
- 使用
pub关键字可以将字段标记为公有,从而允许在定义它们的模块之外进行访问。 - 你可以根据设计意图,灵活地选择将哪些字段设为公有或私有,以封装内部实现细节。
- 枚举变体也遵循类似的访问规则。

建议你通过修改字段的 pub 状态、编写文档测试或在模块外部使用这些结构体来实践这一概念,以加深对Rust访问控制机制的理解。
082:模块总结 🧩

在本节课中,我们将一起总结Rust模块系统的核心概念。你将学习如何组织代码、控制可见性,并理解模块如何帮助你构建清晰且可维护的程序。
上一节我们介绍了模块的创建与使用,本节中我们来对整个模块系统进行总结。
现在,你应该对如何将程序扩展到 lib.rs 或 main.rs 之外感到更加得心应手了。这取决于你正在构建的程序类型。你学会了如何创建模块,以及如何将这些模块引入你的代码中,使其可用。
以下是关于模块可见性的关键点:
- 你可以控制模块内容是公开可用还是私有可用。
- 你现在知道如何公开特定内容,例如,你可能只想公开结构体中的某些字段,而不是所有内容或它定义的每一样东西。
因此,现在你有能力定义其工作方式。根据你的库或命令行工具(你的二进制文件)应该实现的功能,你可以决定哪些内容应该保持私有,哪些应该公开可用。
这些概念非常有用,因为它们允许你精确调整Rust程序中你希望的行为方式。

本节课中,我们一起学习了Rust模块系统。你掌握了如何通过模块组织代码、使用 pub 关键字控制项(item)的可见性,以及根据项目需求(库或二进制可执行文件)来设计公开的API。这些技能是构建结构良好、易于维护的Rust程序的基础。
083:测试介绍 🧪

在本节课中,我们将要学习Rust中测试的重要性、基本概念以及如何开始为你的代码编写测试。测试是确保代码逻辑正确性和程序健壮性的关键实践。
测试的重要性
现在你可能会想,我真的需要在Rust中编写测试吗?这真的有必要吗?Rust是一门强类型语言。我已经在非常细致地定义函数接收的类型、期望的类型以及函数返回的类型(如果函数有返回值的话)。在我构建的代码中,为什么还需要测试呢?
答案是,测试将增加你对代码的信心,是开发任何编程语言时的一种健壮的最佳实践。尤其是在像Rust这样非常适合测试的语言中,你绝对应该开始并习惯于测试。
类型定义与测试逻辑
定义类型、参数、返回类型和值,这很好。但你仍然需要测试逻辑。测试一个完全没有逻辑的东西不一定有必要。但是,如果你期望在某些情况下有不同的行为,或者一段代码在满足特定条件时应该执行某些操作,那么这就是一个绝佳的测试示例。
Rust测试入门
我们将看到如何在Rust中开始测试。我们将看到如何组织你的测试:你是否需要创建一个名为tests的目录,是否需要添加一些文件,或者甚至是否需要为已经是库一部分的现有代码添加测试。
例如,在我们定义并一直试图解决的项目用例中,我们将把所有内容整合起来。我们将添加一些测试,并尝试构建一个非常、非常健壮的库项目,以便我们可以有信心地构建这些内容。
核心概念与代码示例
在Rust中,测试通常使用#[test]属性来标记。一个基本的测试函数如下所示:
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
assert_eq!宏是测试中最常用的工具之一,用于断言两个表达式的结果相等。
测试的组织结构
以下是如何在Rust项目中组织测试的常见方式:
- 单元测试:通常放在每个源代码文件的
tests模块中,并使用#[cfg(test)]属性来条件编译。 - 集成测试:位于项目根目录下的
tests目录中,每个文件都是一个独立的集成测试套件。
总结
本节课中我们一起学习了Rust中测试的基础知识。我们了解了为什么即使在一个强类型系统中测试逻辑仍然至关重要,并预览了如何通过#[test]属性编写简单的测试用例以及如何组织测试代码。从下一节开始,我们将深入实践,学习如何为具体的功能编写和运行测试。
084:组织测试文件 📂
在本节课中,我们将学习如何在Rust项目中组织测试文件。我们将了解测试代码可以放置的不同位置,以及每种组织方式的适用场景。

概述
Rust项目中的测试代码可以放置在不同的位置。理解这些组织方式有助于我们编写更清晰、更易于维护的测试。本节将介绍两种主要的测试组织模式:在tests目录中创建独立的测试文件,以及在源代码文件中内联编写测试模块。
测试目录结构
在我们的项目中,虽然还没有编写实际的测试代码,但我们已经添加了几个不同的内容,例如我们一直在使用的dog测试。

在编写测试时,你可能会思考最佳策略是什么。Rust提供了几种不同的方式。
独立的测试目录
首先,我们来看一种常见的组织方式:使用独立的tests目录。
以下是项目结构的一个示例:
tests/
└── test_standard.rs
在test_standard.rs文件中,我们可以为其他文件添加一些测试。这是一种可行的测试组织方式。
为了更直观地展示实际结构,我喜欢使用tree命令,而不是仅仅在这里用文字描述。让我演示一下。如果我打开终端并执行ls命令,你会看到我有tests目录、src目录、target目录以及README文件和其他文件。
如果我执行tree tests命令,你会看到目前只有一个文件。
tree tests
随着项目不断扩展,你可以向tests目录中添加更多文件。那么,为什么要这样做呢?原因在于,任何位于tests目录中的内容,在构建项目或发布crate时,不会被包含在最终的产物中。因为tests是一个特殊的目录,Cargo能够识别它,并知道这些文件包含测试代码,从而在发布时排除它们。
源代码文件中的内联测试
然而,tests目录并不是唯一可以放置测试的地方。你可能会在其他地方看到测试,例如在源代码文件内部。
让我们看一个具体的文件,比如config.rs。我滚动到文件底部。以lib.rs文件为例,我滚动到最底部,你会看到里面有几个不同的函数。
我们在第43行附近有一个mod tests模块。实际上,在第3行我们也有一个#[cfg(test)]属性。这是一个属性(attribute),它告诉Rust(实际上是Cargo)这是一个测试模块,应该被自动发现并执行。
让我们回到lib.rs文件。这里有几个测试。那么,这背后的逻辑是什么呢?逻辑在于,有时你可能会在一个模块(比如这里的lib.rs)内部发现一个测试模块。这样做的原因可能是,我们需要访问模块内部某些非公开(private)的内容。
因此,在一个模块内部看到一个tests模块,并且里面有一些(甚至很多)测试,这并不奇怪。可以说,这是一个在模块内部编写测试的充分理由。
总结
本节课中,我们一起学习了Rust项目组织测试的两种主要方式。
- 一种是将测试代码放在项目根目录下的独立
tests目录中。这种方式适合集成测试或需要测试多个模块协作的场景,并且测试代码不会被打包到最终发布的crate中。 - 另一种是在源代码文件内部使用
#[cfg(test)]属性和mod tests模块来编写单元测试。这种方式特别适合测试模块内部的私有函数或逻辑。
通常,你可以根据测试的目标来选择组织方式:对于涉及多个模块的“集成测试”,使用tests目录;对于测试单个模块内部功能的“单元测试”,则内联在源代码中编写。随着项目的构建,你可以开始按照这些方式来组织你的测试。


085:Rust测试入门 🧪

在本节课中,我们将要学习如何在Rust中编写和运行最简单的测试。我们将从创建一个测试文件开始,了解测试的基本结构,并使用命令行工具来执行测试。
测试的基本结构
上一节我们介绍了测试的重要性,本节中我们来看看如何实际编写一个Rust测试。最简单的Rust测试非常直接明了。
我们有一个tests目录。它目前是空的。
让我们尝试在这里添加一个简单的文件。当我创建名为test_simple.rs的文件时,它里面没有任何内容。
为了编写测试,我们必须添加测试属性。这个属性是#[test]。该属性会告诉测试运行器:“请注意,我这里有一个需要测试的函数”。
让我们定义一个名为test_simple的函数。我们不做太复杂的事情。我们将在这里做一个简单的断言语句,让它断言true。这样就完成了。这是你能写出的最简单的测试。
运行测试
我们一直在借助VSCode和Rust Analyzer来运行这些测试。如果我们运行它,我们会得到test_simple的结果。
如果我们移除那个#[test]属性,测试会立即不被识别。但这不是其他系统运行测试的方式。
让我们看看如何在终端中操作。我将关闭资源管理器。我们会使用cargo,而cargo有cargo test命令。它有很多不同的选项,我们可以使用不同的标志来验证这些测试是否工作。我们必须运行cargo test。
进入这个目录的测试通常被称为集成测试,但你当然也可以在那里放置单元测试,这没有硬性规定。但需要理解的是,按照惯例,编写Rust的人通常称之为集成测试,尽管你完全可以100%在那里放置单元测试。
我们的做法是,当我运行cargo test --test并指定测试名称时,test_simple就会出现在那里。
如果我们把这个断言改为false,我们应该会得到一个失败的结果。我们运行它,就会得到一个失败。
测试组件详解
因为这是最简单的情况,我们实际上没有做任何太复杂的事情。我们只是做了一个简单的断言。让我们再次回顾一些组件。
我们有一个test_simple.rs文件。这是函数,这是文件,这是目录。我们有一个tests目录。Rust开发者可能称此为集成测试目录。人们会把集成测试放在这个目录里。这不是硬性规定。你实际上也可以在那里放置单元测试,但惯例是集成测试放在这里,而单元测试会放在你的库文件本身内部。我们稍后会看到这一点。
现在我们有了这个#[test]属性。如果我们移除它,我们就无法运行这个测试。定义测试的方法是声明一个函数,然后加上#[test]属性,接着写你想要进行的测试,最后做出你的断言。这是你代码的主体。你可以在里面做任何类型的设置。它不一定需要是公开的。我们实际上可以移除pub关键字。
切换回终端并再次运行,我们会得到失败,因为我们在断言false。让我们把它改成true并保存,再次运行,它就会通过所有测试。
断言与测试体
接下来是断言。这是关键所在。然后是我们将要断言的内容。
但这不一定必须是一个断言。你完全可以注释掉它,运行一个println!宏并输出“hello”,这样仍然可以工作。
如果我们运行那个测试,我们仍然会得到“ok”,并且会在终端打印出“hello”。
以下是测试的主要组成部分:
- 测试函数:一个用
#[test]属性标记的普通函数。 - 测试目录:通常名为
tests,用于存放集成测试文件。 - 断言宏:如
assert!(true),用于验证条件。 - 测试体:可以包含任何有效的Rust代码,用于设置测试环境和执行操作。
这些就是组件,这就是你如何向项目添加一些测试的方法。目前我们还没有将任何东西引入作用域。我们将在下一节看到。
总结

本节课中我们一起学习了Rust测试的基础。我们创建了一个简单的测试文件,了解了#[test]属性的作用,并使用cargo test命令来运行测试。我们看到了测试成功和失败的情况,并了解了测试的基本组成部分:测试函数、测试目录、断言和测试体。这是构建更复杂测试的坚实基础。
086:为代码编写测试 🧪

在本节课中,我们将学习如何为一个已有的Rust模块编写测试。我们将从一个没有测试的colors模块开始,逐步为其添加测试用例,以验证其功能的正确性。通过这个过程,你将掌握为现有代码库添加测试的基本流程和方法。
模块概览
我们有一个名为colors的模块,它包含一些之前未见过的额外功能。该模块提供了将字符串着色为红色、绿色、蓝色和粗体的函数。实现相当直接:我们传入一个字符串,得到一个着色的输出。
这些是ANSI转义码。其作用是,将来自参数s的字符串进行着色,使其在终端上显示为彩色输出。我们处理的就是这些功能,包括红色、绿色、蓝色、粗体,以及一个重置功能。
我们将重点关注reset函数来编写测试。原因是,reset允许我们传入一个先前已被着色的字符串(例如蓝色),它会移除转义码,使其没有任何格式。这是一个非常好的测试用例。
让我们看看这些函数是如何被使用的。这实际上也是你在处理代码时会遇到的情况。很可能你不会从头开始编写所有内容,而是会被抛入类似的问题中,例如,你可能需要查看像colors这样的模块,并被告知:“嘿,这些功能需要一些测试覆盖。目前还没有测试,你打算如何着手进行?”
代码结构分析
我们有一个名为Color的枚举,包含Red、Green、Blue和Bold。还有一个ColorString结构体,它是公开的,包含color(颜色)、original(原始字符串)和colorized(着色后的字符串)字段。原始字符串不会改变,增加的是着色后的输出。这样为想要同时使用两者的用户提供了更大的灵活性。
我们通过一个paint函数来扩展ColorString,该函数根据颜色进行特定的转换。你可以看到,colorized字段在颜色为Red时,会使用我们的red函数来更新。我们在这里做了很多事情,并且使用了match表达式,这很好。
最后,这里还有一个名为reset的方法,它基本上将字符串重置为仅使用我们之前看到的reset函数。
开始编写测试
那么,我们如何开始编写测试呢?首先,我们将创建一个测试目录。在该目录中,创建一个新文件,命名为test_colors.rs。
在这个test_colors.rs文件中,我们首先要导入或引入我们想要测试的内容,这些内容来自colors模块。因此,我们使用use crate::colors::ColorString;。这正我们想要测试的结构体。
确保lib.rs中正确定义了colors模块。如果注释掉模块定义,测试文件将会报错,因为无法找到模块。
当我们将其引入作用域后,首先要做的就是编写一个测试。我们使用#[test]属性来标记一个测试函数。我们将开始为ColorString编写测试。
编写第一个测试用例
这看起来还不错,但我们还不完全接受它。我们需要先弄清楚我们想要测试什么。
我想测试的是,如果我有一个ColorString,我这样定义它,然后调用paint方法,根据颜色进行验证。让我们从红色开始进行验证。
我们将测试命名为test_red_coloring。在这个测试中,我们创建ColorString结构体实例。我们使用Color::Red枚举值。然后我们调用paint方法,并使用assert_eq!宏进行断言,验证着色后的字符串是否符合预期。
让我们运行一下测试,看看会发生什么。
我们得到了第一个运行的测试,显示一切正常,验证通过。这看起来很不错。现在,我注意到一个实现细节看起来不太正确:colorized字段的初衷可能不是必须在构造函数中添加。也许为ColorString添加一个新的构造函数方法会更有帮助,以便为我们设置这些内容。但这目前是可行的。
我们所做的是:构造我们想要测试的对象,调用paint方法,然后进行断言。断言来自assert_eq!宏,它用于判断两个值是否相等。
验证测试失败情况
让我们故意制造一点错误,保存并再次运行测试,看看会发生什么。
我们得到了一个失败。如果我们滚动到顶部,会看到左侧和右侧的值。这里我移除了括号,所以那里缺失了,本质上导致了验证失败。
我将重新添加那个括号,保存并再次运行测试,测试就会通过。这基本上就是我们编写第一个测试的方式:我们确定测试用例,尝试弄清楚需要做什么,然后引入来自我们项目(库)中colors模块的相关内容(本例中是ColorString和Color枚举),接着构造对象,对其进行一些更改,最后对期望结果进行断言。
总结

本节课中,我们一起学习了如何为一个没有测试的普通Rust项目添加测试。我们通过使用#[test]属性来标记测试函数,并利用assert_eq!宏进行断言,成功地验证了代码功能。这个过程包括:识别测试用例、引入待测试模块、构造测试对象、执行操作,最后断言期望结果。这就是为现有Rust代码库添加测试的基本方法。
087:测试私有代码 🧪

概述
在本节课中,我们将学习如何在Rust中测试私有代码。我们将通过一个具体示例,演示如何重构函数以使其可测试,同时保持公共API的简洁性。你将学习到如何编写针对私有函数的测试,以及如何使用#[cfg(test)]属性来管理测试代码。
私有代码测试的挑战
上一节我们介绍了基本的测试方法,本节中我们来看看如何测试私有函数。我们面临的一个尚未解决的问题是如何测试私有代码。
我们这里有一个名为read_standard_in的函数。这个函数目前存在一个问题:它不接受任何参数,并在第24行直接定义了标准输入。随后,它使用standard_in.lock()来创建可变的reader变量。对于这个函数本身来说,这没有问题,但如果我们无法访问standard_in.lock(),该如何测试它呢?
让我进一步解释我们在这里尝试做什么。我正在思考如何向这个read_standard_in函数注入输入,以便我们能够为这个函数的工作创造条件。但我无法做到这一点,因为它不接受参数,并且直接在那里调用standard_in。
重构函数以支持测试
我将采取的方法是将其拆分为两个函数。解决这个问题肯定有多种不同的方法,但我将采用以下方式。
首先,我将提取所有相关代码,然后创建一个单独的私有函数。我将给它加上下划线前缀,命名为_read_standard_in。
这个函数将使用泛型。我将使其类型为R。我知道我们还没有详细学习泛型,但我本质上是在说这个函数将接受一个reader参数。只要它实现了BufRead特性,就可以正常工作。
我们将声明它为可变类型。然后它将返回一个String。
我们将创建一个可变的line变量,然后简单地返回reader的read_line结果,接着使用expect处理。然而,我们还需要像之前一样返回line.trim().to_string(),因为这是我们想要返回的内容。
很好,我们已经将这个函数拆分开来。现在在第25行,我将调用_read_standard_in并传入可变reader。
我们做了什么?我们从read_standard_in中提取了这部分代码。现在read_standard_in将调用_read_standard_in。
我们为什么要这样做?原因是我们现在能够向这个函数传递参数。这个reader参数可以是任何类型,只要它实现了上述的读取属性和函数方法。这将使我们能够进行测试。
现在我可以构造这个参数,完全没有问题。这实际上会起作用。这也意味着我不一定能够测试read_standard_in本身,但这没关系,因为定义这两个变量是可以接受的。我可以接受这部分不被测试。
但我试图测试的最重要部分就在这里。我想测试的是,当有人想要使用我的API时,他们将执行这个操作,而不一定关注这个函数。这允许我传递某些内容作为参数,并构造内容,使测试更容易,因为它接受reader参数。
测试私有函数的方法
那么这意味着什么?如何测试私有代码?请注意,我没有添加pub关键字。我没有给这个函数加上pub前缀。这个函数是pub,而这个不是。这个函数能够使用_read_standard_in,因为它们在同一个模块中,没有问题。
那么我们如何测试这个私有代码呢?我们不能回到我们的测试中,因为这不是公开可用的。我们无法访问它。
如果我将其改为read_standard_in,它将无法工作。如果我保存并运行cargo test,它会说这是私有函数,我无法访问它。所以这不会起作用。
让我恢复它并使其正常工作。实际上,让我把它放回去。如果我运行cargo test以确保一切正常,是的,它正常工作。
编写私有函数测试
现在我将编写测试。如何为私有函数编写测试?
首先,我将定义#[cfg(test)]。我稍后会告诉你这是什么。我将声明mod tests。
以下是创建测试模块的步骤:
- 创建一个名为
tests的模块 - 使用
use super::*将当前模块的所有内容引入作用域 - 显式地引入需要测试的函数
- 使用
Cursor来创建测试输入
我将尝试在这里显式一些,我将说use super::read_standard_in。我可以访问它,因为我在同一个模块中。所以这将起作用。
接下来,我将使用Cursor,因为它允许我为我们的函数创建那个参数,也就是这里的这个reader。我将使用引入作用域的Cursor来创建它。
接下来,我将开始创建一个测试。我将在这里编写一个测试。
使用那个属性,我将说function test_read_standard_in。然后我将打开花括号,然后我将说可变reader将是Cursor::new("test\n")。
然后我可以尝试运行那个测试。我在这里创建的这个test_read_standard_in看起来对我来说是正确的。然后我将通过使用_read_standard_in来测试,这是一个来自那里的私有函数。那个reader实际上是一个实现了BufRead的Cursor,所以它应该实际工作。
现在如果我们运行它,我们得到了一个OK。让我们快速看一下这里的断言。我们说来自_read_standard_in的line实际上来自第44行的这个。我们说,嘿,即使我传递了一个额外的换行符,我应该实际得到的是"test",没有换行符。这是因为trim的作用,我能够将其与字符串进行比较。
添加更多测试用例
这就是你如何测试私有函数的方法。你可以继续添加更多测试。
例如,我们可以说function test_read_standard_in_empty,如果我们想说,如果我的Cursor完全为空怎么办?那将是空的。现在我们缺少属性,我注意到因为我得到了花括号下划线,所以你可以看到它从未被使用。让我继续这样做。
然后如果我运行那个测试,我们将得到一个OK。现在如果我打开终端,我可以做cargo test --lib。如果我这样做,你将看到test_read_standard_in_empty和test_read_standard_in都将工作。
为什么我要做--lib?因为如果我仅仅做cargo tests,你将看到那里有很多输出,包括我的cargo test。它运行所有内容,包括测试,但--lib与我们之前看到的不同之处在于,它将忽略所有的cargo test,并且只包括定义在我创建的库、包、crate中的代码中的测试。
理解#[cfg(test)]属性
我们需要看到的最后一件事是为什么我添加了#[cfg(test)]。它接受一个参数,在这种情况下是test。这是一种方式,以便我们可以告诉cargo,顺便说一下,当你构建我的库并执行所有代码时,不要包含这段代码,因为这段代码仅用于测试。除非我明确要求包含测试代码,否则我不希望包含它。这是一种轻松、优雅地排除代码的方式,使其不会编译到将要发布的版本中,这样它就不一定是库、包的一部分,无论我将要在crate中发布什么。
总结

在本节课中,我们一起学习了如何测试Rust中的私有代码。我们进行了一些"手术",将read_standard_in拆分为另一个函数,使我能够很好地测试它,因为我能够传入reader。这样我的外部API看起来非常漂亮和干净,只是一个简单的调用,不需要用户构造那个standard_in。我们能够在这里进行一些实际的测试,因为我们能够使用未公开的私有函数,通过使用一些测试代码,在这种情况下,注意我没有使用外部测试目录,其中包含另一个测试模块在测试目录内。
088:使用测试失败信息 📝

在本节课中,我们将学习如何在Rust测试中为失败情况添加上下文信息。当测试失败时,清晰的错误信息能帮助我们更快地定位问题。
为测试失败添加上下文
上一节我们介绍了基本的测试断言。本节中我们来看看如何为断言失败提供更详细的说明信息。
在某些情况下,你可能希望为测试可能出现的失败添加更多上下文信息。例如,在我们创建测试读取标准输入并处理换行符的场景中,我们可以通过扩展 assert_eq! 宏来实现这一点。这个方法同样适用于 assert_ne! 宏。
以下是具体操作方法:
assert_ne!(line, "test");
这行代码会失败,因为 line 实际上等于 "test"。运行测试时,我们会看到失败信息。如果向上滚动查看失败详情,可以看到“左边不等于右边”的提示,这是因为断言失败了。
添加自定义失败信息
让我们假设在某些情况下我们预期会出现失败。如果测试失败,我们希望提供更多上下文信息。实现方法是在断言宏后添加一个逗号,然后传入额外的参数。
以下是具体操作示例:
assert_eq!(line, "test", "line should be 'test' but it wasn't");
这两个参数是必需的。如果我们运行这个测试并且它失败了(例如,将 "test" 改为 "des"),我们将在失败信息中看到我们添加的自定义消息:“line should be 'test' but it wasn't”。
当你希望为测试失败提供更多上下文信息时,这无疑是正确的方法。如果测试名称不够清晰,或者测试逻辑变得相当复杂,又或者在一个测试中有多个断言语句,这实际上是最佳实践。
测试命名建议
我有几个建议:尽量使测试函数名称有意义。如果名称有用,那么失败信息应该相当自解释。例如,test_read_standard_in 这个名称不够好。
让我们改进一下:
fn test_read_standard_in_empty_with_new_line() {
// 测试逻辑
}
这样我就能更好地组织我的预期:标准输入为空且带有换行符。这听起来更好,也让我能够更清晰地编写测试。这个测试应该能正常工作。
使用注意事项
这些技巧非常有用,但不要过度使用。如果你发现添加这些信息很繁琐,而收益不大(就像这个简单示例一样),那么可能不值得这样做。特别是当你的测试开始变得更加复杂,或者断言变得更加复杂时,需要仔细考虑。
在这个例子中,我们使用 assert_eq!,这相当直接。这只是三行代码,我们的预期相当清晰。

本节课中我们一起学习了如何在Rust测试中添加失败信息。当你需要更多上下文来解释测试失败的原因时,这是非常有用的技巧。记住要保持测试名称有意义,并根据需要适度使用自定义失败信息。
089:测试总结 🧪

在本节课中,我们将对Rust中的测试进行总结,梳理测试公共与私有代码的关键点,并理解测试在确保程序逻辑正确性方面的重要性。
测试公共与私有代码
上一节我们介绍了具体的测试编写方法,本节中我们来看看测试的适用范围。在Rust中进行测试时,如果你不了解公共(public)和私有(private)代码的区别,可能会感到有些复杂。从之前的例子中,现在应该很清楚如何在编程语言中处理这些约束了。无论是测试不可访问的私有内容,还是测试可公开访问的内容,我们都看到了它们之间的一些差异。
理解这一点非常有用,因为在你构建Rust程序时,你肯定希望对它们进行测试,以确保一切按预期运行。
测试的必要性
现在有些人可能会说,既然编译器能确保我的程序在没有任何错误的情况下构建,那我为什么还需要测试呢?这种说法在一点上是完全正确的:一个程序确实可以在没有任何编译器错误或警告的情况下构建成功。但这并不意味着程序内部的逻辑处理方式符合你的预期。
当程序规模增长时,你会希望进行越来越多的测试,并添加更多的验证来确保所有功能都正确运行。
总结与行动指南
因此,你现在应该已经掌握了有效测试Rust程序所需的一切知识。请尽可能提高测试覆盖率,以确保程序逻辑完全符合你的期望。
本节课中我们一起学习了:Rust测试中公共与私有代码的测试方法,理解了即使编译成功程序逻辑也可能存在错误,从而认识到全面测试对于保证程序质量至关重要。
090:课程总结
在本节课中,我们将对Rust编程语言的基础知识进行总结,回顾所学内容,并展望未来的学习方向。
我们已经完成了Rust编程语言的基础知识学习,现在你可以独立编写有效的Rust代码了。你掌握了所有入门所需的知识。当然,Rust编程语言本身更为庞大和深入,还有很多细节可以进一步探讨,但本课程涵盖的内容足以让你顺利起步。
🚀 核心能力回顾
以下是你在本课程中获得的核心能力:
- 项目构建:你可以开始构建一个库,或者着手开发一个命令行工具。
- 逻辑管理:你理解了如何管理程序逻辑。
- 调试技巧:你掌握了如何进行代码调试。
- 环境配置:你学会了如何设置开发环境。
- 项目扩展:你了解了如何通过更多模块来扩展项目,并控制其可见性。
- 测试验证:你甚至可以为代码添加测试,包括文档测试,以验证你编写的文档是否正确工作。
💡 Rust语言的优势

Rust无疑是一门令人兴奋的语言。

它为你带来了以下优势:
- 高性能:
let performance = "great"; - 易于分发:你可以创建可轻松分发到多种不同系统的二进制文件。
- 应用广泛:它拥有许多不同的用例,例如系统编程和命令行工具开发。
- 内存占用低:其内存使用占用非常低。
当你想要更进一步,将项目部署到云端(本课程未涵盖此部分)时,你将能够以最小的资源占用完成部署,这将转化为更低的成本和更好的性能。这无疑非常令人兴奋。
🎯 总结与展望
本节课中,我们一起回顾了Rust编程语言的基础学习旅程。希望你和我一样对这门编程语言感到兴奋。随着你运用本课程中学到的一些概念,你将能够编写出越来越多高效的Rust代码。

浙公网安备 33010602011771号