前言:
最近小匹夫參與的游戲項目到了需要讀取數據的階段了,那么覺得自己業余時間也該實踐下數據相關的內容。那么從哪入手呢?因為用的是Unity3d的游戲引擎,思來想去就選擇了C#讀取XML文件這個小功能。網上的例子倒也不少,但總是覺得缺點什么。比如讀取xml文件之后該如何處理?看到的文章基本上都是手動創建一個目標類的實例,然后手動從讀取的XML文件的內容中給剛才創建的目標類實例相關字段賦值。缺點什么呢?對嘞,感覺上不夠簡單和智能。
正所謂驅動科技發展的原因就是懶,為了使我們的小工具能夠傻瓜到只需要指定一個需要的目標類型和要讀取的xml的地址就能實現目標類實例的動態生成,下面的文字就誕生了。
需要解決的問題:
問,從xml文件到需要的目標類實例需要幾步?
答,讀取XML文件,實例化一個目標實例,賦值。
問題一:如何讀取XML文件
所以第一個問題就是如何讀取XML文件,參考這篇博客《c#讀取XML》,我們可知備選答案無非如下幾種:
- XmlDocument
- XmlTextReader
- Linq to Xml
1.XmlDocument的使用:
//XmlDocument使用 XmlDocument doc = new XmlDocument(); doc.Load("./Assets/xml-to-egg/xml-to-egg-test/Test.xml");
XmlNode root = doc.SelectSingleNode("Test");
...
但是要注意的是,XmlDocument是讀取整個XML的,所以如果XML內容過多,則會消費很多內存。所以XML內容過大時,不推薦使用XmlDocument。
2.XmlTextReader的使用:
//XmlTestReader的使用方法 XmlTextReader reader = new XmlTextReader("./Assets/xml-to-egg/xml-to-egg-test/Test.xml"); //使用read()方法向下讀取 while (reader.Read()) { ..... }
要說明與XmlDocument的最大區別,其實也很簡單,XmlReader使用Steam(流)來讀取文件,所以不會對內存造成太大的消耗。XmlReader通過read()方法不斷向下讀取,我們就可以在這個過程中進行我們需要的操作。不過這個也不是我們的答案,我們選擇的答案在下面。
3.Linq to Xml
在System.Xml.Linq命名空間中,操作十分簡單和方便。
//Linq to Xml的使用 XElement xml = XElement.Load("./Assets/xml-to-egg/xml-to-egg-test/Test.xml"); //讀取的xml文件的元素都在生成的XElement的實例xml.Elements中。 string name = xml.Element("name").Value; ......
可見十分簡單明了。傳入xml文件的路徑就會返回一個XElement類型的實例,並且xml文件的元素也都存入了XElement實例中。那么我們讀取XML文件的任務就交給它了。
讀取XML相關邏輯的代碼如下:
/// <summary> /// Sets the xml path. /// </summary> public static void SetXmlPath(string p) { path = p; } /// <summary> /// Loads the XML Files. /// </summary> private static XElement LoadXML() { if(path == null) return null; XElement xml = XElement.Load(path); return xml; }
問題二:如何實例化一個目標實例。
假設我們並不知道我們的這個動態讀取XML創建實例並賦值的小工具要處理的是什么類型的對象,那問題就來了,總不能每一個不同的類都對應一套處理方法吧?那也太不智能且代碼太難以復用了。所以這里我們實例化一個目標實例碰到的第一個問題就來了,也就是如何破解目標類型的問題?
答案是使用泛型。
在實例化具體對象的時候,才確定類型,這樣就可以避免由於類型不同而導致的代碼無法復用的問題。
那么,下面我們的小工具---XMLToEgg就要出場了,對,就是一個處理引用類型的泛型類。
public static class XmlToEgg<T> where T : class { }
可是光解決了實例類型的問題還是差一步啊,差點什么呢?對啊,那就是如何實例化一個泛型目標實例。這也就是我們在實例化一個目標實例時遇到的第二個問題。
答案是使用反射。
那下面繼續上代碼:
/// <summary> /// Creates the class initiate. /// </summary> private static void CreateInitiate() { Type t = typeof(T); ConstructorInfo ct = t.GetConstructor(System.Type.EmptyTypes); target = (T)ct.Invoke(null); }
當然這里小匹夫假設我們的目標類的構造函數是不需要參數的,如果需要參數也很簡單,看官們自己可以查到這里就不贅述了。
好了,到這里我們如何創建一個一開始我們不知道是什么類型,只有到創建的時候才知道是什么東西的類的實例的問題就解決了。(好繞)
問題三:如何為創建好的實例中的字段賦值
終於來到了我們的終極問題,也是我們最終的目標,實現從XML到目標類實例的最后一步。在問題二的時候已經說了,作為一個可以復用的工具,對處理的目標類型應該有包容性,那么既然連目標類型都不確定,那么目標類型的字段咋能確定呢?所以這個問題的本質其實就是我不知道目標類有啥字段啊。。。(如果你把字段寫死,是不是就沒有一點擴展性了。。。low爆有木有),那問題連環一個接一個,我既然不知道目標類有啥字段,那我更不可能知道目標類的字段的類型了吧。好,就算我啥都知道,我應該怎么設呢?直接用instance.field = XXX? 圖樣圖森破。
所以問題的本質是明確的:
- 我不知道目標類有啥字段
- 我不知道各個字段是啥類型
- 就算1,2我都知道,但是我就是不知道咋把值賦給相應字段。
正所謂“車到山前必有路,答案還是用反射”。只要能解決上面三個小問題,那么最后這一步就算是邁過去了。話不多說,下面上代碼:
/// <summary> /// attribute assignment, /// 由於反射中設置字段值的方法會涉及到賦值的目標類型和當前類型的轉化, /// 所以需要使用Convert.ChangeType進行類型轉化 /// </summary> public static T ToEgg() { if(target != null) { target = null; } CreateInitiate(); XElement xml = LoadXML(); Type t = target.GetType(); FieldInfo[] fields = t.GetFields(); string fieldName = string.Empty; foreach(FieldInfo f in fields) { fieldName = f.Name; if(xml.Element(fieldName) != null) { f.SetValue(target, Convert.ChangeType(xml.Element(fieldName).Value, f.FieldType)); } } return target; }
所以看代碼就很明白了,簡單介紹一下:
- Q:我不知道目標類有啥字段 A:拿到實例的Type,之后調用GetFields獲取字段。
- Q:我不知道各個字段是啥類型 A: 其實知道賦值目標字段類型的目的就是為了能把從XML中讀取的元素Value類型轉化為字段類型,所以問題就變成了如何把XML的元素Value類型轉化為目標字段類型,所以字段類型為FieldInfo.FieldType,轉化就是Convert.ChangeType(xml.Element(fieldName).Value, f.FieldType)。
- Q:我不知道該如何給字段賦值 A:當然還是用反射,FieldInfo.SetValue(obj, obj)。
這樣,一個處理動態讀取XML創建類實例並賦值的類或者說小工具XMLToEgg就完成了,下面是完整的代碼。
/// <summary> /// XmlToEgg /// Created by chenjd /// http://www.cnblogs.com/murongxiaopifu/ /// https://github.com/chenjd/ /// </summary> using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml.Linq; using System.IO; using System.Reflection; using System.Reflection.Emit; namespace EggToolkit { public static class XmlToEgg<T> where T : class { private static string path; private static T target; static XmlToEgg() { } /// <summary> /// Sets the xml path. /// </summary> public static void SetXmlPath(string p) { path = p; } /// <summary> /// Loads the XML Files. /// </summary> private static XElement LoadXML() { if(path == null) return null; XElement xml = XElement.Load(path); return xml; } /// <summary> /// Creates the class initiate. /// </summary> private static void CreateInitiate() { Type t = typeof(T); ConstructorInfo ct = t.GetConstructor(System.Type.EmptyTypes); target = (T)ct.Invoke(null); } /// <summary> /// attribute assignment, /// 由於反射中設置字段值的方法會涉及到賦值的目標類型和當前類型的轉化, /// 所以需要使用Convert.ChangeType進行類型轉化 /// </summary> public static T ToEgg() { if(target != null) { target = null; } CreateInitiate(); XElement xml = LoadXML(); Type t = target.GetType(); FieldInfo[] fields = t.GetFields(); string fieldName = string.Empty; foreach(FieldInfo f in fields) { fieldName = f.Name; if(xml.Element(fieldName) != null) { f.SetValue(target, Convert.ChangeType(xml.Element(fieldName).Value, f.FieldType)); } } return target; } } }
測試:
完整的項目代碼以及使用方法、測試可以從這里獲取:XMLToEgg(https://github.com/chenjd/Unity3D_XMLToEgg)
裝模作樣的聲明一下:本博文章若非特殊注明皆為原創,若需轉載請保留原文鏈接(http://www.cnblogs.com/murongxiaopifu/p/4175395.html)及作者信息慕容小匹夫
更新(之前在游戲蠻牛更新了,忘了在這里同步)
有童鞋提出了為什么不介紹使用序列化和反序列化?小匹夫覺得這個問題挺好噠。那么就在這里回答一下:
1序列化&反序列化的應用情景一般是類-->xml-->類有一個保存的概念在里面。這里主要介紹的是純粹從xml到類。如果覺得還是沒區別那么看下面。
2.聊聊XmlSerializer的實現。
1)XmlSerializer首先你要告訴它你要序列化的類型。例如。XmlSerializer xs = new XmlSerializer(typeof(chenjiadong));
2)XmlSerializer的構造函數會使用 反射 去掃描這個類的內容(用反射並不生成新的代碼)。
3)之后會生成C#的方法去序列化這個類型(此時會生成新的代碼)。
4)並且會動態編譯C#到IL (這樣做當然有好處,就是在序列化和反序列化進行的過程中無需反射,而是直接生成新的代碼去處理,速度上比反射好的多。但是在IOS上新的IL意味着什么呢?)
5)所以,不管你是序列化,還是反序列化,都會有上面的4個步驟。
3.聊聊這篇文章的目的:細說的含義其實就是講下原理。你可以把文中的XmlToEgg就當成一個類似處理工具,不過本文的目的是介紹XML的讀取,泛型和反射,XmlToEgg是個衍生品。而且其實它的使用也很簡單。