65.9K
CodeProject 正在变化。 阅读更多。
Home

UniversalSerializer

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (105投票s)

2013年7月13日

Ms-RL

31分钟阅读

viewsIcon

306067

downloadIcon

4570

.NET 和 .NET Core 的通用且易用的序列化库。

官方网站是 universalserializer.com
并且有 NuGet 包可供使用
我也会尽量保持本文档的更新。

它是什么?

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 示例

有一个专门用于 WPF 的 DLL,可以管理更多的 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]);
}
  1. UniversalSerializer 只序列化一个 TextBox 实例。
  2. 它只反序列化一个 TextBox 实例,并创建两个指向它的引用。证明:sameReference 为 true,并且两个引用的 Text 相同。

自定义修改器:ITypeContainer 和过滤器

鉴于 UniversalSerializer 的主要目标是能够序列化任何类型,分享我们的经验至关重要。

因此,我尝试使容器和过滤器的创建尽可能容易,以帮助您使用它们并分享您的解决方案。

创建 ITypeContainer [ 3.14.5 版本修改 ]

目标是用一个简单的实例替换有问题的实例。通常,容器将包含非常简单的类型(stringintbyte 数组等)。

举个例子:

/// <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。可用于非常通用的容器。

步骤是:

  1. 序列化器检查源类型(MyStrangeClassNeedsACustomerContainer)是否由容器管理。我们的容器类(ContainerForMyStrangeClass)通过 IsValidType()ApplyEvenIfThereIsANoParamConstructorApplyToStructures 回答“是”。
  2. 序列化器通过 CreateNewContainer() 构建我们的容器实例。CreateNewContainer 构建一个实例并设置其字段 AnInteger
  3. 序列化器将此容器实例存储(序列化)在源实例的位置。
  4. 序列化器检索(反序列化)容器实例。
  5. 反序列化器调用 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 版本修改 ]

请注意,过滤器机制完全独立于 ITypeContainers。它们可以一起使用,也可以单独使用。

举个例子:

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 解决方案中的 FastJSONFastBinaryJSON,因为它们无法通过新的反序列化类型检查。
  • 移除: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,并进行了许多扩展和修改。

未来

我想改进一些方面:

  • 添加更多容器和过滤器。您可以帮助我。当类型未正确序列化时告诉我。并且请分享您创建的修改器(容器和过滤器)。

让序列化器工作更轻松

这次经历教会了我为什么序列化器在处理某些类型时会遇到困难,以及当我们编写一个类时如何才能使它们的工作更轻松。

  1. 如果可能,请编写一个默认(无参数)构造函数。序列化器将从其字段和属性重构实例。
  2. 如果无法编写默认(无参数)构造函数,请编写一个公共参数化构造函数以及相应的私有字段。
    字段应具有相同的类型和名称。使用 UniversalSerializer,名称可以略有不同:“MyParam” → myParam” 或 “_myParam” 或 “_MyParam”。
  3. 实现一个 ValueSerializerAttributeTypeConverterAttribute,当实例可以从字符串等简单内容构造时。
    特别是当您的类包含许多公共优化字段或属性时。例如,FontFamily 可以从一个简单的字符串构造,无论它包含多少其他信息,它们都来自这个简单的字符串。我不知道许多序列化器是否考虑了这些属性,但至少 UniversalSerializer 会。
  4. 所有优化(如缓存)字段或属性都应设为私有(如果可能)。参见上文。
  5. 当您创建一个对象集合类时,请实现 IList。因为 ICollection 不允许向集合中添加项目。
  6. 当您创建一个泛型集合类时,请实现 ICollection<>。因为 IEnumerable<> 不允许向集合中添加项目。

致谢

  • 我感谢 Mehdi Gholam 的 fastBinaryJSON。我从阅读他的代码中学到了有趣的东西。感谢您分享它。
  • 也感谢 Mono 团队,我使用了这个优秀框架的两个函数。
  • 我还要感谢 Protobuf-net 团队,不仅因为我使用了他们一项工作的功能,还因为他们的序列化器迫使我也改进了我的。这是一场有益的竞争。:)
    注意:UniversalSerializer 并不直接派生自 Protobuf-net,除了这一个微小功能。

以前的版本

常见问题解答

注意:完整且更新的常见问题解答在 官方网站

  • 问:所有错误(异常和调试器消息)
    答:请阅读文本 "Documentation\Errors.html",这应该能帮助您很多。
  • 问:编译期间出现错误“找不到类型或命名空间 UniversalSerializer3”。
    答:请确保您的项目与 DLL 的框架版本相同。

如何报告错误、困难或问题

如果您收到错误编号,您可以在源代码归档中的 "Documentation\Errors.html" 中找到更多详细信息。

由于序列化器的特殊性,您必须区分您遇到的错误或问题的类型。
存在 3 种截然不同的错误类型:

  1. 您设计了一个类型,使其与 UniversalSerializer 不兼容。
    请阅读 最佳实践,这可能有助于您调整您的类型。
    如果类型来自外部 DLL,请告知其作者关于 UniversalSerializer 的情况。
  2. 一个框架(.NET、Silverlight、Mono 等)类型未被正确管理。
    这可能需要对 UniversalSerializer 进行调整。我不认为这是真正的错误,它只意味着一个标准类型需要更多考虑(创建 过滤器 和/或 容器)。
  3. UniversalSerializer 中存在一般性问题。
    这将使我能够纠正一个可能影响许多程序员的问题。

一般来说,第一类问题可以由您自己解决。
第二类问题应由我解决,因为每个程序员都将受益于改进。尽管您可以自己解决,然后告知我。
第三类问题需要我的关注。

对于第三类问题和其他问题,您可以在 GitHub 上创建一个 issue。

建议

  • 如果出现问题,将 DLL 项目添加到自己的解决方案中(而不是仅链接编译后的 DLL 文件),调试会更容易。
  • 尽可能简化您的类,直到确定有问题的确切数据。
  • 内部异常通常提供比上层异常更有用的信息。

 

(1) Apple:某些库是为 macOS 和 iOS 编译的。但我没有在这些系统上测试过它们(我没有这些硬件)。
您可以通过在这些平台上测试库来提供帮助。

© . All rights reserved.