如何在Protobuf中实现VARIANT

时间:2011-06-29 10:56:52

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

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

如何在保持邮件大小最小化和性能最佳的同时实现最佳效果?

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

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

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

5 个答案:

答案 0 :(得分:4)

您可能会收到如下消息:

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特定。

答案 1 :(得分:4)

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个字节(通常),所以不是节俭。

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

答案 2 :(得分:3)

提出问题总能让我思考。我找到了一种方法,可以将用于传输的字节数减少到最低限度。

我在这里做的是使用可选属性。说我想发送一个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存储库以获取此概念的完整实现。

答案 3 :(得分:0)

实际上protobuf不支持任何类型的VARIANT类型。 您可以尝试使用联盟,详情请参阅here 主要思想是将所有现有消息类型的消息包装器定义为可选字段,并使用union只指定它的具体消息类型。 按照上面的链接查看示例。

答案 4 :(得分:0)

我使用带有抽象基类型和子类的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&lt; d'd类实例的长度(例如,int为1字节,125字节字符串以下)。为了控制静态类型,我愿意忍受这个。