UniversalSerializer






4.97/5 (105投票s)
.NET 和 .NET Core 的通用且易用的序列化库。
官方网站是 universalserializer.com
并且有 NuGet 包可供使用。
我也会尽量保持本文档的更新。
- 下载源代码为 Zip UniversalSerializer_3.18.3.14_6B1E085392256A5A38FD0229FFA13F032E67A4BDECF68223BE9880D385F0110D.zip - 810 KB
(sha-256 : 6B1E085392256A5A38FD0229FFA13F032E67A4BDECF68223BE9880D385F0110D)
- 下载源代码为 Rar UniversalSerializer_3.18.3.14_E839F19AF67436EA0C4C5EF0FDB2647C2BCDF74D47DCB39592D58463DC86EA82.rar - 510 KB
(sha-256 : E839F19AF67436EA0C4C5EF0FDB2647C2BCDF74D47DCB39592D58463DC86EA82)
它是什么?
UniversalSerializer 是一个免费的 开源 高级序列化库,适用于 .NET 和 .NET Core。
它可以在 Windows、Linux、Android 以及(可能)macOS1 和 iOS1 上运行。
换句话说,它将复杂的对象值/实例保存到(文件)流中并从流中加载。流格式可以是二进制、JSON 或 XML。
它被设计成能够轻松地序列化非常复杂的类型(例如 WPF 的 Window 类)。
它包含 DLL 文件,用于
- .NET 4.0 和 .NET 4.5。
在 Windows、macOS1、Linux、Android 和 iOS1 上。 - .NET 3.5.
在 Windows 上。 - .NET Core 2.
在 Windows、macOS1 和 Linux 上。 - .NET Standard 2.
在 Windows、macOS1、Linux、Android 和 iOS1 上。 - Android(特别是)。
- Silverlight.
在 Windows 和 macOS1 上。 - UWP(通用 Windows 平台).
在 Windows 上。 - 一个额外的 DLL 文件,用于特定的WPF 类型。
在 Windows 上。 - 一个额外的 DLL 文件,用于特定的Windows Form 类型。
在 Windows 上。
在 文档 中有一个合成表。
摘要
UniversalSerializer 的目标是能够轻松地序列化任何类型,包括非常复杂的类型。
- 无需为类型(类和结构)添加属性或接口。
- 当一个类实例被多次引用时,它只被序列化一次。
- 允许循环引用。
- 重用现有的序列化或转码机制。目前包括:
[Serializable]
、ISerializable
、[ValueSerializer]
和[TypeConverter]
。 - 允许使用没有默认构造函数的类型,前提是能找到参数化构造函数(这是自动的)。
- 非泛型
ICollection
类可以被序列化,前提是能找到 Add 或 Insert 方法(这是自动的)。
当然,所有普通的构造,如类、结构、继承、公共属性、公共字段、枚举、集合、字典、泛型、多态性等,都由 UniversalSerializer 进行序列化。
当一个类型无法开箱即用序列化时,UniversalSerializer 提供两个类别的修改器:
- 容器(ITypeContainers):我们可以将有问题的类型(或一组类型)包含在一个自定义类中,该类将管理其序列化和反序列化。
- 一组过滤器:我们可以阻止某些类型,并强制序列化器存储选定的私有字段。
我最希望的是人们将来能添加更多的修改器(容器和过滤器),或许有一天所有类型都能被序列化。
UniversalSerializer 可以序列化为三种流格式:自定义二进制、JSON 和 XML。
安全性
- DLL 文件是安全的(即不不安全),它们不使用指针,甚至不使用 IL 代码生成。
- 序列化是线程安全的。
用法示例
带文件名的示例
var data = new Hashtable(); data.Add(0, 1);
using (var s = new UniversalSerializer("serialized.uniser"))
{
s.Serialize(data);
var data2 = s.Deserialize<Hashtable>();
}
就是这么简单!
带流的示例
var data = new Hashtable(); data.Add(0, 1);
using (var ms = new MemoryStream())
{
var s = new UniversalSerializer(ms);
s.Serialize(data);
var data2 = s.Deserialize<Hashtable>();
}
XML 示例
var data = new Hashtable(); data.Add(0, 1);
using (var ser = new UniversalSerializer("TestXmlFormatter.uniser.xml", SerializerFormatters.XmlSerializationFormatter))
{
ser.Serialize(data);
var deserialized = ser.Deserialize<Hashtable>();
}
JSON 示例
var data = new Hashtable(); data.Add(0, 1);
using (var ser = new UniversalSerializer("TestXmlFormatter.uniser.json", SerializerFormatters.JSONSerializationFormatter))
{
ser.Serialize(data);
var deserialized = ser.Deserialize<Hashtable>();
}
WPF 示例
var data = new System.Windows.Window() { Title = "Hello!" }; using (var s = new UniversalSerializerWPF("serialized.uniser")) { s.Serialize(data); var data2 = s.Deserialize<System.Windows.Window>(); }
Windows Forms 示例
有一个专门用于 Windows Forms 的 DLL,可以管理更多的 Windows Forms 类型。
var data = new System.Windows.Forms.Form() { Text = "Hello!" };
using (var s = new UniversalSerializerWinForm("serialized.uniser"))
{
s.Serialize(data);
var data2 = s.Deserialize<System.Windows.Forms.Form>();
}
文档
在归档文件中,在“Documentation”目录中,有这些文本:
- 介绍 [ index.html ]
- 示例
- 常见问题
- 解决方案和项目(DLL 文件)
- 最佳实践
- 属性
- 容器
- 过滤器。
- 执行错误
对于解决序列化问题以及区分三类错误非常重要。 - 历史(版本)
- 许可证
- 联系方式
与 .NET 序列化器可能性的比较
测试使用 基准测试 进行。
我无意与所有序列化器进行比较,它们太多了。我更侧重于技术和限制。
类条件 ↓ | UniversalSerializer | BinaryFormatter | DataContractSerializer | JavaScriptSerializer | Protobuf.net | SoapFormatter | XmlSerializer |
非作者编写(不可修改) | 是 | 否 | 是 | 是 | 否 | 否 | 是 |
字段 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
只读字段 | 是 | 是 | 是 | 是 | 是 | 是 | 否 |
属性 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
所有基本类型 | 是 | 是 | 是 | 否 | 是 | 是 | 是 |
在对象中 | 是 | 是 | 否 | 否 | 否 | 是 | 否 |
无默认构造函数 | 是 | 是 | 是 | 否 | 否 | 是 | 否 |
需要默认构造函数 | 是 | 否 | 否 | 是 | 是 | 否 | 是 |
需要参数化构造函数 | 是 | 否 | 否 | 否 | 否 | 否 | 否 |
参考 | 是 | 是 | 是 | 否 | 否 | 是 | 否 |
继承 | 是 | 是 | 否 | 否 | 否 | 是 | 否 |
循环引用 | 是 | 是 | 是 | 否 | 否 | 是 | 否 |
泛型列表中的循环引用 | 是 | 是 | 是 | 否 | 否 | 否 | 否 |
Generics | 是 | 是 | 是 | 是 | 是 | 否 | 是 |
泛型字典 | 是 | 否 | 否 | 否 | 否 | 否 | 否 |
复杂类型(WPF Window) | 是 | 否 | 否 | 否 | 否 | 否 | 否 |
详细说明
- 非作者编写(不可修改)
此类由其他作者编写,我们无法添加属性、接口,甚至修改它。 - 字段
序列化一个字段。 - 只读字段
C# 示例public readonly int i;
- 属性
一个自动属性及其隐藏字段。 - 所有基本类型
bool、DateTime、sbyte、byte、short、ushort、int、uint、long、ulong、Single、double、Decimal、char、string。 - 在对象中
主要数据被装箱到 Object 中。 - 无默认构造函数
仅具有参数化构造函数的类。 - 需要默认构造函数
需要通过其默认构造函数构造的类。 - 需要参数化构造函数
此类必须通过参数构造。默认(无参数)构造会导致数据损坏。 - 参考
测试实例重复。序列化器必须多次引用一个实例。 - 循环引用
一个类型包含与其自身相同的类型作为字段。 - 泛型列表中的循环引用
一个类型 T 包含 List<T> 作为字段。 - Generics
测试了一个泛型类。 - 泛型字典
此类继承 Dictionary<int, string> 并包含一个值。 - 复杂类型(WPF Window)
一个包含某些控件的 Window。
注释
- BinaryFormatter、DataContractSerializer 和 SoapFormatter 不考虑构造函数。
这可能导致数据损坏,例如当构造函数在静态列表中注册实例时。
有关性能测试,请阅读 本章。
为什么以及如何实现的?
我需要一个通用序列化器,能够序列化任何内容而无需修改。
但是现有的序列化器存在缺点:
- 一些需要特殊属性(BinaryFormatter, Protobuf.net)或接口。
- 其他则不考虑字段(sharpSerializer)。
- 引用是一个问题。通常实例会被复制,反序列化后的对象指向不同的实例。循环引用很少被管理。
- 据我所知,没有一个能管理无默认构造函数的参数构造函数。它们使用系统构造,可能产生副作用。
- 特定的 .NET 类需要更多关注,但它们是密封的,因此无法用序列化属性继承。
[ 更多详情请参阅 序列化器比较 ]
因此,解决方案需要一套机制和技术。我们将在接下来的章节中了解这些机制。
序列化无默认构造函数的类
在 .NET 中,默认(无参数)类构造函数不是强制性的。但几乎所有序列化器都需要它们。
并且它们在框架中很常见。
示例:System.Windows.Controls.<a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.uielementcollection.aspx">UIElementCollection</a>
。
一些序列化器使用FormatterServices.GetUninitializedObject,但通常当一个类具有参数化构造函数而没有默认构造函数时,参数化构造函数是正确初始化实例所必需的。
UniversalSerializer 中的解决方案是查找参数化构造函数,并在类中找到构造函数参数与字段之间的对应关系,即使这些字段是私有的。
在 UIElementCollection
中,我们有这个构造函数:
public UIElementCollection( UIElement visualParent, FrameworkElement logicalParent )
以及在同一类中可用的这些字段:
-
private readonly UIElement _visualParent;
-
private readonly FrameworkElement _logicalParent;
类型相等且名称非常接近。这足以让 UniversalSerializer 尝试使用这些值创建实例。并且它有效!
序列化非泛型 ICollection 类
出于某种模糊的原因,ICollection
接口不提供 Add 或 Insert 方法。换句话说,它定义了一个只读集合,与泛型的 ICollection<T>
相反。通常,我们无法反序列化实现 ICollection
而非 ICollection<T>
的类。
幸运的是,在现实世界中,几乎所有这些类都有一个 Add 或 Insert 方法,至少是用于内部使用的。UniversalSerializer 可以找到这些方法并使用它们来反序列化集合类实例。
重用现有机制并在容器(ITypeContainers)中进行管理
在某些情况下,使用现有的转码机制更有效或唯一可行。
当我尝试序列化 WPF 控件时,我发现:
[TypeConverter]
允许将对象转码为某些可序列化的类型(字符串、字节数组等)。示例:System.Windows.Input.Cursor
。[ValueSerializer]
允许与字符串进行双向转码。示例:System.Windows.Media.FontFamily
。[Serializable]
(和ISerializable
)允许使用BinaryFormatter
。示例:System.Uri
。
如果你考虑 FontFamily
,你会明白将其转码为字符串比尝试保存其属性要容易得多。而且更安全,因为设置属性可能会对未知或复杂的类产生不可预测的后果。
对于这些属性,我创建了 ITypeContainer
机制。一个容器将源实例替换为其转码值,通常是字符串或字节数组。容器可以应用于一组类型,例如所有兼容属性的类型。
示例和详细信息可在下方找到。
过滤器
某些类型需要特殊处理,这由一组过滤器完成。
类型验证器过滤器
此过滤器允许您阻止 UniversalSerializer 序列化某些有问题的类型。
例如,我遇到过一些使用 System.IntPtr
的类。序列化此类型只会导致问题,因为它们仅在类内部使用,即使它们存储在公共属性/字段中。
私有字段添加器过滤器
此过滤器指示序列化器将特定的私有字段添加到序列化数据中。
例如,System.Windows.Controls.<a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.panel.aspx">Panel</a>
需要 _uiElementCollection
来填充其 Children
属性,因为 Children
是只读的。通过过滤器,解决方案很简单。任何继承 Panel 的类型,例如 StackPanel,都将受益于此过滤器。
强制参数化构造函数类型
它不是过滤器,而是一个类型列表。当一个类型在此列表中时,UniversalSerializer 将忽略其默认(非参数化)构造函数并搜索参数化构造函数。示例:System.Windows.Forms.PropertyManager
。使用其参数化构造函数比为该类型编写 ITypeContainer
要容易得多。
CanTestDefaultConstructor(2.14.3 版本新增)
UniversalSerializer 通常会尝试使用默认构造函数(如果可用)为每种类型构建一个实例。问题是有些类型不应该在反序列化之外进行构造,例如当它们的构造函数会增加静态计数器时。此过滤器允许 UniversalSerializer 避免此构造函数测试。
DefaultConstructorTestCleaner(2.14.3 版本新增)
UniversalSerializer 通常会尝试使用默认构造函数(如果可用)为每种类型构建一个实例。有些类型,如 System.Windows.Window,需要在实例销毁前调用一个清理器。在 WPF Window 的示例中,我们必须调用 Close(),否则应用程序将无法正确关闭(它会等待所有 WPF 窗口关闭)。
引用
考虑这段代码:
var data = new System.Windows.Controls.TextBox[2];
data[0] = new System.Windows.Controls.TextBox() { Text = "TextBox1" };
data[1] = data[0]; // Same reference
using (var s = new UniversalSerializerWPF(@"d:\temp\serialized.bin"))
{
s.Serialize(data);
var data2 = s.Deserialize<System.Windows.Controls.TextBox[]>();
data2[0].Text = "New text"; // Affects the two references.
bool sameReference = object.ReferenceEquals(data2[0], data2[1]);
}
- UniversalSerializer 只序列化一个
TextBox
实例。 - 它只反序列化一个
TextBox
实例,并创建两个指向它的引用。证明:sameReference
为 true,并且两个引用的Text
相同。
自定义修改器:ITypeContainer 和过滤器
鉴于 UniversalSerializer 的主要目标是能够序列化任何类型,分享我们的经验至关重要。
因此,我尝试使容器和过滤器的创建尽可能容易,以帮助您使用它们并分享您的解决方案。
创建 ITypeContainer [ 3.14.5 版本修改 ]
目标是用一个简单的实例替换有问题的实例。通常,容器将包含非常简单的类型(string
、int
、byte
数组等)。
举个例子:
/// <summary>
/// . No default (no-param) constructor.
/// . The only constructor has a parameter with no corresponding field.
/// . ATextBox has a private 'set' and is different type from constructor's parameter.
/// </summary>
public class MyStrangeClassNeedsACustomContainer
{
/// <summary>
/// It is built from the constructor's parameter.
/// Since its 'set' method is not public, it will not be serialized directly.
/// </summary>
public TextBox ATextBox { get; private set; }
public MyStrangeClassNeedsACustomContainer(int NumberAsTitle)
{
this.ATextBox = new TextBox() { Text = NumberAsTitle.ToString() };
}
}
正如摘要中所述,此类对序列化器造成了一些困难。
为了克服这个问题,我们创建了一个容器:
class ContainerForMyStrangeClass : ITypeContainer
{
#region Here you add data to be serialized in place of the class instance
public int AnInteger; // We store the smallest, sufficient and necessary data.
#endregion Here you add data to be serialized in place of the class instance
public ITypeContainer CreateNewContainer(object ContainedObject)
{
MyStrangeClassNeedsACustomContainer sourceInstance = ContainedObject as MyStrangeClassNeedsACustomContainer;
return new ContainerForMyStrangeClass() { AnInteger = int.Parse(sourceInstance.ATextBox.Text) };
}
public object Deserialize()
{
return new MyStrangeClassNeedsACustomContainer(this.AnInteger);
}
public bool IsValidType(Type type)
{
return Tools.TypeIs(type, typeof(MyStrangeClassNeedsACustomContainer));
}
public bool ApplyEvenIfThereIsAValidConstructor
{
get { return false; }
}
public bool ApplyToStructures
{
get { return false; }
}
}
细节:所有方法都表现为静态方法(但并非如此),除了 Deserialize()
。
让我们更详细地看看它们:
-
public int AnInteger
它不属于ITypeContainer
接口。在这里,我们将存储稍后反序列化时需要的信息。 -
ITypeContainer CreateNewContainer(object ContainedObject)
在序列化过程中使用。这是一种用于此容器实例的构造函数。参数将是要序列化的源类实例。 -
object Deserialize()
在反序列化过程中使用。容器实例将产生一个新的实例,它是源实例的副本,使用我们的字段
AnInteger
。 -
bool IsValidType(Type type)
在序列化过程中使用。如果类型继承自源类型或就是源类型,则返回 true。这是一个过滤器。我们可以选择接受继承的类型或不接受,接受几个兼容的类型,等等。 -
bool ApplyEvenIfThereIsAValidConstructor
在序列化过程中使用。如果此容器应用于具有默认(无参数)构造函数的类类型,则返回 true。可用于非常通用的容器。 -
bool ApplyToStructures
在序列化过程中使用。如果此容器应用于结构类型,而不仅仅是类类型,则返回 true。可用于非常通用的容器。
步骤是:
- 序列化器检查源类型(
MyStrangeClassNeedsACustomerContainer
)是否由容器管理。我们的容器类(ContainerForMyStrangeClass
)通过IsValidType()
、ApplyEvenIfThereIsANoParamConstructor
和ApplyToStructures
回答“是”。 - 序列化器通过
CreateNewContainer()
构建我们的容器实例。CreateNewContainer
构建一个实例并设置其字段AnInteger
。 - 序列化器将此容器实例存储(序列化)在源实例的位置。
- 反序列化器检索(反序列化)容器实例。
- 反序列化器调用
Deserialize()
并获得源类实例的副本。Deserialize()
使用其字段AnInteger
创建此副本。
[ 3.14.5 版本新增 ] 我们声明了 CustomModifiers。
public class CustomContainerTestModifiers : CustomModifiers
{
public CustomContainerTestModifiers()
: base(Containers: new ITypeContainer[] {
new ContainerForMyStrangeClass() // ?-----
})
{
}
}
此声明将由反序列化器自动找到。[ 3.14.5 版本新增 ]
现在我们序列化它:[ 3.14.5 版本简化 ]
/* This example needs a custom ITypeContainer.
Normally, this class can not be serialized (see details in its source).
But thanks to this container, we can serialize the class as a small data (an integer).
*/
var data = new MyStrangeClassNeedsACustomContainer(123);
using (MemoryStream ms = new MemoryStream())
{
var p = new Parameters() { Stream = ms };
UniversalSerializer ser = new UniversalSerializer(p);
ser.Serialize(data);
var data2 = ser.Deserialize<MyStrangeClassNeedsACustomContainer>();
bool ok = data2.ATextBox.Text == "123";
}
如您所见,实现非常简单。
工具辅助函数
Tools 静态类提供了一些帮助。
-
Type Tools.TypeIs(Type ObjectType, Type SearchedType)
它等同于 C# 的 'is
',但用于 Types。例如,TypeIs((typeof(List<int>)
,typeof(List<>))
返回true
。 -
Type DerivedType(Type ObjectType, Type SearchedType)
返回
ObjectType
继承的SearchedType
对应的类型。例如,DerivedType(typeof(MyList), typeof(List<>))
在MyList
是时返回typeof(List<int>)
:MyList: List<int> { }.
-
FieldInfo FieldInfoFromName(Type t, string name)
返回类型的指定字段的FieldInfo
。我们将在下一章中使用它。
创建一组过滤器 [ 3.14.5 版本修改 ]
请注意,过滤器机制完全独立于 ITypeContainer
s。它们可以一起使用,也可以单独使用。
举个例子:
public class ThisClassNeedsFilters
{
public ShouldNotBeSerialized Useless;
private int Integer;
public string Value { get { return this.Integer.ToString(); } }
public ThisClassNeedsFilters()
{
}
public ThisClassNeedsFilters(int a)
{
this.Integer = a;
this.Useless = new ShouldNotBeSerialized();
}
}
public class ShouldNotBeSerialized
{
}
这个类(ThisClassNeedsFilters
)存在一些问题:
- 它包含一个
ShouldNotBeSerialized
。让我们假设ShouldNotBeSerialized
类因为某些原因需要避免,我不知道为什么,也许它被污染了! - 字段
Integer
不是公共的,因此被序列化器忽略。 - 甚至构造函数参数的名称也与任何字段或属性都不同。无论如何,序列化器不需要这个构造函数,因为它已经有一个默认构造函数。
为了解决这些问题,我们编写了一套自定义过滤器:
/// <summary>
/// Tells the serializer to add some certain private fields to store the type.
/// </summary>
static FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)
{
if (Tools.TypeIs(t, typeof(ThisClassNeedsFilters)))
return new FieldInfo[] { Tools.FieldInfoFromName(t, "Integer") };
return null;
}
/// <summary>
/// Returns 'false' if this type should not be serialized at all.
/// That will let the default value created by the constructor of its container class/structure.
/// </summary>
static bool MyTypeSerializationValidator(Type t)
{
return ! Tools.TypeIs(t, typeof(ShouldNotBeSerialized));
}
它们是不言自明的:
-
FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)
使序列化器将私有字段(Integer
)添加到此类型(ThisClassNeedsFilters
)的每个源实例中。 -
bool MyTypeSerializationValidator(Type t)
阻止序列化器存储此类型(ShouldNotBeSerialized
)的任何实例。因此,ThisClassNeedsFilters
的任何实例都不会设置Useless
字段。
[ 3.14.5 版本新增 ] 我们声明了 CustomModifiers。
public class CustomFiltersTestModifier : CustomModifiers
{
public CustomFiltersTestModifier()
: base(FilterSets : new FilterSet[] {
new FilterSet() {
AdditionalPrivateFieldsAdder=MyAdditionalPrivateFieldsAdder,
TypeSerializationValidator=MyTypeSerializationValidator } })
{
}
}
此声明将由反序列化器自动找到。[ 3.14.5 版本新增 ]
现在我们序列化它:[ 3.14.5 版本简化 ]
/* This example needs custom filters.
Normally, this class can be serialized but with wrong fields.
Thanks to these filters, we can serialize the class appropriately.
*/
using (MemoryStream ms = new MemoryStream())
{
var p = new Parameters() { Stream = ms };
var ser = new UniversalSerializer(p);
var data = new ThisClassNeedsFilters(123);
ser.Serialize(data);
var data2 = ser.Deserialize<ThisClassNeedsFilters>();
bool ok = data2.Value == "123" && data2.Useless == null;
}
实现比 ITypeContainer
更简单。
UniversalSerializer 的源代码
所有源代码都用 C# 编写。
解决方案使用 Visual Studio 2013 和 2017 编写(2015 未测试)。
一些项目可能需要 VS 2017、UWP、.NET Core、Xamarin、Android 模拟器。
但是主要的 .NET DLL 可以单独用 VS 2013 编译。
重要注意事项
- 版本控制:序列化/反序列化过程通过其完整名称(来自
type.AssemblyQualifiedName
)来标识类型。这种名称取决于类型的程序集版本,因此请注意框架和 DLL 的版本控制! - 文件格式:当前的流格式未来可能会发生变化。因此,DLL 名称中包含版本号。如果您序列化到文件,我建议您在文件名中添加版本号。
版本 1
第一个版本基于 fastBinaryJSON 并对其进行了扩展,使用修改器(容器和过滤器)来允许序列化许多类型。生成的源代码没有经过优化。它更像是一个原型,或概念验证,而不是一个设计良好的软件。它速度慢,消耗大量内存,并且产生大文件,但至少它能够序列化非常复杂的数据。
版本 2
新源代码
我编写了一个新的序列化器,实际上是一个新的软件,从零开始。源代码中不再包含 FastBinaryJSON 的任何内容。
与旧的基于 FastBinaryJSON 的版本 1 的区别是:
- 速度提高 70 倍,文件大小减小 100 倍,内存消耗减少 110 倍。根据基准测试(200k 项,5 轮,3 字节结构)测量。现在这个序列化器又快又高效。
- 序列化器使用流,而不是字节数组。目标是消耗非常少的内存,并尽可能快速地写入磁盘(或任何流目标)。一个重要的后果是我们可以直接序列化到压缩流,没有中间过程或结构。我认为这对于序列化非常大的数据或集合至关重要。
- 支持三种文件格式:自定义二进制、JSON 和 XML。您还可以使用 Formatter 接口创建自己的格式。
- 一种新的多路复用二进制格式。这种格式是针对 .NET 结构设计的,旨在简化、加速和减小序列化数据的尺寸。这种格式整合了集合、字典、属性、字段、实例和引用的概念。类型描述符和对象值在同一个流中进行多路复用。请注意,此格式与 FastBinaryJSON 的格式没有任何共同之处,因此与 UniversalSerializer 版本 1 不兼容。
- 与版本 1 相比的一些改进:
- 参数化构造函数现在对结构(例如
KeyValuePair<,>
)具有优先于容器的地位。 Nullable<T>
被正确管理。- 整数可以选择性地压缩为 7 位可变长度。请注意,未压缩的整数有时可以通过外部压缩器(如 WinRar)进行更好的压缩。
- 参数化构造函数现在对结构(例如
新许可证
由于代码是完全原创的,与版本 1 不同,我得以更改许可证。我选择了 Ms-RL,这是一个允许您在任何项目中使用源代码的许可证。唯一值得注意的条件是,如果您修改了源文件,您必须公开此修改(仅限于您修改的文件,而不是任何其他文件)。我希望这将有助于分享对许多人有用的容器和改进。
与版本 1 的文件兼容性
由于格式完全不同,因此没有任何兼容性。如果您想读取旧格式的数据,我建议您引用旧的 "UniversalSerializer1.dll"。版本 1 和 2 的 DLL 可以链接到同一个应用程序,因为它们使用不同的命名空间。
将您的源代码适配到新 API
与版本 1 的 API 相比,有一些区别:
- 命名空间现在是 UniversalSerializer2。
- 序列化器现在使用流。
- 现在您必须首先创建一个 UniversalSerializer 的新实例,然后调用 Serialize 和/或
Deserialize()
。 ITypeContainer.ApplyEvenIfThereIsANoParamConstructor
已更改为ITypeContainer.ApplyEvenIfThereIsAValidConstructor
。
版本 3
文件流格式现在包含一个修改器程序集列表。这样,反序列化器就能始终知道序列化时使用了哪些修改器。
流中的这个重要信息是修改器声明和使用方式的结构化修改的原因。请阅读相应上方章节中的示例。
性能
在开发过程中,我需要将此序列化器与现有序列化器进行比较,以消除我代码中可能存在的弱点。我创建了一个简单的基准测试,用于计算资源需求:处理器、流和 RAM。尽管基准测试可以无限期地讨论,因为它们远非完美并且依赖于许多参数,但它们很有用。我们只需要记住它们不是非常精确。
我们将要序列化和反序列化的数据是包含 3 个字节的小结构体数组。该数组包含 200,000 个此结构的项,我们序列化和反序列化 5 次。请阅读下面的章节,了解有关测试条件的更多详细信息。
注释
- 这个基准测试可以序列化更复杂的类型和场景。您可以在 UI 中选择它们。
- 本章(是的,您正在阅读的就是本章)有点老了,来自 2015 年和版本 2。但我相信从那时起情况没有太大变化。
处理器
UniversalSerializer 列出了 4 次:二进制格式、JSON 格式、XML 格式和旧版本 1。所有序列化器中最慢的是 UniversalSerializer 的旧版本 1.0,它基于 FastBinaryJSON,但增加了大量的资源浪费(抱歉!)。
文件长度
生成的文件的长度可能对您的项目很重要。因此,我努力减小数据浪费。实际上,UniversalSerializer 试图在其结构中消除所有多余的数据。在这个例子中,一个完美的未压缩文件长度将是 600,000 字节。UniversalSerializer 仅超出 338 字节,这是由于类型描述符。
Protobuf-net 有点特殊,因为它似乎进行了一种压缩。当使用另一个包含 3 个 Int32(而不是 3 个字节)的数据结构时,这一点更为明显:Protobuf-net 产生相同的文件长度:每个结构大约 8.8 字节。供参考,默认情况下,UniversalSerializer 压缩整数(除字节外)使用 7 位压缩方案,并在序列化 3 个 Int32 结构体数组时生成 897,750 字节的文件(平均每个结构 4.5 字节)。此压缩可以关闭。
旧的 UniversalSerializer 版本 1 继续显得很可笑。我将它保留在图中,太有趣了!
RAM
衡量 RAM 消耗是一项艰巨且不确定的工作。在这里,我使用 System.GC.GetTotalMemory(..)
展示了 GC 内存消耗。应该注意的是,使用 System.Diagnostics.Process.GetCurrentProcess().WorkingSet64
会得到不同的数字。但在多次尝试后,GC 方法似乎更稳定和有意义。
我在 UniversalSerializer 中付出了特别的努力来减少 RAM 消耗。结果似乎反映了这些努力。再说一遍,旧的 UniversalSerializer 版本 1 产生天文数字,消耗约 700MB RAM。真可笑!这比新版本 2 的消耗高 110 倍。这让我发笑。;)
我在这里学到的一个重要教训是:我们序列化的数组越大,序列化器消耗的资源就越多,实际上比源数据的大小还要多得多。这个教训似乎对所有序列化器都有效(是的,包括 UniversalSerializer)。
整体资源
在这里,我们尝试评估每个序列化器的整体资源消耗。数字是基于 UniversalSerializer(二进制)资源消耗的百分比。Total 数字是三个资源百分比的平均值。例如,Protobuf-net:时间=137%,文件长度=277%,RAM=208%,平均值=(137+277+208)/3=207 %。
通常的笑话是旧的 UniversalSerializer 版本 1 的总资源消耗:9,187 %。比大象还大!对于图来说太大了,我不得不将其剪掉。;)
结论
我的个人结论是,我达到了我的目标:创建一个节省资源的序列化器,让我们能够序列化各种类型的对象。XML 和 JSON 格式化程序消耗的资源太多,但我知道通过替换当前使用的 .NET 类,我可以大大改进它们。
测试条件
有关此测试条件的详细信息。
基准测试项目源代码可以在 UniversalSerializer 的主源代码归档中找到。这允许您检查它并告诉我您是否认为我的方法有问题。
此应用程序的截图
注意:我修复了消息“Computation terminated”。现在它是“Computation completed”。这张截图有点旧了。;)
我们在示例中序列化的结构
public struct MyByteColor
{
public byte R;
public byte G;
public byte B;
}
请注意,基准测试中可以测试其他类型。特别是 WPF 的 Window:一组非常复杂的类型。
需要考虑的几点:
- 测试只运行一次(以避免预缓存)。注意:第二次运行 UniverslSerializer 会快得多,因为所有类型分析都已经完成。但我没有作弊,这些图表仅显示第一次运行。:)
- Windows 配置为无虚拟内存文件,以避免在序列化器需要大量内存时访问磁盘。
- 文件流写入 RAM 磁盘(以避免访问磁盘)。此 RAM 磁盘格式化为 FAT-32,以避免索引或其他干扰访问。
- GC 内存通过另一个线程中的计时器连续(每 60 毫秒)测量,以获得其峰值而不是仅最终值。
历史:所有发布说明
版本 3.18.3.14,2018-03-30
- UniversalSerializer_3.18.3.14_E839F19AF67436EA0C4C5EF0FDB2647C2BCDF74D47DCB39592D58463DC86EA82.rar
- UniversalSerializer_3.18.3.14_6B1E085392256A5A38FD0229FFA13F032E67A4BDECF68223BE9880D385F0110D.zip
- 新增:.NET Core 2 的库。
- 新增:UWP(通用 Windows 平台)的库。
- 新增:Android(Mono.Android 和 Xamarin Forms)的两个库。
- 新增:.NET Standard 2 的库(在 Xamarin Forms 上测试)。
- 新增 UniversalSerializer 的类构造函数。
- 新增:UniversalSerializer 类现在是线程安全的。
- 新增:每个库都有一个带有用户界面的 Tester 应用程序。
- 新增:Guid 可以被序列化,这要归功于新的 GuidContainer。
- 改进:与 Linux 的兼容性(在 Mono 和 .NET Core 2 上)。
- 改进:文件格式更具可移植性。添加了一个 Nullable<T> 的可移植容器。
注意:使用先前版本序列化的文件可能不兼容。 - 改进:UniversalSerializerResourceTests.ResourceCounter 的兼容性。
它不再使用 Win32 方法(p/invoke)。 - 改进:容器(
ITypeContainer
)现在可以是结构。 - 修改:源代码目录组织,以及许多可执行文件的名称。
- 修改:支持的 Visual Studio 版本是 2013 和 2017。
其他版本未测试或不支持。 - 已修复:.NET 3.5 框架上的
Nullable<T>
。 - 已修复:可以反序列化只读流。
- 已修复:接口作为项目类型的泛型集合。
- 已移除:Windows 应用商店框架(Windows Runtime 8 和 8.1)的实验,转而支持 UWP。
注意:源代码中可能仍包含对此框架的提及。 - 已移除:PCL(可移植类库)的实验,转而支持 UWP、.NET Core 和 .NET Standard 库。
注意:源代码中可能仍包含对此框架的提及。 - 已移除:Windows Phone 7.1 和 8 上的 Silverlight。
注意 1:源代码中可能仍包含对此框架的提及。
注意 2:仍然有一个用于 Windows 上的 Silverlight 的库。
版本 3.15.3.13,2015-03-25
- 改进:参数化构造函数可以使用继承的字段。
这使得更多类型可序列化。 - 已更正:添加了 Nullable<T>.hasValue。
请注意,这可能会导致此版本的 UniversalSerializer 无法读取使用先前版本序列化的文件。
版本 3.15.1.12,2015-03-11
- 已更正:ParsedAssemblyQualifiedName 中的数组。
- 已更正:参数化构造类中的循环类型现在可以工作(除一些罕见情况外)。
- 已更正:泛型字典的循环类型现在可以工作。
- 已更正:删除了无用的代码。
版本 3.14.10.11,2014-10-14
- 新增:文档中的错误描述。
- 新增:文档中的“最佳实践”。
- 改进:错误编号与文档关联。
- 改进:.NET 中不正确的外部转换器不再阻止容器。
- 新增:现在可以处理更多 WPF 类型。
- 新增:为 .NET 3.5 分离了 WinForm 测试解决方案。
- 改进:System.Windows.Media 中的更多类型现在可以被序列化。
- 已更正:.NET 3.5 的主 DLL 在纯 .NET 3.5 解决方案中存在问题。
版本 3.14.9.10,2014-09-10
- 改进:类型分析速度。
- 已更正:“ForceSerializeAttribute”在字段上未正确工作。
- 已更正:在 Tester 解决方案中,“ForceSerializeOnPrivateFieldAndProperty.ForcedPrivateField”不是一个字段。(哎呀!)
版本 3.14.7.9,2014-07-25
- 新增:TypeMismatchException,反序列化后类型转换错误。
- 新增:当循环反序列化时,最后会发送 EndOfStreamException。
- 新增:在 Benchmark 中添加了许多新的测试结构。
- 已更正:通过 ForceSerialize 标记的私有属性现在可以被序列化。
- 已更正:Benchmark 中 JavascriptSerializer 的循环序列化测试中的异常。
- 已更正:Windows 8 的 PCL 现在具有新属性。
- 已更改:版本编号为:Main.Year-2000.Month.Release#。
版本 3.14.6.2,2014-06-10
- 新增“Documentation”目录,包含一些 HTML 文本。
- 新增属性 "ForceSerializeAttribute" 和 "ForceNotSerializeAttribute"。
- 新增:考虑 "EditorBrowsableAttribute" 以不序列化字段或属性。这有助于 Windows Forms。
- 已更正:在某些条件下使用过滤器时的类型分析问题。
版本 3.14.6,2014-06-04
- 新增:基准测试生成的表中新增了列:“Bytes/ms”、“Data/file lengths”、“data length/GC memory”、“data length/Working set”。
- 修改:基准测试 UI 的微小改动。
- 改进:CLRBinaryFormatterContainer 中的异常消息。
- 新增:Tester 解决方案中的测试系列 'DifficultiesTests()'。
- 新增解决方案 "UniversalSerializer Lib .NET 4.5",针对 .NET 4.5 进行了优化,使用 "AggressiveInlining"。
- 新增解决方案 "UniversalSerializer Lib .NET 3.5",优化程度低于 .NET 4.x 的解决方案,但可能需要。
- 已更正:.NET 4.0 解决方案 "UniversalSerializer Lib" 不再包含 "AggressiveInlining" 属性。不再与未安装 .NET 4.5 的计算机不兼容。
- 已更正:在基准测试中,DataContractSerializer 现在可以序列化实例和循环引用。
版本 3.14.5.2 revision 2,2014-05-16
- 一些更正。
版本 3.14.5.2,2014-05-13
- 已更正:循环类型问题。
- 新增:Test 解决方案中的新类型。
- 新增:Benchmark 中的新类型。
版本 3.14.5,2014-05-05
- 修改:API 和流格式为版本 3.0。
- 修改:命名空间重命名。UniversalSerializerLib2 -> UniversalSerializerLib3。
- 修改:修改器(过滤器和容器)现在声明为类,并由序列化器自动查找。
- 新增:选项 Parameters.ModifiersAssemblies 用于声明定义修改器的程序集。当序列化器无法自动找到修改器时很有用。
- 修改:流格式版本现在是 3.0。旧版 UniversalSerializer(版本 2.x)的 DLL 无法读取它。
- 改进:在 DateTime 中保存 DateTime.Kind。
- 改进:Benchmark 的界面稍作澄清。
- 改进:所有项目都通过 VS 的代码分析,没有错误。
- 新增:测试结构(如 PrimitiveValueTypesStructure)。
- 新增:在 IDE 中记录警告。
- 新增:解决方案 "UniversalSerializer Windows Store 8 PCL"。
- 新增:解决方案 "UniversalSerializer Windows Phone 7.1 experimental"。不完美,但可能有用。
- 移除:选项 CustomModifiers.DoNotDuplicateStrings。字符串始终作为引用保存,在流格式 3.0 中。
- 移除:Benchmark 解决方案中的 FastJSON 和 FastBinaryJSON,因为它们无法通过新的反序列化类型检查。
- 移除:Benchmark 解决方案中的旧 UniversalSerializer 版本 1。
- 已更正:Benchmark 中的一些回归。
- 注意:DLL 版本 3 可以读取和修改版本 2 的流,但不能读取版本 1 的流。
版本 2.14.3,2014-03-17
- 新过滤器:CanTestDefaultConstructor。
- 新过滤器:DefaultConstructorTestCleaner。对 WPF 的 System.Windows.Window 有用。
- 新容器:DependencyPropertyContainer。使用正确的静态 DependencyProperty。
- 改进:基准测试在文件路径不存在时会告知。
- 改进:CLRTypeConverterContainer.cs 先检查类型转换能力。
- 修改:基准测试现在是 64 位,就像现在大多数已安装的 Windows 一样。
- 已修复:Windows Phone 8 的测试解决方案现在工作良好。
- 已修复:`public readonly` 字段作为构造函数参数的问题。
- 已修复:源代码中的一些翻译。
- 注意:文件格式与版本 2.0 兼容。
版本 2.0,2013-10-09
- 全新序列化器。所有代码都是新的,没有来自版本 1.0 的代码。
- 速度提高 70 倍,文件大小减小 100 倍,内存消耗比旧版本 1 减少 110 倍。
- 注意:文件格式与版本 1.0 没有任何共同之处。DLL 版本 2.0 无法读取用版本 1.0 编写的文件。
版本 1.0,2013-07-13
此版本基于 FastBinaryJSON,并进行了许多扩展和修改。
未来
我想改进一些方面:
- 添加更多容器和过滤器。您可以帮助我。当类型未正确序列化时告诉我。并且请分享您创建的修改器(容器和过滤器)。
让序列化器工作更轻松
这次经历教会了我为什么序列化器在处理某些类型时会遇到困难,以及当我们编写一个类时如何才能使它们的工作更轻松。
- 如果可能,请编写一个默认(无参数)构造函数。序列化器将从其字段和属性重构实例。
- 如果无法编写默认(无参数)构造函数,请编写一个公共参数化构造函数以及相应的私有字段。
字段应具有相同的类型和名称。使用 UniversalSerializer,名称可以略有不同:“MyParam” → myParam” 或 “_myParam” 或 “_MyParam”。 - 实现一个
ValueSerializerAttribute
或TypeConverterAttribute
,当实例可以从字符串等简单内容构造时。
特别是当您的类包含许多公共优化字段或属性时。例如,FontFamily
可以从一个简单的字符串构造,无论它包含多少其他信息,它们都来自这个简单的字符串。我不知道许多序列化器是否考虑了这些属性,但至少 UniversalSerializer 会。 - 所有优化(如缓存)字段或属性都应设为私有(如果可能)。参见上文。
- 当您创建一个对象集合类时,请实现 IList。因为
ICollection
不允许向集合中添加项目。 - 当您创建一个泛型集合类时,请实现
ICollection<>
。因为IEnumerable<>
不允许向集合中添加项目。
致谢
- 我感谢 Mehdi Gholam 的 fastBinaryJSON。我从阅读他的代码中学到了有趣的东西。感谢您分享它。
- 也感谢 Mono 团队,我使用了这个优秀框架的两个函数。
- 我还要感谢 Protobuf-net 团队,不仅因为我使用了他们一项工作的功能,还因为他们的序列化器迫使我也改进了我的。这是一场有益的竞争。:)
注意:UniversalSerializer 并不直接派生自 Protobuf-net,除了这一个微小功能。
以前的版本
- 下载 UniversalSerializer3.15.3.13.zip - 710.8 KB
(sha-256 : 232B6024002D37DDC30EC8EC41CD24C5470BD64C04D4E0240F46370AA64D7756) - 下载 UniversalSerializer3.15.1.12.zip - 708.9 KB
(sha-256 : 12BDCE2AC3B5DADCBAD3D65CCD1DC11D910078E1F39071A2F79B25360B7C1E60) - 下载 UniversalSerializer3.14.10.11.zip - 706.2 KB
(sha-256 : 453F399A0D5021F68F94284276134B233CE57CA12766E7CE13569747FB915B2B) - 下载 UniversalSerializer3.14.9.10.zip - 675.3 KB
(sha-256 : 4A9BEC28B14522383C2D1B63246B6A74C951944EC7A71C9B6ED62EE1C8220182) - 下载 UniversalSerializer3.14.7.9.zip - 675.1 KB
- 下载 UniversalSerializer3.14.6.2.zip - 672.1 KB
- 下载 UniversalSerializer3.14.5.2.zip - 652.6 KB
- 下载 universalserializer2.14.3.zip - 583.1 KB
- 下载 universalserializer2.00.zip - 554.9 KB
- 下载 UniversalSerializer_1.0.zip - 139.5 KB
常见问题解答
注意:完整且更新的常见问题解答在 官方网站。
- 问:所有错误(异常和调试器消息)
答:请阅读文本 "Documentation\Errors.html",这应该能帮助您很多。 - 问:编译期间出现错误“找不到类型或命名空间 UniversalSerializer3”。
答:请确保您的项目与 DLL 的框架版本相同。
如何报告错误、困难或问题
如果您收到错误编号,您可以在源代码归档中的 "Documentation\Errors.html" 中找到更多详细信息。
由于序列化器的特殊性,您必须区分您遇到的错误或问题的类型。
存在 3 种截然不同的错误类型:
- 您设计了一个类型,使其与 UniversalSerializer 不兼容。
请阅读 最佳实践,这可能有助于您调整您的类型。
如果类型来自外部 DLL,请告知其作者关于 UniversalSerializer 的情况。 - 一个框架(.NET、Silverlight、Mono 等)类型未被正确管理。
这可能需要对 UniversalSerializer 进行调整。我不认为这是真正的错误,它只意味着一个标准类型需要更多考虑(创建 过滤器 和/或 容器)。 - UniversalSerializer 中存在一般性问题。
这将使我能够纠正一个可能影响许多程序员的问题。
一般来说,第一类问题可以由您自己解决。
第二类问题应由我解决,因为每个程序员都将受益于改进。尽管您可以自己解决,然后告知我。
第三类问题需要我的关注。
对于第三类问题和其他问题,您可以在 GitHub 上创建一个 issue。
建议
- 如果出现问题,将 DLL 项目添加到自己的解决方案中(而不是仅链接编译后的 DLL 文件),调试会更容易。
- 尽可能简化您的类,直到确定有问题的确切数据。
- 内部异常通常提供比上层异常更有用的信息。
(1) Apple:某些库是为 macOS 和 iOS 编译的。但我没有在这些系统上测试过它们(我没有这些硬件)。
您可以通过在这些平台上测试库来提供帮助。