如何将字节块读入结构体中

3
我是一名有用的助手,可以为您翻译文本。

我有一个资源文件需要处理,其中包含一组文件。

首先,资源文件列出了包含在内的所有文件,以及一些其他数据,例如这个结构体中:

struct FileEntry{
     byte Value1;
     char Filename[12];
     byte Value2;
     byte FileOffset[3];
     float whatever;
}

我需要读取刚好这个大小的数据块。

我正在使用FileStream中的Read函数,但是如何指定结构体的大小呢? 我使用了:

int sizeToRead = Marshal.SizeOf(typeof(Header));

然后将此值传递给Read,但是我只能读取一组byte[],我不知道如何将其转换为指定的值(当然我知道如何获取单个字节值...但不知道其他值)。
另外,我需要指定一个不安全的上下文,我不知道是否正确...
在.NET中,似乎读取字节流比我想象的要困难 :)
谢谢!

你能告诉我你使用的是什么编程语言吗?我猜测是C#,所以我把[tag:c#]加到了标签里。 - Joey Adams
1
可能是A C# equivalent of C's fread file i/o的重复问题。 - Hans Passant
不好意思,确实是C#。 - M Rajoy
5个回答

8
假设这是C#,我不会把结构体作为FileEntry类型创建。我会用字符串替换char[20],并使用BinaryReader - http://msdn.microsoft.com/en-us/library/system.io.binaryreader.aspx来读取单个字段。您必须按照写入时的相同顺序读取数据。
类似于:
class FileEntry {
     byte Value1;
     char[] Filename;
     byte Value2;
     byte[] FileOffset;
     float whatever;
}

  using (var reader = new BinaryReader(File.OpenRead("path"))) {
     var entry = new FileEntry {
        Value1 = reader.ReadByte(),
        Filename = reader.ReadChars(12) // would replace this with string
        FileOffset = reader.ReadBytes(3),
        whatever = reader.ReadFloat()           
     };
  }

如果您坚持使用结构体,您应该使其不可变并创建一个构造函数,为每个字段设置参数。

这个工作得十分顺利。你怎么“用字符串替换它”?使用ReadString()你不能指定大小,所以它会读取超出期望位置的内容。 - M Rajoy
实际上,如果在之前作为字符串编写,则字符串的大小包含在流中。来自MSDN - “从当前流读取字符串。该字符串以长度为前缀,每次七位编码为一个整数。”(http://msdn.microsoft.com/en-us/library/system.io.binaryreader.readstring.aspx)。然而,那么您还必须在此之前使用BinaryWriter.Write(string)。您可以使用字符构造字符串 - “StringField = new string(reader.ReadChars(20));”。 - Vasea

6

如果您能使用不安全的代码:

unsafe struct FileEntry{
     byte Value1;
     fixed char Filename[12];
     byte Value2;
     fixed byte FileOffset[3];
     float whatever;
}

public unsafe FileEntry Get(byte[] src)
{
     fixed(byte* pb = &src[0])
     {
         return *(FileEntry*)pb;
     } 
}

固定的关键字将数组嵌入结构中。由于它是固定的,如果您不断地创建它们且从未释放它们,则可能会引起GC问题。请记住,常数大小为n*sizeof(t)。因此,Filename[12]分配了24个字节(每个字符为2个字节unicode),而FileOffset[3]则分配了3个字节。如果您不处理磁盘上的Unicode数据,则这很重要。我建议将其更改为byte[]并将结构转换为可用的类,以便您可以转换字符串。
如果您无法使用unsafe,则可以使用整个BinaryReader方法:
public unsafe FileEntry Get(Stream src)
{
     FileEntry fe = new FileEntry();
     var br = new BinaryReader(src);
     fe.Value1 = br.ReadByte();
     ...
}

不安全的方式几乎是瞬间完成的,速度更快,特别是当您一次转换许多结构体时。问题在于您是否想使用不安全的方法。我的建议是,只有在您绝对需要性能提升时才使用不安全的方法。


最好不要使用unsafe,因为只有在处理速率不会真正引人注目的小文件时才会使用。 - M Rajoy

3
基于这篇文章,我将其通用化,下面是直接将数据编组到结构体中的方法。在处理较长的数据类型时非常有用。
public static T RawDataToObject<T>(byte[] rawData) where T : struct
{
    var pinnedRawData = GCHandle.Alloc(rawData,
                                       GCHandleType.Pinned);
    try
    {
        // Get the address of the data array
        var pinnedRawDataPtr = pinnedRawData.AddrOfPinnedObject();

        // overlay the data type on top of the raw data
        return (T) Marshal.PtrToStructure(pinnedRawDataPtr, typeof(T));
    }
    finally
    {
        // must explicitly release
        pinnedRawData.Free();
    }
}

示例用法:

[StructLayout(LayoutKind.Sequential)]
public struct FileEntry
{
    public readonly byte Value1;

    //you may need to play around with this one
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
    public readonly string Filename;

    public readonly byte Value2;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public readonly byte[] FileOffset;

    public readonly float whatever;
}

private static void Main(string[] args)
{
    byte[] data =;//from file stream or whatever;
    //usage
    FileEntry entry = RawDataToObject<FileEntry>(data);
}

是的,字符串最好被读作Byte[],然后使用一些方法将其转换为Char[],再转换为String。通过一些基本的“仅ASCII”检查和经典的以0结尾的C字符串行为,这将类似于String filename = new String(filenameArr.TakeWhile(x => x != 0).Select(x => x < 128 ? Convert.ToChar(x) : '?').ToArray()); - Nyerguds

2
将您的FileStreamBinaryReader包装在一起,将为您提供专用的基本类型Read*()方法: http://msdn.microsoft.com/en-us/library/system.io.binaryreader.aspx 从我的角度来看,您可以使用[StructLayout(LayoutKind.Sequential)]标记您的struct(以确保在内存中正确表示),并在unsafe块中使用指针来实际以C样式填充结构体。但是,如果您不真正需要它(interop、像图像处理这样的重型操作),则不建议使用unsafe

1

这不是完整的答案(我认为已经有人回答过了),但是关于文件名的一个特定说明:

Char类型在C#中可能不是一个字节,因为.NET字符是Unicode,意味着它们支持远远超出255的字符值,因此将文件名数据解释为Char[]数组会导致问题。所以第一步肯定是将其读取为Byte[12],而不是Char[12]

然而,直接从字节数组转换为字符数组也不建议,因为在这样的二进制索引中,文件名比允许的12个字符短的文件名可能会用'00'字节填充,因此直接转换将导致字符串始终为12个字符长,并且可能以这些零字符结尾。

然而,简单地修剪这些零并不可取,因为用于这种数据的读取系统通常只会读取到第一个遇到的零,并且如果写入系统在将字符串放入缓冲区之前不费心地用零清除其缓冲区,那么数组中后面的数据实际上可能包含垃圾。这是很多程序不费心做的事情,因为它们认为读取系统只会解释到第一个零。

假设这确实是典型的以零结尾的(C风格)字符串,以每个字符一个字节的文本编码(如ASCII、DOS-437或Win-1252)保存,第二步是在第一个零处截断字符串。您可以使用Linq的TakeWhile函数轻松完成此操作。然后第三步也是最后一步是将结果的字节数组转换为字符串,使用它写入的每个字符一个字节的文本编码:

public String StringFromCStringArray(Byte[] readData, Encoding encoding)
{
    return encoding.GetString(readData.TakeWhile(x => x != 0).ToArray());
}

正如我所说,编码可能是纯ASCII格式,可以从Encoding.ASCII访问,标准的美国DOS编码为Encoding.GetEncoding(437),或者是Windows-1252,这是标准的美国/西欧Windows文本编码,你可以使用Encoding.GetEncoding("Windows-1252")来检索它。

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