• Json/Xml 的强类型数据转换


      最近都在搞这东西, 虽然市面上很多 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);
            }

      正确, 不管序列化还是反序列化, 都正常, 没有影响到其它使用者的逻辑. 相当完美...

      

  • 相关阅读:
    es5中,一个在js中导入另一个js文件。
    移动端字体小于12号字的时候,line-height居中的问题
    初学者都能懂得 Git 说明
    一探 Vue 数据响应式原理
    文件的命名规则
    Vue 的 watch 和 computed 有什么关系和区别?
    MVC 与 Vue
    博客园皮肤设置
    java使用run和start后的线程引用
    Python改变一行代码实现二叉树前序、中序、后序的迭代遍历
  • 原文地址:https://www.cnblogs.com/tiancaiwrk/p/13566515.html
Copyright © 2020-2023  润新知