C# 中的 delegate, Lambda 表达式 和 event

本文深入解析C#中的委托、Lambda表达式及事件机制,包括委托的定义、初始化、调用,Lambda表达式的使用,以及事件的基本操作与应用,旨在提供清晰的实践指导。

C# 中的 delegate, Lambda 表达式 和 event

在开始之前,先说一下文章的表达习惯。

Object a = new Object();

在上面的例子里,Object 是一种类型,a 是一个引用类型的变量,new Object() 构造了一个对象,构造对象也被称为创建实例。有的文章习惯把 a 也称作实例,请根据上下文理解不要混淆。接下来你会经常看到类型, 引用, 对象这些词汇。

另外,我会使用"方法"而不使用"函数"的称呼。习惯使用 C/C++ 的读者接下来看到"方法"时不要感到困惑。

 

delegate 简介

delegate 的中文意思是委托,在 c# 里也是一个关键字。在 System 命名空间里,有 Delegate 类 和 MulticastDelegate 类,后者继承前者。编译器和其他工具可以从 MulticastDelegate 派生,但是我们无法显式的去继承这两个类。为了使用 delegate,我们需要使用 delegate 关键字来定义一个 delegate 类型:

public delegate int MyDelegate(int arg);

和其他类型一样,这个定义可以放在一个命名空间内部或其他类的内部。我们只需要写这样一行代码,编译器为我们实现细节。注意 delegate 关键字后面的部分,可以是任何合理的方法签名(方法签名由方法的参数列表和返回值组成,这两者都相同的方法具有相同的签名)。

现在我们已经定义了一个 delegate 类型 MyDelegate,接下来用这个类型创建一个实例:

MyDelegate sample = new MyDelegate(Method);

看起来和创建其他类的实例没有什么区别,但是构造函数的参数 Method 是什么呢?Method 必须是与 MyDelegate 定义时的签名一致的方法名称,也就是说 Method 的签名必须含有一个 int 参数和 int 返回值(这样描述并不准确,由于C#的协变和逆变的特性,详细参考:委托中的协变和逆变)。delegate 是引用类型,所以如果没有为 sample 初始化引用实例,那么 sample 的值是 null。

现在有一个问题。如果 Method 是静态方法,那么这样传递参数完全没有问题;如果 Method 不是静态方法,这样就不一定正确了。我们知道类的成员方法必须由具体的对象来发起调用,所以传递一个普通方法给 delegate 的构造函数而不指定所属对象是没有道理的。如果上面这段代码在类的普通方法里,由于 this 关键字可以省略,所以 Method 被编译器理解为 this.Method,所属对象是明确的,所以仍是正确的;如果上面这段代码在类的静态方法里,那么会因为 Method 没有明确的所属对象而错误。这个细节需要留意。

最后我们需要了解如何调用 delegate 实例里存储的方法。

int a = sample(3);

看起来与直接调用 Method 没有什么区别。的确如此,仅需要注意 delegate 引用如果为 null 则会引起空引用异常。delegate 引用与其他类型的引用一样,还可以执行 = 赋值运算、作为方法的参数传递、使用点运算符 . 访问对象成员。

目前为止,您可能觉得 delegate 与 C/C++ 里的函数指针很相似。但是,delegate 类型还可以执行 +/+= 和 -/-= 操作,因为 delegate 对象并不只是保存一个方法的引用,而是保存了一个方法列表。

sample -= Method;
sample += Method;

- 用于从方法列表里移除一个方法,+ 用于增加一个方法。+ 操作不会排除完全相同的方法引用。现在,sample(3) 的意义是依次调用方法列表里的每个方法,最后一个方法的返回值作为 sample(3) 的返回值。

请注意类型转换问题。在上面的代码里,我们把方法与 delegate 相加。其实,还可以直接把方法赋值给 delegate 引用。这些做法之所以合理,是因为方法可以隐式转化为 delegate(这种操作和 string 的工作方式相似,你无法修改 string 类型的对象本身,但可以在执行 +/+= 时创建新的 string 对象并返回给当前引用)。

为了简化编程,System 命名空间里定义了一系列的泛型 delegate:

复制代码
public delegate void Action();
public delegate void Action<T>(T arg); public delegate void Action<T1, T2>(T1 arg1, T2 arg2); // 还有更多参数版本的 Action public delegate TResult Func<TResult>(); public delegate TResult Func<T, TResult>(T arg); public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2); // 还有更多参数版本的 Func
复制代码

 

匿名方法和 Lambda 表达式

在上文中,我们必须事先定义 Method 方法才能初始化 delegate 实例。C# 提供了匿名方法来简化 delegate 的初始化工作。

MyDelegate sample = delegate(int arg)
{
    return arg * arg;
};

看起来好像在运行代码内部定义了一个新方法并使用 sample 保存。这样做使得代码更简洁。注意匿名方法的返回类型,必须是或可以隐式转换为 delegate 定义时签名的返回类型。匿名方法还有一个好处是可以访问外部变量,匿名方法捕获外部变量的引用而不是值,这一点需要留意,请看这段代码:

int n = 0;
sample = delegate(int arg) { return n * arg; }; n = 1; print(sample(1));

结果将输出 1 而不是 0。另外匿名方法里无法访问外部的 ref 或 out 参数。

Lambda 表达式是一种匿名函数。在 lambda 运算符 => 的左边指定输入参数,在右边指定执行表达式或语句。

sample = x => x * x;

看起来与上文的匿名方法非常相似。实际上匿名方法的所有限制和特征在 lambda 表达式中同样适用。

先看 Lambda 表达式的输入参数部分:

复制代码
// 无参数使用()
() => DoSomeThing();

// 一个参数直接写出参数名
arg => arg * arg; // 多个参数使用,分割 (arg1, arg2) => arg1 == arg2; // 参数类型无法推断时可以显式指定 (string arg1, char arg2) => arg1.Split(arg2);
复制代码

在这些例子里,=> 的右侧都是单一表达式,如果需要返回值,那么表达式的值就是返回值。使用大括号可以书写多条语句,但必须使用 return 明确指定返回值。

(arg1, arg2) =>
{
    if(arg1 == arg2) return 0;
    return arg1 > arg2 ? 1 : -1; }

 

event 简介

event 中文意思是事件,在 c# 里也是一个关键字。如下是 event 的定义方式:

public event MyDelegate myEvent
{
    add { print("[hello] " + value(3)); } remove { print("[world] " + value(7)); } }

看起来与属性的定义相似。event 具有两种操作:+= 和 -=,右操作数必须为与 event 定义类型一致的 delegate 实例(或 null)。+= 调用的是 add,-= 调用的是 remove。下面是调用的示例:

myEvent += x => x;
myEvent -= x => x;

结果打印出 "[hello] 3" 和 "[world] 7"。

有一种简化版的定义方法:

public event MyDelegate myEvent;

这种定义本质类似于:

复制代码
private MyDelegate _sample;
public event MyDelegate myEvent
{
    add { _sample += value; } remove { _sample -= value; } }
复制代码

使用上面的这种简单版的定义方法,由于没有可见的 _sample,那么对 MyDelegate 实例的访问就成了问题。为了解决这个问题,C# 是这样设计的:你可以在 myEvent 所在的类中像对 _sample 一样对 myEvent 进行各种操作,包括 = 赋值运算和调用存储的方法;但是在其他地方,myEvent 就是标准的 event,你只能使用 += 和 -= 运算。这样做的目的是,把所有可能不安全的操作封装在类内部。对就是这样,没有更多了。

 

最后,文章的目的是尽可能用简单的语言把委托和事件描述清楚,因此忽略了所有的异步操作等相关细节。更细节的资料请参考 msdn 或这篇文章: C# in Depth: Delegates and Events
<think>Lambda表达式C#中是一种简洁的匿名函数表示方式,它允许我们以更少的代码来创建委托或表达式树。Lambda表达式的主要作用是简化代码,提高可读性,并支持函数式编程风格。以下是Lambda表达式的几个关键作用: 1. **简化匿名方法**:Lambda表达式可以替代匿名方法,使代码更简洁。 2. **支持LINQ查询**:Lambda表达式是LINQ查询操作的基础,用于定义查询条件、投影等。 3. **创建委托实例**:Lambda表达式可以隐式转换为委托类型。 4. **表达式树**:Lambda表达式可以转换为表达式树(Expression Tree),用于在运行时分析代码结构(例如在Entity Framework中转换为SQL查询)。 ### Lambda表达式的语法 Lambda表达式的基本语法有两种形式: 1. **表达式Lambda**:当Lambda体只有一条表达式时使用。 ``` (input-parameters) => expression ``` 2. **语句Lambda**:当Lambda体包含多条语句时使用(用大括号包围)。 ``` (input-parameters) => { statement; } ``` ### 示例代码 以下代码展示了Lambda表达式的多种使用场景: ```csharp using System; using System.Collections.Generic; using System.Linq; class LambdaExample { delegate int MathOperation(int x, int y); static void Main() { // 1. 使用Lambda表达式创建委托实例 MathOperation add = (a, b) => a + b; Console.WriteLine($"5 + 3 = {add(5, 3)}"); // 2. 使用Lambda表达式作为参数(例如在List.Find方法中) List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0); Console.WriteLine("偶数: " + string.Join(", ", evenNumbers)); // 3. 在LINQ查询中使用Lambda表达式 var squares = numbers.Select(x => x * x); Console.WriteLine("平方: " + string.Join(", ", squares)); // 4. 多参数Lambda表达式 Func<int, int, int> multiply = (x, y) => x * y; Console.WriteLine($"3 * 4 = {multiply(3, 4)}"); // 5. 无参数Lambda表达式 Action greet = () => Console.WriteLine("Hello, Lambda!"); greet(); // 6. 语句Lambda(多条语句) Action<string> log = message => { string logMessage = $"[{DateTime.Now}] {message}"; Console.WriteLine(logMessage); }; log("这是一个日志消息"); // 7. Lambda表达式闭包(捕获外部变量) int factor = 3; Func<int, int> multiplier = n => n * factor; Console.WriteLine($"3 * 5 = {multiplier(5)}"); // 修改外部变量,Lambda会看到改变 factor = 5; Console.WriteLine($"5 * 5 = {multiplier(5)}"); // 8. 在事件处理中使用Lambda表达式 var button = new Button(); button.Click += (sender, e) => Console.WriteLine("按钮被点击了!"); button.SimulateClick(); // 9. 表达式树 System.Linq.Expressions.Expression<Func<int, int, int>> expression = (a, b) => a + b; var compiled = expression.Compile(); Console.WriteLine($"表达式树: 2 + 3 = {compiled(2, 3)}"); } } // 模拟按钮类 class Button { public event EventHandler Click; public void SimulateClick() { Click?.Invoke(this, EventArgs.Empty); } } ``` ### 关键特性解释 1. **类型推断**:Lambda表达式中的参数类型通常可以由编译器推断,因此不需要显式声明类型。 2. **闭包**:Lambda表达式可以捕获外部变量,形成闭包。被捕获的变量的生命周期会延长至与委托相同。 3. **表达式树**:当Lambda表达式赋值给`Expression<TDelegate>`类型时,编译器会生成表达式树而非可执行代码。表达式树可以在运行时被解析,常用于LINQ to SQL等场景。 ### 注意事项 - **变量捕获**:在循环中使用Lambda表达式捕获循环变量时需要注意,因为捕获的是变量本身而不是循环迭代时的值。这可能导致意外的结果(通常需要创建循环内的局部变量副本)。 - **性能**:对于性能敏感的场景,注意Lambda表达式创建的开销(尤其是涉及闭包时,因为闭包会生成一个类来保存捕获的变量)。 - **可读性**:虽然Lambda表达式可以使代码更简洁,但过度使用或复杂的Lambda表达式可能降低可读性。 ### 闭包陷阱示例 ```csharp var actions = new List<Action>(); for (int i = 0; i < 3; i++) { // 错误:所有委托都捕获同一个变量i // actions.Add(() => Console.WriteLine(i)); // 正确:创建局部变量副本 int temp = i; actions.Add(() => Console.WriteLine(temp)); } foreach (var action in actions) { action(); // 输出0,1,2 而不是3,3,3 } ``` ### Lambda表达式与匿名方法的比较 Lambda表达式是匿名方法的进一步简化。例如: ```csharp // 匿名方法 Func<int, int> square = delegate(int x) { return x * x; }; // Lambda表达式 Func<int, int> squareLambda = x => x * x; ``` ### 总结 Lambda表达式C#中一个强大的特性,它极大地简化了委托的创建匿名方法的使用,是LINQ函数式编程风格的基石。通过Lambda表达式,开发者可以编写更简洁、更具表达力的代码。 §§相关问题§§: 1. C#中的闭包是什么?它是如何实现的? 2. 表达式树(Expression Tree)在C#中有什么作用?它与Lambda表达式有什么关系? 3. 如何在多线程环境中安全使用Lambda表达式捕获的变量? 4. Lambda表达式匿名方法在性能上有何差异?</think># C#Lambda 表达式的作用详解 Lambda 表达式C# 中一种简洁的匿名函数表示方式,它极大地简化了委托的使用,使代码更加简洁、可读性更强。Lambda 表达式在函数式编程、LINQ 查询、事件处理等场景中发挥着重要作用。 ## Lambda 表达式的主要作用 | 作用 | 描述 | 示例 | |------|------|------| | **简化委托** | 替代匿名方法,简洁表示委托 | `(x, y) => x + y` | | **LINQ 查询** | 作为 LINQ 查询的筛选、投影等操作 | `.Where(x => x > 5)` | | **事件处理** | 简化事件处理程序的编写 | `btn.Click += (s, e) => {...}` | | **函数式编程** | 支持高阶函数函数组合 | `.Select(x => x * x)` | | **闭包支持** | 捕获外部变量,形成闭包 | `int factor = 2; ... x => x * factor` | | **表达式树** | 构建可解析的表达式树 | `Expression<Func<int, bool>> expr = x => x > 5` | ## Lambda 表达式基础语法 ```csharp // 基本语法 (parameters) => expression // 示例: Func<int, int> square = x => x * x; Console.WriteLine(square(5)); // 输出 25 ``` ## Lambda 表达式类型详解 ### 1. 表达式 Lambda ```csharp using System; public class ExpressionLambdaDemo { public static void Main() { // 1. 无参数 Lambda Action greet = () => Console.WriteLine("Hello, Lambda!"); greet(); // 2. 单参数 Lambda (可省略括号) Func<int, int> square = x => x * x; Console.WriteLine($"5 的平方: {square(5)}"); // 3. 多参数 Lambda Func<int, int, int> add = (a, b) => a + b; Console.WriteLine($"3 + 7 = {add(3, 7)}"); // 4. 显式类型参数 Func<double, double, double> divide = (double x, double y) => x / y; Console.WriteLine($"10 / 3 = {divide(10, 3):F2}"); // 5. 在 LINQ 中使用 int[] numbers = { 1, 2, 3, 4, 5 }; var evenSquares = numbers .Where(n => n % 2 == 0) .Select(n => n * n); Console.WriteLine("\n偶数的平方:"); foreach (var num in evenSquares) { Console.WriteLine(num); } } } ``` ### 2. 语句 Lambda ```csharp using System; public class StatementLambdaDemo { public static void Main() { // 1. 带多条语句的 Lambda Action<string> printDetails = name => { string greeting = $"Hello, {name}!"; Console.WriteLine(greeting); Console.WriteLine($"当前时间: {DateTime.Now:T}"); }; printDetails("Alice"); // 2. 带返回值的语句 Lambda Func<int, int, int> max = (a, b) => { if (a > b) return a; else return b; }; Console.WriteLine($"最大值: {max(25, 17)}"); // 3. 在事件处理中使用 var button = new Button(); button.Click += (sender, e) => { Console.WriteLine("按钮被点击!"); Console.WriteLine($"事件参数: {e}"); }; button.SimulateClick(); // 4. 在异步操作中使用 Func<Task> asyncOperation = async () => { Console.WriteLine("开始异步操作..."); await Task.Delay(1000); Console.WriteLine("异步操作完成!"); }; asyncOperation().Wait(); } } // 模拟按钮类 public class Button { public event EventHandler Click; public void SimulateClick() { Click?.Invoke(this, EventArgs.Empty); } } ``` ## 闭包与变量捕获 Lambda 表达式可以捕获外部变量,形成闭包: ```csharp using System; public class ClosureDemo { public static void Main() { // 1. 捕获局部变量 int baseValue = 10; Func<int, int> addBase = x => x + baseValue; Console.WriteLine($"10 + 5 = {addBase(5)}"); // 修改捕获的变量 baseValue = 20; Console.WriteLine($"20 + 5 = {addBase(5)}"); // 闭包捕获的是变量,不是值 // 2. 循环中的闭包问题 Console.WriteLine("\n循环中的闭包问题:"); var actions = new List<Action>(); for (int i = 0; i < 3; i++) { // 错误方式:所有委托都捕获同一个变量i // actions.Add(() => Console.WriteLine($"值: {i}")); // 正确方式:创建局部副本 int temp = i; actions.Add(() => Console.WriteLine($"值: {temp}")); } foreach (var action in actions) { action(); // 输出 0, 1, 2 } // 3. 闭包在延迟执行中的应用 Console.WriteLine("\n延迟执行:"); var functions = new List<Func<int>>(); for (int i = 0; i < 5; i++) { int current = i; functions.Add(() => current * current); } foreach (var func in functions) { Console.WriteLine(func()); } } } ``` ## Lambda 表达式的高级应用 ### 1. 表达式树 ```csharp using System; using System.Linq.Expressions; public class ExpressionTreeDemo { public static void Main() { // 1. 创建表达式树 Expression<Func<int, int, int>> addExpr = (a, b) => a + b; // 2. 分析表达式树 Console.WriteLine("表达式树结构:"); Console.WriteLine($"节点类型: {addExpr.NodeType}"); Console.WriteLine($"返回类型: {addExpr.ReturnType}"); Console.WriteLine($"主体: {addExpr.Body}"); Console.WriteLine($"参数: {string.Join(", ", addExpr.Parameters)}"); // 3. 编译表达式树为委托 var addFunc = addExpr.Compile(); Console.WriteLine($"3 + 4 = {addFunc(3, 4)}"); // 4. 动态构建表达式树 ParameterExpression paramA = Expression.Parameter(typeof(int), "a"); ParameterExpression paramB = Expression.Parameter(typeof(int), "b"); BinaryExpression addBody = Expression.Add(paramA, paramB); Expression<Func<int, int, int>> dynamicAddExpr = Expression.Lambda<Func<int, int, int>>(addBody, paramA, paramB); var dynamicAddFunc = dynamicAddExpr.Compile(); Console.WriteLine($"8 + 9 = {dynamicAddFunc(8, 9)}"); // 5. 在 LINQ to SQL 中的应用 // 实际应用中,表达式树会被转换为 SQL 查询 // var query = db.Products.Where(p => p.Price > 100); } } ``` ### 2. 高阶函数 ```csharp using System; public class HigherOrderFunctions { public static void Main() { // 1. 函数作为参数 int[] numbers = { 1, 2, 3, 4, 5 }; // 应用函数到每个元素 ProcessNumbers(numbers, x => x * x, "平方"); ProcessNumbers(numbers, x => x * 2, "双倍"); ProcessNumbers(numbers, Factorial, "阶乘"); // 2. 函数作为返回值 Func<int, Func<int, int>> createMultiplier = factor => x => x * factor; var doubleFunc = createMultiplier(2); var tripleFunc = createMultiplier(3); Console.WriteLine($"双倍 5: {doubleFunc(5)}"); Console.WriteLine($"三倍 5: {tripleFunc(5)}"); // 3. 函数组合 Func<int, int> square = x => x * x; Func<int, int> increment = x => x + 1; Func<int, int> squareThenIncrement = x => increment(square(x)); Func<int, int> incrementThenSquare = x => square(increment(x)); Console.WriteLine($"先平方后加1: {squareThenIncrement(4)}"); // 16 + 1 = 17 Console.WriteLine($"先加1后平方: {incrementThenSquare(4)}"); // (4+1)^2 = 25 } // 高阶函数示例 static void ProcessNumbers(int[] numbers, Func<int, int> processor, string operationName) { Console.WriteLine($"\n{operationName}操作:"); foreach (int num in numbers) { Console.WriteLine($"{num} -> {processor(num)}"); } } static int Factorial(int n) { if (n <= 1) return 1; return n * Factorial(n - 1); } } ``` ### 3. 模式匹配与 Lambda ```csharp using System; public class PatternMatchingDemo { public static void Main() { object[] items = { 42, "Hello", 3.14, null, new int[] { 1, 2, 3 } }; foreach (var item in items) { // 使用 switch 表达式 Lambda string description = item switch { int i => $"整数: {i}", string s => $"字符串: {s}", double d => $"双精度浮点数: {d:F2}", null => "空引用", Array a => $"数组,长度: {a.Length}", _ => "未知类型" }; Console.WriteLine(description); } // 使用 when 子句 int[] values = { 10, 20, 30, 40, 50 }; foreach (int value in values) { string result = value switch { < 20 => "小于20", >= 20 and < 40 => "20到39之间", >= 40 => "40或更大" }; Console.WriteLine($"{value}: {result}"); } } } ``` ## Lambda 表达式最佳实践 1. **保持简洁性** ```csharp // 简洁 var squares = numbers.Select(n => n * n); // 复杂时使用命名方法 var results = data.Where(IsValidData); private bool IsValidData(DataItem item) { // 复杂的验证逻辑 } ``` 2. **避免修改外部状态** ```csharp // 不良实践:修改外部变量 int counter = 0; items.ForEach(item => { Process(item); counter++; // 副作用 }); // 良好实践:无副作用 int processedCount = items.Count(item => { Process(item); return true; }); ``` 3. **正确处理闭包** ```csharp // 避免循环闭包陷阱 for (int i = 0; i < 10; i++) { int current = i; // 创建局部副本 tasks.Add(Task.Run(() => Console.WriteLine(current))); } ``` 4. **性能考虑** ```csharp // 高频调用的地方避免重复创建委托 private static readonly Func<int, int> Square = x => x * x; void ProcessData(IEnumerable<int> data) { var results = data.Select(Square); // 重用委托 } ``` 5. **表达式树与委托选择** ```csharp // 需要编译时:使用委托 Func<int, int> square = x => x * x; // 需要分析表达式结构时:使用表达式树 Expression<Func<int, int>> squareExpr = x => x * x; ``` ## Lambda 表达式常见陷阱 1. **闭包捕获变量而非值** ```csharp int value = 10; Func<int> getValue = () => value; value = 20; Console.WriteLine(getValue()); // 输出 20,不是 10 ``` 2. **循环变量捕获问题** ```csharp var actions = new List<Action>(); for (int i = 0; i < 3; i++) { // 所有委托捕获同一个变量 i actions.Add(() => Console.WriteLine(i)); } foreach (var action in actions) { action(); // 全部输出 3 } ``` 3. **内存泄漏** ```csharp eventSource.Event += (s, e) => { // 如果 eventSource 生命周期长,可能造成内存泄漏 HandleEvent(e); }; // 解决方案:使用弱引用或显式取消订阅 ``` 4. **异步 Lambda 中的异常处理** ```csharp // 错误:异步 Lambda 中的异常可能被忽略 button.Click += async (s, e) => { await SomeAsyncOperation(); }; // 正确:添加异常处理 button.Click += async (s, e) => { try { await SomeAsyncOperation(); } catch (Exception ex) { // 处理异常 } }; ``` ## Lambda 表达式总结 | 特性 | 描述 | 使用场景 | |------|------|----------| | **简洁性** | 减少代码量,提高可读性 | 简单委托实现 | | **闭包** | 捕获外部变量 | 状态保持、回调函数 | | **LINQ 集成** | 查询操作的核心 | 数据筛选、转换、聚合 | | **函数式编程** | 支持高阶函数 | 函数组合、策略模式 | | **表达式树** | 可分析代码结构 | ORM 查询、动态代码生成 | | **事件处理** | 简化事件订阅 | UI 事件、异步回调 | Lambda 表达式是现代 C# 编程的核心特性之一,它使代码更加简洁、表达力更强。掌握 Lambda 表达式的各种用法注意事项,能够显著提高开发效率代码质量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值