欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

[编写高质量iOS代码的52个有效方法](五)接口与API设计(下)

程序员文章站 2022-05-09 20:36:51
先睹为快 19.使用清晰而协调的命名方式 20.为私有方法名加前缀 21.理解Objective-C错误模型 22.理解NSCopying协议 第19条:使用清晰而协调的...

先睹为快

19.使用清晰而协调的命名方式

20.为私有方法名加前缀

21.理解Objective-C错误模型

22.理解NSCopying协议

第19条:使用清晰而协调的命名方式

类、方法及变量的命名是Objective-C编程的重要环节。其语法结构使得代码读起来和句子一样。名称中一般都带有in、for、with等介词,而其他编程语言则很少使用这些它们认为多余的字眼:

// C++
string text = "The quick brown fox jumped over the lazy dog";
string newText = text.replace("fox","cat");

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

text.replace(“fox”,”cat”)从字面上无法判断是用fox替换cat还是用cat替换fox,而Objective-C的命名方式虽然长一点,但却非常清晰。

C++或Java更习惯简省的函数名,在这种命名方式下,若想知道每个参数的用途,就得查看函数原型,这会令代码难于读懂,以一个矩形的类为例:

// C++
class Rectangle{
    public:
        Rectangle(float width, float height);
        float getWidth();
        float getHeight();
    private:
        float width;
        float height;
};

// Objective-C
@interface EOCRectangle : NSObject

@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;

- (id)initWithWidth:(float)width andHeight:(float)height;
@end

当创建该类实例时:

// C++
Rectangle aRectangle = new Rectangle(5.0f,10.0f);
// Objective-C
Rectangle *aRectangle = [[Rectangle alloc]initWithWidth:5.0f andHeight:10.0f];

C++代码无法判断5.0f和10.0f分别表示什么,就算能推测出来表示矩形的尺寸,也不知道宽度在先还是高度在先,需要查函数定义才能确定。而Objective-C的代码却很清晰。

虽说长名字可伶代码更为易读,但也不能长得太过分了,应尽量言简意赅:

// 好的方法名
- (EOCRectangle*)unionRectangle:(EOCRectangle*)rectangle
- (float)area

// 不好的方法名
- (EOCRectangle*)union:(EOCRectangle*)rectangle // 不清晰
- (float)calculateTheArea  // 太冗余

给方法命名时的注意事项可以总结为:

如果方法的返回值是新创建的,那么方法名的首个词应是返回值类型,除非前面还有修饰语。属性的存取方式不遵循这种命名方式。 应该把表示参数类型的名词放在参数前面。 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。 不要使用str这种简称,应该使用string这样的全称。 返回Bool值的方法应加上has或is前缀。 get这个前缀留给那些借由输出参数来保存返回值的方法。

第20条:为私有方法名加前缀

Objective-C语言没办法将方法标为私有,每个对象都可以响应任意消息。需要开发者在命名惯例中体现私有方法等语义。笔者喜欢用p_作为前缀,p表示private,而下划线可以把这个字母和真正的方法名区隔开。

#import 

@interface EOCObject : NSObject
- (void)publicMethod;
@end

@implementation EOCObject

// 公有方法
- (void)publicMethod{
    // code
}

// 私有方法
- (void)p_privateMethod{
    // code
}
@end

苹果公司喜欢单用一个下划线作私有方法的前缀,所以请不要照苹果公司的办法来做,不然有可能无意间重写父类的同名方法。

#import 

@interface EOCViewController : UIViewController
@end

@implementation EOCViewController
- (void)_resetViewController{
    // code
}
@end

以上代码看起来没有问题,但UIViewController类本身已经实现了一个名叫_resetViewController的方法。这样写的话所有调用都将执行子类中的这个方法。由于超类中的同名方法并未对外公布,除非深入研究这个库,否则根本不会察觉无意间重写了这个方法。

第21条:理解Objective-C错误模型

很多编程语言都有异常(exception)机制,Objective-C也不例外,但需要注意的是,ARC在默认情况下不是异常安全的。这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。想要生成异常安全的代码,需要打开编译器标志-fobjc-arc-exceptions。

Objecti-C现在所采用的方法是:只在极其罕见的情况下抛出异常,异常抛出之后,无需考虑恢复问题,应用程序直接退出。异常只用于处理严重错误,出现一般错误时,令方法返回nil/0,或使用NSError。

比如初始化无法根据传入的参数来初始化当前实例,那么就令其返回nil/0。

- (id)initWithValue:(id)value{
    if((self = [super init])){
        if(/* 无法用value初始化实例 */){
            self = nil;
        }else{
            // 初始化
        }
    }
    return self;
}

而NSError的用法更加灵活,可以把导致错误的原因回报给调用者。NSError对象里封装了三条信息:Error domain(错误范围,类型为字符串)、Error code(错误码,类型为整数)、User info(用户信息,类型为字典)。

// EOCErrors.h
#import 

// 声明全局变量错误范围
extern NSString *const EOCErrorDomain;

// 错误码
typedef NS_ENUM(NSUInteger, EOCError){
    EOCErrorUnkown = -1,
    EOCErrorInteralInconsistency = 100,
    EOCErrorGeneralFault = 105,
    EOCErrorBadInput = 500,
};

@interface EOCErrors : NSObject

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

@end

// EOCErrors.m
#import "EOCErrors.h"

NSString *const EOCErrorDomain = @"EOCErrorDomain";

@implementation EOCErrors

- (BOOL)doSomething:(NSError *__autoreleasing *)error{
    if(/* error发生 */){
        if (error) {
        *error = [NSError errorWithDomain:EOCErrorDomain code:EOCErrorGeneralFault userInfo:@{@"EOCErrorUserInfo":@"General fault occurs!"}];
        }
        return NO;
    }
    return YES;
}

@end

这样就能经由输出参数把NSError对象回传给调用者:

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

第22条:理解NSCopying协议

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

- (id)copyWithZone:(NSZone*)zone

现在每个程序只有一个区了(默认区),所以实现这个方法时不用担心其中的zone参数。copy方法由NSObject实现,该方法只是以默认区为参数来调用copyWithZone:方法。所以我们想的是重写copy方法,真正需要实现的是copyWithZone:方法。

下面是一个实现NSCopying协议的例子:

// EOCPerson.h
#import 

@interface EOCPerson : NSObject

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

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
@end

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

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

@implementation EOCPerson{
    NSMutableSet *_friends;
}

- (void)addFriend:(EOCPerson *)person{
    [_friends addObject:person];
}

- (void)removeFriend:(EOCPerson *)person{
    [_friends removeObject:person];
}

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

// 实现NSCopying协议
- (id)copyWithZone:(NSZone *)zone{
    // 以全能初始化方法初始化拷贝对象
    EOCPerson *copy = [[[self class]allocWithZone:zone]initWithFirstName:_firstName lastName:_lastName];
    // 将实例变量拷贝到拷贝对象
    copy->_friends = [_friends mutableCopy];
    return copy;
}

@end

本例中,_friends是使用mutableCopy方法来复制的,此方法来自另一个叫做NSMutableCopying的协议,与NSCopying类似,也只定义了一个方法:

- (id)mutableCopyWithZone:(NSZone*)zone

如果需要返回不可变的拷贝,则应该实现NSCopying协议,而若需要返回可变的拷贝,则应实现NSMutableCopying协议:

[NSMutableArray copy] // 返回NSArray
[NSArray mutableCopy] // 返回NSMutableArray

编写拷贝方法时,还要决定一个问题,就是应该执行深拷贝还是浅拷贝。深拷贝的意思就是:在拷贝对象自身时,将底层数据也一并复制过去。而浅拷贝只拷贝容器本身,而不复制其中数据。Foundation框架中的所有容器在默认情况下都执行浅拷贝。

在EOCPerson那个例子中,若需要深拷贝的话,可以编写一个专供深拷贝的方法:

- (id)deepCopy{
    EOCPerson *copy = [[[self class]alloc]initWithFirstName:_firstName lastName:_lastName];
    // copyItem参数设为YES,则会向容器中每个元素都发送copy消息,用拷贝好的元素创建新set返回给调用者。
    copy->_friends = [[NSMutableSet alloc]initWithSet:_friends copyItem:YES];
    return copy;
}