使用代码生成建立可扩展序列化器(上)

使用代码生成建立可扩展序列化器(上)

地狱门神

 

在很多程序中,配置文件和用户数据的保存和读取都是一个需要考虑的问题。

在以前,用户数据经常保存在INI文件中,后来出现了注册表,于是也有保存在注册表中的。

注册表不是一种稳定的方式,不便于用户管理。

INI和注册表都难以保存复杂结构的对象,需要手写很多代码。

 

随着XML和.Net的兴起,越来越多的程序使用XML文件来保存用户数据。

不过.Net的内置序列化器System.Xml.Serialization.XmlSerializer是在.Net 2.0之前推出的,对泛型的支持有限。

同时,由于其使用特性来标记数据模型中的类和变量,并需要对象实现IXmlSerializable。这是对数据模型的严重污染,非常不利于程序维护。

 

此外,出于性能的考虑,在数据达到一定规模之后,我们必须使用二进制序列化,以加快加载速度。

对于二进制序列化,有很多实现,而且一般都互不兼容。

由于很多二进制格式是已经事先确定的,要实现对这些二进制格式的反序列化和序列化支持,是非常困难的,没有很好的工具解决这个问题,经常需要手工维护读文件和写文件两部分代码。

.Net的默认实现使得类必须实现ISerializable。这和XML序列化的问题是一样的。

 

这两种序列化器均难以方便的实现定制功能。

 

由于这些问题,我建立了两个全新的可扩展序列化器,并使用相同的基础结构。

设计目标是:

1)可扩展性,二进制序列化应可扩展支持任意二进制格式;

2)性能,不能太慢,对于多次序列化、反序列化,每次不应有较大的性能损失;

3)无污染,不应对模型数据进行任何插入式的标记;

4)可加的版本支持,这一部分应通过可扩展性来实现;

5)自动支持泛型集合,特别是List(T)和Dictionary(TKey, TValue)。

6)自动支持不变类型,即只读的公开属性和构造函数参数匹配的类型,例如KeyValuePair(TKey, TValue)。

 

.Net对于泛型的支持不够完整,缺乏泛型λ表达式、泛型委托等语法元素,泛型约束也有很大的限制。

因此,代码生成是唯一的强类型的选择。另一个选择是使用弱类型进行动态绑定,不过感觉性能和类型安全都不能保证,所以不用。

 

先上代码。C#的代码请参见附件。

 

数据模型如下:

 

PublicClassDataEntry

    Public Name AsString

    Public Data AsByte()

EndClass

 

PublicClassImmutableDataEntry(Of T)

    PublicReadOnlyProperty Name AsString

        Get

            Return NameValue

        EndGet

    EndProperty

    PublicReadOnlyProperty Data AsT

        Get

            Return DataValue

        EndGet

    EndProperty

 

    Private NameValue AsString

    Private DataValue AsT

    PublicSubNew(ByVal Name AsString, ByVal Data AsT)

        Me.NameValue = Name

        Me.DataValue = Data

    EndSub

EndClass

 

PublicClassDataObject

    Public DataEntries AsNewDictionary(OfString, DataEntry)

    Public ImmutableDataEntries AsNewDictionary(OfString, ImmutableDataEntry(OfByte()))

EndClass

 

XML序列化代码如下:

 

'创建自定义XML序列化器实例

Dim mxs AsNew Firefly.Mapping.XmlSerializer

 

'创建数据

Dim Obj AsNewDataObject

Obj.DataEntries.Add("DataEntry1", NewDataEntryWith {.Name = "DataEntry1", .Data = NewByte() {1, 2, 3, 4, 5}})

Obj.DataEntries.Add("DataEntry2", NewDataEntryWith {.Name = "DataEntry2", .Data = NewByte() {6, 7, 8, 9, 10}})

Obj.ImmutableDataEntries.Add("ImmutableDataEntry1", NewImmutableDataEntry(OfByte())("ImmutableDataEntry1", NewByte() {1, 2, 3, 4, 5}))

Obj.ImmutableDataEntries.Add("ImmutableDataEntry2", NewImmutableDataEntry(OfByte())("ImmutableDataEntry2", NewByte() {6, 7, 8, 9, 10}))

 

'XML序列化

Dim Element = mxs.Write(Obj)

 

'XML反序列化

Dim RoundTripped = mxs.Read(OfDataObject)(Element)

 

'输出到命令行

Dim Setting = NewXmlWriterSettingsWith {.Encoding = Console.Out.Encoding, .Indent = True, .OmitXmlDeclaration = False}

Using w = XmlWriter.Create(Console.Out, Setting)

    Element.Save(w)

EndUsing

 

这样就自动生成以下XML文本:

 

<?xmlversion="1.0"encoding="gb2312"?>

<DataObject>

  <DataEntries>

    <KeyValuePairOfStringAndDataEntry>

      <Key>DataEntry1</Key>

      <Value>

        <Name>DataEntry1</Name>

        <Data>

          <Byte>1</Byte>

          <Byte>2</Byte>

          <Byte>3</Byte>

          <Byte>4</Byte>

          <Byte>5</Byte>

        </Data>

      </Value>

    </KeyValuePairOfStringAndDataEntry>

    <KeyValuePairOfStringAndDataEntry>

      <Key>DataEntry2</Key>

      <Value>

        <Name>DataEntry2</Name>

        <Data>

          <Byte>6</Byte>

          <Byte>7</Byte>

          <Byte>8</Byte>

          <Byte>9</Byte>

          <Byte>10</Byte>

        </Data>

      </Value>

    </KeyValuePairOfStringAndDataEntry>

  </DataEntries>

  <ImmutableDataEntries>

    <KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

      <Key>ImmutableDataEntry1</Key>

      <Value>

        <Name>ImmutableDataEntry1</Name>

        <Data>

          <Byte>1</Byte>

          <Byte>2</Byte>

          <Byte>3</Byte>

          <Byte>4</Byte>

          <Byte>5</Byte>

        </Data>

      </Value>

    </KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

    <KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

      <Key>ImmutableDataEntry2</Key>

      <Value>

        <Name>ImmutableDataEntry2</Name>

        <Data>

          <Byte>6</Byte>

          <Byte>7</Byte>

          <Byte>8</Byte>

          <Byte>9</Byte>

          <Byte>10</Byte>

        </Data>

      </Value>

    </KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

  </ImmutableDataEntries>

</DataObject>

 

这个文件中,字节数组使用了默认的集合表示,不利于查看和修改,我们可以使用扩展机制来处理这个问题。

两个序列化器均提供三种扩展机制:

1) PutReader|PutWriter,用于提供直接的读写替代,直接操作需要读写的对象和数据流|数据树;

2) PutReaderTranslator|PutWriterTranslator,提供更高层的抽象,用于将需要读写的对象替代成另一种对象,交给序列化器做后续处理;

3) (ReaderResolver|WriterResolver).(ProjectorResolvers|AggregatorResolvers),提供直接的类型解析替代,但此机制中类型均为运行时类型,编写代码较麻烦。

详细的说明将在后面介绍,这里我们使用2),即对象替代。

 

声明对象替代器:

 

'用于将字节数组转换为字符串处理

PrivateClassByteArrayCodec

    ImplementsIProjectorToProjectorRangeTranslator(OfByte(), String) 'Reader

    ImplementsIProjectorToProjectorDomainTranslator(OfByte(), String) 'Writer

 

    PublicFunction TranslateProjectorToProjectorRange(Of D)(ByVal Projector AsFunc(OfD, String)) AsFunc(OfD, Byte()) ImplementsIProjectorToProjectorRangeTranslator(OfByte(), String).TranslateProjectorToProjectorRange

        ReturnFunction(k) Regex.Split(Projector(k).Trim(" \t\r\n".Descape.ToCharArray), "( |\t|\r|\n)+", RegexOptions.ExplicitCapture).Select(Function(s) Byte.Parse(s, Globalization.NumberStyles.HexNumber)).ToArray

    EndFunction

    PublicFunction TranslateProjectorToProjectorDomain(Of R)(ByVal Projector AsFunc(OfString, R)) AsFunc(OfByte(), R) ImplementsIProjectorToProjectorDomainTranslator(OfByte(), String).TranslateProjectorToProjectorDomain

        ReturnFunction(ba) Projector(String.Join(" ", (ba.Select(Function(b) b.ToString("X2")).ToArray)))

    EndFunction

EndClass

 

将对象替代器注册到序列化器:

 

Dim mxs AsNew Firefly.Mapping.XmlSerializer

mxs.PutReaderTranslator(NewByteArrayCodec)

mxs.PutWriterTranslator(NewByteArrayCodec)

 

这样就自动生成以下XML文本:

 

<?xmlversion="1.0"encoding="gb2312"?>

<DataObject>

  <DataEntries>

    <KeyValuePairOfStringAndDataEntry>

      <Key>DataEntry1</Key>

      <Value>

        <Name>DataEntry1</Name>

        <Data>01 02 03 04 05</Data>

      </Value>

    </KeyValuePairOfStringAndDataEntry>

    <KeyValuePairOfStringAndDataEntry>

      <Key>DataEntry2</Key>

      <Value>

        <Name>DataEntry2</Name>

        <Data>06 07 08 09 0A</Data>

      </Value>

    </KeyValuePairOfStringAndDataEntry>

  </DataEntries>

  <ImmutableDataEntries>

    <KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

      <Key>ImmutableDataEntry1</Key>

      <Value>

        <Name>ImmutableDataEntry1</Name>

        <Data>01 02 03 04 05</Data>

      </Value>

    </KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

    <KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

      <Key>ImmutableDataEntry2</Key>

      <Value>

        <Name>ImmutableDataEntry2</Name>

        <Data>06 07 08 09 0A</Data>

      </Value>

    </KeyValuePairOfStringAndImmutableDataEntryOfArrayOfByte>

  </ImmutableDataEntries>

</DataObject>

 

这里解释一下ByteArrayCodec的作用。

ByteArrayCodec是一个转换字节数组到字符串,并转换回来的类,实现了两个接口:

 

PublicInterfaceIProjectorToProjectorRangeTranslator(Of R, M)

    Function TranslateProjectorToProjectorRange(Of D)(ByVal Projector As Func(Of D, M)) As Func(Of D, R)

EndInterface

 

PublicInterfaceIProjectorToProjectorDomainTranslator(Of D, M)

    Function TranslateProjectorToProjectorDomain(Of R)(ByVal Projector As Func(OfM, R)) As Func(OfD, R)

EndInterface

 

这两个接口,其实是用来约束两个泛型高阶函数。D是输入类型,M是中间类型,R是输出类型。

所谓的Projector,是指投影函数,即一个将原来的对象转换为具有相同信息或者更少信息的对象的函数。

与其相对的,我还定义了一种叫Aggregator的东西,即聚合函数,用于将一个对象的信息加入到另一个已有对象。这个现在暂不描述。

通常我们认为Projector比Aggregator更好书写。

IProjectorToProjectorRangeTranslator,就是用来将一个Projector的值域类型变换到另一个类型,也就是:

IProjectorToProjectorRangeTranslator(D, M, R): Projector(D, M) -> Projector(D, R)

由于.Net不支持返回泛型λ表达式,不能做泛型参数偏特化,因此将本来的三个泛型参数(D, M, R)分成两组(R, M)和(D),(R, M)定义为接口参数,(D)定义成函数类型参数。

这样我们可以先对(R, M)进行特化,再对(D)进行特化,使得高阶函数的实现与定义域类型D无关。

同样,IProjectorToProjectorDomainTranslator用于将一个Projector的定义域类型变换到另一个类型。

 

在ByteArrayCodec中:

TranslateProjectorToProjectorRange用于将Projector(D, String)转化为Projector(D, Byte()),内部做了String到Byte()的转换,使用正则表达式实现;

TranslateProjectorToProjectorDomain用于将Projector(Byte(), R)转化为Projector(String, R),内部做了Byte()到String的转换,使用Byte.ToString("X2")来完成。

 

下一步,我们需要变更数据模型,但需要保持对已有用户数据的兼容。

这个时候,我们仍然通过对象替代来解决。

 

首先变更数据模型,将DataEntry增加一个Attribute字段,原DataEntry更名保留:

 

'版本1的DataEntry

PublicClassDataEntryVersion1

    Public Name AsString

    Public Data AsByte()

EndClass

 

'当前版本(版本2)的DataEntry

PublicClassDataEntry

    Public Name AsString

    Public Data AsByte()

    Public Attribute AsString

EndClass

 

增加一个对象替代器:

'用于适配DataEntry的版本1和版本2

PublicClassDataEntryVersion1To2Translator

    ImplementsIProjectorToProjectorRangeTranslator(OfDataEntry, DataEntryVersion1) 'Reader

    ImplementsIProjectorToProjectorDomainTranslator(OfDataEntry, DataEntryVersion1) 'Writer

 

    PublicFunction TranslateProjectorToProjectorRange(Of D)(ByVal Projector AsFunc(OfD, DataEntryVersion1)) AsFunc(OfD, DataEntry) ImplementsIProjectorToProjectorRangeTranslator(OfDataEntry, DataEntryVersion1).TranslateProjectorToProjectorRange

        ReturnFunction(DomainValue)

                   Dim v1 = Projector(DomainValue)

                   ReturnNewDataEntryWith {.Name = v1.Name, .Data = v1.Data, .Attribute = "Version1's attribute"}

               EndFunction

    EndFunction

    PublicFunction TranslateProjectorToProjectorDomain(Of R)(ByVal Projector AsFunc(OfDataEntryVersion1, R)) AsFunc(OfDataEntry, R) ImplementsIProjectorToProjectorDomainTranslator(OfDataEntry, DataEntryVersion1).TranslateProjectorToProjectorDomain

        ReturnFunction(v2)

                   Dim v1 = NewDataEntryVersion1With {.Name = v2.Name, .Data = v2.Data}

                   Return Projector(v1)

               EndFunction

    EndFunction

EndClass

 

然后声明两个版本的序列化器,版本1不放入DataEntryVersion1To2Translator,版本2放入DataEntryVersion1To2Translator。

为了在文档中加入版本标志,我们可以使用XML元素的Attribute,在写入后增加标记,如:

 

Dim Element = SerializerVersion2.Write(Obj)

Element.@<SchemaType> = "MyDataFormat"

Element.@<Version> = 2

 

在读取的时候,首先读取XElement:

 

Dim SchemaType = Element.@<SchemaType>

If SchemaType <> "MyDataFormat"ThenThrowNewInvalidDataException("数据不是MYDF格式数据")

Dim Version = Integer.Parse(Element.@<Version>)

 

再通过版本号来选择序列化器进行反序列化。

 

<?xmlversion="1.0"encoding="gb2312"?>

<DataObjectSchemaType="MyDataFormat"Version="2">

 

这样生成的第二个版本的XML文件就会具有版本号,同时程序可以对各个版本的文件进行兼容。

不会出现手写代码时,由于要兼容,出现多套数据模型或者难以修改数据模型的问题。

详细的代码,以及二进制序列化的示例请参见附件,有VB和C#两个版本的示例代码,和一个简化的库。

简化的库是因为这两个序列化器均是作为萤火虫汉化框架的一部分来开发的。

 

关于内部实现,主要是通过System.Linq.Expression来进行的,生成的表达式可以被垃圾回收。

实现我将在下篇中描述。

 

所有的示例代码均按Public Domain授权,所有的库代码均按BSD协议授权。如果需要在GPL程序中使用,请与我单独联系授权。

 

最后携带一点私货,为了解耦我们不要拘泥于面向对象。

 

示例下载地址:

https://files.cnblogs.com/Rex/FireflyForSerializing.rar

 

posted @ 2010-11-18 16:09  地狱门神  阅读(2171)  评论(12编辑  收藏  举报