【问题标题】:What are the best practices for multiple mutable and immutable borrows? [duplicate]多个可变和不可变借用的最佳实践是什么? [复制]
【发布时间】:2020-11-30 15:38:26
【问题描述】:

我正在开发一个函数,它返回 Zip 存档中特定文件的内容。因为我知道文件的位置,所以我尝试使用ZipArchive.by_name 方法访问它。但是,文件名可能以不同的大小写书写。如果发生这种情况 (FileNotFound),我需要遍历存档中的所有文件并与模板执行不区分大小写的比较。但是,在这种情况下,我遇到了两个与借用有关的错误。

这是最小的示例(我使用 BarcodeScanner Android 应用程序 (../test_data/simple_apks/BarcodeScanner4.0.apk),但您可以使用任何 apk 文件,只需替换它的路径即可。您可以下载一个,例如在 ApkMirror 上):

use std::{error::Error, fs::File, path::Path};

const MANIFEST_MF_PATH: &str = "META-INF/MANIFEST.MF";

fn main() {
    let apk_path = Path::new("../test_data/simple_apks/BarcodeScanner4.0.apk");
    let _manifest_data = get_manifest_data(apk_path);
}

fn get_manifest_data(apk_path: &Path) -> Result<String, Box<dyn Error>> {
    let f = File::open(apk_path)?;
    let mut apk_as_archive = zip::ZipArchive::new(f)?;

    let _manifest_entry = match apk_as_archive.by_name(MANIFEST_MF_PATH) {
        Ok(manifest) => manifest,
        Err(zip::result::ZipError::FileNotFound) => {
            let manifest_entry = apk_as_archive
                .file_names()
                .find(|f| f.eq_ignore_ascii_case(MANIFEST_MF_PATH));

            match manifest_entry {
                Some(entry) => apk_as_archive.by_name(entry).unwrap(),
                None => {
                    return Err(Box::new(zip::result::ZipError::FileNotFound));
                }
            }
        }
        Err(err) => {
            return Err(Box::new(err));
        }
    };

    Ok(String::new()) //stub
}

Cargo.toml:

[package]
name = "multiple_borrows"
version = "0.1.0"
authors = ["Yury"]
edition = "2018"

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

[dependencies]
zip = "^0.5.8"

以下是错误:

error[E0502]: cannot borrow `apk_as_archive` as immutable because it is also borrowed as mutable
  --> src/main.rs:17:34
   |
14 |     let _manifest_entry = match apk_as_archive.by_name(MANIFEST_MF_PATH) {
   |                                 ----------------------------------------
   |                                 |
   |                                 mutable borrow occurs here
   |                                 a temporary with access to the mutable borrow is created here ...
...
17 |             let manifest_entry = apk_as_archive
   |                                  ^^^^^^^^^^^^^^ immutable borrow occurs here
...
31 |     };
   |      - ... and the mutable borrow might be used here, when that temporary is dropped and runs the destructor for type `std::result::Result<ZipFile<'_>, ZipError>`

error[E0499]: cannot borrow `apk_as_archive` as mutable more than once at a time
  --> src/main.rs:22:32
   |
14 |     let _manifest_entry = match apk_as_archive.by_name(MANIFEST_MF_PATH) {
   |                                 ----------------------------------------
   |                                 |
   |                                 first mutable borrow occurs here
   |                                 a temporary with access to the first borrow is created here ...
...
22 |                 Some(entry) => apk_as_archive.by_name(entry).unwrap(),
   |                                ^^^^^^^^^^^^^^ second mutable borrow occurs here
...
31 |     };
   |      - ... and the first borrow might be used here, when that temporary is dropped and runs the destructor for type `std::result::Result<ZipFile<'_>, ZipError>`

我了解这些错误与糟糕的架构决策有关。在这种情况下,最佳做法是什么?

【问题讨论】:

标签: rust borrow-checker borrowing


【解决方案1】:

by_name() 返回指向表示存档的对象内部的数据。同时,它需要一个&amp;mut 对归档的引用,大概是因为它在读取时需要更新一些内部数据结构。不幸的结果是您不能在保留先前调用by_name() 返回的数据时调用by_name()。在您的情况下,继续访问档案的匹配臂包含对档案的引用,但借用检查器还不够聪明,无法检测到。

通常的解决方法是分两步进行:首先,确定清单文件是否存在,然后通过名称检索它或在file_names() 中搜索它。在后一种情况下,您将需要进行另一次拆分,这次是在再次调用by_name() 之前克隆文件的名称。出于同样的原因:by_name() 需要对存档的可变引用,当您保留引用其中数据的文件名时,无法获得该引用。克隆名称会以(通常可以忽略不计)运行时成本创建一个新副本,从而允许第二次调用 by_name()

最后,您可以将ok_or_else() 组合子与? 运算符结合起来以简化错误传播。应用所有这些后,函数可能如下所示:

fn get_manifest_data(apk_path: &Path) -> Result<String, Box<dyn Error>> {
    let f = File::open(apk_path)?;
    let mut apk_as_archive = zip::ZipArchive::new(f)?;

    let not_found = matches!(
        apk_as_archive.by_name(MANIFEST_MF_PATH),
        Err(zip::result::ZipError::FileNotFound)
    );
    let _manifest_entry = if not_found {
        let entry_name = apk_as_archive
            .file_names()
            .find(|f| f.eq_ignore_ascii_case(MANIFEST_MF_PATH))
            .ok_or_else(|| zip::result::ZipError::FileNotFound)?
            .to_owned();
        apk_as_archive.by_name(&entry_name).unwrap()
    } else {
        apk_as_archive.by_name(MANIFEST_MF_PATH)?
    };

    Ok(String::new()) //stub
}

【讨论】:

  • 根本问题是原始代码应该是安全的并且可以接受,因为“第二个”可变访问仅在 Err 的情况下完成,而 没有 i> 引用了apk_as_archive。这就是借用检查器提议的“Polonius”扩展所允许的。
  • @Shepmaster 说得好,我现在修改了答案以提及它。但请注意,OP 的代码比这要复杂一些,因为它调用 by_name() 并使用 file_names() 提取的条目名称,Polonius 不会修复它。这使得根据先前答案中的说明修复代码变得不简单,尤其是对于初学者。 (这个问题可能仍然是重复的,只是不是一个微不足道的问题。)
  • 感谢您的解释和工作代码!确实,我是 Rust 的新手:这是我在 Rust 中的第一个宠物项目,所以我花了很多时间来了解错误是什么以及导致错误的原因。因此,由于 Rust 与大多数语言完全不同,我试图了解通常的事情是如何在其中实现的。
猜你喜欢
  • 1970-01-01
  • 2018-04-15
  • 1970-01-01
  • 1970-01-01
  • 2017-12-23
  • 2017-05-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多