C# 表达式目录树 Expression

# C# 表达式目录树 Expression

什么是表达式目录树

这个是委托

Func<int, int, int> func = (m, n) => m * n + 2;
Console.WriteLine("func:" + func(2, 3));//调用
//输出:func:8

这个是表达式树

Expression<Func<int, int, int>> expression = (m, n) => m * n + 2;
var result = expression.Compile();//返回的是个委托方法
Console.WriteLine("expression:" + result(2, 3));//调用
//输出:expression:8

表达式树和委托十分相似,但是它们是有区别的。委托是不能被修改的,而表达式树是可以被修改的。

表达式树就像一个数据结构体,用来存储计算逻辑的一种结构。

动态拼装表达式目录树

我们使用反编译工具反编译这段代码,来剖析它

Expression<Func<int, int, int>> expression = (m, n) => m * n + 2;

下面是反编译后的代码。看完之后,我们发现表达式树被通过Lambda声明之后编译为

//声明一个参数名称为 m 的参数表达式 parameterExpression
ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "m");
//声明一个参数名称为 n 的参数表达式 parameterExpression2
ParameterExpression parameterExpression2 = Expression.Parameter(typeof(int), "n");

//得到 参数表达式 parameterExpression, parameterExpression2 的乘法运算的二进制表达式 multiplyBinaryExpression
BinaryExpression multiplyBinaryExpression = Expression.Multiply(parameterExpression, parameterExpression2);
//声明一个值为 2 的常量表达式
ConstantExpression constantExpression = Expression.Constant(2, typeof(int));

//得到 二进制表达式 multiplyBinaryExpression 与常量表达式 constantExpression 的加法运算的二进制表达式 addBinaryExpression
BinaryExpression addBinaryExpression = Expression.Add(multiplyBinaryExpression, constantExpression);
//参数表达式数组
ParameterExpression[] parameters = new ParameterExpression[]
{
    parameterExpression,
    parameterExpression2
};
//声明表达式树 
Expression<Func<int, int, int>> expression = Expression.Lambda<Func<int, int, int>>(addBinaryExpression, parameters);

我们也可以尝试拼装自己的表达式树

拼装自己的表达式树

(x,y)=>x+y-2;
//声明两个参数表达式
ParameterExpression parameterExpressionX = Expression.Parameter(typeof(int), "x");
ParameterExpression parameterExpressionY = Expression.Parameter(typeof(int), "y");
//声明一个常量为2的常量表达式
ConstantExpression constantExpression = Expression.Constant(2, typeof(int));
//将x与y相乘 得到新的表达式树
BinaryExpression addExpression = Expression.Add(parameterExpressionX, parameterExpressionY);
//将新的表达式 与 常量表达式详见
BinaryExpression subExpression = Expression.Subtract(addExpression, constantExpression);
//声明一个Lambda 表达式
Expression<Func<int, int, int>> expression = Expression.Lambda<Func<int, int, int>>(subExpression, new ParameterExpression[] { parameterExpressionX, parameterExpressionY });
Console.WriteLine("myExpression:" + expression.Compile()(4, 5));//调用
//输出:myExpression:7

当然也可以拼装更为复杂的 如:

Expression<Func<Student, bool>> expression1 = (s) => s.ID.Equals(5);

拼装代码

ConstantExpression constantExpression = Expression.Constant(5, typeof(Int32));
ParameterExpression parameterExpression = Expression.Parameter(typeof(Student), "s");
MemberExpression memberExpression = Expression.Property(parameterExpression, typeof(Student).GetProperty("ID").GetMethod);
MethodInfo methodInfo = typeof(Int32).GetMethod("Equals", new Type[] { typeof(Int32) });
MethodCallExpression methodCallExpression = Expression.Call(memberExpression, methodInfo, new Expression[] { constantExpression });
Expression<Func<Student, bool>> expression2 = Expression.Lambda<Func<Student, bool>>(methodCallExpression, new ParameterExpression[] { parameterExpression });
Console.WriteLine("expression2:" + expression2.ToString());//输出:expression2:s => s.ID.Equals(5)

扩展应用

当然有的朋友就会问了,这种有什么用呢?我来给大家举个例子(虽然很无聊)

比如有这样一个需求,需要将Student对象转换为StudentCopy对象

创建类

public class Student
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age;
}


public class StudentCopy
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age;
}

对象初始化

Student student = new Student() { ID = 1, Age = 18, Name = "Oliver" };

方法一,直接硬编码:

当然这个方法有个缺点,不灵活。

StudentCopy studentCopy = new StudentCopy() { ID = student.ID, Age = student.Age, Name = student.Name };

方法二,使用反射:

代码

/// <summary>
/// 通过反射完成对象映射
/// </summary>
public class ReflectionMapping
{
    public static outT Trans<inT, outT>(inT obj)
    {
        var newObj = (outT)Activator.CreateInstance(typeof(outT));

        foreach (PropertyInfo propertyInfo in typeof(inT).GetProperties())
        {
            typeof(outT).GetProperty(propertyInfo.Name).SetValue(newObj, propertyInfo.GetValue(obj));
        }

        foreach (FieldInfo fieldInfo in typeof(inT).GetFields())
        {
            typeof(outT).GetField(fieldInfo.Name).SetValue(newObj, fieldInfo.GetValue(obj));
        }

        return newObj;
    }
}

调用

StudentCopy studentCopy = ReflectionMapping.Trans<Student, StudentCopy>(student);

方法三,使用表达式树

当然还可以使用我们今天说的主题--表达式树

我们先写一个Lambda形式的表达式树,再将其反编译,最后将里面的字段和属性通过动态的方式获取出来。

Expression<Func<Student, StudentCopy>> expression1 = (s) => new StudentCopy() { ID = s.ID, Age = s.Age, Name = s.Name };
ParameterExpression parameterExpression = Expression.Parameter(typeof(Student), "s");

MethodInfo methodInfo = typeof(StudentCopy).GetProperty("ID").GetMethod;
MemberAssignment idMemberAssignment = Expression.Bind(typeof(StudentCopy).GetProperty("ID").SetMethod, Expression.Property(parameterExpression, typeof(Student).GetProperty("ID").GetMethod));
MemberAssignment ageMemberAssignment = Expression.Bind(typeof(StudentCopy).GetField("Age"), Expression.Field(parameterExpression, typeof(Student).GetField("Age")));
MemberAssignment nameMemberAssignment = Expression.Bind(typeof(StudentCopy).GetProperty("Name").SetMethod, Expression.Property(parameterExpression, typeof(Student).GetProperty("Name").GetMethod));

Expression<Func<Student, StudentCopy>> expression = Expression.Lambda<Func<Student, StudentCopy>>(Expression.MemberInit(Expression.New(typeof(StudentCopy)), new MemberBinding[]
{
    idMemberAssignment,
   ageMemberAssignment,
   nameMemberAssignment
}), new ParameterExpression[]
{
    parameterExpression
});

StudentCopy studentCopy = expression.Compile()(student);

方法四,通过字典封装下

我们将上面的代码封装下,将表达式树中的func使用字典缓存起来

代码:

public class ExpressionDictMapping<TIn, TOut>
{
    private static Dictionary<string, Func<TIn, TOut>> _Dict = new Dictionary<string, Func<TIn, TOut>>();


    public static TOut Trans(TIn obj)
    {
        string key = string.Format("key_{0}_{1}", typeof(TIn).Name, typeof(TOut).Name);
        if (!_Dict.ContainsKey(key))
        {
            ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "s");

            MethodInfo methodInfo = typeof(TOut).GetProperty("ID").GetMethod;
            List<MemberAssignment> list = new List<MemberAssignment>();

            foreach (PropertyInfo propertyInfo in typeof(TIn).GetProperties())
            {
                MemberAssignment memberAssignment = Expression.Bind(typeof(TOut).GetProperty(propertyInfo.Name).SetMethod, Expression.Property(parameterExpression, propertyInfo.GetMethod));
                list.Add(memberAssignment);
            }

            foreach (FieldInfo fieldInfo in typeof(TIn).GetFields())
            {
                MemberAssignment memberAssignment = Expression.Bind(typeof(TOut).GetField(fieldInfo.Name), Expression.Field(parameterExpression, fieldInfo));
                list.Add(memberAssignment);
            }
            MemberBinding[] memberBindings = list.ToArray<MemberBinding>();
            Expression<Func<TIn, TOut>> expression = Expression.Lambda<Func<TIn, TOut>>(
                Expression.MemberInit(Expression.New(typeof(TOut)), memberBindings)
                , new ParameterExpression[] { parameterExpression }
            );

            _Dict[key] = expression.Compile();
        }
        return _Dict[key](obj);
    }
}

调用:

StudentCopy studentCopy = ExpressionDictMapping<Student, StudentCopy>.Trans(student);
StudentCopy studentCopy2 = ExpressionDictMapping<Student, StudentCopy>.Trans(student);

方法五,通过泛型封装(之前的泛型文章有提到泛型缓存数据)

我们使用泛型的方式缓存起来,这样更加的高效。

代码:

public class ExpressionGenericMapping<TIn, TOut>
{
    private static Func<TIn, TOut> func = null;

    static ExpressionGenericMapping()
    {
        ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "s");

        MethodInfo methodInfo = typeof(TOut).GetProperty("ID").GetMethod;
        List<MemberAssignment> list = new List<MemberAssignment>();

        foreach (PropertyInfo propertyInfo in typeof(TIn).GetProperties())
        {
            MemberAssignment memberAssignment = Expression.Bind(typeof(TOut).GetProperty(propertyInfo.Name).SetMethod, Expression.Property(parameterExpression, propertyInfo.GetMethod));
            list.Add(memberAssignment);
        }

        foreach (FieldInfo fieldInfo in typeof(TIn).GetFields())
        {
            MemberAssignment memberAssignment = Expression.Bind(typeof(TOut).GetField(fieldInfo.Name), Expression.Field(parameterExpression, fieldInfo));
            list.Add(memberAssignment);
        }
        MemberBinding[] memberBindings = list.ToArray<MemberBinding>();


        Expression<Func<TIn, TOut>> expression = Expression.Lambda<Func<TIn, TOut>>(
              Expression.MemberInit(Expression.New(typeof(TOut)), memberBindings)
              , new ParameterExpression[] { parameterExpression }
          );
        func = expression.Compile();
    }

    public static TOut Trans(TIn obj)
    {
        return func(obj);
    }
}

调用

StudentCopy studentCopy = ExpressionGenericMapping<Student, StudentCopy>.Trans(student);
StudentCopy studentCopy2 = ExpressionGenericMapping<Student, StudentCopy>.Trans(student);

比较效率

然后我们比较下上面各个版本的效率

 //普通方式
long common = 0;
//反射方式
long reflection = 0;
//dict缓存方式
long dict = 0;
//Generic 缓存方式
long generic = 0;

{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < 1000000; i++)
    {
        StudentCopy studentCopy = new StudentCopy()
        {
            ID = student.ID,
            Name = student.Name,
            Age = student.Age
        };
    }
    watch.Stop();
    common = watch.ElapsedMilliseconds;
}

{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < 1000000; i++)
    {
        StudentCopy studentCopy = ReflectionMapping.Trans<Student, StudentCopy>(student);
    }
    watch.Stop();
    reflection = watch.ElapsedMilliseconds;
}

{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < 1000000; i++)
    {
        StudentCopy studentCopy = ExpressionDictMapping<Student, StudentCopy>.Trans(student);
    }
    watch.Stop();
    dict = watch.ElapsedMilliseconds;
}
{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < 1000000; i++)
    {
        StudentCopy studentCopy = ExpressionGenericMapping<Student, StudentCopy>.Trans(student);
    }
    watch.Stop();
    generic = watch.ElapsedMilliseconds;
}


Console.WriteLine($"common = { common} ms");
Console.WriteLine($"reflection = { reflection} ms");
Console.WriteLine($"dict = { dict} ms");
Console.WriteLine($"generic = { generic} ms");

/*
 输出:
    common = 22 ms
    reflection = 1780 ms
    dict = 339 ms
    generic = 94 ms
 */

通过执行100w次 输出的时间来看,毫无疑问普通的硬编码方式执行时间是最快的(但是不灵活),然后是通过泛型缓存表达式树,再是通过字典缓存表达式树,最后是反射最慢。

修改表达式树

最上面我们就提到表达式树是可以被修改的而委托的方法是不能被修改的,现在我们就试着修改下下面这个表达式树

Expression<Func<int, int, int>> expression = (m, n) => m * n + 2;

我们希望将这个表达式修改为: (m, n) => m * n - 2;(将+变为-)。

目的就是为了让我们更加了解表达式树

修改表达式树必须使用这个类 ExpressionVisitor ,但是因为这个类是抽象类所以我们必须实例化它

//
// 摘要:
//     表示表达式树的访问者或重写者。
public abstract class ExpressionVisitor

实例化:

public class MyExpressionVisitor : ExpressionVisitor
{

    public Expression Modify(Expression expression)
    {
        return this.Visit(expression);
    }

    protected override Expression VisitBinary(BinaryExpression node)
    {

        //判断操作是否是+ ,如果是则修改为 -
        if (node.NodeType == ExpressionType.Add)
        {
            Expression left = node.Left;
            Expression right = node.Right;
            return Expression.Subtract(left, right);
        }
        return base.VisitBinary(node);
    }
}

调用:

Expression<Func<int, int, int>> expression = (m, n) => m * n + 2;
//我们希望将这个表达式修改为: (m, n) => m * n - 2;(将+变为-)//目的就是为了让我们更加了解表达式树

MyExpressionVisitor myExpressionVisitor = new MyExpressionVisitor();
Expression<Func<int, int, int>> newExpression = (Expression<Func<int, int, int>>)(myExpressionVisitor.Modify(expression));

Console.WriteLine("expression:" + expression.Compile()(2, 3));//输出8
Console.WriteLine("newExpression:" + newExpression.Compile()(2, 3));//输出4

表达式树拼装组合条件

在工作中我经常会遇到以下情况:

根据前台传入的条件不同,筛选不同的数据,这种情况下如果单纯的使用IF判断非常痛苦。但是如果使用我们的表达式树就非常轻松了。

if判断语句

List<Student> list = null;
bool isAge = true;
bool isID = true;
if (isAge && !isID)
{
    list = students.Where(c => c.Age == 10).ToList();
}
else if (isID && !isAge)
{
    list = students.Where(c => c.ID > 5).ToList();
}
else if (isID && isAge)
{
    list = students.Where(c => c.ID > 5 && c.Age == 10).ToList();
}
else if (!isID && !isAge)
{
    list = students.ToList();
}

表达式树拼装组合条件

实例化ExpressionVisitor类。(因为需要动态的修改表达式树的参数,所以需要覆写)


public class NewExpressionVisitor : ExpressionVisitor
{

    /// <summary>
    /// 将参数表达式 存储起来
    /// </summary>
    private ParameterExpression _ParameterExpression = null;
    public NewExpressionVisitor(ParameterExpression expression)
    {
        _ParameterExpression = expression;
    }

    public Expression Replace(Expression expression)
    {
        return base.Visit(expression);
    }

    /// <summary>
    /// 访问ParameterExpression 时会进入该方法,在该方法里面将参数替换
    /// </summary>
    /// <param name="node"></param>
    /// <returns></returns>
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return this._ParameterExpression;
    }
}

将转换代码封装到扩展类中

/// <summary>
/// 定义一个表达式树的扩展类
/// </summary>
public static class ExpressionExtend
{
    /// <summary>
    /// and 方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="expression1"></param>
    /// <param name="expression2"></param>
    /// <returns></returns>
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
    {
        return expression1.Trans(expression2, Expression.And);
    }

    /// <summary>
    /// or  方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="expression1"></param>
    /// <param name="expression2"></param>
    /// <returns></returns>
    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
    {
        return expression1.Trans(expression2, Expression.Or);
    }

    /// <summary>
    /// 转换方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="expression1">表达式树1</param>
    /// <param name="expression2">表达式树2</param>
    /// <param name="func">需要执行的方法</param>
    /// <returns></returns>
    private static Expression<Func<T, bool>> Trans<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2, Func<Expression, Expression, BinaryExpression> func)
    {
        if (expression1 == null) return expression2;
        if (expression2 == null) return expression1;

        //重新声明一个参数表达式
        ParameterExpression parameterExpression = Expression.Parameter(typeof(T), "c");
        //将参数表达式传递给我们新定义的NewExpressionVisitor 
        NewExpressionVisitor newExpressionVisitor = new NewExpressionVisitor(parameterExpression);

        //得到表达式的左边
        var left = newExpressionVisitor.Replace(expression1.Body);
        //得到表达式的右边
        var right = newExpressionVisitor.Replace(expression2.Body);
        //使用相应的方法拼接
        var body = func(left, right);

        //返回新的表达式树
        return Expression.Lambda<Func<T, bool>>(body, parameterExpression);
    }
}

调用

调用时就非常的灵活了 ,可以根据传入的条件灵活的进行操作。



List<Student> students = new List<Student>();
students.Add(new Student() { ID = 1, Age = 10, Name = "A" });
students.Add(new Student() { ID = 2, Age = 20, Name = "A2" });
students.Add(new Student() { ID = 3, Age = 10, Name = "A3" });
students.Add(new Student() { ID = 4, Age = 20, Name = "A4" });
students.Add(new Student() { ID = 5, Age = 10, Name = "A5" });
students.Add(new Student() { ID = 6, Age = 20, Name = "A6" });
students.Add(new Student() { ID = 7, Age = 10, Name = "A7" });
students.Add(new Student() { ID = 8, Age = 20, Name = "A8" });
students.Add(new Student() { ID = 9, Age = 10, Name = "A9" });
students.Add(new Student() { ID = 10, Age = 20, Name = "A10" });
students.Add(new Student() { ID = 11, Age = 10, Name = "A11" });
students.Add(new Student() { ID = 12, Age = 20, Name = "A12" });
students.Add(new Student() { ID = 13, Age = 10, Name = "A13" });
students.Add(new Student() { ID = 14, Age = 20, Name = "A14" });
students.Add(new Student() { ID = 15, Age = 10, Name = "A15" });
students.Add(new Student() { ID = 16, Age = 20, Name = "A16" });


Expression<Func<Student, bool>> express1 = (s) => s.ID > 5;
Expression<Func<Student, bool>> express2 = (s) => s.Age == 10;


Expression<Func<Student, bool>> express3 = express1.And(express2);

var list1 = students.Where(express1.Compile());
var list2 = students.Where(express2.Compile());
var list3 = students.Where(express3.Compile());

Console.WriteLine("\r\nlist1 表达式:{0} 结果:", express1.ToString());
Console.WriteLine(string.Join("\r\n", list1.Select(c => $"ID:{ c.ID}   Name:{c.Name}  Age:{c.Age}")));


Console.WriteLine("\r\nlist2 表达式:{0} 结果:", express2.ToString());
Console.WriteLine(string.Join("\r\n", list2.Select(c => $"ID:{ c.ID}   Name:{c.Name}  Age:{c.Age}")));


Console.WriteLine("\r\nlist3 表达式:{0} 结果:", express3.ToString());
Console.WriteLine(string.Join("\r\n", list3.Select(c => $"ID:{ c.ID}   Name:{c.Name}  Age:{c.Age}")));

/*
输出:
list1 表达式:s => (s.ID > 5) 结果:
ID:6   Name:A6  Age:20
ID:7   Name:A7  Age:10
ID:8   Name:A8  Age:20
ID:9   Name:A9  Age:10
ID:10   Name:A10  Age:20
ID:11   Name:A11  Age:10
ID:12   Name:A12  Age:20
ID:13   Name:A13  Age:10
ID:14   Name:A14  Age:20
ID:15   Name:A15  Age:10
ID:16   Name:A16  Age:20

list2 表达式:s => (s.Age == 10) 结果:
ID:1   Name:A  Age:10
ID:3   Name:A3  Age:10
ID:5   Name:A5  Age:10
ID:7   Name:A7  Age:10
ID:9   Name:A9  Age:10
ID:11   Name:A11  Age:10
ID:13   Name:A13  Age:10
ID:15   Name:A15  Age:10

list3 表达式:c => ((c.ID > 5) And (c.Age == 10)) 结果:
ID:7   Name:A7  Age:10
ID:9   Name:A9  Age:10
ID:11   Name:A11  Age:10
ID:13   Name:A13  Age:10
ID:15   Name:A15  Age:10


 */

转载于:https://www.cnblogs.com/haowuji/p/9518339.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C# 中的表达式目录(Expression Tree)是一个抽象语法(AST),可以表示一个 C# 表达式的结构和含义。它们提供了一种将代码表示为数据的方式,可以用于编写动态查询、ORM 映射、编译器、代码生成器等工具。 表达式目录是由 Expression 类型的对象组成的,每个对象都代表了一个 C# 表达式的一部分。例如,一个二元运算符表达式可以表示为一个 BinaryExpression 类型的对象,它包含了左右操作数和运算符等信息。 表达式目录可以通过编写 Lambda 表达式来创建。Lambda 表达式是一种匿名函数,可以将其编译成表达式目录。例如,以下代码创建了一个表示加法的表达式目录: ``` Expression<Func<int, int, int>> addExpr = (x, y) => x + y; ``` 这个表达式目录表示了一个接受两个 int 类型参数并返回 int 类型结果的函数,函数体是 x + y。 表达式目录也可以用于动态构建 LINQ 查询。例如,以下代码构建了一个查询,查询所有 age 大于 18 的人: ``` var people = new List<Person> { ... }; var paramExpr = Expression.Parameter(typeof(Person), "p"); var agePropExpr = Expression.Property(paramExpr, "Age"); var ageConstExpr = Expression.Constant(18, typeof(int)); var greaterThanExpr = Expression.GreaterThan(agePropExpr, ageConstExpr); var lambdaExpr = Expression.Lambda<Func<Person, bool>>(greaterThanExpr, paramExpr); var query = people.AsQueryable().Where(lambdaExpr); ``` 这个代码通过表达式目录构建了一个 Lambda 表达式,查询所有 age 大于 18 的人。其中,Expression.Parameter() 方法创建了一个表示 Person 类型的参数,Expression.Property() 方法创建了一个表示 Age 属性的表达式Expression.Constant() 方法创建了一个表示常量值的表达式Expression.GreaterThan() 方法创建了一个表示大于运算的表达式Expression.Lambda() 方法将这些表达式组合成一个 Lambda 表达式表达式目录可以在编译时或运行时进行解析和执行。例如,以下代码编译并执行了一个表示加法的表达式目录: ``` var addExpr = Expression.Add(Expression.Constant(1), Expression.Constant(2)); var addFunc = Expression.Lambda<Func<int>>(addExpr).Compile(); var result = addFunc(); // 3 ``` 这个代码创建了一个表示加法的表达式目录,然后将其编译成一个函数(addFunc),最后执行这个函数得到结果(result)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值