上一章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 = [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 = [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对象数组
创建数组
与本章前面描述的值类一样,可以通过分配和初始化、类工厂方法或字面量语法来创建数组。
有各种不同的初始化和工厂方法,取决于对象的数量:
+ (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的一组对象
因为集不维持有序,他们提供了一个性能改善阵列时测试成为成员。
基本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字典对象
注意:可以使用其他对象作为键,但需要注意的是,每个键都是为字典使用而复制的,因此必须支持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) ) {
...
}
与快速枚举一样,你不能在枚举时更改集合。而且,从名称中可以看出,使用快速枚举比手动使用枚举器对象更快。
许多集合支持基于块的枚举
还可以使用块枚举NSArray、NSSet和NSDictionary。下一章将详细讨论这些块。
评论前必须登录!
注册