通过协议提供匿名方法

协议定义了一系列方法,遵从此协议的对象应该实现它们(如果这些方法不是可选的, 那么就必须实现)。于是,我们可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。

这样的话,想要隐藏的类名就不会出现在API之中了。若是接口背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法——因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。

此概念经常称为“匿名对象"(anonymous object),这与其他语言中的“匿名对象”不同,在那些语言中,该词是指以内联形式所创建出来的无名类,而此词在Objective-C中则不是这个意思。

我们之前说的委托与数据源对象,就用到了这个方法。例如在定义“受委托者(delegate)”属性时:

@property {nonatomic, weak) id <EOCDelegate> delegate;

由于该属性的类型是id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性, 即便该类不继承自NSObject也可以,只要遵循EOCDelegate协议就行。对于具备此属性的类来说,delegate就是“匿名的”(ammymous)。如有需要,可在运行期査出此对象所属的类型(参见第14条)。然而这样做不太好,因为指定属性类型时所写的那个EOCDelegate契约已经表明此对象的具体类型无关紧要了。

当然还有其它的用法,这里我们就不举例了。

要点

  • 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)。
  • 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
2017/9/2 posted in  第四章 协议与分类

第27条 使用“class-continuation分类”隐藏实现细节

第一种用途

类中经常会包含一些无须对外公布的方法及实例变最。其实这些内容也可以对外公布, 并且写明其为私有,开发者不应依赖它们。但是OC的动态性,使得不可能实现真正的私有方法或私有实例变量。

但是我们最好还是只把确实需要对外公布的那部分内容公开。那么,这种不需对外公布但却应该具有的方法及实例变量应该怎么写呢?此时,这个特殊的“class-continuation分类”就派上用场了。

“class-continuation分类”和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。

例如:

//EOCPerson.m
@interface EOCPerson () {
    NSString * _anInstanceVariable;
}
// Method declarations here 
@end
@implementation EOCPerson {
    int _anotherInstanceVariable;
}
// Method implementations here 
@end

我们这样定义的目的是将这些方法或者实例变量隐藏起来,只供本类使用。即便在公共接口里将其标注为private,也还是会泄漏实现细节。

例如有个绝密的类,不想给其他人知道。 假设你所写的某个类拥有那个绝密类的实例,而这个实例变量又声明在公共接口里面:

#import <Foundation/Foundation.h>

@class EOCSuperSecretClass;

@interface EOCClass : NSObject {
@private
    EOCSuperSecretClass *_secretInstance;
@end

这样别人就会知道有一个叫EOCSuperSecretClass的类了。

所以我们通常应该这样:

// EOCClass.h
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject 
@end

// EOCClass .m 
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"
@interface EOCClass ()  {
    EOCSuperSecretClass *_secretInstance;
@end

@implementation EOCClass
// Methods here
@end

第二种用途

编写Objective-C++代码时 “class-continuation分类”也很有用。Objective-C++OCC++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后 端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用 C++来编码。在这些情况下,使用"class-continuation分类"会很方便。假设某个类打算这样写:

#import <Foundation/Foundation.h> 
#include "SomeCppClass.h" 
@interface EOCClass : NSObject { 
@private
    SomeCppClass _cppClass;
@end

该类的实现文件可能叫做EOCClass.mm,其中.mm扩展名表示编译器应该将此文件按Objective-C++来编译,否则,就无法正确引人SomeCppClass.h了。然而请注意,名为SomeCppClass的这个C++类必须完全引入,因为编译器要完整地解析其定义方能得知_cppClass实例变量的大小。于是,只要是包含EOCClass.h的类,都必须编译为 Objective-C++才行,因为它们都引入了SomeCppClass类的头文件。这很快就会失控,最终 导致整个应用程序全部都要编译为ObjeCtive-C++。这样显然会增加编码的负担。

也许我们会想用前向声明来避免导入SomeCppClass.h,比如:

#import <Foundation/Foundation.h> 

class SomeCppClass;

@interface EOCClass : NSObject { 
@private
    SomeCppClass *_cppClass;
@end

现在实例变量必须是指针,若不是,则编译器无法得知其大小,从而会报错。但所有指针的大小确实都是固定的,于是编译器只需知道其所指的类型即可。

虽然我们这样做没有#include "SomeCppClass.h"但是我们前向声明该类时所用的class关键字还是C++下的关键字,所以仍然需要按照OC来编译才行。

我们这里的解决方法还是一样,既然变量是private的,我们还是可以将它在“class-continuation分类”声明,改写成:

// EOCClass. h

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject


// EOCClass.mm 
#import "EOCClass.h"
#include "SomeCppClass.h"

@interface EOCClass ()  {
    SomeCppClass _cppClass;
}
@end

@implementation EOCClass 
@end

改写后的EOCClass类,其头文件里就没有C++代码了,使用头文件的人甚至意识不到其底层实现代码中混有C++成分。某些系统库用到了这种模式,比如网页浏览器框架WebKit,其大部分代码都以C++编写,然而对外展示出来的却是一套整洁的Objective-C接口。CoreAnimation里面也用到了此模式,它的许多后端代码都用C++写成,但对外公布的却是一套纯Objective-C接口。

第三种用法

就是将public接口中声明为“只读”的 属性扩展为“可读写”,以便在类的内部设置其值。

例如:

// .h文件
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName;

-(id) initWithFirstName : (NSString*) firstName
                lastName: (NSString*) lastName;

@end

我们一般会在“class-continuaticm分类”中把这两个属性扩展为“可读写”:

@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

-(void)p_privateMethod;
@end

只需要用上面几行代码就行了。现在EOCPerson的实现代码可以随意调用“setFirstName:”“setLastName:”这两个设置方法,也可以用“点语法”来设置属性。这样做很有用,既能令外界无法修改对象,又能在其内部按照需要管理其数据。

只会在类的实现代码中用到的私有方法也可以声明在“class-continuation分类”中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。上述的私有方法加上了p_前缀。

第四种用法

当我们想要把对象所遵守的协议视为私有,就可以在“class-continuation分类”中声明。例如:

#import "EOCPerson•h"
#import "EOCSecretDelegate.h"
@interface EOCPerson () <EOCSecretDelegate> 
@end
@implementation EOCPerson
    /*.....*/
@end

要点

  • 通过“class-continuation分类”向类中新增实例变量。

  • 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”

  • 把私有方法的原型声明在“class-continuation分类”里面。

  • 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。

2017/9/1 posted in  第四章 协议与分类

第26条 不要在分类中声明属性

属性是封装数据的方式(参见第6条)。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。

原因在于,除了“class-continuation分类”(参见第27条)之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。如果我们在分类中声明了一个friends属性。会提示我们

warning: property 'friends' requires method 'friends' to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation]

warning: property 'friends' requires method 'setFriends:' to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation]

说明系统没有为我们自动合成属性的setget方法。我们要自己在分类中去实现,可以把存取方法声明为@dynamic, 也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制(参见第12条)在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。

当然我们也可以使用关联对象的方法。但是还是不建议我们在分类中定义封装数据的属性。

正确做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量(也就是数据)的地方。而属性只是定义实例变量及相关存取方法所用的“语法糖”,所以也应遵循同实例变量一样的规则。至于分类机制,则应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。

但是有时候,只读属性(readonly)可以在分类中使用,但是我们要手动实现它的get方法。当然我们不建议搞特殊。最好还是在主接口中声明。然后在分类中声明一个获取方法,来获取数据。

要点

  • 把封装数据所用的全部属性都定义在主接口里。
  • “class-contimiation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
2017/9/1 posted in  第四章 协议与分类

第25条 总是为第三方类的分类名称加前缀

分类机制通常用于向无源码的既有类中新增功能。这个特性极为强大,但在使用时也很容易忽视其中可能产生的问题。

我们在分类中添加方法,系统在运行期时会将分类中的方法加入类中。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次榭盖,比如某个分类中的方法覆盖了“主实现”中的相关方法,而另外一个分类 中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。

所以我们为了解决此问题,一般做法是:以命名空间来区别各个分类的名称与其中所定义的方法。我们这里的命名空间只是在相关名称前都加上公用的前缀。

所以我们举例来说就是这样的:

@interface NSString (ABC_HTTP)
//Encode a string with URL encoding 
-(NSString*) abc_urlEncodedStiring;

// Decode a URL ©ncodeci string
-(NSString*) abc_urlDecodedString;
@end

要点

  • 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
  • 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。
2017/9/1 posted in  第四章 协议与分类

第24条 将类的实现代码分散到便于管理的数个分类之中

一个类里面经常会填满各种各样的方法,有时候将那么多方法放在一个类的实现文件里是合理的。但是我们如果通过OC的分类机制将类代码按逻辑划入几个分区中,这对于开发和调试都有帮助。

我们来举个例子,我们对个人信息建模分类:

#import <Foundation/Foundation.h>
Sinterface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName; 
@property (nonatomic, strong, readonly) NSArray *friends;

-(id)initWithFirstName:(NSString*) firstName
            andLastName:(NSString*)lastName;
            
/* Friendship methods */
-(void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
-(BOOL)isFriendsWith:(EOCPerson*)person;

/* Work methods */
-(void)performDaysWork;
-(void)takeVacationFromWork;

/* Play methods */
-(void)goToTheCinema;
-(void)goToSportsGame;

在实现该类时,所有方法的代码可能会写在一个大文件里。显得很臃肿。所以可以用“分类”机制把刚才的类改写成下面这样:

@import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, strong, readonly) NSArray *friends;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, copy, readonly) NSString *firstName; 
 
-(id) initWithFirstName: (NSString*)firstName
            andLastNames:(NSString*)lastName;

@end

@interface EOCPerson (Friendship)
-(void)addFriend:(EOCPerson*)person;
-(void)removeFriend:(EOCPerson*)person;
-(BOOL)isFriendsWith:(EOCPerson*)person; 
@end

@interface EOCPerson (Work)
-(void)performDaysWork;
-(void)takeVacationFromWork; 
@end

@interface EOCPerson (Play)
-(void)goToTheCinema;
-(void)goToSportsGame;
@end

现在,类的实现代码按照方法分成了好几个部分。所以说,这项语言特性当然就叫做“分类”啦。

但是这些代码还是在一个文件中声明的,所以我们还可以将其拆分为多个文件:

  • EOCPerson+Friendship(.h/.m)
  • EOCPerson+Work(.h/.m)
  • EOCPerson+Play(.h/.m)

比方说,与交友功能相关的那个分类可以这样写:

// EOCPerson+Friendship.h
#import "EOCPerson.h"
@interface EOCPerson (Friendship)
-(void)addFriend:(EOCPerson*)person;
-(void)removeFriend:(EOCPerson*)person;
-(BOOL)isFriendsWith:(EOCPerson*)person;
@end

// EOCPerson+Friendship.m 

#import EOCPerson+Friendship.h
@implementation EOCPerson (Friendship)
-(void)addFriend:(EOCPerson*)person {
    /*...*/
}
-(void)removeFriend:(EOCPerson*)person {
    /*...*/
}
-(BOOL)isFriendsWith:(EOCPerson*)person {
    /*...*/
}
@end

通过分类机制,可以把类代码分成很多个易于管理的小块,以便单独检视。使用分类机制之后,如果想用分类中的方法,那么要记得在引入EOCPerson.h时一并引入分类的头文件

这样使用分类之后,对于某个分类中的所有 方法来说,分类名称都会出现在其符号中。例如,“addFriend:”方法的“符号名”(symbol name)如下:

-[EOCPerson(Friendship) addFriend:]

也可以创建名为Private的分类把这种方法全都放在里面。这个分类里的方法一般只会在类或框架内部使用,而无须对外公布。这样一来,类的使用者有时可能会在査看回溯信息时发现private一词,就知道不应该直接调用此方法了。

要点

  • 使用分类机制把类的实现代码划分成易于管理的小块。
  • 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。
2017/8/31 posted in  第四章 协议与分类

第23条:通过委托与数据源协议进行对象间通信

Objective-C语言有一项特性叫做“协议”(protocol),它与Java的“接口"(interface)类似Objective-C不支持多重继承,因而我们把某个类应该实现的一系列方法定义在协议里面。协议最为常见的用途是实现委托模式,不过也有其他用法。理解并善用协议可令代码变得更易维护,因为协议这种方式能很好地描述接口。

“分类”(Category)也是Objective-C的一项重要语言特性。利用分类机制,我们无须继承子类即可直接为当前类添加方法,而在其他编程语言中,则需通过继承子类来实现。由于 Objective-C运行期系统是髙度动态的,所以才能支持这一特性,然而,其中也隐藏着一些陷阱,因此在使用分类之前,应该先理解它。

OC中的通信方式有很多种,我们经常使用一种叫做“委托模式”(Delegate pattem)的编程设计模式来实现对象间的通信,该模式的主旨是: 定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其“委托对象”(delegate)。而这“另一个对象”则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。

此模式可将数据与业务逻辑解耦。比方说,用户界面里有个显示一系列数据所用的视图,那么,此视图只应包含显示数据所需的逻辑代码,而不应决定要显示何种数据以及数据之间如何交互等问题。视图对象的属性中,可以包含负责数据与事件处理的对象。这两种对象分别称为“数据源"(data source)与“委托”(delegate)

我们来举个假设要编写一个从网上获取数据的类。此类也许要从远程服务器的某个资源里获取数据。那个远程服务器可能过很长时间才会应答,而在获取数据的过程中阻塞应用程序则是一种非常糟糕的做法。于是,在这种情况下,我们通常会使用委托模式:获取网络数据的类含有一个“委托对象”,在获取完数据之后,它会回调这个委托对象。

EOCDataModel对象就是EOCNetworkFetcher的委托对象。EOCDataModel请求EOCNetworkFetcher "以异步方式执行一项任务"(perform a task asynchronously),EOCNetworkFetcher在执行完这项任务之后,就会通知其委托对象,也就是EOCDataModel

利用协议机制,很容易就能以Objective-C代码实现此模式。在图4-1所演示的这种情况 下,协议可以这样来定义:

@protocol EOCNetworkFetcherDelegate 
-(void)networkFetcher:(EOCNetworkFetcher*)fetcher 
        didReceiveData:(NSData*)data;
-(void)networkFetcher:(EOCNetworkFetcher*)fetcher 
     didFailWithError:(NSError*)error;
@end

委托协议名通常是在相关类名后面加上Delegate一词,整个类名采用“驼峰法”来写。 以这种方式来命名委托协议的话,使用此代码的人很快就能理解其含义了。

然后我们要在这个EOCNetworkFetcher中设置一个属性来存放其委托对象。接口可以写成这样:

@interface EOCNetworkFetcher : NSObject 
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate; 

一定要注意:这个属性需定义成weak,而非strong,因为两者之间必须为“非拥有关系” (nonowning relationship)。通常情况下,扮演delegate的那个对象也要持有本对象。所以我们要用weak来声明一种非拥有关系来避免“保留环”。

实现委托对象的办法是声明某个类遵从委托协议,然后把协议中想实现的那些方法在类 里实现出来。某类若要遵从委托协议,可以在其接口中声明,也可以在“class-contimiation分类”中声明。如果要向外界公布此类实现了某协议,那么就在接口中声明,而如果这个协议是个委托协议的话,那么通常只会在类的内部使用。所以说,这种情况一般都是在“class-continuation分类”里声明的:

@implementation EOCDataModel () <EOCNetworkFetcherDelegate> 
@end
@implementation EOCDataModel
-(void)networkFetcher:(EOCNetworkFetcher*)fetcher 
        didReceiveData:(NSData*)data {
/* Handle data */
}
-(void)networkFetcher:(EOCNetworkFetcher*)fetcher 
        didFailWithError:(NSError*)error {
/* Handle error */
}
@and

之后要用委托对象来调用方法时,必须提前使用类型信息査询方法(参见第14条)判断这个委托对象能否响应相关选择子。以EOCNetworkFetcher为例,应该这样写:

NSData *data = /*data obtained from network */;
if ([_delegate respondsToSelector:
     @selector(networkFetcher:didReceiveData:)))
{ 
    [_delegate networkFetcher:self  didReceiveData:data];
}

但是当方法变多了,我们就会频繁的来检查委托对象是否能够相应选择子,其实这个操作检测一次就可以了,所以我们可以用“位段(bitfield)”数据类型将该方法响应能力缓存起来。

这是一项乏人问津的C语言特性,但在此处用起来却正合适。我们可以把结构体中某个字段所占用的二进制位个数设为特定的值。比如像这样:

struct data {
    unsigned int fieldA : 8; 
    unsigned int fieldB : 4; 
    unsigned int fieldC : 2; 
    unsigned int fieldD : 1;
};

在结构体中,fieldA位段将占用8个二进制位,fieldB占用4个,fieldC占用两个,fieldD占用1个。于是,fieldA可以表示0至255之间的值,而fieldD则可以表示0或1这两个值。 我们举例子来说的话就是:

@interface EOCNetworkFetcher ()     {
    struct {
        unsigned int didReceiveData :   1;
        unsigned int didFailWithError : 1;      
        unsigned int didUpdateProgressTo : 1;
        }_delegateFlags;

这个结构体用来缓存委托对象是否能响应特定的选择子。实现缓存功能所用的代码可以 写在delegate属性所对应的设置方法里:

-(void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate {
    _delegate = delegate;   
    
    _delegateFlags.didReceiveData  = 
        [delegate respondsToSelector:
              @selector(networkFetcher:didReceiveData:)]; 
    
    _delegateFlags.didFailWithError =
        [delegate respondsToSelector:
              @selector(networkFetcher:didFailWithError:)]; 

    _delegateFlags.didUpdateProgressTo =
        [delegate respondsToSelector:
              @selector(networkFetcher:didUpdateProgressTo:)];

}

这样的话,每次调用delegate的相关方法之前,就不用检测委托对象是否能响应给定的选择子了,而是直接查询结构体里的标志:

if (_delegateFlags.didUpdateProgressTo) {
    [_delegate networkFetcher:self
          didUpdateProgressTo:currentProgress];
}

在相关方法要调用很多次时,值得进行这种优化。而是否需要优化,则应依照具体代码来定。这就需要分析代码性能,并找出瓶颈,若发现执行速度需要改进,则可使用此技巧。如果要频繁通过数据源协议从数据源中获取多份相互独立的数据,那么这项优化技术极有可能会提高程序效率.

要点

  • 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
  • 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。
  • 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情境下,该模式亦称“数据源协议”(data source protocal)。
  • 若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息 缓存至其中。
2017/8/30 posted in  第四章 协议与分类