如何正确将 `Vec<u16>` 的内容写入文件?

16

我在写Vec<u16>内容到文件时遇到了问题:

use std::fs::File;
use std::io::{Write, BufWriter};
use std::mem;

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ImageFormat {
    GrayScale,
    Rgb32,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct ImageHeader {
    pub width: usize,
    pub height: usize,
    pub format: ImageFormat,
}

pub struct Image {
    pub header: ImageHeader,
    pub data: Vec<u16>,
}

fn write_to_file(path: &str, img: &Image) -> std::io::Result<()> {
    let f = try!(File::create(path));
    let mut bw = BufWriter::new(f);
    let slice = &img.data[..];
    println!("before length: {}", slice.len());
    let sl: &[u8];
    unsafe {
        sl = mem::transmute::<&[u16], &[u8]>(slice);
    }
    println!("after length: {}", sl.len());
    try!(bw.write_all(sl));
    return Ok(());
}

fn main() {}

由于write_all()要求使用&[u8],因此我正在对&[u16]进行不安全的转换以获得&[u8]。由于转换不会改变切片长度(slice.len()sl.len()具有相同的值),因此仅输出了图像数据的一半到文件中。

如果我不需要任何不安全的转换或复制,那将更好。

2个回答

13
要直接完成它,您需要使用 std::slice::from_raw_parts() 函数:
use std::{mem, slice};

fn main() {
    let slice_u16: &[u16] = &[1, 2, 3, 4, 5, 6];
    println!("u16s: {:?}", slice_u16);

    let slice_u8: &[u8] = unsafe {
        slice::from_raw_parts(
            slice_u16.as_ptr() as *const u8,
            slice_u16.len() * mem::size_of::<u16>(),
        )
    };

    println!("u8s: {:?}", slice_u8);
}

这需要使用 unsafe,因为 from_raw_parts() 无法保证您传递给它的指针是有效的,并且它还可以创建具有任意生命周期的切片。

另请参阅:

这种方法不仅可能不安全,而且也不可移植。当您使用大于一个字节的整数时,就会立即出现字节序问题。如果在 x86 机器上以这种方式编写文件,则在 ARM 机器上读取到的将是垃圾数据。正确的方法是使用像 byteorder 这样的库,它允许您明确指定字节序:

use byteorder::{LittleEndian, WriteBytesExt}; // 1.3.4

fn main() {
    let slice_u16: &[u16] = &[1, 2, 3, 4, 5, 6];
    println!("u16s: {:?}", slice_u16);

    let mut result: Vec<u8> = Vec::new();
    for &n in slice_u16 {
        let _ = result.write_u16::<LittleEndian>(n);
    }

    println!("u8s: {:?}", result);
}

请注意,这里使用了Vec<u8>,但它实现了Write,并且WriteBytesExt特性中定义的write_u16()和其他方法也适用于任何Write,因此您可以直接在例如BufWriter上使用这些方法。
写入后,您可以使用ReadBytesExt特性中的方法读取数据。
虽然这可能比重新解释一块内存略微低效,但它是安全和可移植的。
另请参见:

谢谢!我尝试了你使用slice::from_raw_parts()的方法,它完美地工作了。 - rillomas
@rillomas 请考虑使用 byteorder 方法!现在ARM机器非常普遍,不仅仅是手机,新的Mac也是。如果您正在使用 from_raw_parts,那么在x86机器上保存的文件(大多数Windows和桌面Linux)将无法从手机或新的Mac上读取。 - Kroltan
(我知道这个问题已经五年了,只是为了给未来的路人留言) - Kroltan
我是 Rust 的新手。我喜欢第一种方式。它更类似于 C 的 fwrite。虽然可能不太可移植和不安全,但在内存有限的情况下,这是一种更快速的转储或读取大型临时文件的方法。第二种方法更具可移植性和安全性,但似乎我们必须逐个元素进行新的内存分配和转换为字节。感谢您提供的精彩答案。 - gbinux

10

我建议使用现有的序列化库,如serdebincode

extern crate bincode;
extern crate serde;
#[macro_use]
extern crate serde_derive;

use std::error::Error;

#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
pub enum ImageFormat {
    GrayScale,
    Rgb32,
}

#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
pub struct ImageHeader {
    pub width: usize,
    pub height: usize,
    pub format: ImageFormat,
}

#[derive(Serialize, Deserialize)]
pub struct Image {
    pub header: ImageHeader,
    pub data: Vec<u16>,
}

impl Image {
    fn save_to_disk(&self, path: &str) -> Result<(), Box<Error>> {
        use std::fs::File;
        use std::io::Write;
        let bytes: Vec<u8> = try!(bincode::serialize(self, bincode::Infinite));
        let mut file = try!(File::create(path));
        file.write_all(&bytes).map_err(|e| e.into())
    }
}

fn main() {
    let image = Image {
        header: ImageHeader {
            width: 2,
            height: 2,
            format: ImageFormat::GrayScale,
        },
        data: vec![0, 0, 0, 0],
    };

    match image.save_to_disk("image") {
        Ok(_) => println!("Saved image to disk"),
        Err(e) => println!("Something went wrong: {:?}", e.description()),
    }
}

1
谢谢!我也尝试了您使用bincode + rustc-serialize的方法,个人而言,我更喜欢这种方法,而不是将原始数据转储到文件中(没有unsafe,也不需要担心字节序问题)。我接受了Vladimir的答案,因为它更符合主题,但正如您所说,使用序列化可能是更好的方法。 - rillomas

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