本文谈谈 iOS 开发中的命名规范,主要涉及常量命名、枚举命名、类及其方法命名,以及分类及其方法命名。如果你找的是官网的编码规范,请移步: Coding Guidelines for Cocoa。当然本文会讲一些官网没有的东西。

常量命名

iOS 开发中,肯定避免不了要命名一些常量,那么,我们应该怎样来命名常量呢?

在讨论上述问题前,先来了解定义常量的两种方式。

第一种,使用 #define 预处理定义常量。例如:

#define ANIMATION_DURATION 0.3

定义一个 ANIMATION_DURATION 常量来表示 UI 动画的一个常量时间,这样,代码中所有使用 ANIMATION_DURATION 的地方都会被替换成 0.3,但是这样定义的常量无类型信息,且如果你在调试时想要查看 ANIMATION_DURATION 的值却无从下手,因为在预处理阶段ANIMATION_DURATION 就已经被替换了,不便于调试。因此,弃用这种方式定义常量。

第二种,使用类型常量。将上面的预处理定义常量修改成类型常量:

static const NSTimeInterval kAnimationDuration = 0.3;

这样就为常量带入了类型信息,那么定义类型常量又有什么规范呢?

  • 对于局限于某编译单元(实现文件)的常量,通常以字符k开头,例如上文中的 kAnimationDuration,且需要以 static const 修饰,例如:

static const NSTimeInterval kAnimationDuration = 0.3;

  • 对于定义于类头文件的常量,外部可见,则通常以定义该常量所在类的类名开头,例如 EOCViewClassAnimationDuration, 仿照苹果风格,在头文件中进行 extern 声明,在实现文件中定义其值:
1
2
3
4
5
6
7
8
EOCViewClass.h

extern const NSTimeInterval EOCViewClassAnimationDuration;


EOCViewClass.m

const NSTimeInterval EOCViewClassAnimationDuration = 0.3;

对于字符串常量,则会像这样:

1
2
3
4
5
6
7
8
EOCViewClass.h

extern NSString *const EOCViewClassStringConstant;


EOCViewClass.m

NSString *const EOCViewClassStringConstant = @"EOCStringConstant";

常量定义是从右往左解读,上面的示例中就是定义了一个常量指针,其指向一个 NSString 对象, 这样该常量就不会被随意修改。至于这种暴露出来的类常量最前面是否加上字母k, 可以根据自己习惯在团队中进行约定,因为从 iOS 的接口中我看到这两种情况都有, 如

1
2
NSString *const UIApplicationLaunchOptionsRemoteNotificationKey;
NSString *const UIApplicationLaunchOptionsLocalNotificationKey;

还有:

1
2
NSString *const kCAAnimationCubic;
NSString *const kCAAnimationCubicPaced;

用枚举表示状态、选项、状态码

项目中,可用枚举来表示一系列的状态、选项和状态码。例如 iOS SDK 中表示 UICollectionView 滑动方向的枚举定义如下:

1
2
3
4
typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {
UICollectionViewScrollDirectionVertical,
UICollectionViewScrollDirectionHorizontal
};

或者定义 UITableView Style 的枚举:

1
2
3
4
typedef NS_ENUM(NSInteger, UITableViewStyle) {
UITableViewStylePlain, // regular table view
UITableViewStyleGrouped // preferences style table view
};

从这里可以看出,定义的枚举类型名称应以 2~3 个大写字母开头,而这通常与项目设置的类文件前缀相同,跟随其后的命名应采用驼峰命名法则,命名应准确表述枚举表示的意义,枚举中各个值都应以定义的枚举类型开头,其后跟随各个枚举值对应的状态、选项或者状态码。

对于需要以按位或操作来组合的枚举都应使用 NS_OPTIONS 宏来定义,例如 SDK 中:

1
2
3
4
5
6
7
8
9
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

这样定义的选项能够以 按位或操作符 来进行组合,枚举中每个值均可启用或者禁用某一选项,在使用时,可以使用 按位与操作符 来检测是否启用了某一选项,如下:

1
2
3
4
5
6
UIViewAutoresizing resizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

if (resizing & UIViewAutoresizingFlexibleWidth) {
// UIViewAutoresizingFlexibleWidth is set

}

另外,我们可能使用 switch 语句时,会在最后加上 default 分支,但是若用枚举定义状态机,则最好不要使用 default 分支,因为如果稍后再加了一种状态,那么编译器就会发出警告,提示新加入的状态并未在 switch 分支中处理。假如写上了 default 分支,那么它就会处理这个新状态,从而导致编译器不发出警告,用 NS_ENUM 定义其他枚举类型时也要注意此问题。例如在定义代表 UI 元素样式的枚举时,通常要确保 switch 语句能正确处理所有样式。

总结一下:

  • 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
  • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为 2 的幂,以便通过按位或操作将其组合起来。
  • 用 NS_ENUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样就可以确保枚举是用开发者所选的底层数据类型实现出来的,而不是采用编译器所选的类型。
  • 在处理枚举类型的 switch 语句中不要实现 default 分支。这样加入新的枚举值之后,编译器就会发出警告提示,switch 还有未处理的枚举值。

类及其方法命名

Objective-C 没有其他语言那种内置的命名空间(namespace)机制。因此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。避免此问题的唯一方法就是变相实现命名空间: 为所有名称都加上适当前缀。所选前缀可以是与公司、应用程序或者二者皆有关联的名字。使用 Cocoa 和 Cocoa Touch 创建应用程序时一定要注意,Apple 宣传其保留使用所有"两个字母前缀"(tow-letter prefixed)的权利。所以你自己选用的前缀应该是三个字母的。你可以在苹果官网 Class Names Must Be Unique Across an Entire App 看到上述说明:

In order to keep class names unique, the convention is to use prefixes on all classes. You’ll have noticed that Cocoa and Cocoa Touch class names typically start either with NS or UI. Two-letter prefixes like these are reserved by Apple for use in framework classes.

Your own classes should use three letter prefixes. These might relate to a combination of your company name and your app name, or even a specific component within your app.

You should also name your classes using a noun that makes it clear what the class represents, like these examples from Cocoa and Cocoa Touch:

NSWindow | CAAnimation | NSWindowController | NSManagedObjectContext

另外,在文档 Coding Guidelines for Cocoa 中提到:

Use prefixes when naming classes, protocols, functions, constants, and typedef structures. Do not use prefixes when naming methods; methods exist in a name space created by the class that defines them. Also, don’t use prefixes for naming the fields of a structure.

这里需要说明两点:

  • 上述所说的functions指的是纯 C 函数。对于纯 C 函数和全局变量,不论其处于头文件或者其实现文件中,在编译好的目标文件中,这些名称要算作"顶级符号"(top-level symbol)的。因此我们总应该为这种 C 函数的名字加上前缀。通常情况下,这类 C 函数我们可以以定义其类的名字作为前缀。这样,在调试时,若此符号出现在栈回溯信息中,则很容易就能判明问题源自哪块代码。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ACLSoundPlayer.h

#import <Foundation/Foundation.h>

@interface ACLSoundPlayer : NSObject

@end


ACLSoundPlayer.m

#import "ACLSoundPlayer.h"
#import <AudioToolbox/AudioToolbox.h>

void ACLSoundPlayerCompletion(SystemSoundID ssID, void *clientData) {

}

@implementation ACLSoundPlayer

@end
  • 上述引用说到不应该为 Objective-C methods 添加前缀,我觉得应该添加一个例外,当我们定义分类中的方法时,则总应该为其添加前缀,这会在下一条详细说明。

最后一种情况,若为第三库编写自己的代码,并准备将其发布为程序库供他人开发应用程序所用时,你应该给你所用的那份第三方库代码都加上你自己的前缀。为便于说明,假如你要发布的程序库叫 EOCLibrary,你所使用的第三方库叫 XYZLibrary,则你应该把你使用的 XYZLibrary 中所有名字都冠以 EOC, 成为 EOCXYZLibrary, 原因如下:

  • 你的程序所包含的那个第三方库也许还会为应用程序本身所引入。
  • 你可能会想,让应用程序本身不要直接引入 XYZLibrary,改用 EOCLibrary 中使用的那个,但是,应用程序也许还会引入另一个名为 ABCLibrary 的第三方库,而该库中又包含了 XYZLibrary。此时,如果你和 ABCLibrary 的作者都不给各自所用的 XYZLibrary 加前缀,那么应用程序依然会出现重复符号错误。
  • 你的库里所引用的第三方库是 X 版本的,而应用程序却需要引用第三库的 Y 版本的功能。

对于类中的方法命名,应遵循以下规则:

  • 如果方法的返回值是新建的,那么方法名的首个词应是返回值的类型,例如: +stringWithString:。除非前面还有修饰语,例如 localizedString。属性的存取方法不遵循这种命名方式。
  • BOOL属性应加 is 前缀。如果某方法返回非属性的 Boolean 值,那么应该根据你功能,选用 has 或 is 当前缀。

分类及其方法命名

分类机制通常用于向无源码的既有类中新增功能。分类中的方法是直接加在类里面的,它们就好比这个类固有的方法,将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类中又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了"主实现"中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法,多次覆盖的结果以最后一个分类为准。当有多份实现时,无法确定运行时使用的是那份实现。由这样引发的 bug 很难追查。

那么,如何来最大限度的避免这种覆盖呢?

在 iOS 开发中,没有命名空间的概念。通常,我们通过为项目中所有类命名加上特有的前缀来实现命名空间的功能,来避免可能发生的与使用第三方库中的方法命名相同。同样的,这里我们也是为分类,以及分类中的所有方法都加上特定前缀。这个前缀应该与应用程序库中其他地方所用的前缀相同,通常会使用公司名或应用程序名来做决定。例如来编写一个判断对象是否为空的分类方法:

1
2
3
4
5
6
7
NSObject+ACLNetworkingMethods.h

@interface NSObject (ACLNetworkingMethods)

- (BOOL)acl_isEmptyObject;

@end

这里为分类名以及分类中的方法加上了 ACL 的前缀。注意在方法中,需前缀小写。

另外,使用分类时,不要刻意覆写分类中的方法,尤其是当你把代码发布为程序库供其他开发者使用时,因为你无法决定其他开发者需要的是方法哪份实现。

总结:

  • 向第三方类中添加分类时,总应该给分类名称加上你专有的前缀,前缀须大写。
  • 向第三方类中添加分类时,总应该给分类中的方法名加上你专有的前缀,前缀须小写,且以下划线连接前缀与方法名。

本文内容主要来自[编写高质量iOS与OS X代码的52个有效方法] 一书,欢迎留言交流。


如果觉得本文对你有帮助,就请用微信随意打赏我吧^_^