从 C++ 到Objective-C----4

从 C++ 到Objective-C(18):字符串和 C++ 特性

字符串

Objective-C 中唯一的 static 对象

在 C 语言中,字符串就是字符数组,使用char* 指针。处理这种数据非常困难,并且可能引起很多 bug。C++ 的 string 类是一种解脱。在 Objective-C 中,前面我们曾经介绍过,所有对象都不是自动的,都要在运行时分配内存。唯一不符合的就是 static 字符串。这导致可以使用 static 的 C 字符串作为 NSString 的参数。不过这并不是一个好的主意,可能会引起内存浪费。幸运的是,我们也有 static 的 Objective-C 字符串。在使用引号标记的 C 字符串前面加上 @ 符号,就构成了 static 的 Objective-C 字符串。

NSString* notHandy = [[NSString alloc] initWithUTF8String:"helloWorld"];
NSString* stillNotHandy = // initWithFormat 类似 sprintf()
                          [[NSString alloc] initWithFormat:@"%s", "helloWorld"];
NSString* handy = @"hello world";

另外,static 字符串可以同普通对象一样作为参数使用。

int size = [@"hello" length];
NSString* uppercaseHello = [@"hello" uppercaseString];

NSString 和编码

NSString 对象非常有用,因为它增加了很多好用的方法,并且支持不同的编码,如 ASCII、UNICODE、ISO Latin 1等。因此,翻译和本地化应用程序也变得很简单。

对象描述,%@扩展,NSString 转 C 字符串

在 Java 中,每一个对象都继承自 Object,因此都有一个 toString 方法,用于使用字符串形式描述对象本身。这种功能对于调试非常有用。Objective-C 中,类似的方法叫做 description,返回一个 NSString 对象。

C 语言的 printf 函数不能输出 NSString。我们可以使用 NSLog 获得类似的功能。NSLog 类似于 printf,可以向控制台输出格式化字符串。需要注意的是,NSString 的格式化符号是 %@,不是 %s。事实上,%@ 可以用于任意对象,因为它实际是调用的 -(NSString*) description。

NSString 可以使用 UTF8String 方法转换成 C 风格字符串。

char* name = "Spot";
NSString* action1 = @"running";
printf("My name is %s, I like %s, and %s...\n",
    name, [action1 UTF8String], [@"running again" UTF8String]);
NSLog(@"My name is %s, I like %@ and %@\n",
    name, action1, @"running again");

C++ 特性

现在,你已经了解到 C++ 的面向对象概念在 Objective-C 中的描述。但是,另外一些 C++ 的概念并没有涉及。这些概念并不相关面向对象,而是关于一些代码编写的问题。

引用

Objective-C 中不存在引用(&)的概念。由于Objective-C 使用引用计数器和autorelease 管理内存,这使得引用没有多大用处。既然对象都是动态分配的,它们唯一的引用就是指针。

内联

Objective-C 不支持内联 inline。对于方法而言,这是合理的,因为Objective-C 的动态性使得“冻结”某些代码变得很困难。但是,内联对某些用 C 编写的函数,比如 max(), min() 还是比较有用的。这一问题在 Objective-C++ (这是另外一种类似的语言)中得到解决。

无论如何,GCC 编译器还是提供了一个非标准关键字 __inline 或者 __inline__,允许在 C 或者 Objective-C 中使用内联。另外,GCC 也可以编译 C99 代码,在 C99 中,同样提供了内联关键字 inline(这下就是标准的了)。因此,在基于 C99的 Objective-C 代码中是可以使用内联的。如果不是为了使用而使用内联,而是关心性能,那么你应该考虑 IMP 缓存。

模板

模板是独立于继承和虚函数的另外一种机制,主要为性能设计,已经超出了纯粹的面向对象模型(你注意到使用模板可以很巧妙的访问到 private 变量吗?)。Objective-C 不支持模板,因为其独特的方法名规则和选择器使得模板很难实现。

运算符重载

Objective-C 不支持运算符重载。

友元

Objective-C 没有友元的概念。事实上,在 C++ 中,友元很大程度上是为了实现运算符重载。Java 中包的概念在一定程度上类似友元,这可以使用分类来处理。

const 方法

Objective-C 中方法不能用 const 修饰。因此也就不存在 mutable 关键字。

初始化列表

Objective-C 中没有初始化列表的概念。

从 C++ 到Objective-C(19):STL 和 Cocoa

C++ 标准库是其强大的一个原因。即使它还有一些不足,但是已经能够算作是比较完备的了。这并不是语言的一部分,而是属于一种扩展,其他语言也有类似的部分。在 Objective-C 中,你不得不在 Cocoa 里面寻找容器、遍历器或者其他一些真正可以使用的算法。

容器

Cocoa 的容器比 C++ 更加面向对象,它不使用模板实现,只能存放对象。现在可用的容器有:

·        NSArray 和 NSMutableArray:有序集合;

·        NSSet 和 NSMutableSet:无序集合;

·        NSDictionary和 NSMutableDictionary:键值对形式的关联集合;

·        NSHashTable:使用弱引用的散列表(Objective-C 2.0 新增)。

你可能会发现这其中并没有 NSList 或者 NSQueue。事实上,这些容器都可以由 NSArray 实现。

不同于 C++ 的 vector,Objective-C 的 NSArray 真正隐藏了它的内部实现,仅能够使用访问器获取其内容。因此,NSArray 没有义务为内存单元优化其内容。NSArray的实现有一些妥协,以便 NSArray 能够像数组或者列表一样使用。既然 Objective-C 的容器只能存放指针,单元维护就会比较有效率了。

NSHashTable 等价于 NSSet,但它使用的是弱引用(我们曾在前面的章节中讲到过)。这对于垃圾收集器很有帮助。

遍历器

经典的枚举

纯面向对象的实现让 Objective-C 比 C++ 更容易实现遍历器。NSEnumerator就是为了这个设计的:

NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = [enumerator nextObject];
while (anObject != nil)
{
    [anObject doSomethingWithString:aString];
    anObject = [enumerator nextObject];
}

容器的 objectEnumerator 方法返回一个遍历器。遍历器可以使用 nextObject 移动自己。这种行为更像 Java 而不是 C++。当遍历器到达容器末尾时,nextObject 返回 nil。下面是最普通的使用遍历器的语法,使用的 C 语言风格的简写:

NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = nil;
while ((anObject = [enumerator nextObject])) {
    [anObject doSomethingWithString:aString];
}
// 双括号能够防止 gcc 发出警告

快速枚举

Objective-C 2.0 提供了一个使用遍历器的新语法,隐式使用 NSEnumerator(其实和一般的 NSEnumerator 没有什么区别)。它的具体形式是:

NSArray* someContainer = ...;
for(id object in someContainer) { // 每一个对象都是用 id 类型
    ...
}
for(NSString* object in someContainer) { // 每一个对象都是 NSString
    ...// 开发人员需要处理不是 NSString* 的情况
}

函数对象

使用选择器

Objective-C 的选择器很强大,因而大大减少了函数对象的使用。事实上,弱类型允许用户无需关心实际类型就可以发送消息。例如,下面的代码同前面使用遍历器的是等价的:

NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSString* aString = @"foo";
[array makeObjectsPerformSelector:@selector(doSomethingWithString:)
                       withObject:aString];

在这段代码中,每个对象不一定非得是 NSString 类型,并且对象也不需要必须实现了 doSomethingWithString: 方法(这会引发一个异常:selector not recognized)。

IMP 缓存

我们在这里不会详细解释这个问题,但是的确可以获得 C 函数的内存地址。通过仅查找一次函数地址,可以优化同一个选择器的多次调用。这被称为 IMP 缓存,因为 Objective-C 用于方法实现的数据类型就是 IMP。

调用 class_getMethodImplementation() 就可以获得这么一个指针。但是请注意,这是指向实现方法的真实的指针,因此不能有虚调用。它的使用一般在需要很好的时间优化的场合,并且必须非常小心。

算法

STL 中那一大堆通用算法在 Objective-C 中都没有对等的实现。相反,你应该仔细查找下各个容器中有没有你需要的算法。

从 C++ 到Objective-C(20):隐式代码

本章中心是两个能够让代码更简洁的特性。它们的目的截然不同:键值对编码可以通过选择第一个符合条件的实现而解决间接方法调用;属性则可以让编译器帮我们生成部分代码。键值对编码实际上是 Cocoa 引入的,而属性则是 Objective-C 2.0 语言新增加的。键值对编码(KVC)

原则

键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在 Cocoa 中就是 NSDictionary),数据成员的名字就是这里的键。NSObject 有一个 valueForKey: 和 setValue:forKey: 方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号 . 分割,对应的方法是 valueForKeyPath: 和 setValue:forKeyPath:。

@interface A {

    NSString* foo;

}

... // 其它代码

@end

 

@interface B {

    NSString* bar;

    A* myA;

}

... // 其它代码

@end

 

@implementation B

...

// 假设 A 类型的对象 a,B 类型的对象 b

A* a = ...;

B* b = ...;

NSString* s1 = [a valueForKey:@"foo"]; // 正确

NSString* s2 = [b valueForKey:@"bar"]; // 正确

NSString* s3 = [b valueForKey:@"myA"]; // 正确

NSString* s4 = [b valueForKeyPath:@"myA.foo"]; // 正确

NSString* s5 = [b valueForKey:@"myA.foo"]; // 错误

NSString* s6 = [b valueForKeyPath:@"bar"]; // 正确

...

@end

这种语法能够让我们对不同的类使用相同的代码来处理同名数据。注意,这里的数据成员的名字都是使用的字符串的形式。这种使用方法的最好的用处在于将数据(名字)绑定到一些触发器(尤其是方法调用)上,例如键值对观察(Key-Value Observing, KVO)等。

拦截

通过 valueForKey: 或者 setValue:forKey: 访问数据不是原子操作。这个操作本质上还是一个方法调用。事实上,这种访问当某些方式实现的情况下才是可用的,例如使用属性自动添加的代码等等,或者显式允许直接访问数据。

Apple 的文档对 valueForKey: 和 setValue:forKey: 的使用有清晰的文档:

对于 valueForKey:@”foo” 的调用:

·        如果有方法名为 getFoo,则调用 getFoo;

·        否则,如果有方法名为 foo,则调用 foo(这是对常见的情况);

·        否则,如果有方法名为 isFoo,则调用 isFoo(主要是布尔值的时候);

·        否则,如果类的 accessInstanceVariablesDirectly 方法返回 YES,则尝试访问 _foo 数据成员(如果有的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;

·        如果前一个步骤成功,则返回对应的值;

·        如果失败,则调用 valueForUndefinedKey:,这个方法的默认实现是抛出一个异常。

对于 forKey:@”foo” 的调用:

·        如果有方法名为 setFoo:,则调用 setFoo:;

·        否则,如果类的 accessInstanceVariablesDirectly 返回 YES,则尝试直接写入数据成员 _foo(如果存在的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;

·        如果失败,则调用 setValue:forUndefinedKey:,其默认实现是抛出一个异常。

注意 valueForKey: 和 setValue:forKey: 的调用可以用于触发任何相关方法。如果没有这个名字的数据成员,则就是一个虚假的调用。例如, 在字符串变量上调用 valueForKey:@”length” 等价于直接调用 length 方法,因为这是 KVC 能够找到的第一个匹配。但是,KVC 的性能不如直接调用方法,所以应当尽量避免。

原型

使用 KVC 有一定的方法原型的要求:getters 不能有参数,并且要返回一个对象;setters 需要有一个对象作为参数,不能有返回值。参数的类型不是很重要的,因为你可以使用 id 作为参数类型。注意,struct 和原生类型(int,float 等)都是支持的:Objective-C 有一个自动装箱机制,可以将这些原生类型封装成 NSNumber 或者 NSValue 对象。因此,valueForKey: 返回值都是一个对象。如果需要向 setValue:forKey: 传入 nil,需要使用 setNilValueForKey:。

高级特性

有几点细节需要注意,尽管在这里并不会很详细地讨论这个问题:

1.    keypath 可以包含计算值,例如求和、求平均、最大值、最小值等;使用 @ 标记;

2.    注意方法一致性,例如 valueForKey: 或者 setValue:forKey: 以及关联数组集合中常见的 objectForKey: 和 setObject:forKey:。这里,同样使用 @ 进行区分。

从 C++到 Objective-C(21):隐式代码(续)

属性

使用属性

在定义类时有一个属性的概念。我们使用关键字 @property 来标记一个属性,告诉编译器自动生成访问代码。属性的主要意义在于节省开发代码量。

访问属性的语法比方法调用简单,因此即使我们需要编写代码时,我们也可以使用属性。访问属性同方法调用的性能是一样的,因为属性的使用在编译期实际就是换成了方法调用。大多数时候,属性用于封装成员变量。但是,我们也可以提供一个“假”的属性,看似是访问一个数据成员,但实际不是;换句话说,看起来像是从对象外部调用一个属性,但实际上其实现要比一个值的管理操作要复杂得多。

属性的描述

对属性的描述实际上是要告诉编译器如何生成访问器的代码:

·        属性从外界是只读的吗?

·        如果数据成员是原生类型,可选余地不大;如果是对象,那么使用 copy 封装的话,是要用强引用还是弱引用?

·        属性是线程安全的吗?

·        访问器的名字是什么?

·        属性应该关联到哪一个数据成员?

·        应该自动生成哪一个访问器,哪一个则留给开发人员?

我们需要两个步骤来回答这些问题:

·        在类的@interface 块中,属性的声明需要提供附属参数;

·        在类的@implementation 块中,访问器可以隐式生成,也可以指定一个实现。

属性访问器是有严格规定的:getter 要求必须返回所期望的类型(或者是相容类型);setter 必须返回 void,并且只能有一个期望类型的参数。访问器的名字也是规定好的:对于数据 foo,getter 的名字是 foo,setter 的名字是 setFoo:。当然,我们也可以指定自定义的名字,但是不同于前面所说的键值对编码,这个名字必须在编译期确定,因为属性的使用被设计成要和方法的直接调用一样的性能。因此,如果类型是不相容的,是不会有装箱机制的。

以下是带有注释的例子,先来有一个大体的了解。

@interface class Car : NSObject
{
    NSString* registration;
    Person* driver;
}
 
// registration 是只读的,使用 copy 设置
@property NSString*  (readonly, copy) registration;
 
// driver 使用弱引用(没有 retain),可以被修改
@property Person* (assign) driver;
 
@end
...
@implementation
 
// 开发者没有提供,由编译期生成 registration 的代码
@synthesize registration;
 
// 开发者提供了 driver 的 getter/setter 实现
@dynamic driver;
 
// 该方法将作为 @dynamic driver 的 getter
-(Person*) driver  {
    ...
}
 
// 该方法将作为 @dynamic driver 的 setter
-(void) setDriver:(Person*)value {
    ...
}
@end

属性的参数

属性的声明使用一下模板:

@property type name;

或者

@property(attributes) type name;

如果没有给出属性的参数,那么将使用默认值;否则将使用给出的参数值。这些参数值可以是:

·        readwrite(默认)或者 readonly:设置属性是可读写的(拥有 getter/setter)或是只读的(只有 getter);

·        assign(默认),retain 或 copy:设置属性的存储方式;

·        nonatomic:不生成线程安全的代码,默认是生成的(没有 atomic 关键字);

·        getter=…,setter=…:改变访问器默认的名字。

对于 setter,默认行为是 assign;retain 或者 copy 用于数据成员被修改时的操作。在一个 -(void) setFoo:(Foo*)value 方法中,会因此生成三种不同的语句:

·        self->foo= value ;  // 简单赋值

·        self->foo= [value retain];  // 赋值,同时引用计数器加 1

·        self->foo= [value copy];  // 对象拷贝(必须满足协议 NSCopying)

在有垃圾收集器的环境下,retain 同 assign 没有区别,但是可以加上 __weak 或者 __strong。

@property(copy,getter=getS,setter=setF:) __weak NSString* s; // 复杂声明

注意不要忘记 setter 的冒号 : 。

 

从 C++到 Objective-C(22):隐式代码(续二)

属性的自定义实现

上一章中我们提到的代码中有两个关键字 @synthesize 和 @dynamic。@dynamic 意思是由开发人员提供相应的代码:对于只读属性需要提供 setter,对于读写属性需要提供 setter 和 getter。@synthesize 意思是,除非开发人员已经做了,否则由编译器生成相应的代码,以满足属性声明。对于上次的例子,如果开发人员提供了 -(NSString*)registration,编译器就会选择这个实现,不会用新的覆盖。因此,我们可以让编译器帮我们生成代码,以简化我们自己的代码输入量。最后,如果编译期没有找到访问器,而且没有使用 @synthesize 声明,那么它就会在运行时添加进来。这同样可以实现属性的访问,但是即使这样,访问器的名字也需要在编译期决定。如果运行期没有找到访问器,就会触发一个异常,但程序不会停止,正如同方法的缺失。当我们使用 @synthesize 时,编译器会被要求绑定某一特定的数据成员,并不一定是一样的名字。

@interface A : NSObject {

    int _foo;

}

@property int foo;

@end

 

@implementation A

@synthesize foo=_foo; // 绑定 "_foo" 而不是 "foo"

@end

访问属性的语法

为获取或设置属性,我们使用点号:这同简单的 C 结构是一致的,也是在 keypath 中使用的语法,其性能与普通方法调用没有区别。

@interface A : NSObject {

    int i;

}

@property int i;

@end

 

@interface B : NSObject {

    A* myA;

}

@property(retain) A* a;

@end

...

A* a = ...

B* b = ...;

a.i = 1; // 等价于 [a setI:1];

b.myA.i = 1;// 等价于 [[b myA] setI:1];

请注意上面例子中 A 类的使用。self->i 和 self.i 是有很大区别的:self->i 直接访问数据成员,而 self.i 则是使用属性机制,是一个方法调用。

高级细节

64 位编译器上,Objective-C 运行时环境与 32 位有一些不同。关联到 @property 的实例数据可能被忽略掉,例如被视为隐式的。更多细节请阅读 Apple 的文档。

从 C++到 Objective-C(23):动态

RTTI (Run-Time Type Information)

RTTI 即运行时类型信息,能够在运行的时候知道需要的类型信息。C++ 有时被认为是一个“假的”面向对象语言。相比 Objective-C,C++ 显得非常静态。这有利于在运行时获得最好的性能。C++ 使用 typeinfo 库提供运行时信息,但这不是安全的,因为这个库依赖于编译器的实现。一般来说,查找对象的类型是一个很少见的请求,因为语言是强类型的,一般在编译时就已经确定其类型了;但是,有时候这种能力对于容器很常用。我们可以使用 dynamic_cast 和 typeid 运算符,但是程序交互则会在一定程度上受限。那么,如何由名字获知这个对象的类型呢?Objective-C 语言可以很容易地实现这种操作。类也是对象,它们继承它们的行为。

class,superclass, isMemberOfClass, isKindOfClass

对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。

isMemberOfClass: 可以用于回答这种问题:“我是给定类(不包括子类)的实例吗?”,而 isKindOfClass: 则是“我是给定类或其子类的实例吗?”使用这种方法需要一个“假”关键字的 class(注意,不是 @class,@class 是用于前向声明的)。事实上,class是 NSObject 的一个方法,返回一个 Class 对象。这个对象是元类的一个实例。请注意,nil 值的类是 Nil。

BOOL test = [self isKindOfClass:[Foo class]];
if (test)
    printf("I am an instance of the Foo class\n");

注意,你可以使用 superclass 方法获取其父类。

conformsToProtocol

该方法用于确定一个对象是否和某一协议兼容。我们前面曾经介绍过这个方法。它并不是动态的。编译器仅仅检查每一个显式声明,而不会检查每一个方法。如果一个对象实现了给定协议的所有方法,但并没有显式声明说它实现了该协议,程序运行是正常的,但是 conformsToProtocol: 会返回 NO。

respondsToSelector,instancesRespondToSelector

respondsToSelector: 是一个实例方法,继承自 NSObject。该方法用于检查一个对象是否实现了给定的方法。这里如要使用 @selector。例如:

if ( [self respondsToSelector:@selector(work)] )
{
    printf("I am not lazy.\n");
    [self work];
}

如果要检查一个对象是否实现了给定的方法,而不检查继承的方法,可以使用类方法 instancesRespondToSelector:。例如:

if ([[self class] instancesRespondToSelector:@selector(findWork)])
{
    printf("I can find a job without the help of my mother\n");
}

注意,respondsToSelector: 不能用于仅仅使用了前向声明的类。

强类型和弱类型 id

C++ 使用的是强类型:对象必须符合其类型,否则不能通过编译。在 Objective-C 中,这个限制就灵活得多了。如果一个对象与消息的目标对象不相容,编译器仅仅发出一个警告,而程序则继续运行。这个消息会被丢弃(引发一个异常),除非前面已经转发。如果这就是开发人员期望的,这个警告就是冗余的;在这种情形下,使用弱类型的 id 来替代其真实类型就可以消除警告。事实上,任何对象都是 id 类型的,并且可以处理任何消息。这种弱类型在使用代理的时候是必要的:代理对象不需要知道自己被使用了。例如:

-(void) setAssistant:(id)anObject
{
    [assistant autorelease];
    assistant = [anObject retain];
}
-(void) manageDocument:(Document*)document
{
    if ([assistant respondToSelector:@(manageDocument:)])
        [assistant manageDocument:document];
    else
        printf("Did you fill the blue form ?\n");
}

在 Cocoa 中,这种代理被大量用于图形用户界面的设计中。它可以很方便地把控制权由用户对象移交给工作对象。

运行时操作 Objective-C 类

通过添加头文件 ,我们可以调用很多工具函数,用于在运行时获取类信息、添加方法或实例变量。Objective-C 2.0 又引入了一些新函数,比 Objective-C 1.0 更加灵活(例如使用 class_addMethod(…) 替代 class_addMethods(…)),同时废弃了许多 1.0 的函数。这让我们可以很方便的在运行时修改类。

从 C++ 到Objective-C(24):结语

《从 C++ 到 Objective-C》系列已经结束。再次重申一下,本系列不是一个完整的 Objective-C 的教学文档,只是方便熟悉 C++ 或者类 C++ 的开发人员(例如广大的 Java 程序员)能够很快的使用 Objective-C 进行简单的开发。当然,目前 Objective-C 的最广泛应用在于 Apple 系列的开发,MacOS X、iOS 等。本系列仅仅介绍的是 Objective-C 语言本身,对于 Apple 系列的开发则没有很多的涉及。正如你仅仅知道 C++ 的语法,不了解各种各样的库是做不出什么东西的,学习 Objective-C 也不得不去了解 MacOS 或者 iOS 等更多的库的使用。这一点已经不在本系列的范畴内,这一点还请大家见谅。下面是本系列的目录:

1.    前言

2.    语法概述

3.    类和对象

4.    类和对象(续)

5.    类和对象(续二)

6.    类和对象(续三)

7.    继承

8.    继承(续)

9.    实例化

10.  实例化(续)

11.  实例化(续二)

12.  实例化(续三)

13.  内存管理

14.  内存管理(续)

15.  内存管理(续二)

16.  内存管理(续三)

17.  异常处理和多线程

18.  字符串和C++ 特性

19.  STL 和 Cocoa

20.  隐式代码

21.  隐式代码(续)

22.  隐式代码(续二)

23.  动态

24.  结语

本文来自 DevBean's World:http://www.devbean.info

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值