除了极少数例外,使用 Xcode 预处理器宏是一种代码气味。C++ 程序员们已经深有体会:"不要使用预处理器来做语言本身提供的事情"。不幸的是,还有很多的 Objective-C 程序员尚未领悟到这一点。
本文是Objective-C 中的代码气味系列文章中的一篇。
这是一个可以在终端运行的便捷命令。它可以检查并显示当前目录下的源文件,预处理器宏的使用情况,你应该仔细检查。
find . \( \( -name "*.[chm]" -o -name "*.mm" \) -o -name "*.cpp" \) -print0 | xargs -0 egrep -n '^\w*\#' | egrep -v '(import|pragma|else|endif)'
该命令包含一些例外情况。例如,#import
指令至关重要。......但我想对几乎所有其他内容提出质疑!这有什么关系呢?因为每次使用预处理器时,你看到的并不是你编译的内容。对于作为常量使用的 #define
宏,我们需要避免一些陷阱——其实我们完全可以避免这些陷阱。
以下是一些常见的 Xcode 预处理器宏,以及如何替换它们:
1、#include
让我们从传统 C 中的一个简单例子开始:
Smell
#include "foo.h"
除非您提供的是平台无关的 C 或 C++ 代码,否则没有理由使用 #include
以及与之一起的 include guards。使用 #import
可以省去那些 include guards的 #ifndef
。
2、Macros - 宏
Smell
#define WIDTH(view) view.frame.size.width
使用 Objective-C 并不意味着不能使用普通的 C 语言函数!除非您的自定义宏依赖于 Xcode 预处理器宏(如__LINE__
),否则请将其重写为一个独立函数。(即便依赖于 Xcode 预处理宏,也要让您的宏调用另一个函数,并尽可能多地转移到该函数中)。
C 语言和 C++ 的有一些相似的地方。其中之一就是内联函数的能力:
static inline CGFloat width(UIView *view) { return view.frame.size.width; }
3、常量:数字常量
现在,我们开始使用一组围绕常量的 Xcode 预处理器宏。使用常量而不是重复字面值是值得称赞的。而使用 #define
创建常量则不值得称赞。
Smell
#define kTimeoutInterval 90.0
如果一个常量只在单个文件中使用,则应将其设置为静态常量。我们赋予常量一个明确的类型,增加了它的语义。如果你愿意,数字字面的表达也可以更简单,因为显式类型明确了可接受的值域。下面就是我们得到的结果:
static const NSTimeInterval kTimeoutInterval = 90;
如果一个常量是跨文件共享的,那么就像处理其他文件一样:在头文件中创建一个声明,在一个实现文件中创建一个定义。(当然,你要遵循苹果公司的编码指南,在名称上使用前缀,对吗?)因此,.h 文件中将包含如下声明:
extern const NSTimeInterval JMRTimeoutInterval;
.m文件中有定义:
const NSTimeInterval JMRTimeoutInterval = 90;
4、常量: 升序整数常量
Smell
#define firstNameRow 0
#define lastNameRow 1
#define address1Row 2
#define cityRow 3
// etc.
升序整数常量在编码表格视图时非常方便,可以确定哪些信息属于哪个单元格。......这就是枚举类型的作用。
enum {
firstNameRow,
lastNameRow,
address1Row,
cityRow,
// etc.
};
枚举类型可以方便地重新排列顺序或添加新值。一般来说,人们使用 #define
是因为构造一个危险的宏比构造一个安全的常量更容易。但在这里,语言所提供的不仅更安全,而且更简单。
枚举类型不必命名。但如果将这些值作为参数传递,就需要定义一个类型名,以增加编译器检查和语义。与其在所有需要使用 Address
枚举类型的地方都写 enum Address
,不如创建一个这样的类型定义:
typedef enum {
firstNameRow,
lastNameRow,
address1Row,
cityRow,
// etc.
} AddressRow;
5、常量:字符串常量
Smell
#define JMRResponseSuccess @"Success"
与数字常量一样,使用语言来定义常量。只不过,这次我们定义的是一个常量字符串,它实际上是一个对象,在 Objective-C 中表示为指针。因此,我们要定义一个常量指针。
常量字符串通常在多个文件中共享,因此这里介绍如何在 .h 文件中声明常量:
extern NSString *const JMRResponseSuccess;
因此,.m 文件中的定义是
NSString *const JMRResponseSuccess = @"Success";
6、条件编译:注释代码
各种形式的条件编译(#if
、#ifdef
等)是一种选择性启用或禁用代码块的方法。它用于不同的目的,但始终是一种。
Smell
#if 0
…
#endif
在以前的 C 语言中,唯一的注释形式是 /* ... */。要注释一段代码,可以在前面加上 /*,在后面加上 */。后来有人发现,如果代码中已经包含了注释,这种方法就不起作用了。怎么办呢?当时的答案是使用预处理器:用 #if 0
封装代码就可以了。
但那是很久以前的事了,那时还没有现代集成开发环境和彩色编码方式。颜色编码可以帮助我们更直观地解析代码......但在这种情况下并不适用。尽管在这种情况下有一个 0,但一般来说,集成开发环境无法知道是否要显示条件编译删除了源文件中的某段代码。因此,没有任何可视化指示器显示代码被注释掉了!它看起来就像其他代码一样。
C 和 Xcode 快速发展到今天。C 语言不断发展,并采用了 C++ 的 //
注释风格。Xcode 充分利用了这一点,并在菜单中提供了 "注释选择 "命令。只需按⌘/
即可注释出代码的一部分:Xcode 会在每一行的开头添加 //
并用颜色标记为注释。再次按下 ⌘/
,过程就会逆转,代码就会恢复原状。
因此,Xcode 可以轻松启用和禁用代码。但还有一个问题,我们将在下一节中讨论:如果注释掉的代码是临时性的,并且您计划很快将其清理干净,那么注释掉代码是没有问题的。但通常情况下,这些代码会被丢在那里任其腐烂......
7、条件编译:在实验之间切换
Smell
#if EXPERIMENT
…
#else
…
#endif
有时,您需要进行实验性编码。或者你想快速在两种方法之间来回切换。这很好。
但在某些时候,我们会做出决定。实验方法得到验证,你就可以准备发货了。自行清理之后!除非有重要的历史原因需要将被拒绝的代码作为注释保留,否则请将其删除。如果您选择保留,请删除 Xcode 预处理器宏。将它变成真正的注释,并附上解释,而不仅仅是代码。
8、 条件编译: 在暂存和生产 URL 之间切换
Smell
#if STAGING
static NSString *const fooURLString = @"https://dev.foo.com/services/fooservice";
#else
static NSString *const fooURLString = @"https://foo.com/services/fooservice;
#endif
当你开发基于服务的应用程序时,你希望能够指定是与真正的生产服务对话,还是与暂存服务对话。
对于只有少量 URL 的简单应用程序,我会为 URL 创建一个类,然后通过方法访问它们:
- (NSString *)fooURLString
{
DebugSettings *debugSettings = [self debugSettings];
if ([debugSettings usingStaging])
return @"https://dev.foo.com/services/fooservice";
else
return @"https://dev.foo.com/services/fooservice";
}
对于与许多服务对话的复杂应用程序,可以考虑将 URL 放入 plist 中。有关 plist 的示例,请参阅《我如何在暂存和生产 URL 之间切换(How I Switch between Staging and Production URLs)》。
9、条件编译:支持多个项目或平台
Smell
#if PROJECT_A
…
#else
…
#endif
在多个项目(或多个平台)中共享代码时,很容易在共享源文件中偷偷加入特定于项目的扩展。这样做看似方便,但会污染源代码,并掩盖统一代码的机会。
我们使用的是面向对象的语言,所以让我们使用 OO 模式,好吗?基本策略是将包含项目特定代码的方法改写为模板方法(Template Methods),由项目特定的子类提供项目特定的操作。
步骤
- 为每个项目变量创建一个子类。
- 在每个项目中,为该项目添加子类。
- 编译每个项目。
- 创建一个工厂方法,使用
#if
创建正确的子类。(我们引入预处理器的一种用法,这样就可以消除其他用法)。 - 找到每个实例化原始类的地方。让它调用工厂方法。
- 编译和测试每个项目。
- 对于每个有条件编译的部分:
- 执行提取方法,确定所需的签名。
- 将主体的每个平台特定部分向下移动到平台特定子类,直到基类的方法为空。
- 编译和测试每个项目。
- 查找每个子类内部以及子类之间的重复代码。
如果你的代码中存在多个特定于平台的子类层次结构,你可能会发现使用桥接模式的机会。
避免使用 Xcode 预处理器宏!
请再次在终端中执行此命令,以查找代码中可能违规的 Xcode 预处理器宏。您找到了多少?能否减少它们?剩余的宏是否合理?
请记住不要使用 Xcode 预处理器宏来做语言本身提供的事情!
译自 Jon Reid 的 9 Ways You Can Avoid ObjC Xcode Preprocessor Macros
侵删
网友评论