65.9K
CodeProject 正在变化。 阅读更多。
Home

通用 BinaryReader 和 BinaryWriter 扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (16投票s)

2009年2月27日

CPOL

5分钟阅读

viewsIcon

106022

downloadIcon

683

C++/CLI 中的泛型和扩展方法

A view from .NET Reflector

引言

不久前,我正在开发一个需要 struct 和 byte[] 之间进行转换的应用程序。我需要处理许多不同类型的 struct,起初,我为每种 struct 类型维护了大量的方法。你可以想象我的代码是多么臃肿。

最终,我决定尝试让所有这些转换代码都通用化,于是这个小程序项目就诞生了。该项目提供了一个静态类(类似于 BitConverter)以及 BinaryReader 和 BinaryWriter 的扩展方法,用于在 struct(任意类型)和作为 Array 或 Stream 的一系列字节之间进行转换。

入门:不可能的方式

由于我对 C# 已经有了一些了解,所以很自然地,我也会用 C# 来启动这个项目。为了开始,我写了一些关于两种转换方法之一的代码(如下所示)然后编译。

public static unsafe byte[] GetBytes<T>(T value)
{
    // Check to see if the input is null.
    if (value == null)
    {
        throw new ArgumentNullException();
    }
 
    // Check to see if the input has value type semantics.
    if (!(value is ValueType))
    {
        throw new ArgumentException();
    }
 
    // Create an array of bytes the same size as the input.
    byte[] bytes = new byte[sizeof(T)];
 
    // Pin the byte array down so the garbage collector can't move it.
    fixed (byte* p1 = &bytes[0])
    {
        // Cast the pointer type from byte to T.
        T* p2 = (T*)p1;
 
        // Assign the value to the byte array.
        *p2 = value;
    }
 
    // Return the byte array.
    return bytes;
}

Visual Studio 没有成功编译,而是给我一个错误:“无法获取托管类型 ('T') 的地址、大小或声明指针。” 这是合乎逻辑的。毕竟,不能保证 T 具有值类型语义;你无法获取一个大小不确定的东西的大小,而一个类在运行时的大小可能任意,除非它碰巧继承自 System.ValueType(例如 Byte、Int32、UInt64、Single、Decimal、DateTime…)。下一步合乎逻辑的做法是强制输入为 ValueType。我删除了错误检查代码,并将类型参数限制为 ValueType。

public static unsafe byte[] GetBytes<T>(T value) where T : ValueType
{
    // Create an array of bytes the same size as the input.
    byte[] bytes = new byte[sizeof(T)];
 
    // Pin the byte array down so the garbage collector can't move it.
    fixed (byte* p1 = &bytes[0])
    {
        // Cast the pointer type from byte to T.
        T* p2 = (T*)p1;
 
        // Assign the value to the byte array.
        *p2 = value;
    }
 
    // Return the byte array.
    return bytes;
}

点击编译后,Visual Studio 报错:“Constraint cannot be special class 'System.ValueType'。” 经过进一步研究,我发现一篇 MSDN 文章建议将类型参数限制为 struct。编译器接受了此限制,但再次抱怨无法获取泛型类型的地址、指针或大小。(聪明的编译器,嗯?)对我来说,这似乎是一条死胡同。我还能如何编写这个方法呢?

C++/CLI 的方式

大约在同一时间,我还在 Visual Studio 中尝试 C++/CLI,这时我脑海中闪过一个想法。如果 C++/CLI 能做许多 C# 做不到的事情(比如混合托管代码和非托管代码),它能否让我绕过 C# 中的两种限制?只有一个办法可以找出答案,在阅读了一个小时的 C++/CLI 教程后,我得出了这个结果:

generic <typename T>
where T : System::ValueType
array<unsigned char> ^GenericBitConverter::GetBytes(T value)
{
       array<unsigned char> ^bytes = gcnew array<unsigned char>(sizeof(T));
       *reinterpret_cast<interior_ptr<T>>(&bytes[0]) = value;
       return bytes;
}

这个方法实际上奏效了,而且只需要 3 行代码!

这是扩展的、更易读的版本(与上面的等效,但分开以使步骤更清晰):

generic <typename T>
where T : System::ValueType
array<unsigned char> ^Iku::GenericBitConverter::GetBytes(T value)
{
       // Create an array of bytes the same size as the input.
       array<unsigned char> ^bytes = gcnew array<unsigned char>(sizeof(T));
 
       // Create a pointer to the byte array.
       interior_ptr<unsigned char> ptr1 = &bytes[0];
 
       // Cast the pointer to the same type as the input.
       interior_ptr<T> ptr2 = reinterpret_cast<interior_ptr<T>>(ptr1);
 
       // Assign the value to the byte array.
       *ptr2 = value;
 
       // Return the byte array.
       return bytes;
}

GenericBitConverter

…这就是 GenericBitConverter。它适用于任何继承自 ValueType(struct)的类型,因此适用于所有基本的 .NET 数据类型,如 double 和 decimal(除了 string,它实际上是 object),以及枚举和其他复杂结构。

在 C++/CLI 中创建扩展方法

现在 GenericBitConverter 已经就位,如果 BinaryReader 和 BinaryWriter 类也能获得相同的功能就好了,所以我决定也用相同的功能来扩展这些类。这并不比正确获取 GenericBitConverter 容易。起初编写方法很简单:

generic <typename T>
where T : System::ValueType
T Iku::IO::BinaryReaderExtension::ReadValue(BinaryReader ^binaryReader)
{
       return GenericBitConverter::ToValue<T>
		(binaryReader->ReadBytes(sizeof(T)), 0);
}

它只是没有在 C# 代码编辑器中显示为扩展方法。

是时候调查原因了,所以我用 C# 写了一个并反编译了程序集。在检查输出时,我发现了 ExtensionAttribute 属性应用于三个地方:程序集、包含扩展方法的静态类以及扩展方法本身。将属性应用到 C++/CLI 项目的正确位置后,我成功让扩展方法显示了出来。

Extension method for BinaryWriter

…这就是通用的 BinaryReader 和 BinaryWriter 扩展。

现在你可以这样做:

DateTime timeStamp = binaryReader.ReadValue<DateTime>();
static void Main(string[] args)
{
    SampleEnumeration sampEnum = SampleEnumeration.All ^ SampleEnumeration.Four;

    byte[] enumBytes = GenericBitConverter.GetBytes<SampleEnumeration>(sampEnum);

    // Writes "f0 ff ff ff ff ff ff ff"
    foreach (byte bite in enumBytes)
    {
        Console.Write("{0:x2} ", bite);
    }

    Console.WriteLine();
}

enum SampleEnumeration : long
{
    None = 0x0000000000000000,
    All = -1L,
    One = 0x0000000000000001,
    Four = 0x000000000000000f
}

很酷吧?随时可以在你自己的 C# 或 Visual Basic 项目中使用它(通过引用此项目的输出)。它完美无缺!

关注点

  • C++/CLI 还允许你将泛型参数限制为 ValueType 类型;它不会抱怨。C# 只要求使用 struct 而不是 ValueType。
  • C++/CLI 的 unsigned char 类型实际上是 Byte 类型的同义词,而不是 Char 类型。C++/CLI 的 Char 类型实际上是 wchar_t。
  • 内部指针(interior_ptr)指向其对象,但不会将它们固定在原地。这与 C# 的 fixed 行为明显不同,fixed 会将对象固定在内存中,并防止垃圾回收器移动它(只要 fixed 语句仍在范围内)。
  • 尽管 C++/CLI 没有为创建扩展方法提供任何语法糖,但可以通过将 ExtensionAttribute 属性应用于方法、其包含的类和其包含的程序集来完成。(ExtensionAttribute 可以在 System::Runtime::CompilerServices 命名空间下找到)扩展方法的这种特性意味着它可以由任何支持属性、静态类和静态方法的语言来创建。
  • C# 编译器会假定泛型类型可以是任何常规对象,即使你将其限制为 struct(或 ValueType)。我在 C# 中没有找到任何解决办法(因此诞生了这个项目)。

更新

  • 2009 年 2 月 27 日:添加了已编译程序集的下载;并非所有人都能够编译 C++/CLI 项目(例如 Express 版本用户)。现在每个人都可以使用了!
© . All rights reserved.