代码改变世界

F#中的XML序列化

2010-01-03 21:24 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

这两天在用F#写一小段代码,需要把一些对象存到外部文件中去。这个功能很容易,因为.NET本身就内置了序列化功能。方便起见,我打算将这个对象序列化成XML而不是二进制数据流。这意味着我需要使用XmlSerializer而不是BinaryFormatter。这本应没有问题,但是在使用时候还是发生了一些小插曲。

定义类型

在F#中有多种定义方式。除了F#特有的Record类型外,在F#中也可以定义普通的“类”,如:

#light

module XmlSerialization

type Post() = 

    [<DefaultValue>]
    val mutable Title : string

    [<DefaultValue>]
    val mutable Content : string

    [<DefaultValue>]
    val mutable Tags : string array

上面的代码在XmlSerialization模块中定义了一个Post类,其中包含三个公开字段。简单地说,它和C#中的如下定义等价:

public class Post
{
    public string Title;

    public string Content;

    public string[] Tags;
}

可见,在定义这种简单类型时,F#并没有什么优势,反而需要更多的代码。

使用XmlSerializer进行序列化

原本我以为使用XmlSerializer来序列化一个对象非常容易,写一个简单的(泛型)函数就可以了:

let byXmlSerializer (graph: 'a) =
    let serializer = new XmlSerializer(typeof<'a>)
    let writer = new StringWriter()
    serializer.Serialize(writer, graph)
    writer.ToString()

使用起来更加不在话下:

let post = new XmlSerialization.Post()
post.Title <- "Hello"
post.Content <- "World"
post.Tags <- [| "Hello"; "World" |]

let xml = XmlSerialization.byXmlSerializer(post)

但是,在运行的时候,XmlSerializer的构造函数却抛出了InvalidOperationException:

XmlSerialization cannot be serialized. Static types cannot be used as parameters or return types.

这句话的提示似乎是在说XmlSerialization是一个静态类型——但这其实是F#的模块啊。不过使用.NET Reflector查看编译后的程序集便会发现,其实Post类是这样定义的:

public static class XmlSerialization
{
    public class Post { ... }
}

虽然.NET中也有“模块”的概念,但是它和F#中的模块从各方面来讲几乎没有相同之处。F#的模块会被编译为静态类,自然模块中的方法或各种函数便成为静态类中的内嵌类型及方法。这本没有问题,从理论上来说XmlSerializer也不该有问题,不是吗?

可惜XmlSerializer的确有这样的问题,我认为这是个Bug——但就算这是个Bug也无法解决目前的状况。事实上,互联网上也有人提出这个问题,可惜半年来都没有人回应

手动序列化

那么我又该怎么做呢?我想,算了,既然如此,我们进行手动序列化吧。反正就是简单的对象,写起来应该也不麻烦。例如在C#中我们便可以:

public class Post
{
    ...

    public string ToXml()
    {
        var xml = 
            new XElement("Post",
                new XElement("Title", this.Title),
                new XElement("Content", this.Content),
                new XElement("Tags",
                    this.Tags.Select(t => new XElement("Tag", t))));

        return xml.ToString();
    }
}

很简单,不是吗?但是用F#写同样的逻辑便有一些问题了,最终得到的结果是:

type Post() = 
    ...

    member p.ToXml() =
        let xml = new XElement(XName.Get("Post"))
        xml.Add(new XElement(XName.Get("Title"), p.Title))
        xml.Add(new XElement(XName.Get("Content"), p.Content))

        let tagElements = p.Tags |> Array.map (fun t -> new XElement(XName.Get("Tag"), t))
        xml.Add(new XElement(XName.Get("Tags"), tagElements))
        
        xml.ToString()

C#之所以可以写的简单,其中有诸多因素:

  • XElement的构造函数最后使用了params object[],这意味着我们可以把参数“罗列”出来,而不需要显式地构造一个数组。
  • XElement的构造函数接受的其实是XName类型参数,但字符串可以被隐式地转化为XName类型。
  • XElement的构造函数可以将IEnumerable<XElement>对象转化为独立的元素。

但是,除了最后一条外,其他两个特性在F#里都无法享受到。因此,我们只能用命令式编程的方式编写此类代码。您可以发现,这样的F#代码几乎可以被自动转化为Java代码。F#在写这样的代码时实在没有优势。

使用DataContractSerializer

手动进行XML序列化虽然并不困难,但是实在麻烦。这不是一种通用的做法,我们必须为每个类型各写一套序列化(和反序列化)逻辑,在类型字段有所改变的时候,序列化和反序列化的逻辑还必须有所变化。就在我打算写一个简单的,通用的XML序列化方法时,我忽然想到以前看到过的一篇文章,说是在.NET 3.0中发布了新的类库:DataContractSerializer。

DataContractSerializer看似和WCF有关,如DataContractAttribute,DataMemberAttribute等标记最典型的作用也一直用在WCF里。但事实上,这些类型都是定义在System.Runtime.Serialization.dll中的,这意味着这些功能从设计之初与WCF分离开来,可以独立使用。那么我们不如尝试一下吧:

let serialize (graph : 'a) = 
    let serializer = new DataContractSerializer(typeof<'a>)
    let textWriter = new StringWriter();
    let xmlWriter = new XmlTextWriter(textWriter);
    serializer.WriteObject(xmlWriter, graph)
    textWriter.ToString()

果然好用,DataContractSerializer并没有出现XmlSerializer那样傻乎乎地错误。自然,与之相对的反序列化函数也很容易写:

let deserialize<'a> xml = 
    let serializer = new DataContractSerializer(typeof<'a>)
    let textReader = new StringReader(xml)
    let xmlReader = new XmlTextReader(textReader)
    serializer.ReadObject(xmlReader) :?> 'a

试验一下,看看效果?

let post = new XmlSerialization.Post()
post.Title <- "Hello"
post.Content <- "World"
post.Tags <- [| "Hello"; "World" |]

let xml = XmlSerialization.serialize post
let post' = XmlSerialization.deserialize<XmlSerialization.Post> xml

经过更多试验,我发现DataContractSerializer对于复杂类型的字段也可以正常应对,而得到这些功能也只需要在目标类型上标记一个SerializableAttribute就行了,更细节的控制也可以通过DataContractAttribute等进行控制。这样看来,XmlSerializer似乎已经可以退出历史舞台了?

本文代码