一个关于解决序列化问题的编程技巧

在前一篇文章中我曾经说过,现在正在做一个小小的框架以实现采用统一的API实现对上下文(Context)信息的统一管理。这个框架同时支持Web和GUI应用,并支持跨线程传递和跨域传递(这里指在WCF服务调用中实现客户端到服务端隐式传递),以及对上下文项目(ContextItem)的读写控制。关键就在于后面两个特性的支持上面,出现一个小小的关于序列化的问题。解决方案只需要改动短短的一行代码,结果却让我折腾了老半天。

一、问题重现

为了重现我实际遇到的问题,我特意将问题简化,为此我写了一个简单的例子(你可以从这里下载)。在下面的代码片断中,我创建了一个名称为ContextItem的类型,代表一个需要维护的上下文项。由于需要在WCF服务调用实现自动传递,我将起定义成DataContract。ContextItem包含Key,Value和ReadOnly三个属性,不用说ReadOnly表示该ContextItem可以被修改。注意Value属性Set方法的定义——如果ReadOnly则抛出异常。

   1: [DataContract(Namespace = "http://www.artech.com")]
   2: public class ContextItem
   3: {
   4:     private object value = null;
   5:     [DataMember]
   6:     public string Key { get; private set; }
   7:     [DataMember]
   8:     public object Value
   9:     {
  10:         get
  11:         {
  12:             return this.value;
  13:         }
  14:         set
  15:         {
  16:             if (this.ReadOnly)
  17:             {
  18:                 throw new InvalidOperationException("Cannot change the value of readonly context item.");
  19:             }
  20:             this.value = value;
  21:         }
  22:     }
  23:     [DataMember]
  24:     public bool ReadOnly { get; set; }
  25:     public ContextItem(string key, object value)
  26:     {
  27:         if (string.IsNullOrEmpty(key))
  28:         {
  29:             throw new ArgumentNullException("key");
  30:         }
  31:         this.Key = key;
  32:         this.Value = value;
  33:     }
  34: }

为了演示序列化和反序列化,我写了如下两个静态的帮助方法。Serialize和Deserialize分别用于序列化和反序列化,前者将对象序列成成XML并保存到指定的文件中,后者则从文件读取XML并反序列化成相应的对象。

   1: public static T Deserialize<T>(string fileName)
   2: {
   3:     DataContractSerializer serializer = new DataContractSerializer(typeof(T));
   4:     using (XmlReader reader = new XmlTextReader(fileName))
   5:     {
   6:         return (T)serializer.ReadObject(reader);
   7:     }
   8: }
   9:  
  10: public static void Serialize<T>(T instance, string fileName)
  11: {
  12:     DataContractSerializer serializer = new DataContractSerializer(typeof(T));
  13:     using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
  14:     {
  15:         serializer.WriteObject(writer, instance);
  16:     } 
  17:     Process.Start(fileName);
  18: }

我们的程序很简单。从如下的代码片断中,我们先创建一个ContextItem对象,然后将ReadOnly属性设置成true。然后调用Serialize方法将对象序列化成XML并保存在一个名称为context.xml的文件中。然后调用Deserialize方法,读取该文件进行反序列化。

   1: static void Main(string[] args)
   2: {
   3:     var contextItem1 = new ContextItem("__userId", "Foo");
   4:     contextItem1.ReadOnly = true;
   5:     Serialize<ContextItem>(contextItem1, "context.xml");
   6:     var contextItem2 = Deserialize<ContextItem>("context.xml");           
   7: }

序列化操作能够正常执行,但当程序执行到Deserialize的时候抛出如下一个InvalidOperationException异常。

image

二、问题分析

从上面给出的截图,我们不难看出,异常是在给ContextItem对象的Value属性赋值的时候抛出的。如果对DataContractSerializer序列化器的序列化/反序列化规则的有所了解的话,应该知道:对于数据契约(DataContract)基于属性(Property)的数据成员(DataMember),序列器在反序列化的时候是通过调用Set方法对其进行初始化的。在本例中,由于ReadOnly是True,在对Value进行反序列化的时候必然会调用Set方法。但是,只读的ContextItem却不能对其赋值,所以异常抛出。

那么,如何来解决这个问题呢?我最初的想法是这样:在序列化的时候将ReadOnly属性设置成False,然后添加另一个属性专门用于保存真实的值。在进行反序列的时候,由于ReadOnly为false,所以不会出现异常。当反序列化完成之后,在将ReadOnly的初始值赋上。虽然上述的方案能够解决问题,但是为此对ContextItem添加一个只在序列化和反序列化的过程中在有用的属性,总觉得很丑陋。

我们不妨换一种思路:异常产生于对Value属性凡序列化时发现ReadOnly非True的情况。那么怎样采用避免这种情况的发生呢?如果Value属性先于ReadOnly属性被序列化,那么ReadOnly的初始值就是False,这个问题不就解决了吗?这就是我们的第一个解决方案。

三、解决方案一:通过控制属性反序列化顺序

那么,如果控制那么属性先被反序列化,那么后被序列化呢?这就是要了解DataContractSerializer序列化器的序列化和发序列化规则了。在默认的情况下,DataContractSerializer是按照数据成员的名称的顺序进行序列化的。这可以从生成出来的XML的结构看出来。而XML元素的先后顺序决定了反序列化的顺序。

   1: <ContextItem xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
   2:     <Key>__userId</Key>
   3:     <ReadOnly>true</ReadOnly>
   4:     <Value xmlns:d2p1="http://www.w3.org/2001/XMLSchema" i:type="d2p1:string">Foo</Value>
   5: </ContextItem>

在上面的例子中,ContextItem的ReadOnly排在Value的前面,会先被序列化。那么,是不是我们要更新Value或者ReadOnly的数据成员(DataMember,不是属性名称)呢?这肯定不是我们想要的解决方案。在SOA的世界中,DataMember是契约的一部分,往往是不容许更改的。

如果在不更改数据成员名称的前提下让属性Value先于ReadOnly被序列化,需要用到DataContractSerializer另一条反序列化规则:我们可以通过DataMemberAttribute特性的Order属性控制序列化后的属性在XML元素列表中的位置。

为此,我们有了答案,我们只需要将ContextItem稍加改动就可以了。在如下的代码中,在为Value和ReadOnly两个属性应用DataMemberAttribute的时候,将Order属性分别设置成1和2,这样就能使ContextItem对象在被序列化的时候,Value和ReadOnly属性对应的XML元素将永远会有前后之分。这里还需要注意的是,在Value属性的Set方法中,判断是否只读,采用的不是ReadOnly属性,而是对应的readonly字段。这一点非常重要,如果调用ReadOnly属性将会迫使该属性被反序列化。

   1: [DataContract(Namespace = "http://www.artech.com")]
   2: public class ContextItem
   3: {
   4:     private object value = null;
   5:     private bool readOnly;
   6:     [DataMember]
   7:     public string Key { get; private set; }
   8:  
   9:     [DataMember(Order = 1)]
  10:     public object Value
  11:     {
  12:         get
  13:         {
  14:             return this.value;
  15:         }
  16:         set
  17:         {
  18:             if (this.readOnly)
  19:             {
  20:                 throw new InvalidOperationException("Cannot change the value of readonly context item.");
  21:             }
  22:             this.value = value;
  23:         }
  24:     }
  25:     [DataMember(Order =2)]
  26:     public bool ReadOnly
  27:     {
  28:         get
  29:         {
  30:             return readOnly;
  31:         }
  32:         set
  33:         {
  34:             readOnly = value;
  35:         }
  36:     }
  37:     //Others
  38: }

有兴趣的读者可以亲自试试看,如果我们进行了如上的更改,前面的程序就能正常运行了。到这里,有的读者可以要问了,你不是说仅仅有一行代码的变化吗,我看上面改动的不止一行嘛。没有错,我们完全可以作更少的更改来解决问题。

四、解决方案二:将数据成员定义在字段上而不是属性上

我们再换一种思维,之所以出现异常是在反序列化的时候调用Value属性的Set方法所致。如果在反序列化的时候不调用这个方法不就得了吗?那么,如何才能避免对Value属性的Set方法的调用呢?方法很简单,那就是将数据成员定义在字段上,而不是属性上。基于属性的数据成员在反序列化的时候不得不通过调用Set方法对数据项进行初始化,而基于字段的数据成员在反序列化的时候只需要直接对其复制就可以了。

基于这样的思路,我们对原来的ContextItem进行简单的改动——将DataMemberAttribute特性从Value属性移到value字段上。需要注意的,为了符合于原来的Schema,需要将DataMemberAttribute特性的Name属性设置成“Value”。

   1: [DataContract(Namespace = "http://www.artech.com")]
   2: public class ContextItem
   3: {
   4:     [DataMember]
   5:     public string Key { get; private set; }
   6:  
   7:     [DataMember(Name = "Value")]
   8:     private object value = null;
   9:     public object Value
  10:     {
  11:         get
  12:         {
  13:             return this.value;
  14:         }
  15:         set
  16:         {
  17:             if (this.ReadOnly)
  18:             {
  19:                 throw new InvalidOperationException("Cannot change the value of readonly context item.");
  20:             }
  21:             this.value = value;
  22:         }
  23:     }
  24:     [DataMember]
  25:     public bool ReadOnly { get; set; }     
  26:      //Others
  27:     }
  28: }

总结

虽然这仅仅是一个很小的问题,解决的方案看起来也是如此的简单。但是,这并不意味着这是一个可以被忽视的问题,背后隐藏对DataMemberAttribute序列化的序列化规则的理解。


作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
原文链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/257174.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

踩坑之路anaconda创建虚拟环境

浑浑噩噩的过了三年渣硕生涯&#xff0c;虽然说自己是搞图像的&#xff0c;但基本是一些机器视觉的东西&#xff0c;最近突然想好好搞搞深度学习这方面&#xff0c;想着那就先搭搭环境跑个demo吧&#xff0c;经历了好多莫名其妙的踩坑操作&#xff0c;demo跑的终于没bug了&…

dns服务器未响应

昨天还好好的&#xff0c;今天打开电脑显示DNS服务器为响应。 解决办法&#xff1a;右击电脑下方图标栏——打开Windows任务管理器——服务——服务&#xff08;s&#xff09;——找到DNS client和DHCP client——右击重启

ubuntu安装pytorch镜像修改及下载

ubuntu安装pytorch镜像修改及下载 下载pytorch下载太慢&#xff0c;搞了很长时间&#xff0c;终于改好镜像能快速下载了&#xff0c;记录以下。 1.在/home/用户名/ 下找到/.condarc 文件&#xff0c;可能需要你右击鼠标显示隐藏文件才能显示&#xff0c; 2.把内容修改为清华等镜…

R--线性回归诊断(一)

线性回归诊断--R 【转载时请注明来源】&#xff1a;http://www.cnblogs.com/runner-ljt/ Ljt 勿忘初心 无畏未来 作为一个初学者&#xff0c;水平有限&#xff0c;欢迎交流指正。 在R中线性回归&#xff0c;一般使用lm函数就可以得到线性回归模型&#xff0c;但是得到的模型…

妙趣横生的算法--栈和队列

栈 栈的特点是先进后出&#xff0c;一张图简单介绍一下。 #include "stdio.h" #include "math.h" #include "stdlib.h" #define STACK_INIT_SIZE 20 #define STACKINCRE…

resure挽救笔记本系统和一些相关的操作记录

使用fedora23很久了, 但是感觉不是很流畅, 出现了一些不太稳定的体验, 所以想改到centos7. 因为centos7的很多东西 跟 fedora23 很相近了. 所以应该是无缝过渡是选择32位的系统还是选择64位的系统?还是要使用 32位的 它是90%的人的选择使用, 是普通人的通用选择, 几乎支持linu…

2021-06-08

opencv无法读取mp4文件opencv读取mp4文件时&#xff0c;总是VideoCapture.isopen()返回0,即无法打开cap。解决方法&#xff0c;将opencv安装包的opencv_videoio_ffmpeg451_64文件复制进工程中。

Cocos2d-3.x版的HelloWorld工程分析 (二)

我们HelloWorld 从applicationDidFinishLaunching()后&#xff0c; 大部分人都会从这部分代码开始研究&#xff0c;如果想要研究main函数 如何调用applicationDidFinishLaunching() 传送门 http://blog.csdn.net/hiwoshixiaoyu/article/details/51472707 #include "App…

NO.1 python_人工智能_学习路线

***##学习路线&#xff1a;* 1.python基础 计算机组成原理、python开发环境、python变量、流程控制语句、文件操作、异常处理、模块与包、飞机大战游戏制作等 2.python高级应用 网络编程、并发编程、数据库编程、正则表达式、Linux系统应用、函数的高级应用、python的语法进阶…

iOS开发网络篇—数据缓存

iOS开发网络篇—数据缓存 一、关于同一个URL的多次请求 有时候&#xff0c;对同一个URL请求多次&#xff0c;返回的数据可能都是一样的&#xff0c;比如服务器上的某张图片&#xff0c;无论下载多少次&#xff0c;返回的数据都是一样的。 上面的情况会造成以下问题 &#xff08…

[WinError 10061] 由于目标计算机积极拒绝,无法连接错误解决办法

爬虫的时候会经常出现"[WinError 10061] 由于目标计算机积极拒绝&#xff0c;无法连接"错误这种情况&#xff0c;有可能是LAN口设置不正确 我是在爬取全国天气情况的时候出现的这种错误&#xff0c;后面调了以后可以了1.控制面板——网络和 Internet—— Internet选项…

这门课有什么用?

每个老师都苦恼于学生常问的问题&#xff1a;“某某课学了有什么用&#xff1f;”老师费劲巴拉解释一通&#xff0c;结果还是&#xff1a;然并卵。 一门课有什么用&#xff0c;很难解释得令人信服&#xff0c;因为这和人的认知水平有关。认知水平达不到&#xff0c;解释的多深入…

NO.1_python_scrapy组成爬取多页数据连接数据库配置文件书写

scrapy框架组成及各部分作用 item pipelines: 用于存放需要存储数据的数据模型&#xff0c;一般格式为&#xff1a; #需要存储多少中类型的数据就写多少行&#xff0c;一般是key_value组合 数据名称&#xff0c;即key scrapy.Field()spiders 用于解析返回来的response im…

“智云大咖秀”:大咖摄影师谈惊艳亮相的“大咖级”设备

古人云&#xff0c;善书者不择笔。 古人又云&#xff0c;工欲善其事必先利其器。 古人很矛盾。 这两句话如果用在影像创作这个领域&#xff0c;可以说都有道理&#xff1a;没有好的设备&#xff0c;创意大师一样能够拍出足够惊艳的作品&#xff1b;有足够强的设备&#xff0c;但…

Cobub无码埋点关键技术的实现

随着大数据时代的到来&#xff0c;数据采集也已经变的越来越重要。前端埋点作为一个比较成熟的数据接入手段被广泛应用着。目前埋点分为两种方式&#xff0c;有码与无码埋点。有码埋点比较容易理解&#xff0c;即调用SDK的API&#xff0c;在代码中插入埋点的相关代码&#xff0…

Dedesql数据库类详解(二次开发必备教程)(转)

http://www.dedecms.com/help/development/2009/1028/1076.html 织梦DedeCMS的二次开发不仅仅是会写写织梦的标签&#xff0c;会制作织梦的模板。很多时候&#xff0c;我们需要对织梦DedeCMS的数据库进行查询、插入、删除等等之类的操作&#xff0c;进行这一类的操作之前&#…

留言板

本软件是作为部门内员工之间留言及发送消息使用。 系统必须通过口令验证&#xff0c;登录进入。方法是从数据库内取出用户姓名和口令的数据进行校验。 系统包含四部分功能 1 登录&#xff1a;验证用户名与口令&#xff0c;保存会话信息&#xff0c;进入主界面。 界面显示代码 &…

2017-2018-1 20155301 《信息安全系统设计基础》第7周学习总结

2017-2018-1 20155301 《信息安全系统设计基础》第7周学习总结 教材学习内容总结 作为我们处理器实现的运行实例&#xff0c;因为受x86-64指令集的启发&#xff0c;它被俗称为“x86”&#xff0c;所以我们称我们的指令集为“Y86-64”&#xff0c;与“x86-64”相比&#xff0c;Y…

Dubbo原理解析-监控

Dubbo发布代码中&#xff0c;自带了一个简易的监控中心实现。对于一般的小业务这个监控中心应该能够满足需求&#xff0c;对于那些大业务量的大公司一般都会有自己的监控中心&#xff0c;更加丰富的功能如常用的报警短信通知等等。这章讲解分析使得读者能够了解一般的监控中心实…

学习笔记(04):Python网络编程并发编程-什么是socket

立即学习:https://edu.csdn.net/course/play/24458/296233?utm_sourceblogtoedu 1.什么是网络编程&#xff1f; 答&#xff1a;网络编程也叫做socket编程即套接字编程&#xff0c;基于遵循socket套接字的规定进行的编程 2.什么是socket&#xff1f; 答&#xff1a;socket就是…