【问题标题】:Dynamic casting of unknown types for serialization用于序列化的未知类型的动态转换
【发布时间】:2011-12-29 02:12:10
【问题描述】:

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

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) );

然后对数组做类似的事情。

有什么想法吗?

【问题讨论】:

  • +1 这个问题,我一直在寻找一种可靠的方法来解决这个问题。
  • 如果丑陋的代码最终成为唯一的选择,我通常在这种情况下使用 T4 模板来避免愚蠢的错误,并让 Visual Studio 自动为我生成所有代码。您只需要一个类型列表来迭代或类似的东西。
  • @Jean-BernardPellerin - 不。它需要返回实际类型,就像演员一样。否则编译器无法判断使用bw.Write 的哪个重载。
  • @drdwilcox - FieldInfo,我从 Type.GetFields() 获取。

标签: c# .net serialization dynamic casting


【解决方案1】:

您可以使用反射来调用正确版本的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) });
}

【讨论】:

  • @Polynomial 请注意,它会慢很多......反射速度非常慢。不过,根据您的情况,它仍然可能足够快
  • 太棒了。工作绝对完美! :D
【解决方案2】:

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

如果你能够做你想做的事,那么如果它出现问题或者需要做更多的事情而其他程序员可能不理解它,那么它就很难维护......或者你可能忘记了自己做了什么,不得不重新学习。

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

有时我们喜欢解决过于简单的问题,让它们更具挑战性。但通常好的业务代码只是平淡无奇的代码。

【讨论】:

  • 关于“它只是重复”,当我必须考虑 T[]List<T>Dictionary<TK,TV> 以及反序列化时,它变得更加可怕。
  • 我明白了。我认为 Jacob 的回答非常简单......如果你想走这条路,我会选择 vcsJones 解决方案。
【解决方案3】:

我为 protobuf-net 做了一些非常相似的代码; Type.GetTypeCode(...) 是一个福音,允许switch

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
}

还是有点重复,但你只看一次Type - 其余的是switch

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

dynamic value = fi.GetValue(obj);
bw.Write(value);

哪个尝试在运行时选择最合适的重载。但是,在我看来,这还不足以在这里使用dynamic

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

【讨论】:

    【解决方案4】:

    如果你想简化它,你可以使用一个表达式来动态地做出正确的调用。

    //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));
    

    我们在这里所做的是动态生成代码以对二进制编写器进行所需的调用。这听起来比实际复杂,但我们正在做的是为BinaryWriter 的“Write”方法创建一个表达式。我们还使用Expression.Convert 动态转换它,因此会调用Write 的正确重载。我们接受 BinaryWriter 和要写入的值的两个参数。最后,我们编译 lambda 并为该类型缓存它以供以后重用。

    根据您的需要,这将比在BinaryWriter 上使用反射快得多。

    【讨论】:

    • 如果您添加更多有关其工作原理和原因的详细信息,我将对此表示赞同。
    • 这会考虑Write 的重载吗?当fi.GetValue(obj)的返回类型为Int32时,它必须调用Write(Int32),尽管GetValue的方法签名是object GetValue(object)
    • 一个很好的解决方案,除了它将字段值作为常量传递,因此每次序列化类/结构的任何实例时都需要创建一个新的 lambda 并编译它。您能否添加一个 Expression.Convert 以将字段值转换为正确的类型,然后将字段值作为参数?然后你可以重复调用它而无需再次编译。
    • @PhilWright 字段类型已经属于该类型;我们不需要显式地转换它,因为常量知道常量是什么类型;它只是不是它的强类型形式。参数化版本即将推出。
    【解决方案5】:

    我能想到三个选项:

    1) BinaryFormatter - 这也许可以通过Serialize 方法非常简单地完成您的任务。
    2)正如你所建议的,使用反射。代码看起来像这样:

    // 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 模板快速生成代码。代码仍然很丑陋,但至少需要更少的维护工作。我经常在我的一些项目中使用这种模式,因为它是两全其美的模式 - 反射没有性能损失,但动态生成代码的所有好处。

    【讨论】:

      【解决方案6】:

      即使您不做任何其他事情,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 的子类化,并为框中的类型添加更多的重载没有处理。 如果您经常这样做,那么某种缓存 NameTypeMethodInfo 可能会很有用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-10-26
        • 2018-10-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多