为了账号安全,请及时绑定邮箱和手机立即绑定

用 Rust 编写我的第一个命令行应用

标签:
杂七杂八
我的 Rust(一种编程语言)之旅

Rust 正变得非常流行,这也不难解释。作为一种比 C++ 更快和更安全的替代语言,Rust 为开发者提供了性能和稳定性的完美结合。由于其对内存安全和零成本抽象的高度重视,Rust 已成为希望构建高效和高性能应用同时保持代码稳定的开发者的首选语言。

最近,我决定深入学习 Rust,很快就发现它有多强大和多灵活。Rust 不只是一个普通的编程语言,它是一个实用的工具,很好地平衡了性能、安全性与现代开发实践。

为了测试我新发现的兴趣,我决定从头开始构建一个命令行工具。这种方法让我得以探索Rust的各种特性,比如它的强类型系统、所有权模式和丰富的生态系统。

在这篇文章中,我将分享我对Rust的初体验——我学到了什么,何时适合使用Rust,以及它与其他语言相比有何特别之处。我还将带您了解我构建我的CLI任务管理器的过程,突出Rust的独特特性,这些特性让开发过程既充满挑战又极具满意度。沿途,我还将把Rust与其他我熟悉的语言进行比较,提供对其优点、局限性及其亮点的见解。无论您是打算学习Rust,还是对其实际应用感兴趣,我希望这篇文章能帮助您更清晰地了解Rust的潜力,以及它为何值得一试。

介绍

Rust 的旅程始于 2006 年,当时 Mozilla 员工 Graydon Hoare 作为个人项目开始开发这门语言。他的目标是开发一种高性能且内存安全的编程语言,旨在解决他在使用像 C 和 C++ 这样的现有系统语言时遇到的问题。Mozilla 认识到该项目的潜力,并于 2009 年正式开始支持其开发。

在2010年,Rust 首次公开亮相于公众,当时 Mozilla 宣布了这门语言。它旨在创建一个比 C++ 更安全的替代方案,同时保持对硬件的底层控制。Rust 的开发得到了开源社区的贡献和支持,并在2015年达到了第一个稳定版本(Rust 1.0)。这一里程碑标志着 Rust 成为生产环境适用的,并为它的快速增长奠定了基础,开启了迅速发展的道路。

Rust的主要目的

Rust 是为了应对软件开发中的三个主要挑战而设计的:

  1. 无需垃圾回收的内存安全
    许多编程错误源于不安全的内存管理,比如空指针解引用和缓冲区溢出。Rust的所有权模型实施严格的编译时检查,确保内存安全,无需依赖垃圾收集器。
  2. 高性能
    像 C 和 C++ 一样,Rust 允许开发人员编写接近硬件运行的代码,使其非常适合性能关键的应用程序。它的零成本的抽象确保高级代码不会带来额外的运行时开销。
  3. 并发与并行
    Rust 的所有权和类型系统可以防止诸如数据竞争等常见问题,使其成为构建并发和并行系统更安全的选择。其独特的保证使得编写多线程应用程序变得更加容易,无需担心细微错误。
Rust 为何独特

Rust常被选为各种领域的首选编程语言:

  • 系统编程: 涉及操作系统、浏览器引擎和嵌入式设备。
  • WebAssembly: Rust 的性能和安全性非常适合 WebAssembly 项目。
  • 命令行工具: 轻量且快速的命令行工具,具有强大的错误处理功能。
  • 网络: 高性能的服务器和应用程序,在金融和游戏等行业的应用。

接下来,我将把 Rust 与我已经熟悉的语言进行比较:C#JavaScriptGo 这些语言。让我们来看看这些语言怎么比吧!

关于 Rust、Go、C Sharp 和 JavaScript 的比较
1. 性能
  • Rust :接近 C++ 的性能,具备零成本抽象概念和无垃圾回收的特性。
  • Go :适合服务器端任务,但依赖垃圾回收,会导致偶尔的暂停和额外的开销。
  • C# :也采用垃圾回收机制,适合开发高级应用程序,但不适合系统级编程或实时应用场合。
  • JavaScript :运行在托管环境中(如 V8 引擎、Node.js)。适合网页应用开发领域,但不适合处理高性能任务。

Winner:Rust:凭借其出色的性能和系统级控制。

2. 内存安全
  • Rust :通过其所有权和借用模型提供编译时的内存安全保证,消除了空指针解引用和数据竞争等问题。
  • Go :垃圾回收防止了一些内存问题,但并未像 Rust 那样严格保证安全性。
  • C# :托管内存和垃圾回收减少了风险的可能性,但仍允许空引用,这是常见的错误类型。
  • JavaScript :自动内存管理(垃圾回收)确保了较高的安全级别,但不能提供更深层次的控制。

赢家:Rust,因为它拥有卓越的内存安全而不增加运行时成本。

3. 并发性
  • Rust:安全且显式的并发模型,包含编译时检查以防止数据竞争的发生。虽然需要精心规划,但能够确保程序的可靠性。
  • Go:并发是核心特性,通过轻量级的goroutineschannels使编写并发代码变得容易。但是它不会在编译时强制线程安全,这意味着开发者需要自行确保代码的线程安全性。
  • C#:提供async/await进行高级异步编程,但线程安全依赖于同步机制的谨慎使用。
  • JavaScript:通过async/await和事件循环机制实现事件驱动的并发模型。非常适合I/O密集型任务,但不适合进行多线程操作。

胜者:Go 因其简洁易用的并发特性方面而胜出,但 Rust 提供了更安全、更可靠的并发性,更适合高级场景。

4. 生态系统和工具
  • Rust:Cargo 提供了依赖管理和测试的流畅体验,但生态系统仍在发展中。
  • Go:丰富的标准库,优秀的工具(例如 go fmtgo build),以及成熟的生态系统,专注于服务器端和云原生开发。
  • C#:依托微软的 .NET 生态系统,拥有广泛的库和强大的 IDE,如 Visual Studio,适用于企业级开发。
  • JavaScript:由于 npm 和其在 web 开发中的主导地位,JavaScript 拥有最庞大的库和框架生态系统。

这取决于情况:这取决于使用场景。Go 和 C# 这两个语言在后端开发方面拥有强大的生态系统,JavaScript 在 web 开发领域占据主导地位,而 Rust 则比较稳定,仍在发展中。

5 学习进度
  • Rust:由于所有权和借用模型导致学习曲线较陡,但结果更安全、更可靠的代码。
  • Go:极简的语法和设计使其非常适合初学者,优先考虑简单性而非复杂性。
  • C#:对于初学者来说相对容易,具有强大的工具支持和抽象,但其特性和运行时行为可能给新手带来困扰。
  • JavaScript:学习基础知识相对容易,但由于语言特性和生态系统中的诸多特性,精通起来会非常困难且容易遇到各种问题。

Winner : Go,由于它的简单和易用。

总的来说:
  • Rust:最适合性能、内存安全以及系统级编程。适合需要控制和可靠的开发者。
  • Go:最适合简单快速开发服务器端和云原生应用。适合注重生产力的团队。
  • C#:最适合企业解决方案、桌面应用和.NET生态系统中的游戏开发。工具强大,但在其领域外的灵活性较差。
  • JavaScript:最适合 web 开发和前端编程。

每种语言都有其特色,但Rust因其极高的安全性和性能而特别突出,Go则在友好的并发性方面表现出色。C#在企业级和游戏开发中主导地位,而JavaScript则是网页和跨平台解决方案的首选语言。

接下来,让我一步步展示我是如何构建一个简单的任务管理命令行工具的。

基于命令行的任务管理器
我们来看看目标和计划

想达成的事
  • 添加新任务
    允许用户创建包含描述、优先级和截止日期的新任务。
  • 列出任务
    以组织化且用户友好的方式展示所有任务,并提供按优先级、状态或截止日期过滤的选项。
  • 删除任务
    让用户删除不再需要的任务。
  • 持久化数据
    将任务保存到持久存储中,确保任务列表在不同会话间保持一致。
  • 命令行界面
    创建一个直观的CLI,提供清晰的命令和选项,方便用户使用。
  • 错误处理
    实现强大的错误处理机制,优雅地处理或忽略无效输入或其他意外行为。
  • 学习Rust概念
    利用项目练习Rust的所有权模型、结构类型、错误处理和模块化编程。
设计:

一开始,我就决定利用第三方库来处理命令行、异步任务和数据库连接,而不是自己重新实现这些功能。为了数据存储,我选择使用MySQL数据库来存储数据,而不是使用文件存储,以便练习用Rust操作数据库,从而让项目更加复杂和贴近实际。

这是我的第三方库列表:

  • clap: 一个强大的库,用于解析命令行参数并构建用户友好的 CLI 应用程序。使用 clap,我很容易定义了任务管理器的命令、子命令及其选项。
  • tokio: 一个为 Rust 设计的异步运行环境。我使用 tokio 来管理数据库交互。
  • sqlx: 一个轻量级且异步的 ORM(对象关系映射器)用于与 MySQL 数据库进行交互。
  • thiserror: 用于创建自定义错误类型的库,减少了样板代码的编写。

我们先来看看程序入口点的定义:

    use clap::{Arg, Command};  
    use commands::{add, error::AppError, list, remove};  

    mod commands;  
    mod models;  

    #[tokio::main]  
    async fn main() -> Result<(), Box<dyn std::error::Error>> {  
        let matches = Command::new("任务管理器")  
            .version("1.0")  
            .author("你的名字 <youremail@example.com>")  
            .about("通过命令行管理你的任务")  
            .subcommand(  
                Command::new("添加")  
                    .about("添加任务")  
                    .arg(Arg::new("标题").required(true))  
                    .arg(Arg::new("描述").required(false)),  
            )  
            .subcommand(  
                Command::new("移除")  
                    .about("通过索引移除任务")  
                    .arg(Arg::new("索引").required(true)),  
            )  
            .subcommand(Command::new("列出").about("列出所有任务"))  
            .get_matches();  

        if let Some(matches) = matches.subcommand_matches("添加") {  
            let 标题 = matches  
                .get_one::<String>("标题")  
                .ok_or_else(|| AppError::ValidationError("标题不能为空".to_string()))?;  

            let 描述 = matches.get_one::<String>("描述").map(|d| d.as_ref());  

            add(标题, 描述).await?;  
        } else if let Some(matches) = matches.subcommand_matches("移除") {  
            let 索引 = matches  
                .get_one::<String>("索引")  
                .and_then(|i| i.parse::<i64>().ok())  
                .ok_or_else(|| AppError::ValidationError("索引无效".to_string()))?;  

            remove(&索引).await?;  
        } else if let Some(_) = matches.subcommand_matches("列出") {  
            list().await?;  
        }  

        Ok(())  
    }

一行行来看吧!

导入库和本地模块:

使用 clap 包中的 Arg 和 Command 模块;
使用 commands 包中的 add、error 下的 AppError、list 和 remove 函数。

关于内部组件的更多细节我们会稍后提供.

    命令模块;  
    模型模块;

主要功能如下:

    #[tokio::main]  
    // 使用Tokio的main宏启动异步主函数
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
  • **#[tokio::main]**: 一个过程宏标记,用于标记使用 tokio 运行时的异步应用程序的入口点。

  • **async fn main**: main 函数是异步的,可以使用 await 关键字来进行异步操作。

  • **Result <(), Box<dyn std::error::Error>>**: 该函数返回一个 Result。如果成功,返回 Ok(())。如果出错,则返回一个包含实现了 std::error::Error 特性的任何类型的 Box

这里定义了主要命令及其子命令:

    let matches = Command::new("任务管理器")  
            .version("1.0")  
            .author("你的名字 <youremail@example.com>")  
            .about("从命令行管理你的任务")  
            .subcommand(  
                Command::new("添加")  
                    .about("添加新任务")  
                    .arg(Arg::new("标题").required(true))  
                    .arg(Arg::new("描述").required(false)),  
            )  
            .subcommand(  
                Command::new("移除")  
                    .about("通过索引来移除任务")  
                    .arg(Arg::new("索引").required(true)),  
            )  
            .subcommand(Command::new("列出").about("列出所有任务"))  
            .get_matches();

指令匹配:

    if let Some(matches) = matches.subcommand_matches("add") {
        // 如果子命令是"add",则匹配
    }

从 matches 对象中,获取 **title**

    let title = matches  
        .get_one::<String>("title")  
        .ok_or_else(|| AppError::ValidationError("请输入标题".to_string()))?;

获取可选的**描述**参数,如果有提供的话就将其转换为&str格式。

从 `matches` 中获取键为 `description` 的字符串值,并将其转换为字符串切片。

调用 add 函数(可能在 commands 模块中定义),传入提供的标题和描述信息。await 关键字用于等待异步操作完成:

等待添加标题和描述,然后继续执行

如果没有任何错误发生,返回 Ok(())

    Ok(())

// 成功 (成功表示操作成功完成,但没有返回任何值)

我们现在来看一下 **add** 函数中的代码。

    use crate::commands::{conn::connect, error::AppError};  

    pub async fn add(title: &str, description: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {  
        // 确保标题长度在1到255个字符之间
        if title.is_empty() || title.len() > 255 {  
            return Err(Box::new(AppError::ValidationError("标题必须长度在1到255个字符之间".to_string())));  
        }  

        let pool = connect().await?;  

        sqlx::query("INSERT INTO tasks (title, description) VALUES (?, ?)")  
            .bind(title)  
            .bind(description)  
            .execute(&pool)  
            .await?;  

        println!("任务 '{}' 已成功添加!", title);  

        Ok(())  
    }

该方法 connect 用于链接到 MySQL 数据库。

use sqlx::{Error, MySql, MySqlPool, Pool};  

// 这个异步函数用于创建数据库连接。
pub async fn connect() -> Result<Pool<MySql>, Error> {  
    // 使用提供的连接字符串创建MySQL数据库连接。
    MySqlPool::connect("mysql://root:root@localhost:3306/task_manager").await  
}

我设定了几个自定义错误信息:

    #[derive(Debug, thiserror::Error)]  
    pub enum AppError {  
        #[error("未找到任务")]  
        NotFound,  

        #[error("验证出错: {0}")]  
        ValidationError(String),  
    }

特别是 **任务模型**

    use std::fmt::Display;  

    #[derive(Debug, sqlx::FromRow)]  
    pub struct Task {  
        pub id: i64,  
        pub title: String,  
        pub description: Option<String>,  
        pub done: bool,  
    }  

    impl Display for Task {  
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {  
            let status = if self.done { "完成" } else { "未完" };  
            let description = self.description.as_deref().unwrap_or("无描述");  
            write!(f, "{}. {}: {} ({}).", self.id, self.title, description, status)  
        }  
    }

要编译并运行程序,你需要安装 Rust 工具链环境。最简单的方法是按照 https://rustup.rs/ 上的简单说明操作。Rustup 是推荐用来安装和管理 Rust 版本的工具。

安装后,您可以运行,如下命令:

    cargo run help

你应该看看接下来的输出。

    从命令行管理任务  

    用法:task-manager [COMMAND]  

    命令:  
      add     添加新任务  
      remove  删除任务(通过索引)  
      list    列出任务  
      help    显示帮助信息或特定子命令的帮助  

    选项:  
      -h, --help     显示帮助  
      -V, --version  显示版本

在项目根目录下执行以下命令(请参见下方项目链接),确保MySQL集群已经启动并运行,请在执行任何CRUD操作之前完成这一步骤。

docker compose up -d  (启动并分离运行)

最后,你可以试试创建一些简单的任务,比如。

添加任务:研究新的Rust库,探索适合Rust的高级日志记录库,并评估哪个更符合项目需求。

接下来你应该看到的是:

添加了任务 '研究新的 Rust 库'!

你可以通过运行命令 cargo run list 来查看所有任务的完整列表。

    任务:  

1. 研究新 Rust 库:探索适合 Rust 的高级日志库,并评估一下哪个库符合项目的需要,(未完成)
应用代码

你可以在这里查看该项目的完整代码哦。

GitHub - misikdmytro/task-manager-rust在 GitHub 上注册并参与 misikdmytro/task-manager-rust 的开发.

如果觉得有用,别忘了给仓库点个赞!你的支持真的很重要!🌟

结论

构建这个任务管理命令行界面是一段令人兴奋且充满成就感的 Rust 学习之旅。虽然我自认还不能算是 Rust 方面的专家,但这个项目让我对 Rust 的几个核心概念,比如所有权、错误处理和异步操作有了扎实的理解。

在继续学习Rust的过程中,我下一个要面对的挑战是开发一个网络服务。我完全清楚,使用Rust开发网络服务不会像使用Go、C#或JavaScript那样直接或高效(在开发速度上),这些语言有着成熟的生态系统和专门针对网络开发设计的工具。但是,我用Rust构建的网络服务将在这个关键领域表现优异:性能。

从我的角度来看,Rust 不仅仅是一门语言,它更是一种思维方式的转变。尽管 Rust 在某些情况下可能无法取代更成熟的语言,但它的优势使其成为高性能和可扩展应用的理想选择。

对于任何考虑学习 Rust 的人,我的建议很简单:从简单开始,勇敢面对挑战,并享受过程。这门语言可能需要你多花一点时间去适应,但你将学到的技能将彻底改变你对编程的看法。我期待未来能更多地分享有关 Rust 的知识和项目。下次再聊!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消