当存在main.rs和lib.rs时,Rust模块容易混淆。

166
我有4个文件:

main.rs

mod bar;

fn main() {
    let v = vec![1, 2, 3];
    println!("Hello, world!");
}

lib.rs

pub mod foo;
pub mod bar;

foo.rs

pub fn say_foo() {

}

bar.rs

use crate::foo;

fn bar() {
    foo::say_foo();
}


当我运行 cargo run 时,出现一个错误提示:
error[E0432]: unresolved import `crate::foo`
 --> src/bar.rs:1:5
  |
1 | use crate::foo;
  |     ^^^^^^^^^^ no `foo` in the root

有人能解释一下如何修复这个问题吗?更广泛地说,当存在 main.rslib.rs 时,模块查找是如何工作的?
编辑:在 main.rs 中添加 mod foo 可以解决问题。但我不明白这一点——我认为 lib.rs 是“公开”所有模块的地方?为什么我还要在 main.rs 中声明模块呢?
我的 Cargo.toml 文件:
[package]
name = "hello-world"
version = "0.1.0"
authors = ["me@mgail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

你想做什么?如果 bar 需要 foo 才能工作,那么在没有 foo 的情况下,你想如何在二进制文件中使用 bar? - Boiethios
你的 Cargo.toml 里有什么?你在一个项目中同时拥有 库和二进制文件 吗?看起来你有点混淆了。 - zrzka
6个回答

327

让我们从头开始。看一下The Cargo Book中的Package Layout章节。如您所见,您的包可以包含很多内容:

  • 一个二进制文件(可运行的东西)或多个二进制文件,
  • 一个库(共享代码),
  • 示例(s),
  • 基准测试(s),
  • 集成测试。

包布局

这里没有列出所有可能性,只有二进制/库组合。

一个二进制文件

这是一个具有单个二进制文件的包的示例。入口点是src/main.rs中的main函数。

Cargo.toml

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

src/main.rs:

fn main() {
    println!("Hallo, Rust here!")
}

$ cargo run
Hallo, Rust here!

一个库

这是一个带有库的包示例。库没有入口点,无法运行。它们用于功能共享。

Cargo.toml

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

src/lib.rs:

pub fn foo() {
    println!("Hallo, Rust library here!")
}

$ cargo run
error: a bin target must be available for `cargo run`

你在 Cargo.toml 文件中看到了关于二进制文件或库的信息吗?没有。原因是我遵循了Package Layoutcargo 知道在哪里寻找东西。

二进制和库

这是一个包含二进制文件和库的示例。

Cargo.toml:

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

src/lib.rs:

pub const GREETING: &'static str = "Hallo, Rust library here!";

src/main.rs:

use hallo::GREETING;

fn main() {
    println!("{}", GREETING);
}

同样的问题,你在 Cargo.toml 文件中有没有看到任何关于二进制或库的内容? 没有。

这个包含两个东西:

  • 一个二进制文件(根目录为 src/main.rs,入口点为 src/main.rs::main),
  • 一个库(根目录为 src/lib.rs,共享代码)。

一个库可以通过 use hallo::... 从二进制文件中引用,其中 hallo 是这个包的名称(Cargo.toml -> [package] -> name)。

你的问题

Cargo.toml:

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

相同的包布局

库部分

src/lib.rs:

pub mod bar;
pub mod foo;

src/foo.rs:

pub fn say_foo() {
    println!("Foo");
}

src/bar.rs:

use crate::foo;

pub fn bar() {
    foo::say_foo();
}

crate指的是src/lib.rs,因为我们在这里的上下文是库。

将其视为一个独立的单元,并通过外部使用use hallo::...;来引用它。

二进制部分

src/main.rs

use hallo::bar::bar;

fn main() {
    bar();
}

这里我们只是在使用我们的库。

没有库

相同的代码,但将lib.rs重命名为utils.rs,并将(foo|bar).rs文件移动到src/utils/文件夹中。

src/utils.rs:

pub mod bar;
pub mod foo;

src/utils/foo.rs:

pub fn say_foo() {
    println!("Foo");
}

src/utils/bar.rs:

use super::foo;
// or use crate::utils::foo;

pub fn bar() {
    foo::say_foo();
}

我们也可以在这里使用crate,但因为我们处于二进制文件的上下文中,所以路径不同。 src/main.rs:
use utils::bar::bar;

mod utils;

fn main() {
    bar();
}

在这里,我们只是声明了另一个模块(utils)并使用它。

摘要

Cargo.toml 内容:

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

如果有一个src/main.rs文件,你基本上是在说这个:

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

[[bin]]
name = "hallo"
src = "src/main.rs"

如果有一个src/lib.rs文件,基本上您是在说:

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

[lib]
name = "hallo"
path = "src/lib.rs"

如果两者都存在,你基本上是在说这个:

[package]
name = "hallo"
version = "0.1.0"
edition = "2018"

[[bin]]
name = "hallo"
path = "src/main.rs"

[lib]
name = "hallo"
path = "src/lib.rs"

文档


3
你的意思是 use crate::foo; 吗?为什么你认为它是一个外部引用,它不是。crate 关键字指的是当前的 crate,也就是库本身。如果你愿意,可以使用 use super::foo; - zrzka
实际上无妨了,我可能是自己犯了个错误。 - L.Y. Sim
没错,看起来我确实有些困惑。因为 OP 声称将 mod bar 添加到 main.rs 中解决了问题,而事实上删除它才能使代码编译通过,因为这样 bar 将被正确声明为 lib.rs 的一个模块,然后 use crate::foo 才能正确地引用 lib.rs 中的 foo 模块。 - L.Y. Sim
1
你的帖子最后有些令人困惑。你在 Cargo.toml 中引用了 src/lib.rs,但它已经被重命名为 utils.rs。 - decades
@zrzka,非常感谢您提供如此详细的解释。我按照“相同包布局”部分创建了一个示例,但仍然出现了相同的错误。https://github.com/videni/rust-package-layout-demo - Vidy Videni
显示剩余5条评论

61

简而言之,Rust 官方文档如下所述:

如果一个包含有 src/main.rssrc/lib.rs,那么它就有两个板条箱:一个库和一个二进制文件,两者的名称与包同名。

此外,Rust 参考手册如下所述:

crate 将路径与当前板条箱相对解析。

因此,在你的项目中实际上有两个板条箱,crate 限定符解析到哪个板条箱取决于你调用它的位置。

现在在你的代码示例中,如果你想让程序编译通过,你必须从 src/main.rs 中删除 mod bar;。否则,你会声明 bar 是两个板条箱内的一个模块。

在你删除这个之后,因为在 src/lib.rs 中你已经有了:

pub mod foo;
pub mod bar;

bar现在将成为src/lib.rs的crate中的一个模块,因此bar.rs中的crate限定符将引用src/lib.rshello-world crate,这正是您想要的。


还有一件事,如果您想从src/main.rs访问在src/lib.rs中公开的项,您需要像@zrzka说的那样,命名两者共享的crate名称。例如,在名为hello-world的项目中:

use hello_world::foo;
fn main() {
    foo::say_foo();
}

这是如何将在src/lib.rs中声明的foo模块导入到src/main.rs中的方法。

然而,似乎导入行为并不适用于另一种情况。即使在指定了crate名称时,如果您在src/main.rs中声明了某个公共模块,也无法将其导入到src/lib.rs crate中。我找不到描述这种行为的文档,但通过在Rust 1.37.0中进行测试,发现确实是这种情况。


23

lib.rsmain.rs是您的软件包的两个独立入口点。

当您使用cargo run(或显式地构建二进制文件并运行它)时,将使用main.rs作为入口点,并且关键字crate指的是二进制库。 它甚至不需要知道lib.rs中有什么:二进制文件将像处理任何其他外部crate一样处理库,必须通过extern crate hello_world或例如use hello_world :: foo导入它。

然而,当您导入库时,入口点是lib.rs,而crate库crate。 在这种情况下,是的,您添加到lib.rs的所有内容都会暴露给整个crate。

在这种情况下的通常工作流程是使二进制文件成为库的薄包装器-在某些极端情况下,main.rs可能仅包含类似以下内容的内容:

fn main() {
    library::main();
}

整个逻辑(以及所有项目结构)都放在库箱中。其中一个原因正是您遇到的:可能会混淆这个具体模块是否在软件包中的每个箱中导入。


2
这个答案非常有帮助。换句话说,你可以想象一下:1. lib.rs 实际上被称为 mod.rs,2. 文件夹 src 实际上被称为 Cargo.toml[package] 表格中的 name 字段所给出的 name_of_my_crate,3. main.rs 实际上位于你的 crate 之外(因此你必须通过它的名称而不是 crate:: 来引用你的 crate)。 - BallpointBen

0
如果你想进行单元测试,最佳实践是创建一个名为app的二进制项目。 lib.rs 生成一个库 crate,main.rs 生成一个二进制 crate,而二进制 crate 只是调用库 crate。
  • lib.rs
pub mod my_module;
  • main.rs
use app::my_module::run;

fn main() {
   run();
}

0
(这里没有新的信息,只是可视化呈现)
场景 独立 依赖
依赖关系图 独立 依赖
main.rs mod foo; use hello::foo;
解释 这两个独立的 crate(入口点)mainlib 使用相同的源文件,但 main 不依赖于 lib 该项目主要由一个库组成,main 只是使用这个库。 main 通过 lib 在该项目中使用模块(大部分情况下)。
评估 通常不是人们想要的。 这种方式使项目更加模块化,通常是人们想要使用的方式。它还使得 main 成为 lib 的一种集成测试。这样,我们更容易注意到例如,从外部应该可以访问的库中的代码是否可用。

0

还有一件事: mod 在你的 crate 中定义一个新模块,无论是二进制 crate 还是库 crate;而 use 只是将模块引入当前作用域。

在你的例子中,bar.rs 中的 use crate::foo 尝试将名为 foo 的模块带入 crate 根目录下的作用域。但是因为在 main.rs 中没有 mod foo,所以 foo 模块不是二进制 crate 的一部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接