[译] Objective-C 中 9 种避免使用 Xcode 预处理器宏的方法

除了极少数例外,使用 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 等)是一种选择性启用或禁用代码块的方法。它用于不同的目的,但始终是一种\color{Red}{钝器}
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
侵删

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容