OC基础

OC基础编程技巧

正如它的名字所传达的含义,Foundation 框架是所有 iOS 和 Mac OS X 编程所使用的基本工具。要成为这两个平台上成功的开发者,必须对这套工具了如指掌。

Foundation 框架定义了数量众多的类以及协议,它们各司其职。但三种类和协议的地位更加突出,它们是最基本的部分:

  • 根类和相关的协议。 根类,即  NSObject ,还伴有一个同名的协议。它确定了所有 Objective-C 对象的基本接口和行为。同时也有一些协议,其他类可以采用这些协议来拷贝这些类的实例并对编码它们的状态。
  • 数值类。 数值类能够产生一个实例(称为数值对象),也就是将字符串、数字、日期、二进制数据等基本类型数据封装起来的面向对象包装。
  • 群体类。 群体类的一个实例(称为群体)管理着一组对象。区分不同类型的群体就要看访问它所包含的对象的方式是什么。通常,群体中包含的项目都是一系列数值对象。

群体和数值对象是 Objective-C 编程中极其重要的内容,因为它们经常被用作方法的参数和返回值。

根类和 Objective-C 对象

在类继承中,根类不从其他类继承,同时所有其他的类都最终继承自根类。 NSObject  是 Objective-C 继承中的根类。其他类都从  NSObject  继承一套基本的接口到 Objective-C 运行时体系中。这些类的实例又都是从  NSObject  继承而获得 Objective-C 最根本的特性。

但就其自身而言, NSObject  的实例做不了什么有趣的事,顶多只是个对象而已。要使用更多属性和逻辑来定制你的程序,就必须创造一个或多个继承自  NSObject  的类,或者使用已有的直接或间接继承自  NSObject  的类。

NSObject  采用了  NSObject  的协议,它声明了一些附加方法,可以被所有对象的接口使用。另外, NSObject.h (包含了  NSObject  类定义的头文件)中包含  NSCopyingNSMutableCopying  和  NSCoding  协议。当某个类采用了这些协议后,它便获得了对象拷贝和对象编码的基本对象行为。模型类(封装了应用数据并管理这些数据的实例的类)经常采用对象拷贝和对象编码协议。

NSObject  类和相关协议定义了创建对象、浏览继承链、查阅对象的特征和功能、比较对象、拷贝对象和把对象进行编码等的一系列方法。本文接下来主要讲述的就是这类任务的基本要求。

创建对象

通常,创建对象时,要先为它分配内存,然后将它初始化。虽然这是两个单独的步骤,但它们联系甚密。有些类可以通过调用它们的工厂方法来创建对象。

创建对象 – 分配内存和初始化

要为对象分配内存,对它的类发送一个  alloc  消息就能得到该类的一个“原始”(未初始化)的实例。当你为一个对象分配内存时,Objective-C 运行时会在应用的虚拟内存中为该对象预留足够大的内存空间。除了分配内存本身之外,这个环节还有另外几个用途,例如把实例变量全部设为 0 等。

为原始实例分配好内存之后,你必须将其初始化。初始化也就是将对象设置为初始状态,换句话说,就是让它的实例变量和属性为合理的值,然后再返回这个对象。初始化是为了保证返回的对象可以被使用。

你会发现在不少框架中都含有  initializers (初始器)方法,即可以初始化对象的方法。它们的形式大多类似。初始器是实例方法,方法开头为  init ,返回一个  id  类型的对象。根对象  NSObject  声明了  init  方法,所有其他的类都继承了这个方法。其他的类当然也可以声明自己的初始器,各自要有自己的关键字和参数类型。例如,NSURL 类声明了如下初始器:

- (id)initFileURLWithPath:(NSString *)path isDirectory:(BOOL)isDir

当你为一个对象分配内存并将其初始化的时候,可以将内存分配方法和初始化方法嵌套起来。如果使用上边这个初始器的话,可以写成这样:

NSURL *aURL = [[NSURL alloc] initFileURLWithPath:NSTemporaryDirectory() isDir:YES];

作为一种安全的编程习惯,你可以检查返回的对象以验证对象的创建是否正确。如果创建过程中发生了意外而导致对象创建失败,初始器将返回  nil 。虽然 Objective-C 允许对  nil  发送消息而不会产生任何副作用(比如抛出异常),但你的代码显然不可能正常工作,因为没有任何方法能够被调用。你不应该使用  alloc  返回的实例,而要使用初始器返回的实例。

通过调用类的工厂方法来创建对象

通过调用类的工厂方法也能创建一个对象。工厂方法是一种类方法,它能够分配内存、初始化,并返回实例自身。类的工厂方法属于一种便捷方法,因为它们只需一步就可以创建对象,而不是上文讲过的两步。它们的形式是这样的:

+ ( type ) className … (这里的  类名称  不包含任何前缀)

Objective-C 框架中的类有些会定义一种工厂方法,这种工厂方法实际上起到了初始器的作用。比如, NSString  就声明了如下两种方法:

- (id)initWithFormat:(NSString *)format, …;

+ (id)stringWithFormat:(NSString *)format, …;

下边的例子就是  NSString  工厂方法的一种用法:

NSString *myString = [NSString stringWithFormat:@"Customer: %@", self.record.customerName];

用对象的术语来思考

在运行时,每个应用都是一组互相协作的对象构成的;这些对象互相之间可以通信,以完成应用所需的工作。每个对象都有自己的角色,至少要对一件事 负责,并且至少连接一个其他对象。(孤立的对象毫无价值。)如下图所示,对象所组成的网络中既有框架对象也有应用程序对象。应用程序对象时自定义的子类的 实例,一般继承自某个父类框架。这些对象组成的网络一般被成为对象图。

app_as_object_network

你需要创建这些连接,或者关系,在各个对象之间进行引用。引用的语言形式有很多,其中有实例变量、全局变量,甚至包括(在有限的作用域内)本地 变量。而关系,可以是一对一关系,也可以是一对多关系,可以表示出一系列从属关系的概念。这些关系就是某个对象对其他对象进行访问、沟通或者控制的手段。 被引用的对象自然也就成了消息的接收者。

应用的对象间传递的消息是让应用持续工作的重要因素。好比乐团中的演奏家一样,应用中的每个对象都有各自的角色,为应用的运行履行自己的这部分 职责。有的对象可以显示一个椭圆形的界面响应点按动作,有的会管理一些承载各种数据的数据集合,有的则控制整个应用生命周期内的各大事件。但为了完成它们 各自的任务,它们还必须能够互相交流。每个对象都要有向同一应用中别的对象发送消息的能力,也要有接收别的对象发来的消息的能力。

有些对象之间紧密成对,即互相之间直接相连,它们互发消息时是很容易的。但还有些非紧密成对的对象,即在对象图中被分隔开的对象,它们之间要进 行通信就要另想办法。Cocoa 和 Cocoa Touch 框架含有许多帮助非紧密成对的对象进行通信的功能和机制(如下图所示)。这些机制和技术都建立在一些设计模式之上(我们会在后面探讨),这样就使得应用更 加高效并且具有超强的可扩展性。

communication_loosely_coupled

管理对象图,避免内存泄漏

Objective-C 程序里的对象共同组成一张对象图:由各个对象和其他对象的关系(或引用)而形成的网络。对象之间的引用分为一对一和一对多(通过对象集合)引用。对象图十 分重要,因为它是使对象保持生命力的关键因素。编译器会检查对象图中引用的强弱,并根据需要保持对象发出或释放对象消息。

在 C 语言或 Objective-C 语言中,可以使用含有全局变量、实例变量或本地变量的结构来构造对象间的引用。这些结构各自都有自己暗含的作用域。比如,本地变量引用的一个对象的作用域 就是声明它的函数块所在的位置。同样重要的是,对象间的引用也是分强弱的。强引用会指示出自己的所有者是谁;指向别人的对象拥有被指向的对象。弱引用则是 指向别人的对象和被指向的对象之间没有从属关系。对象的生命周期由它的强引用数量多少决定。只要对象有强引用关系,它就不会被释放。

Objective-C 里的引用默认都是强引用。通常来说这很方便,让编译器管理对象的运行时生命周期,当你使用对象时它们不会被释放。但是如果粗心未作全面检查,对象间的强引 用可能会形成无限循环,如下图左边所示。这样的循环链在运行时会导致运行时不会释放任何一个对象,它们都有指向自己的强引用。继而,这样的死循环就造成了 内存泄露。

strong-ref-cycle-weak-ref

就图中的对象而言,如果你取消 A 和 B 之间的引用,则 B、C、D、E 构成的子对象图则“永远”不会从内存中释放,因为这些对象每一个都有强引用,形成了一个死循环。如果在 E 和 B 之间引入弱引用,就可以打破强引用死循环了。

为了修正强引用死循环的问题,精明的程序员会使用弱引用。运行时会持续跟踪对象的弱引用。一旦对象不再有强引用,运行时就会从释放该对象,并将所有指向该对象的引用改为  nil 。对变量来说(全局、实例和本地变量),在对象名前面加上  __weak  限定词就可以将其标记为弱引用。对于属性来说,可以使用  weak  选项。在以下这几类引用中,你应该使用弱引用:

  • 委托

    @property(weak) id delegate;

    在《设计模式》篇里,“用设计模式让应用开发流水线化”教程将向你详解委托和目标机制。

  • 未被顶级对象引用的插座变量(Outlet)

    @property(weak) IBOutlet NSString *theName;

    插座变量是对象间的一种连接(或引用),被归档在故事版文件或 nib 文件中,当应用运行并载入故事版或 nib 文件时就会恢复插座变量。故事版或 nib 文件中顶级对象的插座变量一般而言是窗口、视图、视图控制器或其他控制器等,应该为  强引用 (默认的,或未标记的)。

  • 目标

    (void)setTarget:(id __weak)target

  • 块对象中指向  self  的引用

    __block typeof(self) tmpSelf = self;
    [self methodThatTakesABlock:^ {
        [tmpSelf doSomething];
    }];

    块对象会对它捕获的变量产生强引用。如果你在块对象里使用了  self ,则会对  self  产生强引用。所以,如果  self  对块对象也有强引用(通常都会这样),就形成了强引用死循环。为了避免死循环,你需要在块对象的外面创建一个指向  self  的  (或  __block )引用,如上边的范例所示。

管理对象的可变性

可变对象是指在你创建后能够变更其状态的对象。一般来说你需要使用属性或存取方法来进行改变。不可变对象则是创建后便被封装好,状态不可改变的对象。在 Objective-C 框架中创建的大部分类的实例都是可变的,但有几种是不可变的。不可变对象具有如下优点:

  • 在使用不可变对象时,不用担心它的值会发生意外变化。
  • 对于许多类型的对象而言,不可变对象能够提升应用程序的性能。

在 Objective-C 框架中,不可变类的实例通常是封装起来的离散值或缓冲区值的集合,比如数组和字符串。这些类通常带有一个可变的衍生类,类名里多出“ Mutable ”(可变)一词。比如有一个  NSString  类(不可变)和  NSMutableString  类。需要注意的是,对于  NSNumber  或  NSDate  等封装了离散值的不可变对象,就没必要存在可变的衍生类了。

如果你需要经常改变对象的内容,那么就使用可变对象,而不使用不可变对象。如果你从框架中接收到的对象是个不可变对象,请遵照返回的类型来行事,不要尝试改变对象的内容。

创建并使用值对象

值对象是指封装了(C 语言类型的)基本数据类型值的对象,并提供一系列与该值有关的功能。值对象在对象表中代表的是标量类型。Foundation 框架为你提供了下列类,用来生成字符串、二进制数据、日期和时间、数字等值对象:

  • NSString  和  NSMutableString
  • NSData  和  NSMutableData
  • NSDate
  • NSNumber
  • NSValue

值对象在 Objective-C 编程中十分重要,因为应用会把这些对象当作方法、函数的参数和返回值进行调用。通过传递值对象,框架里的各个部分甚至不同的框架之间便能够交换数据。因为 值对象代表的是标量值,因此你可以在集合或者其他需要用到对象的地方使用它们。值对象除了有普通数据类型的相同特征和作为编程的必要成分之外,还有更大的 优势:你能够通过更加有效而且十分优雅的方式对这些封装起来的值进行操作。就拿  NSString  类来举例,它有搜索并替换字符串的方法,有写入字符串到文件或(更常用)到 URL 的方法,还有构建文件系统路径的方法等。

在有些场合中,你可能觉得使用基本数据类型更加有效和直接,例如  int (整数型)、 float (浮点型)等等。举个具体的例子就是在计算某个值的时候。所以  NSNumber  和  NSValue  对象很少被当作框架中方法的参数和返回值。然而,需要注意到许多框架会声明自己的数值数据类型,并把这些数据类型当作参数和返回值进行传递和调用,比如  NSInteger  和  CGFloat 。你需要在合适的场合使用这些框架定义的数据类型,这样能够帮助你把代码提炼出来,远离底层平台。

使用值对象的基本方法

创建值对象的基本模式是:为你的代码或框架代码利用基本数据类型值创建一个值对象(可能稍后就将其作为方法的参数传递出去)。在你的代码中,你稍后就会访问对象中封装的数据了。用  NSNumber  类来举例再合适不过了:

int n = 5; // 基本数据类型的赋值
NSNumber *numberObject = [NSNumber numberWithInt:n]; // 利用基本数据类型创建一个值对象
int y = [numberObject intValue]; // 从值对象中获得封装后的数值(y == n)

多数“值”类会声明一个初始器以及用来创建实例的工厂方法。有些类——比如  NSString  和  NSData  不仅提供了初始器,还有利用存储在本地、远程文件甚至内存中的数据来创建实例的工厂方法。这些类还提供了一些补充方法,可以将字符串和二进制数据写入某个文件或 URL 制定的位置中。下边的范例代码演示了  initWithContentsOfURL:  方法利用一个 URL 对象中制定的文件的内容创建了一个  NSData  对象;在使用完数据之后,代码将数据对象写回文件系统中:

NSURL *theURL = // 利用字符串路径创建文件 URL 的代码…
NSData *theData = [[NSData alloc] initWithContentsOfURL:theURL];
// 使用得到的数据…
[theData writeToURL:theURL atomically:YES];

大多数值类除了能够创建值对象并让你访问封装好的值以外,还提供一系列简单的操作比如比较对象等。

字符串

作为 C 语言的超集,Objective-C 关于字符串的用法和 C 语言一样。换句话说,单个字母用单括号包裹,字符串用双括号。不过,Objective-C 框架一般而言不会使用 C 风格的字符串,而是使用  NSString  对象。

在《你的第一个 iOS 应用》教程中,在编写  HelloWorld  应用时你曾创建了一个格式化的字符串:

NSString *greeting = [[NSString alloc] initWithFormat:@”Hello, %@!”, nameString];

NSString  类为字符串提供了一个对象包裹,因此自带有不定长字符串存储的内存管理功能、支持众多字符编码(尤其是 Unicode 编码)、以及  printf  风格的格式化语法。因为你会经常用到字符串,因此 Objective-C 提供了利用常量创建  NSString  对象的快捷形式。要使用这种快捷形式,只需在常规的双引号包裹的字符串前边加上  @  符号,像下面的范例中这样:

// 创建字符串“My String”并带上一个换行符
NSString *myString = @”My String\n”;
// 创建一个格式化字符串“1 String”
NSString *anotherString = [NSString stringWithFormat:@"%d %@", 1, @"String"];
// 利用一个 C 语言字符串创建 Objective-C 字符串
NSString *fromCString = [NSString stringWithCString:"A C string" encoding:NSASCIIStringEncoding];

时间和日期

NSDate  对象和其他的值对象不同,因为它在根本上是时间而不是基本数据类型。日期对象利用参考时间,按秒封装了一个间隔值。参考时间就是 GMT 2001 年 1 月 1 日的第一个实例。

光是  NSDate  的实例自身可能用处还不是很大。它确实能代表某个时刻,但没有日历、时区和个别地区的时间约定等这些上下文,并无太大意义。幸好 Foundation 类提供了这些概念的实体:

  • NSCalendar  和  NSDateComponents :你可以将日期和日历联系起来,包括由此引申出的时间单位例如年、月、小时、一周中的某一天等。你还可以进行日期的计算。
  • NSTimeZone :当日期和时间必须反映出某个地区的时区时,你可以将时区对象和日历关联起来。
  • NSLocale :本地化对象,里面封装了和时间有关的文化和语言格式的规约。

下面的代码段展示了如何使用  NSDate  对象配合上述这些对象来获取你需要的信息(本例中,当前时间的打印格式为小时,分钟,秒)。请参考代码段下边对应的数字项后边的注释:

NSDate *now = [NSDate date]; // 1
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; // 2
[calendar setTimeZone:[NSTimeZone systemTimeZone]]; // 3
NSDateComponents *dc = [calendar components:(NSHourCalendarUnit|NSMinuteCalendarUnit|
    NSSecondCalendarUnit) fromDate:now];  // 4
NSLog(@”The time is %d:%d:%d”, [dc hour], [dc minute], [dc second]); // 5

  1. 创建一个表示当前时间的日期对象。
  2. 创建一个表示公历的对象。
  3. 用代表系统偏好设置中设定的时区的对象,设置日历对象的时区。
  4. 调用日历对象的  components:fromDate:  方法,将第一步里创建的日期对象作为参数传递。调用此方法后会返回一个包含了时间对象的小时、分钟和秒元素的对象。
  5. 在控制台打印出当前的时、分、秒。

虽然这个范例最终将结果打印出来,但更加推荐的使用方式是利用日期格式化器( NSDateFormatter  类的实例)在应用的界面上显示日期信息。在进行日期计算时一定要选用正确的类和方法;不要对时、分、秒、日等数值单位进行硬编码。

创建并使用群体

群体也是一种对象,它能够以特定方式存储其他对象并允许客户访问那些对象。你通常会将群体当作方法和函数的参数进行传递,也常常从方法和函数的返回值获得一个群体。群体往往包含值对象,但其实它们可以包含任何类型的对象。大部分群体对它们所包含的对象会产生强引用。

Foundation 框架种有好几种群体,其中三种在 Cocoa 和 Cocoa Touch 编程中极其重要:数组、字典和集合。这些群体的类同样分别有不可变与可变的形式。可变群体能够添加和移除对象,不可变群体只能含有它们创建时所包含的对 象。所有群体都可以进行枚举,也就是轮流检查所包含的每个对象。

不同类型的群体会以各自不同的方式组织它们所包含的对象:

  • NSArray  和  NSMutableArray :数组是按顺序存储的一系列对象。你可以通过某个对象的位置序号来找到它(也就是它的索引)。数组中的第一个对象索引为 0(数字零)。
  • NSDictionary  和  NSMutableDictionary :字典将条目以“键值对(Key-Value)”的形式存储在一起。键是唯一标识符,通常是字符串;值就是你想要存储的对象本身。你可以通过键来直接访问它对应的对象。
  • NSSet  和  NSMutableSet :集合里的对象是无序存储的,并且每个对象只能出现一次。通常要访问集合里的某个或某几个对象时,你必须使用筛选或对对象进行判断等方式。

collections

由于它们的存储、访问和性能各有不同,在不同的场合也就各有利弊。

在数组中以特定顺序存储对象

数组中的对象是按顺序存储的。因此,当顺序比较重要时你就可以选择数组。举个例子,许多应用都采用数组来存储表格视图中的内容或者菜单中的项目;索引值为 0 的对象代表第一排,索引值 1 上的对象对应第二排,以此类推。访问数组中对象的速度比访问集合的速度稍慢。

NSArray  类有多个初始器和类工厂方法用来创建和初始化数组,其中有几个尤其常用。你可以利用一系列对象来创建数组,使用  arrayWithObjects:count:  和  arrayWithObjects:  方法(及其对应的初始器)即可。前边一个方法的第二个参数可以用来限制第一个参数中的对象个数;后面的方法中你可以使用  nil  来中止一系列用半角逗号分隔的对象。

// 创建一个含有字符串对象的静态数组
NSString *objs[3] = {@”One”, @”Two”, @”Three”};
// 用该静态对象创建一个新数组对象
NSArray *arrayOne = [NSArray arrayWithObjects:&(*objs) count:3];
// 创建一个用 nil 结尾的对象列表的数组
NSArray *arrayTwo = [[NSArray alloc] initWithObjects:@”One”, @”Two”, @”Three”, nil];

在创建可变数组时,你可以使用  arrayWithCapacity: (或  initWithCapacity: )方法来创建此数组。容量参数只是作为期待数组大小的预设值,能够让数组在运行时更加高效。也就是说,数组的实际大小可以超过所指定的容量。

一般情况下,要通过索引位置(从 0 起始)访问数组中的对象时需要调用  objectAtIndex:  这个方法:

NSString *theString = [arrayTwo objectAtIndex:1]; // 返回数组中的第二个对象

NSArray  还有其他方法,你可以访问数组中的对象,也可以访问它们的索引。比如  lastObjectfirstObjectCommonWithArray:  和  indexOfObjectPassingTest:  方法。

数组的另一个重要功能是对所包含的每个对象均进行操作,这个过程叫做枚举。你通常会枚举某个数组,以此判断某个或某些对象是否符合某个值或者条 件,如果条件成立则可以进一步进行操作。共有三种枚举方式可供选用:快速枚举,块对象枚举,或者使用 NSEnumerator 对象。快速枚举正如其名 称所示,一般而言在获取数组中的对象时比其他枚举方式更快。快速枚举有其特定的语法:

for   (type variable   in   array)   { /* 规定   variable ,并执行所需的操作 */ }

比如此例:

NSArray *myArray = // 获取数组
for (NSString *cityName in myArray) {
    if ([cityName isEqualToString:@"Cupertino"]) {
        NSLog(@”We’re near the mothership!”);
        break;
    }
}

有几种  NSArray  方法是通过块对象进行枚举的,最简单的一个是  enumerateObjectsUsingBlock: 。块对象有三个参数:当前对象,它的索引值,以及一个布尔值,如果它为  YES  则枚举结束。块对象中的代码效果和花括号里的快速枚举效果完全一样:

NSArray *myArray = // 获取数组
[myArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    if ([(NSString *)obj isEqualToString:@"Cupertino"]) {
        NSLog(@”We’re near the mothership!”);
        *stop = YES;
    }
}];

NSArray  还有数组排序、搜索、对数组中每个对象起作用等的方法。

若要往可变数组中添加对象,则要调用  addObject:  方法;新增的对象会被放置在数组末尾。你也可以使用  insertObject:atIndex:  将对象放在数组中的某特定位置。通过调用  removeObject:  或者  removeObjectAtIndex:  方法就可以将对象从数组中移除了。

用字典存储键值对

利用字典可以将对象以键值对的形式存储在群体中,键值对是指一个标识符(键)与一个对象(值)组成的对子。字典是无序群体,因为键值对可以以任何顺序存储。虽然键可以是任意形式,但最好是能够描述值的字符串,比如  NSFileModificationDate  或  UIApplicationStatusBarFrameUserInfoKey (都是字符串常量)。当它们是公有键时,用字典在任意类型的对象之间传递信息再好不过了。

通过它的初始器和类工厂方法, NSDictionary  类有许多创建字典的方式,但其中两个是最为常用的: dictionaryWithObjects:forKeys:  和  dictionaryWithObjectsAndKeys: (或者它们对应的初始器)。前一个方法中你需要传入一个对象数组和键数组;键和值要在位置上一一对应。后面一个方法中你需要指定第一个对象值和它的键、第二个对象值和它的键、第三个、第四个,以此类推;用  nil  便可以结束这个对象系列。

// 首先创建一个键的数组以及一个值的补充数组
NSArray *keyArray = [NSArray arrayWithObjects:@"IssueDate", @"IssueName", @"IssueIcon", nil];
NSArray *valueArray = [NSArray arrayWithObjects:[NSDate date], @”Numerology Today”,
    self.currentIssueIcon, nil];
// 创建字典,将键数组和值数组传入
NSDictionary *dictionaryOne = [NSDictionary dictionaryWithObjects:valueArray forKeys:keyArray];
// 用值、键轮流的方式创建数组,用 nil 来结束本字典
NSDictionary *dictionaryTwo = [[NSDictionary alloc] initWithObjectsAndKeys:[NSDate date],
    @”IssueDate”, @”Numerology Today”, @”IssueName”, self.currentIssueIcon, @”IssueIcon”, nil];

要访问字典中的对象值,需要调用  objectForKey:  方法,并在参数中指定一个键。

NSDate *date = [dictionaryTwo objectForKey:@"IssueDate"];

你可以向可变字典中通过调用  setObject:forKey:  添加条目,也可以用  removeObjectForKey:  删除条目,还可以用  setObject:forKey:  来替换任何给定键所对应的值。这些方法运行速度都很快。

用集合存储无序对象

集合与数组相似也是对象群体,但集合中的条目是无序存储的。你无法通过索引或者键来访问集合里的对象,而是随机访问( anyObject ),通过枚举群体或者使用筛选器、测试等方式查找对象。

虽然在 Objective-C 中集合对象不像字典和数组那么常用,它们仍然是某些技术中非常重要的群体类型。在 Core Data(一种数据管理技术)中,当你声明一个一对多关系的属性时,属性类型就应该是  NSSet  或者  NSOrderedSet 。集合在 UIKit 框架中的原生触摸事件处理中也是非常重要的,比如:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *theTouch = [touches anyObject];
    // 处理代码……
}

有序集合是集合定义之外的一个特例。在有序集合中,条目的顺序十分重要。测试某个条目是否存在时,有序集合比数组的速度更快。

在运行时检验对象能力

内省(Introspection)是 Objective-C 中  NSObject  类的一个强大而实用的特性,可以让你在运行时获知关于对象的一些信息。这样,你就能避免一些错误,比如将消息发送给一个不认识它的对象,或者以为某个对象继承自另一个对象,实际上却不是。

在运行时,对象可以传达关于它自己的三种重要类型的信息。

  • 它是否是某个类或子类的实例
  • 它是否能响应某条消息
  • 它是否遵守某个协议

探究对象是否是某个类或其子类的实例

这样做的方式是对对象调用  isKindOfClass:  方法:

static int sum = 0;
for (id item in myArray) {
    if ([item isKindOfClass:[NSNumber class]]) {
        int i = (int)[item intValue];
        sum += i;
    }
}

isKindOfClass:  方法需要一个  Class  类型的对象作为参数;要获得这个对象,在类符号上调用  class  方法便可。检查此方法返回的布尔值并进行下一步操作。

NSObject  还声明了其他用来探究对象继承信息的方法。比如  isMemberOfClass:  方法会告诉你对象是否是某个指定类的实例,而  isKindOfClass:  会告诉你对象是否是某个类或其子类的成员。

探究对象是否能够响应某个消息

这样做的方法是对对象调用  respondsToSelector:  方法:

if ([item respondsToSelector:@selector(setState:)]) {
    [item setState:[self.arcView.font isBold] ? NSOnState : NSOffState];
}

respondsToSelector:  方法需要一个选择器作为参数。选择器是 Objective-C 的一个数据类型,可以在运行时标识某个方法;利用  @selector  编译器指令可以指定该选择器。在你的代码中,检查该方法返回的布尔值并进行下一步操作。

为了标识要发送给对象的消息,通常是调用  respondsToSelector:  方法,这比检测类的类型要更有用。比如,某个类的最新版本中可能实现了一个旧版本中不存在的方法。

探究对象是否遵守某个协议

这样做的方法是对对象调用  conformsToProtocol:  方法:

- (void) setDelegate:(id __weak) obj {
    NSParameterAssert([obj conformsToProtocol:
        @protocol(SubviewTableViewControllerDataSourceProtocol)]);
    delegate = obj;
}

conformsToProtocol:  方法需要协议的运行时标识符作为参数;利用  @protocol  编译器指令可以指定此标识符。接下来检查该方法返回的布尔值并进行下一步操作。

比较对象

利用  isEqual:  方法可以将两个对象进行比较。接收到此消息的对象将和作为参数传入的对象进行比较;如果它们相同则此方法返回  YES 。范例:

BOOL objectsAreEqual = [obj1 isEqual:obj2];
if (objectsAreEqual) {
    // 执行某些操作…
}

注意,对象的相等并不是对象的同一。对象的同一性要用  ==  来检测两个变量是否指向同一个实例。

那么当你比较两个对象时,究竟是在比较什么内容呢?这要视具体的类而定了。根类  NSObject  使用指针相等性来作为比较的基本点。其下任何层级的子类均可将父类比较的基本点的实现按照特定类的条件进行重写,比如对象状态。举例来说,假设有个 Person(人)对象和另一个 Person 对象,如果它们的姓、名、生日属性都相同,那么就判定它们相等。

Foundation 框架的值和群体对象以  isEqualToType:  的形式声明比较方法,这里的  Type  是去掉“NS”前缀的类名称,例如  isEqualToString:  和  isEqualToDictionary: 。比较方法可以用来确定传入的对象是否适合于某个给定的类型,以便接下来进行其他形式的比较。

拷贝对象

要拷贝某个对象,对其调用  copy  方法即可:

NSArray *myArray = [yourArray copy];

作为被拷贝的对象,接收消息的对象的类必须遵守  NSCopying  协议。要让对象能够被拷贝,你必须采用这个协议的  copy  方法并实现它。

当你需要使用程序另一个地方的对象并想要完全保持对象的各项状态的话,就需要拷贝这个对象。

拷贝行为是根据类的不同而各自区别的,而且还依赖于个别实例的特性。大多数类实现了深拷贝,它会复制实例中的所有实例变量和属性;有些类则实现浅拷贝,它只会复制实例变量和属性的引用。

拥有可变与不可变两种变体的类还会声明一个  mutableCopy  方法,用来创建对象的可变拷贝。比如,如果对一个  NSString  对象调用  mutableCopy  方法,你会得到一个  NSMutableString  实例。

你可能感兴趣的:(OC基础)