如何将Vec<T>转换为Vec<U>而不复制向量?

23
我希望将一个 Vec<T> 转换为一个 Vec<U>,其中 T 是某种原始类型,而 UT 的新类型: struct U(T)
我尝试了类似以下的代码:
struct Foo(u32);

fn do_something_using_foo(buffer: &mut Vec<Foo>) {}

fn main() {
    let buffer: Vec<u32> = vec![0; 100];

    do_something_using_foo(&mut buffer as Vec<Foo>);
}

我不想复制向量,我想用新类型Foo包装u32字段。

这会导致错误:

error[E0308]: mismatched types
 --> main.rs:8:28
  |
8 |     do_something_using_foo(&mut buffer as Vec<Foo>);
  |                            ^^^^^^^^^^^^^^^^^^^^^^^ expected mutable reference, found struct `std::vec::Vec`
  |
  = note: expected type `&mut std::vec::Vec<Foo>`
         found type `std::vec::Vec<Foo>`
  = help: try with `&mut &mut buffer as Vec<Foo>`

error: non-scalar cast: `&mut std::vec::Vec<u32>` as `std::vec::Vec<Foo>`
 --> main.rs:8:28
  |
8 |     do_something_using_foo(&mut buffer as Vec<Foo>);
  |                            ^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error(s)

4
说白了,你需要把Vec<T>&[U]&mut [U]&mut Vec<U>的形式访问?(提示:使用切片会简单得多。) - bluss
对于我的情况来说,&mut [U] 是可以接受的,尽管 &mut Vec<U> 更可取。 - tversteeg
1
如果&mut [U]是可以接受的,那么它比&mut Vec<U>更可取 - Shepmaster
2
很久很久以前,Vec 有一个 fn map_in_place<U, F>(self, f: F) -> Vec<U> 方法,当 TU 的大小相同时可用。这个方法在 Rust 1.4 中被删除了,但似乎有一个包可以做到这一点。但在那种情况下可能过度使用。 - mcarton
2个回答

24
您不能在安全的Rust中直接更改值的类型。没有保证两种类型具有相同的大小、对齐或语义。

这适用于单个值(T -> U)以及聚合值(Vec<T> -> Vec<U>HashMap<K1,V1> -> HashMap<K2,V2>)。请注意,聚合值实际上只是“单个”值的特殊情况。

创建一个新向量

最直接的解决方案:

let buffer2 = buffer.into_iter().map(Foo).collect();

在编译器可以确定你实际上没有改变重要内容的情况下,这实际上就成为了一个无操作:
fn convert(b: Vec<u32>) -> Vec<Foo> {
    b.into_iter().map(Foo).collect()
}

使用优化后的 Rust 1.54 输出的 x86_64 汇编代码:

playground::convert:
    movq    %rdi, %rax
    movq    (%rsi), %rcx
    movups  8(%rsi), %xmm0
    movq    %rcx, (%rdi)
    movups  %xmm0, 8(%rdi)
    retq

很遗憾,这种轻量级转换目前并不是一项有保障的属性,只是实现细节。

使用泛型

你可以调整do_something_using_foo来接受一个泛型类型,并让Foou32都实现一个trait:

use std::borrow::{Borrow, BorrowMut};

#[derive(Debug, Clone)]
struct Foo(u32);

impl Borrow<u32> for Foo {
    fn borrow(&self) -> &u32 {
        &self.0
    }
}

impl BorrowMut<u32> for Foo {
    fn borrow_mut(&mut self) -> &mut u32 {
        &mut self.0
    }
}

fn do_something_using_foo<T>(buffer: &mut [T])
where
    T: BorrowMut<u32>,
{
}

fn main() {
    let mut buffer_u32 = vec![0u32; 100];
    let mut buffer_foo = vec![Foo(0); 100];

    do_something_using_foo(&mut buffer_u32);
    do_something_using_foo(&mut buffer_foo);
}

不安全的Rust

从技术上讲,这是可能的 - 你可以尽情地给自己惹麻烦。

如果你知道自己在做什么,你可以使用类似于std::mem::transmute的东西。

然而,使用transmuteVec是未定义行为,因为Vec的表示未定义。相反,请参见Sven Marnach的答案

另请参阅:


1
不能保证这两种类型的大小相同。但至少可以通过 std::mem::size_of 进行验证。 - Boiethios
对于一个无操作的原地转换,你不需要像旧的 map_in_place() 那样复杂。它之所以复杂主要是因为它必须处理向量的部分已经具有类型 U 而其余部分仍然是类型 T 时的中间状态。即使在恐慌事件发生时也要正确处理这一点是相当复杂的。但是,如果源类型和目标类型具有相同的内存布局,并且您只想将内存重新解释为不同的类型,则可以相对容易地完成此操作。 - Sven Marnach
文档中说transmute在底层上等同于C语言的memcpy,因此我认为这不符合“不复制向量”的标准。我提到这一点是因为它是“rust transmute without copying”在谷歌搜索结果中排名较高的内容。 - detly
1
@detly 是的,在64位机器上,Vec<T>24个字节 会被复制。但数据本身不会被复制。否则你需要像转换 &Vec<T> 的8个字节一样,这仍然涉及到复制这8个字节。 - Shepmaster
我之前误读了文档,我以为它也会复制T的整个基础切片。这是否意味着转换切片仅会复制组成“胖指针”的数据,而不是指向的连续数据数组? - detly

16
根据std::mem::transmute()的文档,在Rust 1.38中,结合使用Vec::from_raw_partsManuallyDrop是最佳选择。
let v_from_raw = unsafe {
    // Ensure the original vector is not dropped.
    let mut v_clone = std::mem::ManuallyDrop::new(v_orig);
    Vec::from_raw_parts(v_clone.as_mut_ptr() as *mut U,
                        v_clone.len(),
                        v_clone.capacity())
};

这需要满足TU具有相同的大小、最小对齐方式,并且T有效的所有比特模式也都有效于U。如果按照你问题中的定义,无法保证这一点。 struct U(T)定义了一个元组结构体,其内存布局是完全未定义的。但是,可以通过使用transparent表示来强制它们的内存表示相同。
#[repr(transparent)]
struct U(T);

未来选项

夜间版 Rust 有Vec::into_raw_parts,它可以减少代码量和错误的发生:

#![feature(vec_into_raw_parts)]

fn convert_using_into_raw_parts(v: Vec<T>) -> Vec<U> {
    let (ptr, len, cap) = v.into_raw_parts();
    unsafe { Vec::from_raw_parts(ptr as *mut U, len, cap) }
}

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