从匹配表达式返回切片(或未知大小的数组)

Return slice (or array of unknown size) from match expression

提问人:Roman Liutko 提问时间:10/26/2023 最后编辑:Roman Liutko 更新时间:10/26/2023 访问量:127

问:

我正在尝试实现以下想法:

let command = ...;

let request = match subcommand {
    Some(x) => [command, x as u8, (x >> 8) as u8],
    None => [command],
};

request应为 1 的数组(如果没有子命令)或 3(如果有 2 字节的子命令)。显然,这种方法不起作用,因为匹配表达式的分支“返回”不同的类型([u8;1] 或 [u8;3]);

我的逻辑跟进是使用切片:

let request: &[u8] = match subcommand {
    Some(x) => &[...],
    None => &[...],
};

这在常量值(例如)上效果很好,但是当我尝试使用我的变量构建数组时,它失败了。&[0, 0, 0]temporary value dropped while borrowed

我明白了,在这种情况下,我的引用是转义到高级代码块,但是有没有办法解决它(即一些生命周期注释?

事先创建缓冲区并使用切片确实有效,但感觉不是最佳的。

UPD:这是我尝试调试的函数:

// Read the data from the device.
async fn read<R>(&mut self, command: u8, subcommand: Option<u16>) -> Result<R, ChipError<E>>
where
    R: TryFrom<u16>,
{
    // If the caller has provided a subcommand, build the request with command and subcommand.
    // If there is no subcommand, use the plain command instead
    let request: &[u8] = match subcommand {
        Some(x) => &[command, x as u8, (x >> 8) as u8],
        None => &[command],
    };

    // Send the bytes to the device
    self.i2c.write(self.addr, &request).await?;

    // And read the response...
    let mut response = [0, 0];
    self.i2c
        .write_read(self.addr, &[commands::CONTROL], &mut response)
        .await?;

    match u16::from_le_bytes(response).try_into() {
        Ok(value) => Ok(value),
        Err(_) => Err(ChipError::Value),
    }
}

这就是编译器输出:

error[E0716]: temporary value dropped while borrowed
  --> xxxx/src/lib.rs:69:25
   |
68 |           let request: &[u8] = match subcommand {
   |  ______________________________-
69 | |             Some(x) => &[command, x as u8, (x >> 8) as u8],
   | |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
   | |                         |                                |
   | |                         |                                temporary value is freed at the end of this statement
   | |                         creates a temporary value which is freed while still in use
70 | |             None => &[command],
71 | |         };
   | |_________- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value
数组 rust slice variable-length-array

评论

0赞 Aleksander Krauze 10/26/2023
请向我们展示您的有问题的代码和完整的编译器错误消息的示例。
0赞 Aleksander Krauze 10/26/2023
如果你想从你的函数中返回对堆栈分配变量的引用,这将永远行不通。您必须在堆上分配内存(例如返回),或者您可以以某种方式为两个变体静态保留足够的内存(例如创建枚举)。Box<[u8]>
0赞 Roman Liutko 10/26/2023
@AleksanderKrauze添加了一个完整的函数和输出
0赞 Roman Liutko 10/26/2023
@AleksanderKrauze枚举非常有趣,但我无法将它们应用于这项任务。我正在考虑具有 2 个变体的枚举,但将变体与可变长度数组匹配的问题仍然存在。我是 Rust 的新手,所以选择使用哪些功能有点令人生畏 - 在 C 中,我可能只会创建 2 个单独的函数;)

答:

3赞 Chayim Friedman 10/26/2023 #1

有多种方法可以做到这一点。以下是一些建议。

  1. 只需使用 (或 )。是的,它的效率较低,但这对你重要吗?这可能是最明确的解决方案。VecBox<[u8]>
  2. 使用堆栈分配的向量,例如 ArrayVec
  3. 如果类型具有简单的默认值,则在外部作用域中有一个数组并初始化其中的一部分,然后返回该部分的切片:
let mut scratch_space = [0; 3];
let request = match subcommand {
    Some(x) => {
        let data = &mut scratch_space[..3];
        data.copy_from_slice(&[command, x as u8, (x >> 8) as u8]);
        data
    },
    None => {
        scratch_space[0] = command;
        &scratch_space[..1]
    },
};
  1. 如果无法做到这一点,请声明多个这样的数组,并且只初始化一个。那是:
let scratch_space1;
let scratch_space2;
let request = match subcommand {
    Some(x) => {
        scratch_space1 = [command, x as u8, (x >> 8) as u8];
        &scratch_space1[..]
    },
    None => {
        scratch_space2 = [command];
        &scratch_space2[..]
    },
};

评论

0赞 Roman Liutko 10/26/2023
我应该补充一点,堆分配器对我来说并不理想,因为我在no_std环境中运行(此代码为小型微控制器上的 I2C 总线准备事务)。
0赞 cafce25 10/26/2023
@RomanLiutko那么第 2 点和第 3 点应该适合您,不是吗?
0赞 Roman Liutko 10/27/2023
@cafce25是的。ArrayVec是一个有趣的选择。如图所示重新排列代码也有效
3赞 MeetTitan 10/26/2023 #2

这并不能直接回答标题中的问题,但假设你的编写者是 std::io::write(我知道它是 i2c,但你应该能够适应),我会做一个简单的枚举,如下所示:

use std::io::{Write, Error};

enum Request {
    Command(u8),
    CommandWithSubcommand(u8, u16)
}

impl Request {
    fn write_to(&self, mut writer: impl Write) -> Result<(), Error> {
        match self {
            Self::Command(command) => {
                writer.write_all(&[*command])?
            }
            Self::CommandWithSubcommand(command, subcommand) => {
                let [b1, b2] = subcommand.to_le_bytes(); //maybe to_be_bytes()?
                writer.write_all(&[*command, b1, b2])?
            }
        };
        Ok(())
    }
}

...
let request = match subcommand {
    Some(subcommand) => {
        Request::CommandWithSubcommand(command, subcommand)
    }
    None => {
        Request::Command(command)
    }
};
request.write_to(your_std_writer)?;
...

因为我们对每个分支分别使用 write_all() 方法,并且它们都返回相同的结果类型,所以我们不必担心生存期。这可能不优雅,但有利。

从技术上讲,您可以在 中使用单独的写入调用,但我建议使用枚举来帮助推理 1-of-N-known-implementation 问题。match subcommand

评论

0赞 Roman Liutko 10/27/2023
我知道这并不能直接解决 OP 中的问题,但我可能会选择那个。可能是最自然的解决方案 - 没有额外的复制或虚拟变量。
0赞 MeetTitan 10/27/2023
@RomanLiutko,另一个很酷的好处是你可以在函数参数中传递这些枚举。我发现它对于接受值和其他可选值的函数特别有用 - 就像你所做的那样 - 因为你通常可以使额外的可选参数枚举变体。此外,编译器使人们很难忘记变体的实现。read()
1赞 Joshua Megnauth 10/26/2023 #3

上面的两个答案列出了很好的解决方案。正如他们的回答中提到的:

  1. Rust 必须知道返回值的大小,因为它存在于堆栈上。返回两个不同大小的值不起作用,因为它不是确切的大小。
  2. 您不能返回对堆栈上存在的数据的引用,因为这会导致释放后使用。因此,您不能返回切片。

这是另一个不在堆上分配的解决方案。

use either::Either;

fn split(yes: bool) -> Either<[u8; 2], [u8; 4]> {
    if yes {
        Either::Left([1, 2])
    } else {
        Either::Right([1, 2, 3, 4])
    }
}

任何一个都是任意两种类型的有用求和类型。作为求和类型,大小是两个变体中较大的一个加上判别式。在这种情况下,返回值的总大小为 5 个字节,因为 是两个值中较大的一个,判别式为 1 个字节。如果你不想再添加一个板条箱,你可以简单地编写自己的板条箱,因为该类型不涉及任何魔法。[u8; 4]Either

pub enum Either<L, R> {
    Left(L),
    Right(R)
}