有时候一些简单的数据缓存, 遭遇到了不同类型的变量, 就会变得比较尴尬, 比如远程发来的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 = 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 = 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 = 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 = 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 == null && y._userData != null) || (x._userData != null && y._userData == 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 = 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 = 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之后输出的结果有点差别.