Protobuf-net:非官方手册






4.85/5 (28投票s)
Protobuf-net 是一个基于 Google Protocol Buffers 的快速且完整的 .NET 序列化库。这是一份非常简短的手册。
Protobuf-net 是一个基于 Google Protocol Buffers 的快速、多功能的 .NET 序列化库。它是那种被广泛使用但文档却很差的库之一,因此用法信息散落在互联网各处(即便如此,我还是要感谢作者在 StackOverflow 上对问题做出*令人难以置信的*快速响应)。大部分官方文档都在 入门指南 中,源代码的 XML 文档注释里也有一小部分信息(只要你的 protobuf-net.dll 文件旁边有 protobuf-net.xml 文件,这些信息就会自动出现在 Visual Studio 的智能感知中)。不过,文档注释通常只是对某个功能或含义的模糊提示。作者的博客也有各种信息。
因此,我正在将大量零散信息整合到这篇博文中。我提到了一些我**不知道**或者仅仅是推断出来的事情。如果你知道一些我不知道的内容,请留言,我会将这些信息整合到文章中。
目录概述
Protobuf-net 与标准的 .NET 序列化系统不兼容。虽然可以配置它使其行为相当类似,但应该注意以下几点:- protobuf-net 会忽略 **[Serializable]** 特性
- protobuf-net 不支持使用 ISerializable 和私有构造函数进行自定义序列化,而且据我所知,它也*没有*提供非常类似的功能。
当然,Protobuf 受限于 Protocol Buffers 的工作方式。Protocol Buffers 没有定义“记录类型”的概念(反序列化器应预先知道数据类型,因此在反序列化时必须在根级别指定数据类型),它只有少数几种数据的线路格式,并且 Protocol Buffer 中的所有字段都只有数字标识符(称为“标签”),而不是字符串。这就是为什么你应该在每个想要序列化的字段或属性上添加 **[ProtoMember(N)]** 特性。
标准的 Protocol Buffers 没有提供共享对象(在两个地方使用同一个对象)或支持引用循环的方法,因为它们的设计初衷就不是一个序列化机制。尽管如此,protobuf-net *确实*支持引用和循环(更多信息见下文)。支持引用和循环在时间和输出大小方面成本更高,所以默认是禁用的。至少有两种方式可以启用它:
- 如果你知道某个特定类的实例经常被共享,请使用 **[ProtoContract(AsReferenceDefault=true)]**。这将应用于该类型的所有字段以及包含该类型的集合。
- 你可以通过 **[ProtoMember(N, AsReference=true)]** 在单个字段上启用引用系统。引用将与任何其他同样使用 **AsReference=true** 的字段共享。如果某个对象同时被放置在具有 **AsReference=true** 的字段和没有该设置的字段中,那么 **AsReference=false** 的字段将持有该对象的副本,这些副本在反序列化时会变成独立的对象。我只能假设 **AsReference=false** 会覆盖 **AsReferenceDefault=true**。
当你在一个集合类型上使用 **AsReference=true** 时,集合内的实例会通过引用进行跟踪。然而,集合本身似乎*不会*被引用跟踪。如果同一个集合对象在多个地方使用,它将被多次序列化,并且这些副本在反序列化时不会被合并。
Protobuf 在序列化字段和属性方面同样得心应手,并且它可以序列化**公共**和**私有**的字段和属性(嗯,也许在 Silverlight 或部分信任环境中无法序列化私有成员?我怀疑 Silverlight 中有安全限制)。没有 **[ProtoMember(N)]** 特性的字段和属性通常不会被序列化,除非类有 **ImplicitFields** 选项,如 **[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]** 或 **[ProtoContract(ImplicitFields = ImplicitFields.AllFields)]**,这会导致数字被自动分配。
当使用 **ImplicitFields** 时,protobuf-net 会忽略标记为 **[ProtoIgnore]** 的字段/属性。需要明确的是,如果你不使用 **ImplicitFields**,字段和属性默认是被忽略的,必须使用 **[ProtoMember(N)]** 显式进行序列化。
[ProtoContract(ImplicitFields = ImplicitFields.AllFields)] [ProtoInclude(100, typeof(Derived))] [ProtoInclude(101, typeof(Derive2))] class Base { int Old; } [ProtoContract(ImplicitFields = ImplicitFields.AllFields)] class Derived : Base { int New; } [ProtoContract(ImplicitFields = ImplicitFields.AllFields)] class Derive2 : Base { int Eew; }具有以下模式:
message Base { optional int32 Old = 1 [default = 0]; // the following represent sub-types; at most 1 should have a value optional Derived Derived = 100; optional Derived Derive2 = 101; } message Derived { optional int32 New = 1 [default = 0]; } message Derive2 { optional int32 Eew = 1 [default = 0]; }
protobuf-net 中的类型序列化形式
- 普通序列化。在这种模式下,会写入一个标准的 Protocol Buffer,其中 Protocol Buffer 中的每个字段对应于你用 **ProtoMember** 标记的或被 **ImplicitFields** 自动选中的每个字段或属性。在反序列化期间,默认会调用默认构造函数,但可以禁用此行为。我听说 protobuf-net 允许你反序列化到只读字段(?!),这应该能让你处理许多不可变对象的情况。
- 集合序列化。如果 protobuf-net 将特定数据类型识别为集合,则使用此模式进行序列化。值得庆幸的是,集合类型不需要任何 **ProtoContract** 或 **ProtoMember** 特性,这意味着你可以轻松序列化 **List<T>** 和 **T[]** 等类型。(我不知道如果你的“集合”类上存在任何此类特性,protobuf-net 会如何反应)。我听说也支持字典。
- 自动元组序列化。在某些条件下,protobuf-net 可以通过调用其非默认构造函数(参数多于零个的构造函数)来反序列化一个没有 **ProtoContract** 特性的不可变类型。幸运的是,这在链接中有完整文档。此功能自动应用于 **System.Tuple<...>** 和 **KeyValuePair
**。 - 字符串(“可解析”)序列化。Protobuf-net 可以序列化一个具有静态 **Parse()** 方法的类。它调用 **ToString()** 进行序列化,然后调用 **Parse(string)** 来反序列化该字符串。但是,此功能默认是禁用的。通过设置 **RuntimeTypeModel.Default.AllowParseableTypes = true** 来启用它。我不知道 **[ProtoContract]** 是否会禁用此功能。
- “代理”序列化,这对于你不能修改的“封闭”类型(如 BCL(标准库)类型)非常有用。你可以通过 **RuntimeTypeModel.Default.Add(typeof(ClosedType), false) .SetSurrogate(typeof(SurrogateType))** 指定一个用户定义的类型作为代理,而不是直接序列化一个类型。为了在原始类型和代理类型之间进行转换,protobuf-net 会在代理类型上寻找转换运算符(**public static implicit operator ClosedType**,**public static explicit operator SurrogateType**);**implicit** 和 **explicit** 都可以。即使要转换的“对象”是 **null**,转换运算符也会被调用。
- “类似 XML 序列化”。在这种默认模式下,你的类必须有一个默认构造函数。在开始反序列化之前,你的默认(即无参数)构造函数会被调用。然而,与 XML 序列化不同,protobuf-net 可以调用私有构造函数。
- “类似标准序列化”。选项 **[ProtoContract(SkipConstructor = true)]** 将在不调用构造函数的情况下进行反序列化,就像标准序列化一样。太神奇了!如果你添加 **ImplicitFields = ImplicitFields.AllFields**,protobuf-net 的行为会更接近标准序列化。
- 显然,你可以反序列化到你自己创建的对象中,至少在根级别是这样。调用(非静态的)**RuntimeTypeModel.Deserialize(Stream, object, Type)** 方法(例如 **RuntimeTypeModel.Default.Deseralize()**)。我不知道为什么它既需要一个对象*又*需要一个 **Type**。也许它对子对象也能做同样的事情,我不确定。无论如何,如果你想连续反序列化、检查并丢弃多个对象,这会很方便;你可以避免为垃圾回收器制造不必要的工作。
应该注意的是,protobuf-net 支持的一些技术仅在使用完整 .NET 框架和完全信任模式时可用。例如,如此处所述,如果你使用预编译器,你将无法反序列化到私有只读字段。我个人使用的是完整 .NET 框架,不知道在其他环境中会有什么坑。
至少在使用标准序列化模式时(我对其他模式不清楚),你可以编写标记有 `[ProtoBeforeSerialization]`、`[ProtoAfterSerialization]`、`[ProtoBeforeDeserialization]` 或 `[ProtoAfterDeserialization]` 的特殊无参数方法。最重要的可能是 `[ProtoAfterDeserialization]`,它能让你确保对象有效或初始化任何不属于数据流的字段。例如:
[ProtoContract]
struct LatLonPoint
{
[ProtoMember(1)] double X;
[ProtoMember(2)] double Y;
[ProtoAfterDeserialization]
void Validate() {
if (!(Math.Abs(X) <= 180 && Math.Abs(Y) <= 90))
throw new FormatException("Invalid latitude or longitude enountered");
}
}
你也可以使用 `System.Runtime.Serialization` 中的标准特性:`OnSerializingAttribute`、`OnSerializedAttribute`、`OnDeserializingAttribute` 和 `OnDeserializedAttribute`。微软规定使用这些特性的方法需要一个 `StreamingContext` 参数,但 protobuf-net 不需要。如果该参数存在,你会得到一个 `Context` 属性为 `null` 且 `State==StreamingContextStates.Persistence` 的 `StreamingContext`。
Protobuf-net 集合处理
- Protobuf-net 使用一个“repeated”字段(Protocol Buffer 的术语)来序列化集合。因此,你应该可以在不同版本之间安全地更改集合类型。例如,你可以序列化一个 **Foo[]**,然后将其反序列化为一个 **List
**。 - 我不太清楚 protobuf-net 是如何判断一个给定类型是否为集合的。我猜:它可能会寻找 **IEnumerable<T>** 接口和一个 **Add** 方法?
- 如果你序列化一个包含空的但非 null 集合的类,protobuf-net 似乎不会区分空集合和 null。当你反序列化该对象时,protobuf-net 会让集合等于 null(或者如果你的构造函数创建了一个集合,它将保持创建状态)。
- 默认情况下,protobuf-net 是“追加”到一个集合,而不是替换它。所以,如果你写了一个默认构造函数(没有禁用它)来将一个 int[] 数组初始化为10个项目,然后反序列化一个有10个项目的数据缓冲,你最终会得到一个有20个项目的数组。糟糕。使用 **[ProtoMember(N, OverwriteList=true)]** 选项来替换现有的列表(如果有的话)(或使用 **SkipConstructor**)。
- protobuf-net 自动支持类型为 **IEnumerable<T>**、**ICollection<T>** 或 **IList<T>** 的字段。列表数据类型不会被记录,在反序列化期间,它总是将数据加载到一个新的 **List<T>** 中。
- 它也支持类型为 **IDictionary<K,V>** 的字段,并将其反序列化为 **Dictionary<K,V>**。
我的印象是,像 `IReadOnlyList
现在,如果你可以修改包含 `IReadOnlyList
[ProtoContract]
public class MyClass
{
...
public IReadOnlyList<int> MyList;
[ProtoMember(1, OverwriteList=true)]
private IEnumerable<int> PB_MyList {
get { return MyList; }
set { MyList = ((IList<int>)value).AsReadOnly(); }
}
...
}
在这里,`AsReadOnly()` 应该为列表创建一个只读包装器(我把这作为一个“读者练习”)。你可能会发现 `OverwriteList` 选项是必需的,否则 protobuf-net 似乎会尝试调用 `PB_MyList` 的 getter 并尽可能将其视为一个 `IList
如果你不能修改使用 `IReadOnlyList
杂项
- Protobuf.net 的预编译器让你可以在不支持运行时代码生成或反射的平台上使用 protobuf-net(例如 .NET CF、iOS、Silverlight,甚至 .NET 1.1)。它也可以在完整的 .NET 框架上使用,以避免一些运行时的工作。
- 默认情况下,protobuf-net 会接受 **[XmlType]** 和 **[XmlElement(Order = N)]** 来替代 **[ProtoContract]** 和 **[ProtoMember(N)]**,如果你已经在使用 XML 序列化或者想避免显式依赖 protobuf-net,这会很方便。类似地,它也接受 WCF 特性 **[DataContract]** 和 **[DataMember(Order = N)]**。**Order** 选项是支持 protobuf 所必需的。
- **[XmlInclude]** 和 **[KnownType]** 不能用来代替 **[ProtoInclude]**,因为它们没有一个整数参数来用作标签号。
- 标签值必须为正数。**[ProtoMember(0)]** 是非法的。
- 非常有用:打印 **RuntimeTypeModel.GetSchema(typeof(Root))** 的结果,以了解将使用什么 Protocol Buffers 来表示你的数据(例如 **RuntimeTypeModel.Default.GetSchema**)。**注意**:我假设这将是用于序列化的实际模式,但文档仅说明它将“建议一个 .proto 定义”。
- 带有默认参数的构造函数,如 **Constr(int x = 0)**,不被识别为无参数构造函数。
- **SkipConstructor** 和 **ImplicitFields** 选项不会被继承,可能其他选项也不会被继承。因此,例如,如果你在基类上使用 **SkipConstructor**,派生类的构造函数仍然会被调用(并且,因此,基类的构造函数也会被调用)。
- 你可能已经注意到下载页面上的“Visual Studio 2008 / 2010 support”下载包,但这到底是*干什么用的*?我没亲自试过,但根据这篇博文,我怀疑它是一个用于从 .proto 文件自动生成 C# 代码的工具。
- 有时 protobuf-net 可以序列化某些东西但无法反序列化;请务必测试双向操作。
- **RuntimeTypeModel.DeepClone()** 是一个测试序列化和反序列化是否都正常工作的便捷方法。此方法通常会序列化对象并立即再次反序列化它。
- 我怀疑你可以使用标准的 **[NonSerialized]** 特性作用于字段或属性,作为 **[ProtoIgnore]** 的替代方案。
- 在序列化子对象时,protobuf-net 可以写入一个带长度前缀的缓冲区,或者如果包含子对象的字段有 **[ProtoMember(N, DataFormat = DataFormat.Group)]** 选项,它可以使用Marc Gravell所称的“组分隔”记录,这避免了预先测量记录大小的开销。Google 称此功能为“组”,但已弃用该功能,并且据我所知,已删除了过去可能存在的任何相关文档。
- 在 **[ProtoMember]** 中,**IsRequired** 的默认值是 **false**。我只能假设它的意思与 .proto 文件中的 **required** 相同。我猜想一个必需字段总是会被写入,并且如果在反序列化过程中缺少一个必需字段,将会抛出某种异常。
- Protobuf-net 支持 **Nullable<T>**。我听说类型为 **int?** 或任何其他可空类型的值如果为 null,将不会被写入流中(因此,我*完全不知道*如果你在一个可空字段上使用 **IsRequired=true** 会发生什么——这也包括引用类型的字段。)
- Protobuf-net 会(合理地)拒绝序列化一个没有 setter 的属性,并提示“无法将更改应用于属性”。然而,它会序列化一个 setter 为私有的属性,并在反序列化期间调用该 setter。
- 如果你通过 subversion 下载 protobuf-net 的源代码,你将可以访问 Examples 项目,其中演示了使用该库的各种方法。
不使用特性进行序列化
- 调用 **model.Add(typeof(C), false).SetSurrogate(typeof(S))** 将 S 设为 C 在序列化过程中的替代品。如果使用了代理,C 的所有其他选项都会被忽略(转而使用 S 的选项)。如果不使用代理,你可能应该使用 **model.Add(typeof(C), true)**,虽然我不确定 **true** 标志到底做什么,是等同于 **[ProtoContract]** 还是有其他作用。
- **model[type].Add(7, "Foo").Add(5, "Bar")** 等同于在字段/属性 **Foo** 上使用 **[ProtoMember(7)]** 特性,在字段/属性 **Bar** 上使用 **[ProtoMember(5)]** 特性。
- **model[type].Add("Fizz", "Buzz", ...)** 会从 1 开始顺序分配标签号,或者如果已经存在一些标签号,则从现有最高标签号加一开始。所以如果该类型还没有定义字段,**Fizz** 将是 #1,**Buzz** 将是 #2。
- model[type].AddSubType(100, typeof(Derived)) 等同于 [ProtoInclude(100, typeof(Derived))]。示例在此。
数据类型
C#[ProtoContract]
class DefaultRepresentations
{
[ProtoMember(1)] int Int;
[ProtoMember(2)] uint Uint;
[ProtoMember(3)] byte Byte;
[ProtoMember(4)] sbyte Sbyte;
[ProtoMember(5)] ushort Ushort;
[ProtoMember(6)] short Short;
[ProtoMember(7)] long Long;
[ProtoMember(8)] ulong Ulong;
[ProtoMember(9)] float Float;
[ProtoMember(10)] double Double;
[ProtoMember(11)] decimal Decimal;
[ProtoMember(12)] bool Bool;
[ProtoMember(13)] string String;
[ProtoMember(14)] DayOfWeek Enum;
[ProtoMember(15)] byte[] Bytes;
[ProtoMember(16)] string[] Strings;
[ProtoMember(17)] char Char;
}
|
.protomessage DefaultRepresentations { optional int32 Int = 1 [default = 0]; optional uint32 Uint = 2 [default = 0]; optional uint32 Byte = 3 [default = 0]; optional int32 Sbyte = 4 [default = 0]; optional uint32 Ushort = 5 [default = 0]; optional int32 Short = 6 [default = 0]; optional int64 Long = 7 [default = 0]; optional uint64 Ulong = 8 [default = 0]; optional float Float = 9 [default = 0]; optional double Double = 10 [default = 0]; optional bcl.Decimal Decimal = 11 [default=0]; optional bool Bool = 12 [default = false]; optional string String = 13; optional DayOfWeek Enum = 14 [default=Sunday]; optional bytes Bytes = 15; repeated string Strings = 16; optional uint32 Char = 17 [default = (**GetSchema** 中有一个 bug;在 char 类型的字段之后输出被截断了。) |
你可以查看Protocol Buffer 文档,特别是编码部分,以获取更多信息。所有的整数类型都使用 **varint** 线路格式。因为 protobuf-net 使用 **int32/int64** 而不是 **sint32/sint64**,负数存储效率低下,但你可以使用 **DataFormat** 选项选择不同的表示方式,如下所示。**sint32/sint64**(在 protobuf-net 中称为 **ZigZag**)更适合经常为负数的字段;**sfixed32/sfixed64**(在 protobuf-net 中称为 **FixedSize**)更适合大多数时候数值很大的数字(或者如果你只是偏爱更简单的存储表示)。
顺便说一下,string 和 byte[](在 .proto 中为 "bytes")都使用长度前缀表示法。子对象(即 "messages")也使用长度前缀表示法(这有在任何地方被记录吗?),除非你使用 "group" 格式。
[ProtoContract]
class ExplicitRepresentations
{
[ProtoMember(1, DataFormat = DataFormat.TwosComplement)] int defaultInt;
[ProtoMember(2, DataFormat = DataFormat.TwosComplement)] int defaultLong;
[ProtoMember(3, DataFormat = DataFormat.FixedSize)] int fixedSizeInt;
[ProtoMember(4, DataFormat = DataFormat.FixedSize)] long fixedSizeLong;
[ProtoMember(5, DataFormat = DataFormat.ZigZag)] int zigZagInt;
[ProtoMember(6, DataFormat = DataFormat.ZigZag)] long zigZagLong;
[ProtoMember(7, DataFormat = DataFormat.Default)] SubObject lengthPrefixedObject;
[ProtoMember(8, DataFormat = DataFormat.Group)] SubObject groupObject;
}
[ProtoContract(ImplicitFields=ImplicitFields.AllFields)]
class SubObject { string x; }
.protomessage ExplicitRepresentations { optional int32 defaultInt = 1 [default = 0]; optional int32 defaultLong = 2 [default = 0]; optional sfixed32 fixedSizeInt = 3 [default = 0]; optional sfixed64 fixedSizeLong = 4 [default = 0]; optional sint32 zigZagInt = 5 [default = 0]; optional sint64 zigZagLong = 6 [default = 0]; optional SubObject lengthPrefixedObject = 7; optional group SubObject groupObject = 8; } message SubObject { optional string x = 1 [default = 0]; }顺便说一下,既然 Google 似乎没有文档说明“group”格式,我将向你展示这两种子对象格式在二进制中的样子:
[ProtoContract] class SubMessageRepresentations { [ProtoMember(5, DataFormat = DataFormat.Default)] public SubObject lengthPrefixedObject; [ProtoMember(6, DataFormat = DataFormat.Group)] public SubObject groupObject; } [ProtoContract(ImplicitFields=ImplicitFields.AllFields)] class SubObject { public int x; } /* message SubMessageRepresentations { optional SubObject lengthPrefixedObject = 5; optional group SubObject groupObject = 6; } message SubObject { optional int32 x = 1 [default = 0]; } */ using (var stream = new MemoryStream()) { _pbModel.Serialize( stream, new SubMessageRepresentations { lengthPrefixedObject = new SubObject { x = 0x22 }, groupObject = new SubObject { x = 0x44 } }); byte[] buf = stream.GetBuffer(); for (int i = 0; i < stream.Length; i++) Console.Write("{0:X2} ", buf[i]); } // Output: 2A 02 08 22 33 08 44 34 // Interpretation: // 0x2A: ((field=5) << 3) | (wire_type=2) // length-prefixed // 0x02: length=2 // 0x08: ((field=1) << 3) | (wire_type=0) // varint // 0x22: value of x // 0x33: ((field=6) << 3) | (wire_type=3) // start group // 0x08: ((field=1) << 3) | (wire_type=0) // varint // 0x44: value of x // 0x34: ((field=6) << 3) | (wire_type=4) // end group
版本控制
- 你可以在软件的不同版本之间安全地移除一个已序列化的字段(或派生类)。Protobuf-net 会静默地丢弃那些存在于数据流中但不在类中的字段。只要小心不要再次使用被移除的标签号即可。
- 你不能在不同版本之间更改 **AsReference**(或 **AsReferenceDefault**)的值。
- 保存 **GetSchema**(上文提到)的结果来跟踪你的旧版本会很方便。然后你可以“比较”两个模式版本以发现潜在的不兼容性。
- 不要在不同版本之间更改整数的存储表示;如果你在 TwosComplement 和 ZigZag 之间迁移,你的数字会被静默地搞乱。理论上我认为 protobuf-net 可以处理从 FixedSize 到 TwosComplement 的更改,反之亦然,但我不知道它*实际上*是否可以。同样,从 FixedSize int 到 FixedSize long 的更改理论上可行,但实践中我不知道。
- 另一方面,你可以增加一个非 FixedSize 整数的大小,例如从 byte 到 short 或从 int 到 long,因为线路格式没有改变。
- Google 文档有更多与版本控制相关的信息。
引用如何工作
message NetObjectProxy { // for a tracked object, the key of the **first** // time this object was seen optional int32 existingObjectKey = 1; // for a tracked object, a **new** key, the first // time this object is seen optional int32 newObjectKey = 2; // for dynamic typing, the key of the **first** time // this type was seen optional int32 existingTypeKey = 3; // for dynamic typing, a **new** key, the first time // this type is seen optional int32 newTypeKey = 4; // for dynamic typing, the name of the type (only // present along with newTypeKey) optional string typeName = 8; // the new string/value (only present along with // newObjectKey) optional bytes payload = 10; }所以看起来:
- 当第一次遇到一个对象时,会写入 **newObjectKey** 和一个 **payload** 字段;据推测,**payload** 会像它的类型是 **Foo** 一样被存储。
- 当再次遇到该对象时,只写入 **existingObjectKey**。
更多我尚不了解的东西
- 我不知道 protobuf-net 是否能够反序列化一个类型为 object 的字段。默认情况下,它不能。
- 我不知道 protobuf-net 如何*反序列化*接口类型的字段(序列化很容易,当然,它只需使用 GetType() 就能知道类型。)
- 我不知道根对象是否允许是集合或原始类型。
- 我不知道如何在特定类型序列化前运行“准备”代码,或在对象反序列化后运行验证/清理代码,但我知道存在某种“回调”机制用于此目的。