前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OC代码规范1——多用类型常量,少用#define预处理指令

OC代码规范1——多用类型常量,少用#define预处理指令

作者头像
拉维
发布2019-08-12 15:50:01
1.4K0
发布2019-08-12 15:50:01
举报
文章被收录于专栏:iOS小生活

两年前针对这一点写过一篇文章Effective Objective-C 2.0——多用类型常量,少用#define预处理指令,本文是在这篇文章的基础上进行扩展的。

首先来看一段代码:

#define kNumber 11

if (NO) {

#define kNumber 88

}

//if语句中的代码永远不会执行,但是在编译时期,编译器会编译这段代码,而这个时候编译器就会将kNumber的值替换为88

NSLog(@"%d", kNumber);

你觉得这段代码会输出什么呢?我们将kNumber定义成11,然后在一个永不会执行的代码里面重新定义了kNumber为88。if语句中的代码永远不会执行,但是在编译时期,编译器会编译预处理这段代码,此时编译器会将kNumber这个名字替换成88,所以最后的输出结果是88。

另外有一个小常识给大家说一下,OC中凡是前面带#的语句,都是编译预处理语句,在编译器编译代码的时候就会处理

在编写代码时,我们经常要定义常量,比如:将各个控件之间间距的大小提取为常量,将某个动画的执行时间大小提取成常量等。一般情况下,我们可能会这样做:

#define kEdgeHeight 13 #define kAnimationDuration 0.3

这样做的弊端如下:

1,虽然这可以达到代码替换的效果,但是这样定义出来的常量并没有类型信息,别人在阅读代码的时候不能很清晰地知道常量的含义。

2,假设该指令是声明在某个头文件中,那么所有引入了这个头文件的代码,都会将kAnimationDuration替换成0.3。基于上面的例子我们可知,如果在某个引出了该头文件的文件中也声明了一个kAnimationDuration,那么此时就很容易造成程序错误,而且该错误还十分隐蔽,极难被发现。

所以,在定义一个常量的时候,最好是不要使用#define预处理指令,而是采用下面的方式:

static const NSTimeInterval kAnimationDuration = 0.3;

这样,我们可以得到常量的类型信息(NSTimeInterval),进而清楚地知道常量的含义。

注意,常量的名称是有一定的命名规则的:

1,如果常量局限于某“编译单元”(translation unit,也就是“实现文件”,implementation file)之内,则在前面加字母k,如:kAnimationDuration

2,若常量在类之外可见,则通常以类名为前缀,如:CustomClassAnimationDuration

不打算公开某个常量

如果我们不打算公开某个常量,只想在某一个类中使用该常量,那么就将其定义在使用该常量的类的实现文件中。如下:

#import "ViewController.h" static const NSTimeInterval kAnimationDuration = 0.3; @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } @end

变量一定要同时使用static和const来声明。

const修饰符所声明的变量是不可以进行修改的

static修饰符则意味着该变量仅在定义此变量的编译单元(在OC语境下,“编译单元”一词通常指每个类的.m实现文件,编译器每收到一个编译单元,就会输出一份“目标文件”)中可见。假如在声明变量的时候不加static,那么编译器就会为该变量创建一个“外部符号”(external symbol),此时若是另一个编译单元中也声明了同名变量,那么编译器就会报出如下错误:

实际上,如果一个变量既声明为static,又声明为const,那么编译器根本就不会创建符号,而是像#define预处理指令那样,把所有遇到的变量都替换为常值。但是,与#define预处理指令不同的是,使用static和const来联合声明的变量是带有类型信息的。

需要对外公开某个常量

在做项目的时候,我们肯定会需要定义很多全局的常量,此类变量需要放在“全局符号表”(global symbol table)中,以便可以在定义该常量的编译单元之外使用。此时就需要创建一个继承自NSObject的类Contact,在Contact.h文件中声明常量,在Contact.m文件中定义常量,然后将Contact文件放入.pch文件中

与上面的static const有所不同,全局常量应该这样定义:

//.h头文件中 extern NSString *const ECustomClassNotificationName; //.m实现文件中 NSString *const ECustomClassNotificationName = @"notificationName";

这个常量需要在头文件中进行声明,在实现文件中进行定义

extern关键字是要告诉编译器,在全局符号表(global symbol table)中会有一个名叫ECustomClassNotificationName的符号。也就是说,编译器无需查看该常量的定义,就允许代码使用此常量,因为编译器知道,在链接成二进制二进制文件以后,肯定能找到这个常量。

const修饰符定义的变量是不可变的,比如你需要定义一个动画时间(基本数据类型)的常量,可以这样做:

//.h头文件中 extern const NSTimeInterval ECustomClassAnimationDuration; //.m实现文件中 const NSTimeInterval ECustomClassAnimationDuration = 0.3;

当你试图去修改ECustomClassAnimationDuration的值的时候,编译器会报错,更加重要的是,这种方式定义的常量是带有类型信息的,这是define所不具备的。

这是定义基础数据类型的常量,接下来我们看看定义一个对象常量并试图去修改它

static NSString const *str = @"666";

str = @"888";

NSLog(@"%@", str);

我们发现程序运行正常,打印结果为888。

不是说好使用const修饰以后就不可以更改了吗?那么这里的str为什么可以更改呢?

首先,我们要明确的是如下两种写法是一样的:

static NSString const *str = @"666";

static const NSString *str = @"666";

也就是说,const放在类型前和类型后是一样的效果(只要保证在*之前就行)。然后不同效果的是下面这种写法:

static NSString * const str = @"666";

const修饰的是它右边的部分,也就是说:

static NSString const * str = static NSString const (* str)

static NSString * const str = static NSString * const (str)

*在C语言中表示指针指向符,(* str)表示指针所指向的内存块地址,而str表示指针所指向地址所保存的内容(即内存块的内容)。

因此const * str表示指针所指向的地址是不可变的,而其内容可变;* const str表示内容不可变

我们来做个尝试:

static NSString const * str = @"111";

NSLog(@"%p, %@", &str, str);

str = @"222";

NSLog(@"%p, %@", &str, str);

输出如下:

2019-05-23 15:14:43.979164+0800 AppDemo[14837:2504120] 0x10241b080, 111

2019-05-23 15:14:43.979225+0800 AppDemo[14837:2504120] 0x10241b080, 222

我们发现,当改变内存内容的时候,它的地址并没有发生改变,这是符合“const”修饰符的规定的。

而当我们的修饰符是这样的时候:

static NSString * const str = @"111";

我们就无法改变str的值了。

所以当我们需要定义一个不可变的常量的时候,我们还是需要将“const”修饰符放到“*”指针指示符的后边

总结

1,在定义常量的时候,使用extern const或者是static const要优于使用#define预处理指令。

2,不要使用#define预处理指令来定义常量,理由如下:(1)这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找和替换操作;(2)即使有人重新定义了常量值,编译器也不会产生警告⚠️信息,这将导致应用程序中的常量值可能会不一致。我们应该使用extern const或者是static const来声明并定义常量,借助编译器确保常量值不变。

3,在实现文件中使用static const来定义“只在编译单元内可见的常量”(translation-unit-specific constant)。由于此类常量不在全局符号表中,所以无需为其名称加前缀。

4,在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。由于此类常量会出现在全局符号表中,所以常量的名称需要以与之相关的类名做前缀。

5,使用extern const来声明的全局常量,必须并且只能初始化一次,如果对其多次初始化,那么编译器会报错。

6,只有纯常量才可以使用类型常量来定义,而为了简化操作而定义的代码块、涉及到Runtime的各种方法的常量等都是要使用#define来定义的,如下:

#define kDataBaseName [NSString stringWithFormat:@"%@.sqlite", [XYUserDefaults objectForKey:@"mobileno"]]

#define kNoNull(string) (string.length>0 ? string : @"")

以上。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档