C# 用于序列化的未知类型的动态强制转换

C# 用于序列化的未知类型的动态强制转换,c#,.net,serialization,dynamic,casting,C#,.net,Serialization,Dynamic,Casting,我正在为互操作的结构类型编写一个自定义序列化程序,其协议我无法更改。我使用反射提取结构成员值,并将它们写入二进制编写器。它只支持基本类型和数组 if (fi.FieldType.Name == "Int16") bw.Write((Int16)fi.GetValue(obj)); else if (fi.FieldType.Name == "UInt16") bw.Write((UInt16)fi.GetValue(obj)); else if (fi.FieldType.Nam

我正在为互操作的结构类型编写一个自定义序列化程序,其协议我无法更改。我使用反射提取结构成员值,并将它们写入
二进制编写器
。它只支持基本类型和数组

if      (fi.FieldType.Name == "Int16")   bw.Write((Int16)fi.GetValue(obj));
else if (fi.FieldType.Name == "UInt16")  bw.Write((UInt16)fi.GetValue(obj));
else if (fi.FieldType.Name == "Int32")   bw.Write((Int32)fi.GetValue(obj));
else if (fi.FieldType.Name == "UInt32")  bw.Write((UInt32)fi.GetValue(obj));
else if (fi.FieldType.Name == "Int64")   bw.Write((Int64)fi.GetValue(obj));
else if (fi.FieldType.Name == "UInt64")  bw.Write((UInt64)fi.GetValue(obj));
else if (fi.FieldType.Name == "Single")  bw.Write((float)fi.GetValue(obj));
else if (fi.FieldType.Name == "Double")  bw.Write((double)fi.GetValue(obj));
else if (fi.FieldType.Name == "Decimal") bw.Write((decimal)fi.GetValue(obj));
else if (fi.FieldType.Name == "Byte")    bw.Write((byte)fi.GetValue(obj));
else if (fi.FieldType.Name == "SByte")   bw.Write((sbyte)fi.GetValue(obj));
else if (fi.FieldType.Name == "String")  bw.Write((string)fi.GetValue(obj));
显然这很难看,当我想对这些类型的数组做同样的事情时,它会变得更难看

如果我能做这样的事情,那就太好了:

bw.Write( (fi.FieldType) fi.GetValue(obj) );
// sample source data
object src = (uint)234;

var bwType = typeof(BinaryWriter);
var argTypes = new Type[] { src.GetType() };
var m = bwType.GetMethod("Write", argTypes);
var args = new object[] { src };
m.Invoke(bw, args);
然后对数组执行类似的操作


有什么想法吗?

这段代码一点也不难看。。。只是重复而已。但它实际上非常干净、简短,而且很容易理解。如果你有一百万种不同的类型可以解释,那是一回事,但数量有限

如果你能做你想做的事情,那么如果它有问题或者需要做更多的事情,并且其他程序员可能不理解它,那么就很难维护它。。。或者你可能已经忘记了你到底做了什么,不得不重新学习

通过这样做,您将拥有: -增加了额外的开发时间 -可读性降低 -减速 -增加维护


有时我们喜欢把太简单的问题处理得更具挑战性。但是,好的业务代码通常只是平淡无奇、枯燥乏味的代码。

如果您想简化它,可以使用表达式动态地进行正确的调用

//Cache the generated method for re-use later, say as a static field of dictionary. It shouldn't grow too-big given the number of overloads of Write.
private static Dictionary<Type, Action<BinaryWriter, object>> _lambdaCache = new Dictionary<Type, Action<BinaryWriter, object>>();

//...

if (!_lambdaCache.ContainsKey(fi.FieldType))
{
    var binaryWriterParameter = Expression.Parameter(typeof(BinaryWriter));
    var valueParameter = Expression.Parameter(typeof(object));
    var call = Expression.Call(binaryWriterParameter, "Write", null, Expression.Convert(valueParameter, fi.FieldType));
    var lambda = Expression.Lambda<Action<BinaryWriter, object>>(call, binaryWriterParameter, valueParameter).Compile();
    _lambdaCache.Add(fi.FieldType, lambda);
}
var write = _lambdaCache[fi.FieldType];
write(bw, fi.GetValue(obj));
//缓存生成的方法以供以后重用,例如作为字典的静态字段。考虑到写操作的重载数量,它不应该增长太大。
私有静态字典_lambdache=新字典();
//...
if(!_lambdache.ContainsKey(fi.FieldType))
{
var binaryWriterParameter=Expression.Parameter(typeof(BinaryWriter));
var valueParameter=Expression.Parameter(typeof(object));
var call=Expression.call(binaryWriterParameter,“Write”,null,Expression.Convert(valueParameter,fi.FieldType));
var lambda=Expression.lambda(调用,binaryWriterParameter,valueParameter).Compile();
_lambdache.Add(fi.FieldType,lambda);
}
var write=_lambdache[fi.FieldType];
写入(bw,fi.GetValue(obj));
我们在这里所做的是动态地生成代码来调用二进制编写器。这听起来比实际情况更复杂,但我们正在做的是为
BinaryWriter
的“Write”方法创建一个表达式。我们还使用
Expression.Convert
动态强制转换它,以便调用
Write
的正确重载。我们接受BinaryWriter的两个参数和要写入的值。最后,我们编译lambda并将其缓存为该类型,以便以后重用


根据您的需要,这将比在BinaryWriter上使用反射快得多,我为protobuf net编写了一些非常类似的代码<代码>类型。GetTypeCode(…)是一个福音,允许使用
开关

switch(Type.GetTypeCode(fi.FieldType)) {
    case TypeCode.Int16: bw.Write((Int16)fi.GetValue(obj)); break
    case TypeCode.UInt32: bw.Write((UInt16)fi.GetValue(obj)); break;
        ... etc lots and lots
}
仍然有点重复,但是你只看一次
类型
,其余的是一个
开关

如果您使用的是4.0,另一个技巧可能是:

dynamic value = fi.GetValue(obj);
bw.Write(value);
它将尝试在运行时选择最合适的重载。然而,在我看来,这还不足以作为在这里使用动态代码的理由


最后一个想法是:使用元编程(如
ILGenerator
)在运行时创建代码-更复杂,但速度更快,并且在执行时(仅在准备模型时)没有任何这些检查。

您可以使用反射来调用正确版本的
Write

public static void WriteField(BinaryWriter bw, object obj, FieldInfo fieldInfo)
{
    typeof(BinaryWriter)
        .GetMethod("Write", new Type[] { fieldInfo.FieldType })
        .Invoke(bw, new object[] { fieldInfo.GetValue(obj) });
}

我可以想出三个选择:

1)
BinaryFormatter
-通过
Serialize
方法,您可以非常简单地完成任务。
2) 正如您所建议的,使用反射。代码如下所示:

bw.Write( (fi.FieldType) fi.GetValue(obj) );
// sample source data
object src = (uint)234;

var bwType = typeof(BinaryWriter);
var argTypes = new Type[] { src.GetType() };
var m = bwType.GetMethod("Write", argTypes);
var args = new object[] { src };
m.Invoke(bw, args);

3) 使用T4模板快速生成代码。代码仍然很难看,但至少需要更少的工作来维护。在我的一些项目中,我经常使用这种模式,因为它是两全其美的-没有反射带来的性能损失,但是动态生成代码的所有好处。

即使您不做任何其他事情,
switch
也可以处理字符串,这将使您的内容更易于阅读

鉴于显式强制转换正在工作:

Type t = Type.GetType(String.Concat("System.", fi.FieldType.Name));
然后使用

MethodInfo m = typeof(BinaryWriter).GetMethod("Write", new type[] { t });
如果它不是空的

m.Invoke(bw, new object[] { fi.GetValue(obj) });
这是假设
FieldType.Name
对应于范围内的类型。没有说明数组将包含什么,但是如果它是
Int16[]
,这只是一个小把戏,可能是对
BinaryWriter
进行子类化,并为框中不处理的类型添加更多重载。
如果你经常这样做,某种缓存
Name
Type
MethodInfo
,可能会很有用。

+1对于这个问题,我一直在寻找一种可靠的方法来做这件事。如果丑陋的代码最终成为唯一的选择,我通常在这种场景中使用T4模板,以避免愚蠢的错误,并让Visual Studio自动为我生成所有代码。您只需要一个类型列表来进行迭代或类似的操作。这有帮助吗@让·伯纳德·佩林——不。它需要返回实际类型,就像强制转换一样。否则,编译器无法判断要使用的
bw.Write
的哪个重载。@drdwilcox-
FieldInfo
,我从
Type.GetFields()
获取。如果您添加一些关于如何以及为什么这样做的详细信息,我将对此进行表决。这会考虑
Write
的重载吗?当返回的
fi.GetValue(obj)
类型为
Int32
时,它必须调用
Write(Int32)
,尽管
GetValue
的方法签名为
object GetValue(object)
。这是一个很好的解决方案,只是它将字段值作为常量传递,因此需要创建一个n