1. 程式人生 > >[Unity3D·CSV篇]02.CSV高手級讀取

[Unity3D·CSV篇]02.CSV高手級讀取

在上一篇中,儲存CSV檔案每一行的資料為CSVDemo物件時,總是要用if條件判斷當前是哪個欄位,然後再給物件賦值。這樣太煩人了,要是在開發的過程中不斷地有新的想法,不斷地新增修改欄位,那真是改死人了。甚至完全失去了改動的興趣,於是遊戲的創意就大打折扣,後果十分嚴重,嗯嗯。(旁白:說了這麼多,接下來就要宣傳你的“產品”了吧?不過,大家不用怕,有木頭在,木頭有個好招,大家看招吧。

1. 臨時的字典結構

為了以後更方便,我們在搭建程式碼的時候,通常都會麻煩一點,但是,一勞永逸的事情,我是不會抗拒的。

為了實現我們的目的,我們需要先用一個字典結構把CSV檔案儲存起來,如下程式碼:

  1.     /// <summary>
  2.     /// 讀取CSV檔案
  3.     /// 結果儲存到字典集合,以ID作為Key值,對應每一行的資料,每一行的資料也用字典集合儲存。
  4.     /// </summary>
  5.     /// <param name="filePath"></param>
  6.     /// <returns></returns>
  7.     public static Dictionary<string, Dictionary<stringstring>> LoadCsvFile(string filePath)
  8.     {
  9.         Dictionary<string
    , Dictionary<stringstring>> result = new Dictionary<string, Dictionary<stringstring>>();
  10.         string[] fileData = File.ReadAllLines(filePath);
  11.         /* CSV檔案的第一行為Key欄位,第二行開始是資料。第一個欄位一定是ID。 */
  12.         string[] keys = fileData[0].Split(',');
  13.         for (int i = 1; i < fileData.
    Length; i++)
  14.         {
  15.             string[] line = fileData[i].Split(',');
  16.             /* 以ID為key值,建立一個新的集合,用於儲存當前行的資料 */
  17.             string ID = line[0];
  18.             result[ID] = new Dictionary<stringstring>();
  19.             for (int j = 0; j < line.Length; j++)
  20.             {
  21.                 /* 每一行的資料儲存規則:Key欄位-Value值 */
  22.                 result[ID][keys[j]] = line[j];
  23.             }
  24.         }
  25.         return result;
  26.     }
實際上大部分程式碼和上一篇是相同的,主要的改動解釋如下:a. 暫時拋棄了CSVDemo類,把CSV檔案以【Dictionary<string, Dictionary<string, string>>】的形式儲存起來b. 相當於是,以每一行的ID作為key值,value值儲存的是每一行的所有資料c. 每一行的所有資料又由一個Dictionary<string, string>儲存,key值是欄位名,value值是每一列的資料值d. 最終,每一行都以這樣的形式儲存起來這樣儲存起來的結構並不是很方便,因為型別都是字串,不好取值,沒關係,這個只是過度的。

2. 強大的反射

你是否曾經有這樣的感覺,那些看起來明明一樣的程式碼,僅僅只是型別不一樣;那些看起來明明是一樣的程式碼,僅僅只是呼叫的屬性不一樣。總之,那些看起來明明不應該那麼重複的程式碼,它卻總是重複了。大多數開發者都會有這樣的感覺,於是,逆天的反射和泛型出現在我們的面前。沒錯,木頭要用的就是反射+泛型這個強力的組合。看看優化後的程式碼:
  1.         /* 把CSV檔案按行存放,每一行的ID作為key值,內容作為value值 */
  2.         Dictionary<int, CSVDemo> csvDataDic = new Dictionary<int, CSVDemo>();
  3.         /* CSV檔案路徑 */
  4.         string filePath = Application.streamingAssetsPath + "/CSVDemo.csv";
  5.         /* 從CSV檔案讀取資料 */
  6.         Dictionary<string, Dictionary<stringstring>> datasDic = LoadCsvFile(filePath);
  7.         /* 遍歷每一行資料 */
  8.         foreach (string ID in datasDic.Keys)
  9.         {
  10.             /* CSV的一行資料 */
  11.             Dictionary<stringstring> datas = datasDic[ID];
  12.             /* 讀取Csv資料物件的屬性 */
  13.             PropertyInfo[] props = typeof(CSVDemo).GetProperties();
  14.             /* 使用反射,將CSV檔案的資料賦值給CSV資料物件的相應欄位,要求CSV檔案的欄位名和CSV資料物件的欄位名完全相同 */
  15.             CSVDemo obj = new CSVDemo();
  16.             foreach (PropertyInfo pi in props)
  17.             {
  18.                 pi.SetValue(obj, Convert.ChangeType(datas[pi.Name], pi.PropertyType)null);
  19.             }
  20.             /* 按ID-資料的形式儲存 */
  21.             csvDataDic[obj.ID] = obj;
  22.         }
主要修改如下:a. 利用之前寫的LoadCsvFile函式獲取到字典結構的CSV檔案內容b. 遍歷字典結構,取出每一行的內容c. 利用反射獲取CSVDemo類的所有屬性d. 便利CSVDemo類的所有屬性,通過每一個屬性名字到字典裡獲取資料內容,然後給CSVDemo物件相應的屬性賦值e. pi.SetValue就是用來給CSVDemo物件的某個屬性賦值的,Convert.ChangeType是為了把字串內容轉換為屬性對應的型別最後來測試一下:
  1.         /* 測試讀取ID為1的資料 */
  2.         CSVDemo csvDemo1 = csvDataDic[1];
  3.         Debug.Log("ID=" + csvDemo1.ID + ",Name=" + csvDemo1.Name);
嘗試獲取ID為1的那一行資料,輸出日誌如下:結果是完全一樣的,但是我們不需要再一個個if條件去判斷然後賦值了,反射幫我們做了這件事情。(旁白:那泛型呢?

3. 泛型呢?

大家一定還有疑問,每個CSV檔案都要寫這樣一段程式碼來讀取嗎?不,這種重複的事情木頭是絕對不會做的,我一做重複的事情就會頭暈,於是,泛型救了我。最終完美的讀取並儲存CSV檔案的函式是這樣的:
  1.     /// <summary>
  2.     /// 讀取CSV檔案資料(利用反射)
  3.     /// </summary>
  4.     /// <typeparam name="CsvData">CSV資料物件的型別</typeparam>
  5.     /// <param name="csvFilePath">CSV檔案路徑</param>
  6.     /// <param name="csvDatas">用於快取資料的字典</param>
  7.     /// <returns>CSV檔案所有行內容的資料物件</returns>
  8.     private Dictionary<int, T_CsvData> LoadCsvData<T_CsvData>(string csvFilePath)
  9.     {
  10.         Dictionary<int, T_CsvData> dic = new Dictionary<int, T_CsvData>();
  11.         /* 從CSV檔案讀取資料 */
  12.         Dictionary<string, Dictionary<stringstring>> result = LoadCsvFile(csvFilePath);
  13.         /* 遍歷每一行資料 */
  14.         foreach (string ID in result.Keys)
  15.         {
  16.             /* CSV的一行資料 */
  17.             Dictionary<stringstring> datas = result[ID];
  18.             /* 讀取Csv資料物件的屬性 */
  19.             PropertyInfo[] props = typeof(T_CsvData).GetProperties();
  20.             /* 使用反射,將CSV檔案的資料賦值給CSV資料物件的相應欄位,要求CSV檔案的欄位名和CSV資料物件的欄位名完全相同 */
  21.             T_CsvData obj = Activator.CreateInstance<T_CsvData>();
  22.             foreach (PropertyInfo pi in props)
  23.             {
  24.                  pi.SetValue(obj, Convert.ChangeType(datas[pi.Name], pi.PropertyType)null);
  25.             }
  26.             /* 按ID-資料的形式儲存 */
  27.             dic[Convert.ToInt32(ID)] = obj;
  28.         }
  29.         return dic;
  30.     }
基本上把CSVDemo型別改為T_CsvData泛型即可,呼叫方式如下:
  1.         /* 把CSV檔案按行存放,每一行的ID作為key值,內容作為value值 */
  2.         Dictionary<int, CSVDemo> csvDataDic2 = LoadCsvData<CSVDemo>(filePath);
  3.         /* 測試讀取ID為2的資料 */
  4.         CSVDemo csvDemo2 = csvDataDic2[2];
  5.         Debug.Log("ID=" + csvDemo2.ID + ",Name=" + csvDemo2.Name);
輸出的日誌如下:

怎麼樣?以後讀取CSV檔案,只需呼叫【LoadCsvData<CSVDemo>(filePath);】即可。

4. 快取?

大家肯定還有疑問,每次要用到CSV檔案的資料時,都載入一遍,太沒效率了吧?你說得對,這樣太沒效率了,所以下一篇,木頭要開始聊聊如何用最帥的方式快取CSV檔案物件了。