创建自定义的序列化/反序列化器






4.64/5 (8投票s)
在本文中,我们将介绍序列化/反序列化的基本概念以及如何创建一个非常基础的序列化/反序列化器。
介绍
简而言之,序列化是一种将对象或数据结构转换为一种格式,以便以后能够轻松地检索某些数据(大多数情况下是在另一个计算机环境中)。
目录
理解序列化和反序列化
过去,有人问我是否可以用现实生活中的例子来解释序列化到底是什么。这个挑战确实很有趣;然而,我不得不设想一个能够解释序列化/反序列化一般性想法的场景。
*注意:括号中的每一句话都指代计算机科学领域中特定事物的等价物:在本例中是数据存储的上下文。
现在讲故事,假设你路过一台正在出售的电脑,它吸引了你的注意;然而,你决定问你那位懂电脑的朋友,买那台电脑是不是一个聪明的想法。自然,把电脑带回家是不可能的,因此,你必须记下电脑的配置,以便稍后给你的朋友看(这就是序列化)。一旦你把记下的配置给你朋友看,他就会读他拿到的配置(这就是反序列化)。最终,他会决定买那台电脑是否是一个明智的想法(这是基于反序列化数据所做出的决定)。
现在你可以安全地将下面的图与上面的图进行比较——你会注意到在序列化的抽象概念上存在相似之处。
*注意:请注意,如果你的朋友只会说一种语言,而你写的是另一种语言。他肯定无法理解你写下的信息。
示例:他只会说英语,而你用法语描述电脑。
摘要
- 序列化是描述对象以便其他方能够理解的一种方式。
- 反序列化是读取和理解特定对象描述的一种方式。
- 如果你能够理解对象的描述,你就可以对它进行操作。
现在你已经对序列化有了大致的了解,我们可以迅速进入讨论序列化、反序列化以及如何创建序列化器。
序列化
基于我们之前对序列化工作方式的理解,我们可以轻松得出结论:序列化是通过其属性值来描述对象的一种方式。这是想象序列化最简单的方式。如果你是初学者并且想让自己生活更困难,可以深入挖掘。
首先,为了正确理解序列化,我们将尝试处理一个现实生活中的例子。
假设我们有一个结构/类,通过姓名、积分、文章数量和等级来描述一个 CodeProject 用户。
class Member
{
string Name{get;set;}
double Points{get;set;}
int Articles{get;set;}
string Rank{get;set;}
}
和一个成员
Member member = new Member()
{
Name = "Alaa Ben Fatma",
Points = 16.200,
Articles = 8,
Rank = "Legend"
};
说完这些,想象一下如果我们想在以下几种不同的场景中再次使用这个member
对象的信息
- 当你重新启动应用程序时
- 当你运行应用程序的两个实例,并且希望将自定义信息集传递并被这两个实例使用时。
- 当你在一台不同的机器上运行应用程序时
听起来很棒,不是吗?
可悲的是,如果没有一组信息可以操作,我们就无法处理上述任何一种场景;然而,我们不能原封不动地传递对象(member
)给另一个应用程序——我们应该传递一些关键细节,这些细节最终可以被应用程序的其他实例读取(这是本文的反序列化部分,稍后我们会详细介绍)。
说了这么多,所有这一切背后的想法就是将对象的属性值保存在文件或内存缓冲区中。要实现这一壮举,我们需要检索对象的所有属性并提取其值。结果,我们将得到一组反映对象本身的信息。
如果我们序列化上面声明的member
对象,提取的数据可能看起来像这样:
<Member>
<Name>Alaa Ben Fatma</Name>
<Points>16.200</Points>
<Articles>8</Articles>
<Rank>Legend</Rank>
</Member>
这是 XML 代码,请查看 此链接 了解更多关于它的信息。
反序列化
在上一章中,我们讨论了序列化过程是如何抽象工作的。因此,现在我们已经保存了可以使用的数据。
首先,我在现实生活中的例子中已经提到过,如果第二方不理解某种语言,那么它将无法理解用该语言写的任何东西。在我之前的例子中,我们得到了一个用XML编写的代码样本;然而,如果应用程序无法反序列化用 XML 编写的信息集,那么我们就无法有效利用保存的数据。因此,暂时我不会深入技术细节。
为了有效利用保存的数据,我们的应用程序必须能够理解传递给它的信息。
示例
在这种情况下,当我们反序列化数据时,如果对象的属性名称与对象序列化信息中的元素名称匹配,我们将处于有利地位;然而,在某些情况下,这可能不足以确保良好的反序列化。例如,我们可能有一个数值,表示为 999.36.257.3,而宿主属性无法接受这种格式,那么操作将走向一个晦涩的结局并最终失败。
根据我上面提到的内容,我们发现元素和属性的名称不匹配,类名与序列化对象的名称也不匹配。在这种情况下,我们只剩下一个答案:无法进行反序列化。
反序列化数据集意味着我们需要提取该集合中每个元素包含的值,然后将提取的值分配给与我们提取数据从中提取的元素同名的属性。更不用说我们必须遵守属性的类型;为了做到这一点,我们需要在需要转换时进行值转换。太长不看?这里有一张图可以帮助你更好地看待这种情况
现在,让我们回顾一下上面提到的现实生活场景,在这个层面(反序列化层面),你的朋友已经收到了写好的笔记并试图理解它们。这里也一样,总会有一个反序列化器会读取保存的信息并尝试理解它,然后将其值发送到合适的位置。
制作自定义的序列化/反序列化器
将用于本节所有部分的类
private class Pet
{
public string Type { get; set; }
public string Name { get; set; }
public string Age { get; set; }
public double Weight { get; set; }
}
创建它的一个实例
var pet = new Pet();
诚然,在直接开始繁重的工作之前,有一些需要考虑的点
- 序列化/反序列化器不知道对象的名称。
- 致命后果:序列化器如何为序列化后的信息集选择一个合适的名称。
- 序列化/反序列化器不知道对象拥有的属性的类型。
- 序列化/反序列化器在反序列化数据集的操作中进行类型转换时会遇到问题。
- 序列化/反序列化器不知道对象拥有的属性的名称。
- 因此,序列化/反序列化器将无法访问未知属性的值。
为了克服这些困难,我们需要利用反射,它将使我们能够在运行时检索类型的元数据。
制作一个序列化器
要制作一个序列化器,我们需要确保我们已经检索到了对象的名称以及对象的所有属性及其值。
首先要做的是获取我们计划序列化的对象的类型。
var myType = obj.GetType();
运行此代码将返回一个变量myType
,它反映了我们计划序列化的对象pet
的类型。
在找到我们的对象名称和类型后(名称可以从类型本身提取),我们将遍历其所有属性并将它们添加到列表中。
IList<PropertyInfo> props = new List<PropertyInfo>(myType.GetProperties());
现在我们终于成功提取了与对象及其属性相关的每一个关键信息,是时候编写我们的全基础序列化器了,它最终将生成一段可以到处共享并被任何能够反序列化它的应用程序使用的代码。
public static string Serialize(object obj)
{
var sb = new StringBuilder();
sb.AppendLine();
sb.Append("<?");
var myType = obj.GetType();
IList<PropertyInfo> props = new List<PropertyInfo>(myType.GetProperties());
foreach (var prop in props)
{
var propValue = prop.GetValue(obj, null);
sb.AppendLine();
sb.Append(@" [" + prop.Name + "=" + propValue + "]");
}
sb.AppendLine();
sb.Append("?>");
return sb.ToString();
}
序列化测试
pet.Type = "Dragon";
pet.Name = "SkyCloud";
pet.Age = "9200 years";
pet.Weight = 9652.6500;
通过实现上面提到的序列化算法,我们将得到这个最终结果
<?
[Type=Dragon]
[Name=SkyCloud]
[Age=9200 years]
[Weight=9562.6500]
?>
制作一个反序列化器
正如我们之前所见,反序列化就是读取数据集
、理解它、提取其中包含的值,然后将它们分配给匹配的属性。
每个现有的反序列化器都有能力反序列化关于对象的已保存信息集,前提是序列化格式对它来说是可理解的。话虽如此,我们的反序列化器将无法反序列化已序列化为 XML 格式的对象——只有当其语法可以被它操作时,它才会执行反序列化操作。还记得上面那个例子吗,我说如果你用法语写电脑配置,你的朋友就看不懂?我就是这个意思。
话虽如此,我们将考虑我们在之前序列化pet
对象时获得的序列化代码。为了提取其中的数据并将其分配给合适的属性,我们需要将代码分解成几个部分,这些部分按顺序在此呈现:
- 提取两个符号<? 和?>之间的文本块。
- 提取两个括号[ 和] 之间的每个文本块。
- 将每个记录分成两部分——属性名称及其值。
为了更好地了解情况,请看这些步骤以动画形式直观地呈现。
很抱歉有错别字,应该是“years”而不是“year”。我当时想在这里提一下,而不是从头开始重做动画。
实现 GIF 中呈现的行为可能会有点乏味;然而,为了保持文章的整洁并避免在其中插入大段代码,这里是项目的 Github 仓库,如果您想了解我是如何实现的,可以获取更多信息。
现在我们已经提取了我们的数据集并将其格式化为易于处理的形式(所有记录都包含在一个列表中),我们可以轻松地操作它——为了正确地做到这一点,我们将创建一个struct
,它包含属性名称及其值(当然是基于给定的序列化数据集)。
属性的名称,顾名思义,是一个string
——然而,并非我们dataset
中看到的所有值都是原始形式的string
。
尽管并非所有属性类型都是原始形式的string
——.NET 中的每个对象都有自己的文本表示,为了生成这种文本表示,你可以使用<object>.ToString
();
话虽如此,我们的struct
应该看起来像这样:
public struct Data
{
public string PropertyName;
public string Value;
}
对于我们之前成功提取的序列化属性列表中的每一条记录,我们将创建一个Data struct
实例来保存与该记录相关的信息。
现在我们有了结构列表(struct
是我们之前声明的 Data
类型),我们将逐一准确地迭代其元素,看看我们要用作反序列化dataset
宿主的对象是否有可以放置我们数据的位置。
(抱歉这个 gif 行为异常,录制它时事情不是很顺利 :) )
每次我们找到匹配的属性时,我们需要将反序列化的值传递给该属性;然而,为了实现这一壮举,我们需要克服两个障碍:
- 在运行时通过属性名称设置属性的值。
- 将反序列化的值转换为属性的原始类型。
因此,反射的使用变得很方便。首先要做的是迭代我们之前检索到的对象的所有属性(请参阅制作一个序列化器部分),对于每个属性,我们将提取其信息集;为了做到这一点,我们需要一个PropertyInfo
类。这个类将帮助我们获取属性的类型,利用提取的类型,我们将能够使用Convert.ChangeType
方法执行有效的类型转换操作。
foreach (var property in properties)
{
var propInfo = target.GetType().GetProperty(property.PropertyName);
propInfo?.SetValue(target,
Convert.ChangeType(property.Value, propInfo.PropertyType), null);
}
有了这一切,我们现在就拥有了完整的反序列化器架构,可以进行实现了——序列化器和反序列化器的完整代码都可以在 Github 上找到。
实现自定义序列化器
为了更好地利用我们一起构建的这个自定义序列化/反序列化器,我们将尝试执行两个操作。第一个是序列化一个基本类,第二个将侧重于反序列化一个已序列化的类。
重要:首先将序列化/反序列化器添加到您的引用中。
序列化对象
在这个例子中,我们将使用前面提到的Pet
类。我们将创建它的一个实例并为其赋值——然后对其进行序列化。
/*
Input :
Type = Dragon
Name = SkyCloud
Age = 9200 years
Weight = 9562.6500
*/
string code = TinySerializer.Serialize(pet);
如果我们显示代码
,预期的结果是:
<?
[Type=Dragon]
[Name=SkyCloud]
[Age=9200 years]
[Weight=9562.6500]
?>
反序列化对象
假设我们有一个文本文件,其中包含有关宠物的序列化数据,该文件名为pet.txt,其内容是:
<?
[Type=Cat]
[Name=Abby]
[Age=2 Months]
[Weight=3.50]
?>
要反序列化文件中的代码,我们必须读取其所有文本,然后对该代码进行一些操作。
var petData = File.ReadAllText("pet.txt");
var pet = TinySerializer.TinySerializer.DeSerialize(petData, new Pet());
Console.WriteLine(
$"This pet is a {pet.Type} and it is called
{pet.Name}.\nAge:{pet.Age}\nWeight:{pet.Weight}KG");
预期输出
This is a Cat and it is called Abby.
Age: 2 Months
Weight:3.5KG
简要总结
- 序列化是将对象转换为另一种格式的一种方式,该格式可用于稍后检索与该对象相关的某些方面。
- 反序列化是读取和理解该序列化数据的行为。
- 这里的自定义序列化/反序列化器将生成一种独特的语法,其他反序列化器不支持。
- 自定义序列化/反序列化器只能反序列化它支持的格式,而该格式只能使用此自定义序列化/反序列化器生成。换句话说,它不能反序列化例如用XML或JSON编写的数据集。
重要提示
如上所述,此序列化/反序列化器不是为了与现有产品竞争而分享的,它仅用于教育目的。但是,如果它适合您的某些需求,您仍然可以使用它。
注释
- 这个序列化器可能是现有的最小的序列化器。
- 它是基础的。
- 它不支持对象数组或集合。然而,可以实现变通方法来手动执行此壮举。
历史
- 2018年4月13日:初始版本