多次可变和不可变借用的最佳实践是什么?

What are the best practices for multiple mutable and immutable borrows?

我正在开发 returns 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>`

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

by_name() returns 指向代表存档的对象内部的数据。同时,它需要一个 &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
}