专栏首页iOS小生活OC代码规范1——多用类型常量,少用#define预处理指令

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

两年前针对这一点写过一篇文章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 : @"")

以上。

本文分享自微信公众号 - iOS小生活(iOSHappyLife),作者:拉维

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-05-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 依赖管理(二):第三方组件库在Flutter中要如何管理

    前面的文章中,我介绍了Flutter工程的资源管理机制。在Flutter中,资源采用先声明后使用的机制,在pubspec.yaml显示地声明资源路径后,才可以使...

    拉维
  • Flutter中的日期、格式化日期、日期选择器组件在

    所谓时间戳,是指自格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

    拉维
  • Flutter更新showDialog以及showModalBottomSheet中的状态中的内容

    很多人在使用 showDialog 或者 showModalBottomSheet 的时候,都会遇到这个问题:通过 setState 方法无法更新当前的dial...

    拉维
  • 自己动手写工具:百度图片批量下载器

    开篇:在某些场景下,我们想要对百度图片搜出来的东东进行保存,但是一个一个得下载保存不仅耗时而且费劲,有木有一种方法能够简化我们的工作量呢,让我们在离线模式下也能...

    Edison Zhou
  • swift 3.0基本数据语法

    用户1941540
  • 【Vue原理】看Vue源码,不会调试不行啊

    | [v2-36c4f44482da121427fb265c33870a69_hd.jpg]

    神仙朱
  • Mac 下安装运行Rocket.chat

    最近花了一周的时间,复习了HTML、CSS、原生JS,并学习了Node.js、CoffeeScript.js、MongoDB,入了下门。 因为准备在Rocke...

    Haley_Wong
  • Angular ngIf 跟他的新伙伴 else 和 then

    Angular 扩展了ngIf 指令, 加入了两个新伙伴 else 和 then。

    mafeifan
  • C++之const

    程序手艺人
  • 小程序再次释放新能力,聪明商家已经用起来了!

    近日,微信再次释放了小程序的能力,上线了小程序线下物料模板,商家可以下载并使用这个模版,引导用户快速地获取小程序,培养用户使用小程序的习惯,提升小程序访问量。

    场景录小程序

扫码关注云+社区

领取腾讯云代金券