值和集合 – Objective-C编程快速入门教程

上一章Objective-C编程快速入门教程请查看:协议的定义和使用?
虽然Objective-C是一种面向对象的编程语言,但它是C的一个超集,这意味着你可以在Objective-C代码中使用任何标准的C标量(非对象)类型,如int、float和char。在Cocoa和Cocoa Touch应用程序中也有额外的标量类型,比如NSInteger、NSUInteger和CGFloat,它们根据目标架构有不同的定义。
在不需要使用对象表示值的好处(或相关的开销)的情况下,可以使用标量类型。虽然字符串通常表示为NSString类的实例,但数值通常存储在标量局部变量或属性中。
在Objective-C中声明一个c风格的数组是可能的,但是你会发现Cocoa和Cocoa Touch应用中的集合通常使用类的实例来表示,比如NSArray或NSDictionary。这些类只能用于收集Objective-C对象,这意味着你需要创建类的实例,如NSValue NSNumber或NSString来表示值,然后才能将它们添加到集合中。
本指南的前几章经常使用NSString类及其初始化和类工厂方法,以及Objective-C @” string” 字面量,它提供了创建NSString实例的简洁语法。本章解释如何使用方法调用或通过Objective-C值字面量语法创建NSValue和NSNumber对象。
基本的C基本类型在Objective-C中是可用的每个标准的C标量变量类型在Objective-C中都是可用的:

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

以及标准的C操作符:
int someInteger = 42; someInteger++; // someInteger == 43 int anotherInteger = 64; anotherInteger--; // anotherInteger == 63 anotherInteger *= 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访问器方法获取值,然后执行操作,然后使用set访问器方法将值设置为结果。
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位环境(例如现代OS X运行时)时,它们分别是64位有符号整数和无符号整数。
如果你可能要跨API边界(内部和导出API)传递值,例如应用程序代码和框架之间的方法或函数调用中的参数或返回值,那么最好使用这些特定于平台的类型。
对于局部变量,例如循环中的计数器,如果知道值在标准限制内,则可以使用基本的C类型。
C结构可以保存原始值
一些Cocoa和Cocoa Touch API使用C结构来保存它们的值。例如,可以向string对象询问子字符串的范围,如下所示:
NSString *mainString = @"This is a long string"; NSRange substringRange = [mainString rangeOfString:@"long"];

NSRange结构包含位置和长度。在本例中,substringRange的范围为{10,4}—@“long”开头的“l”是mainString中索引10的从零开始的字符,而@“long”的长度为4个字符。
类似地,如果需要编写自定义绘图代码,则需要与Quartz交互,这需要基于CGFloat数据类型的结构,比如OS X上的NSPoint和NSSize, iOS上的CGPoint和CGSize。同样,CGFloat的定义也不同,这取决于目标体系结构。
有关Quartz 2D绘图引擎的更多信息,请参见Quartz 2D编程指南。
对象可以表示基本值如果需要将标量值表示为对象,例如在处理下一节中描述的集合类时,可以使用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!";

这些示例都有效地完成了相同的任务——创建表示提供的字符的string对象。
基本的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"

格式字符串用于从其他对象或值构建字符串
如果需要构建包含变量值的字符串,则需要使用格式字符串。这允许你使用格式说明符来指示如何插入值:
int magicNumber = ... NSString *magicString = [NSString stringWithFormat:@"The magic number is %i", magicNumber];

可用的格式说明符用字符串格式说明符描述。有关字符串的更多信息,请参阅字符串编程指南。
数字由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实例,就可以使用一个访问器方法来请求标量值:
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 = http://www.srcmini.com/[NSValue valueWithRange:substringRange];

也可以创建NSValue对象来表示自定义结构。如果你特别需要使用C结构(而不是Objective-C对象)来存储信息,如下所示:
typedef struct { int i; float f; } MyIntegerFloatStruct;

你可以创建一个NSValue实例通过提供一个指向结构的指针以及一个已编码的Objective-C类型。编译器指令用于创建正确的Objective-C类型,如下所示:
struct MyIntegerFloatStruct aStruct; aStruct.i = 42; aStruct.f = 3.14; NSValue *structValue = http://www.srcmini.com/[NSValue value:& aStruct withObjCType:@encode(MyIntegerFloatStruct)];

标准C引用操作符(& )用于为值参数提供aStruct的地址。
大多数集合都是对象虽然可以使用C数组来保存标量值的集合,甚至对象指针,但Objective-C代码中的大多数集合都是Cocoa和Cocoa Touch集合类的实例,比如NSArray、NSSet和NSDictionary。
这些类用于管理对象组,这意味着你希望添加到集合中的任何项都必须是Objective-C类的实例。如果你需要添加一个标量值,你必须首先创建一个合适的NSNumber或NSValue实例来表示它。
集合类使用强引用来跟踪它们的内容,而不是以某种方式维护每个收集对象的单独副本。这意味着,你添加到集合中的任何对象都将保持活动状态,至少在集合保持活动状态期间是如此,如《通过所有权和责任管理对象图》中所述。
除了跟踪它们的内容之外,每个Cocoa和Cocoa Touch集合类都可以轻松地执行特定的任务,比如枚举、访问特定的项,或者查找特定的对象是否是集合的一部分。
基本的NSArray、NSSet和NSDictionary类是不可变的,这意味着它们的内容是在创建时设置的。每个类还有一个可变的子类,允许你随意添加或删除对象。
有关Cocoa和Cocoa Touch中可用的不同集合类的更多信息,请参见集合编程主题。
数组是有序集合
NSArray用于表示对象的有序集合。唯一的要求是每个项目都是一个Objective-C对象-没有要求每个对象都是同一个类的实例。
为了维护数组中的顺序,每个元素都存储在一个从零开始的索引中,如图6-1所示。
图6-1一个Objective-C对象数组
值和集合 &#8211; Objective-C编程快速入门教程

文章图片
创建数组
与本章前面描述的值类一样,可以通过分配和初始化、类工厂方法或字面量语法来创建数组。
有各种不同的初始化和工厂方法,取决于对象的数量:
+ (id)arrayWithObject:(id)anObject; + (id)arrayWithObjects:(id)firstObject, ...; - (id)initWithObjects:(id)firstObject, ...;

arrayWithObjects:和initWithObjects:方法都接受一个以nil结尾的可变数量的参数,这意味着必须将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 secondObject将被解释为项列表的结束。
字面量语法
也可以使用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单例类,如用NSNull表示nil所述。
查询数组对象
一旦你创建了一个数组,你可以查询它的信息,如对象的数量,或它是否包含一个给定的项目:
NSUInteger numberOfItems = [someArray count]; if ([someArray containsObject:someString]) { ... }

你还可以在给定索引处查询项的数组。如果你试图请求一个无效的索引,你会在运行时得到一个越界的异常,所以你应该总是先检查项目的数量:
if ([someArray count] > 0) { NSLog(@"First item is: %@", [someArray objectAtIndex: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:)];

在这种情况下,包含项目将分为升,不分大小写的@” epsilon” , @” alpha” , @” beta” .。
无序集合
NSSet类似于数组,但是保持一个无序群不同的对象,如图6 – 2所示。
图6 – 2的一组对象
值和集合 &#8211; Objective-C编程快速入门教程

文章图片
因为集不维持有序,他们提供了一个性能改善阵列时测试成为成员。
基本NSSet类是不可变的,所以其内容必须指定在创建,使用分配和初始化或一个类工厂方法,像这样:
NSSet *simpleSet = [NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil];

与NSArray一样,initWithObjects:和setWithObjects:方法都采用以nil结尾的可变数量的参数。可变的NSSet子类是NSMutableSet。
设置仅存储对单个对象的一个引用,即使你多次尝试添加一个对象:
NSNumber *number = @42; NSSet *numberSet = [NSSet setWithObjects:number, number, number, number, nil]; // numberSet only contains one object

有关集合的更多信息,请参见集合:对象的无序集合。
字典集键值对
NSDictionary不是简单地维护对象的有序或无序集合,而是根据给定的键存储对象,然后可以使用这些键进行检索。
最佳实践是使用字符串对象作为字典键,如图6-3所示。
图6-3字典对象
值和集合 &#8211; Objective-C编程快速入门教程

文章图片
注意:可以使用其他对象作为键,但需要注意的是,每个键都是为字典使用而复制的,因此必须支持NSCopying。
但是,如果你希望能够使用键值编码,就像在键值编码编程指南中描述的那样,你必须为dictionary对象使用字符串键。
创建字典
你可以创建字典使用分配和初始化,或类工厂方法,像这样:
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"];

可变性
如果在创建之后需要从字典中添加或删除对象,需要使用NSMutableDictionary子类,如下所示:
[dictionary setObject:@"another string" forKey:@"secondString"]; [dictionary removeObjectForKey:@"anObject"];

用NSNull表示nil
不可能将nil添加到本节描述的集合类中,因为在Objective-C中nil的意思是“没有对象”。如果你需要在一个集合中表示“no object”,你可以使用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... }

如果每个包含的对象都是一个属性列表类型(NSArray, NSDictionary, NSString, NSData, NSDate和NSNumber),就可以从磁盘重新创建整个层次结构,就像这样:
NSURL *fileURL = ... NSArray *array = [NSArray arrayWithContentsOfURL:fileURL]; if (!array) { // an error occurred... }

有关属性列表的更多信息,请参见属性列表编程指南。
如果你需要持久化其他类型的对象,而不仅仅是上面所示的标准属性列表类,那么你可以使用archiver对象,例如NSKeyedArchiver,来创建所收集对象的存档。
创建归档的惟一要求是每个对象必须支持NSCoding协议。这意味着每个对象必须知道如何将自身编码到存档中(通过实现encodeWithCoder: method),并在从现有存档中读取时解码自身(initWithCoder: method)。
NSArray、NSSet和NSDictionary类以及它们的可变子类都支持NSCoding,这意味着你可以使用归档器持久化对象的复杂层次结构。例如,如果你使用Interface Builder来布局窗口和视图,生成的nib文件只是你可视化创建的对象层次结构的归档文件。在运行时,使用相关类将nib文件解档到对象的层次结构。
有关存档的更多信息,请参见存档和序列化编程指南。
使用最有效的集合枚举技术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关键字来中断迭代,或者继续前进到下一个元素。
如果要枚举一个有序集合,则枚举将按该顺序进行。对于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) ) { ... }

与快速枚举一样,你不能在枚举时更改集合。而且,从名称中可以看出,使用快速枚举比手动使用枚举器对象更快。
许多集合支持基于块的枚举
【值和集合 – Objective-C编程快速入门教程】还可以使用块枚举NSArray、NSSet和NSDictionary。下一章将详细讨论这些块。

    推荐阅读