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.