用 BinaryReader/BinaryWriter 时,由于它们不支持泛型参数,必须明确调用指定版本的方法才能工作(比如 ReadInt32),这用起来就很麻烦。如果可以支持泛型参数(比如 Read<int>),就可以自动量产代码了。所以,我们来为 BinaryReader/BinaryWriter 做个泛型扩展吧!
if-else 版本
从最简单的方法开始。早先 C# 的 switch-case 不支持类型匹配,只能使用常量,这种情况下使用 if-else 确定泛型参数类型是很直接的想法。
public static T Read<T>(this BinaryReader reader)
{
var type = typeof(T);
if (type == typeof(int)) return (T)(object)reader.ReadInt32();
if (type == typeof(float)) return (T)(object)reader.ReadSingle();
// ...
throw new Exception($"Type {type.FullName} is not supported.");
}
你可能注意到从 int 到 T 的转换必须通过装箱中介才能完成,这不是一个好办法。其实你可以使用一个显式布局的结构体来解决这个问题。(更正:似乎不行!)
[StructLayout(LayoutKind.Explicit)]
struct GenericUnion<TKnown, TResult>
{
[FieldOffset(0)]
public TKnown known;
[FieldOffset(0)]
public TResult result;
}
public static T Read1<T>(this BinaryReader reader)
{
var type = typeof(T);
if (type == typeof(int)) return new GenericUnion<int, T> { known = reader.ReadInt32() }.result;
if (type == typeof(float)) return new GenericUnion<float, T> { known = reader.ReadSingle() }.result;
// ...
throw new Exception($"Type {type.FullName} is not supported.");
}
switch-case TypeCode 版本
当然,我们不喜欢 if-else,又慢又丑。其实 C# 已经支持对基本类型的 switch-case,就是通过 TypeCode 实现。
public static T Read<T>(this BinaryReader reader)
{
switch (Type.GetTypeCode(typeof(T)))
{
case TypeCode.Int32: return (T)(object)reader.ReadInt32();
case TypeCode.Single: return (T)(object)reader.ReadSingle();
// ...
default: throw new Exception($"Type {typeof(T).FullName} is not supported.");
}
}
这里也存在返回值类型转换问题,你可以使用和之前一样的解决方案来处理类型转换,下面不再赘述。
switch-case Name 版本
其实也可以使用类型名来做 switch-case(更安全的做法是使用 FullName)。
public static T Read<T>(this BinaryReader reader)
{
switch (typeof(T).Name)
{
case "Int32": return (T)(object)reader.ReadInt32();
case "Single": return (T)(object)reader.ReadSingle();
// ...
default: throw new Exception($"Type {typeof(T).FullName} is not supported.");
}
}
如果使用 C# 6,可以使用 nameof 来获取类型名以避免手写类型名。
switch-case Type 版本
从 C# 7 开始,你可以直接使用类型来进行 switch-case。但可惜的是,这个特性不适用于我们要解决问题。
public static T Read<T>(this BinaryReader reader)
{
T result;
switch (default(T))
{
case int _: { if (reader.ReadInt32() is T value) result = value; else result = default; break; }
case float _: { if (reader.ReadSingle() is T value) result = value; else result = default; break; }
// ...
default: throw new Exception($"Type {typeof(T).FullName} is not supported.");
}
return result;
}
首先 switch 的输入只接受变量而不接受类型,我们需要构造一个没有意义的 default(T);然后,对于 string 需要进行特殊处理,因为 string 是引用类型,默认值是 null,会和 case null 匹配。在上面的示例代码中,case int _ 这里有一个下划线,代表一个没有意义的变量名,因为我们不关心它的值,只关心它的类型。
另外,上面的代码在处理返回值类型转换时使用了新的 is 类型匹配,这也是 C# 7 的新特性。仍然非常可惜,不是非常适合我们要解决的问题。新的 is 类型匹配支持将一种类型的变量直接转换为另一种类型的变量,但这个变量仅在转换成功时才能访问。所以,尽管我们已经确认转换一定会成功,还是需要使用额外的 if-else 来确保编译可以成功。(其中 else 后面的代码永远不会执行,只是为了告诉编译器 result 一定会被初始化)。
超级类版本
在以上谈到的方法中,最后都需要一个毫无必要的类型转换,并且还没有一个非常优雅的转换方法。为此,我们可以引入泛型接口来解决这个问题。
interface BinaryReadable<T>
{
T Read(BinaryReader reader);
}
class BinaryReadable : BinaryReadable<int>, BinaryReadable<float> // ...
{
int BinaryReadable<int>.Read(BinaryReader reader)
{
return reader.ReadInt32();
}
float BinaryReadable<float>.Read(BinaryReader reader)
{
return reader.ReadSingle();
}
// ...
}
static BinaryReadable _binaryReadable = new BinaryReadable();
public static T Read<T>(this BinaryReader reader)
{
return ((BinaryReadable<T>)_binaryReadable).Read(reader);
}
在这个解决方案中,我们通过一个实现超多接口的类——我称之为超级类,来实现各种数据类型的读写;在使用时,将这个类的对象转化为匹配类型的接口来调用。最终,我们直接返回了 T 类型的结果,而无需进行额外的转换。
泛型生成版本
群友还提供了这样一个解决方案:
public static class ReaderWriterExtensions
{
struct BinaryReadable<T>
{
internal static Func<BinaryReader, T> read;
}
static ReaderWriterExtensions()
{
BinaryReadable<int>.read = r => r.ReadInt32();
BinaryReadable<float>.read = r => r.ReadSingle();
// ...
}
public static T Read<T>(this BinaryReader reader)
{
return BinaryReadable<T>.read(reader);
}
}
个人认为这个方法也很巧妙,利用了泛型的自动生成类型特性。一般情况下我们认为类的静态成员是全局的、唯一的,但泛型类中静态成员原本并不存在,直到类型参数被指定时才会生成一个特定的类型,这个类型拥有全部独有的静态成员。因此,在上面的代码中,通过传递不同的类型参数生成了不同的类型,同时也生成了不同的 read 实例。而且这个方法从理论上讲,支持运行时注册新类型。你在想什么?你可以将自己定义的类型的读写方法注册进去,然后可以像其他类型一样调用泛型版本的读写!
结语
目前来讲,我最喜欢的解决方案是最后一个。但本质上这个问题还是源于 C# 的局限性。如果你还有其他解决方案,不妨在评论里分享出来。