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

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

第十六条 提供"全能初始化方法"

我们知道,所有的对象都要初始化但是有些对象可能有很多初始化方法(根据初始的信息来选择用哪个).我们以iOS的UI框架UIKit为例,其中有个类叫做UITableViewCell,初始化该类对象 时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。由于这种对象的创 建成本较高,所以绘制表格时可依照标识符来复用,以提升程序效率。我们把这种可为 对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer)

我们看下面这个NSDate的例子:

-(id)init
-(id)initWithString:(NSString*)string
-(id)initWithTimelntervalSinceNow:(NSTimelnterval)seconds
-(id)initWithTimelnterval:(NSTimelnterval)seconds
                sinceDate:(NSDate*)refDate
-(id)initWithTimeIntervalSinceReferenceDate:(NSTimelnterval)seconds
-(id)initWithTimeIntervalSincel970:(NSTimelnterval)seconds

那么多的初始化方法中,我们要选一个全能初始化方法,让其他的初始化方法都来调用它。,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。

我们来用代码举例:

首先定义一个表示矩形的类:

#import <Foundation/Foundation.h>
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height; 
@end

然后定义一个初始化方法:

-(id) initwithwidth: (float) width andHeight:(float)height
{
    if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}

这样就会有一个问题,当有人用[[EOCRectanglealloc]init]来创建矩形时,因为NSObject中已经实现了init方法,如果把alloc方法分配好的EOCRectangle交由此方法来初始化,那么矩形的宽度与高度就是0,因为全部实例变量都设为0了。这种情况我们应该覆写init方法:

// Using default values
-(id)init {
    return [self initWithWidth:5.Of andHeight:10.Of];
)
// Throwing an exception
-(id)init {
    @throw [NSException
    exceptionWithName:NSInternalInconsistencyException 
    reason:@"Must use initWithWidth:andHeight: instead." 
    userInfo:nil];
}

还有一种情况,当我们创建名叫EOCSquare的类,令其成为EOCRectangle的子类时,新类的初始化方法写的时候要注意:

@import "EOCRectangle.h"
@interface EOCSquare : EOCRectangle 
-(id)initWithDimension:(float)dimension; 
@end

@implementation EOCSquare
-(id)initWithDimension:(float)dimension {
    return [super initwithwidth:dimension andHeightidimension];
}
@end

这了我们发现上面代码的初始化方法调用了父类的初始化方法,这样可能会导致一个问题:创建出一个”高度”和“宽度”不相等的正方形。所以:如果子类的全能初始化方法与超类方法的名称不 同,那么总应覆写超类的全能初始化方法。EOCSquare这个例子中,应该像下面这样覆写EOCRectangle的全能初始化方法:

-(id)initWithWidth:(float)width andHeight:(float)height { 
    float dimension = MAX (width, height);  
    return [self initWithDimension:dimension];
}

覆写了这个方法之后,即便使用init来初始化EOCSquare对象,也能照常工作。原因在于, EOCRectangle类覆写了 init方法,并以默认值为参数,调用了该类的全能初始化方法。在用init方法初始化EOCSquare对象时,也会这么调用,不过由于“initWithWidth:andHeight:”已经在子类中覆写了,所以实际上执行的是EOCSquare类的这一份实现代码,而此代码又会调用本类的全能初始化方法。因此一切正常,调用者不可能创建出边长不相等的EOCSquare对象。

当然如果我们不想覆写父类的全能初始化方法,认为这是调用者自己犯了错误。在这种情况下,常用的办法是覆写超类的全能初始化方法并于其中抛出异常:

-(id) initwithwidth: (float) width andHeight: (float) height {
    @throw 
        [NSException 
            exceptionWithName:NSInternallnconsistencyException
            reason: @"Must use initWithDimension: instead."
             userInfo:nil];

有时如果某个队形对象的实例有两种完全不同的创建方式,必须分开处理,所以就要编写多个全能初始化方法。只要记住每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,应该先调用超类的相关方法,然后再执行与本类有关的任务。

要点

  • 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。

  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

2017/8/25 posted in  第三章 接口与API设计

第十五条 用前缀避免命名空间冲突

oc与其他语言一个主要区别是没有那种内置的命名空间(namespace)机制. 所以我们在编码时一定要注意命名冲突,尤其是潜在的命名冲突.

我们在创建应用程序时一定要注意,使用Cocoa创建应用程序时一定要注意,Apple宣称保留使用"两个字母前缀"的权利,也就是说选用的前缀应该都是三个字母.

我们尤其应该注意,当你使用的一个三方库引入了一个你之前引入过的三方库.如果引入的这两个库的作者都没有给自己的库加前缀,那么应用程序很容易出现重复符号错误.

要点

  • 选择与你公司,应用程序或二者有关联的名称作为类名的前缀,并在所有代码中均使用这一前缀.

  • 若自己所开发的程序库用到了第三方库,则应该为其中的名称加上前缀.

2017/8/25 posted in  第三章 接口与API设计