Unity:XML笔记(二)——Xml序列化、反序列化、IXmlSerializable接口
写在前面:
写本系列(自用)的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。
三、Xml序列化
序列化就是把想要存储的内容转换为字节序列用于存储或传递。
1、序列化
我们先创建一个类,之后利用Xml序列化来存储这个类:
public class Test
{public int testPublic = 10;private int testPrivate = 11;protected int testProtected = 12;internal int testInternal = 13;public string testPublicStr = "123";public int testPro { get; set; }public Test2 testClass = new Test2();public int[] arrayInt = new int[3] { 5, 6, 7 };public List<int> listInt = new List<int>() { 1, 2, 3, 4 };public List<Test2> listItem = new List<Test2>() { new Test2(), new Test2() };
}public class Test2
{public int test1 = 1;public float test2 = 1.1f;public bool test3 = true;
}
Xml序列化的第一步是确认存储路径:string path = Application.persistentDataPath + "/Test.xml";该存储路径设置方式和之前使用XmlDocument存储的方式一样。
序列化存储需要在一个using代码块中,如下:
using (StreamWriter stream = new StreamWriter(path)){ }
现在对这行代码进行解释。StreamWriter是向文件流写入字符的类,属于System.IO命名空间。括号内的代码的意思是:写入一个文件流,如果有该文件,直接打开并修改;如果没有该文件,自动释放掉。new StreamWriter()
这里还涉及到using 的新用法:括号内包裹的声明的对象,会在大括号语句块结束后自动释放掉。当语句块结束时会自动调用对象的Dispose方法,让其销毁。using一般是配合 内存占用比较大或者有读写操作时进行使用。
在语句块中,我们需要创建一个“序列化机器”来将我们的类序列化:
XmlSerializer s = new XmlSerializer(typeof(Test));
需要注意的是,序列化机器的类型,一定是要和我们需要序列化存储的对象是同样的类型。接下来就可以使用序列化机器进行序列化:
s.Serialize(stream, lt);
这句代码通过序列化机器,对我们类对象进行翻译,将其翻译成xml文件写入到对应文件中。第一个参数:文件流对象;第二个参数:想要被翻译的对象。这样,就完成了序列化存储:
public class lession1 : MonoBehaviour
{void Start(){Test lt = new Test();string path = Application.persistentDataPath + "/Test.xml";using (StreamWriter stream = new StreamWriter(path)){XmlSerializer s = new XmlSerializer(typeof(Test));s.Serialize(stream, lt);}}
}
我们可以在我们设置的保存路径中,找到序列化后的Xml文件:
<?xml version="1.0" encoding="utf-8"?>
<Test xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><testPublic>10</testPublic><testPublicStr>123</testPublicStr><testClass><test1>1</test1><test2>1.1</test2><test3>true</test3></testClass><arrayInt><int>5</int><int>6</int><int>7</int></arrayInt><listInt><int>1</int><int>2</int><int>3</int><int>4</int></listInt><listItem><Test2><test1>1</test1><test2>1.1</test2><test3>true</test3></Test2><Test2><test1>1</test1><test2>1.1</test2><test3>true</test3></Test2></listItem><testPro>0</testPro>
</Test>
可以看到,我们类中private、protected、internal修饰的变量都没有被序列化。也就是说,Xml序列化方法只能序列化公共成员。此外,不支持字典序列化,如果类中有字典,就会报错。
2、修改节点信息或设置属性信息
可以通过特性修改节点信息或者设置属性信息。例如,如果需要将Test2类中的成员以属性方式存储,可以加上特性:[XmlAttribute()],如果想修改属性的名字,可以在括号内传入想要的属性名:[XmlAttribute("Test1")。
public class Test2
{[XmlAttribute("Test1")]public int test1 = 1;[XmlAttribute()]public float test2 = 1.1f;[XmlAttribute()]public bool test3 = true;
}
如果想要修改变量的名字,可以加上特性:[XmlElement("testPublic111")],括号内传入属性名:
[XmlElement("testPublic111")]
public int testPublic = 10;
如果想要修改数组的名字,可以添加特性:[XmlArray("IntList")],如果想要修改数组中元素节点的名字,可以用特性:[XmlArrayItem("Int32")]
[XmlArray("IntList")]
[XmlArrayItem("Int32")]
public int[] arrayInt = new int[3] { 5, 6, 7 };
四、Xml反序列化
反序列化就是把存储或收到的字节序列信息解析读取出来使用。
1、判断文件是否存在
在反序列化之前,需要判断文件是否存在。判断方式是:File.Exists(path),括号中传入的是文件地址。
void Start()
{string path = Application.persistentDataPath + "/Test.xml";if(File.Exists(path)){}
}
2、反序列化
反序列化和序列化基本相同,区别就是这里使用的是StreamReader,从流中读出字符的类。如下:using (StreamReader reader = new StreamReader(path)){ }
同样的,在using语句块中,初始化一个反序列化翻译机器: XmlSerializer s = new XmlSerializer(typeof(Test));
然后调用s.Deserialize(reader)方法即可完成反序列化:
void Start()
{string path = Application.persistentDataPath + "/Test.xml";if(File.Exists(path)){using (StreamReader reader = new StreamReader(path)){XmlSerializer s = new XmlSerializer(typeof(Test));Test lt = s.Deserialize(reader) as Test;}}
}
这里需要注意的是,在三中定义Test类时,为了方便,很多值都是直接在类中初始化的。对于List对象,如果有默认值,反序列化时不会清空而是会往后继续添加,所以最好不要在类中直接初始化。
五、IXmlSerializable接口
C#的的XmlSerializer提供了可扩展内容,可以让一些不能被序列化和反序列化的特殊类能被处理,例如字典。让特殊类继承 IXmlSerializable接口实现其中的方法即可。
1、自定义序列化和反序列化
先按三、四所学知识创建一个类并书写序列化和反序列化方法:
public class Test3
{public int test1;public string test2;
}public class lession3 : MonoBehaviour
{void Start(){Test3 t = new Test3();string path = Application.persistentDataPath + "/Test3.xml";using (StreamWriter writer = new StreamWriter(path)){XmlSerializer s = new XmlSerializer(typeof(Test3));s.Serialize(writer, t);}using(StreamReader reader = new StreamReader(path)){XmlSerializer s = new XmlSerializer(typeof(Test3));Test3 t2 = s.Deserialize(reader) as Test3;}}
}
这段代码实现了对类Test3进行序列化和反序列化。这里补充一点,在序列化时 如果对象中的引用成员为空 那么xml里面是看不到该字段的,所以这里的xml文件中没有string。
接下来,我们就可以开始自定义序列化方法和反序列化方法。首先需要类Test3继承IXmlSerializable接口,并在Test3中重写接口中的函数:
其中public XmlSchema GetSchema()暂时不需要了解,该函数返回结构,直接return null即可。public void ReadXml(XmlReader reader)是反序列化会调用的方法,在其中书写的代码能够替换掉该类原来的反序列化函数。public void WriteXml(XmlWriter writer)是序列化会调用的方法。这两个函数可以定义序列化的规则。
public class Test3:IXmlSerializable
{public int test1;public string test2;//返回结构public XmlSchema GetSchema(){return null;}//反序列化时会自动调用方法public void ReadXml(XmlReader reader){}//序列化时会自动调用的方法public void WriteXml(XmlWriter writer){}
}
(1)自定义读属性和写属性
首先来自定义读属性和写属性的规则。如果要自定义序列化的规则,一定会用到XmlWriter、XmlReader中的一些方法。
对于写属性,XmlWriter是写入器对象提供一系列方法来生成和写入 XML 格式的数据。可以使用:writer.WriteAttributeString()写入属性,括号内传入的第一个参数是属性名,第二个参数是属性的内容。
对于读属性,XmlReader则是用于读入数据的工具类。可以通过reader["test1"]来获得[]内属性的值。如下所示:
public void ReadXml(XmlReader reader)
{this.test1 = int.Parse(reader["test1"]);this.test2 = reader["test2"];
}public void WriteXml(XmlWriter writer)
{writer.WriteAttributeString("test1", this.test1.ToString());writer.WriteAttributeString("test2", this.test2)
}
(2)自定义读节点和写节点
①方式1
写节点可以通过XmlWriter的方法:writer.WriteElementString(),括号内分别传入节点名、节点的数值即可。
读节点需要用reader.Read(),表示逐步读。一开始Reader位于根节点Test3,调用reader.Read()后读到test1节点,继续调用reader.Read()后读到test1节点包裹的内容,此时就可以将该值读出来。继续调用reader.Read()后读到尾部包裹节点,再调用reader.Read()读到test2节点...以此类推。
这里为了方便看所以给test2赋值为了123再进行读写数据。
public void ReadXml(XmlReader reader)
{reader.Read();//这时是读到的test1节点reader.Read();//这时是读到的test1节点包裹的内容this.test1 = int.Parse(reader.Value);reader.Read();//尾部包裹节点reader.Read();//这时读到的是test2节点reader.Read();//读到的是test2节点包裹的内容this.test2 = reader.Value;
}public void WriteXml(XmlWriter writer)
{writer.WriteElementString("test1", this.test1.ToString());writer.WriteElementString("test2", this.test2);
}
②方式2
方式①读节点的重复代码太多了,可以采用方式2这种写法:
while(reader.Read())
{if(reader.NodeType == XmlNodeType.Element){switch(reader.Name){case "test1":reader.Read();this. test1 = int.Parse(reader.Value);break;case "tese2":reader.Read();this.test2 = reader.Value;break;}}
}
(3)自定义读写包裹节点
如果想自定义写包裹节点,类似于下图,test1中包裹着节点int,test2中包裹着节点string
①写
以第一个写int为例。需要先声明一个序列化器
XmlSerializer s = new XmlSerializer(typeof(int));
然后使用节点相关API:这两句代码表示开始节点test1,结束节点test1,在这两行中间定义的节点就会被包裹在节点test1中间。
writer.WriteStartElement("test1");writer.WriteEndElement();
例如,在以上两句代码中间使用序列化器写入节点teat的值,序列化器是int类型的,所以会生成<int>0</int>节点:
writer.WriteStartElement("test1");
s.Serialize(writer, test1);
writer.WriteEndElement();
②读
同样以读int为例,首先需要创建一个序列化器:
XmlSerializer s = new XmlSerializer(typeof(int));
调用reader.Read()让reader指向test1:
reader.Read();
然后可以使用以下两句代码,表示读test1节点的开始于结束:
reader.ReadStartElement("test1");reader.ReadEndElement();
最后在中间写需要读入的数据即可:
reader.ReadStartElement("test1");
test1 = (int)s.Deserialize(reader);
reader.ReadEndElement();
完整代码:
public void ReadXml(XmlReader reader)
{XmlSerializer s = new XmlSerializer(typeof(int));reader.Read();reader.ReadStartElement("test1");test1 = (int)s.Deserialize(reader);reader.ReadEndElement();XmlSerializer s2 = new XmlSerializer(typeof(string));reader.ReadStartElement("test2");test2 = (string)s2.Deserialize(reader);reader.ReadEndElement();
}public void WriteXml(XmlWriter writer)
{XmlSerializer s = new XmlSerializer(typeof(int));writer.WriteStartElement("test1");s.Serialize(writer, test1);writer.WriteEndElement();XmlSerializer s2 = new XmlSerializer(typeof(string));writer.WriteStartElement("test2");s2.Serialize(writer, test2);writer.WriteEndElement();
}
2、让Dictionary支持序列化反序列化
相当于以上知识的一个小应用,可以拓展一个可以被序列化和反序列化的字典类,所以不多解释:
public class SerializerDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{public XmlSchema GetSchema(){return null;}public void ReadXml(XmlReader reader){XmlSerializer keySer = new XmlSerializer(typeof(TKey));XmlSerializer ValueSer = new XmlSerializer(typeof(TValue));reader.Read();while(reader.NodeType != XmlNodeType.EndElement){TKey key = (TKey)keySer.Deserialize(reader);TValue value = (TValue)ValueSer.Deserialize(reader);this.Add(key, value);}reader.Read();}public void WriteXml(XmlWriter writer){XmlSerializer keySer = new XmlSerializer(typeof(TKey));XmlSerializer ValueSer = new XmlSerializer(typeof(TValue));foreach(KeyValuePair<TKey, TValue> kv in this){keySer.Serialize(writer, kv.Key);ValueSer.Serialize(writer, kv.Value);}}
}