<翻译>[Code Smells]预处理中的9个代码异味

#预处理中的9个代码异味

Every time you use the preprocessor, what you see isn’t what you compile.

每次你使用预处理的时候,你看到的都不是你编译的。

除了一些例外的情况,使用C的预处理机制本身就是代码异味的表现。C++开发者心中都知道:“假如语言本身就提供了这个功能,就不要使用预处理”。但是很不幸,很多的Objective-C开发者没有这种意识。

下面是一个很方便的命令(在命令行工具中使用)。这个命令会检查当前目录下的源代码文件,将所有的预处理命令显示出来。

find . \( \( -name "*.[chm]" -o -name "*.mm" \) -o -name "*.cpp" \) -print0 | xargs -0 egrep -n '^\w*\#' | egrep -v '(import|pragma|else|endif|HC_SHORTHAND)'

这个命令有一些例外的情况。例如:#import指令很重要、#pragma标记很有用……但是,我想质疑这一切!为什么要质疑呢?因为每次你使用预处理,你看到的并不是你编译的。比如,用来定义常量的#define宏,有一些陷阱是已经确定的,需要避免这些陷阱。

下面是一些常见的预处理使用方法,我们要尝试替换掉这些:

  1. #include
  2. Macros 宏
  3. Constants(常量):Numeric constants(数字常量)
  4. Constants(常量):Ascending integer constants(递增的整数常量)
  5. Constants(常量):String constants(字符串常量)
  6. Conditional compilation(条件编译):Commenting out code(注释)
  7. Conditional compilation(条件编译):Switching between experiments(选择执行)
  8. Conditional compilation(条件编译):Switching between staging and production URLs(在开发环境和生产环境之间切换URL)
  9. Conditional compilation(条件编译):Supporting multiple projects or platforms(支持多项目或平台)

#include

让我们从一个简单的开始。

Smell

#include "foo.h"

除非你要引入C或者C++代码文件,否则就不要使用#include,要对它时刻保持警惕。使用#import替换#include;使用#import消除了使用#ifndef的麻烦。

Macros 宏

Smell

#define WIDTH(view) view.frame.size.width

不要认为Objective-C就可以随意地使用C的功能!除非你的宏需要__LINE__这样预处理功能,否则就把它当作一个独立的方法重写。(既是这样,你写的宏时候调用,甚至修改了其他的方法。)

C语言引入了C++中的一些东西,比如可以使用inline方法:

static inline CGFloat width(UIView *view) { return view.frame.size.width; }

Constants(常量):Numeric constants(数字常量)

从现在开始,我们会看到关于常量的一些预处理的代码异味。使用常量代替在代码中重复使用的字面值是惯用方法。但是,使用#define定义常量却不该当作惯用方法。

Smell

#define kTimeoutInterval 90.0

假如一个常量只在一个单独的文件中使用,把它设置为static const。通过给它一个明确的类型可以增加它语义上的意义,如此一来,这个简单的字面数值用起来将更加简单(IDE的自动提示,编译器的提醒等等),并且可以防止一些低级错误的发生。因此,我们会用下面替换#define:

static const NSTimeInterval kTimeoutInterval = 90;

假如一个常量要在多个文件中共享使用,需要这样做:在头文件中声明,然后在实现文件中定义。

在.h文件要这样声明:

extern const NSTimeInterval JRTimeoutInterval;

在.m文件这样定义:

const NSTimeInterval JRTimeoutInterval = 90;

Constants(常量):Ascending integer constants(递增的整数常量)

Smell

#define firstNameRow 0
#define lastNameRow 1
#define address1Row 2
#define cityRow 3
// etc.

连续的整数常量经常用于Table Views(例:通讯录),决定哪些信息放在哪个Cell中。这就是枚举类型所做的。

enum {
    firstNameRow,
    lastNameRow,
    address1Row,
    cityRow,
    // etc.
};

枚举类型可以很方便地排列数值的顺序,也方便添加新的数值。通常,人们使用#define是因为使用方便,但是不能保证定义的宏是安全的。但是,使用枚举即安全又方便。

枚举类型可以不定义类型名称。但是,你要是想将枚举类型中的数值作为参数使用,定义类型名称可以提供编译器检查和语义检查。下面两个示例中,你很可能会选择Address ar;这种用法。

enum {
	firstNameRow,
	lastNameRow,
	address1Row,
	cityRow,
	// etc.
} AddressRow;
enum AddressRow ar;


typedef enum {
	firstNameRow,
	lastNameRow,
	address1Row,
	cityRow,
	// etc.
} AddressRow;
AddressRow ar;

Constants(常量):String constants(字符串常量)

Smell

#define JRResponseSuccess @"Success"

字符串常量的定义类似于前面说的数值常量的定义。但是有一点不一样,在这里定义的字符串常量是一个对象,这是Objective-C中的概念。

NSString *const JRResponseSuccess = @”Success”;

常量字符串经常跨文件使用,因此经常这样定义:

头文件:

extern NSString *const JRResponseSuccess;

实现文件:

NSString *const JRResponseSuccess = @"Success";

Conditional compilation(条件编译):Commenting out code(注释)

条件编译语句(包括#if, #ifdef等)可以达到选择性执行某些代码的目的。条件编译可以用于不同的方面,但是总的来说它是一个比较笨的工具。

Smell

#if 0
...
#endif

在C语言的前期,只有/*…*/这一种注释方式。要想注释一块代码,需要在这块代码前面加上/*,在后面加上*/。但是,你会发现,若这块代码里面已经有了/*….*/注释,你新添加的注释将会有问题。怎么解决这个问题呢?当时的解决方法是使用预处理方法:用#if 0包住这个代码块。

后来有了现代的IDE,可以分色显示代码,大大方便了代码的编写和阅读。但是,对于上面提到的#if 0块,虽然这块代码不会执行,相当于被注释了,但是IDE始终很难给出恰当的分色显示。

随着时间的推移,C语言不断的进化,引进了C++中的//注释风格。//注释方法被IDE良好的支持,Xcode也是。在Xcode中可以使用 ⌘/快捷键方便地注释和解注释选中的代码块。

Conditional compilation(条件编译):Switching between experiments(选择执行)

Smell

#if EXPERIMENT
...
#else
...
#endif

有时,你需要些一些试验性的代码。或者,你想在两块代码之间快速地选择执行。没问题,这些都是经常遇到的。

但是,你要注意。试验性的代码要在正是发布前删掉,除非有些重要的历史原因需要将这段代码当作注释留下来。假如你选择留着它,注意检查预处理是否会有问题。将它变为真正的注释而不是一段不会执行的代码,这是比较妥当的方法。

Conditional compilation(条件编译):Switching between staging and production URLs(在开发环境和生产环境之间切换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://foo.com/services/fooservice";
}

对于那些比较复杂,需要与多个服务器交互的应用,我将URL写进plist文件。查看这个plist的示例: Switching from Staging URL to Production URL

Conditional compilation(条件编译):Supporting multiple projects or platforms(支持多项目或平台)

Smell

#if PROJECT_A
...
#else
...
#endif

当你的代码需要用在多个项目(或者多个平台),很多人会在基础类里面添加具体的项目或平台的代码。这看似是权宜之计,但是这样做污染了代码,并且无法达到统一代码(多个项目或平台使用相同的代码)的目的。

我们一直在使用面向对象语言,让我们用一下面向对象里面的模式吧。最直接的方式就是通过添加特定项目或平台的子类,用模版方法重写那些包含具体项目或平台的代码。

过程:

  1. 对有特殊要求的项目或平台添加子类。
  2. 在每个项目中,引入针对这个项目的子类代码。
  3. 编译每个项目。
  4. 添加工厂方法,通过#if针对不同的项目或平台添加正确的子类。
  5. 检查代码,查看是否会调用该工厂方法。
  6. 编译并测试每个项目。
  7. 对于每个条件预处理项:
    1. 将项目或平台相关的代码移到特定项目或平台的子类中,直到基础类中没有特定项目或平台的代码为止。
    2. 编译测试每个项目。
  8. 检查子类中是否有重复的代码,进行整合。

如果最后你确实这样做了,将针对特定的项目或平台的代码进行了子类化,你很可能对Bridge模式感兴趣。

#尽可能不使用C中的预处理! 再一次,在终端中进入到你的项目目录下执行这个命令。还有多少预处理命令?能够消除吗?它们是否应该使用?

谨记:不要用预处理做语言本身可以做的事情!

转载于:https://my.oschina.net/yongbin45/blog/160082

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值