C#知识大全

1. 结构体

Struct不可以有默认的构造函数和析构函数,会把所有字段初始化为默认值。自定义的构造函数必须对所有字段赋值。Struct可以实现接口,但不能从另一个class、struct继承,而且不能作为其他class的基类。可以不使用new创建。是值类型。

适合使用structs的场景:

实例使用起来像C#的基元类型

需要创建大量的、短暂实例(例如在循环体内)

实例不需要大量的传递

不需要从其他类型继承或不希望被其他类型继承

希望被人操作你实例的副本

不适合使用structs的场景:

实例过大,当实例过大时,传递实例会有很大的开销。微软建议structs的理想大小应该在16bytes以下

当回引起装箱、拆箱操作时

结构体和类的区别:

a. 一个是值类型,一个是引用类型;值类型本身分配在堆栈上,引用类型在堆上。值类型在堆栈释放时自动销毁,而引用类型需要垃圾回收器释放;结构体赋值时会产生副本(任何修改不影响原始结构体),而引用类型只是引用的复制。

b. 适合结构体的场景:大小比较小,生命周期比较短,不需要box和unbox,不需要赋值给其他变量

2. 抽象类和接口

抽象类可以派生自一个抽象类,可以覆盖基类的抽象方法也可以不覆盖,如果不覆盖,则其派生类必须覆盖它们。如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法。

接口除了可以包含方法之外,还可以包含属性、索引器、事件。类是对对象的抽象,而接口只是一个行为的规范或规定。

3. 值类型和引用类

C#的所有值类型均隐式派生自System.ValueType,所有的值类型都是密封(seal)的,所以无法派生出新的值类型。

使用值类型可以减少托管堆上对象的数量,从而减少垃圾回收器(GC)的负担,提高性能,值类型也有明显的缺点:通过值类型传递较大对象的开销比较昂贵、装箱和拆箱对性能造成影响。

class实例化出来的对象,指向了内存堆中分配的空间,引用(内存地址在栈上);struct实例化出来的对象,是在内存栈中分配。
 

1、值类型变量做为局部变量时,该实例将被创建在堆栈上;而如果值类型变量作为类型的成员变量时,它将作为类型实例数据的一部分,同该类型的其他字段都保存在托管堆上。

2、引用类型变量数据保存在托管堆上,但是根据实例的大小有所区别:当实例的大小小于85000Byte时实例将创建在GC堆上;当实例大小>=85000byte时,则该实例创建在LOH(Large Object Heap)堆上。

·        引用类型可以派生出新的类型,而值类型不能;

·        引用类型可以包含null值,值类型不能(可空类型功能允许将null 赋给值类型);

·        引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将拷贝包含的值(新对象)。

4. sealed

当对一个类应用 sealed 修饰符时,此修饰符会阻止其他类从该类继承.。

sealed必须和override一起使用,如果不是虚方法或虚属性会报出错误

防止子类重写特定的方法或属性

5. ref传参

传递了这个引用类型对象的引用的副本(不是对象本身),所以对于在调用方法外部的引用和方法中的引用来说,这两个引用都指向堆上的同一个对象。所以在修改此对象的属性值时,修改同时会应用于内部和外部的两个引用上。但重新分配其引用位置时,则只是修改副本引用的引用位置,原引用(方法外部)的位置不变,原引用还是指向原来的对象。
而如果加上Ref关键字,这时传入的参数则为些引用对象的原始引用,而不是引用的副本,这样的话,你就不但可以修改原始引用对象的内容,还可以再分配此引用的引用位置(用New 来重新初始化)。

使用ref前必须对变量赋值,out不用。退出函数时所有out引用的变量都要赋值

6. 垃圾回收

标记-计划和清理-引用更新和压缩

在应用程序中,只要某对象变得不可达,也就是没有根(root)引用该对象,这个对象就会成为垃圾回收器的目标。

什么时候发生GC

  1、当应用程序分配新的对象,GC的代的预算大小已经达到阈值,比如GC的第0代已满;

  2、代码主动显式调用System.GC.Collect();

  3、其他特殊情况,比如,windows报告内存不足、CLR卸载AppDomain、CLR关闭,甚至某些极端情况下系统参数设置改变也可能导致GC回收。

 .NET的垃圾收集器将对象分为三代(Generation0,Generation1,Generation2)。不同的代里面的内容如下:

  1、G0 小对象(Size<85000Byte):新分配的小于85000字节的对象。

  2、G1:在GC中幸存下来的G0对象

 3、G2:大对象(Size>=85000Byte);在GC中幸存下来的G1对象

GC在一个独立的线程中运行来删除不再被引用的内存。

  Finalizer则由另一个独立(高优先级CLR)线程来执行Finalizer的对象的内存回收。

  对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间,

GC把每一个需要执行Finalizer的对象放到一个队列(从终结列表移至freachable队列)中去,然后启动另一个线程而不是在GC执行的线程来执行所有这些Finalizer,GC线程继续去删除其他待回收的对象。

  在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。也就是说一个实现了Finalize方法的对象必需等两次GC才能被完全释放。这也表明有Finalize的方法(Object默认的不算)的对象会在GC中自动“延长”生存周期。

7. foreach的实现

实现IEnumerable接口,返回一个实现IEnumerator的类。里面实现current,movenext和reset。

调用的后台实现类似于:

var enumerator =((IEnumerable)Persons).GetEnumerator()

while(enumerator.movenet())

{

element= enumerator.current;

}

(enumerator as IDisposable).Dispose();

8. dispose, finalize

如果显式的调用了Dispose方法,我们就在Dispose方法中实现托管资源和非托管资源的释放,使用GC.SuppressFinalize 方法来停止Finalize方法。因为如果用户调用了Dispose方法,那么我们就不必隐式的完成资源的释放,应为Finalizes会大大的减损性能。(Finalize一般只用于用户没有显式的调用Dispose方法,需要我们隐式完成时才使用)
 

publicvoid Dispose()
        {
            //
告诉垃圾搜集器不需要给这个对象调用析构函数了
            //
禁止终结操作(finalization)。你调用GC.SuppressFinalize(this)来完成这种事务
            //当为true时,我们就需要释放托管资源和非托管资源,并且禁止GC的Finalize操作,因为用户可以直接通过显示调用来减小性能开销。
           Dispose(true);
           GC.SuppressFinalize(this);
        }

//写析构函数,这种方法执行时间是不确定的,而且增加系统开销
        ~BaseResource()
        {
            //
析构函数是由GC调用,所以不能访问其它托管对象,所以这里设置为false;
            // 表示本次调用是隐式调用,由Finalize方法调用,即托管资源释放由GC来完成
            //如果为false时,表示我们只需要释放非托管资源,因为本次调用是由GC的Finalize引起的,所以托管资源的释放可以让GC来完成。
           Dispose(false);
        }

protected virtual void Dispose(booldisposing)
        {           
               if (disposing)
               {
                   //
通过调用托管对象的Dispose方法清除托管对象
                   // 用户是显示调用的话,我们就要手工的操作托管资源
                   Components.Dispose();
               }
               //释放非托管对象资源
               //CloseHandle(handle);
               handle = IntPtr.Zero;                      
        }

 

9.泛型

泛型类的不同的封闭类是分别不同的数据类型

T:struct

类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。

T:class

类型参数必须是引用类型,包括任何类、接口、委托或数组类型。

T:new()

类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。

T:<基类名>

类型参数必须是指定的基类或派生自指定的基类。

T:<接口名称>

类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。

T:U

为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束.

在使用子类泛型参数时,必须在子类级别重复在基类级别规定的任何约束

在从泛型基类派生时,既可以提供类型实参,也可以是基类泛型参数

泛型方法既可以包含在泛型类型中,又可以包含在非泛型类型中

编译器允许您将泛型参数显式强制转换到其他任何接口,但不能将其转换到类。通过object类型过渡。

泛型方法的重载可以是泛型也可以是封闭类型。

线程和进程

进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法直接访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。

·        进程优点:编程、调试简单,可靠性较高;

·        进程缺点:创建、销毁、切换速度慢,内存、资源占用大;

·        线程优点:创建、销毁、切换速度快,内存、资源占用小;

·        线程缺点:编程、调试复杂,可靠性较差;

线程有自己唯一的栈。

线程传参(多个):将线程函数和参数包裹在一个类里面。

Task:

创建和启动Tas

1. Task taskA = new Task( () => Console.WriteLine("From taskA.")); // Start

taskA.Start();

2. Task taskA = Task.Run( () => Console.WriteLine("From taskA."));

3. Task taskA = Task.Factory.StartNew(() => Console.WriteLine("From taskA."));

返回值:

Task<int> task1 = Task<int>.Factory.StartNew(() => 1);

int i = task1.Result;

顺序执行:ContinueWith

取消:

private var tokenSource = new CancellationTokenSource();

public void Start()

{

var token = tokenSource.Token;

for (int i = 0; i<5; i++>)

{ Task t = Task.Factory.StartNew( () => DoSomeWork(i, token), token);

Console.WriteLine("Task {0} executing", t.Id); }

}

void DoSomeWork(int taskNum, CancellationToken ct)

{ // 先检查,调度进入时,是否cancel了。

if (ct.IsCancellationRequested == true)

{ Console.WriteLine("Task {0} was cancelled before it got started.", taskNum);

ct.ThrowIfCancellationRequested(); // 抛出异常-- 或者 return }

。。。。。。

}

进程间通信:

管道,命名管道

消息队列MessageQueue

共享内存

信号量(同步)

应用程序域

使用.NET建立的可执行程序 *.exe,并没有直接承载到进程当中,而是承载到应用程序域(AppDomain)当中。应用程序域是.NET引入的一个新概念,它比进程所占用的资源要少,可以被看作是一个轻量级的进程。

在一个进程中可以包含多个应用程序域,一个应用程序域可以装载一个可执行程序(*.exe)或者多个程序集(*.dll)。这样可以使应用程序域之间实现深度隔离,即使进程中的某个应用程序域出现错误,也不会影响其他应用程序域的正常运作。

线程存在于进程当中,它在不同的时刻可以运行于多个不同的AppDomain当中

delegate, event

Event是特殊类型的委托,只可以从声明它们的类中调用。 派生类无法直接调用基类中声明的事件。 尽管有时需要事件仅由基类引发,但在大多数情形下,应该允许派生类调用基类事件。 为此,您可以在包含该事件的基类中创建一个受保护的调用方法。 通过调用或重写此调用方法,派生类便可以间接调用该事件。

事件只能通过“+=”,“-=”方式注册和取消订户处理函数,而委托除此之外还可以使用“=”直接赋值处理函数。

死锁

两个线程,在锁定自己的资源同时,却试图获得对方已锁定的资源,就会发生死锁。避免死锁的最简单方法是使用超时值。死锁发生的最好的方式是避免在同一时间获取多个锁

索引器

T this[int index]

{

get;set;

}

索引器不必根据整数值进行索引,由您决定如何定义特定的查找机制。

索引器可被重载。

索引器可以有多个形参,例如当访问二维数组时。

 

Const和ReadOnly

 

  • const修饰的常量在声明时必须初始化值;readonly修饰的常量可以不初始化值,且可以延迟到构造函数。
  • cons修饰的常量在编译期间会被解析,并将常量的值替换成初始化的值;而readonly延迟到运行的时候。
  • const修饰的常量注重的是效率;readonly修饰的常量注重灵活。
  • const修饰的常量没有内存消耗;readonly因为需要保存常量,所以有内存消耗。
  • const只能修饰基元类型、枚举类、或者字符串类型;readonly却没有这个限制

 

string和stringbuilder

 

string字符串,为引用类型,其具有不可变性一旦其值发生了改变,就是一个新的对象。即每次对字符串进行操作时就会产生一个新的对象。字符串驻留:只有常量拼接才驻留。字符串驻留是进程级的。

 

Assembly

托管PE文件(也就是那些exe,dll)包括4个部分:PE头、CLR头、元数据和IL代码。元数据是由几个表构成的二进制块,有三种元数据表:定义表,引用表和清单表。

反射就是动态获取程序集中的元数据(提供程序集的类型信息)的功能。也就是动态获取程序集中的元数据来操作类型的。

1. Assembly.Load() 

Load()方法接收一个String或AssemblyName类型作为参数,这个参数实际上是需要加载的程序集的强名称(名称,版本,语言,公钥标记)。

2. Assembly.LoadFrom()

LoadFrom()方法可以从指定文件中加载程序集,通过查找程序集的AssemblyRef元数据表,得知所有引用和需要的程序集,然后在内部调用Load()方法进行加载

3. Assembly.LoadFile()

LoadFile()从一个指定文件中加载程序集,它和LoadFrom()的不同之处在于LoadFile()不会加载目标程序集所引用和依赖的其他程序集

 

区别:

LoadFrom 不能用于加载标识相同但路径不同的程序集。

LoadFile 方法用来来加载和检查具有相同标识但位于不同路径中的程序集.但不会加载程序的依赖项。

Assembly加载过程:

有强签名:

  1. 全局程序集缓存
  2. 如果app.config有定义codebase,则以codebase定义为准,如果codebase指定的路径找不到,则直接报告错误
  3. 程序的根目录
  4. 根目录下面,与被引用程序集同名的子目录
  5. 根目录下面被明确定义为私有目录的子目录(在app.config中定义,例如<probing privatePath="libs"/>)

非强签名执行2,3,4,5.

hashtable, hashset, dictionary

hashtable和dictionary,应该总是使用dictionary,因为泛型效率高。

hashset是包含不重复项的无序列表。

 ==,equal, Object.ReferenceEqual

对于==:

2、它会根据需要自动进行必要的类型转换,并根据两个对象的值是否相等返回true或者false。

3、对于引用对象比较其引用(string引用类型除外,string是比较值)

4、对于值类型比较其值

Equals

1、用于比较两个对象的引用是否相等。

2、然而对于值类型,类型相同(不会进行类型自动转换),并且数值相同(对于struct的每个成员都必须相同),则Equals返回 true,否则返回false。

3、对于引用类型,比较是否是同一对象。对于包装过的对于引用类型,调用内部实际类型的Equals。如果内部实际类型为值类型,按照值类型的Equals比较。内部实际类型是引用类型,判断是否为同一对象。

4、对于string,比较的是值

override和overload:

override(重写)
1、方法名、参数、返回值相同。
2、子类方法不能缩小父类方法的访问权限
3、子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
4、存在于父类和子类之间。
5、方法被定义为sealed不能被重写。
overload(重载)
1、参数类型、个数、顺序至少有一个不相同。
2、不能重载只有返回值不同的方法名搜索
3、存在于父类和子类、同类中。

 

extension method:

public static class ExtendClass 
7.    { 
8.        public static void ExtendMethod(this Test test) 
9.        { 
10.            Console.WriteLine("test.ExtendMethod()"); 
11.        } 
12.    }

 

c#6.0新特性:

var a= 1;

var b = 2;

string  t2 = $"{a}_{b}";

 

public DateTime DateCreated { get; private set; } = DateTime.Now;

 

title = post?.Title?.Name;

 

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ErrorTimes")); 

 

 

Convert.ToInt32("FF",16) //16-->10

Convert.ToString(3,2) //10 --> 2

 

反射

这是.Net中获取运行时类型信息的方式,.Net的应用程序由几个部分:‘程序集(Assembly)’、‘模块(Module)’、‘类型(class)’组成,而反射提供一种编程的方式,让程序员可以在程序运行期获得这几个组成部分的相关信息

优点:

  • 1、反射提高了程序的灵活性和扩展性。
  • 2、降低耦合性,提高自适应能力

缺点:

性能问题

跟直接调用相比,逻辑上要复杂一些。

var assem = Assembly.Load("ConsoleApplication2");
            var b = assem.GetTypes()[0];
            var c = Activator.CreateInstance(b);
            MethodInfo mi = b.GetMethod("Test");
            mi.Invoke(c, null);

和配置文件配合使用,达到动态改变和扩展类型类型的目的。工厂模式。
  1. var t = Type.GetType(typeName);  
  2. var m = t.GetMethod(methodName);

序列化:

二进制格式和SOAP格式可序列化一个类型的所有可序列化字段,不管它是公共字段还是私有字段。XML格式仅能序列化公共字段或拥有公共属性的私有字段,未通过属性公开的私有字段将被忽略。XML格式序列化不能序列化字典类型。

使用二进制格式序列化时,它不仅是将对象的字段数据进行持久化,也持久化每个类型的完全限定名称和定义程序集的完整名称(包括包称、版本、公钥标记、区域性),这些数据使得在进行二进制格式反序列化时亦会进行类型检查。SOAP格式序列化通过使用XML命名空间来持久化原始程序集信息。而XML格式序列化不会保存完整的类型名称或程序集信息。这便利XML数据表现形式更有终端开放性。如果希望尽可能延伸持久化对象图的使用范围时,SOAP格式和XML格式是理想选择。

 

使用二进制序列化,必须为每一个要序列化的的类和其关联的类加上[Serializable]特性,对类中不需要序列化的成员可以使用[NonSerialized]特性。

二进制序列化对象时,能序列化类的所有成员(包括私有的),且不需要类有无参数的构造方法。

使用二进制格式序列化时,反序列化时的运行环境要与序列化时的运行环境要相同,否者可能会无法反序列化成功。

SOAP序列化与二进制序列化的区别是:SOAP序列化不能序列化泛型类型。与二进制序列化一样在序列化时不需要向序列化器指定序列化对象的类型。而XML序列化需要向XML序列化器指定序列化对象的类型。

 

使用XML序列化或反序列化时,需要对XML序列化器指定需要序列化对象的类型和其关联的类型。

XML序列化只能序列化对象的公有属性,并且要求对象有一个无参的构造方法,否者无法反序列化。

[Serializable]和[NonSerialized]特性对XML序列化无效!所以使用XML序列化时不需要对对象增加[Serializable]特性。

[XmlElement]/[XmlAttribute]/[XmlIgnore]

SOAP:

简单对象访问协议是交换数据的一种协议规范,是一种轻量的、简单的、基于XML的协议,它被设计成在WEB上交换结构化的和固化的信息。具有简单的,与厂商,编程语言,平台无关的特性。

Marshal:

参数类型要匹配,对于string[]可能要转成char[][]处理。

要事先计算参数内存空间大小,并分配相应内存。

如果数组,每一项的内存空间也要分配。指针类成员,也要分配空间。

最后分配的内存要释放

OOP:

关于封装,相信大家都读过这句话,属性可用来描述同一类事物的特征, 行为可描述一类事物可做的操作,封装就是要把属于同一类事物的共性(包括属性与行为)归到一个类中(当然不是说封装只是局限于类)。将对象信息状态通过访问权限修饰符隐藏在对象内部,不允许外部程序直接访问对象内部信息。

 

子类继承了父类所有的成员方法和属性,并且可以拥有自己特性(属性或方法)继承解决了代码的重用问题。

 

同一种方法用于不同的对象(当然可能是相同的引用类型)会出现不同的体现

1)有继承;2)有方法的重写;3)父类引用指向子类对象(向上造型);

 

静态构造函数没有修饰符修饰(public,private)静态构造函数没有参数静态函数的调用时机,是在类被实例化或者静态成员被调用的时候进行调用,并且是由.net框架来调用静态构造函数来初始化静态成员变量。静态构造函数只会被执行一次。

 

子类的实例构造函数如果不指定父类的构造函数,默认调用父类无参构造函数(如果有的话)。

C#的默认访问权限

1.在namespace中的类、接口默认是internal类型的。
2.在一个类里面,属性和方法默认是private的,可以显示的定义为public、private、protected、internal或protected internal等访问类型。
3.接口中不能定义成员变量,接口中的方法默认为public的访问权限,但是不能显示的定义任何访问类型。
4.抽象类中必须有一个以上的抽象方法,抽象方法可以是public、internal、protected,不能是private的访问类型。

派生类的可访问性不能高于其基类型,成员的可访问性决不能高于其包含类型的可访问性。 

设计原则

1.单一职责原则

2 开闭原则(Open-Closed Principle)--对扩展开放,对修改关闭

3 里氏代替原则(Liskov Substitution Principle)--子类替换父类

4 依赖倒置原则

5 接口隔离原则--多个专门的接口比使用单一的总接口要好

6 合成复用原则

7 迪米特法则--个对象应当对其他对象有尽可能少的了解

 

单例模式:

public sealed class Singleton
{
    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return Nested.instance;
        }
    }

    private class Nested
    {
        internal static readonly Singleton instance = null;
        static Nested()
        {
            instance = new Singleton();
        }
    }
}

 

Windows Communication Foundation(WCF)是由微软开发的一系列支持数据通信的应用程序框架,可以翻译为Windows 通讯开发平台。

整合了原有的windows通讯的 .net Remoting,WebService,Socket的机制,并融合有HTTPFTP的相关技术。

  • Address: 每一个WCF的Service都有一个唯一的地址。这个地址给出了Service的地址和传输协议(Transport Protocol)
  • Binding:通信(Communication)的方式很多,同步的request/reply模式,非同步的fire-and-forget模式。消息可以单向或者双向的发送接收,可以立即发送或者把它放入到某一个队列中再处理。所供选择的传输协议也有Http, Tcp,P2P,IPC等。当要考虑Client/Server如何进行通讯的时候,除了考虑以上提到的几点之外,还有其它很多需要考虑的因素,如安全,性能等。因此,简单来说,Binding只不过是微软提供的一组考虑比较周全、常用的封装好的通信方式。
  • Contract:Contract描述了Service能提供的各种服务。Contract有四种,包括Service Contract, Data Contract, Fault Contract和Message Contract

装饰器模式:





  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为什么我选择学习计算机科学? 我选择学习计算机科学是因为计算机科学是一个与时俱进、充满无限可能的领域。现在的社会已经进入了数字化时代,计算机科学在各个行业都扮演着至关重要的角色。无论是商业、医疗、交通还是通讯,计算机技术都在推动着世界的发展。 首先,计算机科学的前景非常广阔和稳定。随着技术的不断进步,计算机科学领域还有很多未被开发的领域,这意味着有更多的机会和挑战。计算机科学毕业生可以选择从事软件开发、数据分析、人工智能等职业,这些职业都非常有前景。 其次,计算机科学是一门创造性的学科。通过编程和算法设计,我们可以创造出各种各样的应用程序和解决方案。这不仅会带来成就感,还可以帮助解决实际问题。通过学习计算机科学,我可以培养自己的创造力和解决问题的能力,这对于我的个人发展非常重要。 最后,我选择学习计算机科学也是因为对技术的热爱。计算机科学是一个充满挑战和乐趣的学科。我喜欢解决问题时的思考过程和找到问题解决方案的成就感。随着技术的不断发展,计算机科学学习永远不会停止,这也让我保持了对知识探索的激情。 总之,我选择学习计算机科学是因为这是一个前景广阔、创造性高和充满乐趣的学科。我相信通过学习计算机科学,我可以在未来的职业道路上取得成功,并为社会的发展做出贡献。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值