第二十二条 理解NSCopying协议

使用对象时经常需要拷贝它。在Objective-C中,此操作通过copy方法完成。如果想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:

-(id)copyWithZone:(NSZone*)zone

为何会出现NSZone呢?因为以前开发程序时,会据此把内存分成不同的“区”(zone), 而对象会创建在某个区里面。现在不用了,每个程序只有一个区:“默认区”(default zone)。 所以说,尽管必须实现这个方法,但是你不必担心其中的zone参数。

copy方法由NSObject实现,该方法只是以“默认区”为参数来调用“copyWithZone:”。 我们总是想覆写copy方法,其实真正需要实现的却是“copyWithZone:”方法。这里我们一定要注意。

若想使某个类支持拷贝功能,只需声明该类遵从NSCopying协议,并实现其中的那个方法即可。比方说,有个表示个人信息的类,可以在其接口定义中声明此类遵从 NSCopying 协议:

#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
-(id)initWithFirstName:(NSString*)firstName
          andLastName: (NSString*)lastName;

@end

然后,实现协议中规定的方法:

-(id)copyWithZone:(NSZone*)zone {
    EOCPerson *copy = [[[self class] allocWithZone:zone]
                     initWithFirstName :_firstName
                            andLastName:_lastName];
    return copy;
}

当然我们又是也要考虑具体情况,除了要拷贝对象,还要完成其他一些操作,比如类对象中的数据结构可能并未在初始化方法中设置好,需要另行设置。

#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject<NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName;
-(id)initWithFirstName: (NSString*) firstName
            andLastName:(NSString*)lastName;
-(void)addFriend:(EOCPerson*)person;
-(void)removeFriend:(EOCPerson*)person;

@end

@implementation EOCPerson {
    NSMutableSet *_friends;
)
- (id)initWithFirstName: (NSString*) firstName
             andLastName:(NSString*)lastName { 
    if ((self = [super init])) {
    _firstName = [firstName copy];
    _lastName = [lastName copy];
    _friends = [NSMutableSet newJ;
    }
    return self;

}
-(void)addFriend:(EOCPerson*)person {
    [_friends addObject:person];
}
-(void)removeFriend:(EOCPerson*)person {
    [_friends removeObject:person];
}
-(id)copyWithZone:(NSZone*)zone {
    EOCPerson *copy =[[[self class] allocWithZone:zone]
                        initWithFirstName: _firstName
                        andLastName:_lastName]; 
    copy->_friends = [_friends mutableCopy]; 
    return copy;
}
@end

这次所实现的方法比原来多了一些代码,它把本对象的_friends实例变量复制了一份, 令copy对象的_frieiids实例变量指向这个复制过的set。注意,这里使用了->语法,因为_friends并非属性,只是个在内部使用的实例变量。其实也可以声明一个属性来表示它,不过由于该变量不会在本类之外使用,所以那么做没必要。

我们在上面发现了一个mutableCopy方法,此方法来自另一个叫做NSMutableCopying的协议。该协议与NSCopying类似,也只定义了一个方法,然而方法名不同:

- (id)mutableCopyWithZone:(NSZone*)zone

mutableCopy这个“辅助方法”(helper)与copy相似,也是用默认的zone参数来调“mutableCopyWithZone:”。如果你的类分为可变版本(mutable variant)与不可变版本 (immutable variant),那么就应该实现NSMutableCopying。若采用此模式,则在可变类中覆写“copyWithZone:”方法时,不要返回可变的拷贝,而应返回一份不可变的版本。无论当前实例是否可变,若需获取其可变版本的拷贝,均应调用mutableCopy方法。同理,若需要不可变的拷贝,则总应通过copy方法来获取。

对于不可变的NSArray与可变的NSMutableArray来说,下列关系总是成立的:

-[NSMutableArray copy] =>NSArray
-[NSArray mutableCopy】 =>NSMutableArray

所谓我们会发现:在可变对象上调用copy方法会返冋另外一个不可变类的实例。这样做是为了能在可变版本与不可变版本之间自由切换。

浅拷贝与深拷贝

在编写拷贝方法时,还要决定一个问题,就是应该执行“深拷贝”(deep copy)还是“浅拷贝”(shallow copy)深拷贝的意思就是:在拷贝对象自身时,将其底层数据也一并复制过 去。Foundation框架中的所有collection类在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的主要原因在于,容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中的每个对象

一般情况下,我们会遵照系统框架所使用的那种模式,在自定义的类中以浅拷贝的方式实现“copyWithZone:”方法。但如果有必要的话,也可以增加一个执行深拷贝的方法。以 NSSet为例,该类提供了下面这个初始化方法,用以执行深拷贝:

-(id)initWithSet:(NSArray*)array copyltems:(BOOL)copyltems

copyltem参数设为YES,则该方法会向数组中的每个元素发送copy消息,用拷贝好的元素创建新的set,并将其返回给调用者。
EOCPerson那个例子中,存放朋友对象的set是用“ copyWithZone:”方法来拷贝的, 根据刚才讲的内容可知,这种浅拷贝方式不会逐个复制set中的元素。若需要深拷贝的话, 则可像下面这样,编写一个专供深拷贝所用的方法:

-(id)deepCopy {
    EOCPerson *copy =[[[self class] alloc]
                initWithFirstName:_firstName
                      andLastName:_lastName];               
    copy->_friends = [[NSMutableSet alloc] initWithSetfriends copyltems:YES];
    return copy;
}

因为没有专门定义深拷贝的协议,所以其具体执行方式由每个类来确定,你只需决定自 己所写的类是否要提供深拷贝方法即可。另外,不要假定遵从了 NSCopying协议的对象都会执行深拷贝。在绝大多数情况下,执行的都是浅拷贝。如果需要在某对象上执行深拷贝,那 么除非该类的文档说它是用深拷贝来实现NSCopying协议的,否则,要么寻找能够执行深拷贝的相关方法,要么自己编写方法来做。

要点

  • 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
  • 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyingNSMutableCopying 协议。
  • 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
  • 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
2017/8/29 posted in  第三章 接口与API设计

第二十一条 理解Objective-C的错误模型

Objective-C语言不例外的也有“异常”(exception)机制,但是与其它语言肯定存在差异。我们要重新学习一下。

首先就是,“自动引用计数”(Automatic ReferenceCounting, ARC)在默认情况下不是“异常安全的"(exception safe)。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions

但是我们应该注意,Objective-C语言只有在极其罕见的情况下拋出异常,异常拋出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。

异常只应该用于极其严重的错误,比如说,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。

与其他语言不同,Objective-C中没办法将某个类标识为“抽象 类”。要想达成类似效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。这样的话,只要有人直接创建抽象基类的实例并使用它,即会拋出异常:

-(void)mustOverrideMethod {
NSString *reason = [NSStringstringWithFormat: 
                    @"%@ must be overridden",
                    NSStringFromSelector(_cmd)];
@throw [NSException
    exceptionWithName:NSInternalInconsistencyException 
    reason:reason 
    userInfo:nil];
}

既然异常只用于处理严重错误(fatal error,致命错误),那么对其他错误怎么办呢?在出 现“不那么严重的错误"(nonfatal error,非致命错误)时,Objective-C语言所用的编程范式为: 令方法返回nil/0,或是使用NSError,以表明其中有错误发生。例如,如果初始化方法无法根据传入的参数来初始化当前实例,那么就可以令其返回nil/0:

-(id)initWithValue:(id)value { 
    if ((self = [super init])){
        if ( /* Value means instance can11 be created */ ) { 
        self = nil;
     } else {
        // Initialize instance
        }
     }
    return self;
}

在这种情况下,如果if语句发现无法用传人的参数值来初始化当前实例(比如这个方法 要求传入的value参数必须是non-nil的),那么就把self设置成nil,这样的话,整个方法的 返回值也就是nil了。调用者发现初始化方法并没有把实例创建好,于是便可确定其中发生了错误。

NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。 NSError对象里封装了三条信息:

  • Error domain(错误范围,其类型为字符串)
    错误发生的范围。也就是产生错误的根源,通常用一个特有的全局变量来定义。比方说,“处理URL的子系统”(URL-handling subsystem)在从URL中解析或取得数据时如果出错了,那么就会使用NSURLErrorDomain来表示错误范围。

  • Error code(错误码,其类型为整数)
    独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。例如,当HTTP请求出错时,可能会把HTTP状态码设为错误码。

  • Uesr info(用户信息,其类型为字典)
    有关此错误的额外信息,其中或许包含一段“本地化的描述”(localized description), 或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条 “错误链”(chain of errors)

NSError的一种常见用法是,经由方法的“输出参数”返回给调用者。比如像这样:

-(BOOL)doSomething: (NSError**)error

用例为:

NSError *error = nil;
BOOL ret = [object doSomething:&error];
    if (error) {
//There was an error
}

也可以通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其委托对象(delegate)。这里不做过多说明。

要点

  • 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
  • 在错误不那么严重的情况下,可以指派“委托方法”(delegate method)来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。
2017/8/28 posted in  第三章 接口与API设计

第二十条 为私有方法名加上前缀

一个类所做的事情,通常要比外面看到的更多。编写类的实现代码时,我们经常要编写在内部使用的方法。这里我们要注意,一定要为这种内部实现的方法加上前缀,这有助于调试,有利于区分私有方法和公共方法。区分私有和公共方法,主要是为了方便修改内部的私有方法和相关实现代码,防止随意修改公共API。

我通常在私有方法前面加上“_p”,例如:


-(void)p_privateMethod{
    /*.....*/
}

我们之所以要加上前缀,是因为OC不像java和c++语言可以在前面将方法声明为私有。每个对象都可以响应任何方法(由于其强大的动态特性造成的)。

加前缀的时候要避免只用一个“_”因为苹果公司在自己的类中就是这么做的,所以我们要避开这种方式,防止造成继承一个类的时候,子类无意间覆写了父类的方法。

要点

  • 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
  • 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
2017/8/28 posted in  第三章 接口与API设计

第十九条 使用清晰而协调的命名方式

我们在使用OC的时候,发现这门语言很繁琐,代码中一般有“in“,”for”,“with”等介词,其他编程语言则很少使用这些他们认为多余的字眼。以下面代码为例子:

NSString *text = @"The quick brown fox jumped over the lazy dog";
NSString *newText =
[text stringByReplacingOccurrencesOfString: @"fox",
                                withString:@"cat"];

但是,Objective-C的命名方式虽然长一点,但是却非常淸晰。

方法与变量名使用了“驼峰式大小写命名法"(camel casing)——以小写字母开头,其后每个单词首字母大写。类名也用驼峰命名法,不过其首字母要大写,而且前面通常还有两三个前缀字母。

方法命名

方法名很长对冉繁琐,但是易于阅读,理解其中的意思。但是如果过长会起到反效果。例如:

-(EOCRectangle*)union:(EOCRectangle*)rectangle // Unclear 
-(float) calculateTheArea // Too verbose

//应该改成
-(EOCRectangle*)unionRectangle:(EOCRectangle*)rectangle 
-(float) area

给方法命名时的注意事项可总结成下面几条规则:

  • 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还 有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认 为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当 于原有的对象。这些存取方法应该按照其所对应的属性来命名。
  • 应该把表示参数类型的名词放在参数前面。
  • 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数, 则应该在动词后面加上一个或多个名词。
  • 不要使用str这种简称,应该用string这样的全称。
  • Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功 能,选用has或is当前缀。
  • 将get这个前缀留给那些借由“输出参数”来保存返回值的方法,比如说,把返回值 填充到“C语言式数组”(C_style array)里的那种方法就可以使用这个词做前缀。

类与协议的命名

应该为类与协议的名称加上前缀,以避免命名空间冲突(参见第15条),而且应该像给 方法起名时那样把词句组织好,使其从左至右读起来较为通顺。例如,在NSArray的子类中,有一个用于表示可变数组的类,叫做NSMutableArray, mutable这个词放在array前面, 用以表明这是一种特殊的array(数组)。

例如iOS的UI库UIKit,其中协议与类的命名惯例为:

  • UIView (类)
  • UIViewController(类)
  • UITableView (类)
  • UITableViewController(类)
  • UITableViewDelegate(协议)

最重要的一点就是,命名方式应该协调一致。

要点

  • 起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解。
  • 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
  • 方法名里不要使用缩略后的类型名称。
  • 给方法起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。
2017/8/26 posted in  第三章 接口与API设计

第十八条 尽量使用不可变对象

我们在设计类的时候,运用属性来封装数据。并且使用属性时候,可以将属性直接设置为“只读”(read-only)。默认情况下是“可读也可写”的。

不过,一般情况下我们要建模的数据未必需要改变。比方说,某数据所表示的对象源自一项只读的网络服务(web service),里面可能包含一系列需要显示在地图上的相关点,像这种对象就没必要改变其内容。即使修改了,新数据也不会推送回服务器.

而且如果把可变对象(mutable object)放入collection之后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。

我们来举例:

为了将EOCPointOflnterest做成不可变的类,需要把所有属性都声明为readonly:

#import <Foundation/Foundation.h>
@interface EOCPointOfInterest : NSObject
@property (nonatomic, copy, readonly) NSString *identifier; 
@property (nonatomic, copy, readonly) NSString* title;
@property (nonatomic, assign, readonly) float latitude; 
@property (nonatomic, assign, readonly) float longitude;
-(id) initWithldentifier: (NSString*) identifier 
                   title:(NSString*)title 
                   latitude: (float) latitude 
                   longitude: (float) longitude;
@end

这样后如果有人想要改变属性值,那么编译时就会报错。对象中的属性值可以读出,但是无法写入,这就能保证EOCPointOfluterest中的各个数据之间总是相互协调的。

但是我们有时想要修改封装在对象内部的数据,不想令这些数据为外人所改动。这种情况下,通常做法是在对象内部将readonly属性重新声明为readwrite。当然,如果该属性是nonatomic 的,那么这样做可能会产生“竞争条件”(racecondition)。在对象内部写人某属性时,对象外的 观察者也许正读取该属性。若想避免此问题,我们可以在必要时通过“派发队列"(dispatchqueue)等手段,将(包括对象内部的)所有数据存取操作都设为同步操作将属性在对象内部重新声明为readwrite这一操作可于“class-continuation分类”中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变,而readonly可扩展为readwrite

“class-continuation分类”可以这样写:

// .m文件中
#import "EOCPointOfInterest.h"

@interface EOCPointOfInterest : NSObject
@property (nonatomic, copy, readwrite) NSString *identifier; 
@property (nonatomic, copy, readwrite) NSString* title;
@property (nonatomic, assign, readwrite) float latitude; 
@property (nonatomic, assign, readwrite) float longitude;

@implementation EOCPointOfInterest

/* ... */

@end

现在,只能于EOCPoimOflnterest实现代码内部设置这些属性值了。但是我们其实可以同构KVC键值编码来设置这些属性值。不过,这样显然违背了我们的本意,绕过了提供的API。不推荐这种做法。

我们定义类公共的API时,要注意一件事情:对象里表示各种collection的那些属性究竞应该设成可变的,还是不可变的。例如,我们用某个类来表示个人信息,该类里还存放了一些引用,指向此人的诸位朋友。你可能想把这个人的全部朋友都放在一个“列表"(list)里,并将其做成属性。假如开发者可以添加或删除此人的朋友,那么这个属性就需要用可变的set来实现。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性将返回不可变的set, 而此set则是内部那个可变set的一份拷贝。比方说,下面这段代码就能够实现出这样一个类:

// EOCPerson.h
#import <Foundation/Foundation•h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName; 
@property (nonatomic, strong, readonly) NSSet *friends;
-(id)initWithFirstName:(NSString*)firstName
          andLastName:(NSString*)lastName;
-(void)addFriend:(EOCPerson*)person;
-(void)removeFriend:(EOCPerson*)person;

@end

// EOCPerson.m #import "EOCPerson•!!
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName; 
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
    NSMutableSet *_internalFriends;
}

-(NSSet*)friends {
    return [_internalFriends copy];
}
-(void)addFriend:(EOCPerson*)person {
    [_internalFriends addObject:person];
} 

-(void)removeFriend:(EOCPerson*)person {
    [_internalFriends removeObjectrperson];
}

-(id)initWithFirstName: (NSString*)firstName andLastName:(NSString*)lastName { 
    if ((self = [super init】)){
         _firstName = firstName;
         _lastName = lastName;
         _internalFriends = [NSMutableSet new];
    }
    return self;
}

@end

要点

  • 尽量创建不可变的对象。
  • 若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性
  • 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection
2017/8/26 posted in  第三章 接口与API设计

第十七条 实现description方法

调试程序时,我们一般要将对象的属性NSLog出来:

NSLog(@"object = %@",object);

如果我们输出的对象是数组,或者字典和其它数据是可以将数据打印出来。但是如果打印的是自定义的类,那么输出的对象就类似于:

object = <EOCPerson:0x7fd9a1600600>

上面这种信息很不实用,所以我们要在自己的类里覆写description方法,否则打印信息时就会调用NSObject类所实现的默认方法。此方法定义在NSObject协议里,不过NSObject类也实现了它。

想输出更为有用的信息也很简单,只需覆写description方法并将描述此对象的字符串 返回即可。例如,有下面这个代表个人信息的类:

//EOCPerson.h
#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

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

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

@end

//EOCPerson.m
#import "EOCPerson.h"

@implementation EOCPerson

-(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{
    if ((self = [super init])) {
        _firstname = [firstName copy];
        _lastName = [lastName copy];
    }
    return self;
}
//description方法
-(NSString*)description{

    return [NSString stringWithFormat:@"<%@ : %p , \"%@ %@\">" , [self class], self ,_firstname ,_lastName ];
}

@end


那么输出就是:

#import <Foundation/Foundation.h>
#import "EOCPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Liang" lastName:@"Zhonghao"];
        NSLog(@"Person = %@" , person);
    }
    return 0;
}

建议:在新实现的 description方法中,也应该像默认的实现那样,打印出类的名字和指针地址,因为这些内容 有时也许会用到。

我们也可以借助字典类型的description方法来将打印何种信息标识出来:


-(NSString*)description{
    return [NSString stringWithFormat:@"<%@ : %p , %@>" , 
    [self class],
     self, 
     @{@"firstname":_firstname,
       @"lastname":_lastName}];
}

输出为:

NSObject协议中还有个方法要注意,那就是debugDescription,此方法的用意与 description非常相似。二者区别在于,debugDescription方法是开发者在调试器(debugger) 中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description。我们还拿上个例子来说明:

我们在NSLog下面打一个断点,进入调试模式,之后向调试控制台里输入命令。LLDB的“po”命令 可以完成对象打印(print-object)工作,其输出如下:

当然我们可以把人名放在EOCPerson对象的普通描述信息中,而把更详尽的内容放在调试所用的描述信息里:

-(NSString*)description{
    return [NSString stringWithFormat:@"%@ : %@>" , _firstname ,_lastName ];
}

-(NSString*)debugDescription{
    return [NSString stringWithFormat:@"<%@ : %p , %@>" , [self class], self , @{@"firstname":_firstname,@"lastname":_lastName}];
}

运行之后如下:

你可能不想把类名与指针地址这种额外内容放在普通的描述信息里,但是却希望调试的 时候能够很方便地看到它们,在此情况下,就可以使用这种输出方式来实现。Foundation框架的NSArray类就是这么做的.

要点

  • 实现description方法返回一个有意义的字符串,用以描述该实例。
  • 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。
2017/8/26 posted in  第三章 接口与API设计