【Rust笔记】06-包和模块

06 - 包和模块

6.1 - 包

  • Cargo.toml 文件可以列取包名,及其指定版本号。用于编译前取得。

  • cargo build 的技巧:

    • --verbose 选项:了解包的协作方式;
    • --crate-type lib 选项:告诉 rustc 不要去找 main() 函数执行,而是生成一个.rlib 文件,其中包含编译后的代码,可供之后的 rustc 命令用作输入。
    • --crate-type bin 选项,编译结果是一个二进制文件。
    • --extern 选项:给出当前包用到的每个库的文件名。
    • --release 选项:产生优化后的代码,运行速度更快,但是编译时间会延长。此时,不会检查整数溢出,且会跳过 debug_assert!() 断言。同时,针对诧异生成的栈追踪信息不可靠。
  • 构建分析:

    命令行 Cargo.toml 使用的区块
    cargo build [profile.dev]
    cargo build --release [profile.release]
    cargo test [profile.test]
  • 如果想要分析程序,获得最全面的数据,需要同时启用优化(--release 选项,用于程序的发布构建)和调试(在开发期间调试构建)符号(symbol),那么必须在 cargo.toml 中添加如下代码:

    [profile.release]
    debug = true   # 在发布构建中启用调试标记
    
    • debug 设置控制 rustc 中的 -g 选项
    • 此时执行 cargo build --release 可以得到一个带有调试符号的二进制文件。
    • 优化设置不受影响。

6.2 - 模块

  • 模块是 Rust 的命名空间,也是函数、类型、常量等构成 Rust 程序或库的容器。

    • 创建模块的方式如下所示:
    mod mod_name1 {
        ...
    }
    
    mod mod_name2;
    
  • 包解决项目间代码共享的问题,模块解决项目内代码组织的问题。

  • 模块是特性项(item)的集合,通过关键字 pub 标记公有或私有特性项,任何没有标记为 pub 的特性项都是模块私有的

6.2.1 - 模块与文件

  • 模块可以使用文件创建,在文件中不需要再添加 mod 声明。
  • 模块也可以有自己的目录。如果调用一个模块 mod test; 时,既会检查 test.rs 文件,也会检查是否存在 test/mod.rs 文件。如果两个文件都存在,或者都不存在,会报错。

6.2.2 - 路径和导入

  • :: 操作符:用于访问模块的特性。通过绝对路径来引用。

    • ::std:以双冒号开头,表示引用的是标准库的顶级模块。
    • ::std::mem:引用的是标准库的子模块。
    • ::std::mem::swap:引用的是该模块中的一个公有函数。
  • use 声明:在整个代码块或者整个模块中,导入一个模块或公有函数。

    • 支持一次导入多个模块:use std::collections::{HashMap, HashSet};
    • 支持导入所有模块:use std::io::prelude::*
    • 模块不会自动从自己的父模块继承名字。比如模块 proteins/mod.rs 中有如下声明:
    // proteins/mod.rs
    
    pub enum AminoAcid {...}
    pub mod synthesis;
    
    • 子模块 proteins/synthesis.rs 中的代码,不会自动看到类型 AminoAcid
    // proteins/synthesis.rs
    
    pub fn synthesize(seq: &[AminoAcid]) {  // 错误:找不到类型AminoAcid
        ...
    }
    
    // 修改为如下:
    use super::
    
  • super 关键字:代表当前模块的父模块。上述代码可修改为如下所示:

    // proteins/synthesis.rs
    use super::AminoAcid;
    
    pub fn synthesize(seq: &[AminoAcid]) {
        ...
    }
    
  • self 关键字:代表当前模块。

    // proteins/mod.rs
    
    use self::synthesis::synthesize;
    
    use self::AminoAcid::*;
    
  • 子模块可以访问其父模块中的私有特性项,但必须通过名字导入每一项。使用 super::*; 只会导入那些被标记为 pub 的特性项

6.2.3 - 标准前置模块

  • 标准库 std 会自动链接到每个程序中,即包含隐藏的声明 extern crate std;

  • 特别常用的名字如:

    Vec
    

    Result
    

    都会包含在标准前奏里。

    • std::prelude::vl 是唯一会自动糊导入的前置模块。其中包含了常用的特型和类型。

6.2.4 - 特性项

模块由特性项构成,Rust 的主要特性项如下所示:

  • 函数。

  • 类型。

    • 用户自定义类型包括 strust 结构体、enum 枚举和 trait 特型。
  • 类型别名:

    • type 关键字,为现有类型定义一个别名。
    • type Table = HashMap>;
  • impl 块:

    • 将方法添加到类型上。
    • 不能为 impl 标记 pub,只能标记个别的方法。
  • 常量:

    • const 关键字用于定义常量。与 let 的区别,可以标记为 pub,而且必须写明类型。

      pub const ROOM_TEMPERATURE: f64 = 20.0;
      
    • static 关键字定义静态特型,等价于常量。

      pub static ROOM_TEMPERATURE: f64 = 68.0;
      
    • 常量的值会编译到代码中使用它的每个地方。

      • 常量在代码中,常用于保存魔法数值和字符串。
      • 常量没有 mut
    • 静态变量是在程序运行前就已经存在,且会持续存在,知道程序退出。

      • 静态变量在代码中,常用于保存大量数据,或者用于借用对常量值的引用。
      • 静态变量可以标记为 mut可修改的静态变量,本质上不是线程安全的,绝对不能在安全代码中使用
  • 模块:

    • 模块可以包含子模块。
    • 可以是私有的,也可以是公有的。
  • 导入:

    • use 关键字。
    • extern crate 关键字。导入一个外部的库。
  • extern 块:

    • 声明其他语言编写的函数集合,以便在 Rust 代码中调用它们。
    • 如果要在其他包里使用这个块,那么需要将这个函数乃至整个包含模块,都标记为公有。

6.3 - 库

将程序代码改成一个库的步骤:

  • 把现有的项目分成两部分:
    • 一个要称为库的包,包含所有的共享代码;
    • 一个可执行文件,包含仅供现有的命令行程序使用的代码。
  • 将这个库发布:
    • src/main.rs 重命名为 src/lib.rs
    • src/lib.rs 中,要称为库的公有特型的项添加 pub 关键字。
    • main 函数临时转移到其他地方。
  • 补充:
    • 默认情况下,cargo build 会从 src 代码目录中查找文件,然后决定如何创建。如果看到了 src/lib.rs,那么就知道是要构建一个库。
    • src/lib.rs 中的代码构成了库的根模块。使用这个库的其他包,只能访问这个根模块中的公有特性。

6.4-src/bin 目录

  • cargo 会自动将 src/bin 中的.rs 文件作为要构建的额外程序。

6.5 - 属性

  • 任何特性项都可用属性来修饰。

  • 属性:是写给编译器看的各种指令和建议的普适语法。

  • 常用的属性技巧:

    • #[allow] 属性:在编译时,禁用某些警告。

      // 在编译时,不会报告关于non_camel_case_types关键字的警告
      #[allow(non_camel_case_types)]
      pub struct git_revspec {
          ...
      }
      
    • #[cfg] 属性:将条件编译作为一个特型:

      // 只在针对安卓编译时包含此模块
      #[cfg(target_os = "android")]
      mod mobile;
      
      • 常用的#[cfg] 语法

        #[cfg(...)] 选项 启用场景
        test 启用测试(当以 cargo testrustc --test 编译时)
        debug_assertions 启用调试断言(通常用于非优化构建)
        unix 为 Unix(包括 macOS)编译
        windows 为 Windows 编译
        target_pointer_width = "64" 针对 64 位平台。另一个可能值是 “32”
        target_arch = "x86_64" 针对 x86-64 架构,其他的值还有:“x86”、“arm”、“aarch64”、“powerpc”、“powerpc64” 和 “mips”
        target_os = "macos" 为 macOS 编译。其他的值还有 “windows”、“ios”、“android”、“linux”、“openbasd”、“netbsd”、“dragonfly” 和 “bitrig”
        feature = "robots" 启用用户定义的名为 “robots” 的特性(当以 cargo build --feature robotsrustc --cfg feature='"robots"' 编译时)。特性在 Cargo.toml 的 [feature] 部分声明
        not(A) A 不满足时要提供一个函数的两个不同实现,将其中一个标记为#[cfg(x)],另一个标记为#[cfg(not(x))]
        all(A, B) A 和 B 都满足时,等价于 &&
        any(A, B) A 或 B 满足时,等价于 ||
    • #[inline] 属性:对函数的行内扩展,进行一些微观控制。

      • 如果函数或方法在一个包里定义,但在另一个包里调用,那么 Rust 就不会将其在行内扩展。除非它是泛型的(有类型参数)或者明确标记为#[inline]
      • #[inline(always)],要求每处调用都将函数进行行内扩展。
      • #[inline(never)],要求永远不要行内化。
    • #[cfg]#[allow],可以添加到整个模块中,并应用于其中所有的特性。

    • #[test]#[inline],只能添加到个别特性项。

    • 要将属性添加给整个包,需要在 main.rslib.rs 文件的顶部、任何特性之前添加,而且要用#! 而不是#标记。

      // lib.rs
      #![allow(non_camel_case_types)]  // 可以将属性添加给整个特性项,而不是其后的个别特性项
      pub struct git_revspec {
          ...
      }
      pub struct git_error {
          ...
      }
      
    • #![feature] 属性:用于开启 Rust 语言和库的不安全特性。比如一些新增的测试功能。

6.6 - 测试和文档

  • #[test] 属性:标记一些函数,表示将要进行单元测试。

  • cargo test 会运行所有#[test] 标记的代码,并生成测试结果。

  • 测试中经常使用的两个断言宏,用于检查不变形(invariant):

    • assert!(expr) 宏:在 exprtrue 时成功;否则,会诧异并导致测试失败。
    • assert_eq!(v1, v2) 宏:等价于 assert!(v1 == v2),但是如果断言失败,那么错误消息会显示两个值。
    • 即使在发布构建中也会包含 assert!assert_eq!
    • 可以使用 debug_assert!debug_assert_eq!,编写只在调试构建中检查的断言。
  • 标记为#[test] 的函数会被有条件编译。cargo buildcargo build --release 会跳过测试代码。

  • 当单元测试项比较多,建议放在一个 tests 模块里,并用#[cfg] 属性将整个模块声明为仅供测试使用。

    #[cfg(test)]  // 只在测试构建时包含此模块
    mod tests {
        ...
    }
    
  • Rust 默认通过多个线程运行多个测试。cargo test testname 可以禁用多线程,那么每次只运行一个测试。

6.6.1 - 集成测试

  • 集成测试是放在 tests 目录中的.rs 文件。
  • tests 目录与 src 目录放在一起。
  • Cargo 会把每个集成测试都编译成一个独立的包,并将其链接到你的库和 Rust 测试套件。
  • cargo test 既可运行单元测试,也运行集成测试。如果只运行特定的文件(如 test/unfurl.rs)中的集成测试,那么可以执行 cargo test --test unfurl

6.6.2 - 文档

  • cargo doc 命令为库创建 HTML 文档

    cargo doc --no-deps --open
    
    • --no-deps 选项:使 Cargo 只为当前包生成文档,不考虑它所依赖的包。
    • --open 选项:使 Cargo 生成文档后,就在浏览器中打开。
  • Cargo 把新生成的文档文件保存在 target/doc 目录中。

  • Cargo 生成的文档基于库中的 pub 特型以及它们对应的文档注释生成。

  • #[doc] 属性:用来标注文档注释。

    • /// 开头的注释,会被视作#[doc] 属性;

      /// test
      等价于
      #[doc = "test"]
      
    • //! 开头的注释,也会被视作#[doc] 属性,可以添加到相应的包含特性,通常是模块或包中。

  • 文档注释中的内容会被按照 Mardown 来解析。

6.6.3 - 文档测试

  • Rust 会将文档注释中的代码块,自动转换为测试。

  • /// #可以隐藏某些代码行。

  • no_run 注解:结合代码块界定符号,可以对特定的代码块禁用测试。

    /// ```no_run
    /// ...
    /// ```
    
  • ignore 注解:不希望测试代码被编译。

    /// ```ignore
    /// ...
    /// ```
    
  • 如果文档注释是其他语言,那么需要使用语言的名字标记。

    /// ```c++
    /// ...
    /// ```
    

6.7 - 指定依赖

  • 指定版本号:

    image = "0.6.1"
    
  • 指定 Git 仓库地址和修订版本:

    image = { git = "https://github.com/test/test.git", rev = "rust666" }
    
  • 指定包含依赖包源代码的目录:

    image = { path = "test/image" }
    
  • 如果发现某个开源包不太合用,那么可以 Fork 下来,然后修改 Cargo.toml 文件中的一行代码即可。接下来,cargo build 会立即切换到 Fork 复制下来的版本,而不再使用官方版本。

6.7.1 - 版本

  • 版本兼容性:

    • 0.0 开头的版本比较原始,Cargo 不会认为它与任何其他版本兼容。
    • 0.x 开头的版本,会被认为同其他 0.x 的版本兼容。
    • 如果项目达到 1.0 版本,只有新的主版本才会破坏兼容性。
  • 支持使用操作符指定版本:

    Cargo.toml 中的写法 含义
    image = “=0.10.0” 只使用 0.10.0 版本
    image = “>=1.0.5” 使用 1.0.5 版本或更高的版本
    image = “>1.0.5 <1.1.9” 使用大于 1.0.5 但小于 1.1.9 的版本
    image = “<=2.7.10” 使用小于等于 2.7.10 的版本
  • 另一种指定版本的策略是使用通配符 *(不常用)。

6.7.2-Cargo.lock

  • Cargo.lock 可以避免每次构建都升级一次依赖版本。确保在不同的机器上,得到一致的、可再现的构建。
  • 在第一次构建项目时,Cargo 会输出一个 Cargo.lock 文件,记录项目使用的每个包的确切版本号。后续的构建都会参考这个文件,并继续使用相同的文件。
  • 只会在以下操作 Cargo 才会升级:
    • 手工修改了 Cargo.toml 文件中的版本号
    • 运行了 cargo update:升级到与 Cargo.toml 中指定版本兼容的最新版本。
  • 如果项目是一个可执行文件,那么应把 Cargo.lock 提交到版本控制系统。任何构建这个项目的人,都可以拿到相同的版本。

6.8-crates.io

  • cargo package 命令会创建一个文件(target/package/XXX.0.1.0.crate),其中包含库的所有源文件,以及 Cargo.toml。这个文件可以上传到 crates.io,与世界共享。

  • cargo package --list 可以查看包含什么文件。

  • 可以在 Cargo.toml 文件中添加如下许可信息(可删除敏感信息如 authors 中的邮箱):

    [package]
    name = "test_code"
    version = "0.1.0"
    authors = ["You "]
    license = "MIT"
    homepage = "https://www.example.com"
    repository = "https://gitee.com/test/test_code"
    documentation = "http://www.example.com/docs"
    description = """
    Test Code.
    """
    
  • 登录到 crates.io,并取得 API 秘钥(需要保密),然后只在可控制的安全计算机上执行:

    cargo login ^^&@*&...
    
  • 最后执行 cargo publish 来发布库到 crates.io 上。

6.9 - 工作空间

  • Cargo 会保证每个包都有自己的构建目录:target,包含一份这个包所依赖的独立构建。

  • Cargo 工作空间(workspace):共享相同构建目录和 Cargo.lock 文件的一组包。可以节省编译时间和磁盘空间。

  • 创建工作空间的方法:

    • 在存储库的根目录下创建一个 Cargo.toml 文件

    • 在其中添加如下代码:其中 fern_sim 等是包含各自包的子目录的名称。

      [workspace]
      members = ["fern_sim", "fern_img", "fern_video"]
      
    • 把各个子目录中的 Cargo.lock 文件和 taerget 目录都删除。

    • 做完这些后,无论在任何包中运行 cargo build,都会自动在根目录中创建一个共享的构建目录,所有包都共享。

  • cargo build --all 命令会构建当前工作空间中所有的包。

  • cargo testcargo doc 也支持 --all 选项。

6.10 - 其他内容

  • 当在 crates.io 上发布了开源包后,包的文档会自动发布到 docs.rs
  • 如果项目在 Github 上,Travis CI 可以在每次推送代码时,自动构建和测试。详情查看(https://travis-ci.org/)。
  • 可以基于包的顶级文档注释生成一个 README.md 文件。由第三方插件提供 cargo install readme。支持 cargo readme --help 来学习这个插件。

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第八章
原文地址

你可能感兴趣的:(rust,rust,开发语言,后端)