Hello World
Spiga

F#中的XML序列化

2010-01-03 21:24 by 老赵, 6444 visits

这两天在用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似乎已经可以退出历史舞台了?

本文代码

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

33 条回复

  1. 麒麟
    *.*.*.*
    链接

    麒麟 2010-01-03 21:35:00

    占沙发

  2. 程序设计的艺术
    *.*.*.*
    链接

    程序设计的艺术 2010-01-03 21:56:00

    第二个沙发

  3. Kurodo
    *.*.*.*
    链接

    Kurodo 2010-01-03 22:09:00

    对F#没有什么好感...

  4. 老赵
    admin
    链接

    老赵 2010-01-03 22:15:00

    @Kurodo
    现在和异步IO有关的东西我都用F#鸟……

  5. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2010-01-03 22:25:00

    看样子真得花时间去学习一下F#,很早以前就学习了,但是没有认真

  6. 老赵
    admin
    链接

    老赵 2010-01-03 22:32:00

    @温景良(Jason)
    还是要用,我以前也写不好,先在写得很顺。

  7. ITniao
    *.*.*.*
    链接

    ITniao 2010-01-03 22:50:00

    f#做web开发如何??

  8. 老赵
    admin
    链接

    老赵 2010-01-03 22:57:00

    @ITniao
    看用web做什么了,F#作Web的话,可能语言特色没有很大优势,当然异步调用时还是很方便的。

  9. xiao_p
    *.*.*.*
    链接

    xiao_p 2010-01-04 00:15:00

    最近看F# 的language specification 看的云里雾里的。

    有个问题问下老赵,framework 4.0对F# 和3.5相比有没有改进?

    我用windows xp 跑vs 2010太苦闷了,速度灰常灰常的慢,不得已装了一个vs 2008的ctp add-on,但是又不知道4.0是不是有了优化。。。

  10. 老赵
    admin
    链接

    老赵 2010-01-04 00:21:00

    @xiao_p
    语言没有区别,F#标准库也一样,不一样的只是.NET Fx这部分……

  11. xiao_p
    *.*.*.*
    链接

    xiao_p 2010-01-04 00:52:00

    Jeffrey Zhao:
    @xiao_p
    语言没有区别,F#标准库也一样,不一样的只是.NET Fx这部分……



    哦,谢谢

  12. Seasun海豚
    *.*.*.*
    链接

    Seasun海豚 2010-01-04 09:18:00

    好像每次我想学一小样新东西的时候就会有相关文章出来?幻觉?
    现在去看下赵大哥有没有C#XML序列化的文章先。

  13. Seasun海豚
    *.*.*.*
    链接

    Seasun海豚 2010-01-04 09:23:00

    。。。没找到

  14. 老赵
    admin
    链接

    老赵 2010-01-04 09:34:00

    @Seasun海豚
    MSDN能找到的东西我不写。

  15. Seasun海豚
    *.*.*.*
    链接

    Seasun海豚 2010-01-04 09:36:00

    @Jeffrey Zhao
    哦哦

  16. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2010-01-04 10:04:00

    -0- 似乎还真的不太能指望用f#设计Usercontrol啊

  17. 老赵
    admin
    链接

    老赵 2010-01-04 10:47:00

    @韦恩卑鄙 alias:v-zhewg
    不过,只要抱有写Java代码的觉悟,用F#还是没有问题的……

  18. kkun
    *.*.*.*
    链接

    kkun 2010-01-04 11:14:00

    赵老师,用这个东西可以否序列化Dictionary<T,T>这样的实例到二进制?
    一直头疼这个东东...

  19. 老赵
    admin
    链接

    老赵 2010-01-04 11:19:00

    @kkun
    这里在谈语言,没有谈类库。

  20. 小城故事
    *.*.*.*
    链接

    小城故事 2010-01-04 13:17:00

    老赵喜新厌旧,爱上F#了,我们这些C#的咋办?

  21. pangxiaoliang[北京]流浪者
    *.*.*.*
    链接

    pangxiaoliang[北京]流浪者 2010-01-04 13:42:00

    老赵换工作了肯定,大家请看签名。

  22. 老赵
    admin
    链接

    老赵 2010-01-04 14:39:00

    小城故事:老赵喜新厌旧,爱上F#了,我们这些C#的咋办?


    C#还有什么可谈的吗?

  23. 小城故事
    *.*.*.*
    链接

    小城故事 2010-01-04 16:32:00

    Jeffrey Zhao:

    小城故事:老赵喜新厌旧,爱上F#了,我们这些C#的咋办?


    C#还有什么可谈的吗?


    有啊,当然指我们看来。我们大家要给老赵找点课题了,要不你再开个置顶贴吧。

    我最近在想一个课题,全面论证C#及.Net平台语言的先进性,把23个经典设计模式,许多其实在.Net Framework中的实现综述一下。这个话题怎么样?

  24. 老赵
    admin
    链接

    老赵 2010-01-04 17:50:00

    @小城故事
    不知道该写啥,你开个头?

  25. 小城故事
    *.*.*.*
    链接

    小城故事 2010-01-04 19:16:00

    @Jeffrey Zhao
    好啊,我试试

  26. 小城故事
    *.*.*.*
    链接

    小城故事 2010-01-04 20:05:00

    老赵这些日子可能挺不好受,不过这只是心力交瘁后的幻觉,过一阵子就烟消云散了,老赵你还会是那个无往不利的常山赵吉力。

  27. 老赵
    admin
    链接

    老赵 2010-01-04 20:39:00

    @小城故事
    我没不好受啊,我现在就想等体检赶快开始赶快拿到报告然后可以早点去上班……这段时间要一直呆在家里了……

  28. 小城故事
    *.*.*.*
    链接

    小城故事 2010-01-04 23:15:00

    @Jeffrey Zhao
    原来已经找到了,这么快,老赵就是老赵

  29. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-01-05 16:15:00

    不会吧?F#不支持隐式类型转换?

  30. 老赵
    admin
    链接

    老赵 2010-01-05 16:32:00

    @Ivony...
    是啊,不支持。要么是用XName.Get,要么自己调用op_Implict。

  31. 青羽
    *.*.*.*
    链接

    青羽 2010-01-06 08:54:00

    Jeffrey Zhao:
    @Kurodo
    现在和异步IO有关的东西我都用F#鸟……


    老赵可否介绍一些异步编程的思路?
    或者ASP.NET异步编程的书籍。

  32. 老赵
    admin
    链接

    老赵 2010-01-06 15:16:00

    @青羽
    不知道啥叫思路……asp.net异步编程就是几页纸的东西,看MSDN,没有啥书……

  33. 链接

    羊牮 2014-08-30 00:49:09

    老赵最近没有在研究F#了?好久没看你写F#。不好意思我挖坟了。 现在的F#可以支持类C#的方法了。

    var xml = 
        new XElement("Post",
            new XElement("Title", this.Title),
                new XElement("Content", this.Content))
    

    只要声明一个:

    let inline (~&&) name = XName.Get name
    

    就可以写出一个等价于上面代码的东东出来:

    let xml = 
        XElement(&&"Post",
            XElement(&&"Title", this.Title),
                XElement(&&"Content", this.Content))
    

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我