有時候一些簡單的資料快取, 遭遇到了不同型別的變數, 就會變得比較尷尬, 比如遠程發來的json資料是個number型別的, 可能是數字型別, 可是也可能是可空型別:
{ "value": 1 }
{ "value": null }
如果在測驗用例中給的是第一個, 可能我們本地的自動生成就生成出int型別了, 或者它 { "value" : 1.2 } 本地就生成float型別之類的, 總之就是無法保證自動生成的對應型別能正確匹配:
public class AA { public int value; }
public class AA { public int? value; }
這時候還不如封裝一個可自動轉換的資料型別DataTable, 以前在游戲中也是經常使用的, 因為資料有來自服務器的, 來自Lua的, 還有來自配置表或本地資料庫的, 總之沒有辦法統一起來.
先來看看基礎型別, bool, int這些的, 因為我們要做通用存盤, 所以基礎型別都要有, 還要讓他們共享記憶體空間以節省記憶體消耗, 就是Union結構體了:
[StructLayout(LayoutKind.Explicit, Size = 8)] private struct VariantData { [FieldOffset(0)] public bool boolVal; [FieldOffset(0)] public char charVal; [FieldOffset(0)] public byte byteVal; [FieldOffset(0)] public sbyte sbyteVal; [FieldOffset(0)] public short shortVal; [FieldOffset(0)] public ushort ushortVal; [FieldOffset(0)] public int intVal; [FieldOffset(0)] public uint uintVal; [FieldOffset(0)] public float floatVal; [FieldOffset(0)] public long longVal; [FieldOffset(0)] public ulong ulongVal; [FieldOffset(0)] public double doubleVal; }
把基礎型別一起封在 VariantData 里, 共用記憶體.
然后是 DataTable 類, 為了讀取高效性也是使用結構體, 它里面有 VariantData 以及一個 object 作為一般型別的參考, 其實這些都是參考了System.TypeCode 以及 IConvertible 介面而來的:
using DataType = System.TypeCode; namespace Common { public struct DataTable { public DataType dataType { get; private set; } private VariantData _variantData; private object _userData; } }
這樣基礎型別就能通過_variantData設定和獲取了, 而一般型別和string型別就存盤在_userData中即可, 因為希望把dataType暴露出去, 所以就不強行使用 StructLayout 屬性了, 讓它自動排列吧, 反正都是4位元組的倍數, 這樣來算一下至少需要 4+4+8=16位元組, 犧牲了一些存盤.
有了資料之后就看怎樣進行數值修改了, 修改比較簡單, 可以通過泛型搞定, 因為保存資料的時候我們只關心資料的型別和存盤位置, 所以可以通過泛型直接保存資料即可, 不過在怎樣保存基礎資料的地方還是要做些微操的:
using System; using System.Runtime.InteropServices; using DataType = System.TypeCode; namespace Common { public struct DataTable { [StructLayout(LayoutKind.Explicit, Size = 8)] private struct VariantData { // ...... } private static class Setter<TVal> { public static VariantDataAccess<TVal> SetterCall = null; } delegate void VariantDataAccess<T>(ref VariantData variantData, T value); static DataTable() { Setter<bool>.SetterCall = SetData; Setter<char>.SetterCall = SetData; Setter<byte>.SetterCall = SetData; Setter<sbyte>.SetterCall = SetData; Setter<short>.SetterCall = SetData; Setter<ushort>.SetterCall = SetData; Setter<int>.SetterCall = SetData; Setter<uint>.SetterCall = SetData; Setter<float>.SetterCall = SetData; Setter<long>.SetterCall = SetData; Setter<ulong>.SetterCall = SetData; Setter<double>.SetterCall = SetData; } public DataType dataType { get; private set; } private VariantData _variantData; private object _userData; public void SetData<T>(T data) { var typeCode = Type.GetTypeCode(typeof(T)); this.dataType = typeCode; switch(typeCode) { case TypeCode.String: case TypeCode.Object: { _userData = data; } break; default: { var call = Setter<T>.SetterCall; if(call != null) { call.Invoke(ref _variantData, data); _userData = null; } else { UnityEngine.Debug.LogError("Not Support DataType : " + typeof(T).Name); } } break; } } #region Delegates private static void SetData(ref VariantData variantData, bool value) { variantData.longVal = 0; variantData.boolVal = value; } private static void SetData(ref VariantData variantData, char value) { variantData.longVal = 0; variantData.charVal = value; } private static void SetData(ref VariantData variantData, byte value) { variantData.longVal = 0; variantData.byteVal = value; } private static void SetData(ref VariantData variantData, sbyte value) { variantData.longVal = 0; variantData.sbyteVal = value; } private static void SetData(ref VariantData variantData, short value) { variantData.longVal = 0; variantData.shortVal = value; } private static void SetData(ref VariantData variantData, ushort value) { variantData.longVal = 0; variantData.ushortVal = value; } private static void SetData(ref VariantData variantData, int value) { variantData.longVal = 0; variantData.intVal = value; } private static void SetData(ref VariantData variantData, uint value) { variantData.longVal = 0; variantData.uintVal = value; } private static void SetData(ref VariantData variantData, float value) { variantData.longVal = 0; variantData.floatVal = value; } private static void SetData(ref VariantData variantData, long value) { variantData.longVal = value; } private static void SetData(ref VariantData variantData, ulong value) { variantData.ulongVal = value; } private static void SetData(ref VariantData variantData, double value) { variantData.doubleVal = value; } #endregion } }
這里對 VariantData 進行設定變數的時候, 使用了一個 Setter<TVal> 的靜態型別, 它里面是一個 VariantDataAccess<TVal> 回呼, 這樣就能高效地定義變數設定函式的呼叫了, 簡單的一個Type選擇器, 通過靜態呼叫減少運行開銷, 這也是一種實作泛型呼叫的小技巧.
PS : 每次將longVal設為0是清除一遍記憶體, 因為后面的獲取代碼沒有作約束, 所以要添加清記憶體操作.
然后復雜的是怎樣獲取資料, 因為我們的輸入資料可能是各種型別的, 比如輸入 int 型別輸出 char 型別, 都有可能, 如果能夠通過泛型解決那就是最好的了, 不僅代碼簡潔還能避免各種裝箱拆箱操作, 不過暫時沒有找到很好的泛型方法來解決, 先看看怎樣通過資料轉換來獲取資料先吧:
using System; using System.Runtime.InteropServices; using DataType = System.TypeCode; namespace Common { public struct DataTable { [StructLayout(LayoutKind.Explicit, Size = 8)] private struct VariantData { // ...... } // ...... public DataType dataType { get; private set; } private VariantData _variantData; private object _userData; // ...... public int ToInt() { var doubleValue =https://www.cnblogs.com/tiancaiwrk/p/ GetCurrentVariantDataValue(); var retVal = Convert<double, Int32>(doubleValue); return retVal; } private double GetCurrentVariantDataValue() { switch(dataType) { case DataType.Boolean: case DataType.Char: case DataType.Byte: case DataType.SByte: case DataType.Int16: case DataType.UInt16: case DataType.Int32: case DataType.UInt32: case DataType.Int64: { return _variantData.longVal; } case DataType.UInt64: { return _variantData.ulongVal; } case DataType.Single: { return _variantData.floatVal; } case DataType.Double: { return _variantData.doubleVal; } case DataType.String: { return Convert<string, double>(_userData as string); } } return 0; } // FIX : 整型負數使用補碼, 不能用高位代替低位變數 private double GetCurrentVariantDataValue() { switch(dataType) { case DataType.Boolean: { return _variantData.boolVal ? 1 : 0; } case DataType.Char: { return _variantData.charVal; } case DataType.SByte: { return _variantData.sbyteVal; } case DataType.Byte: { return _variantData.byteVal; } case DataType.Int16: { return _variantData.shortVal; } case DataType.UInt16: { return _variantData.ushortVal; } case DataType.Int32: { return _variantData.intVal; } case DataType.UInt32: { return _variantData.uintVal; } case DataType.Int64: { return _variantData.longVal; } case DataType.UInt64: { return _variantData.ulongVal; } case DataType.Single: { return _variantData.floatVal; } case DataType.Double: { return _variantData.doubleVal; }case DataType.String: { return Convert<string, double>(_userData as string); } } return 0; } public static TVal Convert<T, TVal>(T raw) { try { return (TVal)System.Convert.ChangeType(raw, typeof(TVal)); } catch { return default(TVal); } } } }
GetCurrentVariantDataValue() 方法用來獲取對應的值型別資料, 而基礎資料型別中, 整型資料的結構都是相似的, 所以高位的整型可以兼容低位整型資料, 所以用long型別來表達低位的int, uint, byte...等( 每次修改變數的時候都進行了記憶體清除, PS : 整型的負數使用補碼, 所以不能使用高位變數替代低位變數!!! ), 而它的回傳值全部自動轉換為double, 可以減少一次裝箱程序, 在型別為 String 的時候同樣如果轉換為double的泛用性更強, 避免直接轉換整型出錯.
雖然功能上沒有什么問題了, 不過在型別轉換上不怎么好看, 比如一個string的資料, 要輸出為int, 需要經過:
1. 字符轉換成double
2. double轉成int
這些程序...并且都經過 System.Convert.ChangeType() 方法, 有裝箱操作...
然后如果每次獲取都進行字符轉換的話, 就很浪費時間, 下面試試修改一下裝箱, 然后做一個臨時快取來解決轉換開銷問題:
using System; using System.Runtime.InteropServices; using DataType = System.TypeCode; namespace Common { public struct DataTable { [StructLayout(LayoutKind.Explicit, Size = 8)] private struct VariantData { // ...... } public DataType dataType { get; private set; } private VariantData _variantData; private object _userData; private DataType _parsedDataType; // 添加臨時快取 private VariantData _parsedVariantData; // 添加臨時快取 public void SetData<T>(T data) { _parsedDataType = DataType.Empty; var typeCode = Type.GetTypeCode(typeof(T)); this.dataType = typeCode; switch(typeCode) { case TypeCode.String: case TypeCode.Object: { _userData = data; } break; default: { var call = Setter<T>.SetterCall; if(call != null) { call.Invoke(ref _variantData, data); _parsedDataType = typeCode; _parsedVariantData = _variantData; _userData = null; } else { UnityEngine.Debug.LogError("Not Support DataType : " + typeof(T).Name); } } break; } } public int ToInt() { if(_parsedDataType == DataType.Int32) { return _parsedVariantData.intVal; } var doubleValue =https://www.cnblogs.com/tiancaiwrk/p/ GetCurrentVariantDataValue(); var retVal = System.Convert.ToInt32(doubleValue); _parsedDataType = DataType.Int32; _parsedVariantData.intVal = retVal; return retVal; } // FIX : 忘了負數使用的是補碼, 并不能是使用高位變數代替低位變數 private double GetCurrentVariantDataValue() { switch(dataType) { case DataType.Boolean: { return _variantData.boolVal ? 1 : 0; } case DataType.Char: { return _variantData.charVal; } case DataType.SByte: { return _variantData.sbyteVal; } case DataType.Byte: { return _variantData.byteVal; } case DataType.Int16: { return _variantData.shortVal; } case DataType.UInt16: { return _variantData.ushortVal; } case DataType.Int32: { return _variantData.intVal; } case DataType.UInt32: { return _variantData.uintVal; } case DataType.Int64: { return _variantData.longVal; } case DataType.UInt64: { return _variantData.ulongVal; } case DataType.Single: { return _variantData.floatVal; } case DataType.Double: { return _variantData.doubleVal; } case DataType.String: { return (_userData as IConvertible).ToDouble(null); } } return 0; } } }
恩, 這樣在所有地方都使用了非裝箱操作了, 然后還添加了臨時快取, 在SetData的時候會對臨時快取進行清除, 在獲取資料的時候會根據上次獲取的結果決定回傳快取或是重新獲取, 雖然占用記憶體又變大了, 達到 16 +12 = 28Byte大小了.
同樣在判斷快取型別上也是可以再繼續優化的, 先忽略. 這樣大體上一個DataTable就完成了, 在 get / set 方面沒有過多的性能消耗, 這就是最基本要求.
PS : 在轉換字串ToString方面有點歧義, 這里直接new了原方法, 各個需求不同...
public new string ToString() { switch(dataType) { case DataType.String: { return _userData as string; } case DataType.Object: { return _userData != null ? _userData.ToString() : "NULL"; } case DataType.Char: { return ToChar().ToString(); } default: { return GetCurrentVariantDataValue().ToString(); } } }
Char型別的話, 輸出還是應該從Char轉換來, 不應該由number轉換來.
PS : 同樣ToBool, ToChar都是要特殊處理的, 因為bool的字串式序列化也是不能從number來, 而Char在型別為String的時候也不能通過轉換number來, 這里可以直接截取第一個字符而來.......這樣就完全不能使用泛型了.
public bool ToBool() { if(dataType == DataType.Boolean) { return _variantData.boolVal; } if(_parsedDataType == DataType.Boolean) { return _parsedVariantData.boolVal; } bool retVal = false; if(dataType == DataType.String) { try { retVal = System.Convert.ToBoolean(_userData as string); } catch { return default(bool); } } else { var doubleValue =https://www.cnblogs.com/tiancaiwrk/p/ GetCurrentVariantDataValue(); retVal = System.Convert.ToBoolean(doubleValue); } _parsedDataType = DataType.Boolean; _parsedVariantData.boolVal = retVal; return retVal; } public char ToChar() { if(dataType == DataType.Char) { return _variantData.charVal; } if(_parsedDataType == DataType.Char) { return _parsedVariantData.charVal; } char retVal = default(char); if(dataType == DataType.String) { try { var str = _userData as string; retVal = str[0]; } catch { return default(char); } } else { var byteValue =https://www.cnblogs.com/tiancaiwrk/p/ ToByte(); retVal = System.Convert.ToChar(byteValue); } _parsedDataType = DataType.Char; _parsedVariantData.charVal = retVal; return retVal; }
然后是最重要的應用上了, 為了方便使用, 添加一些operator運算子:
#region operators public static explicit operator bool(DataTable data) { return data.ToBool(); } public static explicit operator char(DataTable data) { return data.ToChar(); } public static explicit operator byte(DataTable data) { return data.ToByte(); } public static explicit operator sbyte(DataTable data) { return data.ToSByte(); } public static explicit operator short(DataTable data) { return data.ToShort(); } public static explicit operator ushort(DataTable data) { return data.ToUShort(); } public static explicit operator int(DataTable data) { return data.ToInt(); } public static explicit operator uint(DataTable data) { return data.ToUInt(); } public static explicit operator float(DataTable data) { return data.ToFloat(); } public static explicit operator long(DataTable data) { return data.ToLong(); } public static explicit operator ulong(DataTable data) { return data.ToULong(); } public static explicit operator double(DataTable data) { return data.ToDouble(); } public static explicit operator string(DataTable data) { return data.ToString(); } public static implicit operator DataTable(bool value) { var table = new DataTable(); table.SetData(value); return table; } public static implicit operator DataTable(char data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(byte data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(sbyte data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(short data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(ushort data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(int data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(uint data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(float data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(long data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(ulong data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(double data) { var table = new DataTable(); table.SetData(data); return table; } public static implicit operator DataTable(string data) { var table = new DataTable(); table.SetData(data); return table; } #endregion
把DataTable轉換為基礎型別的方法使用顯式轉換, 基礎型別轉換成DataTable的方法使用隱式轉換, 這樣在使用上比較簡單明了, 可以看下面幾個例子:
1. 一般用法
Common.DataTable dataTable = "123.456"; Debug.Log("Float : " + (float)dataTable); Debug.Log("Int : " + (int)dataTable); Debug.Log("Bool : " + (bool)dataTable); Debug.Log("Double : " + (double)dataTable); Debug.Log("Short : " + (short)dataTable); Debug.Log("ULong : " + (ulong)dataTable); Debug.Log("---------- A ----------"); dataTable.SetData('A'); Debug.Log("String A : " + dataTable.ToString()); Debug.Log("Byte A : " + (byte)dataTable); Debug.Log("Long A : " + (long)dataTable);

相應型別的輸出, 沒有問題.
2. 資料轉換
public class AA { public int? intVal1 = null; public int intVal2 = 100; public string str; } public class BB { public Common.DataTable intVal1; public Common.DataTable intVal2; public Common.DataTable str; } [MenuItem("Tools/Test/Common")] static void Test() { var aa = new AA() { intVal2 = 111, str = "STR" }; var json = LitJson.JsonMapper.ToJson(aa); var bb = LitJson.JsonMapper.ToObject<BB>(json); Debug.Log((int)bb.intVal1); Debug.Log((int)bb.intVal2); Debug.Log((string)bb.str); }
我們從AA型別轉換到 Json, 然后再轉換到BB型別, 可以得到輸出 :

我們之前的資料隱式轉換特性, 剛好在LitJson中有隱式呼叫, 所以可以進行正確的資料轉換, 從一般型別到DataTable :

那么可空型別的轉換呢, 其實也是正確的, 斷點看看 int? 轉換成的DataTable是什么樣的 :

它的型別就是空的, 只是我們輸出的時候 (GetCurrentVariantDataValue 函式) 給了個默認值0.
大體上完工了, 還有一些鱗鱗角角的, 像是Json轉char型別會轉成string型別, 導致轉換錯誤之類的, 都不在考慮之內, 并且這個DataTable只是用來代替基礎型別資料的, 不能作為嵌套物件, 它有它的局限性.
在使用上已經很方便快捷了, 考慮到它如果用到容器之類的地方, 就需要多載HashCode那些方法了, 后續再考慮......
跑一下性能測驗:
[MenuItem("Tools/Test/Common")] static void Test() { Common.DataTable data = 123.456f; var watch = System.Diagnostics.Stopwatch.StartNew(); const int Loop = 1024 * 1024; // 百萬次回圈 for(int i = 0; i <= Loop; i++) { data.SetData(i); var ii = (int)data; } watch.Stop(); Debug.Log("Get / Set Data ms : " + watch.ElapsedMilliseconds); }

同種型別的SetData / Get 進行100萬次, 時間89毫秒, 用時是普通型別的10倍, 可以說性能問題不大.......
下來測驗一下作為比較物件來使用的時候的情況, 因為是值型別, 他們基類已經實作了Equals方法, 所以測驗一下在容器中Equals的性能:
[MenuItem("Tools/Test/Common")] static void Test() { Common.DataTable str1 = "String"; Common.DataTable str2 = "String"; HashSet<DataTable> tables = new HashSet<DataTable>(); tables.Add(str1); var watch = System.Diagnostics.Stopwatch.StartNew(); for(int i = 0; i < 1000000; i++) { bool contains = tables.Contains(str2); } watch.Stop(); Debug.Log("111 : " + watch.ElapsedMilliseconds); }

3800+毫秒, 因為值型別在位元組對齊的情況下可以進行快速記憶體比較的, 而無法比較的時候會呼叫反射來獲取所有Field進行對比, DataTable應該不符合快速比較的標準, 而且DataTable中有臨時快取物件, 它會因為獲取的型別不同而改變, 所以自帶的Equals是不正確也沒有效率的, 必須自己重寫Equals方法:
public override int GetHashCode() { return base.GetHashCode(); } public override bool Equals(object obj) { var table = (DataTable)obj; if(table.dataType != this.dataType) { return false; } if(table._userData != this._userData) { return false; } return (this._variantData.longVal) == (table._variantData.longVal); }
這樣重寫之后比較速度應該會提升一些, 代碼不變, 再來跑一次測驗:

減少了1000毫秒, 非常大的進步. 當然在這里的Equals呼叫造成了裝箱和拆箱, 我們試試用其它方式避免裝箱, 因為HashSet提供了IEqualityComparer<T> 比較器選項, 就直接讓DataTable繼承它吧 :
public struct DataTable : IEqualityComparer<DataTable> { public bool Equals(DataTable x, DataTable y) { if(x.dataType != y.dataType) { return false; } // 這里有一個坑, 字符竄型別的對比不能通過不等式簡單對比 if(x._userData != y._userData) { return false; } // 需要通過多載的Equals判斷 同理上面的Equals多載也一樣 if((x._userData =https://www.cnblogs.com/tiancaiwrk/p/= null && y._userData != null) || (x._userData != null && y._userData =https://www.cnblogs.com/tiancaiwrk/p/= null)) { return false; } if(x._userData != null && x._userData.Equals(y._userData) == false) { return false; } return (y._variantData.longVal) == (x._variantData.longVal); } public int GetHashCode(DataTable obj) { return obj.GetHashCode(); } }
對于VariantData的對比, 只需要進行位元組對比就行了, 所以取longVal等于是記憶體了, 測驗代碼微調:
[MenuItem("Tools/Test/Common")] static void Test() { Common.DataTable str1 = "String"; Common.DataTable str2 = "String"; HashSet<DataTable> tables = new HashSet<DataTable>(str1); // 使用比較器 tables.Add(str1); var watch = System.Diagnostics.Stopwatch.StartNew(); for(int i = 0; i < 1000000; i++) { bool contains = tables.Contains(str2); } watch.Stop(); Debug.Log("111 : " + watch.ElapsedMilliseconds); }

確實有微小的性能提升, 到這里就基本完成了一個DataTable了, 因為它是用來代替一般型別的變數的, 所以需要非常關注它的性能問題和泛用性, 接下來就要做一般性測驗了.
對于基礎型別的容器, 肯定不會出錯, 因為比較物件就是基礎型別:
[MenuItem("Tools/Test/Common")] static unsafe void Test() { string testStr = "String"; Common.DataTable strTable1 = "String"; Common.DataTable strTable2 = "String Test"; HashSet<string> stringTables = new HashSet<string>() { testStr }; Debug.Log(stringTables.Contains((string)strTable1) ? "Yes strTable1" : "No strTable1"); // 基礎型別的 容器 肯定不會出錯 Debug.Log(stringTables.Contains((string)strTable2) ? "Yes strTable2" : "No strTable2"); // 基礎型別的 容器 肯定不會出錯 int testInt = 101; Common.DataTable intTable1 = 101; Common.DataTable intTable2 = 111; HashSet<int> intTables = new HashSet<int>() { testInt }; Debug.Log(intTables.Contains((int)intTable1) ? "Yes intTable1" : "No intTable1"); // 基礎型別的 容器 肯定不會出錯 Debug.Log(intTables.Contains((int)intTable2) ? "Yes intTable2" : "No intTable2"); // 基礎型別的 容器 肯定不會出錯 }

然后修改容器, 將它改成 DataTable 的容器:
[MenuItem("Tools/Test/Common")] static unsafe void Test() { string testStr = "String"; Common.DataTable strTable1 = "String"; Common.DataTable strTable2 = "String Test"; HashSet<DataTable> stringTables = new HashSet<DataTable>() { testStr }; // 型別改變 Debug.Log(stringTables.Contains(strTable1) ? "Yes strTable1" : "No strTable1"); Debug.Log(stringTables.Contains(strTable2) ? "Yes strTable2" : "No strTable2"); int testInt = 101; Common.DataTable intTable1 = 101; Common.DataTable intTable2 = 111; HashSet<DataTable> intTables = new HashSet<DataTable>() { testInt }; // 型別改變 Debug.Log(intTables.Contains(intTable1) ? "Yes intTable1" : "No intTable1"); Debug.Log(intTables.Contains(intTable2) ? "Yes intTable2" : "No intTable2"); }

仍然能得到正確的結果, 因為 testStr 和 testInt 都隱式轉換成了 DataTable, 所以比較結果都是正確的.
然后混合起來看看:
[MenuItem("Tools/Test/Common")] static void Test() { int testInt = 101; Common.DataTable intTable1 = "101"; Common.DataTable intTable2 = 101.0f; HashSet<DataTable> intTables = new HashSet<DataTable>() { testInt }; // 型別改變 intTables.Add(intTable1); intTables.Add(intTable2); foreach (var intTable in intTables) { Debug.Log((int)intTable); } }

因為型別不一樣, 所以都能加入到容器中.
而如果在加入時對型別進行轉換之后, 就得到一般結果了:
[MenuItem("Tools/Test/Common")] static void Test() { int testInt = 101; Common.DataTable intTable1 = "101"; Common.DataTable intTable2 = 101.0f; HashSet<DataTable> intTables = new HashSet<DataTable>() { testInt }; // 型別改變 Debug.Log("Add intTable1 " + intTables.Add((int)intTable1)); Debug.Log("Add intTable2 " + intTables.Add((int)intTable2)); foreach (var intTable in intTables) { Debug.Log((int)intTable); } }

這樣, 在替代基礎型別變數上來說DataTable已經可以使用了.......至于它可以有參考物件的設計, 只是為了增加特殊用法的:
private object _userData;
_userData其實應該是繼承于IConvertible的物件, DataTable應該還是服務于基礎變數的, 之后再考慮吧...
決定還是使用 IConvertible 對資料進行限定, 因為 DataTable 不是用作通用資料物件的, 還是限定了比較好, 那么要改動的地方有幾個:
using System; using System.Runtime.InteropServices; using System.Collections.Generic; using DataType = System.TypeCode; namespace Common { public struct DataTable : IEqualityComparer<DataTable> { [StructLayout(LayoutKind.Explicit, Size = 8)] private struct VariantData { // ...... } private static class Setter<TVal> { public static VariantDataAccess<TVal> SetterCall = null; } delegate void VariantDataAccess<T>(ref VariantData variantData, T value); static DataTable() { // ...... } public DataType dataType { get; private set; } private VariantData _variantData; private IConvertible _userData; private DataType _parsedDataType; private VariantData _parsedVariantData; public void SetData<T>(T data) where T : IConvertible // 限定輸入型別 { _parsedDataType = DataType.Empty; var typeCode = Type.GetTypeCode(typeof(T)); this.dataType = typeCode; switch(typeCode) { case TypeCode.String: case TypeCode.Object: { _userData = data; } break; default: { var call = Setter<T>.SetterCall; if(call != null) { call.Invoke(ref _variantData, data); _userData = null; } else { UnityEngine.Debug.LogError("Not Support DataType : " + typeof(T).Name); // 修改為泛用形式 this.dataType = TypeCode.Object; _userData = data; } } break; } } public bool ToBool() { if(dataType == DataType.Boolean) { return _variantData.boolVal; } if(_parsedDataType == DataType.Boolean) { return _parsedVariantData.boolVal; } bool retVal = default(bool); if((IsConvertibleType() && ConvertibleToBool(ref retVal)) == false) { var doubleValue =https://www.cnblogs.com/tiancaiwrk/p/ GetCurrentVariantDataValue(); retVal = System.Convert.ToBoolean(doubleValue); } _parsedDataType = DataType.Boolean; _parsedVariantData.boolVal = retVal; return retVal; } public char ToChar() { if(dataType == DataType.Char) { return _variantData.charVal; } if(_parsedDataType == DataType.Char) { return _parsedVariantData.charVal; } char retVal = default(char); if((IsConvertibleType() && ConvertibleToChar(ref retVal)) == false) { var byteValue =https://www.cnblogs.com/tiancaiwrk/p/ ToByte(); retVal = System.Convert.ToChar(byteValue); } _parsedDataType = DataType.Char; _parsedVariantData.charVal = retVal; return retVal; } private bool ConvertibleToBool(ref bool value) // bool 的特殊轉換 { try { value = _userData.ToBoolean(null); return true; } catch { return false; } } private bool ConvertibleToChar(ref char value) // char 的特殊轉換 { try { value = _userData.ToChar(null); return true; } catch { if(dataType == DataType.String) { try { var str = _userData as string; value = str[0]; return true; } catch { return false; } } return false; } } private double GetCurrentVariantDataValue() { switch(dataType) { // ....... case DataType.String: case DataType.Object: { try { return _userData.ToDouble(null); // 都能轉換型別 } catch { break; } } } return 0; } private bool IsConvertibleType() { return DataType.String == dataType || DataType.Object == dataType; } } }
基本上除了Bool , Char 之外的型別都沒有受到影響, 好了完工...
(2020.06.16)
DataTable并不是一個型別, 而是代替其他型別的資料, 所以在Json寫入的時候需要提供自定義的寫入方法, 補充資料型別轉換的支持, 因為使用的是LitJson來進行轉換, 它支持用戶自定義型別寫入, 非常強大, 所以直接注冊到LitJson之中即可:
static DataTable() { // ...... // 注冊寫入方式, 因為LitJson的轉換也比較強大, 不需要做太多操作 LitJson.JsonMapper.RegisterExporter<Common.DataTable>((_obj, _writer) => { switch(_obj.dataType) { case TypeCode.Empty: case TypeCode.DBNull: case TypeCode.Char: case TypeCode.Object: case TypeCode.String: { _writer.Write((string)_obj); } break; case TypeCode.Boolean: { _writer.Write((bool)_obj); } break; default: { _writer.Write((double)_obj); } break; } }); }
測驗一下, 看看它能否進行雙向轉換:
[MenuItem("Tools/Test/Common")] static void Test() { var aa = new AA(); var jsonAA = LitJson.JsonMapper.ToJson(aa); var bb = LitJson.JsonMapper.ToObject<BB>(jsonAA); Debug.Log((int)bb.i); Debug.Log((string)bb.s); Debug.Log((bool)bb.b); Debug.Log((float)bb.f); bb.i = "60"; var jsonBB = LitJson.JsonMapper.ToJson(bb); aa = LitJson.JsonMapper.ToObject<AA>(jsonBB); Debug.Log(aa.i); Debug.Log(aa.s); Debug.Log(aa.b); Debug.Log(aa.f); } public class AA { public int i = 100; public string s = "AAA"; public bool b; public float f; } public class BB { public Common.DataTable i; public Common.DataTable s; public Common.DataTable b; public Common.DataTable f; }
結果沒有問題 AA -> BB 型別沒有大問題, DataTable的型別可以正確賦值, 所以反過來 BB -> AA 也不是太大問題.
再來一個更兇險的測驗:
[MenuItem("Tools/Test/Common")] static void Test() { var bb = new BB(); bb.i = 123.456f; bb.s = false; bb.b = "true"; bb.f = "21.34"; var jsonBB = LitJson.JsonMapper.ToJson(bb); var aa = LitJson.JsonMapper.ToObject<AA>(jsonBB); Debug.Log(aa.i); Debug.Log(aa.s); Debug.Log(aa.b); Debug.Log(aa.f); }

沒有什么大問題, 不過就是bool型別轉換為string之后輸出的結果大小寫有點差別.
(2020.08.27)
最近在用 Xml 的序列化里面也用 DataTable 來作為一般變數, 不過系統提供的 Xml 物件方法跟 LitJson 不同, 是需要物件通過繼承 IXmlSerializable 介面來實作的, 所以 DataTable 必須繼承它了, 然后再通過自定義寫入和讀取來獲取資料, 實作如下 :
public struct DataTable : IEqualityComparer<DataTable>, IXmlSerializable { ...... // 實作IXmlSerializable介面 public XmlSchema GetSchema() { return null; } public void ReadXml(XmlReader reader) { reader.MoveToContent(); var isEmptyElement = reader.IsEmptyElement; reader.ReadStartElement(); if(false == isEmptyElement) { _userData = reader.ReadString(); dataType = DataType.String; reader.ReadEndElement(); } } public void WriteXml(XmlWriter writer) { writer.WriteString(this.ToString()); } }
然而發現 DataTable 無法作為 [XmlAttribute] 的屬性物件, 通過 XmlSerializer 自動序列化和反序列化, 只能間接依賴 C# 的 get & set 屬性來實作了, 方案如下 :
XML 資料
<info> <entry path="E:\ModulesProjects_CheckOut\ArtistFiles\Assets"> </entry> </info>
C# 代碼
[XmlRoot(ElementName="info")] public class info { [XmlRoot(ElementName="entry")] public class info_entry { [XmlAttribute(AttributeName="path")] public DataTable path; // 反序列化會拋出例外 [XmlAttribute(AttributeName="path")] // 原有反序列化入口不變, 只改變成員 public string _path{ get{ return path.ToString(); } set{ path = value; } } // get & set [XmlIgnore] // 新添加變數屬性, 在序列化時不會出錯 public DataTable path; // 使用變數名稱作為用戶介面 } [XmlElement(ElementName="entry")] public info_entry entry; }
這樣序列化時, DataTable path 作為資料被 [XmlAttribute(AttribtueName="path")] 屬性寫入 Xml, 而在反序列化時, 反序列化器把 _path 的值寫入的時候相當于把值賦給 DataTable path, 這樣實作了 [XmlAttribute] 到 DataTable 的轉換.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/6627.html
標籤:其他
