在 .NET 环境中访问旧数据





4.00/5 (6投票s)
2005年2月26日
6分钟阅读

34156

472
一篇关于如何在 .NET 环境中访问固定记录大小的数据的文章
引言
访问旧数据是非常常见的需求。除非你从头开始开发一个新系统,或者使用一个数据抽象层(例如数据库系统),否则你无法避免导入甚至导出旧数据的需求。
其中许多数据文件是二进制固定记录的。这些非常流行,因为几乎所有编程语言都支持它们。事实上,在 C 语言等具有指针的语言中,实现起来非常容易:你将数据读入一个数据缓冲区,然后告诉系统相应地解释它(例如,通过类型转换或使用指针)。
随着现代语言(PERL、Java 和 .NET 系列)的出现,指针消失了,首选的存储方法也发生了变化:如果数据仅供内部使用,则首选语言内置的序列化机制,因为它易于实现。对于将被其他系统使用或将要被其他系统使用的数据文件,存储方法可以是 XML 或某种数据库管理系统(市面上有一些小型且快速的 RDMS,可以在没有复杂安装或大量内存占用的情况下使用——例如 Berkley Sleepy Cat)。
在这篇文章中,我想展示如何在 .NET 环境中从流中读取和写入固定大小的记录。更具体地说,我将提供一些方法,将最常用的类型转换为/从字节数组。包含的源代码是用托管 C++ 编写的,但可以导入并用于任何 .NET 项目。
关于解决方案的一些说明
如果 .NET 允许直接访问内存,这将是一个简单的问题,我也会写一篇关于它的文章:)。但是,它不允许,这也是一件好事,因为有了这种方法,我们就少了很多内存泄漏,有了垃圾收集器,提高了安全性,但也增加了解决这个问题的难度。我最初解决这个问题的一种方法是从 kernel32.dll 导入 RtlMoveMemory
函数,并尝试用它来捣鼓。但是结果并不令人满意,很快我发现了一个更优雅的解决方案,可以 100% 在 .NET 框架内实现(从而使其与所有实现兼容——甚至可能与 Mono 兼容,尽管我没有检查过)。
基本思想是一样的:我们声明两个内存区域,一个作为字节缓冲区,另一个作为所需类型,然后在它们之间移动数据。假设我们的记录包含两个元素:一个 4 字节的浮点数和一个 2 字节的无符号整数。我们首先声明两个类
[StructLayout(System::Runtime::InteropServices::LayoutKind::Sequential)]
private __gc class record_holder {
public:
[MarshalAs(System::Runtime::InteropServices::UnmanagedType::R4)]
unsigned short float_value;
[MarshalAs(System::Runtime::InteropServices::UnmanagedType::U2)]
unsigned short int_value;
};
[StructLayout(System::Runtime::InteropServices::LayoutKind::Sequential)]
private __gc class record_array_holder {
public:
[MarshalAs(System::Runtime::InteropServices::UnmanagedType::ByValArray,
SizeConst=8)]Byte buffer[];
};
现在我们可以使用以下代码在它们之间进行转换
using namespace System::Runtime::InteropServices;
...
//create a new instance of the record class
record_holder *rec = new record_holder;
rec->float_value = 1.0; //assign values to the member elements
rec->int_value = 2.0;
//create a memory buffer and copy the class there
IntPtr memBuff = Marshal::AllocHGlobal(Marshal::SizeOf(record_holder));
Marshal::StructureToPtr(record_holder, memBuff, false);
//now create a byte array and copy the bytes from the allocated memory area
//one by one
Byte buffer[] = new Byte[Marshal::SizeOf(record_holder)];
for(int i = 0; i < buffer->Length; i++)
buffer[i] = Marshal::ReadByte(memBuff, i);
//destroy the memory buffer – we need to do this explicitly since this is
//outside the garbage collectors reach
Marshal::FreeHGlobal(memBuff);
这样我们就完成了。现在 buffer 变量包含与记录对应的字节序列,并且与常规的二进制文件访问方法兼容。反向转换也同样简单直接
using namespace System::Runtime::InteropServices;
...
record_array_holder *holder = new record_array_holder;
record_holder *valueHolder = new record_holder;
holder->buffer = new Byte[8];
//fill up the buffer with some values, possibly read from a file stream
holder->buffer[0] = ...
holder->buffer[2] = ...
...
//allocate a global memory
IntPtr memBuff = Marshal::AllocHGlobal(Marshal::SizeOf(record_holder));
//write the value class to the memory area
Marshal::StructureToPtr(holder, memBuff, false);
//and now read the value holder back from it. We can do this since they are
//the same size
Marshal::PtrToStructure(memBuff, valueHolder);
//free the memory
Marshal::FreeHGlobal(memBuff);
现在我们可以像访问对象 valueHolder 的字段一样访问数据成员。这是我用来编写随文章附带的通用转换类的基本方法。它用托管 C++ 编写,可以通过添加引用在任何 .NET 项目文件中使用(我个人在 VB .NET 项目中使用它)。大多数方法是不言自明的;但是,我将在下面分析每种数据类型的具体细节。我想提的一点是,包含的类同时支持小端序和大端序字节顺序。要在这两者之间切换,请相应地更改类实例的 ReverseByteOrder
字段,代码将执行必要的更改。这些方法是线程安全的,所以如果你出于性能原因决定使用一个对象实例,你可以这样做。不过我想警告你,如果 ReverseByteOrder
设置为 true,mbf_ 函数不是最优的(它们基本上会执行三次字节旋转)。这是为了使代码更易于理解,但如果你想在任何高性能系统中使用它们,我建议你修改它。
此外,代码实际上不处理异常,所以在调用函数时你应该检查它们。
整数和字节顺序
这里没有什么有趣的。你可以通过上述方法将最常见的整数格式(2 / 4 / 8 字节,有符号 / 无符号)转换为/从字节数组。
浮点数和 Microsoft 二进制格式
这里有两个常见的浮点数格式(4 / 8 字节)。有趣的是它支持 Microsoft 二进制格式 (MBF)。所有现代和大多数非现代编程语言都使用 IEEE 标准来存储浮点数。然而,在早期(当数学协处理器在 PC 上还不普遍时),微软开发了自己的存储浮点数的格式。它们的大小相同,但比特在字段(符号、尾数、指数)之间的分布不同。它们的优点是,通过软件模拟浮点协处理器,使用 MBF 可以获得稍高的速度。当协处理器普及后,对它们的 [MBF] 支持就逐渐被放弃了。不过,你们中有些人可能和我一样,所以我包含了一些使用它们的功能。这些是前面带有 "mbf_" 前缀的函数。这些转换函数不是我写的,而是从互联网上的不同来源获取的。我只是对它们进行了改编。此外,我听说直到 VC++ 6.0,这些例程都包含在主系统中。
字符串(ASCII 和 Unicode)
这里也没有什么有趣的。这些非常简单,你甚至不需要使用介绍中描述的方法来达到结果,但它们包含在这里,以便你有一个完整的工具箱。如果设置了 ReverseByteOrder
标志,Unicode 版本的字节会两字节两字节地交换。MSDN 文档指出,ASCIIEncoder(在 ascii 方法中使用)只能正确处理 00-7F(字符集的低半部分),所以请注意。我还在寻找可能的解决方案。我想支持完整的字符范围,并允许选择转换所使用的代码页。
结论
希望通过阅读本文并下载附件的类,您将能够读取和写入您的旧文件。如果您正在从头开始编写一个新系统,请考虑使用一种易于解码的格式来存储信息,这样下一代程序员就不必通过狭窄的隐藏途径来导入它。XML 目前看来是最佳选择。
祝大家调试顺利,
Cd-MaN