C#语言入门详解笔记(9)—P19 委托详解

14 篇文章 4 订阅

C#语言入门详解_哔哩哔哩_bilibiliC#语言入门详解搬运,随youtube上进度更新。刘老师的C#视频教程到30集就告一段落了,感谢刘老师在这5年间的付出。能上youtube的同学可以去刘老师的主页观看,https://www.youtube.com/channel/UCmvAggiJGwJGSzAtqFRBQ7g重新上传,修复31P无声音问题https://www.bilibili.com/video/BV1wx411K7rb目录

1、什么是委托

1.1、委托(delegate)是函数指针的“升级版”

1.2、一切皆地址

1.3、直接调用和间接调用

1.4、Java中没有与委托相对应的功能实体

1.5、委托的简单使用

2、委托的声明(自定义委托)

2.1、委托是一种类(class),类是数据类型所以委托也是一种数据类型

2.2、它的声名方式与一般的类不同,主要是为了照顾可读性和C/C++传统

2.3、注意声明委托的位置

2.4、委托与所封装的方法必需“类型兼容”

3、委托的一般使用

3.1、实例:把方法当作参数传给另一个方法

(1)正确使用1:模板方法,“借用”指定的外部方法来产生结果

(2)正确使用2:回调(callback)方法,调用指定的外部方法

(3)实例1:模板方法

(4)实例2:回调方法

 3.2、注意:难精通+易使用+功能强大东西,一旦被滥用则后果非常严重

缺点1:这是一种方法级别的紧耦合,现实工作中要慎之又慎(相当于把房间墙给打穿了)

缺点2:使可读性下降、debug的难度增加

缺点3:把委托回调、异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护

缺点4:委托使用不当有可能造成内存泄漏和程序性能下降

4、委托的高级使用

4.1、多播(multicast)委托

(1)单播委托

(2)多播委托

4.2、隐式异步调用

(1)同步与异步的简介

(2)同步调用与异步调用的对比

(3)隐式多线程 v.s. 显示多线程

4.3、应该适时地使用接口(interface)取代一些对委托的使用

Java完全地使用接口取代了委托功能,即Java没有与C#中委托相对应的功能实体 


1、什么是委托

1.1、委托(delegate)是函数指针的“升级版”

  • 实例:C/C++中的函数指针

了解什么是指针

先声明,再使用

#include <stdio.h>

int(* Calc)(int a, int b);    //定义函数指针

int Add(int a, int b)
{
    int result = a + b;
    return result;
}

int Sub(int a, int b)
{
    int result = a - b;
    return result;
}

int main()
{
    int x = 100;
    int y = 200;
    int z = 0;

    Calc funcPoint1 = &Add;
    Calc funcPoint2 = &Sub;

    Calcz = funcPoint1(x, y);    //通过函数地址调用——间接调用
    print("%d+%d=%d\n", x, y, z);

    z = Add(x, y);      //通过函数名调用——直接调用
    print("%d+%d=%d\n", x, y, z);

    z = Sub(x, y);      //通过函数名调用——直接调用
    print("%d-%d=%d\n", x, y, z);

    system("pause");
    return 0;
}

1.2、一切皆地址

(1)变量(数据)是以某个地址为起点的一段内存中所存储的值

程序的本质是数据+算法。

(2)函数(算法)是以某个地址为起点的一段内存中所存储的一组机器语言指令

1.3、直接调用和间接调用

(1)直接调用:通过函数名来调用函数,CPU通过函数名直接获取函数所在地址并开始执行➡返回

(2)间接调用:通过函数指针来调用函数,CPU通过读取函数指针存储的值获得函数所在地址并开始执行➡返回

1.4、Java中没有与委托相对应的功能实体

1.5、委托的简单使用

(1)Action委托:调用void函数

(2)Func委托:

class Program
    {
        static void Main(string[] args)
        {
            Calculator calculator = new Calculator();
            Action action = new Action(calculator.Report);  //这里没有“()”,Report()
            calculator.Report();    //直接调用
            action.Invoke();    //间接调用
            action();   //简介调用-简便写法

            Func<int, int, int> func1 = new Func<int, int, int>(calculator.Add);
            Func<int, int, int> func2 = new Func<int, int, int>(calculator.Sub);

            int x = 100;
            int y = 200;
            int z = 0;

            z = func1.Invoke(x, y);
            //z = func1(x, y);函数指针形式写法
            Console.WriteLine(z);
            z = func2.Invoke(x, y);
            Console.WriteLine(z);
        }

        class Calculator
        {
            public void Report()
            {
                Console.WriteLine("I have 3 methods");
            }

            public int Add(int a, int b)
            {
                int result = a + b;
                return result;
            }

            public int Sub(int a, int b)
            {
                int result = a - b;
                return result;
            }
        }

2、委托的声明(自定义委托)

2.1、委托是一种类(class),类是数据类型所以委托也是一种数据类型

2.2、它的声名方式与一般的类不同,主要是为了照顾可读性和C/C++传统

2.3、注意声明委托的位置

  • 避免写错地方结果声明成嵌套类型

    public delegate double Calc(double a, double b);

    class Program
    {
        static void Main(string[] args)
        {
            Calculator calculator = new Calculator();
            Calc calc1 = new Calc(calculator.Add);
            Calc calc2 = new Calc(calculator.Sub);
            Calc calc3 = new Calc(calculator.Mul);
            Calc calc4 = new Calc(calculator.Div);

            double a = 100;
            double b = 200;
            double c = 0;

            c = calc1.Invoke(a, b);//c = calc1(a, b);
            Console.WriteLine(c);
            c = calc2.Invoke(a, b);
            Console.WriteLine(c);
            c = calc3.Invoke(a, b);
            Console.WriteLine(c);
            c = calc4.Invoke(a, b);
            Console.WriteLine(c);
        }
        
        class Calculator
        {
            public double Add(double a, double b)
            {
                double result = a + b;
                return result;
            }

            public double Sub(double a, double b)
            {
                double result = a - b;
                return result;
            }
            public double Mul(double a, double b)
            {
                double result = a * b;
                return result;
            }
            public double Div(double a, double b)
            {
                double result = a / b;
                return result;
            }
        }

    }

2.4、委托与所封装的方法必需“类型兼容”

delegatedouble Calc(double x, double y);

double Add(double x, double y);

double Sub(double x, double y);

double Mul(double x, double y);

double Div(double x, double y);

{return x + y;}

{return x - y;}

{return x * y;}

{return x / y;}

  • 返回值的数据类型一致

  • 参数列表在个数和数据类型上一致(参数名不需要一样)

3、委托的一般使用

3.1、实例:把方法当作参数传给另一个方法

在日常工作当中使用委托,我们一般把委托当作方法的参数传到方法里面去。

这样做的好处在于,你写了一个方法,这个方法有一个委托类型的参数(那我们都知道委托封装了一个方法),然后我们在方法体里使用传进来的委托,间接的调用委托封装了的方法,这样就形成了一种动态调用方法的代码结构。

像这种把委托当作参数传进方法的用法,具体分为两种:

(1)正确使用1:模板方法,“借用”指定的外部方法来产生结果

  • 相当于“填空题”

解释:相当于你写了一个方法,这个方法它是一个模板。这个模板里面有一处是不确定的,其余部分都是确定好了的。这个不确定的部分就靠你传进来的委托类型的参数所包含的方法来填补

  • 常位于代码中部

  • 委托有返回值

(2)正确使用2:回调(callback)方法,调用指定的外部方法

  • 回调指的是这样一种场景:

        某一天我遇到你了,然后我把名片双手奉上,说:“上面有我的电话,如果你需要帮助,打我电话。”

那我们就知道了,如果有一天你需要帮助的时候,你就会打这个电话来找我。找我做什么呢,是相当于调用我的某个功能来帮助你解决一些问题;如果你不需要我帮忙,那你永远都不会打这个电话。现在我和你之间就构成了一种会回调关系。

       所谓回调关系就是——某个方法,我可以调用它,也可以不调用。用得着的时候调用,用不着就不调用,而且回调方法还给了我们一个机会,让我们可以动态的选择后续将被调用的方法。这就相当于不止我一个人给了你名片,而是很多人都给了你名片。当你需要帮助的时候,你可以从一大把名片中选出一个人来,打他的电话,向他寻求帮助。

       这就是回调方法。

  • 当以回调方法的形式来使用委托的时候:

       我们要做的是把委托类型的参数,传进主调方法里面去。被传进主调方法里去的委托类型的参数,它内部会封装一个被回调的方法,也就是我们常说的回调方法。主调函数会根据自己的逻辑来决定是否调用这个回调方法。

        那么整个代码的逻辑看上去就像是一个流水线,一般情况下,我的主调方法会在主要逻辑执行完了之后,决定是不是调用这个回调方法,那么回调方法一般都会位于主调方法的末尾之处。而且回调方法一般的用处是来执行一些后续的工作,它构成一个流水线结构,所以说回调方法一般是没有返回值的

  • 相当于“流水线” 

  • 常位于代码末尾

  • 委托无返回值

(3)实例1:模板方法

    class Program
    {
        static void Main(string[] args)
        {
            ProductFactory productFactory = new ProductFactory();
            WrapFactory wrapFactory = new WrapFactory();

            Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
            Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);

            Box box1 = wrapFactory.WrapProduct(func1);
            Box box2 = wrapFactory.WrapProduct(func2);

            Console.WriteLine(box1.Product.Name);
            Console.WriteLine(box2.Product.Name);
        }
        
        class Product
        {
            public string Name { get; set; }
        }

        class Box  //
        {
            public Product Product { get; set; }
        }

        class WrapFactory  //打包盒子的工厂
        {
            //
            //模板方法,大部分逻辑已经固定了
            //逻辑中的【2】部分,委托的调用是可以修改的逻辑。
            //传进去的委托封装了什么方法,就可以得到对应产出的产品;
            //这样的好处是 Product、 Box 、WrapFactory三个类都不需要更改,只需要不停扩展 ProductFactor
            //就可以生成不同的产品,最大限度的实现代码的重复使用。
            public Box WrapProduct(Func<Product> getProduct) 
            {
                //【1】准备一个Box
                Box box = new Box();
                //【2】获取一个产品
                Product product = getProduct.Invoke();
                //【3】把产品装到Box里面
                box.Product = product;
                //【4】把Box返还回去
                return box;
            }

        }

        class ProductFactory  //制造产品的工厂
        {
            public Product MakePizza()
            {
                Product product = new Product();
                product.Name = "Pizza";
                return product;
            }

            public Product MakeToyCar()
            {
                Product product = new Product();
                product.Name = "ToyCar";
                return product;
            }
        }
    }

  • Reuse,重复使用,也叫“复用”。代码的复用不但可以提高工作效率,还可以减少bug的引入。良好的复用结构是所有优秀软件所追求的共同目标之一。

(4)实例2:回调方法

    class Program
    {
        static void Main(string[] args)
        {
            ProductFactory productFactory = new ProductFactory();
            WrapFactory wrapFactory = new WrapFactory();

            Func<Product> func1 = new Func<Product>(productFactory.MakePizza);
            Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);

            Logger logger = new Logger();
            Action<Product> log = new Action<Product>(logger.Log);

            Box box1 = wrapFactory.WrapProduct(func1, log);
            Box box2 = wrapFactory.WrapProduct(func2, log);

            Console.WriteLine(box1.Product.Name);
            Console.WriteLine(box2.Product.Name);
        }
        
        //记录程序的运行状态
        class Logger
        {
            public void Log(Product product)
            {
                //UtcNow是不带时区的时间,Now是带时区的
                Console.WriteLine("Product '{0}' created at {1}. Price is {2}.", product.Name, DateTime.UtcNow, product.Price);
            }
        }
        class Product
        {
            public string Name { get; set; }
            public double Price { get; set; }
        }

        class Box  //
        {
            public Product Product { get; set; }
        }

        class WrapFactory  //打包盒子的工厂
        {
            public Box WrapProduct(Func<Product> getProduct, Action<Product> logCallback) 
            {
                Box box = new Box();
                Product product = getProduct.Invoke();
                if (product.Price >= 50)
                {
                    logCallback(product);
                }

                box.Product = product;
                return box;
            }

        }

        class ProductFactory  //制造产品的工厂
        {
            public Product MakePizza()
            {
                Product product = new Product();
                product.Name = "Pizza";
                product.Price = 12;
                return product;
            }

            public Product MakeToyCar()
            {
                Product product = new Product();
                product.Name = "ToyCar";
                product.Price = 100;
                return product;
            }
        }
    }

 3.2、注意:难精通+易使用+功能强大东西,一旦被滥用则后果非常严重

  • 缺点1:这是一种方法级别的紧耦合,现实工作中要慎之又慎(相当于把房间墙给打穿了)

  • 缺点2:使可读性下降、debug的难度增加

  • 缺点3:把委托回调、异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护

举例:一层套一层,Operation还有许多派生类,定位bug需要花费十几个小时。

    class Program
    {
        static void Main(string[] args)
        {
            Operation opt1 = new Operation();
            Operation opt2 = new Operation();
            Operation opt3 = new Operation();

            opt3.InnerOperation = opt2;
            opt2.InnerOperation = opt1;

            opt3.Operate(new object(), null, null);
            //问题1:如果传入的两个参数为null,失败和成功的效果是什么?答:内层的操作会调用外层的回调!
            //问题2:如果传入的两个参数不为null,会出现什么情况?答:所有默认callback都被“穿透性”屏蔽
        }

        class Operation
        {
            public Action DefaultSuccessCallback { get; set; }
            public Action DefaultFailureCallback { get; set; }
            public Operation InnerOperation { get; set; }

            public object Operate(object input, Action successCallback, Action failureCallback)
            {
                if (successCallback == null)
                {
                    successCallback = this.DefaultSuccessCallback;
                }

                if (failureCallback == null)
                {
                    failureCallback = this.DefaultFailureCallback;
                }

                object result = null;
                try
                {
                    result = this.InnerOperation.Operate(input, successCallback, failureCallback);
                    //Do something here
                }
                catch
                {
                    failureCallback.Invoke();
                }

                successCallback.Invoke();
                return result;
            }
        }
    }

  • 缺点4:委托使用不当有可能造成内存泄漏和程序性能下降

  •  原因

委托会引用一个方法,这如果是一个实例方法,那么这个方法必定隶属于一个对象。你那一个委托引用这个方法,那么这个对象必需存在内存当中,即便是没有其他的引用变量,引用这个对象了,这个对象的内存也不能被释放。因为释放了这个内存,委托就不能间接调用这个对象的方法了,所以说委托有可能造成内存泄漏。

4、委托的高级使用

4.1、多播(multicast)委托

(1)单播委托

using System;
using System.Collections.Generic;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
            Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
            Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };
            Action action1 = new Action(stu1.DoHomeword);
            Action action2 = new Action(stu2.DoHomeword);
            Action action3 = new Action(stu3.DoHomeword);

            action1.Invoke();
            action2.Invoke();
            action3.Invoke();
        }

        class Student
        {
            public int ID { get; set; }
            public ConsoleColor PenColor { get; set; }

            public void DoHomeword()
            {
                for (int i = 0; i < 5; i++)
                {
                    Console.ForegroundColor = this.PenColor;
                    Console.WriteLine("Student {0} doing homework {1} hour(s).", this.ID, i);
                    //在那个线程中调用,这个现场就睡1秒钟
                    Thread.Sleep(1000);
                }
            }
        }
    }
}

(2)多播委托

 用一个委托封装多个方法的使用方式;

多播委托执行方法的顺序是封装顺序。

        static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
            Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
            Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };
            Action action1 = new Action(stu1.DoHomeword);
            Action action2 = new Action(stu2.DoHomeword);
            Action action3 = new Action(stu3.DoHomeword);

            action1 += action2;//把action2合并到action1
            action1 += action3;//把action3合并到action1

            action1.Invoke();
        }

 

        static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
            Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
            Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };
            Action action1 = new Action(stu1.DoHomeword);
            Action action2 = new Action(stu2.DoHomeword);
            Action action3 = new Action(stu3.DoHomeword);

            action2 += action1;//把action1合并到action2
            action3 += action2;//把action2合并到action3

            action3.Invoke();
        }

4.2、隐式异步调用

(1)同步与异步的简介

  • 中英文的语言差异

  • 同步:你做完了我(在你的基础上)接着做

  • 异步:咱们两个同时做(相当于的汉语中的”同步进行“)

(2)同步调用与异步调用的对比

  • 每一个运行的程序就是一个进程(process)

  • 每个进程可以有一个或者多个线程(thread)

  • 同步调用实在同一个线程内

  • 异步调用的底层机理是多线程

  • 串行==同步==单线程,并行==异步==多线程

红线:主线程

蓝线:方法1

绿线:方法2

黄线:方法3

同步调用1

异步调用1和异步调用2

  一个线程的开始或结束不影响其他线程

这里说的”互不相干“指的是逻辑上,而现实工作中经常会遇到多个线程共享(即同时访问)同一个资源(比如某个变量)的情况,这时候如果处理不当就会产生线程间争夺资源的冲突。

(3)隐式多线程 v.s. 显示多线程

  • 直接同步调用:使用方法名

        static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
            Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
            Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };

            stu1.DoHomeword();
            stu2.DoHomeword();
            stu3.DoHomeword();

            for (int i = 0; i < 4; i++)
            {
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("Main thread {0}", i);
                Thread.Sleep(1000);
            }
        }

  • 间接同步调用:使用单播/多播委托的Inoke方法

        static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
            Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
            Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };
            Action action1 = new Action(stu1.DoHomeword);
            Action action2 = new Action(stu2.DoHomeword);
            Action action3 = new Action(stu3.DoHomeword);

            action1.Invoke();
            action2.Invoke();
            action3.Invoke();

            //多播委托同步调用
            //action1 += action2;
            //action1 += action3;
            //action1.Invoke();

            for (int i = 0; i < 4; i++)
            {
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("Main thread {0}", i);
                Thread.Sleep(1000);
            }
        }
  • 隐式异步调用:使用委托的BeginInvoke,自动生成

        static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
            Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
            Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };
            Action action1 = new Action(stu1.DoHomeword);
            Action action2 = new Action(stu2.DoHomeword);
            Action action3 = new Action(stu3.DoHomeword);

            action1.BeginInvoke(null, null);
            action2.BeginInvoke(null, null);
            action3.BeginInvoke(null, null);

            for (int i = 0; i < 4; i++)
            {
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("Main thread {0}", i);
                Thread.Sleep(1000);
            }
        }

这里由于多个线程都在访问同一个资源,所以颜色有冲突。可以通过线程锁的方式去避免。

  • 显示异步调用:使用Thread或Task

  •  Thread调用

            static void Main(string[] args)
            {
                Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
                Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
                Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };
    
                Thread thread1 = new Thread(new ThreadStart(stu1.DoHomeword));
                Thread thread2 = new Thread(new ThreadStart(stu2.DoHomeword));
                Thread thread3 = new Thread(new ThreadStart(stu3.DoHomeword));
    
                thread1.Start();
                thread2.Start();
                thread3.Start();
    
                for (int i = 0; i < 4; i++)
                {
                    Console.ForegroundColor = ConsoleColor.Cyan;
                    Console.WriteLine("Main thread {0}", i);
                    Thread.Sleep(1000);
                }
            }

  • Task调用

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 1, PenColor = ConsoleColor.Yellow };
            Student stu2 = new Student() { ID = 2, PenColor = ConsoleColor.Green};
            Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red };

            Task task1 = new Task(new Action(stu1.DoHomeword));
            Task task2 = new Task(new Action(stu2.DoHomeword));
            Task task3 = new Task(new Action(stu3.DoHomeword));
            task1.Start();
            task2.Start();
            task3.Start();

            for (int i = 0; i < 4; i++)
            {
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("Main thread {0}", i);
                Thread.Sleep(1000);
            }
        }
    }
    
    class Student{省略}
    
}

4.3、应该适时地使用接口(interface)取代一些对委托的使用

  • Java完全地使用接口取代了委托功能,即Java没有与C#中委托相对应的功能实体 

 接口取代委托的实例——3.1.(3)代码重构

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            IProductFactory pizzaFactory = new PizzaFactory();
            IProductFactory toycarFactory = new ToyCarFactory();
            WrapFactory wrapFactory = new WrapFactory();

            Box box1 = wrapFactory.WrapProduct(pizzaFactory);
            Box box2 = wrapFactory.WrapProduct(toycarFactory);

            Console.WriteLine(box1.Product.Name);
            Console.WriteLine(box2.Product.Name);
        }

        interface IProductFactory
        {
            Product Make();
        }
        class PizzaFactory : IProductFactory
        {
            public Product Make()
            {
                Product product = new Product();
                product.Name = "Pizza";
                return product;
            }
        }
        class ToyCarFactory : IProductFactory
        {
            public Product Make()
            {
                Product product = new Product();
                product.Name = "ToyCar";
                return product;
            }
        }

        class Product
        {
            public string Name { get; set; }
        }

        class Box  //
        {
            public Product Product { get; set; }
        }

        class WrapFactory  //打包盒子的工厂
        {
            public Box WrapProduct(IProductFactory productFactory)
            {
                Box box = new Box();
                Product product = productFactory.Make();

                box.Product = product;
                return box;
            }

        }
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值