何为代码质量?——用脑子写代码引言正文总结

引言

不重视代码质量的工程师永远是初级工程师

为什么项目维护困难、BUG 反复?实际上很多时候就是代码质量的问题。代码架构就像是建筑的钢筋结构,代码细节就像是建筑的内部装修,建筑的抗震等级、简装或豪装完全取决于团队开发人员的水平。

本文是笔者对于一些代码质量技巧的小总结,编写高质量代码的思路在任何技术栈都是基本相通的,文章内容仅代表笔者的个人看法,抛砖引玉,不喜勿喷?。

正文

1、使用 ++i 而不是 i++

经常看到这样的代码:

for (int i = 0;; i++) {}

单步自增 (或自减) 操作,最好是使用++i而不是i++,效率略高。

大家应该都知道++i的返回值是自增过后的,而i++的返回值是自增之前的。其实从这点就可以猜测:++i内部实现应该是直接将 i 这块内存 +1 然后返回,而i++需要使用一个局部变量来存储 i 的值,然后 i 加一,最后返回局部变量的值(别告诉我你能先 return 再执行自增)。

如果某一种语言的i++不能作为左值,那么也可以猜测这个局部变量是用const修饰的。

所以,i++理论上比++i有更多的消耗,代码就这样写吧:

for (int i = 0;; ++i) {}

2、巧用位运算

位运算效率很高,而且有很多巧妙的用法,这里提出一个需求:

typedef enum : NSUInteger {
    TestEnumA = 1,
    TestEnumB = 1 << 1,
    TestEnumC = 1 << 2,
    TestEnumD = 1 << 3
} TestEnum;

对于该多选枚举,如何判断该枚举类型的变量是否是复合项?

如果按照常规的思路,就需要逐项判断是否包含,时间复杂度最差为O(n)。而使用位运算可以这么写:

TestEnum test = ...;
if (test == (test & (-test))) {
    //不是复合项
}

实际上就是通过负数二进制的一个特性来判断,看如下分析便一目了然:

test           0000 0100
反码           1111 1011
补码           1111 1100
test & (-test) 0000 0100

3、灵活使用组合运算符

不明白有些工程师为什么排斥组合运算符,他们喜欢这么写:

bool is = ...;
if (is) a = 1;
else a = 2;

使用三目运算符:

bool is = ...;
a = is ? 1 : 2;

其他组合运算符比如 ?: %=等,灵活的使用它们可以让代码更加的简洁清晰。

4、const 和 static 和宏

static可以让变量进入静态区,提高变量生命周期至程序结束。值得注意的是,文件中最外层(#include下)的变量本身就是在静态区的,而这种情况使用static是为了变量的私有化。

const 修饰的变量在常量区不可变,是在编译阶段处理;宏是在预编译阶段执行宏替换。所以频繁使用 const 不会产生额外的内存,而所有使用宏的地方都可能开辟内存,况且,预编译阶段的大量宏替换会带来一定的时间消耗。

所以笔者的建议是,能用常量的不用宏,比如一个网络请求的 url:

.h 接口文件
extern NSString * const BaseServer;
.m 实现文件
NSString * const BaseServer = @"https://...";

值得注意的是,const 是修饰右边内存,所以这里是想要BaseServer字符串指针指向的内容不可变,而不是*BaseServer内容不可变。

5、空间换时间

在很多场景中,可以牺牲一定的空间来降低时间复杂度,为了程序的高效运行,工程师可以自行判断是否值得,下面举一个代码例子,判断字符串是否有效:

BOOL notEmpty(NSString *str) {
    if (!str) return NO;
    static NSSet *emptySet;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        emptySet = [NSSet setWithObjects:@"", @"(null)", @"null", @"<null>", @"NULL", nil];
    });
    if ([emptySet containsObject:str]) return NO;
    if ([str isKindOfClass:NSNull.class]) return NO;
    return YES;
}

使用一个 hash 来提高匹配效率,这在数据较少时可能体现不出优势,甚至会让效率变低,但是在数据量稍大的时候优势就明显了,而且这样写可以避免大量的if-elseif等判断,逻辑更清晰。

值得注意的是,此处使用static来提升局部变量emptySet的生命周期,而不是将这句代码写在方法体外面。在变量声明时,一定要明确它的使用范围,限定合适的作用域。

6、容器类型的合理选择

在 C++ 中,若不需要键值对的 hash ,就使用set而不是map;若不需要排序的集合就使用unordered_set而不是set

归根结底也是对时间复杂度的考虑,选择容器类型时,一定要选择“刚好”能满足需求的,能用更“简单”效率更高的容器就不用“复杂”效率更低的容器。

7、初始化不要交给编译器

对于变量的使用,尽量在类或结构体初始化方法中对其赋初值,而不要依赖于编译器。因为在可见的未来,不管是编译器的更新或是代码跨平台移植,这些变量的初始值都不会受编译器影响。

8、多分支结构 switch 代替 if - else if

这是一个老生常谈的东西了,多分支结构尽量使用 switch 而不是大量的 if - else if 语句,若非要用 if - else if 来写,则频率高的优先判断,可以从整体上最大限度的减少判断次数。

不要小看这些少量的效率提升,放大到整个项目也是有不小的收益。

9、避免数据同步

经常会有一些需求,对一系列的数据有很多额外的操作,比如选择、删除、筛选、搜索等。代码设计时,要尽量将所有的操作状态都缓存到同一个数据模型中,而不是使用多个容器数据结构来处理,我们应该尽量避免数据同步防止出错。

10、合理使用局部指针

经常会看到这种代码:

doSomething(city.school.class.jack.name,
             city.school.class.jack.age,
             city.school.class.jack.sex);

当同一个变量的调用过深且使用频繁时,可以使用一个局部指针来处理:

Person *jack = city.school.class.jack;
doSomething(jack.name,
             jack.age,
             jack.sex);

相对于指针变量所占用的空间来说,代码的简洁和美观度稍显重要一点。

11、避免滥用单例

单例作为一种设计模式应用非常广泛,在移动端开发中,有些开发者利用它来实现非缓存传值,笔者认为这是一个错误的做法,使用单例传值的时候你需要管理单例中的数据何时释放与更新,可能会引发数据错乱。

单例存在的意义应该是缓存数据,而非传值,切勿为了方便滥用单例。

12、避免滥用继承

继承本身和解耦思想有些冲突,代码设计中要尽量避免过深的继承关系,因为子类与父类的耦合将无法真正剥离。过深的继承关系会增加调试的困难程度,并且若继承关系设计有缺陷,修改越深的类影响面将会越广,可能带来灾难性的后果。

可以使用分类的方式做一些通用配置,然后在具体类中简洁的调用一次方法;也可以使用 AOP 思想,hook 住生命周期方法无侵入配置(比如埋点)。

比如 iOS 开发中,可能会有开发者喜欢写一套基类,实际上只是基于系统的类做了小量的配置,比如BaseViewControllerBaseViewBaseModelBaseViewModel,甚至是BaseTableViewCell。控制器基类可以对栈和导航栏做一些配置,还是有一点使用意义,至于其它的笔者感觉就是过度设计,其实很大意义上BaseViewController也没有存在的必要。

记住:过多的基类并不是代码规范,那是你囚禁其他开发者的牢笼。

13、避免过度封装

提取方法的原则是功能单一性,但若功能本身就是很少的一两句代码可能就没必要额外提取了。在保证代码清晰的情况下,很多时候提取逻辑也是需要酌情考虑的。

有见过开发者使用一套所谓的简洁配置 UI 的框架,不过就是将 UI 控件的属性封装成链式语法之类的,用起来有种快一些的错觉,殊不知这就是过度封装的典范。

封装的意义在于简洁的解决一类问题,而非少敲那几个字母,过度封装只会增加其他开发者阅读你代码的成本。

比如业界知名的 Masonry,使用它时比原生的 layout 快了不止 10 倍,而且代码很简洁易懂,极大的提高了开发效率。

14、避免过多代码块嵌套

比如代码中大量的 if - else 嵌套判断,大量的嵌套循环,大量的闭包嵌套。

出现这种情况首先要考虑的是分支结构处理是否多余?循环是否可以优化时间复杂度?当排除这些可优化项过后,可以做一些方法提取减少大量的代码块嵌套,方便阅读。

15、时刻注意空值和越界

写某块代码中,要时刻注意空值和越界的处理,比如给NSDictionary插入空值会崩溃,从NSArray越界取值会崩溃,这些情况要时刻考虑到。

当然,可能有人会说有方法可以全局避免崩溃。实际上笔者不是很赞同这种做法,这可能会让新手开发者永远发现不了自己代码的漏洞。

16、时刻注意代码的调用时机和频率

当你写一块代码时,需要习惯性的思考两个问题:这块代码的共有变量会被多线程访问从而存在安全问题么?这块代码可能会在一个 RunLoop 循环中调用很频繁么?

对于第一个问题,可能需要使用“锁”来保证线程安全,而锁的选择有一些技巧,比如整形使用原子自增保证线程安全:OSAtomicIncrement32();调用耗时短的代码使用dispatch_semaphore_t更高效;可能存在重复获取锁时使用递归锁处理...

对于第二个问题,只需要在合适的地方加入自动释放池 (autoreleasepool) 避免内存峰值就行了。

17、减少界面代码复用、增加功能代码的复用

对于大前端来说,界面是项目中重要的组成部分,而有时候设计师给的图中,不同界面有很多相同的元素,看起来一模一样,所以很多工程师偷懒直接复用界面了。

在这里,笔者建议尽量少的复用界面,宁愿选择复制一份。

试想,目前版本两个界面相同,你复用了它,当下个版本其中一个界面要调整一下,这时你继续偷懒,加入一些判断来区分逻辑,下一次迭代又增加了差异,你又偷懒加入判断逻辑...... 最终你会发现,这个界面里面已经逻辑爆炸了,拆分成两个界面将变得异常困难。

而对于功能代码,笔者是提倡多提取,多复用,切记命名规范和适当的注释。

18、组件的设计技巧

在封装一些小组件时,一定要形成习惯,不想暴露给使用者的属性和方法不要写在接口文件中,甚至于某些延续父类的方法不想使用者使用,可以如下处理:

- (instancetype)init UNAVAILABLE_ATTRIBUTE;

当然,不用担心组件内部如何获取父类特性,可以通过[super init]来处理。

同时,在多人开发中,组件的开放方法名最好加入一些前缀,便于区别,也避免方法重名,最容易导致方法重名的情况就是各种分类里面的方法重复,会带来意想不到的错误。

19、缓存机制的设计

不管是任何技术栈的缓存机制设计,都需要一套缓存淘汰算法,使用最广泛的淘汰算法就是 LRU,即是最近最少使用淘汰算法,开发者需要严格的控制磁盘缓存和内存缓存的空间占用。

在 iOS 开发中,可以使用 YYCache 来处理缓存机制,该框架的源码剖析可见笔者博客:YYCache 源码剖析:一览亮点

还有一点需要提出的是磁盘缓存的位置问题。iOS 设备沙盒中有 Documents、Caches、Preferences、tmp 等文件夹,其中 Documents 和 Preferences 会被 iCloud 同步。

Documents 适合存储比较重要的数据;Caches 适合存储大量且不那么重要的数据,比如图片缓存、网络数据缓存啥的;tmp 存储临时文件,重启手机或者内存告急时会被清理;Preferences 是偏好设置,适合存储比较个性化的数据。

值得注意的是,NSUserDefaults是存储在 Preferences 下的文件,发现有很多开发者为了偷懒频繁的使用NSUserDefaults做任意数据的磁盘缓存,这是一个很不合理的做法,用处不大且大量的数据一般缓存在 Caches 中,就算是从技术角度考虑,NSUserDefaults是以 .plist 形式存储的,不适合大数据存储。

20、待续

总结

代码技巧都是实践加思考总结出来的,在代码编写过程中,开发者需要时刻明白自己的代码是干什么的,不要随意的复制代码。同时,开发者需要有算法思维和工程思维,力求使用高效率和高可维护的代码来实现业务。

笔者最后总结几点提高代码质量的途径:

  • 设计架构制定规范,经常 code review。(不要说小公司没人陪你 review,告诉你一个人也可以 review 得不亦乐乎)
  • 多阅读优秀的开源代码。(希望你能判断何为优秀?)
  • 找一家技术驱动的公司。(一切以工时定贡献的公司都是耍流氓,殊不知高效代码设计能减少相当多工作量)
  • 找到有能力打你脸的人,并和 TA 成为朋友。(相信我,技术人员要经常被打击才能茁壮成长?)

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Crossin的编程教室

【Python 第7课】if

感觉又一次被微信坑了。前两天刚说改变课程发送方式,今天微信就突然服务器升级,暂时不能新增接收文章的关键字了。所以这两天,还是用回老方式,直接推送。过去的课程0~...

2936
来自专栏程序员互动联盟

【答疑解惑第二十三讲】C语言main函数那点事

疑惑一 C语言函数的参数问题 在C语言中main函数大家见到的基本有两种:一种是带参数的如int main(char * argc,char *argv[])...

2773
来自专栏Python爬虫与算法进阶

爬虫之全站爬取方法

其实这个很好理解。比如说知乎,一个大V有100W粉丝,从这个大V出发,抓取粉丝的粉丝,一直循环下去。(可能是个死循环)

3723
来自专栏令仔很忙

Spring从入门到精通(一)----IoC(控制反转)

在采用面向对象方法设计的软件系统中,它的底层实现都是由N个对象组成的,所有的对象通过相互合作,最终实现系统的业务逻辑。

1042
来自专栏Android开发经验

ExpandableStickyListHeadersListView遇到的一个问题

1424
来自专栏landv

我的第一个Java程序和Java简介

1282
来自专栏阮一峰的网络日志

Javascript的10个设计缺陷

前几篇文章,我经常说Javascript的设计不够严谨,有很多失误。 今天的这一篇,前半部分就谈为什么会这样,后半部分将列举Javascript的10个设计缺陷...

3627
来自专栏java架构师

设计模式之访问者模式

借用大神李建忠的思路,应用一个模式的时候,我们的动机是什么,如果最初的动机都不清楚,那只能是为了用模式而用模式了。 用一种模式,是为了解决一类实际项目中遇到的问...

37015
来自专栏程序员互动联盟

【编程基础】聊聊如何学习Java——Java的特性

上一篇文章聊了学习编程可能会遇到的心里障碍和为什么学习Java,看了网友们的回复小编很激动,我会积极听取网友们的留言,在我以后的文章中改进。现在说Java语言的...

3889
来自专栏一个会写诗的程序员的博客

UML类图关系(泛化 、继承、实现、依赖、关联、聚合、组合)

继承 指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;...

1051

扫码关注云+社区

领取腾讯云代金券