Rust 的面向对象编程————项目管理

20 minute read Published: 2019-06-16
介绍 Rust 项目组织里面的概念

本文翻译自博文 Rust for OOP - Project Management

所有权利归原作者所有。

我们开始我们的新系列——「Rust 的面向对象编程」。第一个任务是开始一个新的项目。当然,我需要工具去扩展它,我们将会看到 Rust 提供给我们的这些组件和概念。工作空间、库、可执行文件、模块、Crate、隐私边界(privacy boundary)、路径和外部 Crate 都会在这篇文章里面。

请确认已经在这里 读过这个系列的介绍文章。

重要提示:这篇文章已经更新到 Rust 2018,你需要 Rust 1.32 或更高的版本。

项目管理

让我们从这个事实开始。我是一个 Rust 初学者。我既没有机会创建一个大的 Rust 项目,也没有获得很多关于项目布局的知识。然而,这不会阻止我快速建立第一个实质性项目。我知道不同语言之间代码管理方式差异很大。有些做得比较好(Java),有些不怎么样(C++)。取决于你的背景,你对 Rust 的欣赏程度可能不同。不像其它语言,Rust 提供了一个杰出的项目管理方式,而没有引入昂贵的运行时环境。使用 Rust,你可以很轻松的构建一个跨平台的应用,编译到不同目标环境也非常简单,集成第三方库也是愉快的,构建你的项目也不需要精通一整个新的与平台无关的独立框架(makefile,说的就是你)。再次强调,所有的这些都不需要引入昂贵的运行时环境,或者像 Java 那样的虚拟机。不像 C++ 这个可以作为比较的系统语言,Rust 在项目管理方面更喜欢遵从惯例而不是配置。遵守 Rust 管理,一切都会奏效。我相信这个明智的决定是使 Rust 与众不同的一个重要原因。

Crate——项目

Rust 项目布局的基础部分是简单的,和其它语言差不多。有你项目的工件,基本的工件是可执行二进制文件和库。当你想生成一个可运行应用的时候就使用二进制。对于可重复使用的代码,选择库。Rust 在这里没有突出的地方。在我的项目里面,我更喜欢把几乎所有东西都放到库里面,因为人们永远不知道他何时会重用一段代码。通常我只是让我的可执行文件是库的一个简单包装。Rust 对一个库或者可执行文件有统一的名字————crate。crate 要么是一个可执行文件,要么是一个库。创建一个库或者二进制 crate 非常简单:

包(Package)

下一个概念是。 包是一个至少包含一个 crate 的结构。包最多可以包含一个库 crate 和任意多个二进制 crate。 其中一个 crate 是主 crate。按照惯例,主 crate 的名称与包的名称相同。代码的入口点,对于库 crate 是 src/lib.rs,对于二进制 crate 是 src/main.rs。默认情况下,您将其余的二进制 crate 放到 src/bin 下面。每个包都有一个名为「cargo.toml」的特殊文件,它描述这个包,包含包版本、名称和依赖项等信息。 它还确定了包的根。到目前为止我提到的所有路径都是这个包根的相对路径。如果你想覆盖掉在前面讨论过的一些默认值(“主” crate 的入口点和辅助二进制文件的位置),你都可以在 cargo.toml 文件中完成。

cargo.toml

cargo.toml 的格式有详细的记录,并且十分容易理解。你能在这里 找到关于怎么去写它的完整文档。对于我来说,最简单的方式去理解它就是以下面这样的文件作为基本示例:

# This is a comment
# Section in the file are marked with [].
# Variable has the format: variable_name = "value"

# Information about the package and its name
[package]
name = "tlv_message"
version = "0.1.0"
authors = ["oribenshir <oribenshir@gmail.com>"]
edition = "2018"

# This is an optional tag for a library crate, by default the name will be the same as the package, and the path is src/lib.rs
# You can comment the entire [lib] section and everything will work
[lib]
path = "src/lib.rs" # Optional as this is the default path
name = "tlv_message" # Optional as this is the default name

# Extra binary crate
[[bin]] # A binary section. Note we use two brackets, as we might have multiple binaries
name = "tester"
# path = src/bin/tester.rs # This is the default path, we don't have to specify it

# Another binary
[[bin]]
name = "example"
path = "example/example.rs" # I want a non-default path

# This is where we state our dependencies, and their version
[dependencies]
byteorder = "1.3.1"

关于包这个概念的一个警告就是,Rust 中包的存在感非常薄弱和令人困惑。通常,人们不会区分 crate 和包。例如,我不确定,可以在辅助二进制文件中引入一个完整的 crate,其中包含了 crate 的所有含义(我们将在后面看到这个含义)。 此外,尽管它有一些问题,你也可以在同一个包中同时拥有默认的库 crate 和二进制 crate(例如,src/lib.rs 和 src/main.rs 在同一个 crate 中)。

工作空间

到目前为止,我们已经开始使用 crate,我们将它们包装到包里面,现在我们将来到更高一层————工作空间。管理一个项目时,我们经常需要在项目中使用多个库。正如我们刚刚讨论的那样,每个包只能包含一个库。我们的解决方案是将所有包打包到一个工作空间里面。因此工作空间只是多个包的容器。与包一样,工作空间的根目录也包含一个 cargo.toml 文件,该文件描述了工作空间本身。在使用 IDE 或管理复杂项目时,工作空间很有用,项目是从各种包上面构建的,而每个包都有自己的发布周期。

代码管理

我们已经涵盖了所有「高级别」的项目管理。从 crate 到包到工作空间。这些为项目提供了结构,并且有助于组织和描述项目工件。但我们还没有完成。 管理一个项目时,我们还需要管理代码。我们需要一种组织和描述代码本身的方法。与第一部分一样,我们将从 crate 开始,但这次我们将进入到更下层。

Crate——代码

我们从项目的角度讨论了 crate。现在让我们从代码的角度来介绍它。每个 crate 都有一个主文件:库是在 src/lib.rs,二进制是在 src/main.rs。这个文件是 crate 的入口点,对于二进制 crate,这是你的 main 函数所在的位置。你可以通过 crate 名称来引用 Rust 中的每个项(函数、结构等……)。例如,如果你有一个名为 connect 的函数。你可以通过这种方式在 crate 内访问它:crate :: connect。如果这个函数是从外部 crate 中导入的,比如 afternoon_rusting,你可以这样访问它:afternoon_rusting :: connect。通过官方 crate 仓库 crates.io 发布的每个 crate 都有一个唯一的名称。当然,这已经引起了一些问题。但是它确实保证只要你使用已发布的 crate,代码中每个项的名称就是全局唯一的。crate 也可以作为函数和结构提的「可见性屏障」,稍后将对此进行更多介绍。

模块

到目前为止,我们看到的库 crate 或者二进制 crate 都是只包含一个文件(lib.rs/main.rs)。但是通常来说,我们想把我们的库和二进制按照逻辑单元把它们分开单独存放。因此,我们使用模块来将库或者二进制分成多个逻辑组件。每个逻辑组件可以在不同文件里(尽管它不是必须要这样)。我们将会讨论模块的三个方面:

后两个是更一般的概念,也和 crate 有关,但是如果不理解模块系统,我也不能完全将它解释清楚。如果你有时间,我推荐你阅读 Rust book 中的这个章节来有一个全景,否则的话,呆在这里看一个压缩紧凑的版本。

模块的语法很简单。在你的 lib.rs/main.rs 里面,把你想单独放到一个模块的代码用 mod 关键字包起来就可以。模块也可以向下面这样嵌套的定义:

mod my_module {
    mod nested_module {
        fn my_function() {

        }
    }
}

有时我们想把我们的模块单独放到一整个文件里面。为了能这样做,在我们的 lib.rs/main.rs 里面声明模块就可以。像下面这样使用 mod 关键字但不用模块的内容:

// src/lib.rs or src/main.rs
mod my_module;

这个模块它自己的内容放在 src/my_module.rs 这个文件里面。不要用模块声明再去包住你的代码,这里整个文件的内容都已经是这个模块的一部分了:

// src/my_modules.rs
mod nested_module {
    fn my_function() {

    }
}

// Note but that the following module is nested module of my_module
mod my_module {
    // Module: my_module/my_module
    fn my_function() {

    }
}

对于嵌套模块的处理也是类似的,我们在父模块里面声明,但是文件放的位置有点不同,它需要放到以父模块的名字为名的文件夹里面。比如像这种情况:

// src/my_modules/nested_module.rs
fn my_function() {

}

如果你想,你可以把父模块的代码也放到以父模块的名字为名的文件夹里面,只需放在里面一个名为 mod.rs 的特殊文件即可:

// src/my_modules/mod.rs
mod nested_module;

特别注意一点,对于老版本的 Rust,当你将子模块拆分到不同文件的时候,需要后一种语法(mod.rs 文件的方式)。因此你可能会发现这种方式仍然相当普遍。

Item 之路径

即使我们之前已经见过多次,但我还是想在定义一下术语 item。item 是 crate 里面的组件,它包含各种各样的语言结构,它们在编译时确定并且只驻存在只读存储器中。比如在 Rust 中的 item 有:模块、函数、类型、结构体、枚举、常量等等。

每一个 item 都有路径。我之前提到过 Rust 中的每一个函数和结构体都可以通过它所属的 crate 引用到,这就是路径这个概念的结果。在模块中,每一个 item 都有一个唯一的名字。路径就是 item 的「全名」。它以 crate 名字开头,然后包含这个 item 所有的父模块,my_function 的路径是 afternoon_rusting::my_module::nested_module::my_function。现在你就能明白为什么每个 item 在整个程序中有唯一的名字了(假设我们使用了已发布的 crate)。通过路径来使用函数实在太长和冗余了。在我的 crate 里面,我能使用相对路径,它能简化一点。比如在 my_module 里面,我能通过 nested_module::my_function 来调用 my_function。但它还是太长了,不用担心,Rust 有解决办法。

use 关键字允许你把名字添加到当前作用域里面:use nested_module::my_function 可以让你使用 my_function() 来调用 my_functionuse 关键字也是我们如何和外部 crate 打交道的方式。我们可以使用 use afternoon_rusting::my_module::nested_module::my_function 来把函数引入到当前作用域。在之前版本的 Rust 中,你不得不使用 extern crate afternoon_rusting 来显式导入外部 crate。现在这个不需要,但你可能在阅读代码的时候还会发现它。有些时候我们还能使用 use 来重命名和多重导入。use afternoon_rusting::my_module::nested_module::my_function as func 可以让我们使用 func() 来调用 my_function。当我们想从同一个 crate 中导入多个 item 时,可以将它们聚集在大括号里面:

use std::{
    io::{self, BufReader}, // self allows you to alias the module io it self as if you wrote use std::io
    net::TcpStream,
    sync::mpsc,
};

Item 之可见性

我们现在只差可见性了。Rust 的每个 item 默认都是私有的。你可以在它们所属的模块和下级的模块里面使用它。如果你想它能在所属的模块外面访问,你需要使用 pub 关键字来将之声明为公有。请每个人注意,即使是对外部 crate 而言,它也能使用这些公有字段。下面这个例子展示来需要和可见性相关的规则。

mod my_module {
    pub mod nested_module {
        pub fn my_function() {

        }
    }
}

fn example() {
    // we can access nested_module here
}

必须注意可见性的路径行为:如果我们在私有模块中有一个公有 item,那么要先看这个模块的可见性,因为它出现在路径的更前面。任何人如果因为这个模块是私有的而不能访问它,那他也不能访问里面的 item,即使这个 item 是公有的。这与 Unix 中的文件权限非常相似。如果无法访问该目录,即使你具有读取该文件的权限,也无法在该目录中打开该文件。与文件系统不同,私有模块仍可被它的直接父进行访问(因为直接父可以访问你声明的私有字段)。

在上一个例子中,nested_module 是公有的,但是 my_module 是私有的。这意味着外部 crate 不能使用我们的 nested_module,因为它们不能访问 my_module。另一方面,example 函数里面可以访问 nested_module,因为它们都在相同的作用域,你可以相同的作用域里面使用私有 item。我第一次学习的时候这个地方很让我迷惑。除了 pub 关键字之外,还有一些其他关键字可用于暴露可见性。你可以阅读这里。最常见的是使用 pub(crate) 来将 item 暴露给整个 crate,但又不暴露到外部去。不可否认,可见性规则有点令人困惑,但通过一些实践(和测试),你将理解它的行为是直接且可预测的。

哇,这是一段漫长的旅程。这里有很多概念需要理解。我对这个主题的建议是去动手做。尝试创建一个包含大量库和二进制文件的模仿项目。每个都有很多模块。尝试使用 pub 关键字。看看你在哪能使用、哪里不能使用私有 item。我承认在撰写这篇文章时,我必须测试一些概念,以确保我正确记住了所有内容、完全理解 Rust 新版本的变更。

我认为这篇文章不太令人兴奋。酷的因子肯定不如 Rust 的其他特性那么高。然而,我们今天讨论的内容,能让写 Rust 项目写得非常愉快。虽然所有权、模式匹配、生命周期等功能都会带您进入 Rust,但项目管理的简易性将使您迷上它。今天的帖子没有非常的技术性,或者面向代码。不过不用担心,本系列的其余部分会满足你。下一篇文章将讨论一个特性,这个特性让我想知道如何才能再次编写 C++ 代码!