这两天在用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似乎已经可以退出历史舞台了?
原文链接:https://www.f2er.com/xml/294964.html