个性化阅读
专注于IT技术分析

值和集合 – Objective-C编程快速入门教程

上一章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对象数组

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。下一章将详细讨论这些块。

赞(5)
未经允许不得转载:srcmini » 值和集合 – Objective-C编程快速入门教程

评论 抢沙发

评论前必须登录!