【问题标题】:How to implement VARIANT in Protobuf如何在 Protobuf 中实现 VARIANT
【发布时间】:2011-09-25 01:27:06
【问题描述】:

作为我的 protobuf 协议的一部分,我需要能够发送动态类型的数据,有点像 VARIANT。我大致要求数据是整数、字符串、布尔值或“其他”,其中“其他”(例如DateTime)被序列化为字符串。我需要能够将它们用作单个字段并在协议中多个不同位置的列表中使用。

如何在保持最小消息大小和最佳性能的同时最好地实现这一点?

我正在使用带有 C# 的 protobuf-net。

编辑:
我在下面发布了一个建议的答案,它使用了我认为所需的最小内存。

EDIT2:
http://github.com/pvginkel/ProtoVariant 创建了一个 github.com 项目并完成了实现。

【问题讨论】:

    标签: c# .net protocol-buffers protobuf-net


    【解决方案1】:

    我使用带有抽象基类型和子类的 ProtoInclude 来静态设置类型和单个值。这是 Variant 的开始:

    [ProtoContract]
    [ProtoInclude(1, typeof(Integer))]
    [ProtoInclude(2, typeof(String))]
    public abstract class Variant
    {
        [ProtoContract]
        public sealed class Integer
        {
            [ProtoMember(1)]
            public int Value;
        }
    
        [ProtoContract]
        public sealed class String
        {
            [ProtoMember(1)]
            public string Value;
        }
    }
    

    用法:

    var foo = new Variant.String { Value = "Bar" };
    var baz = new Variant.Integer { Value = 10 };
    

    这个答案会占用更多空间,因为它对 ProtoInclude 类实例的长度进行编码(例如,int 为 1 字节,小于 125 字节的字符串)。为了静态控制类型,我愿意忍受这一点。

    【讨论】:

      【解决方案2】:

      提问总是能帮助我思考。我找到了一种将用于传输的字节数降至最低的方法。

      我在这里所做的是利用可选属性。假设我想发送一个 int32。当值不为零时,我可以检查消息上的属性是否有值。否则,我将类型设置为 INT32_ZERO。这样我就可以正确存储和重建值。下面的示例具有多种类型的此实现。

      .proto 文件:

      message Variant {
          optional VariantType type = 1 [default = AUTO];
          optional int32 value_int32 = 2;
          optional int64 value_int64 = 3;
          optional float value_float = 4;
          optional double value_double = 5;
          optional string value_string = 6;
          optional bytes value_bytes = 7;
          optional string value_decimal = 8;
          optional string value_datetime = 9;
      }
      
      enum VariantType {
          AUTO = 0;
          BOOL_FALSE = 1;
          BOOL_TRUE = 2;
          INT32_ZERO = 3;
          INT64_ZERO = 4;
          FLOAT_ZERO = 5;
          DOUBLE_ZERO = 6;
          NULL = 7;
      }
      

      以及随附的部分 .cs 文件:

      using System;
      using System.Collections.Generic;
      using System.Text;
      using System.Globalization;
      
      namespace ConsoleApplication6
      {
          partial class Variant
          {
              public static Variant Create(object value)
              {
                  var result = new Variant();
      
                  if (value == null)
                      result.Type = VariantType.NULL;
                  else if (value is string)
                      result.ValueString = (string)value;
                  else if (value is byte[])
                      result.ValueBytes = (byte[])value;
                  else if (value is bool)
                      result.Type = (bool)value ? VariantType.BOOLTRUE : VariantType.BOOLFALSE;
                  else if (value is float)
                  {
                      if ((float)value == 0f)
                          result.Type = VariantType.FLOATZERO;
                      else
                          result.ValueFloat = (float)value;
                  }
                  else if (value is double)
                  {
                      if ((double)value == 0d)
                          result.Type = VariantType.DOUBLEZERO;
                      else
                          result.ValueDouble = (double)value;
                  }
                  else if (value is decimal)
                      result.ValueDecimal = ((decimal)value).ToString("r", CultureInfo.InvariantCulture);
                  else if (value is DateTime)
                      result.ValueDatetime = ((DateTime)value).ToString("o", CultureInfo.InvariantCulture);
                  else
                      throw new ArgumentException(String.Format("Cannot store data type {0} in Variant", value.GetType().FullName), "value");
      
                  return result;
              }
      
              public object Value
              {
                  get
                  {
                      switch (Type)
                      {
                          case VariantType.BOOLFALSE:
                              return false;
      
                          case VariantType.BOOLTRUE:
                              return true;
      
                          case VariantType.NULL:
                              return null;
      
                          case VariantType.DOUBLEZERO:
                              return 0d;
      
                          case VariantType.FLOATZERO:
                              return 0f;
      
                          case VariantType.INT32ZERO:
                              return 0;
      
                          case VariantType.INT64ZERO:
                              return (long)0;
      
                          default:
                              if (ValueInt32 != 0)
                                  return ValueInt32;
                              if (ValueInt64 != 0)
                                  return ValueInt64;
                              if (ValueFloat != 0f)
                                  return ValueFloat;
                              if (ValueDouble != 0d)
                                  return ValueDouble;
                              if (ValueString != null)
                                  return ValueString;
                              if (ValueBytes != null)
                                  return ValueBytes;
                              if (ValueDecimal != null)
                                  return Decimal.Parse(ValueDecimal, CultureInfo.InvariantCulture);
                              if (ValueDatetime != null)
                                  return DateTime.Parse(ValueDatetime, CultureInfo.InvariantCulture);
                              return null;
                      }
                  }
              }
          }
      }
      

      编辑:
      @Marc Gravell 的更多 cmets 显着改进了实现。有关此概念的完整实现,请参阅 Git 存储库。

      【讨论】:

      • 你序列化了多少个字段?只是类型?还是类型和值?
      • 我不确定我是否正确理解了您的问题,但类型保留为 AUTO(可选,因此未序列化),例如ValueInt32 设置为 1。我在 github.com/pvginkel/ProtoVariant.git 创建了一个完整的实现,包括单元测试等。
      • @Pieter k;我只是想确保您没有发送不必要的 dat5。不过,我不确定您为什么要调出零 - 序列化零整数与序列化枚举说“我是零整数”所需的空间完全相同......同样的布尔值和长; doublefloat 作为枚举更短,同意。
      • @Marc Gravell - 我从文档中了解到,不序列化可选字段的触发器是它是否具有默认值。这就是我叫零的原因。我虽然这是实际返回 0f 或 0L 的唯一方法。
      • @Pieter 啊,对;正如我试图在我的回答中解释的那样,你可以比这更细粒度(加上这些隐式默认值是一个经常引起混淆的原因 - 我可能会在 v2 中进行切换以杀死它们)。有很多方法可以做到这一点;可空值 (int?) 就是这样,但另一个是 bool ShouldSerialize*() 模式(从核心 .NET 借来) - 即,如果您有 int ValueInt {...}bool ShouldSerializeValueInt() { /* true to serialize, false not to */ },它将按照您的喜好行事。
      【解决方案3】:

      Jon 的多个选项涵盖了最简单的设置,尤其是在您需要跨平台支持时。在 .NET 方面(以确保您不会序列化不必要的值),只需从任何不匹配的属性返回 null,例如:

      public object Value { get;set;}
      [ProtoMember(1)]
      public int? ValueInt32 {
          get { return (Value is int) ? (int)Value : (int?)null; }
          set { Value = value; }
      }
      [ProtoMember(2)]
      public string ValueString {
          get { return (Value is string) ? (string)Value : null; }
          set { Value = value; }
      }
      // etc
      

      如果您不喜欢空值,您也可以使用 bool ShouldSerialize*() 模式执行相同操作。

      将其包装在 class 中,您应该可以在字段级别或列表级别使用它。您提到了最佳性能;我可以建议的唯一额外的事情可能是考虑将其视为“组”而不是“子消息”,因为这更容易编码(只要您期望数据,也同样容易解码)。为此,请使用Grouped 数据格式,通过[ProtoMember],即

      [ProtoMember(12, DataFormat = DataFormat.Group)]
      public MyVariant Foo {get;set;}
      

      但是,这里的差异可能很小 - 但它避免了在输出流中进行一些回溯以固定长度。无论哪种方式,就开销而言,“子消息”至少需要 2 个字节;字段标题为“至少一个”(如果12 实际上是1234567,则可能需要更多) - 长度为“至少一个”,对于较长的消息,长度会变得更大。一个组需要 2 x 字段标头,因此如果您使用低字段编号,则无论封装数据的长度如何,这将是 2 个字节(可能是 5MB 的二进制文件)。

      一个单独的技巧,对更复杂的场景有用,但不能互操作,是通用继承,即一个抽象基类,将ConcreteType<int>ConcreteType<string> 等列为子类型 - 但是,这需要额外的 2 个字节(通常),所以不那么节俭。

      进一步远离核心规范,如果您真的无法告诉您需要支持哪些类型,并且不需要互操作性 - 有一些支持在数据中包含(优化)类型信息;请参阅 ProtoMember 上的 DynamicType 选项 - 这比其他两个选项占用更多空间。

      【讨论】:

        【解决方案4】:

        实际上 protobuf 不支持任何类型的 VARIANT 类型。 你可以尝试使用 Unions,查看更多细节here 主要思想是将所有现有消息类型的消息包装器定义为可选字段,并使用union 指定具体消息的类型。 通过上面的链接查看示例。

        【讨论】:

        • 我知道我可能需要这样做。但是,我正在寻找的是可以执行且节省空间的东西。请参阅 Jon Skeet 的回答和我的评论。
        【解决方案5】:

        您可能会收到这样的消息:

        message Variant {
            optional string string_value = 1;
            optional int32 int32_value = 2;
            optional int64 int64_value = 3;
            optional string other_value = 4;
            // etc
        }
        

        然后编写一个辅助类 - 可能还有扩展方法 - 以确保您只在变体中设置 一个 字段。

        您可以选择包含一个单独的枚举值来指定设置哪个字段(使其更像一个标记的联合),但检查可选字段的能力只是意味着数据已经存在。这取决于您是想要找到正确字段的速度(在这种情况下添加鉴别器)还是包括数据本身的空间效率(在这种情况下不添加鉴别器)。

        这是一种通用协议缓冲区方法。当然,可能还有更具体的 protobuf-net。

        【讨论】:

        • 我可能需要一个鉴别器,如果只是用于序列化的字符串。我担心开销。这意味着我需要 2 个额外字节用于字段类型和标签,1 个字节用于实际鉴别器值。对于一个简单的整数,这是原始整数的 400%。我希望有更有效的东西:)。您对此有什么建议吗?
        • 顺便说一句;如果 protobuf-csharp-port 给我一个更节省空间的选择,我愿意切换实现。没有什么是一成不变的。
        • @Pieter:不要忘记,如果简单的整数很小,它可能会映射到单个字节......即使有所有额外的信息,它也会给你 4 个字节。基本上,可区分的联合有额外的开销,并且在协议缓冲区中没有任何东西可以对它们进行特殊处理。可能有一种有线格式,它“知道”一个字段将被填充,并将其与标签放在某处......但正常的有线格式中没有任何内容。
        • @Pieter:protobuf-csharp-port 肯定对此没有任何特殊情况支持。它会生成部分类,因此您可以通过这种方式添加额外的功能,仅此而已。 (protobuf-net 也可能生成部分类;我不知道。)
        • @Pieter 空间几乎是由规范预先确定的;p(除非我们允许亚规范形式)
        猜你喜欢
        • 2012-11-01
        • 2012-11-24
        • 1970-01-01
        • 2019-04-26
        • 1970-01-01
        • 1970-01-01
        • 2010-12-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多