找回密码
 立即注册
查看: 31710|回复: 100

经典面试题之——如何自由转换两个没有继承关系的字段及类型相同的实体模型

    [复制链接]
  • TA的每日心情
    郁闷
    2022-12-4 03:02
  • 签到天数: 495 天

    连续签到: 2 天

    [LV.9]妙领天机

    727

    主题

    3017

    回帖

    5216

    积分

    VIP会员

    积分
    5216
    发表于 2018-7-23 20:37:42 | 显示全部楼层 |阅读模式

    马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×


                                   
    登录/注册后可看大图

      相信很多童鞋们都被问到过这个问题,不管是在面试的时候被问过,还是笔试题里考过,甚至有些童鞋们找我要学习资料的时候我也考过这个问题,包括博主我自己,也曾被问过,而且博主现在有时作为公司的面试官,也喜欢问应试者这样的问题。
      这确实是一道基础题,不管是在java里面还是在C#里面,都属于一道非常基础的题,但很多童鞋竟然说没遇到过这样的场景,或者说学习基础的时候没重视过,所以自然不会了,那行,今天博主在这里就针对这个问题,做出详细的解释并由这个问题,同时,也给大家做个扩展学习吧。
      在实际的项目当中,这是个非常非常需要的,我们经常需要在不同用途的实体之间相互转换进行数据传输,最典型的当然就是Entity和DTO之间的相互转换咯(不懂什么是DTO的自己去科普下,或者在这儿你看成是ViewModel也行,这无所谓)。当然,开这篇文章肯定不是只讲简简单单反射这么简单,那我写这篇文章可能有点大动干戈了,所以,你们赚了,除了讲反射,自然也就引出了各种创建对象的效率问题,以及除了反射还有哪些方式可以转换实体。
      首先说一下,我听到的最多的答案,就是:
      1. new一个目标实体出来,然后把对应字段的值赋值上就好了
      2. 通过反射获取对象的属性,然后挨个循环属性,把对应属性名赋值到另一个对象里面
      3. 使用AutoMapper完成不同实体的转换。
      上面这几个答案都是对的,只是答案1的童鞋钻漏子勉强回答对了,而答案2的通用性比较高了,答案3的就是拿来主义,有现成的直接用就好了,底层怎么实现的不知道…。可能有些童鞋以前答不上来的时候看到这些答案就恍然大悟了,而有的童鞋可能还是有点懵,这样的话确实应该回头再学一遍基础了。好吧,来点实际的吧。
      那就从上面三个答案开始吧。
      先准备两个实体吧:
      public class Person
      {
      public string Name { get; set; }
      public int Age { get; set; }
      }
      public class PersonDto
      {
      public string Name { get; set; }
      public int Age { get; set; }
      }
      1. 硬编码实现实体的相互转换
      最简单粗暴的方法也就是这样的咯,但如果面试这样回答,基本上不会加分。
      Person p = new Person()
      {
      Age = 10,
      Name = "张三"
      };
      PersonDto pd = new PersonDto()
      {
      Name = p.Name,
      Age = p.Age
      };
      这样做,也不是不可以,毕竟效率是最高的,但是,通用性不强,如果要转换其他实体,每个都必须重新手写,不可复用,如果一个实体属性达到了几十上百个呢?这样写你不嫌手累嘛,看上去也不优雅是不。那下面这个,看上去稍微优雅点的:
      2. 委托实现实体的相互转换
      相比之上,稍微优雅了一点点,但,还是硬编码,面试这样回答的可能会加分;
      Person p = new Person()
      {
      Age = 10,
      Name = "张三"
      };
      Func<;Person, PersonDto> func = c => new PersonDto()
      {
      Age = c.Age,
      Name = c.Name
      };
      PersonDto pd = func(p);
      这里直接写了个Func类型的委托,在委托里面硬编码给DTO实体的字段挨个赋值,看上去稍微优雅点了,毕竟用上了委托,哈哈……
      那好,又优雅又不是硬编码的来了。
      3. 使用AutoMapper完成不同实体的转换
      当然,这拿来主义在实际项目中确实也用得非常多,包括博主我自己,在项目中也是用AutoMapper居多,毕竟人家实体转换效率还是蛮高的,面试中可能会加分。
      Person p = new Person() { Age = 10, Name = "张三" };
      Mapper.Initialize(m => m.CreateMap<;Person, PersonDto>());
      PersonDto pd = Mapper.Map<;PersonDto>(p);
      这代码,看上去确实很简单就完成了,拿来主义嘛,那要是就不准让你用AutoMapper,又要达到它这样的高性能,你咋办?!那好,后面有终极武器,比AutoMapper的转换效率更高效,所以看到这篇文章的童鞋们,你们赚大了!
      4. 通过反射实现实体的相互转换
      可能这样回答的人基础都是比较好的了,面试都会加分的。
      首先准备一个方法,专门对实体进行映射:
      /// <summary>
      /// 实体映射
      /// </summary>
      /// <typeparam name="TDestination">目标实体</typeparam>
      /// <param name="source">源实体</param>
      /// <returns></returns>
      public static TDestination MapTo<TDestination>(object source) where TDestination : new()
      {
      TDestination dest = new TDestination();//创建目标实体对象
      foreach (var p in dest.GetType().GetProperties())//获取源实体所有的属性
      {
      p.SetValue(dest, source.GetType().GetProperty(p.Name)?.GetValue(source));//挨个为目标实体对应字段名进行赋值
      }
      return dest;
      }
      然后在需要进行实体转换的地方直接调这个方法就OK了;
      Person p = new Person() { Age = 10, Name = "张三" };
      PersonDto pd = MapTo<;PersonDto>(p);
      很好,这很优雅,但是,博主我觉得,还不够优雅,毕竟,可能好多人没看懂?
      那好,来个更简单更直接的办法:反序列化进行实体转换;
      5. 通过Json反序列化实现实体的相互转换
      到这儿为止,很多童鞋就想不到了,实体转换还能通过json来进行;确实可以的,这也方便,而且还自带支持多成实体的映射转换,比如实体里面还包含List这样的数据结构;一句话就能搞定实体映射,不信你看:
      首先通过nuget把Newtonsoft.Json引进来,然后coding...
      Person p = new Person() { Age = 10, Name = "张三" };
      PersonDto pd = JsonConvert.DeserializeObject<;PersonDto>(JsonConvert.SerializeObject(p));
      卧槽,居然这就搞定了,还能这么优雅?!那你试试吧。
      上面这几种方式都是经常用到的,但是,当你进行超大批量的实体转换时,你会发现,反射和反序列化的方式出现瓶颈了,那有没有办法来优化呢,肯定有啊,不能优化的话那怎么去支撑千万级数据?!那你想到的是什么?表达式树?emit?
      既然反射效率那么低,那是不是就不能用反射了?还是离不开反射,前方带你弯道超车,还请各位站稳扶好!
      如果你在面试当中用到了下面这几种方式,那么,可能面试官都会膜拜你了,最后的可能,就是你选公司而不是公司选你了,哈哈…,当然,可能有点夸大其词,毕竟面试你的也不是什么技术菜鸟,至少会给你的面试大大加分!
      6. 表达式树实现实体的相互转换
      来吧,拼接表达式,走起!
      Person p = new Person() { Age = 10, Name = "张三" };
      ParameterExpression parameterExpression = Expression.Parameter(typeof(Person), "p");
      List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)
      foreach (var item in typeof(PersonDto).GetProperties()) //遍历目标类型的所有属性
      {
      MemberExpression property = Expression.Property(parameterExpression, typeof(Person).GetProperty(item.Name));//获取到对应的属性
      MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性
      memberBindingList.Add(memberBinding);
      }
      foreach (var item in typeof(PersonDto).GetFields())//遍历目标类型的所有字段
      {
      MemberExpression property = Expression.Field(parameterExpression, typeof(Person).GetField(item.Name));//获取到对应的字段
      MemberBinding memberBinding = Expression.Bind(item, property);//同上
      memberBindingList.Add(memberBinding);
      }
      MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(PersonDto)), memberBindingList.ToArray());//初始化创建新对象
      Expression<Func<;Person, PersonDto>> lambda = Expression.Lambda<Func<;Person, PersonDto>>(memberInitExpression, parameterExpression);
      PersonDto pd = lambda.Compile()(p);
      卧槽,看起来好复杂,不明觉厉啊!那,自己慢慢摸索下吧,都说了要站稳扶好的,这就不怪老司机了哈!

                                   
    登录/注册后可看大图

      结果如上图所示,但是问题又来了,我们不可能只有一个类,也不可能只有一个Dto,那我们应该怎么实现呢?
      对 ,可以用泛型来实现。
      但是,博主我觉得还不够,每次转换还得写这么一大坨看不懂的东西,封装一下岂不更好用?那好,开始封装吧;
      7. 表达式树的封装实现通用实体的相互转换
      public static TDestination ExpressionTreeMapper<TSource, TDestination>(TSource source)
      {
      ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");
      List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)
      foreach (var item in typeof(TDestination).GetProperties()) //遍历目标类型的所有属性
      {
      MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//获取到对应的属性
      MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性
      memberBindingList.Add(memberBinding);
      }
      foreach (var item in typeof(TDestination).GetFields())//遍历目标类型的所有字段
      {
      MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//获取到对应的字段
      MemberBinding memberBinding = Expression.Bind(item, property);//同上
      memberBindingList.Add(memberBinding);
      }
      MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化创建新对象
      Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);
      return lambda.Compile()(source); //拼装是一次性的
      }
      封装之后,调用倒是方便了,但我觉得没区别,封装之后每次调用这个方法不也得走一遍编译表达式树的过程,这个过程岂不多余了?这个过程性能瓶颈很大。
      那好,能不能把编译之后的表达式树缓存起来?答案是肯定可以的,继续优化,走起;
      8. 表达式树缓存实现通用实体的相互转换
      实现思路:把每次编译后的表达式树缓存起来,如果存在,直接拿现成的编译好的表达式树调用就好了
      /// <summary>
      /// 生成表达式目录树。字典缓存
      /// </summary>
      public class ExpressionMapper
      {
      private static Dictionary<string, object> _dic = new Dictionary<string, object>();//缓存字典,缓存后的就是硬编码所以性能高。
      /// <summary>
      /// 字典缓存表达式树
      /// </summary>
      /// <typeparam name="TSource"></typeparam>
      /// <typeparam name="TDestination"></typeparam>
      /// <param name="source"></param>
      /// <returns></returns>
      public static TDestination Map<TSource, TDestination>(TSource source)
      {
      string key = $"funckey_{typeof(TSource).FullName}_{typeof(TDestination).FullName}";
      if (!_dic.ContainsKey(key)) //如果该表达式不存在,则走一遍编译过程
      {
      ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");
      List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)
      foreach (var item in typeof(TDestination).GetProperties()) //遍历目标类型的所有属性
      {
      MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//获取到对应的属性
      MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性
      memberBindingList.Add(memberBinding);
      }
      foreach (var item in typeof(TDestination).GetFields())//遍历目标类型的所有字段
      {
      MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//获取到对应的字段
      MemberBinding memberBinding = Expression.Bind(item, property);//同上
      memberBindingList.Add(memberBinding);
      }
      MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化创建新对象
      Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);
      _dic[key]
      = lambda.Compile(); //拼装是一次性的
      }
      return ((Func<TSource, TDestination>)_dic[key]).Invoke(source);
      }
      }
      恩,这样缓存起来,再执行重复类型的转换,就直接取缓存好的表达式树进行调用了,性能貌似提升了,非常棒!但博主我认为,这还有优化的余地,既然有了缓存,那我干脆直接把泛型搬到整个类上面,进行缓存起来,好的,走起;
      9. 表达式树泛型缓存实现通用实体的相互转换
      /// <summary>
      /// 生成表达式目录树  泛型缓存
      /// </summary>
      public class ExpressionGenericMapper
      {
      private static object func;
      public static TDestination Map<TSource, TDestination>(TSource source)
      {
      if (func is null)//如果表达式不存在,则走一遍编译过程
      {
      ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");
      var memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)
      foreach (var item in typeof(TDestination).GetProperties()) //遍历目标类型的所有属性
      {
      MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//获取到对应的属性
      MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性
      memberBindingList.Add(memberBinding);
      }
      foreach (var item in typeof(TDestination).GetFields())
      {
      MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//获取到对应的字段
      MemberBinding memberBinding = Expression.Bind(item, property);//同上
      memberBindingList.Add(memberBinding);
      }
      MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化创建新对象
      Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);
      func = lambda.Compile();
      }
      return ((Func<TSource, TDestination>)func)(source); //拼装是一次性的
      }
      }
      10. 最后,来个性能测试吧
      我们挨个用这些方法,都循环映射1,000,000次,看谁跑得最快!
      单线程测试代码:
      static void Main(string[]
      args)
      {
      Person p = new Person() { Age = 10, Name = "张三" };
      Stopwatch sw = new Stopwatch();
      sw.Start();
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = new PersonDto() { Name = p.Name, Age = p.Age };
      }
      sw.Stop();
      Console.WriteLine($"使用硬编码映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      Func<;Person, PersonDto> func = c => new PersonDto()
      {
      Age = c.Age,
      Name = c.Name
      };
      sw.Restart();
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = func(p);
      }
      sw.Stop();
      Console.WriteLine($"使用委托映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Mapper.Initialize(m => m.CreateMap<;Person, PersonDto>());
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = Mapper.Map<;PersonDto>(p);
      }
      sw.Stop();
      Console.WriteLine($"使用AutoMapper映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = MapTo<;PersonDto>(p);
      }
      sw.Stop();
      Console.WriteLine($"使用反射映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = JsonConvert.DeserializeObject<;PersonDto>(JsonConvert.SerializeObject(p));
      }
      sw.Stop();
      Console.WriteLine($"使用Json反序列化映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = ExpressionTreeMapper<;Person, PersonDto>(p);
      }
      sw.Stop();
      Console.WriteLine($"使用编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = ExpressionMapper.Trans<;Person,PersonDto>(p);
      }
      sw.Stop();
      Console.WriteLine($"使用缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      for (int i = 0; i < 1_000_000; i++)
      {
      PersonDto pd = ExpressionGenericMapper.Map<;Person,PersonDto>(p);
      }
      sw.Stop();
      Console.WriteLine($"使用泛型缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Console.ReadKey();
      }
      测试结果:

                                   
    登录/注册后可看大图

      并行计算测试代码:
      static void Main(string[]
      args)
      {
      Person p = new Person() { Age = 10, Name = "张三" };
      Stopwatch sw = new Stopwatch();
      sw.Start();
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = new PersonDto() { Name = p.Name, Age = p.Age };
      });
      sw.Stop();
      Console.WriteLine($"使用硬编码映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      Func<;Person, PersonDto> func = c => new PersonDto()
      {
      Age = c.Age,
      Name = c.Name
      };
      sw.Restart();
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = func(p);
      });
      sw.Stop();
      Console.WriteLine($"使用委托映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Mapper.Initialize(m => m.CreateMap<;Person, PersonDto>());
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = Mapper.Map<;PersonDto>(p);
      });
      sw.Stop();
      Console.WriteLine($"使用AutoMapper映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = MapTo<;PersonDto>(p);
      });
      sw.Stop();
      Console.WriteLine($"使用反射映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = JsonConvert.DeserializeObject<;PersonDto>(JsonConvert.SerializeObject(p));
      });
      sw.Stop();
      Console.WriteLine($"使用Json反序列化映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = ExpressionTreeMapper<;Person, PersonDto>(p);
      });
      sw.Stop();
      Console.WriteLine($"使用编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = ExpressionMapper.Trans<;Person, PersonDto>(p);
      });
      sw.Stop();
      Console.WriteLine($"使用缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Parallel.For(0, 1_000_000, (l, state) =>
      {
      PersonDto pd = ExpressionGenericMapper.Map<;Person, PersonDto>(p);
      });
      sw.Stop();
      Console.WriteLine($"使用泛型缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");
      sw.Restart();
      Console.ReadKey();
      }
      测试结果:

                                   
    登录/注册后可看大图

      很显然,不管是串行代码还是并行代码,硬编码效率肯定是最高的,至于为什么直接new的硬编码在并行的时候似乎慢了点,这就需要你们从多线程和内存的角度考虑下了,Newtonsoft.Json虽然是高性能的序列化插件,但是使用不当也会造成性能瓶颈的,你们如果用JavascriptSerializer序列化,估计看出来的差距更大了,所以一开始我也就选择Newtonsoft.Json来做本次的评测,也能看出来,表达式树不缓存的时候也是非常耗性能的,毕竟每次都要走一遍编译过程,所以就慢了,而一旦加上了缓存机制,就得到了质的飞跃,而最后的表达式树泛型缓存的写法,性能最接近硬编码的写法,所以,优化无止境!
      至于为什么AutoMapper的性能还没有表达式树高,有兴趣的童鞋可以去github研究下它的源码,它的底层实现其实是用的emit写法,这篇文章就不准备再讲emit了,估计这个更容易把你们给搞晕,毕竟涉及到很多中间语言代码(MSIL),所以,有兴趣的你,可以作为扩展研究。
    楼主热帖
  • TA的每日心情
    奋斗
    2019-8-30 05:00
  • 签到天数: 474 天

    连续签到: 6 天

    [LV.9]妙领天机

    3

    主题

    3005

    回帖

    3010

    积分

    声名显赫

    积分
    3010
    发表于 2018-7-23 20:50:39 | 显示全部楼层
    鼎力支持!!
    回复

    使用道具 举报

  • TA的每日心情
    难过
    2019-8-30 13:15
  • 签到天数: 476 天

    连续签到: 4 天

    [LV.9]妙领天机

    0

    主题

    3030

    回帖

    3024

    积分

    声名显赫

    积分
    3024
    发表于 2018-7-27 03:10:23 | 显示全部楼层
    支持楼主,用户楼主,楼主英明呀!!!
    回复

    使用道具 举报

  • TA的每日心情

    2019-8-30 17:20
  • 签到天数: 483 天

    连续签到: 1 天

    [LV.9]妙领天机

    2

    主题

    3065

    回帖

    3067

    积分

    声名显赫

    积分
    3067
    发表于 2018-7-28 08:07:04 | 显示全部楼层
    支持,楼下的跟上哈~
    回复

    使用道具 举报

  • TA的每日心情

    2019-8-30 00:48
  • 签到天数: 477 天

    连续签到: 7 天

    [LV.9]妙领天机

    1

    主题

    3142

    回帖

    3144

    积分

    声名显赫

    积分
    3144
    发表于 2018-7-28 10:08:12 | 显示全部楼层
    我只是路过,不发表意见
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2019-8-28 04:18
  • 签到天数: 474 天

    连续签到: 2 天

    [LV.9]妙领天机

    0

    主题

    3036

    回帖

    3035

    积分

    声名显赫

    积分
    3035
    发表于 2018-7-28 21:31:36 | 显示全部楼层
    鼎力支持!!
    回复

    使用道具 举报

  • TA的每日心情
    奋斗
    2019-8-30 18:09
  • 签到天数: 471 天

    连续签到: 2 天

    [LV.9]妙领天机

    7

    主题

    3045

    回帖

    3056

    积分

    声名显赫

    积分
    3056
    发表于 2018-7-29 08:35:36 | 显示全部楼层
    不错 支持下
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2019-8-30 11:06
  • 签到天数: 472 天

    连续签到: 1 天

    [LV.9]妙领天机

    2

    主题

    3070

    回帖

    3072

    积分

    声名显赫

    积分
    3072
    发表于 2018-8-1 06:44:24 | 显示全部楼层
    非常好,顶一下
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2019-8-30 19:33
  • 签到天数: 475 天

    连续签到: 1 天

    [LV.9]妙领天机

    6

    主题

    3136

    回帖

    3141

    积分

    声名显赫

    积分
    3141
    发表于 2018-8-2 04:52:18 | 显示全部楼层
    我是个凑数的。。。
    回复

    使用道具 举报

  • TA的每日心情

    2019-8-30 09:16
  • 签到天数: 481 天

    连续签到: 6 天

    [LV.9]妙领天机

    5

    主题

    2974

    回帖

    2980

    积分

    声名显赫

    积分
    2980
    发表于 2018-8-2 13:54:52 | 显示全部楼层
    珍爱生命,果断回帖。
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|Archiver|手机版|小黑屋|任逍遥

    GMT+8, 2024-11-22 20:49 , Processed in 0.071892 second(s), 48 queries .

    Powered by 任逍遥 X3.5

    Copyright © 2001-2024, Rxiaoyao Cloud.

    快速回复 返回顶部 返回列表