Objective-C - 值类型和集合类型

1,094 阅读16分钟

Objective-C 是一门面向对象的语言, 是C语言的超集, 我们可以在Objective-C中使用任何标准C中的值类型(scalar types), 如 int, float, char. 除此之外, 在Cocoa 和 Cocoa Touch应用中还提供了一些额外的值类型数据, 如NSInteger, NSUIntegerCGFloat, 它们在不同的架构上拥有着不同的定义.

当我们不需要使用对象类型的特性时, 我们可以选择使用值类型的数据. 比如为了NSString类提供的特性与功能, 我们常常使用NSString类的实例来表示字符串, 而数值类型的数据经常被存储在值类型的本地变量或属性中.

在Objective-C中是可以声明C语言风格的数组的, 但是我们会发现, Cocoa 和 Cocoa Touch应用中的集合经常使用NSArray或者NSDictionary来表示. 这些类只能用来存储Objective-C对象, 这使得如果我们想要将值存储进这些集合中时, 需要先将值类型的数据封装成NSValue,NSNumber,NSString等Objective-C的实例对象.

前面的文章中我们多次使用了NSString类以及它的初始化方法和工厂方法,并使用了@字面量语法创建字符串. 这篇文章中我们将会讲述如何通过方法或者字面量语法创建NSValueNSNumber的对象.

在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在逻辑上和true1等价, NO等价于false0.

Cocoa和Cocoa Touch中的很多方法参数接收一些特殊的值类型, 比如NSInteger或者CGFloat.

比如, NSTableViewDataSouceUITableViewDataSource协议中的方法 :

@protocol NSTableViewDataSource <NSObject>
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView;
...
@end

这些类型(比如NSIntegerNSUInteger), 在不同的处理器架构上拥有不同的定义. 在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中的NSPointNSSize, iOS中的CGPointCGSize. 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, NSSetNSDictionary.

这些类用来管理对象群组, 这就意味着如果我们向集合中添加的元素必须是Objective-C类实例对象, 如果我们需要像这些集合中添加一个值类型的元素, 必须先使用NSNumber或者NSValue对其进行封装.

这些集合类并不是通过在内部维护集合中元素的副本来实现的, 它们内部对内部对象进行了强引用. 因此, 只要我们向集合中添加了元素对象, 只要这个集合存在, 这个对象必然存在. 涉及到的内存管理详见Manage the Object Graph through Ownership and Responsibility.

除了存储内部元素外, 每一个Cocoa/Cocoa Touch集合类都提供了比如遍历内部元素, 获取指定对象等便捷方法.

基本的NSArray, NSSetNSDictionary类都是不可变的, 它们也都有一个对应的可变版本类.

对于Objective-C集合类的详细介绍, 参考Collections Programming Topics.

使用NSArray管理有序集合

NSArray用来表示有序的对象集合. 唯一的要求就是它其中的每一个元素都要是Objective-C对象, 并没有要求NSArray中的元素必须是相同类的实例.

数组中表示元素顺序的数组下标从0开始:

Image

创建数组

可以通过初始化/类工厂/字面量语法创建数组.

+ (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. 原因在于secondObjectnil,会认为在这里数组就结束了.

还使用字面量语法创建数组:

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和数组很相似, 但是它维护的是一个没有顺序的, 不包含重复元素的集合 :

Image

由于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

更多关于NSSetNSMutableSet的信息, 详见Sets: Unordered Collections of Objects.

使用NSDictionary管理键值对(Key-Value Pairs)

NSDictionary不是简单地管理有序或者无序集合, 它根据给定的key存储对象, 也可以通过key来获取对应的对象. 一般来说最好通过字符串对象作为字典的key :

Image

Note: 使用其它类型的对象作为key也是可能的, 但是需要注意的是每一个keyNSDictionary使用时 都会被copy , 因此key类型的对象必须支持NSCopying协议. 如果我们期望使用Key-Value Coding , 详见Key-Value Coding Programming Guide, 我们就必须使用字符串作为NSDictionarykey.

创建字典(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). 因此NSNullnull 方法总会返回同一实例. 这就意味着我们可以检查数组中的对象是否和[NSNull null] 相等.

    for (id object in array) {
        if (object == [NSNull null]) {
            NSLog(@"Found a null object");
        }
    }

持久化

使用NSArrayNSDictionary类将其内容直接写入磁盘是很容易的:

    NSURL *fileURL = ...
    NSArray *array = @[@"first", @"second", @"third"];
 
    BOOL success = [array writeToURL:fileURL atomically:YES];
    if (!success) {
        // an error occured...
    }

如果NSArrayNSDictionary中的所有对象都是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]) {
        ...
    }

在上面的例子中, 将会按照相反的顺序遍历数组.

也可以通过反复调用enumeratornextObject 方法遍历集合 :

    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, NSSetNSDictionary也可以使用Block进行遍历. 在下篇文章中, 我们将详细讲述Block.

参考资料: Values and Collections