Objective-C编程语言(Programming with Objective-C)(六):值与集合

投稿作者:简书/杨浩宇

新浪微博:@杨浩宇-小橘爷

原文链接:

http://www.jianshu.com/p/acac68006f77

值与集合

尽管 Objective-C 是一种面向对象的编程语言,它也是 C 语言的加强版,这意味着你可以在 Objective-C 中使用标准 C 中任意的标量类型(非对象的),例如int,float,char。在 Cocoa 和 Cocoa Touch 应用中,你还可以使用一些额外的类型,例如 NSInteger,NSUInteger 和 CGFloat,在不同的系统结构中,他们的定义方式也不同。

标量类型用于当你不需要用一个对象来表示值的时候。字符型经常作为 NSString 的实例而使用,数值被存储在标量类型的局部变量或属性中。

你可以在 Objective-C 中定义与 C 语言类似的数组,但是在 Cocoa 和 Cocoa Touch 应用中,集合被用于实例化像 NSArray 或 NSDictionary 这样的类。这些类只能存放对象,这意味着在添加对象到集合之前,你就要用 NSValue, NSNumber 或 NSString 这样的类把标量包装起来。

在前面的章节中,我们多次使用了 NSString 类、它的初始化、方法函数库,@”string” 字符为创建 NSString 的实例提供了简单的语法。在本章中,我们会通过方法调用和赋值语句来示范如何使用 NSValue 和 NSNumber 类。

你可以使用 C 中的基本数据类型

在 Objective-C 中,C 的每一个标量变量类型都是可以使用的:

int someInteger = 42;
float someFloatingPointNumber = 3.1415;
double someDoublePrecisionFloatingPointNumber = 6.02214199e23;

还有 C 的运算符也都可用:

int someInteger = 42;
someInteger++; // someInteger == 43int anotherInteger = 64;
anotherInteger--; // anotherInteger == 63anotherInteger *= 2; // anotherInteger == 126

如果你要为 Objective-C 属性使用标量类型 ,请这样做:

@interface XYZCalculator : NSObject
@property double currentValue;
@end

你也可以对属性使用 C 运算符,用点语法进行赋值操作,就像这样:

@implementation XYZCalculator
- (void)increment {
    self.currentValue++;
}
- (void)decrement {
    self.currentValue--;
}
- (void)multiplyBy:(double)factor {
    self.currentValue *= factor;
}
@end

点语法是一种纯粹用于存取器调用方法的语法,所以这个例子中的每一条操作都是先使用 get accessor 方法获取实例变量值,运行程序后,再使用 set accessor 方法存储实例变量值作为结果。

Objective-C 中其他的基本类型

BOOL 类型在 Objective-C 中用来表示布尔值:yes 和 no。正如你想的那样,yes 在逻辑上等于 true 和1,no 等于 false 和0;

在 Cocoa 和 Cocoa Touch 中,许多方法的参数也可以使用特殊的数据类型,例如 NSInteger 和 CGFloat。

例如,NSTableViewDataSource 和 UITableViewDataSource 协议(在之前章节提到的)都有令数据按行显示的方法:

@protocol NSTableViewDataSource <NSObject>
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView;
...
@end

像NSInteger和NSUInteger这样的类型,在不同的系统结构中有不同的定义方式。当为32位运行环境(例如 iOS)编程时,他们分别是32位的有符号整数和无符号整数;当为64位运行环境(例如 modern OS X runtime)编程时,他们分别是64位的有符号整数和无符号整数。

如果你希望在 API 边界间(包括内外部 API)传值,最好使用这种特定平台的类型,例如在你的应用代码和平台架构间,利用方法、函数调用传递参数或返回数值。

对于局部变量,例如循环中的计数值,如果你知道该值作用域的限制,使用 C 的基本类型定义它也是可以的。

C 的结构体可以存储基本数据

一些 Cocoa 和 Cocoa Touch API 使用 C 的数据结构存储数值。例如,一个字符串对象可以作为一个子字符串的取值域,就像这样:

NSString *mainString = @"This is a long string";
NSRange substringRange = [mainString rangeOfString:@"long"];

一个 NSRange 结构保存了一个地址和一个长度值。在这个例子中,substringRange 中会保存{10,4}这两个值:以0开始计算的位置,主字符串中的第10个字母为子字符串@”long”的第 1 个字母,@”long”的长度为 4。

相似的是,如果你要编写自定义图形代码,你需要使用 Quartz,它要使用以 CGFloat 数据类型为基础的结构,就像 OS X 上的 NSPoint 和 NSSize,iOS 上的 CGPoint 和 CGSize。CGFloat 也同样在不同的系统结构上有不同的定义方式。

想看更多关于 Quartz 2D 绘制机制?点击 Quartz 2D Programming Guide。

对象可以表示基本数值

如果你需要用标量表示一个对象的值,你可以使用 Cocoa 和 Cocoa Touch 提供的基础赋值类,例如我们在下一个部分里面提到的集合类。

通过NSString类的实例表示字符串

就像之前章节里面提到的,NSString 是用来表示字符串的,例如 Hello World。创建 NSString 对象有很多种方式,包括分配空间并初始化,使用工厂方法或者纯语法:

NSString *firstString = [[NSString alloc] initWithCString:"Hello World!" encoding:NSUTF8StringEncoding];
NSString *secondString = [NSString stringWithCString:"Hello World!" encoding:NSUTF8StringEncoding];
NSString *thirdString = @"Hello World!";

每一个例子都高效的完成了同一件事:创建了一个字符串对象,其值为给定内容。

基本 NSString 类是不可变的,它的内容在创建时设好就不能再改变了。如果你想要一个不同的字符串,你必须创建一个新的对象,就像这样:

NSString *name = @"John";
name = [name stringByAppendingString:@"ny"]; // returns a new string object

NSMutableString类是NSString的可变子类,允许在方法中修改,与appendString和appendFormat相似:

NSMutableString *name = [NSMutableString stringWithString:@"John"];
[name appendString:@"ny"]; // same object, but now represents "Johnny"
使用 Format Strings 创建来自其他对象和值的字符串

如果你需要创建一个带有变量的字符串,你要使用 format string。它允许你使用格式符来表示值是如何插入的:

int magicNumber = ...
NSString *magicString = [NSString stringWithFormat:@"The magic number is %i", magicNumber];

可用的格式符在这里 String Format Specifiers。想了解更多关于字符串,请点击 String Programming Guide。

通过 NSNumber 类的实例表示数字

NSNumber 类用来表示所有 C 基础的标量类型,包括char,double,float,int,long,short以及它们的无符号变量,还有 Objective-C 中的布尔类型,BOOL。

就像 NSString,你有很多种方式可以创建 NSNumber 实例,包括空间分配并初始化,以及使用工厂方法:

NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42];
NSNumber *unsignedNumber = [[NSNumber alloc] initWithUnsignedInt:42u];
NSNumber *longNumber = [[NSNumber alloc] initWithLong:42l];

NSNumber *boolNumber = [[NSNumber alloc] initWithBOOL:YES];

NSNumber *simpleFloat = [NSNumber numberWithFloat:3.14f];
NSNumber *betterDouble = [NSNumber numberWithDouble:3.1415926535];

NSNumber *someChar = [NSNumber numberWithChar:'T'];

你也可以用Objective-C中的语法来创建NSNumber实例:

NSNumber *magicNumber = @42;
NSNumber *unsignedNumber = @42u;
NSNumber *longNumber = @42l;

NSNumber *boolNumber = @YES;

NSNumber *simpleFloat = @3.14f;
NSNumber *betterDouble = @3.1415926535;

NSNumber *someChar = @'T';

这些例子都等同于使用 NSNumber 类的工厂方法。

一旦你已经创建了一个 NSNumber 实例,你就可以使用 accessor 方法申请一个纯量值:

int scalarMagic = [magicNumber intValue];
unsigned int scalarUnsigned = [unsignedNumber unsignedIntValue];
long scalarLong = [longNumber longValue];

BOOL scalarBool = [boolNumber boolValue];
float scalarSimpleFloat = [simpleFloat floatValue];
double scalarBetterDouble = [betterDouble doubleValue];
char scalarChar = [someChar charValue];

NSNumber 类也为 Objective-C 额外的基本类型提供了方法。如果你想创建 NSInteger 和 NSUInteger 的对象,请使用如下方法:

NSInteger anInteger = 64;
NSUInteger anUnsignedInteger = 100;

NSNumber *firstInteger = [[NSNumber alloc] initWithInteger:anInteger];
NSNumber *secondInteger = [NSNumber numberWithUnsignedInteger:anUnsignedInteger];

NSInteger integerCheck = [firstInteger integerValue];
NSUInteger unsignedCheck = [secondInteger unsignedIntegerValue];

所有的 NSNumber 实例都是不可变的,并且它也没有可变的子类;如果你需要一个不同的数字,请使用另一个 NSNumber 实例。

提示 NSNumber 实际上是一个类集。这意味着当你在运行时创建了一个实例时,你会得到一个合适的具体化的子类,值为给定值。只把创建的对象当做一个 NSNumber 的实例就可以了。

使用 NSValue 类的实例表示其他值

NSNumber 类是基础 NSValue 类的子类,它给单独的数据或数据项提供了包装对象。除了 C 的基本标量类型,NSValue 也可以表示指针和结构体。

NSValue 类提供了很多的工厂方法来创建一个给定结构的值,这使得创建一个实例变得十分简单,例如之前例子中的 NSRange:

NSString *mainString = @"This is a long string";
NSRange substringRange = [mainString rangeOfString:@"long"];
NSValue *rangeValue = [NSValue valueWithRange:substringRange];

你也可以使用 NSValue 来创建自定义的结构体对象。如果你一定要用 C 中结构体存储信息,请这样做:

typedef struct {
    int i;
    float f;
} MyIntegerFloatStruct;

你可以通过指向结构体的指针或编好的 Objective-C 类型来创建一个 NSValue 实例。@encode() 这条编译器指令是用来创建正确的 Objective-C 类型的,如:

struct MyIntegerFloatStruct aStruct;
aStruct.i = 42;
aStruct.f = 3.14;

NSValue *structValue = [NSValue value:&aStruct withObjCType:@encode(MyIntegerFloatStruct)];

标准C中的&符号在这里表示 aStruct 中 value 参数的地址。

大多数集合都是对象

尽管你可以使用 C 中的数组存放标量数据的集合,甚至还可以存放对象指针,大多数 Objective-C 代码中的集合都是 Cocoa 和 Cocoa Touch 集合类中的一个,例如 NSArray, NSSet 和 NSDictionary。

这些类用来管理一组对象,这意味着你加入集合的任何一项都必须是 Objective-C 类的实例。如果你需要添加一个标量值,你必须要先创建一个合适的 NSNumber 或 NSValue 实例。

集合类使用强大的引用持续追踪他们的内容,而不是不管怎样都为集合中的每一个对象做一份拷贝。这意味着你加入集合中的每一个对象的生命周期都将至少和集合的生命周期一样长, 正如这里描述的 Manage the Object Graph through Ownership and Responsibility。

除了追踪他们的内容,Cocoa 和 Cocoa Touch 中的每一个集合类都可以简单地完成一些任务,例如枚举,存取特定的项,查找某一个对象是否在这个集合中。
基本的 NSArray, NSSet 和 NSDictionary 类都是不可变的,它们的值在创建时就固定了。但它们都拥有可变的子类,你可以通过子类来添加和删除对象。

想查看更多 Cocoa 和 Cocoa Touch 中的集合类,请点击 Collections Programming Topics。

数组是有序的集合

NSArray 是用来表示对象的有序集合。对集合中内容唯一的要求是每一项都要是 Obejctive-C 的对象,但它们并不需要是同一个类的实例。

为了满足有序,每一个元素的存储都是从0开始记序,就像图6-1这样。

创建数组

就像之前章节里写的赋值类那样,你可以通过分配空间并初始化,使用工厂类方法或纯语法方式来创建数组。

初始化方法和工厂方法有很多种,你可以根据对象的个数来选择不同的方法:

+ (id)arrayWithObject:(id)anObject;
+ (id)arrayWithObjects:(id)firstObject, ...;
- (id)initWithObjects:(id)firstObject, ...;

arrayWithObjects 和 initWithObjects: 这两种方法都采用零终止,参数个数是可变的,这意味着你数组的最后一个值必须是 nil:

NSArray *someArray = [NSArray arrayWithObjects:someObject, someString, someNumber, someValue, nil];

这个例子创建了一个类似图6-1中的数组。第一个对象:someObject,下标是0;最后一个对象:someValue,下标是3。

如果数组中有一个值为 nil,那么这个数组会在这里被截断:

id firstObject = @"someString";
id secondObject = nil;
id thirdObject = @"anotherString";
NSArray *someArray = [NSArray arrayWithObjects:firstObject, secondObject, thirdObject, nil];

在这里,someArray 数组只包含 firstObject,因为第二个元素为 nil,系统认为它是数组的结束。

语法

你可以用 Objective-C 的语法创建数组:

NSArray *someArray = @[firstObject,secondObject, thirdObject];

在这里你不能用nil来结束数组,事实上 nil 是非法的值。如果你运行下面的代码,系统会抛出异常:

id firstObject = @"someString";
id secondObject = nil;
NSArray *someArray = @[firstObject, secondObject];// exception: "attempt to insert nil object"

如果你一定要使用 nil 值,你可以用 NSNull 类,详情请查看 Represent nil with NSNull。

查找数组对象

一旦你创建了数组,你可以查询有关的信息,例如对象个数或查找特定的元素:

NSUInteger numberOfItems = [someArray count];
if ([someArray containsObject:someString]) {
    ...
}

你也可以查询指定下标的元素。如果你查询的下标数是错误的,系统会抛出越界异常,所以你应该首先确认数组长度:

if ([someArray count] > 0) {
    NSLog(@"First item is: %@", [someArray objectAtIndex:0]);
}

这个例子检查了数组长度是否大于0。如果大于0,它会记录下第一个元素,也就是下标为0的元素信息。

下标

还有另一种下标写法,使用objectAtIndex:这与 C 中数组写法十分像。使用这种方法重写前面的例子:

if ([someArray count] > 0) {
    NSLog(@"First item is: %@", someArray[0]);
}
给数组对象排序

NSArray 类提供了很多种排序的方法。因为 NSArray 是不可变的,所以所有的方法都会返回一个有序数组。

例如,你可以通过调用 compare:给字符串排序:

NSArray *unsortedStrings = @[@"gammaString", @"alphaString", @"betaString"];
NSArray *sortedStrings = [unsortedStrings sortedArrayUsingSelector:@selector(compare:)];
可变性

尽管 NSArray 类是不可变的,这并不会影响集合中的对象。如果你想添加一个可变的字符串,可以这样做:

NSMutableString *mutableString = [NSMutableString stringWithString:@"Hello"];
NSArray *immutableArray = @[mutableString];

没有什么可以阻止你改变它:

if ([immutableArray count] > 0) {
    id string = immutableArray[0];
    if ([string isKindOfClass:[NSMutableString class]]) {
        [string appendString:@" World!"];
    }
}

如果你想要从初始化好的数组中添加或删除一个元素,你需要使用 NSMutableArray 增删初始化后的数组对象,它提供了很多个方法添加、删除和替换一个或多个对象:

NSMutableArray *mutableArray = [NSMutableArray array];
[mutableArray addObject:@"gamma"];
[mutableArray addObject:@"alpha"];
[mutableArray addObject:@"beta"];

[mutableArray replaceObjectAtIndex:0 withObject:@"epsilon"];

这个例子创建了以 @”epsilon”, @”alpha”,@”beta” 结尾的数组。

不用创建一个新的数组也可以给可变数组排序:

[mutableArray sortUsingSelector:@selector(caseInsensitiveCompare:)];

这使得该数组以字母升序排序:@”alpha”, @”beta”, @”epsilon”。

无序集合:Sets

NSSet 和数组很相似,但是它可以存储不同对象的无序组,如图6-2。

因为 sets 是无序的,所以当涉及到测试成员时,sets 比数组更有优势。

基础的 NSSet 类也是不可变的,所以它的值必须在创建时确定,使用空间分配并初始化或工厂方法:

NSSet *simpleSet = [NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil];

和 NSArray, initWithObjects:, setWithObjects:方法一样,NSSet 以 nil 作为结束,参数个数可变。可变的 NSSet 的子类是 NSMutableset。

Sets 中每一个对象只能存储一个引用,所以你不能多次添加同一个对象:

NSNumber *number = @42;
NSSet *numberSet = [NSSet setWithObjects:number, number, number, number, nil];// numberSet only contains one object

查看更多关于sets:Sets: Unordered Collections of Objects

字典是一种存储键值对的集合

NSDictionary 存储了对象和它们的关键字,而不是单单只保存了一个有序或无序的集合,这个功能可以用来恢复信息。

最好使用字符串作为字典的关键字,如图6-3:

提示 使用其他对象作为关键字也可以,但值得注意的是关键字需要被字典复制,所以你的关键字一定要支持 NSCopying。
如果你的代码包含键值,你必须用字符串关键字作为字典对象,正如这里提到的:Key-Value Coding Programming Guide。

字典的创建

你可以用空间分配并初始化或工厂方法创建字典:

NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: 
               someObject, @"anObject", 
         @"Hello, World!", @"helloString", 
                      @42, @"magicNumber", 
                someValue, @"aValue", 
                         nil];

注意使用 dictionaryWithObjectsAndKeys: 和 initWithObjectsAndKeys: 方法时,每个对象在给定关键字前需要确定好,对象和关键字都必须以 nil 结束。

语法

Objective-C 也可以使用纯语法创建字典:

NSDictionary *dictionary = @{
              @"anObject" : someObject,
           @"helloString" : @"Hello, World!",
           @"magicNumber" : @42,
                @"aValue" : someValue
};

注意在字典语法中,关键字需要先确定,然后再确定对象,而且关键字不能以 nil结束。

字典的查询

一旦你创建了字典,你可以从中给对象指定一个关键字:

NSNumber *storedNumber = [dictionary objectForKey:@"magicNumber"];

如果没有找到该对象,objectForKey:方法会返回 nil。

objectForKey:方法还有另一种使用方法也可以完成这个功能:

NSNumber *storedNumber = dictionary[@"magicNumber"];
可变性

如果要在创建 dictionary 后增删对象,你需要使用 NSMutableDictionary 子类:

[dictionary setObject:@"another string" forKey:@"secondString"];
[dictionary removeObjectForKey:@"anObject"];
使用 NSNull 代替 nil

在这一部分中,你不可以在集合类中添加 nil,因为在 Objective-C 中,nil 表示“没有对象”。如果你需要在集合中表示“没有对象”,你要使用 NSNull 类:

NSArray *array = @[ @"string", @42, [NSNull null] ];

NSNull 是一个单例类,这意味着 null 方法总是返回同样的实例。这表示你可以用共享的 NSNull 实例来查找数组中的对象:

for (id object in array) {
    if (object == [NSNull null]) {
        NSLog(@"Found a null object");
    }
}
使用集合来保存对象图

直接写 NSArray 和 NSDictionary 类是十分简单的:

NSURL *fileURL = ...
NSArray *array = @[@"first", @"second", @"third"];

BOOL success = [array writeToURL:fileURL atomically:YES];
if (!success) {
    // an error occured...
}

如果集合中的某一个对象是属性列表(property lists)可以接受的类型(NSArray,NSDictionary,NSString,NSData,NSDate和NSNumber),你可以从磁盘中重新创建完整的层次:

NSURL *fileURL = ...
NSArray *array = [NSArray arrayWithContentsOfURL:fileURL];
if (!array) {
     // an error occurred...
}

查看更多关于property lists,请看Property List Programming Guide。

如果你需要保存除了上面提到的标准 property list 类之外的对象类,你可以使用归档对象,例如 NSKeyedArchiver,来创建一个集合式的对象归档。

创建存档的唯一要求是每一个对象必须支持 NSCoding 协议。这意味着每一个对象必须知道如何在存档中编码(通过实现 encodeWithCoder: 方法),以及在读取存档时解码(通过 initWithCoder: 方法)。

NSArray,NSSet,NSDictionary 以及他们的可变子类,都支持 NSCoding,这意味着你可以通过存档来保存复杂的对象层次。如果你使用 Interface Builder 来布局窗口和视图,那么你的 nib 文件就是视觉化的层次对象存档。在程序执行时,对于使用有关类的对象层次,nib 文件是不能存档的。

查看更多关于 Archives,请看 Archives and Serializations Programming Guide。

使用最有效的集合枚举技术

Objective-C、Cocoa、Cocoa Touch 提供了多种枚举集合内容的方法。当然,使用 C 语言中传统的 for 循环来枚举内容也是可以的:

int count = [array count];
for (int index = 0; index < count; index++) {
    id eachObject = [array objectAtIndex:index];
    ...
}

你最好可以练习使用这部分中描述的其他技术来实现这个功能。

快速枚举使枚举一个集合变得容易

许多集合类都符合 NSFastEnumeration 协议,包括 NSArray,NSSet 和 NSDictionary。这意味着你可以使用快速列举,一种 Objective-C 中特有的语言。

快速枚举数组或集合中内容的语法:

for (<Type> <variable> in <collection>) {
    ...
}

使用快速枚举描述每一个数组中对象:

for (id eachObject in array) {
    NSLog(@"Object: %@", eachObject);
}

其中的 eachObject 变量在每一层循环里自动地被设为当前对象,所以每一个对象都会枚举出来。

如果你在字典中进行快速列举,你要像这样遍历字典的关键字:

for (NSString *eachKey in dictionary) {
    id object = dictionary[eachKey];
    NSLog(@"Object: %@ for key: %@", object, eachKey);
}

快速列举十分像 C 中的 for 循环,所以你可以用 break 来终止循环,用 continue 来进入下一层。

如果你要枚举一个有序的集合,枚举操作会按循序进行。对于 NSArray 类,枚举操作的第一个对象是下标为0的那个元素,第二个对象是下标为1的那个,以此类推。如果你需要记录当前下标的值,只要简单的计算迭代次数:

int index = 0;for (id eachObject in array) {
    NSLog(@"Object at index %i is: %@", index, eachObject);
    index++;
}
大多数集合也支持枚举对象

你可以使用 NSEnumerator 对象来枚举许多 Cocoa 和 Cocoa Touch 集合。

你也可以使用 NSArray 中的 objectEnumerator 或者 reverseObjectEnumerator 来实现快速枚举:

for (id eachObject in [array reverseObjectEnumerator]) {
    ...
}

在这个例子中,循环会倒序输出集合中的对象,所以最后一个对象将会第一个被枚举。

你也可以通过重复地调用 nextObject 方法来迭代内容:

id eachObject;
while ( (eachObject = [enumerator nextObject]) ) {
NSLog(@"Current object is: %@", eachObject);
}

在这个例子中,while 用来设置下一个循环中对象的 eachObject 变量。当集合中没有剩下的对象时,nextObject 方法会返回 nil,它被当成 false 所以循环终止。

提醒 由于在 C 程序中,等号(==)非常容易写错成赋值符号(=),当你在分支程序或循环中设定变量时,编译器会弹出警告:

if (someVariable = YES) {
    ...
}

如果你真的想让逻辑值等于等号左边的变量,你可以将表达式用括号括起来:

if ( (someVariable = YES) ) {
    ...
}

当你使用快速枚举时,你不可以改变集合。而且,使用快速枚举法比人工枚举对象要更快,因为你不用收集对象的名字。

许多集合都支持基于 Block 的枚举

你也可以使用块来枚举 NSArray,NSSet 和 NSDictionary。关于 Block 的详细信息会在下一章中介绍。

热门文章

☞ 给未来程序员的15个顶级职业建议

☞  如何向外行解释产品经理频繁更改需求为什么会令程序员烦恼?

☞ 女程序员做了个梦,各路大神惊现神级评论!

☞ 监狱里的囚犯都在学习编程,你还有什么理由拒绝呢?


左下角点击查看【作者原文】!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值