Objective-C 是一门面向对象的语言, 是C语言的超集, 我们可以在Objective-C中使用任何标准C中的值类型(scalar types), 如 int, float, char. 除此之外, 在Cocoa 和 Cocoa Touch应用中还提供了一些额外的值类型数据, 如NSInteger, NSUInteger 和CGFloat, 它们在不同的架构上拥有着不同的定义.
当我们不需要使用对象类型的特性时, 我们可以选择使用值类型的数据. 比如为了NSString类提供的特性与功能, 我们常常使用NSString类的实例来表示字符串, 而数值类型的数据经常被存储在值类型的本地变量或属性中.
在Objective-C中是可以声明C语言风格的数组的, 但是我们会发现, Cocoa 和 Cocoa Touch应用中的集合经常使用NSArray或者NSDictionary来表示. 这些类只能用来存储Objective-C对象, 这使得如果我们想要将值存储进这些集合中时, 需要先将值类型的数据封装成NSValue,NSNumber,NSString等Objective-C的实例对象.
前面的文章中我们多次使用了NSString类以及它的初始化方法和工厂方法,并使用了@字面量语法创建字符串. 这篇文章中我们将会讲述如何通过方法或者字面量语法创建NSValue和NSNumber的对象.
在Objective-C中 使用 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
我们也可以像这样把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
点语法只是一个getter/setter方法的语法糖, 所以上述例子中的每个操作都等价于先调用getter方法取值后再执行操作符运算, 然后调用setter方法为属性赋值.
Objective-C中定义的基本类型
Objective-C中定义了BOOL值类型使用YES或者NO用于存储布尔类型数据. YES在逻辑上和true和1等价, NO等价于false和0.
Cocoa和Cocoa Touch中的很多方法参数接收一些特殊的值类型, 比如NSInteger或者CGFloat.
比如, NSTableViewDataSouce和UITableViewDataSource协议中的方法 :
@protocol NSTableViewDataSource <NSObject>
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView;
...
@end
这些类型(比如NSInteger和NSUInteger), 在不同的处理器架构上拥有不同的定义. 在32位环境上, 它们分别代表有符号/无符号32位整数, 在64位环境上,它们分别代表有符号/无符号64位整数.
当代码可能跨平台或者在不同环境上使用时(比如与其它framework进行数据交互), 最好使用这些针对不同平台的值类型.
对于局部变量, 比如一个循环中的计数器, 如果我们可以保证循环次数在该类型的数值范围内, 使用基本的C类型是没有问题的.
一些Cocoa和Cocoa Touch的API中使用了C结构体来保存他们的值, 比如NSRange :
NSString *mainString = @"This is a long string";
NSRange substringRange = [mainString rangeOfString:@"long"];
NSRange结构体保存了位置和长度信息. 在上面的例子中, substringRange 中会保存{10 , 4}的一个结构体. 10 代表@"long" 的开始位置下标, 4表示@long的字符串长度.
类似地, 当我们需要使用Quartz自定义绘制方法时, 就会使用到基于CGFloat类型的结构体数据类型, 比如OS X中的NSPoint和NSSize, iOS中的CGPoint和CGSize. CGFloat同样在不同的处理器架构上拥有不同的定义. 更多关于Quartz 2D 绘制引擎的信息, 可以参考官方文档Quartz 2D Programming Guide.
使用Objective-C对象来表示基本数据类型
当我们使用Objective-C的集合类型(NSArray/NSDictionary等)时, 就需要使用对象来表示值类型.
字符串 - NSString
使用NSString 表示字符串, 比如Hello World. 有很多种方式可以创建NSString对象: 基本的alloc+init, 类工厂方法或者字面值语法:
NSString *firstString = [[NSString alloc] initWithCString:"Hello World!"
encoding:NSUTF8StringEncoding];
NSString *secondString = [NSString stringWithCString:"Hello World!"
encoding:NSUTF8StringEncoding];
NSString *thirdString = @"Hello World!";
上述例子都同样创建了一个代表着Hello World的字符串对象.
基本的NSString类是不可变(immutable)的, 它们的值在创建的时候确定下来, 而且之后不可改变. 如果我们需要一个代表不同值的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 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 及其unsigned版本, 还有Objective-C的 Boolean 类型 - BOOL.
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'];
同样, 也可以通过字面量语法创建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类实例对象后 可用通过如下的访问器方法, 获取到对应的值:
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 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类都是不可变的, 并且没有可变版本的子类. 如果我们需要一个不同的值, 创建一个新实例即可.
Note: NSNumber实际上是一个类簇(class cluster). 这意味着当我们在运行时创建了它的实例时, 我们获取到的是它的值对应的具体的子类实例. 但是使用过程中把这个实例当成NSNumber的实例就好.
其它值类型 - NSValue
NSNumber 类本身是NSValue类的子类, 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的地址.
Objective-C中的集合类型
大多数Objective-C中的集合类型都是对象类型. 虽然可以使用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类都是不可变的, 它们也都有一个对应的可变版本类.
对于Objective-C集合类的详细介绍, 参考Collections Programming Topics.
使用NSArray管理有序集合
NSArray用来表示有序的对象集合. 唯一的要求就是它其中的每一个元素都要是Objective-C对象, 并没有要求NSArray中的元素必须是相同类的实例.
数组中表示元素顺序的数组下标从0开始:
创建数组
可以通过初始化/类工厂/字面量语法创建数组.
+ (id)arrayWithObject:(id)anObject;
+ (id)arrayWithObjects:(id)firstObject, ...;
- (id)initWithObjects:(id)firstObject, ...;
arrayWithObjects: 和initWithObjects:方法接受一个以nil结尾,不定量数量的参数.
NSArray *someArray =
[NSArray arrayWithObjects:someObject, someString, someNumber, someValue, nil];
这个例子中someObject的下标为0, someValue的下标为3.
如果我们不小心使用了一个nil的值, 可能会不小心截断数组:
id firstObject = @"someString";
id secondObject = nil;
id thirdObject = @"anotherString";
NSArray *someArray =
[NSArray arrayWithObjects:firstObject, secondObject, thirdObject, nil];
在这个例子中, someArray 只会存储firstObject. 原因在于secondObject为nil,会认为在这里数组就结束了.
还使用字面量语法创建数组:
NSArray *someArray = @[firstObject, secondObject, thirdObject];
在这个语法中, 使用nil 虽然不会截断数组, 但是nil在这个语法中是无效值, 如果尝试将nil作为元素加入数组中, 会在运行时报错:
id firstObject = @"someString";
id secondObject = nil;
NSArray *someArray = @[firstObject, secondObject];
// exception: "attempt to insert nil object"
如果我们确实需要在集合中插入nil值, 我们应当使用NSNull单例类, 后文会提及.
使用数组中的查询方法
我们创建好一个数组后, 就可以通过它查询数组的信息, 比如数组中的对象个数, 或者它是否包含特定的对象:
NSUInteger numberOfItems = [someArray count];
if ([someArray containsObject:someString]) {
...
}
我们也可以通过下标去获取元素. 但是需要注意的是, 如果下标无效, 在运行时会报越界异常, 因此我们需要先检查数组中对象的数量:
if ([someArray count] > 0) {
NSLog(@"First item is: %@", [someArray objectAtIndex: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];
此时修改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".
使用NSSet管理无序集合
NSSet和数组很相似, 但是它维护的是一个没有顺序的, 不包含重复元素的集合 :
由于NSSet不管理顺序, 因此在某些时候和数组相比性能会更加优秀.
同样NSSet是不可变的, 它的内容必须在创建时指定, 可以通过初始化/类工厂方法创建:
NSSet *simpleSet =
[NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil];
和NSArray类似, 上面的方法也是以nil 判断集合结束的. NSSet的可变版本为NSMutableSet.
即使多次向NSSet中多次添加同一个对象, NSSet只会保留一个:
NSNumber *number = @42;
NSSet *numberSet =
[NSSet setWithObjects:number, number, number, number, nil];
// numberSet only contains one object
更多关于NSSet 和NSMutableSet的信息, 详见Sets: Unordered Collections of Objects.
使用NSDictionary管理键值对(Key-Value Pairs)
NSDictionary不是简单地管理有序或者无序集合, 它根据给定的key存储对象, 也可以通过key来获取对应的对象. 一般来说最好通过字符串对象作为字典的key :
Note: 使用其它类型的对象作为key也是可能的, 但是需要注意的是每一个key 在NSDictionary使用时 都会被copy , 因此key类型的对象必须支持NSCopying协议. 如果我们期望使用Key-Value Coding , 详见Key-Value Coding Programming Guide, 我们就必须使用字符串作为NSDictionary的key.
创建字典(NSDictionary)
可以通过初始化/类工厂方法创建字典:
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
someObject, @"anObject",
@"Hello, World!", @"helloString",
@42, @"magicNumber",
someValue, @"aValue",
nil];
对于dictionaryWithObjectsAndKeys:和initWithObjectsAndKeys:方法, 每个对象在key之前指定, 并且需要以nil结束.
字面值语法创建:
NSDictionary *dictionary = @{
@"anObject" : someObject,
@"helloString" : @"Hello, World!",
@"magicNumber" : @42,
@"aValue" : someValue
};
注意: 字面值语法创建字典, key在前, 值在后, 并不以nil结尾.
查询字典
可以通过字典中的key值检索对象值 :
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. 由于nil 在Objective-C语言中代表"没有对象". 如果需要在集合中表示"没有对象", 可以使用NSNull
NSArray *array = @[ @"string", @42, [NSNull null] ];
NSNull是一个单例类(Singleton). 因此NSNull 的null 方法总会返回同一实例. 这就意味着我们可以检查数组中的对象是否和[NSNull null] 相等.
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中的所有对象都是Property List所支持的类型(NSArray, NSDictionary, NSString, NSData, NSDate, NSNumber), 就可以从磁盘中直接读取出整个对象的层次结构:
NSURL *fileURL = ...
NSArray *array = [NSArray arrayWithContentsOfURL:fileURL];
if (!array) {
// an error occurred...
}
更多 Property List 相关, 请参考Property List Programming Guide.
如果需要持久化存储非Property List类型的对象, 我们可以使用归档(Archiver), 比如 NSKeyedArchiver, 创建一个对象集合的归档.
创建归档唯一的需求就是集合中的每个对象都必须支持NSCoding协议: 每个对象必须知道如何将自己归档(encodeWithCoder:), 并知道如何从归档中解档(initWithCoder:).
NSArray, NSSet, NSDictionary 以及它们的可变版本, 都是支持NSCoding的.
另外, 如果我们使用了Interface Builder来对视图和window布局, 它对应的nib类型的文件, 其实就是我们创建的视图层次的归档文件. 在运行时, 这个文件将被解档并使用.
更多归档解档相关, 请参考Archives and Serializations Programming Guide.
遍历
Objective-C中可以使用C的for循环遍历集合:
int count = [array count];
for (int index = 0; index < count; index++) {
id eachObject = [array objectAtIndex:index];
...
}
除了使用C的for循环外, Objective-C, Cocoa, Cocoa Touch 还提供了许多遍历集合内容的方法.
Objective-C建议使用除了C的for 循环之外的遍历方法.
快速遍历 (Fast Enumeration)
包含NSArray, NSSet, NSDictionary在内的很多集合类都遵循了NSFastEnumeration协议.
因此我们可以使用Objective-C的语言级别的特性 - 快速遍历(fast enumeration).
在数组中使用快速遍历的语法如下:
for (<Type> <variable> in <collection>) {
...
}
比如, 我们可以这样使用:
for (id eachObject in array) {
NSLog(@"Object: %@", eachObject);
}
eachObject变量在循环中自动设置为每次循环中的当前对象.
如果在字典中使用快速遍历, 我们遍历的是字典中的key:
for (NSString *eachKey in dictionary) {
id object = dictionary[eachKey];
NSLog(@"Object: %@ for key: %@", object, eachKey);
}
快速遍历的行为和C的for循环很相似, 我们同样可以使用break 关键字中断迭代, 也可以使用continue 跳出本次循环, 执行下次循环.
当我们遍历有序集合时, 遍历是按照顺序执行的. 如果我们需要存储index, 可以在遍历时使用变量记录它:
int index = 0;
for (id eachObject in array) {
NSLog(@"Object at index %i is: %@", index, eachObject);
index++;
}
在快速遍历时, 即使集合是可变的, 我们也不能改变正在遍历的集合. 如果我们尝试在循环中为正在遍历的集合删除或者添加对象, 会在运行时抛出异常.
使用NSEnumerator 遍历
大多数Cocoa和Cocoa Touch集合还提供了另外一种遍历方式: 使用NSEnumerator对象.
我们可以从NSArray的实例对象中获取objectEnumerator 或者reverseObjectEnumerator.
也可以结合NSEnumerator与快速遍历使用:
for (id eachObject in [array reverseObjectEnumerator]) {
...
}
在上面的例子中, 将会按照相反的顺序遍历数组.
也可以通过反复调用enumerator的nextObject 方法遍历集合 :
id eachObject;
while ( (eachObject = [enumerator nextObject]) ) {
NSLog(@"Current object is: %@", eachObject);
}
在这个例子中, 在while循环中不断地为eachObject 设置[enumerator nextObject]. 当全部遍历结束后, nextObject 会返回nil, 从而结束循环.
Note: 由于很容易误用C的赋值运算符(=)为相等运算符 (==), 因此在条件分支或者循环中像这样为变量赋值时, 编译器会给出警告:
if (someVariable = YES) {
...
}
但如果我们真的是想要使用赋值运算符, 我们可以使用括号()将赋值运算包起来:
if ( (someVariable = YES) ) {
...
}
和快速遍历一样, 遍历时不可以改变遍历中的集合.
使用Block遍历
NSArray, NSSet 和NSDictionary也可以使用Block进行遍历. 在下篇文章中, 我们将详细讲述Block.