个人技术分享

本文翻译整理自:Concepts in Objective-C Programming ( Updated: 2012-01-09
https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010810


文章目录


一、关于Cocoa和Cocoa Touch的基本编程概念

Cocoa和Cocoa Touch框架的许多编程接口只有在您了解它们所基于的概念时才有意义。
这些概念表达了框架许多核心设计的基本原理。
了解这些概念将阐明您的software-development实践。

在这里插入图片描述


1、概览

本文档包含解释Cocoa和Cocoa Touch框架的核心概念、设计模式和机制的文章。
文章按字母顺序排列。


2、如何使用本文档

如果您从头到尾阅读本文档,您将了解有关Cocoa和Cocoa Touch应用程序开发的重要信息。
但是,大多数读者通过以下两种方式之一阅读本文档中的文章:

  • 链接到这些文章的其他文档——尤其是那些面向新手iOS和OS X开发人员的文档。
  • 在线迷你文章(当您单击带有破折号下划线的单词或短语时出现),其中包含指向文章的链接作为“权威讨论”

3、先决条件

建议具有先前的编程经验,尤其是面向对象语言。


4、另见

使用Objective-C编程 进一步讨论了本文档中涵盖的许多与语言相关的概念。


二、类集群

类集群是Foundation框架广泛使用的一种设计模式。
类集群将许多私有的具体子类分组在一个公共抽象超类下。
以这种方式分组类简化了面向对象框架的公开可见架构,而不会降低其功能丰富性。
类集群基于抽象工厂设计模式。


1、没有类集群:简单的概念但复杂的接口

为了说明类集群架构及其好处,请考虑构建一个类层次结构的问题,该层次结构定义对象来存储不同类型的数字(charintfloatdouble)。
因为不同类型的数字有许多共同的特征(例如,它们可以从一种类型转换为另一种类型,并且可以表示为字符串),它们可以由一个类来表示。
然而,它们的存储要求不同,因此用同一个类来表示它们是低效的。
考虑到这一事实,可以设计图1-1中描述的类架构来解决这个问题。

图1-1数字类的简单层次结构

在这里插入图片描述

Number是抽象超类,它在其方法中声明子类共有的操作。
但是,它不声明实例变量来存储数字。
子类声明这样的实例变量,并共享由Number声明的编程接口。

到目前为止,这种设计相对简单。
但是,如果考虑到这些基本C类型的常用修改,类层次图看起来更像图1-2。

图1-2更完整的数字类层次结构

在这里插入图片描述

简单的概念——创建一个类来保存数字值——可以很容易地发展到十几个类。
类集群架构提出了一种反映概念简单性的设计。


2、使用类集群:简单的概念和简单的接口

将类集群设计模式应用于此问题会产生图1-3中的类层次结构(私有类为灰色)。

图1-3应用于数字类的类集群架构

在这里插入图片描述

这个层次结构的用户只能看到一个公共类Number,那么如何分配适当子类的实例呢?答案在于抽象超类处理实例化的方式。


3、创建实例

抽象超类在一个类集群中必须声明创建其私有子类实例的方法。
超类有责任根据您调用的创建方法分配适当子类的对象——您不能也不能选择实例的类。

在Foundation框架中,您通常通过调用+class Name...方法或alloc...init...方法来创建对象。
以Foundation框架的NSNumber类为例,您可以发送以下消息来创建number对象:

NSNumber *aChar = [NSNumber numberWithChar:’a’];
NSNumber *anInt = [NSNumber numberWithInt:1];
NSNumber *aFloat = [NSNumber numberWithFloat:1.0];
NSNumber *aDouble = [NSNumber numberWithDouble:1.0];

您不负责释放工厂方法返回的对象。
许多类还提供标准的alloc...init...方法来创建需要您管理其释放的对象。

返回的每个对象——aCharanIntaFloataDouble——可能属于不同的私有子类(事实上确实如此)。
尽管每个对象的类成员身份是隐藏的,但它的接口是公共的,是抽象超类NSNumber声明的接口。
虽然不完全正确,但可以方便地将aCharanIntaFloataDouble对象视为NSNumber类的实例,因为它们是由NSNumber类方法创建的,并通过NSNumber声明的实例方法访问。


4、具有多个公共超类的类集群

在上面的例子中,一个抽象公共类声明了多个私有子类的接口。
这是一个最纯粹意义上的类集群。
有两个(或更多)抽象公共类来声明集群的接口也是可能的,而且通常是可取的。
这在Foundation框架中很明显,其中包括表1-1中列出的集群。

类簇 公共超类
NSData NSData
NSArray NSArray
NSDictionary NSDictionary
NSString NSString
NSMutableString

这种类型的其他集群也存在,但这些清楚地说明了两个抽象节点如何合作声明类集群的编程接口。
在每个集群中,一个公共节点声明所有集群对象都可以响应的方法,另一个节点声明仅适用于允许修改其内容的集群对象的方法。

集群接口的这种分解有助于使面向对象框架的编程接口更具表现力。
例如,想象一个对象代表一本声明此方法的书:

- (NSString *)title;

book对象可以返回它自己的实例变量,或者创建一个新的字符串对象并返回它——没关系。
从这个声明中可以清楚地看出,返回的字符串不能被修改。
任何修改返回对象的尝试都会引发编译器警告。


5、在类集群中创建子类

在类集群架构中需要在简单性和可扩展性之间进行权衡:用一些公共类代替大量私有类可以更容易地在框架中学习和使用这些类,但在任何集群中创建子类却有点困难。
然而,如果很少需要创建子类,那么集群架构显然是有益的。
在这些情况下,在Foundation框架中使用集群。

如果您发现集群不能提供程序所需的功能,那么可能需要一个子类。
例如,假设您想创建一个数组对象,其存储是基于文件的,而不是NSArray类集群中的基于内存的。
因为您正在更改类的底层存储机制,所以您必须创建一个子类。

另一方面,在某些情况下,定义一个在其中嵌入集群对象的类可能就足够了(也更容易)。
假设您的程序需要在某些数据被修改时收到警报。
在这种情况下,创建一个简单的类来包装Foundation框架定义的数据对象可能是最好的方法。
这个类的对象可以干预修改数据的消息,拦截消息,对它们进行操作,然后将它们转发到嵌入的数据对象。

总之,如果您需要管理对象的存储,请创建一个真正的子类。
否则,创建一个复合对象,将标准Foundation框架对象嵌入到您自己设计的对象中。
以下部分详细介绍了这两种方法。


真正的子类

您在类集群中创建的新类必须:

  • 是集群抽象超类的子类
  • 声明自己的存储
  • 重写超类的所有初始化方法
  • 重写超类的原始方法(如下所述)

因为集群的抽象超类是集群层次结构中唯一公开可见的节点,所以第一点很明显。
这意味着新的子类将继承集群的接口,但没有实例变量,因为抽象超类声明没有实例变量。
因此第二点:子类必须声明它需要的任何实例变量。
最后,子类必须重写它继承的任何直接访问对象实例变量的方法。
这种方法称为原始方法。

一个类的原始方法构成了它的接口的基础。
例如,NSArray类,它声明了管理对象数组的对象的接口。
在概念上,数组存储了许多数据项,每个数据项都可以通过索引访问。
NSArray通过它的两个原始方法countobjectAtIndex:表达了这个抽象概念。
以这些方法为基础,可以实现其他方法——派生方法;表1-2给出了两个派生方法的例子。

派生方法 可能的实施
lastObject 通过向数组对象发送此消息找到最后一个对象:[self objectAtIndex: ([self count] –1)]
containsObject: 通过反复向数组对象发送objectAtIndex:消息来查找对象,每次递增索引,直到数组中的所有对象都经过测试。

原始方法和派生方法之间的接口划分使创建子类变得更加容易。
您的子类必须重写继承的原语,但这样做可以确保它继承的所有派生方法都将正常运行。

原语派生的区别适用于完全初始化对象的接口。
还需要解决如何在子类中处理init...方法的问题。

通常,集群的抽象超类声明了许多init...+ className方法。
创建实例中所述,抽象类根据您选择的init...+ className方法来决定实例化哪个具体子类。
您可以考虑抽象类声明这些方法是为了子类的方便。
由于抽象类没有实例变量,因此不需要初始化方法。

您的子类应该声明它自己的init...(如果它需要初始化它的实例变量)和可能的+ className方法。
它不应该依赖于它继承的任何方法。
为了保持它在初始化链中的链接,它应该在它自己指定的初始化方法中调用它的超类指定的初始化器。
它还应该重写所有其他继承的初始化器方法,并实现它们以合理的方式运行。
(有关指定初始化器的讨论,请参阅多个初始化器和指定初始化器。)在类集群中,抽象超类的指定初始化器始终是init


真正的子类:一个例子

假设您想创建一个名为MonthArrayNSArray子类,它返回给定索引位置的月份名称。
但是,MonthArray对象实际上不会将月份名称数组存储为实例变量。
相反,返回给定索引位置的名称的方法(objectAtIndex:)将返回常量字符串。
因此,无论应用程序中存在多少MonthArray对象,都只会分配12个字符串对象。

MonthArray类被声明为:

#import <foundation/foundation.h>
@interface MonthArray : NSArray
{
}
 
+ monthArray;
- (unsigned)count;
- (id)objectAtIndex:(unsigned)index;
 
@end

请注意,MonthArray类没有声明init...方法,因为它没有要初始化的实例变量。
countobjectAtIndex:方法只是重写了继承的原始方法,如上所述。

MonthArray类的MonthArray如下所示:

#import "MonthArray.h"
 
@implementation MonthArray
 
static MonthArray *sharedMonthArray = nil;
static NSString *months[] = { @"January", @"February", @"March",
    @"April", @"May", @"June", @"July", @"August", @"September",
    @"October", @"November", @"December" };
 
+ monthArray
{
    if (!sharedMonthArray) {
        sharedMonthArray = [[MonthArray alloc] init];
    }
    return sharedMonthArray;
}
 
- (unsigned)count
{
 return 12;
}
 
- objectAtIndex:(unsigned)index
{
    if (index >= [self count])
        [NSException raise:NSRangeException format:@"***%s: index
            (%d) beyond bounds (%d)", sel_getName(_cmd), index,
            [self count] - 1];
    else
        return months[index];
}
 
@end

因为MonthArray重写了继承的原始方法,所以它继承的派生方法将正常工作而不会被重写。
NSArraylastObjectcontainsObject:sortedArrayUsingSelector:objectEnumerator和其他方法对MonthArray对象没有问题。


复合对象

通过在您自己设计的对象中嵌入私有集群对象,您可以创建一个复合对象。
此复合对象可以依赖集群对象实现其基本功能,仅拦截复合对象希望以某种特定方式处理的消息。
此架构减少了您必须编写的代码量,并允许您利用Foundation Framework提供的经过测试的代码。
图1-4描述了此架构。

图1-4嵌入集群对象的对象

在这里插入图片描述

复合对象必须声明自己是集群抽象超类的子类。
作为子类,它必须重写超类的原始方法。
它也可以重写派生方法,但这不是必需的,因为派生方法通过原始方法工作。


例如,NSArray类的count方法;中间对象对它重写的方法的实现可以很简单:

- (unsigned)count {
    return [embeddedObject count];
}

但是,您的对象可以将用于自己目的的代码放入它重写的任何方法的实现中。


复合对象:示例

为了说明复合对象的使用,假设您需要一个可变数组对象,它在允许对数组内容进行任何修改之前根据某些验证标准测试更改。
下面的示例描述了一个名为ValidatingArray的类,其中包含一个标准可变数组对象。
ValidatingArray重写了在其超类NSArrayNSMutableArray中声明的所有原始方法。
它还声明了arrayvalidatingArrayinit方法,这些方法可用于创建和初始化实例:

#import <foundation/foundation.h>
 
@interface ValidatingArray : NSMutableArray
{
    NSMutableArray *embeddedArray;
}
 
+ validatingArray;
- init;
- (unsigned)count;
- objectAtIndex:(unsigned)index;
- (void)addObject:object;
- (void)replaceObjectAtIndex:(unsigned)index withObject:object;
- (void)removeLastObject;
- (void)insertObject:object atIndex:(unsigned)index;
- (void)removeObjectAtIndex:(unsigned)index;
 
@end


实现文件显示了如何在ValidatingArray类的init方法中创建嵌入对象并将其分配给嵌入数组变量。
仅访问数组但不修改其内容的消息将被中继到嵌入对象。
可能更改内容的消息将被仔细检查(此处为伪代码),并且仅在它们通过假设的验证测试时才被中继。

#import "ValidatingArray.h"
 
@implementation ValidatingArray
 
- init
{
    self = [super init];
    if (self) {
        embeddedArray = [[NSMutableArray allocWithZone:[self zone]] init];
    }
    return self;
}
 
+ validatingArray
{
    return [[[self alloc] init] autorelease];
}
 
- (unsigned)count
{
    return [embeddedArray count];
}
 
- objectAtIndex:(unsigned)index
{
    return [embeddedArray objectAtIndex:index];
}
 
- (void)addObject:object
{
    if (/* modification is valid */) {
        [embeddedArray addObject:object];
    }
}
 
- (void)replaceObjectAtIndex:(unsigned)index withObject:object;
{
    if (/* modification is valid */) {
        [embeddedArray replaceObjectAtIndex:index withObject:object];
    }
}
 
- (void)removeLastObject;
{
    if (/* modification is valid */) {
        [embeddedArray removeLastObject];
    }
}
- (void)insertObject:object atIndex:(unsigned)index;
{
    if (/* modification is valid */) {
        [embeddedArray insertObject:object atIndex:index];
    }
}
- (void)removeObjectAtIndex:(unsigned)index;
{
    if (/* modification is valid */) {
        [embeddedArray removeObjectAtIndex:index];
    }
}


三、类工厂方法

为了方便客户端,类工厂方法由类实现。
它们将分配和初始化结合在一个步骤中,并返回创建的对象。
但是,接收此对象的客户端不拥有该对象,因此(根据对象所有权策略)不负责释放它。
这些方法的形式是+ (type)className...(其中 className 不包括任何前缀)。

Cocoa提供了大量示例,尤其是在“值”类中。
NSDate包括以下类厂方法:

+ (id)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs;
+ (id)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)secs;
+ (id)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;

并且NSData提供了以下工厂方法:

+ (id)dataWithBytes:(const void *)bytes length:(unsigned)length;
+ (id)dataWithBytesNoCopy:(void *)bytes length:(unsigned)length;
+ (id)dataWithBytesNoCopy:(void *)bytes length:(unsigned)length
        freeWhenDone:(BOOL)b;
+ (id)dataWithContentsOfFile:(NSString *)path;
+ (id)dataWithContentsOfURL:(NSURL *)url;
+ (id)dataWithContentsOfMappedFile:(NSString *)path;

工厂方法可以不仅仅是一种简单的方便。
它们不仅可以将分配和初始化结合起来,而且分配可以通知初始化。
例如,假设您必须从一个属性列表文件中初始化一个集合对象,该文件对集合的任意数量的元素(NSString对象、NSData对象、NSNumber对象等)进行编码。
在工厂方法可以知道要为集合分配多少内存之前,它必须读取文件并解析属性列表,以确定有多少元素以及这些元素是什么对象类型。

类厂方法的另一个目的是确保某个类(例如NSWorkspace)出售一个单例实例。
尽管init...方法可以验证程序中任何时候只存在一个实例,但它需要事先分配一个“原始”实例,然后在内存管理的代码中,必须释放该实例。
另一方面,工厂方法为您提供了一种避免盲目为您可能不使用的对象分配内存的方法,如下例所示:

static AccountManager *DefaultManager = nil;
 
+ (AccountManager *)defaultManager {
    if (!DefaultManager) DefaultManager = [[self allocWithZone:NULL] init];
    return DefaultManager;
}


四、代表和数据源

一个委托是一个对象,当另一个对象在程序中遇到事件时,它代表另一个对象或与另一个对象协调。
委托对象通常是一个响应对象——即从AppKit中的NSResponder或UIKit中的UIResponder继承的对象——它响应用户事件。
委托是一个对象,它被委托控制该事件的用户交互界面,或者至少被要求以application-specific的方式解释事件。

为了更好地理解委托的价值,考虑一个现成的Cocoa对象会有所帮助,例如文本字段(NSTextFieldUITextField的实例)或表格视图(NSTableViewUITableView的实例)。
这些对象旨在以通用方式实现特定角色;例如,AppKit框架中的窗口对象响应鼠标对其控件的操作,并处理关闭、调整大小和移动物理窗口等事情。
这种受限和通用的行为必然会限制对象对事件如何影响(或将影响)应用程序中其他地方的内容的了解,尤其是当受影响的行为特定于您的应用程序时。
委托为您的自定义对象提供了一种将application-specific行为传达给现成对象的方法。

委托的编程机制使对象有机会将其外观和状态与程序中其他地方发生的变化相协调,这些变化通常由用户操作带来。
更重要的是,委托使得一个对象可以改变另一个对象的行为,而无需从它继承。
委托几乎总是您的自定义对象之一,根据定义,它包含了通用和委托对象不可能知道自己的application-specific逻辑。


1、委托如何运作

委托机制的设计很简单——参见图3-1
委托类有一个outlet或属性,通常命名为delegate;如果它是一个outlet,它包括设置和访问outlet值的方法。
它还声明了一个或多个构成正式协议或非正式协议的方法,但没有实现。
使用可选方法的正式协议——Objective-C 2.0的一个特性——是首选方法,但Cocoa框架都使用这两种协议进行委托。

在非正式协议方法中,委托类在一类NSObject上声明方法,委托只实现那些它有兴趣与委托对象协调或影响该对象默认行为的方法。
如果委托类声明正式协议,委托可以选择实现那些标记为可选的方法,但它必须实现所需的方法。

委托遵循通用设计,如图3-1所示。

图3-1授权机制

在这里插入图片描述

协议的方法标记委托对象处理或预期的重要事件。
该对象想要将这些事件传达给委托,或者,对于即将发生的事件,请求委托的输入或批准。
例如,当用户在OS X中单击窗口的关闭按钮时,窗口对象向其委托发送windowShouldClose:消息;这使委托有机会否决或推迟窗口的关闭,例如,如果窗口有必须保存的关联数据(参见图3-2)。

图3-2涉及委托的更现实的序列

在这里插入图片描述

仅当委托实现该方法时,委托对象才会发送消息。
它通过首先调用委托中的NSObject方法respondsToSelector:来进行此发现。


2、委托信息的形式

委派方法有一个传统的形式。
它们以执行委派的AppKit或UIKit对象的名称开始——应用程序、窗口、控件等;这个名称是小写的,没有“NS”或“UI”前缀。
通常(但不总是)这个对象名称后面跟着一个辅助动词,指示报告事件的时间状态。
换句话说,这个动词指示事件是即将发生(“应该”或“将”)还是刚刚发生(“已经”或“已经”)。
这种时间上的区别有助于对那些期望返回值的消息和那些不期望返回值的消息进行分类。
例3-1包括一些期望返回值的AppKit委派方法。


例3-1具有返回值的示例委托方法

- (BOOL)application:(NSApplication *)sender
    openFile:(NSString *)filename;                        // NSApplication
- (BOOL)application:(UIApplication *)application
    handleOpenURL:(NSURL *)url;                           // UIApplicationDelegate
- (UITableRowIndexSet *)tableView:(NSTableView *)tableView
    willSelectRows:(UITableRowIndexSet *)selection;       // UITableViewDelegate
- (NSRect)windowWillUseStandardFrame:(NSWindow *)window
    defaultFrame:(NSRect)newFrame;                        // NSWindow

实现这些方法的委托可以阻止即将发生的事件(通过在前两个方法中返回NO)或更改建议的值(最后两个方法中的索引集和框架矩形)。
它甚至可以推迟即将发生的事件;例如,实现applicationShouldTerminate:方法的委托可以通过返回NSTerminateLater来延迟应用程序终止。

其他委托方法由不期望返回值的消息调用,因此被键入以返回void
这些消息纯粹是信息性的,方法名称通常包含“Do”、“Will”或其他一些已发生或即将发生的事件的指示。
例3-2显示了这类委托方法的几个示例。


例3-2 示例委托方法返回void

- (void) tableView:(NSTableView*)tableView
    mouseDownInHeaderOfTableColumn:(NSTableColumn *)tableColumn;      // NSTableView
- (void)windowDidMove:(NSNotification *)notification;                 // NSWindow
- (void)application:(UIApplication *)application
    willChangeStatusBarFrame:(CGRect)newStatusBarFrame;               // UIApplication
- (void)applicationWillBecomeActive:(NSNotification *)notification;   // NSApplication

关于最后一组方法,有几件事需要注意。
首先,“Will”的助动词(如在第三种方法中)不一定意味着期望返回值。
在这种情况下,事件迫在眉睫,无法阻止,但消息使委托有机会为事件准备程序。

另一个兴趣点涉及例3-2中的第二个和最后一个方法声明。
这些方法中的每一个的唯一参数是一个NSNotification对象,这意味着这些方法是作为发布特定通知的结果而调用的。
例如,windowDidMove:方法与NSWindow通知NSWindowDidMoveNotification相关联。
了解AppKit中通知与委托消息的关系非常重要。
委托对象自动使其委托成为它发布的所有通知的观察者。
委托需要做的就是实现关联的方法来获取通知。

要使自定义类的实例成为AppKit对象的委托,只需将该实例连接到Interface Builder中的delegateoutlet或属性。
或者,您可以通过委托对象的setDelegate:方法或delegate属性以编程方式设置它,最好是在早期,例如在awakeFromNibapplicationDidFinishLaunching:方法中。


3、委托和应用程序框架

在Cocoa或Cocoa Touch应用程序中,委托对象通常是响应对象,例如UIApplicationNSWindowNSTableView对象。
委托对象本身通常是(但不一定是)一个对象,通常是自定义对象,它控制应用程序的某些部分(即协调控制器对象)。
以下AppKit类定义了委托:

  • NSApplication
  • NSBrowser
  • NSControl
  • NSDrawer
  • NSFontManager
  • NSFontPanel
  • NSMatrix
  • NSOutlineView
  • NSSplitView
  • NSTableView
  • NSTabView
  • NSText
  • NSTextField
  • NSTextView
  • NSWindow

UIKit框架还广泛使用委托,并且总是使用正式协议来实现它,应用程序委托在iOS中运行的应用程序中极其重要,因为它必须响应来自应用程序对象的应用程序-启动、应用程序-退出、低内存和其他消息,应用程序委托必须采用UIApplicationDelegate协议。

委派对象不会(也不应该)保留其委派。
但是,委派对象(通常是应用程序)的客户端负责确保其委派在接收委派消息时在场。
为此,他们可能必须在内存管理的代码中保留委派。
这种预防措施同样适用于数据源、通知观察者和操作消息的目标。
请注意,在垃圾收集环境中,对委托的引用是强的,因为保留周期问题不适用。

一些AppKit类有一种更受限制的委托类型,称为模态委托
这些类的对象(例如NSOpenPanel)运行模态对话框,当用户单击对话框的确定按钮时,调用指定委托中的处理程序方法。
模态委托的范围仅限于模态对话框的操作。


成为框架类的代表

实现委托的框架类或任何其他类声明了delegate属性和协议(通常是正式协议)。
协议列出了委托实现的必需和可选方法。
要使您的类实例充当框架对象的委托,它必须执行以下操作:

  • 将对象设置为委托(通过将其分配给delegate属性)。
    您可以通过编程方式或通过Interface Builder执行此操作。
  • 如果协议是正式的,请声明您的类采用类定义中的协议。
    例如:
@interface MyControllerClass : UIViewController <UIAlertViewDelegate> {
  • 实施协议的所有必需方法以及您要参与的任何可选方法。

通过委托属性定位对象

委托的存在还有其他编程用途。
例如,使用委托,同一程序中的两个协调控制器很容易找到并相互通信。
例如,控制整个应用程序的对象可以使用类似于以下代码的代码找到应用程序检查器窗口(假设它是当前键窗口)的控制器:

id winController = [[NSApp keyWindow] delegate];

您的代码可以通过执行类似于以下操作来找到application-controller对象——根据定义,全局应用程序实例的委托:

id appController = [NSApp delegate];


4、数据来源

一个数据源类似于一个委托,只是它不是对用户交互界面的委托控制,而是对数据的委托控制。
数据源是由NSViewUIView对象(如表视图和大纲视图)持有的outlet,这些对象需要一个源来填充它们的可见数据行。
视图的数据源通常是充当其委托的同一对象,但它可以是任何对象。
与委托一样,数据源必须实现非正式协议的一个或多个方法,以向视图提供它需要的数据,并且在更高级的实现中,处理用户在这些视图中直接编辑的数据。

与委托一样,数据源是必须存在以接收来自请求数据的对象的消息的对象。
使用它们的应用程序必须确保它们的持久性,必要时将它们保留在内存管理代码中。

数据源负责传递给用户界面对象的对象的持久性。
换句话说,它们负责这些对象的内存管理。
然而,每当视图对象(如大纲视图或表格视图)从数据源访问数据时,只要它使用数据,它就会保留对象。
但是它不会使用数据很长时间。
通常,它只保留数据足够长的时间来显示它。


5、为自定义类实现委托

要实现自定义类的委托,请完成以下步骤:

  • 在类头文件中声明委托访问器方法。
- (id)delegate;
- (void)setDelegate:(id)newDelegate;

  • 实现访问器方法。
    在内存管理程序中,为避免保留周期,setter方法不应保留或复制您的委托。
- (id)delegate {
    return delegate;
}
 
- (void)setDelegate:(id)newDelegate {
    delegate = newDelegate;
}

在垃圾回收环境中,保留周期不是问题,您不应该使委托成为弱引用(通过使用__weak类型修饰符)。
有关保留周期的更多信息,请参阅 高级内存管理编程指南
有关垃圾回收机制中弱引用的更多信息,请参阅 垃圾回收编程指南 中的Cocoa Essentials垃圾回收

  • 声明包含委托编程接口的正式或非正式协议。
    非正式协议是NSObject类上的类别。
    如果为委托声明正式协议,请确保使用@optional指令标记可选方法组。
    委托消息的形式提供了命名您自己的委托方法的建议。
  • 在调用委托方法之前,请确保委托通过向其发送respondsToSelector:消息来实现它。
- (void)someMethod {
    if ( [delegate respondsToSelector:@selector(operationShouldProceed)] ) {
        if ( [delegate operationShouldProceed] ) {
            // do something appropriate
        }
    }
}

预防措施仅对正式协议中的可选方法或非正式协议中的方法是必要的。


五、反省

自省是面向对象语言和环境的一个强大特性,Objective-C和Cocoa中的自省也不例外。
自省是指对象在运行时泄露自己作为对象的细节的能力。
这些细节包括对象在继承树中的位置,它是否符合特定的协议,以及它是否响应某个消息。
NSObject协议和类定义了许多自省方法,您可以使用这些方法来查询运行时,以便表征对象。

明智地使用自省使面向对象的程序更加高效和健壮。
它可以帮助您避免消息分派错误、对象相等的错误假设和类似问题。
以下部分展示了如何在代码中有效地使用NSObject自省方法。


1、评估继承关系

一旦你知道了一个对象所属的类,你可能就对这个对象有了相当多的了解。
你可能知道它的能力是什么,它代表什么属性,以及它可以响应什么样的消息。
即使在自省之后,你不熟悉一个对象所属的类,你现在也知道了足够多的东西,不会向它发送某些消息。

NSObject协议中声明了几种方法来确定对象在类层次结构中的位置。
这些方法以不同的粒度运行。
例如,classsuperclass实例方法分别返回表示接收方的类和超类的Class对象。
这些方法要求您将一个Class对象与另一个对象进行比较。
例4-1给出了一个简单(有人可能会说微不足道)的使用示例。


示例4-1使用classsuperclass方法

// ...
while ( id anObject = [objectEnumerator nextObject] ) {
    if ( [self class] == [anObject superclass] ) {
        // do something appropriate...
    }
}

注意: 有时您使用classsuperclass方法来获取类消息的适当接收者。

更常见的是,要检查对象的类从属关系,您可以向它发送isKindOfClass:isMemberOfClass:消息。
前一种方法返回接收者是给定类的实例还是从该类继承的任何类的实例。
另一方面,isMemberOfClass:消息告诉您接收者是否是指定类的实例。
isKindOfClass:方法通常更有用,因为从它可以立即知道可以发送到对象的完整消息范围。
考虑例4-2中的代码片段。


示例4-2使用isKindOfClass:

if ([item isKindOfClass:[NSData class]]) {
    const unsigned char *bytes = [item bytes];
    unsigned int length = [item length];
    // ...
}

通过学习对象继承自NSData类,这段代码知道它可以向它发送NSData``byteslength消息。
isKindOfClass:isMemberOfClass:之间的区别变得很明显,如果你假设itemNSMutableData的实例。
如果你使用isMemberOfClass:而不是isKindOfClass:,条件化块中的代码永远不会执行,因为item不是NSData的实例,而是NSMutableDataNSData的子类。


2、方法实现和协议一致性

NSObject比较强大NSObject自省方法有两个是respondsToSelector:conformsToProtocol:
这些方法分别告诉你,一个对象是否实现了某个方法,一个对象是否符合指定的形式化协议(即采用协议,必要时,实现协议的所有方法)。

您可以在代码中的类似情况下使用这些方法。
它们使您能够在发送任何消息之前发现某个潜在的匿名对象是否可以适当地响应特定消息或消息集。
通过在发送消息之前进行此检查,您可以避免因无法识别的选择器而导致运行时异常的风险。
AppKit框架通过在调用该方法之前检查委托是否实现了委托方法(使用respondsToSelector:)来实现非正式协议——委托的基础。


例4-3说明了如何在代码中使用respondsToSelector:方法。


示例4-3使用respondsToSelector:

- (void)doCommandBySelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) {
        [self performSelector:aSelector withObject:nil];
    } else {
        [_client doCommandBySelector:aSelector];
    }
} 

例4-4说明了如何在代码中使用conformsToProtocol:方法。


示例4-4使用conformsToProtocol:

// ...
if (!([((id)testObject) conformsToProtocol:@protocol(NSMenuItem)])) {
    NSLog(@"Custom MenuItem, '%@', not loaded; it must conform to the
        'NSMenuItem' protocol.\n", [testObject class]);
    [testObject release];
    testObject = nil;
}


3、对象比较

尽管它们不是严格意义上的自省方法,但hashisEqual:方法扮演着类似的角色。
它们是识别和比较对象不可或缺的运行时工具。
但是它们不是查询运行时以获取有关对象的信息,而是依赖于特定于类的比较逻辑。

NSObject协议声明的hashisEqual:方法密切相关。
必须实现hash方法以返回一个整数,该整数可以用作哈希表结构中的表地址。
如果两个对象相等(由isEqual:方法确定),它们必须具有相同的哈希值。
如果您的对象可以包含在NSSet对象等集合中,则需要定义hash并验证如果两个对象相等,则返回相同哈希值的不变量。
isEqual:的默认NSObject实现只是检查指针相等。

使用isEqual:方法很简单;它将接收者与作为参数提供的对象进行比较。
对象比较经常通知运行时关于应该对对象做什么的决策。
如例4-5所示,您可以使用isEqual:来决定是否执行操作,在这种情况下保存已修改的用户首选项。


例4-5使用isEqu:

- (void)saveDefaults {
    NSDictionary *prefs = [self preferences];
    if (![origValues isEqual:prefs])
        [Preferences savePreferencesToDefaults:prefs];
}

如果您正在创建一个子类,您可能需要重写isEqual:以添加对相等点的进一步检查。
该子类可能会定义一个额外的属性,该属性必须在两个实例中具有相同的值才能被视为相等。
例如,假设您创建了一个名为MyWidgetNSObject子类,其中包含两个实例变量,namedata
对于MyWidget的两个实例,这两个值必须相同才能被视为相等。
例4-6说明了如何为MyWidget类实现isEqual:


示例4-6 重写isEqual:

- (BOOL)isEqual:(id)other {
    if (other == self)
        return YES;
    if (!other || ![other isKindOfClass:[self class]])
        return NO;
    return [self isEqualToWidget:other];
}
 
- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
    if (self == aWidget)
        return YES;
    if (![(id)[self name] isEqual:[aWidget name]])
        return NO;
    if (![[self data] isEqualToData:[aWidget data]])
        return NO;
    return YES;
}

这种isEqual:方法首先检查指针是否相等,然后是类是否相等,最后调用一个对象比较器,其名称表示比较中涉及的对象的类。
这种类型的比较器强制对传入的对象进行类型检查,这是Cocoa中的常见约定;NSString类的isEqualToString:方法和NSTimeZone类的isEqualToTimeZone:方法只是两个例子。
特定于类的比较器——在本例中为isEqualToWidget:——执行名称和数据相等性的检查。

在Cocoa框架的所有isEqualToType:方法中,nil不是有效参数,这些方法的实现可能会在收到nil时引发异常。
但是,为了向后兼容,Cocoa框架的isEqual:方法确实接受nil,返回NO


六、对象分配

当你分配一个对象时,给定这个术语,发生的部分事情是你可能期望的。
Cocoa从应用程序虚拟内存区域为对象分配足够的内存。
为了计算要分配多少内存,它考虑了对象的实例变量——包括它们的类型和顺序——由对象的类指定。

要分配对象,您需要向对象的类发送消息allocallocWithZone:
作为回报,您将获得该类的“原始”(未初始化)实例。
该方法的alloc变体使用应用程序的默认区域。
区域是一个与页面对齐的内存区域,用于保存应用程序分配的相关对象和数据。
有关区域的更多信息,请参阅 高级内存管理编程指南

除了分配内存之外,分配消息还执行其他重要操作:

  • 它将对象的保留计数设置为1。
  • 它初始化对象的isa实例变量以指向对象的类,即根据类定义编译的运行时对象。
  • 它将所有其他实例变量初始化为零(或为零的等效类型,例如nilNULL0.0)。

对象的isa实例变量继承自NSObject,因此它对所有Cocoa对象都是通用的。
isa分配给对象的类后,对象被集成到运行时对继承层次结构和构成程序的对象(类和实例)的当前网络的视图中。
因此,对象可以在运行时找到它需要的任何信息,例如另一个对象在继承层次结构中的位置、其他对象符合的协议以及它可以在响应消息时执行的方法实现的位置。

总之,分配不仅为对象分配内存,还初始化任何对象的两个小但非常重要的属性:它的isa实例变量和保留计数。
它还将所有剩余的实例变量设置为零。
但是结果对象还不可用。
初始化方法(如init)必须初始化具有特定特征的对象并返回函数对象。


七、对象初始化

初始化将对象的实例变量设置为合理和有用的初始值。
它还可以分配和准备对象所需的其他全局资源,必要时从文件等外部源加载它们。
每个声明实例变量的对象都应该实现一个初始化方法——除非默认的set-everything-to-zero初始化就足够了。
如果一个对象没有实现初始化器,Cocoa会调用最近祖先的初始化器。


1、初始化器的形式

NSObject声明了初始化程序的init原型;它是一个类型为返回id类型对象的实例方法。
重写init对于不需要额外数据来初始化对象的子类来说很好。
但是初始化通常依赖于外部数据来将对象设置为合理的初始状态。
例如,假设您有一个Account类;初始化一个Account对象适当地需要一个唯一的帐号,这必须提供给初始化程序。
因此初始化程序可以采用一个或多个参数;唯一的要求是初始化方法以字母“init”开头。
(风格约定init...有时用于指代初始化程序。)

注意: 子类可以只实现一个简单的init方法,然后在初始化后立即使用“设置”访问器方法将对象设置为有用的初始状态,而不是用参数实现初始化程序。
(访问器方法通过设置和获取实例变量的值来强制对象数据的封装。)或者,如果子类使用属性和相关的访问语法,它可以在初始化后立即为属性赋值。

Cocoa有很多带有参数的初始化器示例。
这里有一些(括号中的定义类):

  • - (id)initWithArray:(NSArray *)array;(来自NSSet
  • - (id)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)anotherDate;(来自NSDate
  • - (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag;(来自NSWindow
  • - (id)initWithFrame:(NSRect)frameRect;(来自NSControlNSView

这些初始化程序是以“init”开头并返回动态类型id的对象的实例方法。
除此之外,它们遵循Cocoa多参数方法的约定,通常在第一个也是最重要的参数之前使用WithType:FromSource:


2、初始化程序的问题

尽管init...方法的方法签名要求它们返回一个对象,但该对象不一定是最近分配的对象——init...消息的接收者。
换句话说,您从初始化程序中返回的对象可能不是您认为正在初始化的对象。

有两个条件提示返回刚刚分配的对象以外的东西。
第一个涉及两种相关的情况:当必须有一个单例实例或当对象的定义属性必须是唯一的。
一些Cocoa类——例如NSWorkspace——只允许程序中的一个实例;在这种情况下,一个类必须确保(在初始化程序中,或者更有可能在类厂方法中)只创建一个实例,如果有任何对新实例的进一步请求,则返回这个实例。

当一个对象需要有一个使其唯一的属性时,也会出现类似的情况。
回想一下前面提到的假设的Account类。
任何类型的帐户都必须有一个唯一的标识符。
如果这个类的初始化程序——比如initWithAccountID:——传递了一个已经与一个对象关联的标识符,它必须做两件事:

  • 释放新分配的对象(在内存管理的代码中)
  • 返回先前使用此唯一标识符初始化的Account对象

通过这样做,初始化程序确保标识符的唯一性,同时提供所需的内容:具有请求标识符的Account实例。

有时init...方法无法执行请求的初始化。
例如,initFromFile:方法期望根据文件的内容初始化对象,文件的路径作为参数传递。
但是如果该位置不存在文件,则无法初始化对象。
如果initWithArray:初始化程序传递的是NSDictionary对象而不是NSArray对象,则会出现类似的问题。
init...方法无法初始化对象时,它应该:

  • 释放新分配的对象(在内存管理的代码中)
  • 返回nil

从初始化程序返回nil表示无法创建请求的对象。
创建对象时,一般应先检查返回值是否nil再进行:

id anObject = [[MyClass alloc] init];
if (anObject) {
    [anObject doSomething];
    // more messages...
} else {
    // handle error
}

因为init...方法可能返回nil或显式分配的对象以外的对象,所以使用allocallocWithZone:返回的实例而不是初始化程序返回的实例是危险的。
考虑以下代码:

id myObject = [MyClass alloc];
[myObject init];
[myObject doSomething];

本例中的init方法可能返回了nil,也可能替换了不同的对象。
因为您可以在不引发异常的情况下向nil发送消息,所以在前一种情况下除了(可能)调试头痛之外什么都不会发生。
但是您应该始终依赖初始化实例,而不是刚刚分配的“原始”实例。
因此,您应该将分配消息嵌套在初始化消息中,并在继续之前测试从初始化程序返回的对象。

id myObject = [[MyClass alloc] init];
if ( myObject ) {
    [myObject doSomething];
} else {
    // error recovery...
}

一旦对象被初始化,您就不应该再次初始化它。
如果您尝试重新初始化,实例化对象的框架类通常会引发异常。
例如,本例中的第二次初始化将导致NSInvalidArgumentException被引发。

NSString *aStr = [[NSString alloc] initWithString:@"Foo"];
aStr = [aStr initWithString:@"Bar"];

3、实现初始化程序

当实现用作类唯一初始化器的init...方法时,有几个关键规则要遵循,或者,如果有多个初始化器,它的指定初始化器(在多个初始化器和指定初始化器中描述):

  • 总是首先调用超类(super)初始化程序。
  • 检查超类返回的对象,如果nil,则不能进行初始化;返回nil接收者。
  • 初始化作为对象引用的实例变量时,根据需要保留或复制对象(在内存管理代码中)。
  • 将实例变量设置为有效初始值后,返回self,除非:
    • 有必要返回一个替换的对象,在这种情况下,首先释放新分配的对象(在内存管理的代码中)。
    • 一个问题阻止初始化成功,在这种情况下返回nil

- (id)initWithAccountID:(NSString *)identifier {
    if ( self = [super init] ) {
        Account *ac = [accountDictionary objectForKey:identifier];
        if (ac) { // object with that ID already exists
            [self release];
            return [ac retain];
        }
        if (identifier) {
            accountID = [identifier copy]; // accountID is instance variable
            [accountDictionary setObject:self forKey:identifier];
            return self;
        } else {
            [self release];
            return nil;
        }
    } else
        return nil;
}

注意: 尽管为简单起见,如果参数为nil,则此示例返回nil,但Cocoa的更好做法是引发异常。

没有必要显式初始化对象的所有实例变量,只需要显式初始化那些使对象正常工作所必需的变量。
在分配期间对实例变量执行的默认设置为零初始化通常就足够了。
确保根据内存管理的要求保留或复制实例变量。

需要调用超类的初始化器作为第一个操作是很重要的。
回想一下,一个对象不仅封装了它的类定义的实例变量,还封装了它的所有祖先类定义的实例变量。
首先,通过调用super的初始化器,您可以帮助确保继承链上的类定义的实例变量首先被初始化。
直接超类在其初始化器中调用其超类的初始化器,该初始化器调用其超类的maininit...方法,依此类推(参见图6-1)。
初始化的正确顺序至关重要,因为子类的后续初始化可能取决于超类定义的实例变量被初始化为合理的值。

图6-1继承链上的初始化

在这里插入图片描述

当您创建子类时,继承的初始化器是一个问题。
有时超类init...方法可以充分初始化您的类的实例。
但是因为它更有可能不会,所以您应该重写超类的初始化器。
如果您不这样做,超类的实现将被调用,并且因为超类对您的类一无所知,您的实例可能无法正确初始化。


4、多个初始化器和指定初始化器

一个类可以定义多个初始化器。
有时,多个初始化器让类的客户端以不同的形式为相同的初始化提供输入。
例如,NSSet类为客户端提供了几个初始化器,这些初始化器接受不同形式的相同数据;一个接受一个NSArray对象,另一个接受一个元素计数列表,另一个接受一个以nil结尾的元素列表:

- (id)initWithArray:(NSArray *)array;
- (id)initWithObjects:(id *)objects count:(unsigned)count;
- (id)initWithObjects:(id)firstObj, ...;

一些子类提供了方便的初始化器,为接受完整初始化参数的初始化器提供默认值。
这个初始化器通常是指定的初始化器,一个类最重要的初始化器。
例如,假设有一个Task类,它用以下签名声明了一个指定的初始化器:

- (id)initWithTitle:(NSString *)aTitle date:(NSDate *)aDate;

这个Task类可能包含辅助初始化器,或者方便的初始化器,这些初始化器简单地调用指定的初始化器,并为其传递辅助初始化器没有明确请求的那些参数的默认值。

- (id)initWithTitle:(NSString *)aTitle {
    return [self initWithTitle:aTitle date:[NSDate date]];
}
 
- (id)init {
    return [self initWithTitle:@”Task”];
}

指定的初始化器对一个类起着重要的作用。
它确保通过调用超类的指定初始化器来初始化继承的实例变量。
它通常是具有最多参数并完成大部分初始化工作的init...方法,它是类的辅助初始化器用给self的消息调用的初始化器。

当你定义一个子类时,你必须能够识别超类的指定初始化器,并通过给super的消息在你子类的指定初始化器中调用它。
你还必须确保以某种方式重写继承的初始化器。
你可以提供你认为必要的尽可能多的便利初始化器。
在设计你的类的初始化器时,记住指定的初始化器通过给super的消息相互链接;而其他初始化器通过给self的消息链接到他们类的指定初始化器。

举个例子就更清楚了。
假设有三个类,A、B和C;B类继承自A类,C类继承自B类。
每个子类都添加一个属性作为实例变量,并实现一个init...方法——指定的初始化器——来初始化这个实例变量。
它们还定义了辅助初始化器,并确保在必要时重写继承的初始化器。
图6-2说明了所有三个类的初始化器及其关系。

图6-2辅助和指定初始化程序的交互
在这里插入图片描述


每个类的指定初始化器是重写最多的初始化器;它是初始化子类添加的属性的方法。
指定的初始化器也是init...方法,它在消息中调用超类的指定初始化器到super
在本例中,类C的指定初始化器,initWithTitle:date:,调用其超类的指定初始化器,initWithTitle:,它又调用类A的init方法。
创建子类时,了解超类的指定初始化器总是很重要的。

尽管指定的初始化器通过消息连接到super的继承链上,但辅助初始化器通过消息连接到类的指定初始化器到self
辅助初始化器(如本例所示)是继承初始化器的经常被重写的版本。
C类重写initWithTitle:以调用其指定的初始化器,并传递给它一个默认日期。
该指定的初始化器反过来调用B类的指定初始化器,即被重写的方法,initWithTitle:
如果您向B类和C类的对象发送initWithTitle:消息,您将调用不同的方法实现。
另一方面,如果C类没有重写initWithTitle:并且您将消息发送到C类的实例,则将调用B类实现。
因此,C实例将不完全初始化(因为它缺少日期)。
创建子类时,确保充分重写所有继承的初始化程序非常重要。

有时,一个超类的指定初始化器对于子类来说可能已经足够了,因此子类不需要实现自己指定的初始化器。
其他时候,一个类的指定初始化器可能是其超类指定初始化器的重写版本。
当子类需要补充超类指定初始化器执行的工作时,通常会出现这种情况,即使子类没有添加任何自己的实例变量(或者它添加的实例变量不需要显式初始化)。


八、Model-View-Controller

Model-View-Controller设计模式(MVC)非常古老。
至少从Smalltalk的早期开始,它的变体就已经存在了。
它是一种高级模式,因为它关注应用程序的全局架构,并根据对象在应用程序中扮演的一般角色对其进行分类。
它也是一种复合模式,因为它包含几个更基本的模式。

面向对象的程序通过为其设计调整MVC设计模式在几个方面受益。
这些程序中的许多对象倾向于更可重用,它们的接口也倾向于更好地定义。
总体而言,程序更能适应不断变化的需求——换句话说,它们比不基于MVC的程序更容易扩展。
此外,Cocoa中的许多技术和架构——如绑定、文档架构和可编写脚本——都基于MVC,并要求您的自定义对象扮演MVC定义的角色之一。


1、MVC对象的角色和关系

MVC设计模式认为有三种类型的对象:模型对象、视图对象和控制器对象。
MVC模式定义了这些类型的对象在应用程序中扮演的角色及其通信线路。
在设计应用程序时,一个重要的步骤是选择——或为——属于这三组之一的对象创建自定义类。
三种类型的对象中的每一种都通过抽象边界与其他对象分开,并通过这些边界与其他类型的对象进行通信。


模型对象封装数据和基本行为

模型对象代表特殊的知识和专业知识。
它们保存应用程序的数据并定义操作这些数据的逻辑。
一个设计良好的MVC应用程序将其所有重要数据封装在模型对象中。
任何属于应用程序持久状态的数据(无论该持久状态存储在文件还是数据库中)都应该在数据加载到应用程序后驻留在模型对象中。
因为它们代表与特定问题领域相关的知识和专业知识,所以它们往往是可重用的。

理想情况下,模型对象与用于呈现和编辑它的用户交互界面没有显式连接。
例如,如果您有一个代表一个人的模型对象(假设您正在编写地址簿),您可能希望存储生日。
存储在Person模型对象中是件好事。
但是,存储日期格式字符串或其他关于如何呈现日期的信息可能最好放在其他地方。

在实践中,这种分离并不总是最好的,这里有一些灵活性的空间,但是一般来说,模型对象不应该关心界面和表示问题。
一个有点例外是合理的例子是一个绘图应用程序,它有代表显示图形的模型对象。
图形对象知道如何绘制自己是有意义的,因为它们存在的主要原因是定义一个视觉事物。
但即使在这种情况下,图形对象也不应该依赖于生活在特定的视图或任何视图中,他们不应该负责知道什么时候画自己。
他们应该被要求通过想要呈现他们的视图对象来绘制自己。


查看对象向用户呈现信息

一个视图对象知道如何显示,并且可能允许用户编辑应用程序模型中的数据。
视图不应该负责存储它正在显示的数据。
(当然,这并不意味着视图从未真正存储它正在显示的数据。
出于性能原因,视图可以缓存数据或使用类似的技巧)。
视图对象可以负责显示模型对象的一部分,或者整个模型对象,甚至许多不同的模型对象。
视图有许多不同的种类。

视图对象倾向于可重用和可配置,它们提供了应用程序之间的一致性。
在Cocoa中,AppKit框架定义了大量视图对象,并在Interface Builder库中提供了其中的许多。
通过重用AppKit的视图对象,例如NSButton对象,您可以保证应用程序中的按钮的行为与任何其他Cocoa应用程序中的按钮一样,确保了应用程序之间外观和行为的高度一致性。

视图应该确保它正确地显示模型。
因此,它通常需要知道模型的更改。
因为模型对象不应该绑定到特定的视图对象,所以它们需要一种通用的方式来指示它们已经更改。


控制器对象将模型绑定到视图

一个控制器对象充当应用程序的视图对象和它的模型对象之间的中介。
控制器通常负责确保视图能够访问它们需要显示的模型对象,并充当视图了解模型更改的管道。
控制器对象还可以为应用程序执行设置和协调任务,并管理其他对象的生命周期。

在典型的Cocoa MVC设计中,当用户通过视图对象输入一个值或指示一个选择时,该值或选择会传递给控制器对象。
控制器对象可能会以某种application-specific的方式解释用户输入,然后告诉模型对象如何处理这个输入——例如,“添加一个新值”或“删除当前记录”——或者它可能会让模型对象在其属性中反映更改的值。
基于相同的用户输入,一些控制器对象还可能会告诉视图对象改变其外观或行为的一个方面,例如告诉按钮禁用自己。
相反,当模型对象发生变化时——例如,访问了一个新的数据源——模型对象通常会将这种变化传达给控制器对象,然后控制器对象请求一个或多个视图对象相应地更新自己。

控制器对象可以是可重用的或不可重用的,这取决于它们的一般类型。
Cocoa控制器对象的类型描述了Cocoa中不同类型的控制器对象。


组合角色

可以合并对象扮演的MVC角色,例如,使对象同时完成控制器和视图角色——在这种情况下,它将被称为视图控制器
同样,您也可以拥有模型控制器对象。
对于某些应用程序,像这样组合角色是一种可以接受的设计。

一个模型控制器是一个主要关注模型层的控制器。
它“拥有”模型;它的主要职责是管理模型和与视图对象通信。
应用于整个模型的操作方法通常在模型控制器中实现。
文档体系结构为您提供了许多这样的方法;例如,一个NSDocument对象(它是文档体系结构的核心部分)自动处理与保存文件相关的操作方法。

一个视图控制器是一个主要与视图层相关的控制器。
它拥有接口(视图);它的主要职责是管理接口并与模型通信。
与视图中显示的数据相关的操作方法通常在视图控制器中实现。
一个NSWindowController对象(也是文档体系结构的一部分)就是视图控制器的一个例子。

MVC应用程序的设计指南提供了一些关于具有合并MVC角色的对象的设计建议。

进一步阅读:基于文档的应用程序概述从另一个角度讨论了模型控制器和视图控制器之间的区别。


2、Cocoa控制器对象的类型

控制器对象将模型绑定到视图勾勒出控制器对象的抽象轮廓,但实际上情况要复杂得多。
在Cocoa中,有两种常见的控制器对象:中介控制器和协调控制器。
每种控制器对象都与一组不同的类相关联,每种类都提供不同的行为范围。

一个中介控制器通常是一个继承自NSController类的对象。
中介控制器对象用于Cocoa绑定技术。
它们促进或调解视图对象和模型对象之间的数据流。

iOS注意: AppKit实现了NSController类及其子类。
这些类和绑定技术在iOS中不可用。

中介控制器通常是您从Interface Builder库中拖动的现成对象。
您可以配置这些对象来建立视图对象属性和控制器对象属性之间的绑定,然后在这些控制器属性和模型对象的特定属性之间建立绑定。
因此,当用户更改视图对象中显示的值时,新值会自动传递给模型对象进行存储——通过中介控制器;当模型的属性更改其值时,该更改会传递给视图进行显示。
抽象NSController类及其具体子类-NSObjectControllerNSArrayControllerNSUserDefaultsControllerNSTreeController-提供支持功能,例如提交和丢弃更改的能力以及选择和占位符值的管理。

一个协调控制器通常是一个NSWindowControllerNSDocumentController对象(仅在AppKit中可用),或者NSObject的自定义子类的实例。
它在应用程序中的作用是监督或协调整个应用程序或部分应用程序的运行,例如从nib文件中未归档的对象。
协调控制器提供如下服务:

  • 回复委派消息并观察通知
  • 响应操作消息
  • 管理拥有对象的生命周期(例如,在适当的时间发布它们)
  • 建立对象之间的连接并执行其他设置任务

NSWindowControllerNSDocumentController是类,它们是Cocoa架构的一部分,用于基于文档的应用程序。
这些类的实例为上面列出的几个服务提供默认实现,您可以创建它们的子类来实现更application-specific的行为。
您甚至可以使用NSWindowController对象来管理不基于文档架构的应用程序中的窗口。

协调控制器经常拥有nib文件中归档的对象。
作为文件所有者,协调控制器位于nib文件中对象的外部,并管理这些对象。
这些拥有的对象包括中介控制器以及窗口对象和视图对象。
请参阅MVC作为复合设计模式,了解更多关于协调控制器作为文件所有者的信息。

自定义NSObject子类的实例可以完全适合作为协调控制器。
这些类型的控制器对象结合了中介和协调功能。
对于它们的中介行为,它们利用目标操作、outlet、委托和通知等机制来促进视图对象和模型对象之间的数据移动。
它们往往包含大量粘合代码,并且因为这些代码是专门application-specific的,所以它们是应用程序中最不可重用的对象。

进一步阅读: 有关Cocoa绑定技术的更多信息,请参阅 Cocoa绑定编程主题


3、MVC作为复合设计模式

Model-View-Controller是由几个更基本的设计模式组成的设计模式。
这些基本模式共同定义功能分离和通信路径,这是MVC应用程序的特征。
然而,传统的MVC概念分配了一组不同于Cocoa分配的基本模式。
区别主要在于赋予应用程序的控制器和视图对象的角色。

在最初的(Smalltalk)概念中,MVC由复合、策略和观察者模式组成。

  • 复合——应用程序中的视图对象实际上是嵌套视图的复合,这些视图以协调的方式协同工作(即视图层次结构)。
    这些显示组件的范围从窗口到复合视图,如表格视图,再到单个视图,如按钮。
    用户输入和显示可以在复合结构的任何级别进行。
  • 策略-控制器对象实现一个或多个视图对象的策略。
    视图对象将自己限制在维护其视觉方面,并将有关界面行为application-specific意义的所有决策委托给控制器。
  • 观察者——模型对象将感兴趣的对象保存在应用程序中——通常是查看对象——告知其状态的变化。

复合、策略和观察者模式协同工作的传统方式如图7-1所示:用户在复合结构的某个级别操纵视图,结果生成了一个事件。
控制器对象接收事件并以application-specific的方式解释它——也就是说,它应用了一个策略。
这个策略可以是请求(通过消息)模型对象改变其状态,或者请求视图对象(在复合结构的某个级别)改变其行为或外观。
模型对象反过来在其状态发生变化时通知所有注册为观察者的对象;如果观察者是视图对象,它可以相应地更新其外观。

图7-1传统版本的MVC作为复合模式
在这里插入图片描述


作为复合模式的Cocoa版本的MVC与传统版本有一些相似之处,实际上基于图7-1中的图表构建一个工作应用程序是非常有可能的。
通过使用绑定技术,您可以轻松创建一个Cocoa MVC应用程序,其视图直接观察模型对象以接收状态变化的通知。
但是,这种设计存在一个理论问题。
视图对象和模型对象应该是应用程序中最可重用的对象。
视图对象代表了操作系统和系统支持的应用程序的“外观和感觉”;外观和行为的一致性至关重要,这需要高度可重用的对象。
根据定义,模型对象封装了与问题域相关的数据,并对该数据执行操作。
从设计角度来看,最好将模型和视图对象彼此分开,因为这增强了它们的可重用性。

在大多数Cocoa应用程序中,模型对象的状态变化通知通过控制器对象传递给视图对象。
图7-2显示了这种不同的配置,尽管涉及了两种更基本的设计模式,但它看起来要干净得多。

图7-2 Cocoa版MVC作为复合设计模式

在这里插入图片描述

这个复合设计模式中的控制器对象包含了中介模式和策略模式;它在两个方向上调解模型和视图对象之间的数据流。
模型状态的变化通过应用程序的控制器对象传达给视图对象。
此外,视图对象通过它们对目标操作机制的实现来合并命令模式。

注意:目标 —— 动作机制使视图对象能够传达用户输入和选择,可以在协调和中介控制器对象中实现。
然而,机制的设计在每种控制器类型中都有不同。
对于协调控制器,您可以在Interface Builder中将视图对象连接到它的目标(控制器对象),并指定一个必须符合特定签名的操作选择器。
协调控制器由于是窗口和全局应用程序对象的委托,也可以在响应器链中。
中介控制器使用的绑定机制也将视图对象连接到目标,并允许具有任意类型的可变数量参数的操作签名。
然而,中介控制器不在响应器链中。

修改后的复合设计模式(如图7-2所示)既有实际原因,也有理论原因,特别是当涉及到中介设计模式时。
中介控制器派生自NSController的具体子类,这些类除了实现中介模式之外,还提供了应用程序应该利用的许多特性,例如选择和占位符值的管理。
如果您选择不使用绑定技术,您的视图对象可以使用Cocoa通知中心等机制来接收来自模型对象的通知。
但是这需要您创建一个自定义视图子类来添加模型对象发布的通知的知识。

在设计良好的Cocoa MVC应用程序中,协调控制器对象通常拥有中介控制器,这些控制器归档在nib文件中。
图7-3显示了两种类型的控制器对象之间的关系。

图7-3协调控制器作为nib文件的所有者

在这里插入图片描述


4、MVC应用程序设计指南

以下准则适用于应用程序设计中的Model-View-Controller考虑:

  • 尽管您可以使用NSObject的自定义子类的实例作为中介控制器,但没有理由完成使其成为中介控制器所需的所有工作。
    使用为Cocoa绑定技术设计的现成NSController对象之一;也就是说,使用NSObjectControllerNSArrayControllerNSUserDefaultsControllerNSTreeController的实例-或这些具体NSController子类之一的自定义子类。
    但是,如果应用程序非常简单,并且您觉得编写使用outlet和目标操作实现中介行为所需的粘合代码更舒服,请随意使用自定义NSObject子类的实例作为中介控制器。
    在自定义NSObject子类中,您还可以使用键值编码、键值观察和编辑器协议实现NSController意义上的中介控制器。
  • 尽管您可以将MVC角色组合在一个对象中,但最好的总体策略是保持角色之间的分离。
    这种分离增强了对象的可重用性和它们所使用的程序的可扩展性。
    如果您要合并类中的MVC角色,请为该类选择一个主要角色,然后(出于维护目的)使用同一实现文件中的类别来扩展类以扮演其他角色。
  • 设计良好的MVC应用程序的一个目标应该是使用尽可能多的(至少理论上)可重用的对象。
    特别是,视图对象和模型对象应该是高度可重用的。
    (当然,现成的中介控制器对象是可重用的。)Application-specific行为经常尽可能集中在控制器对象中。
  • 虽然可以让视图直接观察模型来检测状态的变化,但最好不要这样做。
    视图对象应该始终通过中介控制器对象来了解模型对象的变化。
    原因有两个:
    • 如果使用绑定机制让视图对象直接观察模型对象的属性,就会绕过NSController及其子类为应用程序提供的所有优势:选择和占位符管理以及提交和丢弃更改的能力。
    • 如果您不使用绑定机制,则必须子类化现有视图类以添加观察模型对象发布的更改通知的能力。
  • 努力限制应用程序类中的代码依赖关系。
    一个类对另一个类的依赖关系越大,它的可重用性就越低。
    具体建议因涉及的两个类的MVC角色而异:
    • 视图类不应该依赖于模型类(尽管这在某些自定义视图中可能是不可避免的)。
    • 视图类不应该依赖于中介控制器类。
    • 模型类不应该依赖于其他模型类以外的任何东西。
    • 中介控制器类不应依赖于模型类(尽管,与视图一样,如果它是自定义控制器类,这可能是必要的)。
    • 中介控制器类不应依赖于视图类或协调控制器类。
    • 协调控制器类依赖于所有MVC角色类型的类。
  • 如果Cocoa提供了一个解决编程问题的架构,并且该架构将MVC角色分配给特定类型的对象,请使用该架构。
    如果这样做,将项目组合在一起会容易得多。
    例如,文档架构包括一个Xcode项目模板,该模板将一个NSDocument对象(每个笔尖模型控制器)配置为File的所有者。

5、Model-View-Controller在可可(OS X)

对于许多Cocoa机制和技术来说,Model-View-Controller设计模式是基础。
因此,在面向对象设计中使用MVC的重要性不仅仅是为您自己的应用程序获得更大的可重用性和可扩展性。
如果您的应用程序要合并基于MVC的Cocoa技术,那么如果您的应用程序的设计也遵循MVC模式,那么它将工作得最好。
如果您的应用程序具有良好的MVC分离,那么使用这些技术应该相对轻松,但是如果您没有良好的分离,那么使用这种技术将需要更多的努力。

OS X中的Cocoa包括以下基于Model-View-Controller的架构、机制和技术:

  • 文档体系结构
    在此体系结构中,基于文档的应用程序由整个应用程序的控制器对象(NSDocumentController)、每个文档窗口的控制器对象(NSWindowController)以及结合每个文档的控制器和模型角色的对象(NSDocument)组成。
  • 绑定
    MVC是Cocoa绑定技术的核心。
    抽象NSController的具体子类提供了现成的控制器对象,您可以配置这些对象来建立视图对象和正确设计的模型对象之间的绑定。
  • 应用程序可编写脚本
    在设计应用程序以使其可编写脚本时,不仅必须遵循MVC设计模式,而且必须正确设计应用程序的模型对象。
    访问应用程序状态和请求应用程序行为的脚本命令通常应该发送到模型对象或控制器对象。
  • 核心数据
    核心数据框架管理模型对象的图形,并通过将它们保存到(并从)持久存储中检索来确保这些对象的持久性。
    核心数据与可可绑定技术紧密集成。
    MVC和对象建模设计模式是核心数据架构的重要决定因素。
  • 撤消
    在撤消体系结构中,模型对象再次发挥核心作用。
    模型对象的原始方法(通常是它的访问器方法)通常是您实现撤消和重做操作的地方。
    操作的视图和控制器对象也可能参与这些操作;例如,您可能让这些对象为撤消和重做菜单项提供特定的标题,或者您可能让它们在文本视图中进行撤消选择。

九、对象建模

本节定义术语并提供对象建模和键值编码的示例,这些术语是特定于Cocoa绑定和Core Data框架的。
理解关键路径等术语对于有效使用这些技术至关重要。
如果您是面向对象设计或键值编码的新手,建议阅读本节。

当使用核心数据框架时,您需要一种方法来描述不依赖于视图和控制器的模型对象。
在一个好的可重用设计中,视图和控制器需要一种方法来访问模型属性,而不会在它们之间强加依赖关系。
核心数据框架通过借用数据库技术的概念和术语来解决这个问题——特别是实体关系模型。

实体关系建模是一种表示对象的方法,通常用于描述数据源的数据结构,允许这些数据结构映射到面向对象系统中的对象。
请注意,实体关系建模并不是Cocoa独有的;它是一门流行的学科,有一组规则和术语记录在数据库文献中。
它是一种便于数据源中对象存储和检索的表示。
数据源可以是数据库、文件、Web服务或任何其他持久存储。
因为它不依赖于任何类型的数据源,所以它也可以用来表示任何类型的对象及其与其他对象的关系。

在实体关系模型中,保存数据的对象称为实体,实体的组件称为属性,对其他数据承载对象的引用称为关系
属性和关系一起称为属性
通过这三个简单的组件(实体、属性和关系),您可以对任何复杂的系统进行建模。

Cocoa使用传统实体关系建模规则的修改版本,在本文档中称为对象建模
对象建模在表示Model-View-Controller(MVC)设计模式中的模型对象时特别有用。
这并不奇怪,因为即使在简单的Cocoa应用程序中,模型通常也是持久的——也就是说,它们存储在文件等数据容器中。


1、实体

实体是模型对象。
在MVC设计模式中,模型对象是应用程序中封装指定数据并提供对该数据进行操作的方法的对象。
它们通常是持久的,但更重要的是,模型对象不依赖于数据如何显示给用户。


例如,模型对象的结构化集合(对象模型)可以用来表示公司的客户群、图书馆或计算机网络。
图书馆图书具有属性——如书名、ISBN编号和版权日期——以及与其他对象的关系——如作者和图书馆成员。
理论上,如果可以识别系统的各个部分,则可以将系统表示为对象模型。

图8-1显示了员工管理应用程序中使用的示例对象模型。
在此模型中,部门为部门建模,员工为员工建模。

图8-1员工管理应用程序对象图
在这里插入图片描述


2、属性

属性表示包含数据的结构。
对象的属性可以是简单的值,例如标量(例如integerfloatdouble值),但也可以是C结构(例如char值数组或NSPoint结构)或原始类的实例(例如NSNumberNSData或Cocoa中的NSColor)。
不可变的对象,例如NSColor通常也被视为属性。
(请注意,Core Data本机仅支持一组特定的属性类型,如 NSAttributeDescription类参考 中所述。
但是,您可以使用其他属性类型,如非标准持久性属性在 核心数据编程指南 中所述。)

在Cocoa中,属性通常对应于模型的实例变量或访问器方法。
例如,员工有firstNamelastNamesalary实例变量。
在员工管理应用程序中,您可以实现一个表格视图来显示员工对象及其一些属性的集合,如图8-2所示。
表格中的每一行对应于员工的一个实例,每一列对应于员工的一个属性。

图8-2员工表视图
在这里插入图片描述


3、关系

并非模型的所有属性都是属性——一些属性是与其他对象的关系。
您的应用程序通常由多个类建模。
在运行时,您的对象模型是相关对象的集合,它们构成了一个对象图。
这些通常是用户在终止应用程序之前创建并保存到某个数据容器或文件中的持久对象(如在基于文档的应用程序中)。
可以在运行时遍历这些模型对象之间的关系以访问相关对象的属性。


例如,在员工管理应用程序中,员工和员工工作的部门之间以及员工和员工的经理之间存在关系。
因为经理也是员工,员工-经理关系是反身关系——从实体到自身的关系。

关系本质上是双向的,所以至少在概念上,一个部门和在该部门工作的员工之间也有关系,一个员工和员工的直接下属之间也有关系。
图8-3说明了一个部门和一个员工实体之间的关系,以及员工自反关系。
在这个例子中,部门实体的“员工”关系是员工实体的“部门”关系的逆关系。
然而,关系可能只在一个方向上导航——因为没有逆关系。
例如,如果你从来没有兴趣从一个部门对象中找出哪些员工与它相关联,那么你就不必为这种关系建模。
(请注意,虽然这在一般情况下是正确的,但Core Data可能会对一般Cocoa对象建模施加额外的约束-不建模逆应该被视为一个非常高级的选项。)

图8-3员工管理应用程序中的关系
在这里插入图片描述


关系基数和所有权

每个关系都有一个基数;基数告诉你有多少目标对象可以(潜在地)解析关系。
如果目标对象是单个对象,那么该关系称为一对一的关系
如果目标中可能有多个对象,那么该关系称为对多关系

关系可以是强制的,也可以是可选的。
强制关系是指目标是必需的——例如,每个员工都必须与一个部门相关联。
顾名思义,可选关系是可选的——例如,并非每个员工都有直接下属。
因此,图8-4中描述的directReports关系是可选的。

也可以为基数指定一个范围。
可选的一对一关系具有范围0-1。
员工可以有任意数量的直接下属,或者指定最小值和最大值的范围,例如0-15,这也说明了可选的一对多关系。

图8-4说明了雇员管理应用程序中的基数。
雇员对象和部门对象之间的关系是强制的一对一关系——雇员必须属于一个,而且只能属于一个部门。
部门和它的雇员对象之间的关系是可选的对多关系(由 “*”). 雇员和经理之间的关系是可选的一对一关系(用范围0-1表示)——排名靠前的雇员没有经理。

图8-4关系基数
在这里插入图片描述

还要注意,关系的目标对象有时是拥有的,有时是共享的。


4、访问属性

为了使模型、视图和控制器彼此独立,您需要能够以一种独立于模型实现的方式访问属性。
这是通过使用键值对来完成的。


钥匙

您使用一个简单的键来指定模型的属性,通常是一个字符串。
相应的视图或控制器使用键来查找相应的属性值。
这种设计强调了属性本身不一定包含数据的概念——值可以间接获得或派生。

键值编码用于执行这种查找;它是一种间接访问对象属性的机制,在某些情况下是自动的。
键值编码通过使用对象属性的名称(通常是其实例变量或访问器方法)作为键来访问这些属性的值。


例如,您可以使用name键来获取部门对象的名称。
如果部门对象有一个实例变量或一个名为name的方法,则可以返回该键的值(如果两者都没有,则返回错误)。
同样,您可以使用firstNamelastNamesalary键来获取员工属性。


价值观

给定实体的特定属性的所有值都具有相同的数据类型。
属性的数据类型在其相应实例变量的声明中或在其访问器方法的返回值中指定。
例如,部门对象name属性的数据类型可以是Objective-C中的NSString对象。

请注意,键值编码仅返回对象值。
如果用于为指定键提供值的特定访问器方法或实例变量的返回类型或数据类型不是对象,则为该值创建NSNumberNSValue对象并返回其位置。
如果部门的name属性是NSString类型,则使用键值编码,为部门对象的name键返回的值是NSString对象。
如果部门的budget属性是float类型,则使用键值编码,为部门对象的budget键返回的值是NSNumber对象。

同样,当您使用键值编码设置值时,如果指定键的适当访问器或实例变量所需的数据类型不是对象,则使用适当的-类型Value方法从传递的对象中提取值。

一对一关系的值仅仅是该关系的目标对象。
例如,员工对象的department属性的值是一个部门对象。
一对多关系的值是集合对象。
集合可以是一个集合或一个数组。
如果您使用核心数据,它是一个集合;否则,它通常是一个数组),其中包含该关系的目标对象。
例如,部门对象的employees属性的值是一个包含员工对象的集合。
图8-5显示了员工管理应用程序的示例对象图。

图8-5员工管理应用程序的对象图
在这里插入图片描述


关键路径

一个键路径是一串由点分隔的键组成的字符串,指定要遍历的对象属性序列。
第一个键的属性由前一个属性确定,每个后续键都相对于前一个属性进行评估。
键路径允许您以独立于模型实现的方式指定相关对象的属性。
使用键路径,您可以通过对象图(无论深度如何)指定相关对象的特定属性的路径。

键值编码机制在给定类似于键值对的键路径的情况下实现对值的查找。
例如,在雇员管理应用程序中,您可以使用department.name键路径通过雇员对象访问部门的名称,其中department是雇员的关系,name是部门的属性。
如果您想显示目标实体的属性,键路径很有用。
例如,图8-6中的雇员表视图被配置为显示雇员的部门对象的名称,而不是部门对象本身。
使用Cocoa绑定,部门列的值被绑定到department.name显示数组中的雇员对象。

图8-6显示部门名称的员工表视图
在这里插入图片描述

键路径中的每个关系不一定都有值。
例如,如果员工是首席执行官,manager关系可以nil
在这种情况下,键值编码机制不会中断——它只是停止遍历路径并返回一个适当的值,例如nil.


十、对象易变性

Cocoa对象要么是可变的,要么是不可变的。
您不能更改不可变对象的封装值;一旦创建了这样的对象,它所代表的值在对象的整个生命周期中保持不变。
但是您可以随时更改可变对象的封装值。
以下部分解释了对象类型的可变和不可变变体的原因,描述了对象可变性的特征和副作用,并推荐了当对象的可变性成为问题时如何最好地处理它们。


1、为什么选择可变和不可变对象变体?

默认情况下,对象是可变的。
大多数对象允许您通过setter访问器方法更改其封装的数据。
例如,您可以更改NSWindow对象的大小、定位、标题、缓冲行为和其他特征。
设计良好的模型对象——例如,表示客户记录的对象——需要setter方法来更改其实例数据。

Foundation框架通过引入具有可变和不可变变体的类来增加一些细微差别。
可变子类通常是其不可变超类的子类,并且在类名中嵌入了“可变”。
这些类包括以下内容:

  • NSMutableArray
  • NSMutableDictionary
  • NSMutableSet
  • NSMutableIndexSet
  • NSMutableCharacterSet
  • NSMutableData
  • NSMutableString
  • NSMutableAttributedString
  • NSMutableURLRequest

注意: 除了AppKit框架中的NSMutableParagraphStyle,Foundation框架目前定义了所有明确命名的可变类。
然而,任何Cocoa框架都可能有自己的可变和不可变类变体。

尽管这些类有非典型的名称,但它们比它们不可变的对应物更接近可变规范。
为什么这么复杂?拥有可变对象的不可变变体有什么目的?

考虑这样一个场景:所有对象都可以被改变。
在您的应用程序中,您调用一个方法,然后返回一个表示字符串的对象的引用。
您在用户交互界面中使用这个字符串来标识特定的数据。
现在,您的应用程序中的另一个子系统获得了对同一字符串的引用,并决定改变它。
突然,您的标签从您下面改变了。
例如,如果您获得一个用于填充表视图的数组的引用,事情可能会变得更加可怕。
用户选择了与数组中的一个对象对应的行,该对象已被程序中其他地方的一些代码删除,问题随之而来。
不变性是一种保证,即对象在您使用时不会意外改变值。

适合不变性的对象是封装离散值集合或包含存储在缓冲区中的值的对象(缓冲区本身就是字符或字节的集合)。
但并非所有此类值对象都必然受益于可变版本。
包含单个简单值的对象,例如NSNumberNSDate的实例,不是适合可变的对象。
在这些情况下,当表示的值发生变化时,用新实例替换旧实例更有意义。

性能也是表示字符串和字典等事物的对象的不可变版本的一个原因。
字符串和字典等基本实体的可变对象会带来一些开销。
因为它们必须动态管理可变的后备存储——根据需要分配和释放内存块——可变对象的效率可能低于不可变对象。

虽然从理论上讲,不变性保证了对象的值是稳定的,但在实践中,这种保证并不总是得到保证。
一个方法可能会选择在其不可变变体的返回类型下分发一个可变对象;后来,它可能会决定改变对象,这可能会违反接收者基于先前值所做的假设和选择。
对象本身的可变性可能会随着各种转换而改变。
例如,序列化属性列表(使用NSPropertyListSerialization类)不会保留对象的可变性方面,只保留它们的一般类型——字典、数组等。
因此,当您反序列化此属性列表时,生成的对象可能与原始对象不属于同一类。
例如,曾经是NSMutableDictionary对象现在可能是NSDictionary对象。


2、使用可变对象编程

当对象的可变性成为一个问题时,最好采用一些防御性编程实践。
以下是一些一般规则或指南:

  • 当您需要在创建对象后频繁且增量地修改其内容时,请使用对象的可变变体。
  • 有时最好用另一个不可变对象替换一个不可变对象;例如,大多数保存字符串值的实例变量都应该分配不可变的NSString对象,然后用setter方法替换。
  • 依靠返回类型来指示可变性。
  • 如果您对对象是否或应该是可变的有任何疑问,请使用不可变。

本节探讨这些指南中的灰色地带,讨论使用可变对象进行编程时必须做出的典型选择。
它还概述了Foundation框架中用于创建可变对象以及在可变和不可变对象变体之间进行转换的方法。


创建和转换可变对象

您可以通过标准嵌套alloc-init消息创建可变对象-例如:

NSMutableDictionary *mutDict = [[NSMutableDictionary alloc] init];

但是,许多可变类提供了初始化器和工厂方法,允许您指定对象的初始或可能容量,例如NSMutableArrayarrayWithCapacity:class方法:

NSMutableArray *mutArray = [NSMutableArray arrayWithCapacity:[timeZones count]];

容量提示可以更有效地存储可变对象的数据。
(因为类厂方法的约定是返回自动释放的实例,所以如果您希望在代码中保持对象的可行性,请务必保留该对象。)

您还可以通过制作该通用类型的现有对象的可变副本来创建可变对象。
为此,请调用Foundation可变类的每个不可变超类实现的mutableCopy方法:

NSMutableSet *mutSet = [aSet mutableCopy];

在另一个方向,您可以将copy发送到可变对象以制作该对象的不可变副本。

许多具有不可变和可变变体的Foundation类包括用于在变体之间进行转换的方法,包括:

  • typeWithType:-例如,arrayWithArray:
  • setType:-例如,setString:(仅限可变类)
  • initWith类型:copyItems:-例如,initWithDictionary:copyItems:

存储和返回可变实例变量

在Cocoa开发中,您经常必须决定是让一个实例变量可变还是不可变。
对于一个值可以改变的实例变量,比如字典或字符串,什么时候让对象可变比较合适?当对象的表示值改变时,什么时候让对象不可变并用另一个对象替换它比较好?

一般来说,当你有一个对象的内容发生大规模变化时,最好使用一个不可变的对象。
字符串(NSString)和数据对象(NSData)通常属于这一类。
如果一个对象很可能增量变化,那么使其可变是一个合理的方法。
数组和字典等集合属于这一类。
但是,变化的频率和集合的大小应该是这个决定的因素。
例如,如果你有一个很少变化的小数组,最好使其不可变。

在决定作为实例变量保存的集合的可变性时,还有一些其他考虑因素:

  • 如果您有一个经常更改的可变集合,并且您经常分发给客户端(也就是说,您直接在getter访问器方法中返回它),您将冒着更改客户端可能引用的内容的风险。
    如果这种风险是可能的,则实例变量应该是不可变的。
  • 如果实例变量的值经常更改,但您很少在getter方法中将其返回给客户端,您可以使实例变量可变,但在访问器方法中返回它的不可变副本;在内存管理程序中,此对象将被自动释放(例9-1)。

例9-1返回可变实例变量的不可变副本

@interface MyClass : NSObject {
    // ...
    NSMutableSet *widgets;
}
// ...
@end
 
@implementation MyClass
- (NSSet *)widgets {
    return (NSSet *)[[widgets copy] autorelease];
}

处理返回给客户端的可变集合的一种复杂方法是维护一个标志,该标志记录对象当前是可变的还是不可变的。
如果有更改,请使对象可变并应用更改。
分发集合时,在返回之前使对象不可变(如果需要)。


接收可变对象

方法的调用者对返回对象的可变性感兴趣有两个原因:

  • 它想知道它是否可以更改对象的值。
  • 它想知道对象的值是否会在引用时意外更改。

使用返回类型,而不是内省

要确定它是否可以更改接收到的对象,消息的接收者必须依赖返回值的形式类型。
例如,如果它接收到一个类型为不可变的数组对象,它不应该尝试改变它。
根据对象的类成员身份来确定对象是否可变是不可接受的编程实践——例如:

if ( [anArray isKindOfClass:[NSMutableArray class]] ) {
    // add, remove objects from anArray
}

由于与实现相关的原因,isKindOfClass:在这种情况下的返回可能不准确。
但是出于除此之外的原因,您不应该根据类成员身份来假设对象是否可变。
您的决定应该完全由出售对象的方法的签名所说的关于其可变性的信息来指导。
如果您不确定对象是可变的还是不可变的,请假设它是不可变的。

几个例子可能有助于澄清为什么该指南很重要:

  • 您从文件中读取属性列表。
    当Foundation框架处理该列表时,它注意到属性列表的各个子集是相同的,因此它创建了一组对象,并在所有这些子集之间共享。
    之后,您查看创建的属性列表对象并决定改变一个子集。
    突然间,在没有意识到的情况下,您在多个地方更改了树。
  • 您向NSView询问它的子视图(使用subviews方法),它返回一个声明为NSArray但内部可能是NSMutableArray的对象。
    然后您将该数组传递给其他一些代码,这些代码通过自省确定它是可变的并对其进行更改。
    通过更改此数组,代码正在改变NSView类的内部数据结构。

因此,不要根据内省告诉您的关于对象的信息来假设对象的可变性。
根据您在API边界处收到的内容(即基于返回类型)将对象视为可变或不可变。
如果您需要在将对象传递给客户端时明确地将其标记为可变或不可变,请将该信息作为标志与对象一起传递。


制作接收对象的快照

如果您想确保从方法接收到的假定不可变的对象不会在您不知情的情况下发生变异,您可以通过在本地复制该对象来制作该对象的快照。
然后偶尔将对象的存储版本与最新版本进行比较。
如果对象发生了变异,您可以调整程序中依赖于对象先前版本的任何内容。
例9-2显示了这种技术的可能实现。


例9-2制作潜在可变对象的快照

static NSArray *snapshot = nil;
- (void)myFunction {
    NSArray *thingArray = [otherObj things];
    if (snapshot) {
        if ( ![thingArray isEqualToArray:snapshot] ) {
            [self updateStateWith:thingArray];
        }
    }
    snapshot = [thingArray copy];
}

制作对象快照以供以后比较的一个问题是它很昂贵。
您需要为同一个对象制作多个副本。
更有效的替代方法是使用键值观察。
有关此协议的描述,请参阅 键值观察编程指南


集合中的可变对象

将可变对象存储在集合对象中可能会导致问题。
如果某些集合中包含的对象发生变异,它们可能会变得无效甚至损坏,因为通过变异,这些对象会影响它们在集合中的放置方式。
首先,作为散列集合中的键的对象的属性,例如NSDictionary对象或NSSet对象,如果更改,如果更改的属性影响对象的hashisEqual:方法的结果,则会损坏集合。
(如果集合中对象的hash方法不依赖于它们的内部状态,则损坏的可能性较小。)其次,如果有序集合(例如排序数组)中的对象的属性发生更改,这可能会影响该对象与数组中其他对象的比较方式,从而使排序无效.


十一、outlets

outlet是引用另一个对象的对象的属性。
引用通过Interface Builder归档。
每次从nib文件中取消归档包含对象时,都会重新建立包含对象与其outlet之间的连接。
包含对象持有一个声明为IBOutlet类型限定符和weak选项属性的outlet。
例如:

@interface AppController : NSObject
{
}
@property (weak) IBOutlet NSArray *keywords;

因为它是一个属性,outlet成为对象封装数据的一部分,并由实例变量支持。
但是outlet不仅仅是一个简单的属性。
对象与其outlet之间的连接被存档在nib文件中;当nib文件被加载时,每个连接都被取消存档并重新建立,因此每当需要向另一个对象发送消息时总是可用的。
类型限定符IBOutlet是应用于属性声明的标记,以便接口生成器应用程序可以将属性识别为outlet,并将其显示和连接与Xcode同步。

outlet被声明为弱引用(weak)以防止强引用循环。

在Xcode的Interface Builder功能中创建并连接一个插座
插座的属性声明必须使用IBOutlet限定符进行标记。

应用程序通常会在其自定义控制器对象和用户交互界面上的对象之间设置outlet连接,但它们可以在任何可以在Interface Builder中表示为实例的对象之间建立,甚至可以在两个自定义对象之间建立。
与任何对象状态项一样,您应该能够证明它包含在类中的合理性;对象的outlet越多,它占用的内存就越多。
如果有其他方法可以获得对对象的引用,例如通过它在矩阵中的索引位置找到它,或者通过它作为函数参数包含,或者通过使用标签(分配的数字标识符),您应该这样做。

outlet是对象组合的一种形式,这是一种动态模式,要求对象以某种方式获取对其组成对象的引用,以便向它们发送消息。
它通常将这些其他对象作为实例变量支持的属性保存。
在程序执行期间的某个时候,这些变量必须使用适当的引用进行初始化。


十二、接待员模式

Receptionist设计模式解决了将应用程序的一个执行上下文中发生的事件重定向到另一个执行上下文以进行处理的一般问题。
它是一种混合模式。
虽然它没有出现在“四人帮”一书中,但它结合了该书中描述的命令、备忘录和代理设计模式的元素。
它也是蹦床模式的变体(该书中也没有出现);在这种模式中,蹦床对象最初接收事件,所谓蹦床对象是因为它会立即将事件反弹或重定向到目标对象以进行处理。


1、实践中的接待员设计模式

KVO通知调用观察者实现的observeValueForKeyPath:ofObject:change:context:方法。
如果对属性的更改发生在辅助线程上,则observeValueForKeyPath:ofObject:change:context:代码在同一线程上执行。
在该模式中,中心对象接待员充当线程中介。
如图11-1所示,接待员对象被分配为模型对象属性的观察者。
接待员实现observeValueForKeyPath:ofObject:change:context:将在辅助线程上接收到的通知重定向到另一个执行上下文——在这种情况下是主操作队列。
当属性发生变化时,接待员会收到KVO通知。
接待员会立即将块操作添加到主操作队列中;块包含由客户端指定的代码,这些代码会适当地更新用户交互界面。

图11-1弹跳KVO更新到主操作队列
在这里插入图片描述

您定义了一个接待员类,这样它就有了将自己添加为属性观察者所需的元素,然后将KVO通知转换为更新任务。
因此,它必须知道它正在观察什么对象、它正在观察的对象的属性、要执行什么更新任务以及要在哪个队列上执行它。
例11-1显示了RCReceptionist类及其实例变量的初始声明。


例11-1声明接待员类

@interface RCReceptionist : NSObject {
    id observedObject;
    NSString *observedKeyPath;
    RCTaskBlock task;
    NSOperationQueue *queue;
}

RCTaskBlock实例变量是以下声明类型的块对象:

typedef void (^RCTaskBlock)(NSString *keyPath, id object, NSDictionary *change);

这些参数类似于observeValueForKeyPath:ofObject:change:context:method的参数。
接下来,参数类声明单个类厂方法,其中RCTaskBlock对象是参数:

+ (id)receptionistForKeyPath:(NSString *)path
        object:(id)obj
         queue:(NSOperationQueue *)queue
          task:(RCTaskBlock)task;

它实现此方法以将传入值分配给创建的接待员对象的实例变量,并将该对象添加为模型对象属性的观察者,如示例11-2所示。


例11-2创建接待员对象的类厂方法

+ (id)receptionistForKeyPath:(NSString *)path object:(id)obj queue:(NSOperationQueue *)queue task:(RCTaskBlock)task {
    RCReceptionist *receptionist = [RCReceptionist new];
    receptionist->task = [task copy];
    receptionist->observedKeyPath = [path copy];
    receptionist->observedObject = [obj retain];
    receptionist->queue = [queue retain];
    [obj addObserver:receptionist forKeyPath:path
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:0];
    return [receptionist autorelease];
}

请注意,代码复制块对象而不是保留它。
因为块可能是在堆栈上创建的,所以必须将其复制到堆中,以便在传递KVO通知时它存在于内存中。

最后,参数类实现了observeValueForKeyPath:ofObject:change:context: 方法。
实现(参见例11-3)很简单。


例11-3处理KVO通知

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
        change:(NSDictionary *)change context:(void *)context {
    [queue addOperationWithBlock:^{
        task(keyPath, object, change);
    }];
}

这段代码只是将任务放入给定的操作队列中,将观察到的对象、更改属性的键路径和包含新值的字典传递给任务块。
任务封装在一个NSBlockOperation对象中,该对象在队列中执行任务。

客户机对象在创建一个接待员对象时提供更新用户交互界面的块代码,如例11-4所示。
请注意,当它创建接待员对象时,客户机传入要执行块的操作队列,在本例中是主操作队列。


例11-4创建接待员对象

RCReceptionist *receptionist = [RCReceptionist receptionistForKeyPath:@"value" object:model queue:mainQueue task:^(NSString *keyPath, id object, NSDictionary *change) {
            NSView *viewForModel = [modelToViewMap objectForKey:model];
            NSColor *newColor = [change objectForKey:NSKeyValueChangeNewKey];
            [[[viewForModel subviews] objectAtIndex:0] setFillColor:newColor];
        }];


2、何时使用接待员模式

每当您需要将工作转移到另一个执行上下文进行处理时,您都可以采用接待员设计模式。
当您观察到通知、实现块处理程序或响应操作消息并且您希望确保您的代码在适当的执行上下文中执行时,您可以实现接待员模式以将必须完成的工作重定向到该执行上下文。
使用接待员模式,您甚至可以在退出处理数据的任务之前对传入的数据执行一些过滤或合并。
例如,您可以将数据收集到批处理中,然后每隔一段时间将这些批处理分派到其他地方进行处理。

Receptionist模式有用的一种常见情况是键值观察。
在键值观察中,对模型对象属性值的更改通过KVO通知传达给观察者。
但是,对模型对象的更改可能发生在后台线程上。
这会导致线程不匹配,因为模型对象状态的更改通常会导致用户交互界面的更新,而这些更新必须发生在主线程上。
在这种情况下,您希望将KVO通知重定向到主线程。
应用程序用户交互界面的更新可能发生在主线程上。


十三、目标-行动

尽管委托、绑定和通知对于处理程序中对象之间的某些形式的通信很有用,但它们并不特别适合最明显的通信类型。
典型应用程序的用户交互界面由许多图形对象组成,也许这些对象中最常见的是控件。
控件是真实世界或逻辑设备(按钮、滑块、复选框等)的图形模拟;与无线电调谐器等真实世界的控件一样,您使用它来向它是其中一部分的某个系统(即应用程序)传达您的意图。

控件在用户交互界面上的作用很简单:它解释用户的意图,并指示其他对象执行该请求。
当用户通过单击控件或按返回键对控件进行操作时,硬件设备会生成一个原始事件。
控件接受该事件(为Cocoa适当打包)并将其转换为特定于应用程序的指令。
然而,事件本身并没有提供太多关于用户意图的信息;它们只是告诉您用户单击了鼠标按钮或按下了一个键。
因此必须调用某种机制来提供事件和指令之间的转换。
这种机制称为目标操作。

Cocoa使用目标-动作机制在控件和另一个对象之间进行通信。
这种机制允许控件,以及在OS X的一个或多个单元中,封装向适当对象发送application-specific指令所需的信息。
接收对象——通常是自定义类的实例——称为目标
动作是控件发送给目标的消息。
对用户事件感兴趣的对象——目标——是赋予它意义的对象,这种意义通常反映在它给动作的名称中。


1、目标

一个目标是操作消息的接收者。
控件,或者更常见的是,它的单元将其操作消息的目标作为outlet(参见outlet)。
目标通常是一个自定义类的实例,尽管它可以是任何Cocoa对象,其类实现了适当的操作方法。

您还可以将单元格或控件的目标outlet设置为nil,并在运行时确定目标对象。
当目标nil时,应用程序对象(NSApplicationUIApplication)按规定顺序搜索适当的接收器:

  1. 它开始与第一个响应者在关键窗口,并遵循nextResponder链接到窗口对象的响应链(NSWindowUIWindow)内容视图。
    注意:OS X中的键窗口响应应用程序的按键,是菜单和对话框消息的接收者。
    应用程序的主窗口是用户操作的主要焦点,通常也具有键状态。
  2. 它尝试窗口对象,然后尝试窗口对象的委托。
  3. 如果主窗口与键窗口不同,则它会从主窗口中的第一个响应者重新开始,并沿着主窗口的响应者链向上工作到窗口对象及其委托。
  4. 接下来,应用程序对象尝试响应。
    如果它不能响应,它会尝试它的委托。
    应用程序对象及其委托是最后的接收者。

控件对象不(也不应该)保留其目标。
但是,发送操作消息的控件的客户端(通常是应用程序)负责确保其目标可用于接收操作消息。
为此,它们可能必须在内存管理的环境中保留其目标。
这种预防措施同样适用于委托和数据源。


2、行动

一个动作是控件发送给目标的消息,或者从目标的角度来看,是目标实现的响应动作消息的方法。
控件或——在AppKit中经常出现的情况——控件的单元格将动作存储为SEL类型的实例变量。
SEL是一种Objective-C数据类型,用于指定消息的签名。
动作消息必须具有简单、独特的签名。
它调用的方法不返回任何内容,通常只有一个id类型的参数。
按照惯例,这个参数被命名为sender
下面是NSResponder类的一个例子,它定义了许多动作方法:

- (void)capitalizeWord:(id)sender;

一些Cocoa类声明的Action方法也可以具有等效的签名:

- (IBAction) deleteRecord:(id)sender;

在这种情况下,IBAction不会为返回值指定数据类型;不返回任何值。
IBAction是一个类型限定符,Interface Builder在应用程序开发期间会注意到,以将以编程方式添加的操作与为项目定义的内部操作方法列表同步。

iOS注意: 在UIKit中,动作选择器还可以采用另外两种形式,详见UIKit中的Target-Action

sender参数通常标识发送动作消息的控件(尽管它可以是由实际发送者替换的另一个对象)。
这背后的思想类似于明信片上的返回地址。
如果需要,目标可以查询发送者以获取更多信息。
如果实际发送对象将另一个对象替换为发送者,您应该以相同的方式对待该对象。
例如,假设您有一个文本字段,当用户输入文本时,在目标中调用action方法nameEntered:

- (void)nameEntered:(id) sender {
    NSString *name = [sender stringValue];
    if (![name isEqualToString:@""]) {
        NSMutableArray *names = [self nameList];
        [names addObject:name];
        [sender setStringValue:@""];
    }
}

在这里,响应方法提取文本字段的内容,将字符串添加到缓存为实例变量的数组中,并清除该字段。
对发送方的其他可能查询是向NSMatrix对象询问其选定行([sender selectedRow]),向NSButton对象询问其状态([sender state]),并向与控件关联的任何单元格询问其标签([[sender cell] tag]),标签是数字标识符。


3、AppKit框架中的目标行动

AppKit框架在实现目标操作时使用特定的架构和约定。


控件、单元格和菜单项

AppKit中的大多数控件都是继承自NSControl类的对象。
尽管控件最初负责向其目标发送操作消息,但它很少携带发送消息所需的信息。
为此,它通常依赖于它的一个或多个单元格。

控件几乎总是有一个或多个单元格——从NSCell继承的对象——与之关联。
为什么会有这种关联?控件是一个相对“重”的对象,因为它继承了其祖先的所有组合实例变量,包括NSViewNSResponder类。
因为控件很昂贵,单元格被用来将控件的屏幕空间细分为各种功能区域。
单元格是轻量级对象,可以被认为重写了控件的全部或部分。
但这不仅仅是区域划分,而是分工。
单元格完成了控件必须完成的一些绘图,单元格保存了控件必须携带的一些数据。
该数据的两个项目是目标和动作的实例变量。
图12-1描述了控制单元架构。

作为抽象类,NSControlNSCell都不完全处理目标和动作实例变量的设置。
默认情况下,NSControl只是在其关联的单元格中设置信息(如果存在)。
NSControl本身仅支持自身和单元格之间的一对一映射;NSMatrixNSControl的子类支持多个单元格。)在其默认实现中,NSCell只是引发了一个异常。
您必须在继承链中更进一步才能找到真正实现目标和动作设置的类:NSActionCell

NSActionCell派生的对象为其控件提供目标和操作值,以便控件可以编写并向正确的接收者发送操作消息。
NSActionCell对象通过突出显示其区域并协助其控件向指定目标发送操作消息来处理鼠标(光标)跟踪。
在大多数情况下,NSControl对象的外观和行为的责任完全交给相应的NSActionCell对象。
NSMatrix及其子类NSForm是不遵循此规则的NSControl的子类。)


图12-1目标动作机制在控制单元架构中的工作原理

在这里插入图片描述


当用户从菜单中选择一个项目时,一个动作被发送到一个目标。
然而,菜单(NSMenu对象)及其项目(NSMenuItem对象)在架构意义上与控件和单元格完全分开。
NSMenuItem类为其自己的实例实现目标-动作机制;NSMenuItem对象具有目标和动作实例变量(以及相关的访问器方法),并在用户选择它时将动作消息发送到目标。

注意: 请参阅 控制和单元编程主题应用程序菜单和弹出列表编程主题 ,了解有关控制单元架构的更多信息。


设定目标和行动

您可以通过编程方式或使用Interface Builder设置单元格和控件的目标和操作。
对于大多数开发人员和大多数情况,Interface Builder是首选方法。
当您使用它来设置控件和目标时,Interface Builder提供视觉确认,允许您锁定连接,并将连接存档到nib文件。
过程很简单:

  1. 在具有IBAction限定符的自定义类的头文件中声明一个操作方法。
  2. 在Interface Builder中,将发送消息的控件连接到目标的操作方法

如果动作是由您的自定义类的超类或现成的AppKit或UIKit类处理的,您可以在不声明任何动作方法的情况下建立连接。
当然,如果您自己声明了一个动作方法,您必须确保实现它。

要以编程方式设置操作和目标,请使用以下方法将消息发送到控件或单元格对象:

- (void)setTarget:(id)anObject;
- (void)setAction:(SEL)aSelector;

以下示例显示了如何使用这些方法:

[aCell setTarget:myController];
[aControl setAction:@selector(deleteRecord:)];
[aMenuItem setAction:@selector(showGuides:)];

以编程方式设置目标和操作确实有其优势,在某些情况下,这是唯一可能的方法。
例如,您可能希望目标或操作根据某些运行时条件而变化,例如是否存在网络连接或是否已加载检查器窗口。
另一个例子是,当您动态填充弹出菜单的项目时,您希望每个弹出项目都有自己的操作。


AppKit定义的操作

AppKit框架不仅包括许多用于发送操作消息的基于NSActionCell的控件,它还在许多类中定义了操作方法。
当您创建Cocoa应用程序项目时,其中一些操作连接到默认目标。
例如,应用程序菜单中的退出命令连接到全局应用程序对象(NSApp)中的terminate:方法。

对于文本上的常见操作,NSResponder类还定义了许多默认操作消息(也称为标准命令)。
这允许Cocoa文本系统将这些操作消息发送到应用程序的响应链(事件处理对象的分层序列)上,在那里它可以由实现相应方法的第一个NSViewNSWindowNSApplication对象处理。


4、UIKit中的目标行动

UIKit框架还声明并实现了一套控制类;该框架中的控制类继承自UIControl类,该类定义了iOS的大部分目标操作机制。
然而,AppKit和UIKit框架如何实现目标操作存在一些根本差异。
这些差异之一是UIKit没有任何真正的单元格类。
UIKit中的控件不依赖它们的单元格来获取目标和操作信息。

这两个框架如何实现目标动作的更大区别在于事件模型的性质。
在AppKit框架中,用户通常使用鼠标和键盘来注册事件以供系统处理。
这些事件——例如单击按钮——是有限且离散的。
因此,AppKit中的控制对象通常将单个物理事件识别为它发送给目标的动作的触发器。
(对于按钮,这是一个鼠标向上事件。)在iOS,用户的手指是发起事件的东西,而不是鼠标点击、鼠标拖动或物理击键。
一次可以有多个手指触摸屏幕上的对象,这些触摸甚至可以朝着不同的方向进行。

为了解释这种多点触控事件模型,UIKit在UIControl.h中声明了一组控制事件常量,这些常量指定用户可以在控件上做出的各种物理手势,例如从控件上抬起手指、将手指拖入控件以及在文本字段中向下触摸。
您可以配置一个控件对象,以便它通过向目标发送操作消息来响应这些触摸事件中的一个或多个。
UIKit中的许多控件类都被实现为生成某些控制事件;例如,UISlider类的实例生成一个UIControlEventValueChanged控制事件,您可以使用它向目标对象发送操作消息。

您设置了一个控件,以便它通过将目标和操作与一个或多个控件事件相关联来向目标对象发送操作消息。
为此,请为您要指定的每个目标-操作对向控件发送addTarget:action:forControlEvents:
当用户以指定的方式触摸控件时,控件会将操作消息转发到全局UIApplication对象中的sendAction:to:from:forEvent:消息。
与AppKit中一样,全局应用程序对象是操作消息的集中调度点。
如果控件为操作消息指定了nil目标,则应用程序会查询响应器链中的对象,直到找到愿意处理操作消息的对象——即实现与操作选择器对应的方法的对象。

与AppKit框架相比,一个动作方法可能只有一个或两个有效签名,UIKit框架允许三种不同形式的动作选择器:

  • - (void)action
  • - (void)action:(id)sender
  • - (void)action:(id)sender forEvent:(UIEvent *)event

要详细了解UIKit中的目标-动作机制,请阅读 UIControl类参考


十四、Toll-Free Bridging

Core Foundation框架和Foundation框架中有许多数据类型可以互换使用。
这种称为Toll-Free Bridging的功能意味着您可以使用相同的数据类型作为Core Foundation函数调用的参数或作为Objective-C消息的接收者。
例如,NSLocale(参见 NSLocale类参考 )可以与其Core Foundation对应的CFLocale互换(参见 CFLocale参考 )。
因此,在您看到NSLocale *参数的方法中,您可以传递一个CFLocaleRef,在您看到CFLocaleRef参数的函数中,您可以传递一个NSLocale实例。
您将一种类型强制转换为另一种类型以抑制编译器警告,如以下示例所示。

NSLocale *gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
CFLocaleRef gbCFLocale = (CFLocaleRef) gbNSLocale;
CFStringRef cfIdentifier = CFLocaleGetIdentifier (gbCFLocale);
NSLog(@"cfIdentifier: %@", (NSString *)cfIdentifier);
// logs: "cfIdentifier: en_GB"
CFRelease((CFLocaleRef) gbNSLocale);
 
CFLocaleRef myCFLocale = CFLocaleCopyCurrent();
NSLocale * myNSLocale = (NSLocale *) myCFLocale;
[myNSLocale autorelease];
NSString *nsIdentifier = [myNSLocale localeIdentifier];
CFShow((CFStringRef) [@"nsIdentifier: " stringByAppendingString:nsIdentifier]);
// logs identifier for current locale

请注意,示例中的内存管理功能和方法也可以互换-您可以将CFRelease与Cocoa对象一起使用,并将releaseautorelease与Core Foundation对象一起使用。

注意: 使用垃圾回收机制时,Cocoa对象和Core Foundation对象的内存管理工作方式存在重要差异。
有关详细信息,请参阅将Core Foundation与垃圾收集一起使用

自OS X v10.0以来,免费桥接已经可用。
表13-1提供了Core Foundation和Foundation之间可互换的数据类型列表。
对于每对,该表还列出了它们之间可以使用免费桥接的OS X版本。

核心基础类型 基础班 可用性
CFArrayRef NSArray OS X 10.0
CFAttributedStringRef NSAttributedString OS X 10.4
CFBooleanRef NSNumber OS X 10.0
CFCalendarRef NSCalendar OS X 10.4
CFCharacterSetRef NSCharacterSet OS X 10.0
CFDataRef NSData OS X 10.0
CFDateRef NSDate OS X 10.0
CFDictionaryRef NSDictionary OS X 10.0
CFErrorRef NSError OS X 10.5
CFLocaleRef NSLocale OS X 10.4
CFMutableArrayRef NSMutableArray OS X 10.0
CFMutableAttributedStringRef NSMutableAttributedString OS X 10.4
CFMutableCharacterSetRef NSMutableCharacterSet OS X 10.0
CFMutableDataRef NSMutableData OS X 10.0
CFMutableDictionaryRef NSMutableDictionary OS X 10.0
CFMutableSetRef NSMutableSet OS X 10.0
CFMutableStringRef NSMutableString OS X 10.0
CFNullRef NSNull OS X 10.2
CFNumberRef NSNumber OS X 10.0
CFReadStreamRef NSInputStream OS X 10.0
CFRunLoopTimerRef NSTimer OS X 10.0
CFSetRef NSSet OS X 10.0
CFStringRef NSString OS X 10.0
CFTimeZoneRef NSTimeZone OS X 10.0
CFURLRef NSURL OS X 10.0
CFWriteStreamRef NSOutputStream OS X 10.0

注意: 并非所有数据类型都是免费桥接的,即使它们的名称可能表明它们是免费桥接的。
例如,NSRunLoop不是免费桥接到CFRunLoopRefNSBundle不是免费桥接到CFBundleRefNSDateFormatter不是免费桥接到CFDateFormatterRef


2024-06-16(日)