最近都在搞这东西, 虽然市面上很多 Json2CSharp / Xml2CSharp 的东西, 不过几乎都不对, 在生成 CSharp 类型的时候归并得不好, 他们的逻辑大致就是根据节点名称来生成类型, 然后如果名称相同的话, 就归并到一起, 可是很多时候同名节点下有同名的对象, 在它们类型不同的时候, 就完蛋了, 直接看看下面一个例子, 从 XML 结构生成 C# 代码的 :
XML :
<?xml version="1.0" encoding="UTF-8"?> <info> <entry <!-- 测试List --> path="E:ModulesProjects_CheckOutArtistFilesAssets" revision="553" kind="dir"> <entry name="HH"> <!-- 测试重复类型 --> <user>ME</user> <url name="SB"></url> <!-- 测试重复变量 --> </entry> <url>https://desktop-82s9bq9/svn/UnityProjects/ArtistFiles/Assets</url> </entry> <entry <!-- 测试List --> revision="6" kind="dir" path="E:ModulesProjects_CheckOutArtistFilesAssetsDataConverterModulesEditor"> <entry name="HH"> <!-- 测试重复类型 --> <user>ME</user> <url name="SB"></url> <!-- 测试重复变量 --> </entry> <url>https://desktop-82s9bq9/svn/DataConverterModules/Assets/DataConverterModules/Editor</url> </entry> </info>
可以看到这里故意使用同名节点 <entry>/<url> 并且 <url> 节点都在 <entry> 节点下面, 并且类型不同 :
<entry path="E:ModulesProjects_CheckOutArtistFilesAssets" revision="553" kind="dir"> <entry name="HH"> <user>ME</user> <url name="SB"></url> <!-- 带Attribute --> </entry> <url>https://desktop-82s9bq9/svn/UnityProjects/ArtistFiles/Assets</url> <!-- 普通Element --> </entry>
然后找个 Xml2CSharp 在线转换的转换一下(需要去掉注释), 得到下面的代码 (https://xmltocsharp.azurewebsites.net/) :
using System; using System.Xml.Serialization; using System.Collections.Generic; namespace Xml2CSharp { [XmlRoot(ElementName="url")] public class Url { [XmlAttribute(AttributeName="name")] public string Name { get; set; } } [XmlRoot(ElementName="entry")] public class Entry { [XmlElement(ElementName="user")] public string User { get; set; } [XmlElement(ElementName="url")] public Url Url { get; set; } // 节点下的 string 类型 url 被 URL 类型覆盖了 [XmlAttribute(AttributeName="name")] public string Name { get; set; } [XmlElement(ElementName="entry")] public Entry Entry { get; set; } [XmlAttribute(AttributeName="revision")] public string Revision { get; set; } [XmlAttribute(AttributeName="kind")] public string Kind { get; set; } [XmlAttribute(AttributeName="path")] public string Path { get; set; } } [XmlRoot(ElementName="info")] public class Info { [XmlElement(ElementName="entry")] public List<Entry> Entry { get; set; } } }
这就不对了, 即使反序列化可以运行, 可是我少了一个网址的 url 节点啊, 可以看出它的逻辑就是同名类型归并, 看到 Entry 类型里面还包含了 Entry, 就跟 XML 节点一样, 前面也说了, 这样归并下来的话, 同样是 Url 节点, 它就冲突了, 会变成 :
public string Url {get;set;} public Url Url {get;set;}
这样肯定不行, 上面就是后写入的 Url 类型变量覆盖了 string 类型变量, 并且还有隐患的是节点类型 [XmlElement] 和 [XmlAttribute] 也是可能冲突的, 所以上面的简单转换并没有实用价值.
先来看结论, 目前我制作的转换工具得到的结果 :
XMLToCSharp :
using System; using System.Collections; using System.Collections.Generic; using System.Text; using System.Xml; using System.Xml.Serialization; using System.Xml.Schema; using System.IO; namespace DataConverterModules { [XmlRoot(ElementName="info")] public class info { [XmlRoot(ElementName="entry")] public class Merge_1_entry { [XmlAttribute(AttributeName="path")] public string path; [XmlAttribute(AttributeName="revision")] public string revision; [XmlAttribute(AttributeName="kind")] public string kind; [XmlElement(ElementName="entry")] public Merge_2_entry entry; // 归并唯一性的结果, 不同的类型被分离了 [XmlElement(ElementName="url")] public string url; // 正确保留了变量 } [XmlRoot(ElementName="entry")] public class Merge_2_entry { [XmlAttribute(AttributeName="name")] public string name; [XmlElement(ElementName="user")] public string user; [XmlElement(ElementName="url")] public Merge_3_url url; } [XmlRoot(ElementName="url")] public class Merge_3_url { [XmlAttribute(AttributeName="name")] public string name; } [XmlElement(ElementName="entry")] public List<Merge_1_entry> entry; // 类型名称跟节点名称不同, 这是归并唯一性的结果 } }
对于节点冲突通过另一种归并类型的方式实现, 所有类对象节点都得到了一个唯一命名, 然后再进行归并, 虽然这里看不出来不过保留了正确的变量...
当然这是个中期结果, 只达到了正确性的要求, 其它问题比如自动命名对象的非稳定性, 像 Merge_1_entry 这样的归并类型, 它刚好这次生成给它的 ID 是 1, 下次如果是 2 的话就会变成 Merge_2_entry, 名称会变, 如果大量被引用的话, 就是个惨案... 还有就是一个节点同时有 Attribute 和子节点重名的时候, 仍然有覆盖问题, 不过这是数据设计问题, 本来这些数据结构就是松散的, 强对象语言的强类型是没有办法表现出来的, 不用纠结.
其实逻辑就是 :
1. 所有的 XmlElement 节点都可以分为两种 :
一是纯粹节点, 下面没有任何 Attribute 和其它节点, 那它就可以作为一个变量使用, 就像上面的 <user> 节点 :
<entry name="HH"> <user>ME</user> <url name="SB"></url> </entry>
生成的代码 :
[XmlElement(ElementName="user")] public string user;
二是有子节点或 Attribute 的情况, 它就可以作为一个类对象使用, 就像 <url name="SB"> 节点 :
<url name="SB"></url>
生成的代码 :
[XmlRoot(ElementName="url")] public class Merge_3_url { [XmlAttribute(AttributeName="name")] public string name; }
一般都是这样界定 XmlElement 类型的.
2. 在同级节点中有并列节点的情况的, 可以视为该级节点存在数组的情况, 将之合并为数组或 List 对象, 就像上面的 <info> 下的 <entry> 节点那样 :
<info> <entry ...> ... </entry> <entry ...> ... </entry> </info>
生成的代码 :
[XmlRoot(ElementName="info")] public class info { [XmlElement(ElementName="entry")] public List<Merge_1_entry> entry; // List }
在正常数据结构的情况下, 应该是对的.
3. 每个 Attribute 或简单 XmlElement 中的变量, 直接使用 string 类型即可, 不过我这里有自己实现的多变量方案 DataTable, 通过实现接口 IXmlSerializable 可以对 XmlElement 变量进行类型转换, 可是在 Attribute 类型转换上失败了, 原因不明, 参考如下 :
// DataTable 代替基础类型 bool / int / string... 等 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()); } } // xml 反序列化对象 ...略 [XmlElement(ElementName="user")] public DataTable user; // Element 对象正确 [XmlAttribute(AttributeName="name")] public DataTable name; // Attribute 对象不正确 [XmlAttribute(AttributeName="name")] public string name; // 必须使用 string // 使用 XmlSerializer 反序列化 public static T ToObject<T>(string xml) { T retVla = default(T); var serializer = new XmlSerializer(typeof(T)); using(var stream = new StringReader(xml)) { using(var reader = System.Xml.XmlReader.Create(stream)) { try { var obj = serializer.Deserialize(reader); retVla = (T)obj; } catch(System.Exception ex) { Debug.LogError(ex.Message); } } } return retVla; }
本着万物皆可 string 的原则, 通用数据对象对于数据合并非常有用.
(2020.08.27)
对于名称冲突的 Attribute 和 Element 节点, 也通过修改变量名称的方式来进行支持, 如下 :
<?xml version="1.0" encoding="UTF-8"?> <info> <entry path="E:ModulesProjects_CheckOutArtistFilesAssets"> <path>ElementPath1</path> <path>ElementPath2</path> </entry> </info>
<entry> 节点有 path 的属性, 以及<path> 的节点, 生成的代码 :
using System; using System.Collections; using System.Collections.Generic; using System.Text; using System.Xml; using System.Xml.Serialization; using System.Xml.Schema; using System.IO; namespace DataConverterModules { [XmlRoot(ElementName="info")] public class info { [XmlRoot(ElementName="entry")] public class info_entry { [XmlAttribute(AttributeName="path")] public string path_attribute; [XmlElement(ElementName="path")] public List<DataTable> path_element; } [XmlElement(ElementName="entry")] public info_entry entry; } }
还好正常情况下 Attribute 都是名称唯一的, 这样虽然名称变了, 不过也比较直观. XML 的转换逻辑基本就完成了...
然后是 Json 的, 要比 XML 复杂一些, 因为 XML 本身序列化的可扩展性不高 ( 指的是系统自带的反序列化器 ), 从下面的例子就能看出来 :
// xml <info> <entry1>Value1</entry1> <entry2>Value1</entry2> </info> // json { "entry1" : "Value1", "entry2" : "Value2" }
上面两种数据, 如果看成同样的数据结构的话, XML 只能生成一种 C# 结构 :
[XmlRoot(ElementName="info")] public class info { [XmlElement(ElementName="entry1")] public string entry1; [XmlElement(ElementName="entry2")] public string entry2; }
而 Json 可以生成两种结构 :
// json 第一种 public class CSharpClass { public string entry1; public string entry2; } // json 第二种 public class CSharpClass : Dictionary<string, string> { }
可以看出泛用性的差别, 根据不同需求的扩展性的差别. Xml 序列化天生不支持 Dictionary 类型, 并且 [XmlAttribute] 属性反序列化为 DataTable 会抛出异常, 感觉限制太大...
再来看看 Json 序列化, Json 比较符合强类型的逻辑, 它有哈希表和列表的区别, 像下面这样会导致报错 :
{ "key" : "Value1", "key" : "Value2" }
随便找个在线 Json2CSharp 网站进行代码转换 ( https://json2csharp.com/ ) , 可以看到它刚好是跟 XML 相反, 是完全不进行类型归并, 得到很多冗余的类型, 在结果上是正确的, 因为它把类型全都唯一了, 看看例子 :
{ "Normal": { "size": { "x": 1021, "y": 988 }, "url": "xxxx" }, "Test": { "size": { "x": 222, "y": 988 }, "url": "xxxx" } }
在线转换给出的代码 :
public class Size { public int x { get; set; } public int y { get; set; } } public class Normal { public Size size { get; set; } public string url { get; set; } } public class Size2 { public int x { get; set; } public int y { get; set; } } public class Test { public Size2 size { get; set; } public string url { get; set; } } public class Root { public Normal Normal { get; set; } public Test Test { get; set; } }
其实 Normal / Test 是相同的数据结构, Size / Size2 也是相同的数据结构, 都是可以归并的, 下面是我生成的结构 :
public class CSharpClass { public class Merge_1_Normal { public Merge_2_size size; public string url; } public class Merge_2_size { public string x; public string y; } public Merge_1_Normal Normal; public Merge_1_Normal Test; }
然后像上面一样, 对于某些数据我们可以将它简化为 Dictionary 对象, 比如这样 :
public class CSharpClass : Dictionary<string, CSharpClass.Merge_1_Normal> { public class Merge_1_Normal { public Merge_2_size size; public string url; } public class Merge_2_size { public string x; public string y; } }
或是这样 :
public class CSharpClass : Dictionary<string, CSharpClass.Merge_1_Normal> { public class Merge_1_Normal { public Merge_2_size size; public string url; } public class Merge_2_size : Dictionary<string, string> { } }
相同的类型可归并, 当 Json 是一个数据模板的时候, 可以将对象生成可扩展的 Dictionary 形式, 比较灵活, 并且 LitJson 提供了所有需要的序列化扩展, 像 DataTable 这些也直接通过注入自定义类型来完成序列化和反序列化.
基本上就是这样了, 从上面过程也可以看到生成的代码有些是全部放在同一级的, 有些是放在某个类中的 Nested 的, 因为多个数据结构可能有重叠名称的对象生成, 所以我这里都是生成 Nested 这种形式的, 所以只要保证最外层类型的引用能够正确即可 :
public class CSharpClass : Dictionary<string, CSharpClass.Merge_1_Normal>
最外层只有一个可能, 就是继承 Dictionary 的时候继承的类型, 虽然看起来有点怪...
PS : 比较有意思的是大部分 C# 编译器都能支持中文 类名 / 变量名 / 函数名 这些, 你可以写一大堆中文进去没有问题, 不过在线转换出来的代码还是被坑在了非法字符上 :
大于号没有被删除, 我这里搞了个比较好玩的, 中文转拼音, 之后再删除非法字符即可 :
毕竟中文还涉及编码这些问题, 还是尽量规避的好一些...
(2020.08.28)
继续对 XML 转换施工, 之前的 [XmlAttribute] 属性无法进行类型转换直接反序列化成 DataTable, 找来找去也没有什么借口或是扩展方法来提供自定义转换, 那么就修改一下生成代码逻辑, 使用 get & set 逻辑来完成想要的功能吧.
还是从之前的转换类来看 :
<info> <entry path="E:ModulesProjects_CheckOutArtistFilesAssets"> </entry> </info>
转换的代码 :
[XmlRoot(ElementName="info")] public class info { [XmlRoot(ElementName="entry")] public class info_entry { [XmlAttribute(AttributeName="path")] public string path; } [XmlElement(ElementName="entry")] public info_entry entry; }
而我希望它是 DataTable 类型的话, 因为 DataTable 已经实现了各种类型的隐式转换, 所以修改原有的 path 变量作为 DataTable 的入口, 而旧的变量作为反序列化的入口修改变量名称即可, 修改后的生成代码如下 :
[XmlRoot(ElementName="info")] public class info { [XmlRoot(ElementName="entry")] public class info_entry { [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; }
这样既保证了用户接口, 也保证了 XML 序列化接口, 测试一下 :
[MenuItem("Test/Run Test")] public static void Test() { string path = @"C:UsersCASCDesktopTempxml Test.xml"; var obj = XmlConverter.ToObject<info>(System.IO.File.ReadAllText(path)); Debug.Log((string)obj.entry.path); var toXml = XmlConverter.ToXml(obj); Debug.Log(toXml); var toObj = XmlConverter.ToObject<info>(toXml); Debug.Log((string)toObj.entry.path); }
正确, 不管序列化还是反序列化, 都正常, 没有影响到其它使用者的逻辑. 相当完美...