Objective-C & Sprite Kit太空历险记 : 6. 打造战队——数组、集合与字典

原文

  http://www.ituring.com.cn/article/213927

   

O博士:前面,我们已经设计出了各种型号的机器人,而本章,我们将讨论如何更好的组织和管理机器人队伍,包括与数组、集合、字典等各种类型团队类型,如:

  • C风格数组
  • 不可变数组(NSArray类型)
  • 可变数组(NSMutableArray类型)
  • 集合(Set)
  • 字典(Dictionary)

6.1. C风格数组

O博士:C风格数组用于定义一系列类型相同的数据。在数组中,我们可以通过数值索引访问成员,并可以通过循环快速地对数组成员进行相同或相似的操作,如进行赋值操作等。

在Objective-C中,我们也可以使用C风格的数组,其声明格式如下:

<成员类型> <变量名>[<成员数量>];

大家可以休息时间可以放松一下,玩玩纸牌;如下面的代码,创建了一个成员类型为int,包含54个成员的数组,并通过一个for循环语句结构分别给成员赋值。

int cards[54];
for (int i=0; i<54; i++) {
    cards[i] = i+1;
}

代码中,我们分别将54张纸牌的数值(1-54)赋给了一个名为cards的int类型数组;然后,我们可以通过以下代码进行洗牌(随机排序)操作:

int temp;
for (int i=0; i<54; i++) {
    for (int j=0; j<54; j++) {
        if (arc4random() % 2 == 1) {
            temp = cards[i];
            cards[i] = cards[j];
            cards[j] = temp;
        }
    }
}
// 打印随机排序后的数组
for (int i=0; i<54; i++) {
    NSLog(@"%i", cards[i]);
}

如果数组的成员比较简单,我们还可以在定义数组变量时直接赋值,并不需要指定成员数量,代码会根据赋值内容自动确定数组成员的数量。如下面的代码。

char vowels[] = {'a', 'e', 'i', 'o', 'u'};

前面,我们应用的数组类型称为一维数组,在实际工作中,我们还可能需要使用二维及更多维的数组;比如,在表示一个二维矩阵的数组结构时,我们就需要使用二维数组,如下面的代码。

int matrix[5][5];
for (int row=0; row<5; row++) {
    for (int col=0; col<5; col++) {
        matrix[row][col] = row * 5 + col;
    }
}
//
for (int row=0; row<5; row++) {
NSLog(@"%i,%i,%i,%i,%i", matrix[row][0],matrix[row][1],
    matrix[row][2],matrix[row][3],matrix[row][4]);
}

本代码输出结果类似下图。

O博士:对于更多维的数组,比如表示多维空间数据的时候,我们只需要更多的[]就可以了。在循环访问多维数组成员过程中,请注意索引变量的命名问题,一个简单的原则就是要直观,这样,在操作过程中,我们才会更加清晰、更有意义地使用数组成员,如上面代码中的row和col控制变量的使用。

6.2. 不可变数组(NSArray类型)

O博士:C风格的变量是一种基本的数组形式,在应用过程中,其执行效率比较高,但灵活可能就会差一些;而在Foundation框架中,为我们提供了一些面向对象的用于操作数组的类,如NSArray类,下面,我们就首先来讨论这个基本的数组类,稍后,我们会使用更强大的数组打造机器人战队。

6.2.1. 创建NSArray对象

O博士:首先,我们需要了解,NSArray对象的一个特点,它创建的是一个不可变数组对象,也就是说,当它的成员确定下来以后就不能修改了。根据这一特点,就需要在初始化NSArray对象的同时创建数组成员,此时,我们可以使用arrayWithObjects:方法。这是一个定义在NSArray中的类方法,其参数就是数组的成员,但需要注意的是,数据成员都应该是对象,而且必须以一个nil值(空引用)作为结束,但nil值并不是数组的成员。如下面的代码,我们将创建一个字符串对象数组。

NSArray *arr = 
    [NSArray arrayWithObjects:@"abc",@"def",@"ghi",nil];

此外,我们还可以使用@[]语法结构简化NSArray对象的创建,如下面的代码:

NSArray *arr = @[@"abc",@"def",@"ghi"];

使用@[]语法结构时,数组成员序列并不需要nil值作为结束。

6.2.2. 数字对象(NSNumber类)

O博士:再次说明,NSArray对象的成员只能是对象,也就是说,NSArray数组成员不能直接使用值类型的数据,如int、float、枚举、结构等类型,如果使用NSArray对象处理这些类型的数据,就必须对这些数据进行转换;转换工作是双向的,包括从值类型转换为对象类型,以及从对象类型还原为值类型。下面,我们就来讨论如何使用NSNumber和NSValue类来完成这些工作。

NSNumber类的功能主要有两种,即将数值类型转换为数字对象,以及将数字对象转换为数值类型。

比如,我们将一个int类型数字转换为NSNumber对象,就可以使用如下代码:

NSNumber *objNum = [NSNumber numberWithInt:99];

或者使用相应的初始化方法,如:

NSNumber *objNum = [[NSNumber alloc] initWithInt:99];

反过来,我们可以使用相应的属性将NSNumber对象还原为一个int类型的数值,如下面的代码。

int intNum = objNum.intValue;

O博士:请注意,当一个数值转换为对象以后,我们就不能对它们使用传统的运算符的。对于NSNumber对象,我们可以做一些简单的操作,如数值的比较,此时,可以使用compare方法,此方法返回结果可能是以下三个值(NSComparisonResult枚举类型):

  • NSOrderedAscending,调用对象数值小于参数中的对象数值。
  • NSOrderedDescending,调用对象数值大于参数中的对象数值。
  • NSOrderedSame,两个数值相等。

如下面的代码,我们将比较两个数值对象的大小。

NSNumber *objNum1 = [NSNumber numberWithInt: 10];
NSNumber *objNum2 = [NSNumber numberWithInt: 99];
if ([objNum1 compare: objNum2] == NSOrderedAscending)
    NSLog(@"%@小于%@", objNum1, objNum2);
else if ([objNum1 compare: objNum2] == NSOrderedDescending)
    NSLog(@"%@大于%@", objNum1, objNum2);
else
    NSLog(@"%@等于%@", objNum1, objNum2);

NSNumber中定义了一系列基本数据类型相关的转换方法,下面给出一些类型的,包括创建对象的类方法和初始化方法,以及将数值对象转换为数值类型的方法。

  • char类型(NSChar),numberWithChar:方法、initWithChar:方法和charValue属性。
  • int类型,numberWithInt:方法、initWithInt方法和intValue属性。
  • long int类型,numberWithLong:方法、initWithLong:方法和longValue属性。
  • NSInteger类型,numberWithInteger方法、initWithInteger:方法和integerValue属性。
  • NSUInteger类型,numberWithUnsignedInteger:方法、initWithUnsignedInteger:方法和unsignedIntegerValue属性。
  • float类型,numberWithFloat:方法、initWithFloat:方法和floatValue属性。
  • double类型,numberWithDouble:方法、initWithDouble:方法和doubleValue属性。

其它的基本数据类型,如short、unsigned short、long long int、unsigned long long int等,相信大家并不难猜出相对应的方法。

6.2.3. 使用NSValue类

O博士:前面,我们讨论了如何在基本数据类型和对象之间进行转换,但在开发中,还有一些常用的值类型需要我们注意,如我们在后面的内容中需要使用的CGPoint、CGSize、CGRect、NSRange等结构类型,如果需要将这个类型的数据作为NSArray对象的成员,同样需要将它们转换为相应的对象类型,此时,就可以使用NSValue类。

CGPoint结构用于保存点坐标信息数据,可以使用valueWithPoint:方法将其转换为NSValue对象,如下面的代码。

CGPoint pt = CGPointMake(10.0, 15.0);
NSValue *objPt = [NSValue valueWithPoint: pt];

相应的,将对象还原为结构类型的方法如下:

pt = objPt.pointValue;

下面是常用的结构与NSValue对象之间的转换方法:

  • CGRect结构,表示一个矩形的结构类型,相应转换使用valueWithRect:方法和rectValuen属性。
  • CGSize结构,表示宽度和高度尺寸的结构类型,相应的转换使用valueWithSize:方法和sizeValue属性。
  • NSRange结构,表示一个范围,相应的转换使用valueWithRange:方法和rangeValue属性。

6.2.4. 数组成员操作

O博士:我们已经知道如何创建一个NSArray对象,这是一个成员不可变的数组对象,接下来,我们就来看看这样的对象有哪些基本的操作。

获取成员数量

我们可以使用NSArray对象的实例方法count获取成员的数量,其返回值为NSUInteger,即unsinged long int类型。如下面的代码,我们将获取arr对象的成员数量,并显示出来。

NSUInteger arrCount = [arr count];
NSLog(@"arr对象有%u个成员", (unsigned int)arrCount);
访问成员

接下来,我们可以通过一个循环逐一访问数组的成员,如下面的代码。

for (int i=0; i<arrCount; i++) {
    NSLog(@"%@", [arr objectAtIndex:i]);
}

实际上,对于这样的操作,在NSArray对象中还有一个方法可以实现,其定义如下:

-(void) enumerateObjectsUsingBlock:
    (void)(^)(id obj, NSUInteger idx, BOOL *stop) block

此方法的作用就是将NSArray对象中的每一个成员都使用块(block)中定义的代码进行处理;其中,块的参数obj表示数组元素、参数idx为数组索引值,而参数stop按指针传递,当在块中需要停止对元素的操作时,可以将此参数设置为YES,此时,就像循环语句结构中使用了break语句一样。

如下面的代码,我们将使用此方法显示数组中的成员。

[arr enumerateObjectsUsingBlock:
    ^(id obj, NSUInteger idx, BOOL *stop){
        NSLog(@"%@", obj);
    }];
判断成员是否存在

当我们需要判断一个成员是否存在于NSArray对象时,可以使用如下方法:

-(BOOL) containsObject:obj;

如下面的代码,我们用于判断@"ghi"字符串是否存在于arr对象中:

BOOL result = [arr containsObject:@"ghi"];  //YES
获取成员的索引值

通过indexOfObject:方法,我们可以查找数组成员所在的索引值,如下面的代码。

NSUInteger index =  [arr indexOfObject: @"def"];
NSLog(@"def的位置是%u", (unsigned int)index);
获取第一个成员

如果我们想快速的获取数组的第一个对象,可以使用firstObject方法,如下面的代码。

NSLog(@"第一个对象是%@", [arr firstObject]);

代码会显示abc。此外,如果数组对象中不包含任何成员,则firstObject方法会返回nil值。

获取最后一个成员

如果直接获取数组的最后一个成员,可以使用lastObject方法,如下面的代码。

NSLog(@"最后一个对象是%@", [arr lastObject]);

6.2.5. 保存与载入

NSArray类另一个强大的功能就是能够直接将对象信息保存到磁盘文件中,此操作使用writeToFile::方法,其定义如下:

-(BOOL) writeToFile:(NSString*)path atomically:(BOOL)flag;

其中,参数一为要保存到的文件路径,如果将flag设置为YES,则操作会通过临时文件来完成,可以有效保证文件写入的完整性。下面的代码,将在设备中保存一个数组对象。

// 获取文档存放路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(
    NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docPath = [paths objectAtIndex: 0];
// 确定保存文件名
NSString *filename =
    [docPath stringByAppendingPathComponent:@"myArrayObject"];
// 保存数组对象
[arr writeToFile:filename atomically:YES];

在Mac中,此代码会将arr对象保存到“文稿”目录中,其文件名为“myArrayObject”,我们可以在Finder中查看,如下图。

下面的代码,我们可以使用初始化方法initWithContentsOfFile:从myArrayObject文件中读取这个数组对象。

// 获取用户文档存放路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(
    NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docPath = [paths objectAtIndex: 0];
// 确定保存文件名
NSString *filename =
    [docPath stringByAppendingPathComponent:@"myArrayObject"];
// 读取数组对象文件
NSArray *arr = [[NSArray alloc] initWithContentsOfFile:filename];
NSLog(@"%@", arr[0]);

6.3. 可变数组(NSMutableArray类型)

O博士:前面讨论的是不可变数组对象(NSArray),这类对象的成员在确定后就不允许修改了,对于不变的东西,处理起来自然会快一些;然而,我们时常会需要一个灵活配置的机器人队伍,这样才能应对各种环境下的战斗。

当我们需要对数组成员进行大量的操作,如添加、删除、替换等,使用不可变数组对象显然不太合适;而对于需要动态管理的数组对象,我们可以使用可变数组,此时使用NSMutableArray类,它是NSArray类的子类,所以,前一节介绍的NSArray类的操作方法都可以在NSMutableArray类中使用,下面,我们就来讨论一些在NSMutableArray类中与NSArray类操作不太一样的地方。

6.3.1. 创建NSMutableArray对象

由于NSMutableArray对象成员管理是动态的,所以,我们可以初始化一个空的NSMutableArray对象,如下面的代码:

NSMutableArray *arr = [NSMutableArray array];

此外,我们也可以指定成员的初始数量,可以使用两个方法,如下面的代码都可以初始化10个成员的NSMutableArray对象:

NSMutableArray *arr = [NSMutableArray arrayWithCapacity: 10];

或:

NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity: 10];

对于没有赋值的数组成员,其值默认为nil,即空对象。

O博士:当你需要初始化NSMutableArray对象的成员时,别忘了NSArray中介绍的方法也是可以使用的。

6.3.2. 添加成员

当我们需要在NSMutableArray对象的最后添加一个成员时,可以使用addObject:方法,其定义如下:

-(void) addObject:obj;

如下面的代码,我们将在数组后添加一个机器人成员。

NSMutableArray *commando = [NSMutableArray array];
CRobotUnit *soldier = [[CRobotUnit alloc] init];
soldier.model = @"RobotSoldier-II";
soldier.identifier = 110099;
[commando addObject:soldier];
NSLog(@"%i", ((CRobotUnit*)[commando firstObject]).identifier);

接下来,我们再为突击队添加一个战斗力更强大的机器人,此时,可以使用insertObject::方法,它的定义如下:

-(void) insertObject:obj atIndex:i;

方法中的obj指定要添加到数组中的对象,而i则可以指定对象插入的位置。下面的代码,我们将一个killer级机器人添加到突击队,并第一个成员的位置,谁让它是狠角色呢。^^

CRobotUnit *killer = [[CRobotUnit alloc] init];
killer.model = @"KILLER-I";
killer.identifier = 990011;
[commando insertObject:killer atIndex:0];
NSLog(@"%i", ((CRobotUnit*)[commando firstObject]).identifier);

6.3.3. 删除成员

O博士:在NSMutableArray对象中删除成员同样可以有两个方法可以使用,分别是removeObject:方法和removeObjectAtIndex:方法。

removeObject:方法的参数是需要删除的对象,如:

[commando removeObject:killer];

当删除某个对象后,成员的位置都会相应的变化,如下面的代码,我们将删除第一个成员,即索引值为0的成员,此时,commando对象又会变成了空数组对象。

[commando removeObjectAtIndex:0];
NSLog(@"%lu", [commando count]);

6.3.4. 替换成员

O博士:替换NSMutableArray对象中的成员,我们可以使用replaceObjectAtIndex::方法,其定义如下:

-(void) replaceObjectAtIndex:i withObject:obj;

如下面的代码,我们将第二个成员替换为一个超级机器人士兵。

CRobotUnit *supersoldier = [[CRobotUnit alloc] init];
supersoldier.model = @"SuperSoldier-III";
supersoldier.identifier = 991101;
[commando replaceObjectAtIndex:1 withObject:supersoldier];
NSLog(@"%@", ((CRobotUnit*)[commando objectAtIndex:1]).model);

6.4. 集合(Set)

O博士:实际工作中,我们还可以使用集合(Set)来打造战队;从表面上看,集合与数组的操作方法上非常相似,但集合并不使用数值索引来访问成员。实际应用中,到底使用哪一种类型,就看具体需要以怎样的方式组队了。

本节,我们就简单讨论一下集合的使用。在Foundation框架中,为我们提供了一些关于集合的类,在这里,我讨论一些常用的类型,包括:NSSet(不可变集合)和NSMutableSet(可变集合)类。

6.4.1. 不可变集合(NSSet类)

O博士:首先介绍不可变集合,和不可变数组相似,其成员一旦确定就不能再修改了。搭档就是搭档,没事就不用换了!^_^

创建NSSet对象

创建NSSet对象,可以使用一个类方法,其定义如下:

+(id) setWithObjects:obj1,obj2,...,nil

我们可以看到,这个方法的参数同样是指定集合的成员,并使用nil作为结束。

另一个创建NSSet对象的方法是一个初始化方法,其定义如下:

-(id) initWithObjects:obj1,obj2,...,nil

如下面的代码,我们将创建一个有着三个成员的NSSet对象。

NSSet *set = [NSSet setWithObjects:@"abc",@"def",@"ghi",nil];

接下来,我们看一看关于集合成员的相关操作。

  • count属性,返回成员的数量,其返回值也同样是NSUInteger类型。如set. count
  • anyObject实例方法,随机返回一个集合成员对象,返回值类型为id。如[set anyObject];
  • containsObject:实例方法,判断集合中是否存在指定的对象,返回值类型为BOOL。如[set containsObject:@"def"];

除了对单个成员,我们还可以在一个集合和另一个集合对象之间进行一些操作,如:

  • isSubsetOfSet:实例方法,返回值类型为BOOL,判断当前集合对象中的成员是否都存在于参数指定的NSSet对象之中。如[set1 isSubsetOfSet:set2];
  • intersectsSet:实例方法,返回值类型为BOOL,判断当前集合对象中的成员是否至少有一个存在于参数指定的NSSet对象之中。如[set1 intersectsSet:set2];
  • isEqualToSet:实例方法,返回值类型为BOOL,判断两个集合是否相等。如[set1 isEqualToSet:set2];

如果我们需要对集合中每一个成员进行操作,可以使用enumerateObjectsUsingBlock:方法,其定义如下:

- (void)enumerateObjectsUsingBlock:(void(^)(id obj,BOOL *stop))block

在NSArray类中,我们已经看到此方法的应用。

6.4.2. 可变集合(NSMutableSet类)

O博士:与数组对象操作相似,集合同样有一个可变版本的类型,即NSMutableSet类型,通过这个类,我们可以很灵活地操作集合成员,以及集合与集合之间的一些操作,下面,我们就来看看这些常用的操作方法。

  • addObject:实例方法,用于在集合中添加一个成员,此方法没有返回值。如[set addObject:@"jkl"];
  • removeAllObjects实例方法,删除所有集合成员。
  • removeObject:实例方法,删除指定的对象。
  • unionSet:实例方法,将参数中集合对象中的成员全部添加到当前集合对象中。
  • minusSet:实例方法,从当前集合对象中删除参数中集合对象指定的所有成员。
  • intersectSet:实例方法,从从当前集合对象中删除参数中集合对象指定以外的成员。

下面的代码,我们将创建10个超级士兵组成的突击队。

CRobotUnit *supersoldier = [[CRobotUnit alloc] init];
supersoldier.model = @"SuperSoldier-III";
supersoldier.identifier = 991101;
NSMutableSet *commando = [NSMutableSet set];
[commando addObject: supersoldier];  
for(int i=1; i<=9; i++)
{
    CRobotUnit *newSoldier = [supersoldier copy];  // 克隆机器人
    newSoldier.identifier = supersoldier.identifier + i;  // 修改编号
    [commando addObject:newSoldier]; // 加入突击队
}
// 全体开火
[commando enumerateObjectsUsingBlock:^(id obj,BOOL *stop){
    [(CRobotUnit*)obj fire];
}];

O博士:多次执行此代码,显示的结果并不一样,这一点证明了集合的一个重要特点,也就是,其成员是无序的!

6.5. 字典(NSDictionary)

O博士:字典(Dictionary)用于处理“键(key)/值(value)”格式的对象成员,这些成员都由一个键对应一个值,其中,键是不允许重复的,而值则不允许为nil值。此外,在使用字典的过程中,和集合一样不能假设成员的顺序,因为成员的访问顺序是不确定的。

6.5.1. 创建字典对象

O博士:创建字典对象同样可以使用一个类方法和一个初始化方法,首先看创建字典对象的类方法,其定义格式如下:

+(id) dictionaryWithObjectsAndKeys:
    value1,key1,value2,key2,...,nil

其中的参数每两个为一组,共同组成一个集合成员,第一个为成员的键;第二个为成员的值;最后,同样使用一个nil值作为结束。

如下面的代码,我们创建了一个有着五个成员的集合对象,包含了故乡太阳系中的地内主要天体:

NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
@"太阳",@"Sun",
@"地球",@"Earth",
@"月球",@"Moon",
@"金星",@"Venus",
@"水星",@"Mercury",
nil];

创建集合对象的初始化方法定义如下:

-(id) initWithObjectsAndKeys: value1,key1,value2,key2,...,nil

其参数的使用与dictionaryWithObjectsAndKey:方法相同。

6.5.2. 常用方法

O博士:我们来看看在NSDictionary类中都可以怎么使用字典。

成员数量

NSDictionary对象中,我们同样可以使用count属性成员的数量,其返回值也同样是NSUInteger类型。

获取所有键

如果我们想获取字典中所有的键,可以使用allKeys方法,它定义为实例方法,返回值是一个包含了全部键的NSArray对象。如下面的代码会显示dict对象中所有键:

NSArray *words = [dict allKeys];
[words enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
    NSLog(@"%@", obj);
}];
根据键返回对应的值

当我们需要根据键返回相应的值时,可以使用objectForKey:方法,它定义为实例方法,如下面的方法,将显示键为@"earth"的值。

NSLog(@"Earth : %@", [dict objectForKey:@"Earth"]);
遍历所有成员

如果我需要快速访问字典对象中的所有成员,可以使用enumerateKeysAndObjectsUsingBlock:方法,其定义如下:

- (void)enumerateKeysAndObjectsUsingBlock:
    (void (^)(id key, id obj, BOOL *stop))block

下面的代码,我们将显示dict对象中的所有内容。

[dict enumerateKeysAndObjectsUsingBlock:
    ^(id key, id obj, BOOL *stop){
        NSLog(@"%@ = %@", key, obj);
    }];

6.5.3. NSMutableDictionary类

O博士:NSMutableDictionary类是NSDictionary类的可变版本,我们可以定义此对象的初始成员数量,此操作可以使用以下两种方法:

  • dictionaryWithCapacity:类方法,参数为成员数量。
  • initWithCapacity:实例初始化方法,参数同样为成员数量。

此外,NSMutableDictionary类常用的方法还有:

  • removeAllObjects实例方法,删除所有字典成员。
  • removeObjectForKey:实例方法,根据键删除字典成员,参数为需要删除成员的键。
  • setObject::实例方法,设置指定键成员的值,如果键存在则修改它的值内容,如果键不存在,则添加新的成员。其中,参数一为成员的值,参数二为指定的键。如下面的代码。

    [dict setObject:@"火星" forKey:@"Mars"];

O博士:现在,我们可以看到数组、集合和字典在应用特点上有什么不同,大家应该根据代码中的具体要求和特点,合理地使用这些工具。本章的最后,我们再回顾一下它们的主要特点。

  • C风格数组,可以快速处理一组相同类型的数据,成员可以是传统的值类型数据,也可以是各种对象。
  • 数组对象。可以更灵活地处理一组相关的对象,但其成员只能是对象;和C风格数组相似,我们可以使用数值索引来访问和管理数组成员。在处理值类型时,我们可以使用NSNumber和NSValue类来完成相关的转换工作。
  • 集合,是一种无序对象序列,同样,其成员也必须是对象,但成员的访问顺序是不确定的,所以,我们并不能假设集合中各个成员的位置。
  • 字典是另一种无序对象序列,其每个成员包括唯一的键来确定一个值,而值不能为nil。在处理字典的成员时,我们可以使用键作为索引,但同样不能假设成员的位置。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值