Csharp中表达式树

Csharper中的表达式树

这节课来了解一下表示式树是什么?

在C#中,表达式树是一种数据结构,它可以表示一些代码块,如Lambda表达式或查询表达式。表达式树使你能够查看和操作数据,就像你可以查看和操作代码一样。它们通常用于创建动态查询和解析表达式。

一、认识表达式树

为什么要这样说?它和委托有什么区别?

创建一个简单的表达式树和委托

  public class ExpressionDemo   {        void Show()       {           Func<int, bool> fun1 = x => x > 10;           Expression<Func<int, bool>> expression1 = x => x > 10;       }   } 

然后f12转到定义

public sealed class Expression<TDelegate> : LambdaExpression 

尝试用大括号定义一个表达式树

Csharp中表达式树

debug运行后,用vs查看一下定义的表达式树对象.
Csharp中表达式树

发现表达式树一些特点:

  1. 可以通过lambda表达式来声明
  2. 是一个泛型类的接口,类型参数是一个委托
  3. Expression声明中,不能包含大括号.
  4. 通过VS展开查看,包含body(lamubda的主体部分),描述了参数的名称和类型,描述了返回值的名称和类型; 展开body, body包含 左边是什么,右边是什么,式子的操作类型是什么.

结论:

表达式树,是一个计算式的描述,按照常规的计算逻辑,通过类的属性来进行描述多个节点之间的关系; 形似于一个树形结构----二叉树; 二叉树不断地去分解,可以得到这个式子中的任何一个独立的元素;----是一个二叉树,是一个数据结构; 如果需要可以把这个结构不断的拆解;得到中间的最小元素;在需要的时候,也可以通过每个元素,组装起来;

委托是一个类,而表达式树是一个二叉树的数据结构。

为了更加深入的了解表达式树,这里也使用ilspy进行反编译,以便于更加了解表达式树的本质.

这里使用一个比较复杂的表达式树的语句来方便我们去理解

Expression<Func<int ,int ,int>> expression2= (x, y) => x *y+2+3; 

Csharp中表达式树

优化一下这段代码

//定义2个变量 ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "x"); ParameterExpression parameterExpression2 = Expression.Parameter(typeof(int), "y"); //定义常量 var contact1 = Expression.Constant(2, typeof(int)); var contact2= Expression.Constant(3, typeof(int)); //定义表达式 x*y var MultiplyXy= Expression.Multiply(parameterExpression, parameterExpression2); //定义表达式 x*y的结果+2 var add1 = Expression.Add(MultiplyXy, contact1);  //定义表达式 x*y+2的结果+3 var add2 = Expression.Add(add1, contact2); //定义最终的lambda表达式 Expression<Func<int, int, int>> expression2 = Expression.Lambda<Func<int, int, int>>(add2, new ParameterExpression[2] {     parameterExpression,     parameterExpression2 }); 

如图所示的解析:

Csharp中表达式树

已经将相应的代码粘贴到上方,就是类似二叉树结构的因式分解,转换成为最小的子问题,最后解决一个需要解决的大问题。

二、动态拼装Expression

我们自己去拼装一个表达式树去理解表达式树的秘密.

首先创建一个People类

public class People {     public int Age { get; set; }     public string Name { get; set; }     public int Id; } 

下面来拼接一个比较复杂的表达式

Expression<Func<People, bool>> predicate = c => c.Id == 10 && c.Name.ToString().Equals("张三"); 

对应的表达式树的代码

  //定义一个People类型的参数   ParameterExpression parameterExpression = Expression.Parameter(typeof(People), "c");   //获取People的Id属性   PropertyInfo? propertyId = typeof(People).GetProperty("Id");   //定义10这个常量   ConstantExpression constantExpression = Expression.Constant(10, typeof(int));                   //定义c.Id>10这个表达式       BinaryExpression left =Expression.GreaterThan(Expression.Property(parameterExpression, propertyId), constantExpression);   //获取People的Name属性   PropertyInfo? propertyName = typeof(People).GetProperty("Name");   //c.Name   MemberExpression memName = Expression.Property(parameterExpression, propertyName);   //to string方法   MethodInfo? methodtostring=typeof(string).GetMethod("ToString",new Type[0]);   //调用tostring方法   MethodCallExpression instance =Expression.Call(memName, methodtostring,Array.Empty<Expression>());   //获取equals方法   MethodInfo? methodEquals = typeof(string).GetMethod("Equals", new Type[] { typeof(string) });   //定义c.Name.ToString().Equals("张三")这个表达式   MethodCallExpression right = Expression.Call(instance, methodEquals, Expression.Constant("张三", typeof(string)));   //定义c.Age<25这个表达式   PropertyInfo? propertyAge = typeof(People).GetProperty("Age");   ConstantExpression constantExpression2 = Expression.Constant(25, typeof(int));   BinaryExpression right2 = Expression.LessThan(Expression.Property(parameterExpression, propertyAge), constantExpression2);   //定义c.Id>10 && c.Name.ToString().Equals("张三") && c.Age<25这个表达式   BinaryExpression and1 = Expression.AndAlso(left, right);   BinaryExpression and2 = Expression.AndAlso(and1, right2);   //定义最终的lambda表达式   Expression<Func<People, bool>> expression = Expression.Lambda<Func<People, bool>>(and2, new ParameterExpression[1]   {       parameterExpression   });   //编译表达式   Func<People, bool> func = expression.Compile();   //调用表达式   People people = new People()   {       Id = 11,       Name = "张三",       Age = 20   };   Console.WriteLine(func(people)); 

这样就拼接出来了需要的表达式树.

三、表达式树的应用价值

为什么要拼装这个表达式目录树呢?

现在主流的是Linq:
Linq to Sql -----把相同的逻辑封装,把不同的逻辑通过表达式目录树来传递;
传递表达式目录树:对应的是查询条件;在传递之前就应该把查询的条件拼装好;

例子

 Expression<Func<People, bool>> expression2 = p => p.Id == 10 && p.Name.Equals("阳光下的微笑"); 

拼接后的结果

//按关键字是否存在来拼装; Expression<Func<People, bool>> exp = p=> true; Console.WriteLine("用户输入个名称,为空就跳过"); string name = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(name)) {     //exp = p => p.Name.Contains(name);     exp= exp.And(c=>c.Name.Contains(name)); } Console.WriteLine("用户输入个最小年纪,为空就跳过"); string age = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(age) && int.TryParse(age, out int iAge)) {    //  exp = p => p.Age > iAge;     exp = exp.And(p => p.Age > iAge); }  

例子2

//Expression<Func<People, bool>> newExpress = x => x.Age > 5 && x.Id > 5 

现在使用表达式树进行链接

 Expression<Func<People, bool>> lambda1 = x => x.Age > 5;  Expression<Func<People, bool>> lambda2 = x => x.Id > 5;   //Expression<Func<People, bool>> newExpress = x => x.Age > 5 && x.Id > 5;   Expression<Func<People, bool>> lambda3 = lambda1.And(lambda2); //且   两个都满足,通过&&链接   Expression<Func<People, bool>> lambda4 = lambda1.Or(lambda2);//或   两个只要有一个就可以  通过或者来链接  ||   Expression<Func<People, bool>> lambda5 = lambda1.Not();//非 

这里实现了常见的且、或、非逻辑运算符的表达式

 public static class ExpressionExtend  {      /// <summary>      /// 合并表达式 expr1 AND expr2      /// </summary>      /// <typeparam name="T"></typeparam>      /// <param name="expr1"></param>      /// <param name="expr2"></param>      /// <returns></returns>      public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)      {          //return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, expr2.Body), expr1.Parameters);           ParameterExpression newParameter = Expression.Parameter(typeof(T), "c");          NewExpressionVisitor visitor = new NewExpressionVisitor(newParameter);          var left = visitor.Replace(expr1.Body);          var right = visitor.Replace(expr2.Body); //为了能够生成一个新的表达式目录树           var body = Expression.And(left, right);           return Expression.Lambda<Func<T, bool>>(body, newParameter);       }      /// <summary>      /// 合并表达式 expr1 or expr2      /// </summary>      /// <typeparam name="T"></typeparam>      /// <param name="expr1"></param>      /// <param name="expr2"></param>      /// <returns></returns>      public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)      {           ParameterExpression newParameter = Expression.Parameter(typeof(T), "c");          NewExpressionVisitor visitor = new NewExpressionVisitor(newParameter);           var left = visitor.Replace(expr1.Body);          var right = visitor.Replace(expr2.Body);          var body = Expression.Or(left, right);          return Expression.Lambda<Func<T, bool>>(body, newParameter);      }      public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expr)      {          var candidateExpr = expr.Parameters[0];          var body = Expression.Not(expr.Body);           return Expression.Lambda<Func<T, bool>>(body, candidateExpr);      }  }   internal class NewExpressionVisitor : ExpressionVisitor {     public ParameterExpression _NewParameter { get; private set; }     public NewExpressionVisitor(ParameterExpression param)     {         this._NewParameter = param;     }     public Expression Replace(Expression exp)     {         return this.Visit(exp);     }     protected override Expression VisitParameter(ParameterExpression node)     {         return this._NewParameter;     } } 

现在有一个新的需求,需要把People拷贝到NewPeople这个新的类,来看下效率怎么样?

People和PeopleCopy类

 public class People  {      public int Age { get; set; }      public string Name { get; set; }      public int Id;  }  /// <summary>  /// 实体类Target  /// PeopleDTO  /// </summary>  public class PeopleCopy  {       public int Age { get; set; }      public string Name { get; set; }      public int Id;  } 

直接赋值的方式

 PeopleCopy peopleCopy1 = new PeopleCopy()  {      Id = people.Id,      Name = people.Name,      Age = people.Age  }; 

反射赋值的方式

 public class ReflectionMapper  {      /// <summary>      /// 反射      /// </summary>      /// <typeparam name="TIn"></typeparam>      /// <typeparam name="TOut"></typeparam>      /// <param name="tIn"></param>      /// <returns></returns>      public static TOut Trans<TIn, TOut>(TIn tIn)      {          TOut tOut = Activator.CreateInstance<TOut>();          foreach (var itemOut in tOut.GetType().GetProperties())          {              var propName = tIn.GetType().GetProperty(itemOut.Name);              itemOut.SetValue(tOut, propName.GetValue(tIn));           }           foreach (var itemOut in tOut.GetType().GetFields())          {              var fieldName = tIn.GetType().GetField(itemOut.Name);              itemOut.SetValue(tOut, fieldName.GetValue(tIn));           }          return tOut;      }  }  PeopleCopy peopleCopy2= ReflectionMapper.Trans<People, PeopleCopy>(people); 

json序列化的方式

public class SerializeMapper {     /// <summary>     /// 序列化反序列化方式     /// </summary>     /// <typeparam name="TIn"></typeparam>     /// <typeparam name="TOut"></typeparam>     public static TOut Trans<TIn, TOut>(TIn tIn)     {          string strTin = JsonConvert.SerializeObject(tIn);          return JsonConvert.DeserializeObject<TOut>(strTin);     } }  PeopleCopy peopleCopy3 = SerializeMapper.Trans<People, PeopleCopy>(people); 

表达式目录树的方式

 public class ExpressionMapper {     /// <summary>     /// 字典缓存--hash分布     /// </summary>     private static Dictionary<string, object> _Dic = new Dictionary<string, object>();      /// <summary>     /// 字典缓存表达式树     /// </summary>     /// <typeparam name="TIn"></typeparam>     /// <typeparam name="TOut"></typeparam>     /// <param name="tIn"></param>     /// <returns></returns>     public static TOut Trans<TIn, TOut>(TIn tIn)     {         string key = string.Format("funckey_{0}_{1}", typeof(TIn).FullName, typeof(TOut).FullName);         if (!_Dic.ContainsKey(key))         {             #region 这里是拼装---赋属性值的代码              ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");             //MemberBinding: 就是一个表达式目录树             List<MemberBinding> memberBindingList = new List<MemberBinding>();             foreach (var item in typeof(TOut).GetProperties())   //这里是处理属性的             {                 MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));                  MemberBinding memberBinding = Expression.Bind(item, property);                  memberBindingList.Add(memberBinding);             }             foreach (var item in typeof(TOut).GetFields()) //处理字段的             {                 MemberExpression property = Expression.Field(parameterExpression, typeof(TIn).GetField(item.Name));                 MemberBinding memberBinding = Expression.Bind(item, property);                 memberBindingList.Add(memberBinding);             }             MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());  //组装了一个转换的过程;              Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[]             {                 parameterExpression             });              #endregion               Func<TIn, TOut> func = lambda.Compile();//拼装是一次性的             _Dic[key] = func;         }         return ((Func<TIn, TOut>)_Dic[key]).Invoke(tIn);     } } PeopleCopy peopleCopy4 = ExpressionMapper.Trans<People, PeopleCopy>(people); 

表达式+反射+泛型类的方式

    public class ExpressionGenericMapper<TIn, TOut>//Mapper`2     {         private static Func<TIn, TOut> _FUNC = null;         static ExpressionGenericMapper()         {             ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");             List<MemberBinding> memberBindingList = new List<MemberBinding>();             foreach (var item in typeof(TOut).GetProperties())             {                 MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));                 MemberBinding memberBinding = Expression.Bind(item, property);                 memberBindingList.Add(memberBinding);             }             foreach (var item in typeof(TOut).GetFields())             {                 MemberExpression property = Expression.Field(parameterExpression, typeof(TIn).GetField(item.Name));                 MemberBinding memberBinding = Expression.Bind(item, property);                 memberBindingList.Add(memberBinding);             }             MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());             Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[]             {                     parameterExpression             });             _FUNC = lambda.Compile();//拼装是一次性的         }         public static TOut Trans(TIn t)         {             return _FUNC(t);         }     } } PeopleCopy peopleCopy5 = ExpressionGenericMapper<People, PeopleCopy>.Trans(people); 

最后运行一百万次,来看一下效率。

{     People people = new People()     {         Id = 11,         Name = "Richard",         Age = 31     };     long common = 0;     long generic = 0;     long cache = 0;     long reflection = 0;     long serialize = 0;     {         Stopwatch watch = new Stopwatch();         watch.Start();         for (int i = 0; i < 1_000_000; i++)         {             PeopleCopy peopleCopy = new PeopleCopy()             {                 Id = people.Id,                 Name = people.Name,                 Age = people.Age             };         }         watch.Stop();         common = watch.ElapsedMilliseconds;     }     {         Stopwatch watch = new Stopwatch();         watch.Start();         for (int i = 0; i < 1_000_000; i++)         {             PeopleCopy peopleCopy = ReflectionMapper.Trans<People, PeopleCopy>(people);         }         watch.Stop();         reflection = watch.ElapsedMilliseconds;     }     {         Stopwatch watch = new Stopwatch();         watch.Start();         for (int i = 0; i < 1_000_000; i++)         {             PeopleCopy peopleCopy = SerializeMapper.Trans<People, PeopleCopy>(people);         }         watch.Stop();         serialize = watch.ElapsedMilliseconds;     }     {          Stopwatch watch = new Stopwatch();         watch.Start();         for (int i = 0; i < 1_000_000; i++)         {             PeopleCopy peopleCopy = ExpressionMapper.Trans<People, PeopleCopy>(people);         }         watch.Stop();         cache = watch.ElapsedMilliseconds;     }     {         Stopwatch watch = new Stopwatch();         watch.Start();         for (int i = 0; i < 1_000_000; i++)         {             PeopleCopy peopleCopy = ExpressionGenericMapper<People, PeopleCopy>.Trans(people);         }         watch.Stop();         generic = watch.ElapsedMilliseconds;     }      Console.WriteLine($"common = {common} ms");        //性能最高,但是不能通用;     Console.WriteLine($"reflection = {reflection} ms");     Console.WriteLine($"serialize = {serialize} ms");     Console.WriteLine($"cache = {cache} ms");     Console.WriteLine($"generic = {generic} ms"); //性能好,而且扩展性也好===又要马儿跑,又要马儿不吃草。。。 } 

看运行后的结果
Csharp中表达式树

核心:动态生成硬编码;----代码运行的时候生成了一段新的逻辑;

四、表达式树和sql

为什么要使用表达式目录树来拼装解析呢?
可以提供重用性

如果封装好一个方法,接受一个表达式树,在解析的时候,其实就是不断的访问,访问的时候,会按照固定的规则,避免出错;

任何的一个表达式树都可以用一个通用的方法解析并且支持泛型,更加容易去封装;

例子:

需要的扩展类

 public class OperationsVisitor : ExpressionVisitor  {      public Expression Modify(Expression expression)      {          Console.WriteLine(expression.ToString()) ;           //ExpressionVisitor:          //1.Visit方法--访问表达式目录树的入口---分辨是什么类型的表达式目录          //2.调度到更加专业的方法中进一步访问,访问一遍之后,生成一个新的表达式目录   ---有点像递归,不全是递归;          //3.因为表达式目录树是个二叉树,ExpressionVisitor一直往下访问,一直到叶节点;那就访问了所有的节点          //4.在访问的任何一个环节,都可以拿到对应当前环节的内容(参数名称、参数值。。),就可以进一步扩展          return this.Visit(expression);      }       /// <summary>      /// 覆写父类方法      /// </summary>      /// <param name="b"></param>      /// <returns></returns>      protected override Expression VisitBinary(BinaryExpression b)      {                   if (b.NodeType == ExpressionType.Add)          {              Expression left = this.Visit(b.Left);              Expression right = this.Visit(b.Right);              return Expression.Subtract(left, right);          }          else if (b.NodeType==ExpressionType.Multiply) //如果是相乘          {              Expression left = this.Visit(b.Left);              Expression right = this.Visit(b.Right);              return Expression.Divide(left, right); //相除          }           return base.VisitBinary(b);      }       /// <summary>      /// 覆写父类方法      /// </summary>      /// <param name="node"></param>      /// <returns></returns>      protected override Expression VisitConstant(ConstantExpression node)      {          return base.VisitConstant(node);      } 

对应的表达式解析

 Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;  OperationsVisitor visitor = new OperationsVisitor();  //visitor.Visit(exp);  Expression expNew = visitor.Modify(exp); 

同时表达式树中已经通过使用观察者模式封装好了Visit方法.

  1. Visit方法--访问表达式树的入口---分辨是什么类型的表达式目录
  2. 调度到更加专业的方法中进一步访问,访问一边以后,生成一个新的表达式目录. --- 有点像递归,不全是递归
  3. 因为表达式目录树是一个二叉树,ExpreesionVistor一直往下访问,一直到叶子节点;通过二叉树的遍历就访问了所有的节点.
  4. 在访问的任何一个环节,都可以拿到对应当前环节的内容(参数名称、参数值...)就可以进一步扩展.

现在开始将表达式树跟sql语句进行连接

例子:
扩展类

 public class ConditionBuilderVisitor : ExpressionVisitor  {      private Stack<string> _StringStack = new Stack<string>();       public string Condition()      {          string condition = string.Concat(this._StringStack.ToArray());          this._StringStack.Clear();          return condition;      }       /// <summary>      /// 如果是二元表达式      /// </summary>      /// <param name="node"></param>      /// <returns></returns>      protected override Expression VisitBinary(BinaryExpression node)      {          if (node == null) throw new ArgumentNullException("BinaryExpression");           this._StringStack.Push(")");          base.Visit(node.Right);//解析右边          this._StringStack.Push(" " + node.NodeType.ToSqlOperator() + " ");          base.Visit(node.Left);//解析左边          this._StringStack.Push("(");           return node;      }       /// <summary>      /// 解析属性      /// </summary>      /// <param name="node"></param>      /// <returns></returns>      protected override Expression VisitMember(MemberExpression node)      {          if (node == null) throw new ArgumentNullException("MemberExpression");          //this._StringStack.Push(" [" + node.Member.Name + "] ");          ////return node;           if (node.Expression is ConstantExpression)          {              var value1 = this.InvokeValue(node);              var value2 = this.ReflectionValue(node);              //this.ConditionStack.Push($"'{value1}'");              this._StringStack.Push("'" + value2 + "'");          }          else          {              this._StringStack.Push(" [" + node.Member.Name + "] ");          }          return node;      }        private object InvokeValue(MemberExpression member)      {          var objExp = Expression.Convert(member, typeof(object));//struct需要          return Expression.Lambda<Func<object>>(objExp).Compile().Invoke();      }       private object ReflectionValue(MemberExpression member)      {          var obj = (member.Expression as ConstantExpression).Value;          return (member.Member as FieldInfo).GetValue(obj);      }       /// <summary>      /// 常量表达式      /// </summary>      /// <param name="node"></param>      /// <returns></returns>      protected override Expression VisitConstant(ConstantExpression node)      {          if (node == null) throw new ArgumentNullException("ConstantExpression");          this._StringStack.Push(" '" + node.Value + "' ");          return node;      }      /// <summary>      /// 方法表达式      /// </summary>      /// <param name="m"></param>      /// <returns></returns>      protected override Expression VisitMethodCall(MethodCallExpression m)      {          if (m == null) throw new ArgumentNullException("MethodCallExpression");           string format;          switch (m.Method.Name)          {              case "StartsWith":                  format = "({0} LIKE {1}+'%')";                  break;               case "Contains":                  format = "({0} LIKE '%'+{1}+'%')";                  break;               case "EndsWith":                  format = "({0} LIKE '%'+{1})";                  break;               default:                  throw new NotSupportedException(m.NodeType + " is not supported!");          }          this.Visit(m.Object);          this.Visit(m.Arguments[0]);          string right = this._StringStack.Pop();          string left = this._StringStack.Pop();          this._StringStack.Push(String.Format(format, left, right));           return m;      }  } 

对应的sql语句的解析

{      Expression<Func<People, bool>> lambda = x => x.Age > 5 && x.Id > 5                                              && x.Name.StartsWith("1") //  like '1%'                                              && x.Name.EndsWith("1") //  like '%1'                                              && x.Name.Contains("1");//  like '%1%'      string sql = string.Format("Delete From [{0}] WHERE [Age]>5 AND [ID] >5"         , typeof(People).Name         , " [Age]>5 AND [ID] >5");      ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();     vistor.Visit(lambda);     Console.WriteLine(vistor.Condition()); } {     //  ((( [Age] > '5') AND( [Name] =  [name] )) OR( [Id] > '5' ))      string name = "AAA";     Expression<Func<People, bool>> lambda = x => x.Age > 5 && x.Name == name || x.Id > 5;     ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();     vistor.Visit(lambda);     Console.WriteLine(vistor.Condition()); } {     Expression<Func<People, bool>> lambda = x => x.Age > 5 || (x.Name == "A" && x.Id > 5);     ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();     vistor.Visit(lambda);     Console.WriteLine(vistor.Condition()); } {     Expression<Func<People, bool>> lambda = x => (x.Age > 5 || x.Name == "A") && x.Id > 5;     ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();     vistor.Visit(lambda);     Console.WriteLine(vistor.Condition()); } 

在我自己的看法,使用表达式树而不是传统的方式去解析sql语句的优点

  1. 通过二叉树的方式表达,更加的有条理性
  2. 使用泛型等技术更方式实现一个通用的sql语句的解析。
  3. 会有类型检查,出错后也能使用异常处理。
发表评论

相关文章

当前内容话题