iOS程序猿iOS学习笔记iOS开发记录

文档翻译-Introduction to Coding Guid

2020-02-27  本文已影响0人  Jerry4me

介绍

​ 代码是写给程序员看的,所以代码命名需要一个约定俗成的规范,否则接口就会有歧义,别人则会因此被迷惑从而增加阅读、沟通成本。本文就是为了教你写出更通俗易懂的接口。

​ 引用:Introduction to Coding Guidelines for Cocoa

代码基础命名规范

这一章主要展示OOP(面向对象编程)容易被人忽视的一些关于类、方法、函数、常量以及编程接口的其他元素的命名。

基本原则

前缀

​ 前缀的重要性不言而喻,尤其是当你不得不使用其他人些的一些框架的时候,包括苹果的框架和第三方框架。前缀可以帮助我们避免冲突,例如类重复定义,分类方法命名重复等等...

书写约定

类和协议命名规范

​ 类名必须能从名称中一目了然这个类是干什么的,并且应该带有前缀。Foundation框架中的类处处可见,就不举例子了。

​ 协议则应根据这一组的行为来命名。

​ 大部分协议定义了一系列与类无关的方法,这种协议需要从命名上就与类区分开,最普遍的就是使用动名词的方法("…ing")

NSLocking // good
NSLock // bad,听起来就像一个类

​ 而有一些协议定义了一些列互不关联的方法,这种协议往往作为与协议主要表达式的类关系紧密。这种情况的话协议名就跟类名保持一致。最经典的例子就是NSObject<NSObject>

头文件

​ 头文件名字代表着这个文件里面包含着什么东西。

NSLocale.h // NSLocale这个类的声明,没别的了
NSLock.h // NSLocking协议,以及NSLock、NSConditionLock、NSRecursiveLock这几个类的声明
Foundation.h // Foundation.framework

方法命名规范

​ 这一节主要是讲如何正确命名我们平时打码中随处可见的元素 - Method。

通用规则

​ 以下有几条在给方法命名的时候必须牢记的准则:

存取方法(Accessor Methods)

​ 简单地说,存取方法即set方法和get方法

代理方法(Delegate Methods)

集合类型方法(Collection Methods)

​ 对于管理一个集合的对象,这里直接说容器了哈。约定好方法的形式如下:

- (void)addElement:(ElementType)element;
- (void)removeElement:(ElementType)element;
- (NSArray *)elements;

以下是一些细化的准则:

方法参数(Method Arguments)

直接上注意点吧:

私有方法(Private Methods)

​ 大部分情况下,私有方法命名跟公有方法是一样的。有个很简便的方法去区分私有方法就是给他加一个前缀。但是有个弊端就是,你不知道会不会无意中重写了苹果框架中的私有方法。。。

​ 但是苹果框架中的私有方法大部分都是以下划线作为前缀的。所以我们就尽量不要用下划线作为前缀明明我们自己的方法了。另外如果我们子类化一个使用特别广泛的类例如UIView,那么我们给自己的类加私有方法的时候可以用上独一无二的前缀,例如公司名+项目名XX_addObject:

函数命名规范

​ 函数命名我们需要遵循以下几个规范:

查询属性的函数还有一组命名规则:

以上这三个规则说实话我感觉有点过时了。。。大家看看就好

属性以及数据类型命名规范

​ 这一节讲的是属性、实例变量、常量、通知以及异常的命名规范。

属性和实例变量

​ 命名属性跟前面讲过的存取方法很类似,忘记了的话可以回去看看哦,举个例子应该就能马上回想起来了:

@property (nonatomic, strong) NSString *title;
@property (nonatomic, assign) BOOL showsAlpha;

如果属性表达的是形容词的话,省略is然后在getter方法里体现出来:

@property (nonatomic, assign, getter=isEditable) BOOL editable;

绝大部分情况下,你声明一个属性相当于同时合成(synthesize)了相应的实例变量, 例如:

@implementation MyClass {
  BOOL _showsAlpha;
}

你也可以手动加上@synthesize去决定相应的实例变量名,如:

@implementation MyClass
@synthesize showsAlpha = myShowsAlpha // 此时生成的实例变量名就为myShowsAlpha而不再是_showsAlpha了,一般不建议这样用

有几点需要注意的是:

常量

​ 常量怎么命名往往取决于这个常量是如何被创建的

枚举常量

​ 一般情况下,我们会使用枚举给一系列整形常量分组。具体命名规范和命名函数差不多。大家可以回头去看看,这里给一个例子

typedef enum _NSMatrixMode { // 这个_NSMatrixMode可省略
    NSRadioModeMatrix           = 0,
    NSHighlightModeMatrix       = 1,
    NSListModeMatrix            = 2,
    NSTrackModeMatrix           = 3
} NSMatrixMode; // 甚至连这个NSMatrixMode都可以省略

带const修饰符的变量

​ 可用const修饰符去创建一个跟其他常量没有任何联系的不可变常量,例如:

const float NSLightGray;

其他类型的常量

通知和异常

​ 通知和异常都有类似的命名规范。请往下看:

通知

​ 一般来说,如果一个类有代理(delegate)的话,他的通知都能找到其对应的代理方法。通知的命名一般包括以下几个部分:

[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification

​ 例如:UIApplicationDidBecomeActiveNotification

异常

​ 异常一般如此命名:

[Prefix] + [UniquePartOfName] + Exception

​ 例如:NSDraggingException

常见的缩写及首字母缩写(Acceptable Abbreviations and Acronyms)

​ 一般情况下不建议在写代码的时候使用英文单词缩写,除非是一些人尽皆知的或者前人已经使用过的缩写。例如,allocinitapp等等。另外还有一些名词如果不用缩写的话可能人们看了也不知道为何物,例如,XMLJPGPNG等等。这种情况下就建议直接用缩写了。

对SDK开发者的提示和技巧

​ 技巧都是通用的,不仅仅针对SDK开发者,可以运用在日常开发中。

初始化

​ 以下开始介绍SDK的初始化的建议。

类初始化(Class Initialization)

+initialize方法提供一个地方可以给你以懒加载的形式(即该类运行时首次被使用到的时候)执行某些一次性代码。一般来说会在这里设置版本号。

Runtime会自动帮我们调用继承链上每一个类的+initialize方法,即便你没有实现他,并且当子类没有实现该方法时会默认调用父类方法,所以该方法可能调用不止一次。如果想要保证写在这里的代码整个应用生命周期只会调用一起的话,请使用dispatch_once()

+ (void)initialize {
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        // the initializing code
    }
}

Note:

由于Runtime会帮我们调用每一个类的initialize方法,所以如果某子类没有实现initialize方法的话,就会发生在子类的上下文里调用父类的initialize方法的。所以如果想要在对应的上下文里实现相应的initialize方法,可以在方法里面判断一下类型,这种方法比dispatch_once更好。

if (self == [NSFoo class]) {
    // the initializing code
}

​ 永远不要自己去调用+initialize方法,如果你想要触发这个方法的话,随意调用一个没有副作用的方法即可,例如

[NSImage self];

指定的初始化器(Designated Initializers)

Designated Initializers简单地说就是一个类的init(假设init方法就是该类的Designated Initializers)方法会默认调用其父类的init方法,然后该类的其他初始化方法内部都会调用该类的init方法。例如UIView-initWithFrame:方法。但是如果你的类是类似NSSring和其他面向类族集Class clusters的抽象类,并且不想覆盖init方法,那么子类应该实现自己的初始化方法。

Designated Initializers应该被清楚地标识出来,因为如果其他类想要继承自你这个类的话,他只需要重写这个 Designated Initializers即可让其他初始化方法都按照设计完成初始化工作。

​ 当你在实现SDK中的类时,通常需要实现其归档和解档方法,-initWithCoder:-encodeWithCoder:。需要注意的是,不要在解档期间去做任何事情。如果你的类实现了<NSCoding>协议,那么最好在你的Designated Initializers中调用-initWithCoder方法(-initWithCoder本身也是Designated Initializers)。

初始化期间的错误检测

​ 一个好的设计的初始化方法应该完成以下3个步骤确保正确地检测和传播错误:

  1. 调用super的指定的初始化器后给self重新赋值
  2. 检查self是否为nil,这一步主要检测调用super的初始化方法是否成功
  3. 如果在初始化过程中发生了错误,释放自身,并返回nil

例子如下:

- (id)init {
    self = [super init];  // Call a designated initializer here.
    if (self != nil) {
        // Initialize object  ...
        if (someError) {
            [self release];
            self = nil;
        }
    }
    return self;
}

版本控制和兼容性

​ 通常来说,当你为你的SDK加了新方法、类时,没必要为每个新功能组指定一个新的版本号。开发正可以通过OC的runtime方法respondsToSelector:去验证在指定系统下该方法是否可用。

​ 但是你可以采用多种技术手段去确保SDK的每个新版本都记性正确标号,并且提高对旧版本的兼容性。

SDK版本号

​ 如果没办法简单地通过runtime tests的方式去检测新功能或bug修复的存在,那么则应该为开发人员提供方式来检查更改。一种方法就是存储SDK的版本号,然后开发人员可以根据这个版本号去查相应资料:

键控存档(Keyed Archiving)

​ 如果SDK中的对象需要被写入nib文件中,那么他们必须自己能够归档。除此以外,你还需要利用归档机制去归档所有文档数据。

​ 有以下几点需要注意:

异常和错误

​ 大多数Cocoa的框架都不强制开发者去捕捉并处理异常。因为异常不会在正常使用过程中抛出,并且通常不用于传达预期的运行时或用户错误。这些错误的例子包括:

然而,当出现变成或逻辑错误时Cocoa会抛出异常,例如:

理想的情况下,开发者在应用上线之前的测试环节中就能捕捉到这些类型的异常并且解决掉,因此,应用无需在运行时处理异常。如果一个异常抛出并且应用没有去捕捉他时,最顶层的默认handler将会捕捉并上报这些异常然后继续执行程序。开发者可以选择自己去捕捉并处理这些异常,提供选项让用户保存数据并退出应用程序。

错误是Cocoa的框架鱼其他软件库的一方面。Cocoa的方法一般不返回错误代码。如果存在一个合理的或者类似错误的原因,则该方法依赖对布尔值或者对象是否为空的简单判断;并且在文档中应能够查到返回NO或者nil的原因。你不应使用错误代码来指示要在运行时处理编程错误,而应引发异常,或者在某些情况下,只需记录错误而不引发异常即可。

例如,NSDictionaryobjectForKey:方法要么返回找到对象,要么找不到时返回nil。NSArrayobjectAtIndex:方法则永不能返回nil,因为NSArray对象不能存储nil并且根据定义,任何越界访问都是编程错误,应导致异常。许多init方法当他们根据提供的参数无法初始化时也会返回nil。

在少数情况下,一个方法确实需要多个不同的错误代码,这种情况则应在按引用参数中指定他们,该参数返回错误代码、本地化错误字符串或一些其他的描述错误的信息。

框架数据

​ 处理框架数据的方式会影响性能、跨平台稳定性和其他一些目的。本节讨论涉及框架数据的技术。

常量数据(Constant Data)

​ 从性能方面考虑,最好将尽可能多的框架里面的数据标记为常量,这样能减少Mach-O二进制文件的__DATA段的大小。不是const修饰的全局和静态数据最终都会出现在__DATA段的__DATA节中。这种数据会占用每一个使用到该框架的应用中的部分内存。尽管额外的500字节(例如)看起来无足轻重,但可能会导致所需的页数增加-每个应用额外增加4KB。

​ 你应该将任何常量数据用const来修饰。如果block中没有char *指针,那么就会导致数据被放在__TEXT段(这会使这段代码真正地保持不变);否则这段代码就会被放在__DATA段中,但不会被写入(除非未执行预绑定或者因为必须在加载时滑动二进制文件而违反了预绑定)

​ 你应该初始化静态变量去确保他们被合并到__DATA段的__DATA节中,而不是__bss节中。如果没有明显的值可用于初始化,请使用0、NULL、0.0或者适当的值。

位域(Bitfields)

​ 如果代码假定该值是布尔值的话,使用带符号的值作为位域(尤其是一位位域)则往往会导致奇奇怪怪的行为。因为只能在这样的位域中存储的值是0和-1(取决于编译器的实现),所以讲该位域与1进行比较是错误的。例如,如果你在代码里遇到这种情况:

BOOL isAttachment:1;
int startTracking:1;

​ 你应该把类型改为unsigned int

​ 位域的另一个问题是归档。通常不应该以他的表现形式去写入磁盘或归档,因为在另一种架构或者编译器上面再次读取的时候格式可能会不一样。

内存分配(Memory Allocation)

​ 在写框架的时候,尽量避免完全分配内存。如果处于某种原因需要临时缓冲区,通常使用堆栈要比分配缓冲区更好。但是堆栈的大小有限制(通常总共大小为512KB),因此是否使用堆栈取决于功能和所需缓冲区的大小。通常,如果缓冲区的大小小于等于1000字节,则可以使用堆栈。

​ 一种策略是一开始使用堆栈,如果大小增长到超出了堆栈缓冲区的大小,则切换成分配到内存的缓冲区。以下代码就是做的这件事:

#define STACKBUFSIZE (1000 / sizeof(YourElementType))
YourElementType stackBuffer[STACKBUFSIZE];
YourElementType *buf = stackBuffer;
int capacity = STACKBUFSIZE;  // In terms of YourElementType
int numElements = 0;  // In terms of YourElementType
 
while (1) {
    if (numElements > capacity) {  // Need more room
        int newCapacity = capacity * 2;  // Or whatever your growth algorithm is
        if (buf == stackBuffer) {  // Previously using stack; switch to allocated memory
            buf = malloc(newCapacity * sizeof(YourElementType));
            memmove(buf, stackBuffer, capacity * sizeof(YourElementType));
        } else {  // Was already using malloc; simply realloc
            buf = realloc(buf, newCapacity * sizeof(YourElementType));
        }
        capacity = newCapacity;
    }
    // ... use buf; increment numElements ...
  }
  // ...
  if (buf != stackBuffer) free(buf);

对象比较(Object Comparison)

​ 值得注意的是,通用的对象比较方法-isEqual:和与对象类型关联的比较方法如-isEqualToString:有着很大的区别。-isEqual:方法允许你讲任意对象作为参数传递,如果对象不是同一类,则返回NO。而-isEqualToString:-isEqualToArray:之类的方法通常假定参数为指定的类型(即接收方的类型)。因此,他们内部不会进行类型检查,因此他们执行速度更快、但并不那么安全。对于外部资源,例如应用程序的info.plist或首选项Preferences,首选使用-isEqual:方法,因为他更安全;当已知类型时,请使用-isEqualToString:

​ 关于-isEqual:方法的另一点是他与hash方法的关系。对于Cocoa集合中的对象(例如NSDictionary或者NSSet),一个基本不变式是如果[A isEqual:B] == YES,那么[A hash] == [B hash]。因此,如果在类中重写了-isEqual:方法,则还需要重写-hash方法以保证该不变式。默认情况下,-isEqual:方法查找每个对象地址的指针相等,并且hash根据每个对象的地址返回一个哈希值,因此该不变量成立。

上一篇 下一篇

猜你喜欢

热点阅读