虽然Objective-C是一种面向对象的编程语言,但它是C的超集,这意味着您可以使用Objective-C代码中的任何标准C标量(非对象)类型,如int、float和char。在Cocoa和Cocoa Touch应用程序中也有额外的标量类型,如NSInteger、NSUInteger和CGFloat,它们根据目标体系结构有不同的定义。
在您不需要使用对象的好处(或相关的开销)来表示值的情况下使用标量类型。虽然字符串通常表示为NSString类的实例,但数值通常存储在标量局部变量或属性中。
可以在Objective-C中声明c样式的数组,但是您会发现在Cocoa和Cocoa触摸应用程序中的集合通常使用NSArray或NSDictionary之类的类的实例来表示。这些类只能用于收集Objective-C对象,这意味着您需要创建类的实例,比如NSValue、NSNumber或NSString,以便在将值添加到集合之前表示值。
本指南的前几章经常使用NSString类及其初始化和类工厂方法,以及Objective-C @“string”文本,它提供了创建一个NSString实例的简洁语法。本章解释了如何使用方法调用或通过Objective-C值文字语法创建NSValue和NSNumber对象。
在Objective-C中,每个标准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 accessor方法获取值,然后执行操作,然后使用set accessor方法来设置结果的值。
BOOL标量类型是在Objective-C中定义的,用于保存布尔值,该值要么是YES,要么是NO。正如你所期望的,在逻辑上, YES 等于true和1,而NO 等于false和0。
对于Cocoa和Cocoa Touch对象的方法的许多参数也使用特殊的标量数值类型,如NSInteger或CGFloat。
例如,NSTableViewDataSource和UITableViewDataSource协议(在前一章中描述)都有方法请求显示的行数:
@protocol NSTableViewDataSource
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView;
...
@end
这些类型,如NSInteger和NSUInteger,根据目标体系结构的定义不同。当为32位环境(比如iOS)构建时,它们分别是32位和无符号整数;当为64位环境(比如现代OS X运行时)构建时,它们分别是64位签名和无符号整数。
如果您可能在API边界(包括内部和导出的API)中传递值,那么最好使用这些特定于平台的类型,例如在应用程序代码和框架之间的方法或函数调用中的参数或返回值。
对于局部变量,例如循环中的计数器,如果您知道值在标准范围内,那么使用基本的C类型是可以的。
一些Cocoa和CocoaTouch 的API使用C结构来保存它们的值。例如,可以向string对象查询子字符串的范围,如下所示:
NSString *mainString = @"This is a long string";
NSRange substringRange = [mainString rangeOfString:@"long"];
一个NSRange结构包含一个位置和一个长度。在本例中,substringRange将保留一个{10,4}的范围,@“long”开头的“l”是主字符串中从零开始的索引10的字符,@“long”是长度的4个字符。
类似地,如果您需要编写自定义绘图代码,则需要与Quartz进行交互,这需要基于CGFloat数据类型的结构,比如在OS X上的NSPoint和NSSize,以及iOS上的CGPoint和CGSize。同样,根据目标架构,CGFloat的定义也不同。
有关Quartz 2D绘图机的更多信息,请参阅Quartz 2D编程指南。
如果您需要将标量值表示为对象,例如在下一节中描述的集合类,您可以使用Cocoa和Cocoa Touch提供的基本值类之一。
正如您在前面的章节中看到的,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类是不可变的,这意味着它的内容是在创建时设置的,不能稍后更改。如果需要表示不同的字符串,则必须创建一个新的string对象,如下所示:
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];
更多信息请参考字符串编程指南 String Programming Guide.
NSNumber类用于表示任何基本的C标量类型,包括char、double、float、int、long、short和未签名的每个变体,以及Objective-C Boolean类型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的实例。
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;
您可以通过提供指向结构的指针以及编码的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的地址。
尽管可以使用C数组来保存标量值的集合,或者甚至是对象指针,Objective-C代码中的大多数集合都是Cocoa和Cocoa Touch集合类的实例,比如NSArray、NSSet和NSDictionary。
这些类用于管理对象组,这意味着您希望添加到集合中的任何项都必须是Objective-C类的实例。如果需要添加标量值,则必须首先创建一个合适的NSNumber或NSValue实例来表示它。
集合类使用强引用来跟踪它们的内容,而不是以某种方式维护每个被收集对象的单独副本。这意味着,如果您添加到集合中的任何对象都将被保存,至少只要集合被保存,就像通过所有权和责任管理对象图所描述的那样。
除了跟踪它们的内容之外,每个Cocoa和Cocoa Touch集合类都使得执行某些任务变得容易,比如枚举、访问特定的项,或者查找某个特定对象是否是集合的一部分。
基本的NSArray、NSSet和NSDictionary类是不可变的,这意味着它们的内容是在创建时设置的。每个都有一个可变的子类,允许您随意添加或删除对象。
有关Cocoa和Cocoa Touch中可用的不同集合类的更多信息,请参见Collections Programming Topics.
NSArray用来表示有序的对象集合。惟一的要求是,每个条目都是Objective-C对象——对于每个对象,都不需要成为同一个类的实例。
为了维护数组中的顺序,每个元素都存储在一个从零开始的索引中,如图6-1所示。
图6-1
与本章前面描述的值类一样,您可以通过分配和初始化、类工厂方法或文字语法创建数组。
有多种不同的初始化和工厂方法可用,具体取决于对象的数量:
+ (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"
如果您确实需要在一个集合类中表示一个空值,那么您应该使用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。
还有一个使用objectAtIndex的下标语法替代方法:这就像访问一个标准的C数组中的值一样。前面的例子可以这样改写:
if ([someArray count] > 0) {
NSLog(@"First item is: %@", someArray[0]);
}
NSArray类还提供了各种方法来对其收集的对象进行排序。因为NSArray是不可变的,所以每个方法都返回一个新的数组,其中包含已排序顺序的项。
例如,您可以通过调用比较的结果对字符串进行排序:在每个字符串上,如下所示:
NSArray *unsortedStrings = @[@"gammaString", @"alphaString", @"betaString"];
NSArray *sortedStrings =
[unsortedStrings sortedArrayUsingSelector:@selector(compare:)];
虽然NSArray类本身是不可变的,但它对任何收集的对象都没有影响。如果将可变字符串添加到不可变数组,例如:
NSMutableString *mutableString = [NSMutableString stringWithString:@"Hello"];
NSArray *immutableArray = @[mutableString];
没有什么可以阻止你改变string:
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”。
NSSet类似于数组,但维护一个无序的不同对象组,如图6-2所示。
图6-2
因为集合不维护顺序,所以在测试成员身份时,它们提供了性能改进。
基本的NSSet类仍然是不可变的,所以它的内容必须在创建时指定,使用分配和初始化或类工厂方法,像这样:
NSSet *simpleSet =
[NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil];
与NSArray一样,initWithObjects:和setWithObjects:方法都采用了nil终止的、变量数量的参数。可变NSSet子类是NSMutableSet。
Set仅将一个引用存储到单个对象,即使您多次尝试添加对象:
NSNumber *number = @42;
NSSet *numberSet =
[NSSet setWithObjects:number, number, number, number, nil];
// numberSet only contains one object
Set 更多信息请参考 Sets: Unordered Collections of Objects.
而不是简单地维护一个有序的或无序的对象集合,一个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
};
注意,对于dictionary 文字语法,键在其对象之前指定,而不是nil终止。
一旦你创建了一个字典,你可以查询它对一个给定的键存储的对象,像这样:
NSNumber *storedNumber = [dictionary objectForKey:@"magicNumber"];
如果未找到对象,则objectForKey:方法将返回nil。
还有一个用于使用objectForKey的下标语法:,它看起来是这样的:
NSNumber *storedNumber = dictionary[@"magicNumber"];
如果您需要在创建后从字典中添加或删除对象,您需要使用NSMutableDictionary子类,如下所示:
[dictionary setObject:@"another string" forKey:@"secondString"];
[dictionary removeObjectForKey:@"anObject"];
不可能向本节中描述的集合类添加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...
}
如果每个包含的对象都是属性列表类型(NSArray、NSDictionary、NSString、NSData、NSDate和NSNumber),那么可以从磁盘重新创建整个层次结构,如下所示:
NSURL *fileURL = ...
NSArray *array = [NSArray arrayWithContentsOfURL:fileURL];
if (!array) {
// an error occurred...
}
有关属性列表的更多信息,请参见属性列表编程指南。Property List Programming Guide.
如果您需要持久化其他类型的对象,而不仅仅是上面所示的标准属性列表类,那么您可以使用一个archiver对象,比如NSKeyedArchiver,来创建一个收集对象的存档。
创建存档的惟一要求是每个对象都必须支持NSCoding协议。这意味着每个对象都必须知道如何将自己编码到一个存档(通过实现encodeWithCoder:方法),并在从现有存档(initWithCoder:方法)读取时进行解码。
NSArray、NSSet和NSDictionary类以及它们的可变子类都支持NSCoding,这意味着您可以使用一个archiver来持久化复杂的对象层次结构。例如,如果您使用接口构建器来布局窗口和视图,那么生成的nib文件只是您在视觉上创建的对象层次结构的一个归档。在运行时,nib文件使用相关类不被归档到对象的层次结构中。
有关存档的更多信息,请参阅归档和序列化编程指南.Archives and Serializations Programming Guide.
Objective-C和Cocoa或Cocoa Touch提供了枚举集合内容的各种方法。虽然可以使用传统的C进行循环遍历内容,但如下所示:
int count = [array count];
for (int index = 0; index
...
}
最好使用本节描述的其他技术之一。
许多集合类符合NSFastEnumeration协议,包括NSArray、NSSet和NSDictionary。这意味着您可以使用快速枚举,这是一个Objective-C语言级别的特性。
枚举数组或集合的内容的快速枚举语法如下所示:
for (
...
}
例如,您可以使用快速枚举来记录数组中每个对象的描述,例如:
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,因此您可以使用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等式运算符(==)时,使用C赋值运算符(=)是一个常见的程序员错误,如果你在一个条件分支或循环中设置一个变量,编译器会警告你:
if (someVariable = YES) {
...
}
如果您确实想要重新分配一个变量(整个赋值的逻辑值是左边的最终值),您可以通过将赋值放在括号中来表示这一点:
if ( (someVariable = YES) ) {
...
}
与快速枚举一样,您不能在枚举时更改集合。而且,正如您可能从名称中收集的那样,使用快速枚举比手动使用枚举器对象更快。
还可以使用 block 来枚举NSArray、NSSet和NSDictionary。下一章将详细讨论 block。