如何在Protobuf中实现VARIANT

本文关键字:实现 VARIANT Protobuf | 更新日期: 2023-09-27 18:00:36

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

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

我使用的是带有C#的protobuf网络。

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

第2版:
在上创建了github.com项目http://github.com/pvginkel/ProtoVariant具有完整的实现。

如何在Protobuf中实现VARIANT

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

如果您不喜欢null,也可以使用bool ShouldSerialize*()模式来执行同样的操作。

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

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

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

另一个技巧是泛型继承,它对更复杂的场景很有用,但不具有互操作性,即一个抽象基类,它将ConcreteType<int>ConcreteType<string>等列为子类型——然而,这需要额外的2个字节(通常),因此不那么节省。

如果真的不能告诉你需要支持什么类型,也不需要互操作性,那么离核心规范又远了一步——有一些支持在数据中包含(优化的)类型信息;请参阅ProtoMember上的DynamicType选项-这比其他两个选项占用更多空间。

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

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

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

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

这是一种通用协议缓冲区方法。当然,可能还有一些更为protobuf网络特有的东西。

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

我在这里所做的是利用可选属性。假设我想发送一个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的进一步评论显著改进了实施。有关此概念的完整实现,请参阅Git存储库。

实际上protobuf不支持任何类型的VARIANT。你可以尝试使用工会,点击此处查看更多详细信息其主要思想是将所有现有消息类型定义为可选字段的消息包装器,并通过使用union来指定它是该具体消息的哪种类型。请按照上面的链接查看示例。

我将ProtoInclude与抽象基类型和子类一起使用,以静态设置类型和单个值。以下是变体的开始:

[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字节)。为了静态地控制类型,我愿意接受这一点。