Gulp 是前端工具链中常用的流式任务执行器,适用于许多小型库的编译打包任务。它的设计思想其实很像 Linux 命令行里面的 Pipe(管道):
1 | gulp.src(paths.scripts.src, { sourcemaps: true }) |
gulp 是单向的,即对于同一个 pipeline,数据一般不能被逆向还原。
我们知道,计算机网络协议是分层设计的,每层分别为数据赋予不同的含义、完成不同的使命。源主机采用网络协议栈将原始二进制流 层层编码(encode) 后送往目的主机,目的主机采用同样的协议栈将数据 层层解码(decode) 后得到原始数据。典型的 HTTP 协议将请求数据通过 TCP/IP 协议栈自上而下编码后送出,之后自下而上解码后得到响应数据:
1 | +---------+ |
TCP/IP 协议栈是双向的,即对于同一套协议,数据既可以被编码也可以被解码。
那么问题来了,是否可以抽象一种轻量的 Pipeline,实现类似网络协议栈双向数据流的处理能力,并且能够让用户定制化每层的处理逻辑?
设计之前,先根据数据流划分功能模块,这里 PIPE
是数据和各个数据处理单元的调度者,PIPE_UNIT_x
是每层数据的处理单元,可以有多个,并且按顺序前后串联。
1 | +------------------ PIPE -------------------+ |
用户可以实现自己的 PIPE_UNIT
来达到定制化处理逻辑的功能,也可以任意调换 PIPE_UNIT
的顺序来达到不同的处理效果。
Pipe
需要提供一个数据入口来启动链式处理流程:
1 | const EventEmitter = require('events'); |
PipeUnit
需要暴露编码(encode)和解码(decode)两个接口,考虑到处理单元可能异步执行,因此使用 async
黑膜法:
1 | class PipeUnit extends EventEmitter { |
首先实现一个提供压缩、解压缩功能的 PipeUnit
:
1 | const zlib = require('zlib'); |
下面再实现一个提供 AES
对称加解密功能的 PipeUnit
,这次采用同步执行:
1 | const crypto = require('crypto'); |
1 | // 自由组合处理单元 |
可以看到,通过对 EventEmitter 简单的封装就可以实现双向数据 pipeline,同时支持异步单元操作。
功能实现了,性能又如何呢?抛开 PipeUnit
的业务实现,简单分析一下链式 EventEmitter 结构的性能影响因素,理论上很大程度取决于 EventEmitter 本身的性能,Pipe::feed
只在第一次被调用时构建响应链,之后的调用几乎不会有性能损失。
Node.js 版本如下:
1 | > process.versions |
下面分别考察 0 ~ 30000(每次递增 1000) 个 PipeUnit
实例的执行时间,来评估上述设计的性能表现:
1 | const { performance } = require('perf_hooks'); |
执行4次,可以将结果绘制到一张图中:
可以看到每次运行的结果高度一致,由上万个 PipeUnit
构成的链式 EventEmitter 能够以令人满意的效率完成运行。
不过出人意料的是,在特定数量的 PipeUnit
上总会出现尖峰,这可能和 V8 引擎的优化机制有关,作者能力有限,感兴趣的同学可以深挖原因。
原文作者:Mathias Bynens
原文地址:https://developers.google.com/web/updates/2018/04/loading-wasm
在使用 WebAssembly 的时候,通常需要下载、编译、实例化一个模块,然后通过 JavaScript 调用由该模块导出的一些东西。这篇文章从一个常见但不是很优秀的代码片段开始,讨论几种可能的优化方法,最后得出最简单、最高效的通过 JavaScript 运行 WebAssembly 的方法。
注意:诸如 Emscripten 的一些工具可以为你准备样板代码,因此你不需要自己编写 wasm 代码。如果你要更加细粒度地控制 WebAssembly 模块加载,那么请专注于下面的最佳实践吧。
下面的代码片段完成了下载-编译-实例化的整个过程,尽管不是很优秀的方式:
1 | // 别这么做 |
注意我们是如何使用 new WebAssembly.Module(buffer)
将响应数据转换成一个模块的。这是一个同步 API,意味着它在完成之前会阻塞主线程。为了防止滥用,Chrome 禁止在超过 4KB 的 buffer 上使用 WebAssembly.Module
。为了避免大小限制,我们可以用 await WebAssembly.compile(buffer)
来代替:
1 | (async () => { |
await WebAssembly.compile(buffer)
仍然不是一个优秀的方式,不过姑且先这样。
在修改后的代码片段中,几乎所有的操作都是异步的,因为 await
使之变得很清晰。只有 new WebAssembly.Instance(module)
是个例外。为了一致性,我们可以用异步 WebAssembly.instantiate(module)
。
1 | (async () => { |
让我们回顾一下我之前提到的对于 compile
的优化。利用 流式编译,浏览器可以在模块数据下载过程中就开始编译 WebAssembly 模块。因为下载和编译过程是并发的,特别是对于大模块,这样将会更快。
使用 WebAssembly.compileStreaming
替换 WebAssembly.compile
可以开启这个功能。这么做之后还可以避免产生中间数据,因为现在我们可以直接传递由 await fetch(url)
返回的 Response
实例。
1 | (async () => { |
注意:服务端必须经过配置能够支持以 Content-Type: application/wasm 头发送 .wasm 文件。在之前的例子中,我们将响应数据当做 arraybuffer 传递,因此不需要进行 MIME 类型检查。
WebAssembly.compileStreaming
API 也能接收一个 resolve 为 Response
的 promise 实例。如果你没在别的地方使用 response
,你可以直接传递由 fetch
返回的 promise 对象,而不需要 await
:
1 | (async () => { |
如果你也没在其他地方使用 fetch
的结果,你甚至也可以直接传递它:
1 | (async () => { |
我个人认为将其单独成行可读性更好。
想知道我们是如何将响应数据编译为模块然后实例化的?事实证明,WebAssembly.instantiate
可以一步到位。
1 | (async () => { |
如果你只需要 instance 对象,那没理由再保留 module 对象,简化代码如下:
1 | // 这是加载 WebAssembly 的建议方式。 |
总结一下我们所用过的优化方法:
尽情享用 WebAssembly 吧!
]]>本文简单介绍了 Context API 的提出背景、API 设计和用法;之后比较了 React-Redux 的设计;然后提出了一种基于 Context API 的二次封装;最后再将二次封装和 mobx-react 进行了比较。
React 项目组成员 @acdlite 于 2017-12-05 提出关于新 Context API 的 RFC。
实际上 Context 在 React 的早期版本中就已经存在,但不能解决当 shouldComponentUpdate
返回 false
时,组件无法响应 context 的改变的问题。由于 shouldComponentUpdate
常用于性能优化,被大量开源库或框架广泛使用,因此原版的 Context 变得十分鸡肋,新的 Context API 很好地解决了这一问题。
首先安装 16.3.x 版本的 react
及 react-dom
:
1 | $ yarn add react@next react-dom@next |
来看一个简单的例子:
1 | import React, { createContext } from 'react'; |
使用 Context API 最大的好处就是解决深层嵌套组件层层传递 props 的问题。但这样做也存在一个问题: state 的被保存在 <App>
中,更新状态时必须调用 <App>
的 this.setState()
,如果子组件需要更新 state
,那么需要通过 <Provider>
向下传递封装了 this.setState()
的回调函数:
1 | <Provider value={{ |
之后,子组件要求改变状态时,在 <Consumer>
中调用该回调方法即可:
1 | <Consumer> |
另有一个问题是如何实现在 组件外 更新状态,让组件也能响应状态变化?
这样的需求通常在应用需要与第三方库交互时会遇到,举一个实际的例子:
Q: 一个 web 应用使用 websocket 做数据交换,我们需要在页面上实时显示 websocket 连接的延迟:
1 | // ws.js |
先前例子中将 state 内化的方式显然不可行了,这个时候联想到 Redux,利用它全局 store 的设计,借助 store.dispatch
就可以实现上面的需求了。
React-Redux
是对 React 老版本 Context 的封装,它允许子组件通过 connect
方法建立对 store 中状态变化的响应,下面是一个简单的 Redux 应用:
1 | // app.js |
1 | // child.js |
可以看到在 Redux 的套路中,完成一次 状态更新
需要 dispatch 一个 action 到 reducer,这个过程同时牵扯到三个概念,有些复杂;而在 Context API 的套路中,完成一次 状态更新
只需要 setState(...)
就够了,但单纯的 Context API 无法解决先前提到的 组件外 更新状态的问题。
下面对 Context API 进行二次封装,让它支持类似 Redux 全局 store 的特性,但用法又比 Redux 更加简单。
1 | // context.js |
用法如下:
1 | import React from 'react'; |
现在在应用的任意位置调用 store.setState()
方法,就能更新组件的状态了:
1 | import store from './store'; |
MobX 基于观察者模式,通过 mobx-react 封装后许多地方和 Context API 类似,下面是官方提供的一个例子:
1 | class App extends React.Component { |
在 mobx-react 的套路中,组件可以通过 <Observer>
消费由 observable()
创建出来的对象,直接修改该对象中的键值可以实现组件的重新渲染。
二次封装后的 Context API 相比 mobx-react 用法相近,但 mobx 得益于 setter/getter Hooks 具有更直观的状态改变方式。
An animated colorful Voronoi diagram
powered by d3.
https://micooz.github.io/qnimate
Install via npm:
1 | git clone https://github.com/micooz/qnimate |
Run in development:
1 | npm start |
Build and bundle:
1 | npm run build:prod |
HTML:
<div id=""playground""></div>
js:
<script src=""qnimate.min.js""></script>
Qnimate
will be exposed to window
, create an instance of Qnimate
, pass an option object and then call run()
:
1 | document.addEventListener('DOMContentLoaded', function main() { |
d3.voronoi
- https://github.com/d3/d3-voronoi
Send me issue.
]]>1 | text { |
接触D3三天,发现有些套路可以反复运用,所以记录一下。
D3
相比其他图表库,学习成本较高。但最为灵活,需要使用者精雕细琢图形的每个细节。D3处处体现了函数式的编程思维。
NOTE: 这里所用D3版本是:
1 | ""dependencies"": { |
下面以一个简单的横纵坐标图为例。
1 | var margin = {left: 70, top: 20, right: 20, bottom: 50}; |
这一步实际上比较重要,图形高宽参数会在后面绘图中常常用到。
1 | var svg = d3.select('.container').append('svg') |
这里可以直接在html中放一个<svg>
,但为了可移植性,用脚本生成。
1 | var graph = svg.append('g') |
<g>
中将包含所有图形元素(坐标轴、标签、线条……),现在svg树是这样的:
1 | <svg> |
1 | var x = d3.scaleTime() |
这个时候由于数据还没获取,只能先设置他们的图形坐标范围,注意这个不是数据的定义域。
这个两个函数
通常有两个用途,以x
为例:
1 | var px = x(data); // 根据数据值计算对应的x坐标值 |
一般情况下都是从远端取回特定格式的数据,这是个异步过程:
1 | d3.csv('data.csv', parser, function(err, data) { |
D3很人性化的给你留了个格式化数据的地方parser
。
1 | function parser(d) { |
D3取出数据的每一行,依次传入该函数,然后可以返回你需要的格式化数据,最后以数组形成出现在data
变量中。
1 | // value domain of x and y |
现在可以给x和y设置值域了,值域类型可以很灵活,上面设置x的值域是一个时间范围。
接下来就是构思你图形的各个部分了,坐标轴、标签、图形内容等等,也是逐步生成一个完整svg
的过程。为了简便起见,这里不会贴出冗余代码。
绘制X轴:
1 | // X、Y轴可以利用D3的axisXXXXXX函数简单创建, |
绘制折线:
1 | // 创建一个线段生成器,会根据绑定数据,通过x和y访问器计算每个点的坐标位置 |
至此,就可以看到一个基本图形了。
]]>D3.js的最新v4版提供了d3.zoom模块,用它可以给svg图形增加缩放/移动的特性,但通过非鼠标/触控的方式改变图形的位置和缩放比例后,d3.zoom的行为就变得不正常了。本文给出一个解决方法。
1 | // 1. 初始化一个zoom行为 |
我们一般会关注zoom
事件:
1 | const zoom = () => { |
有趣的是,如果我们在其他地方主动设置:
1 | container.attr('transform', 'translate(10, 10)'); |
再用鼠标或者触控板调整图形时,zoom行为并不会从(10, 10)位置开始计算下一个d3.event.transform
,zoom行为似乎自己保存
了上一次的transform,而不关心我们设置到container
上的transform。
结果就是我们用attr设置的transform其实是临时的
。
如果查看应用了zoom行为的宿主元素(这里是g
)的属性,会发现其Element上有一个__zoom: Transform
,这个玩意儿每次走zoom
方法的时候都会变化,这正是zoom自己保存的transform对象。
所以我们在主动设置transform后,只需要想办法更新这个__zoom就可以骗过
zoom行为,让它下一次调用zoom()事,从我们设置的值开始计算。
查阅文档:
https://github.com/d3/d3-zoom/blob/master/README.md#zoom_transform
https://github.com/d3/d3-zoom/blob/master/README.md#transform_scale
可以这样设置:
1 | // 自定义transform |
之后d3触发zoom回调之前会取g的__zoom计算下一次的transform,这个transform才是我们想要的。
]]>容器A通过–link选项使用容器B的某个服务,docker-compose.yml
配置如下:
A: image: image_a links: - B:B ...B: image: image_b ...
当容器B被重启后,A的link不会被自动更新,要一并重启A才行:
$ docker-compose restart B # A doesn't work$ docker-compose restart A # it works well
]]>webpack-dev-server 是用express和websocket实现的一套在开发环境下前端自动更新的工具。
webpack-dev-server提供CLI接口,读取传入的webpack.config.js配置文件,根据webpack配置,建立一个静态服务器,供前端加载静态资源,其中有一个关键附加脚本是 webpack-dev-server.js
,位于PATH根路径,即 /webpack-dev-server.js
,其中存放着websocket客户端。
可以通过下面的命令运行webpack-dev-server:
$ node node_modules/.bin/webpack-dev-server --config webpack/dev.config.js --inline --profile --colors --watch --display-error-details --display-cached
参数说明参考:这里
执行后会自动运行webpack进行打包等一系列操作。
在webpack配置文件中只需添加一个 devServer
配置项即可定义webpack-dev-server的行为:
1 | devServer: { |
在这个例子中,webpack-dev-server会在本地3000端口上启动一个静态服务器,服务器serve的目录是webpack的必选配置 output.path
,这是一个绝对路径。
请考虑下面这个问题:
我有一个网站项目,分模块,每个模块是一个node项目,且每个模块可以独立存在(启动,调试,运行),它们有些用到了webpack-dev-server。
再次强调每个模块相互独立,它们之间的耦合方式只有一种:请求代理。
现在假设模块A作为API服务器,监听3000端口;模块B作为应用服务器,要提供资源给浏览器,于是用webpack-dev-server在端口3001的 /
上建立了静态服务器。模块B还要从模块A存取数据,那么必定存在从3001跨域请求到3000的问题,消除这个问题有多种解决办法:
Access-Control-Allow-Origin
为B的域。不深入讨论上面的方法,现在假设我们采用方法二解决了跨域请求问题,然后我们再考虑一下接下来的一个问题:
假设存在模块C,和B十分类似,也属于应用服务器;如果B和C存在同名资源,比如 main.js
,访问该资源就会引发冲突,因为两个模块都在 /
上建立了静态服务器,而这又符合每个模块可以独立存在的先决条件:
// Bhttp://localhost/B/index.htmlhttp://localhost/main.js// Chttp://localhost/C/index.htmlhttp://localhost/main.js // 哪个 main.js ?
解决办法看似很明显:
// Bhttp://localhost/B/index.htmlhttp://localhost/B/main.js// Chttp://localhost/C/index.htmlhttp://localhost/C/main.js // everyone is happy
但这又破坏了每个模块的独立性,我希望单独启动C时,C总能从 /
上获取资源,而不是 /C/...
这么冗余。
问题就出在 webpack-dev-server
,它适合作为静态资源服务器,而不是开发服务器。因此,我们的开发环境除了需要 webpack-dev-server
,还需要专门的开发服务器。
// => Module B// dev serverhttp://localhost/B/index.html// webpack-dev-server for Bhttp://localhost:3001/...// => Module C// dev serverhttp://localhost/C/index.html// webpack-dev-server for Chttp://localhost:3003/...
每个模块从对应的 webpack-dev-server
获取资源,解决了冲突又保留了每个模块的独立性。
ngOnInit
,不利于子组件的自我更新,但设法使子组件从Dom中移除后重建就可以多次触发ngOnInit
。1 | <person *ngIf="show"></person> |
1 | class PersonComponent { |
像这种带星号的指令就是Angular2中一种模板语法糖,可以管控组件的生命周期。
]]>ProvidePlugin
暴露对象到全局:1 | plugins: [ |
自定义require返回值:
1 | // webpack.config.js |
开启Hot Module Replacement(HMR)
方法一:
$ webpack --hot --inline
方法二:
1 | // webpack.config.js |
前段时间,有一个任务是需要频繁在大量的数据集合中快速定位并修改某个元素某个字段的值。
数据结构是数组,元素的结构可能相当复杂且乱序。
问题分析:
假定这个数据集如下:
// array dataset[{ name: 'name1', body: { metadata: { header: { id: 1 // unique } } }, ...}]
实际上就是一个查找算法问题,假设要从1000条数据中查找id为1的元素,最SB做法是直接遍历整个数据集:
1 | for(let ele of dataset) { |
最坏的情况是O(n),当然也可以使用其他常见的查找算法减少遍历次数,但如果要频繁查找,同步操作会导致页面直接卡死。
如果有一张哈希表就帮大忙了,不妨先想想下面这个问题:
在数据库里,为什么给一个字段加个索引就可以极大提升查询效率(通常情况)?
解决方案:
首先理解索引的含义,在js中,数组是线性结构,它的下标可以当成一种索引,通过下标访问元素时间复杂度为O(1):
1 | const db = [1, 2, 3, 4, 5, ...]; |
对于一个Object,同样的:
1 | const obj = { |
再看看最开始的那个问题,如果我们可以:
1 | const id = 1; |
实现这个效果实际上就要建立索引,此时的 dataset
显然已经不能是最原始的数组了。当id不是数字的时候,dataset
也不能是数组,
那么Object就理所当然地充当js里的HashMap了(ES6中已经有标准的Map实现)。
编写一个通用的索引创建函数,这个函数可以为一个数组,通过传入的回调函数的返回值创建一个包含所有数据引用的索引对象(Object):
1 | const index = (arr, fn) => { |
函数只需要遍历一次数据集来建立索引。
用法:
1 | const our_index = _index(dataset, ele => ele.body.metadata.header.id); |
有了这个索引 our_index
,就可以愉快的以O(1)的复杂度来访问任意元素,取出的元素是引用,于是也可以直接对原存储空间的数据进行操作:
1 | let ele = our_index[1]; |
小结
原生JavaScript不支持Map数据结构,因此可以通过对象来实现;关键在于如何根据需要建立索引,建立索引的字段必须满足唯一性。
]]>1 | <select [(ngModel)]="value" (change)="onSelect(value)"> |
1 | onSelect(value) { |
解决办法
1 | onSelect(value) { |
考虑下面的代码:
1 | let obj = {a: []}, n = 100; |
浏览器里可以发现两次输出的结果中 a[50]
都是 -100
。这一点如果第一次遇到的话还真是匪夷所思。
这里我故意把 a
数组的元素弄得很多,使 DevTools
以 折叠 方式显示:
Object {a: Array[100]}
Object {a: Array[100]}
看似友好的显示方式,实际上里面有很大的问题。
当我们展开第一个输出时, DevTools
会 及时 读取变量值,由于这是个 引用 类型,实际上它读到的是 obj
的最终值,及 a[50]
是 -100
。
如果数组a只有很少的元素,DevTools
不启用智能显示时就不会出现这个问题。
也就是说,console.log
到 DevTools
里的实际上是引用而不是拷贝,展开操作会及时读取变量值。
如果把上面例子的两个输出改成:
1 | console.log(JSON.stringify(obj)); |
结果将和预期的一致。
因此,在浏览器中调试 js
程序应该以 调试器
下断点为主,日志为辅。
@Component({ template: `<child [value]="bindValue"></child>`})class HomeComponent { bindValue:string = 'hello';}@Component({ selector: 'child', template: `...`})class ChildComponent{ @Input() value; ngOnInit(){ // 这里可以取到value的值为'hello' // 当bindValue动态改变时,此函数不会再次调用,需要在ngOnChanges中手动更新 } ngOnChanges(changes) { // this.value = changes['value'].currentValue; // ... // 但是要小心这个函数会被频繁调用, // 不要做复杂逻辑 }}
map回调如果不给出返回值,则默认返回 undefined
,MDN文档中似乎并未提到这一点。
var numbers = [1, 4, 9];var roots = numbers.map(n => { if (n === 4) { return -1; }});// [undefined, -1, undefined]
]]>.view-container { float: right !important; z-index: 999; /* invalid! */ /* position must be set */ position: relative;}
]]>const _new = Object.assign(target, {...});
会改变 target
本身,引发问题。
The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
有三个解决办法:
逐个拷贝
const _new = {k1: target.k1, k2: target.k2, ...};
ES6
const _new = {...target, ...{...}};
(最佳)把第一个参数设成空对象
const _new = Object.assign({}, target, {...});
]]>@routerCanActive
在加载组件前执行,其回调函数有两种返回方式:@routerCanActive(function() { // return true; 同步 // return Promise.resolve(true); 异步})
]]>Docker的官方定义是:
Docker allows you to package an application with all of its dependencies into a standardized unit for software development.
毫无疑问的是,Docker解决了应用部署上一个巨大的问题:
客户: 安装好了,用不了。
发布者:我的机器上没问题。
如何解决每个应用的依赖在Docker出现之前是个头疼的问题,现在仅仅通过一次配置,Dockerfile或者image作为最终交付,就能在任何Linux上完美运行了。说起来很简单的样子,但在Docker配置过程中,又存在很多值得思考的问题:应用各个组件如何安排?一个Container解决问题还是细化Container?Container之间何如通信?等等。。下面用一个最普遍的WEB应用配置部署来说明这些问题。
NOTE:本文假定读者对Docker中的一些概念有基本的认识,如果不甚了解,我推荐这篇文章:
https://linux.cn/article-6074-weibo.html
典型的PHP应用配置方案是LAMP或者LNMP,本文以LNMP为例。
设计方案如下图:
应用由4个组件组成,分别是Nginx,PHP-FPM(PHP),MySQL以及WWW,4个组件运行在由各自镜像创建出来的独立的容器中。其中WWW Container只是一个存储业务代码和静态资源的容器,可以认为是”死”的。
事实上LNMP架构采用上面的设计方式应该是最容易想到的,也是最清晰的,每个组件有相对的独立性。其中除了WWW容器,其他3个容器都可以直接通过官方镜像构建出来。
然而网上很多同学并不是这样做的,不会分的这么细,通常是把Nginx和WWW放到一个容器内,或者干脆全部放到一个容器中。可以学习一下大家的Dockerfile:
https://github.com/search?utf8=✓&q=docker-lnmp
细化Container这种设计的优缺点:
细化Container面临着另一个问题,就是如何进行容器间通信。下面简要描述一下上图中的数据流程:
这样耦合关系就出来了:
可以看出有两类耦合关系:端口和文件系统。
对于端口耦合,docker是通过–link选项解决的;对于文件系统耦合,docker是通过–volumes-from选项解决的。
解决第一个耦合关系:
1 | $ sudo docker run -p 80:80 -p 443:443 # 主机端口映射到容器 |
解决第二个耦合关系:
1 | $ sudo docker run --volumes-from WWW_CONTAINER_NAME |
参考文档:https://docs.docker.com/reference/run/
因此容器启动的先后顺序就出来了:
其中1和2可以对换。
Dockerfiles 请参见:
https://github.com/micooz/dockerfile
http://git.oschina.net/micooz/dockerfile
利用Docker部署Web应用可以带来很多便利,在宏观上实现应用组件化,为实现分布式系统奠定了基础。
可以看到实际上在Docker容器间共享数据是很方便的,搞清楚各容器的依赖关系就不难解决。
P.s. 本文是我学习docker两天后的心得体会,纰漏在所难免,如有错误还请斧正。
]]>需要switch一个字符串来执行相应代码块,然而原生的switch-case条件选择语法针对condition有严格的限制,下面摘录一段switch的语法标准:
Transfers control to one of the several statements, depending on the value of a condition.
Syntax
1 | attr(optional) switch ( condition ) statement |
原生switch语法的condition支持整数,枚举或者根据上下文能够隐式转换为整数或者枚举的类,再或者是非数组类型的=或{}初始化语句,举例来说就是如下四类:
1 | switch(100) |
必须是常量,这样一来就无法做变量与变量之间的比较。
当statement没有被{}包围的时候,在其内使用声明语句会导致编译错误。比如:
1 | switch(num){ |
显然我的需求不能用原生的switch来实现,得老老实实if-elseif-elseif…来分别判断,导致代码又长又臭,if越套越多,逐条判断效率也让人心塞。
有需求就有想法,有想法就有创新,相信这样的需求早就有人实现过了,比如:
想法很不错,但是我想要更灵活的解决方案,我希望新的switch支持switch(object),case(object),还希望statement对变量声明没有限制,何不完全抛开原生switch的枷锁,自己利用标准库造一个switch来解决问题呢。
我希望利用C++强大的template来兼容任意类型,用C++11的lambda匿名函数实现statement,用操作符重载operator==来匹配条件,用hash表来提升匹配效率,看起来很容易不是吗?
开始Coding之前我先拟定好蓝图:
1 | // 蓝图1 |
我承认我是受到了javascript的影响,我一直以为C++越来越像是一种高级的脚本语言,或许也是它未来的发展趋势。
蓝图的设计首先符合C++的语法规范,没有语法错误,其次力求语义明确,简洁。
蓝图1的大括号太多,书写时容易出错。
蓝图2语法简洁明了,我相信任何会闭包的Coder都能理解。
有了蓝图后我们就可以照着这个模样来写代码了,首先分析一下蓝图2。
C++建议模板类的声明和定义必须写在同一个文件里,因此起一个switch.hpp文件:
1 |
|
这么一来就实现了found得链式操作,存储了kv对,全局(也可以在某个命名空间内)select函数是一个简化书写的帮助函数,创建对象后返回该对象的拷贝,实现了如下调用:
1 | select(std::string("condition")) |
接下来我需要实现查找到对应的target,然后调用它的callback。
增加一个done()方法,该方法被调用意味着结束整个Switch,开始匹配found块,如果没找到,调用others函数(对应default块):
1 | inline void done() { |
std::map的find方法时间复杂度是O(logN),而原生switch匹配时间复杂度是O(1),肯定是有很大差距的,但是为了实现switch没有的功能,这点损失也是十分值得的。
others方法如下:
1 | inline void others(const Scope& callback) { |
当用户调用others方法定义了default块之后,就没必要再调用done了,又可以减少7个字符的书写。
这里has_others_scope_为bool成员;others_是单独存放的lambda表达式成员,为了简化查找,不宜放在reflections_中。
再简化书写,用typedef缩短类型,然后替换原类中相应类型为短类型:
1 | typedef std::function<void(void)> Scope; |
这么一来几乎很完美了,全新的Switch如下:
1 |
|
我想进一步实现自定义class的case,定义一个User类:
1 | class User { |
Switch如下:
1 | User u1(20), u2(22), ux(20); |
非常有必要说明的是这个重载:
1 | bool operator<(const User& user) const { return this->age_ < user.age(); } |
返回bool没有问题,但为什么必须是operator<呢,原因在这句:
1 | auto kv = reflections_.find(target_); |
std::map<>::find不是通过==进行查找的,而是<,因此必须重载<。
该重载必须被const修饰,原因也是find这句里面,const对象只能调用const方法。
标准库里的实现如下:
1 | struct _LIBCPP_TYPE_VIS_ONLY less : binary_function<_Tp, _Tp, bool> |
可以非常明显的看到const和<。
此外我还实现了Switch之间的found块组合,比较简单就不阐述了。
常量字符串的转型问题:
1 | select("condition") |
编译器将”condition”理解为const char[10],数组类型有固定长度,found块的_case参数类型是const char[5],导致编译错误。原因在于:
1 | Switch& found(const Ty& _case, const Scope& callback) |
这里传递const引用,因此编译器把”case”当做了const char[5]。此时Ty的类型和说好的const char[10]不一致,编译失败。
解决方法是通过std::string来避免数组长度不匹配问题:
1 | select(std::string("condition")) |
希望读者有更好地解决方案。
这里直接引用我项目里面的实现:
1 |
|
欢迎各位读者指正。
]]>每个源文件都要对应一个头文件。例外:单元测试文件和仅包含main的小型源文件。
以.h结尾的都是应该是独立的,以.inc结尾的仅用作文本包含,所有头文件都必须是独立的。
inline和template函数的声明和定义(实现)应该在同一个文件中。
Note:这里的独立是指,用户和重构工具可以无特殊限制地包含头文件。
所有头文件都应该使用#define来防止重复包含。格式如下:
头文件路径:foo/src/bar/baz.h
<!-- lang: cpp -->#ifndef FOO_BAR_BAZ_H_#define FOO_BAR_BAZ_H_...#endif // FOO_BAR_BAZ_H_
前置声明用来避免不必要的include,但有很多缺陷。
仅在函数少于10行时,才可以把函数定义为inline。
过度使用inline,实际上会使程序变慢。inline一个非常短的函数可以缩短代码长度,而inline一个很长的函数反而会戏剧性地增大代码长度。
小心析构函数,他们的代码通常比表面上的更长,因为存在虚函数和父析构函数调用。
虚函数和递归函数不应该被inline。
顺序应该是:输入,输出
输入参数通常是按值,或按const引用传入。输出则是non-const的指针(引用也是指针实现的)。
当要新增一个参数时,应该按照上面顺序加入。
注意这个规则不是硬性的,可以放宽这个规则以确保一致性。
顺序:
每个部分再按字典顺序排列。例如:
<!-- lang: cpp -->#include "foo/server/fooserver.h"#include <sys/types.h>#include <unistd.h>#include <hash_map>#include <vector>#include "base/basictypes.h"#include "base/commandlineflags.h"#include "foo/server/bar.h"
系统相关的头文件可用宏来限定,以减小代码和保持本地化。
<!-- lang: cpp -->#include "foo/public/fooserver.h"#include "base/port.h" // For LANG_CXX11.#ifdef LANG_CXX11#include <initializer_list>#endif // LANG_CXX11
在源文件中鼓励使用匿名命名空间( namespace {…} ),对于非匿名命名空间则选取它的路径来命名。不要使用using namespace。不要使用inline命名空间。
可以避免链接期的名字冲突:
namespace { // This is in a .cc file.
// The content of a namespace is not indented.
//
// This function is guaranteed not to generate a colliding symbol
// with other symbols at link time, and is only visible to
// callers in this .cc file.
bool UpdateInternals(Frobber* f, int newval) {
…
}
} // namespace
不要在头文件中使用匿名命名空间。
在includes,gflags声明和定义,其他命名空间中class的前置声明之后,包裹全部的代码:
// In the .h file
namespace mynamespace {
// All declarations are within the namespace scope.
// Notice the lack of indentation.
class MyClass {
public:
...void Foo();
};
} // namespace mynamespace
// In the .cc file
namespace mynamespace {
// Definition of functions is within scope of the namespace.
void MyClass::Foo() {
...
}
} // namespace mynamespace
不要在std中声明任何东西,也不要对标准库的class进行前置声明。
不要使用using namespace
// Forbidden – This pollutes the namespace.
using namespace foo;
可以在源文件的任意位置,在头文件的函数,方法,class中使用using声明
// OK in .cc files.
// Must be in a function, method or class in .h files.
using ::foo::bar;
别名命名空间可以在源文件的任意位置,头文件的namespace内,函数,方法内使用。
// Shorten access to some commonly used names in .cc files.
namespace fbz = ::foo::bar::baz;
// Shorten access to some commonly used names (in a .h file).
namespace librarian {
// The following alias is available to all files including
// this header (in namespace librarian):
// alias names should therefore be chosen consistently
// within a project.
namespace pd_s = ::pipeline_diagnostics::sidetable;
inline void my_inline_function() {
// namespace alias local to a function (or method).namespace fbz = ::foo::bar::baz;...
}
} // namespace librarian
Note:尽量避免在公共头文件中使用别名命名空间。
<!-- lang: cpp -->class Foo { private: // Bar is a member class, nested within Foo. class Bar { ... };};
不要把成员类公开,除非他们确实是这个接口的一部分。
最好在命名空间中使用非成员函数,或使用静态成员函数,而不要或很少使用全局函数。
非成员函数不应该依赖于外部变量,并且应该总处于一个命名空间中。
那些仅仅用来集结静态成员函数,且没有共享静态数据的类,应该用命名空间取而代之。
如果必须要使用非成员函数并且只在当前这个源文件中使用,则用一个匿名命名空间或static修饰来限制它的作用域。
将一个函数中的变量放到尽可能有限的作用域内,并在声明时初始化。
<!-- lang: cpp -->int i;i = f(); // Bad -- initialization separate from declaration.int j = g(); // Good -- declaration has initialization.vector<int> v;v.push_back(1); // Prefer initializing using brace initialization.v.push_back(2);vector<int> v = {1, 2}; // Good -- v starts initialized.
一些供if,while,for使用的变量应该在statement处声明。
<!-- lang: cpp -->while (const char* p = strchr(str, '/')) str = p + 1;
一点注意:如果是变量是一个对象,构造函数将在每次进入作用域时被执行,析构函数将在每次离开作用域时被执行。
<!-- lang: cpp -->// Inefficient implementation:for (int i = 0; i < 1000000; ++i) { Foo f; // My ctor and dtor get called 1000000 times each. f.DoSomething(i);}// 对象放在外面更高效Foo f; // My ctor and dtor get called once each.for (int i = 0; i < 1000000; ++i) { f.DoSomething(i);}
类的静态或全局变量是禁止使用的。然而constexpr的变量是被允许的,因为他们不会动态初始化或销毁。
静态存储的对象,包括全局变量,静态变量,静态类成员变量以及函数内静态变量,必须是Plain Old Data(POD)。
只有int,char,float,pointer,或者arrays/structs属于POD。
静态vector应该用C数组代替,静态string应该用const char []代替。
如果需要使用静态或全局类对象,考虑初使用它的指针类型(不会被order-of-destrctor释放掉),注意必须是一个纯的指针,而不是一个智能指针。
避免在构造函数中进行复杂初始化,比如那些可能会失败或者进行虚函数调用的步骤。
构造函数不应该调用虚函数,或者试图抛出非致命性错误。如果你的对象需要进行重要的初始化工作,考虑使用一个工厂函数或init方法。
在具有一个参数的构造函数上使用explicit关键字。因为构造时传入一个参数可能会被编译器当做拷贝构造进行隐式转换。
<!-- lang: cpp -->explicit Foo(string name);
除了构造函数的第一个参数外,其他参数都应该指定一个默认值,来防止不期望的类型转换。
<!-- lang: cpp -->Foo::Foo(string name, int id = 42)
拷贝构造函数,以及作为其他类的透明包装的类,不应该被explicit修饰。
可拷贝的例子:std::string
可移动但不可拷贝的例子:std::unique_ptr
对于一些不需要拷贝操作的类型,提供拷贝操作符可能会产生混淆,无意义或者完全是不正确的。
基类的拷贝、赋值操作符是有风险的,他们会导致对象分裂。
如果要加入拷贝特性,就要同时定义拷贝构造函数和赋值操作符。
如果你的类型可拷贝,但移动操作符更高效,那么就同时定义移动操作。
避免给试图被继承的类提供赋值操作符或公开的拷贝/移动构造函数。
如果你的基类需要被拷贝,提供一个公开的虚Clone()方法,和一个保护的拷贝构造函数,来使子类能够实现它。
如果你不想支持拷贝/移动操作,使用 = delete 来明确地禁用它们。
委托构造的例子:
<!-- lang: cpp -->X::X(const string& name) : name_(name) { ...}X::X() : X("") { }
继承构造的例子:
<!-- lang: cpp -->class Base { public: Base(); Base(int n); Base(const string& s); ...};class Derived : public Base { public: using Base::Base; // Base's constructors are redeclared here.};
在能减少冗余和改善可读性的前提下使用委托和继承。
struct仅在存储数据时使用,否则使用class。
struct可以直接访问字段而不通过方法调用。struct内的方法只用来设置数据成员。
如果需要更多地函数支持,class更合适,如果不确定用哪个,就用class。
注意struct和class内的成员变量具有不同的命名方式。
组合通常比继承更合适。当使用继承时,指明为public。
实际上,继承在C++中主要应用于两个方面:
所有的继承都应当是public的,如果你需要用private继承,你就应该保存一份基类的成员实例来替代private继承(达到private的效果)。
不要过度使用实现继承,因为代码实现被分散于子类和基类。
继承应该被限定为”is-a”的关系,即”子类是基类的特例”。
如果你的类中有虚函数,那么你的析构函数也必须是virtual。
数据成员应该是private的。
仅有很少的多重实现继承是有用的。你通常可以找到一个不同的,更明确地,更干净的解决方案。
多继承仅允许在父类都是纯接口的时候使用。
一个类是纯接口的条件:
不要重载那些很少使用的,特殊的操作符。不要给用户字面值定义操作符。
为了使类模板函数正常工作,你或许需要定义操作符。
虽然操作符重载可以使代码更简洁,但它有以下几个缺点:
重载也会出现意想不到的后果,比如,一个类重载了一元operator&,它将不能安全的被前置声明。
一般而言,不要定义操作符重载。需要时你可以用普通的函数例如Equals()来替代。
不要重载operator””,即用户字面值。
当然,有些情况下可以进行重载,例如对标准C++库:
<!-- lang: cpp -->operator<<(ostream&, const T&) for logging
使数据成员是private的,然后提供访问函数(getter/setter)来访问它们。例如:一个叫做foo_的变量有一个foo()访问函数,或者set_foo()。
例外:static const 数据成员(kFoo)不应该是private。
访问器的定义通常是内联在头文件中的。
public: protected: private: ;方法在数据成员之前。
每个区块内的顺序:
友元函数总是在private内声明。
在源文件中的方法定义应该和声明顺序尽量保持一致。
书写短小的,清晰地函数。如果一个函数超过40行,可以考虑是否可以在不改变程序结构的前提下进行分割。
最好使用std::unique_ptr来使所有权传递更明确:
<!-- lang: cpp -->std::unique_ptr<Foo> FooFactory();void FooConsumer(std::unique_ptr<Foo> ptr);
在没有一个非常好的理由的前提下,不要把你的代码设计为共享所有权。
不要再新的代码中使用scoped_ptr除非为了适配老版本的C++。
不要使用std::auto_ptr,用std::unique_ptr代替。
使用cpplint.py来检查风格错误。
所有按引用传递的参数必须被const修饰。
<!-- lang: cpp -->void Foo(const string &in, string *out);
有些情况下用const T* 做输入参数比const T&好:
记住大多数时候输入参数都写为const T&。
右值引用只在move构造函数以及move赋值操作符定义时使用,不要使用std::forward。
如果一个函数依靠参数类型的不同来进行重载,读者可能必须理解C++的复杂匹配机制来确定接下来会发生什么。
如果你像重载一个函数,考虑给出一些信息对参数进行限定,例如使用AppendString(), AppendInt()而不仅仅是Append()。
除了下面几个情形,我们不允许默认函数参数。如果合适的话,请用函数重载在替代。
当默认参数做函数指针时容易使人混淆,因为函数签名常常不会匹配调用。加入默认参数时会改变函数的类型,这样会在获取它的地址时造成一些问题。使用函数重载可以避免这个问题。
一些例外:
当一个静态函数出现在源文件中时,由于本地化的缘故上述规则不再适用。
另外,默认参数可在构造函数中使用,上述规则也不适用,因为不可能获取到构造函数的地址。
还有一个例外是默认参数用来模拟变长数组时,例如:
<!-- lang: cpp -->// Support up to 4 params by using a default empty AlphaNum.string StrCat(const AlphaNum &a, const AlphaNum &b = gEmptyAlphaNum, const AlphaNum &c = gEmptyAlphaNum, const AlphaNum &d = gEmptyAlphaNum);
我们不允许使用变长数组或者alloca()
变长数组和alloca()并不是标准C++的一部分。更重要的是,他会在栈空间中分配大量的空间,可能会触发内存覆盖的bug:在我机器上运行的好好的,在生产环境却死掉了。。
友元通常都应该被定义在同一个文件中。
通常会定义一个FooBuilder作为Foo的一个友元。
在创建一个单元测试的时候,使用友元很管用。
我们不使用C++的exceptions。
避免使用RTTI。
在运行时查询对象的类型可以说是一个错误的设计问题。
使用C++的类型转换如static_cast
使用const_cast来除去const限定,只在你知道你在做什么的情况下,使用reinterpret_cast来做不安全的指针转换。
流仅用于日志。使用类似printf的形式来代替流。
在迭代器和其他模板对象上使用前加或前减。
当表达式的返回值被忽略时,++i比i++更高效。如果i是一个迭代器,由于i++的复制,开销很大。
只要讲得通,随时随地使用const。C++11中的constexpr是更好的选择。
const应该放在哪儿?
<!-- lang: cpp -->int const *foo;const int* foo;
把const放在第一位具有更好地可读性,因为它符合英语的习惯:形容词(const),然后是名词(int)。
在C++11中,可以使用constexpr来定义真实地常量或者确保常量初始化。
<stdint.h>中定义了一些不同长度的整形:int16_t, uint32_t, int64_t 等等。你应该总是使用这些整形,特别是你需要保证整形的长度时。
当我们认为一个整数”比较大”时,用int64_t。
你不应该使用无符号整形,如uint32_t,除非有一个合理的原因。特别的,不要认为使用了无符号类型它就不会是负数,使用断言来检验正负。
如果你需要接收一个容器的大小,确保你的类型能够容纳这个数字,否则使用一个更大的类型。
注意整形转换、类型提升可能会导致非预期行为。
关于无符号整形
<!-- lang: cpp -->for (unsigned int i = foo.Length()-1; i >= 0; --i) ...
这将是一个死循环,因为unsigned int 和 int 比较。因此使用断言来证明非负。不要使用无符号类型来表示非负数。
最好使用内联函数、枚举和const量代替宏。在使用宏之前,考虑是否有非宏的解决方案。
如果你使用了宏,应该注意:
用0表示整形,0.0表示实数、nullptr(或NULL)表示指针,’\0’表示字符。
特别在一些情况下,sizeof(NULL)和sizeof(0)不同。
最好使用sizeof(varname)而非sizeof(type)。因为当var改变时,sizeof(type)不会改变而sizeof(varname)会跟着改变。
<!-- lang: cpp -->Struct data;memset(&data, 0, sizeof(data));memset(&data, 0, sizeof(Struct));if (raw_size < sizeof(int)) { LOG(ERROR) << "compressed record not big enough for count: " << raw_size; return false;}
只在类型名十分混乱时使用auto,如果能增加可读性,继续使用完整地类型声明,除了局部变量外不要到处都用auto。
不要在文件域,命名空间域,类成员中使用auto。从不对大括号初始化列表使用auto。
一些例子:
<!-- lang: cpp -->// Basically the same, ignoring some small technicalities.// You may choose to use either form.vector<string> v = {"foo", "bar"};// Usable with 'new' expressions.auto p = new vector<string>{"foo", "bar"};// A map can take a list of pairs. Nested braced-init-lists work.map<int, string> m = {{1, "one"}, {2, "2"}};// A braced-init-list can be implicitly converted to a return type.vector<int> test_function() { return {1, 2, 3}; }// Iterate over a braced-init-list.for (int i : {-1, -2, -3}) {}// Call a function using a braced-init-list.void TestFunction2(vector<int> v) {}TestFunction2({1, 2, 3});
也可以给自己的类型定义初始化列表:
<!-- lang: cpp -->class MyType { public: // std::initializer_list references the underlying init list. // It should be passed by value. MyType(std::initializer_list<int> init_list) { for (int i : init_list) append(i); } MyType& operator=(std::initializer_list<int> init_list) { clear(); for (int i : init_list) append(i); }};MyType m{2, 3, 5, 7};
在没有使用std::initializer_list
<!-- lang: cpp -->double d{1.23};// Calls ordinary constructor as long as MyOtherType has no// std::initializer_list constructor.class MyOtherType { public: explicit MyOtherType(string); MyOtherType(int, string);};MyOtherType m = {1, "b"};// If the constructor is explicit, you can't use the "= {}" form.MyOtherType m{"b"};
不要给{}使用auto:
<!-- lang: cpp -->auto d = {1.23}; // d is a std::initializer_list<double>auto d = double{1.23}; // Good -- d is a double, not a std::initializer_list.
在适当的条件下用lambda表达式,不要使用默认的lambda捕获,把所有捕获明确地写出来。
如果匿名函数超过了5行,考虑给它起一个名字或者使用一个有名函数代替lambda表达式。
合适的时候,用C++11编写的类库。在使用C++11之前,考虑好对其他环境的可移植性。
函数名,变量名和文件名应该是具有描述性的,而不是缩略的。
<!-- lang: cpp -->int price_count_reader; // No abbreviation.int num_errors; // "num" is a widespread convention.int num_dns_connections; // Most people know what "DNS" stands for.int n; // Meaningless.int nerr; // Ambiguous abbreviation.int n_comp_conns; // Ambiguous abbreviation.int wgc_connections; // Only your group knows what this stands for.int pc_reader; // Lots of things can be abbreviated "pc".int cstmr_id; // Deletes internal letters.
文件名都该是小写的,并且可以包含下划线或者短划线-。如果你的项目没有约定,最好用下划线。
一些例子:
<!-- lang: cpp -->my_useful_class.ccmy-useful-class.ccmyusefulclass.ccmyusefulclass_test.cc // _unittest and _regtest are deprecated.
不要使用在/usr/include中已近存在的文件名,例如:db.h
普遍地,明确你的文件名。例如:http_server_logs.h 比 logs.h 好很多。
如果内联函数非常短,应该直接写在头文件中。
类型名以大写字母开头,并且每个词开头都是大写的:MyExcitingClass, MyExcitingEnum。
<!-- lang: cpp -->// classes and structsclass UrlTable { ...class UrlTableTester { ...struct UrlTableProperties { ...// typedefstypedef hash_map<UrlTableProperties *, string> PropertiesMap;// enumsenum UrlTableErrors { ...
变量和数据成员都是小写的,单词间有下划线:a_local_variable, a_struct_data_member, a_class_data_member_。
<!-- lang: cpp -->string table_name; // OK - uses underscore.string tablename; // OK - all lowercase.string tableName; // Bad - mixed case.
类数据成员,末尾一个下划线:
<!-- lang: cpp -->class TableInfo { ... private: string table_name_; // OK - underscore at end. string tablename_; // OK. static Pool<TableInfo>* pool_; // OK.};
结构体数据成员,末尾没有下划线:
<!-- lang: cpp -->struct UrlTableProperties { string name; int num_entries; static Pool<UrlTableProperties>* pool;};
全局变量,没有一个特定的规则。如果你需要一个规则,考虑给全局变量加上g_前缀,来区分局部变量。
在常量之前加上k,例如:const int kDaysInWeek = 7。
和变量命名方式相似,但可以是大小写混合:
<!-- lang: cpp -->MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().
访问器(get)和修改器(set)应该匹配要访问或修改的那个变量名:
<!-- lang: cpp -->class MyClass { public: ... int num_entries() const { return num_entries_; } void set_num_entries(int num_entries) { num_entries_ = num_entries; } private: int num_entries_;};
全为小写,并且尽可能是表示目录结构:google_awesome_project.
<!-- lang: cpp -->enum UrlTableErrors { kOK = 0, kErrorOutOfMemory, kErrorMalformedInput,};enum AlternateUrlTableErrors { OK = 0, OUT_OF_MEMORY = 1, MALFORMED_INPUT = 2,};
通常情况下宏不应该被使用,然而它又绝对是需要的,宏名应该全为大写。
<!-- lang: cpp -->#define ROUND(x) ...#define PI_ROUNDED 3.0
如果你在为现存的C/C++代码工作,依据现存的命名方式,例如:
<!-- lang: cpp -->bigopen() function name, follows form of open()uint typedefbigpos struct or class, follows form of possparse_hash_map STL-like entity; follows STL naming conventionsLONGLONG_MAX a constant, as in INT_MAX
为你的读者写注释,因为下一个读者也许就是你。
保持//和/**/的一致性,通常//更普遍。
以许可协议(例如:Apache 2.0, BSD, LGPL, GPL)开头,接下来是关于内容的描述。
如果你对一个已近存在作者标记的文件进行了修改,请删除作者那一行。
不要重复在头文件和源文件中书写注释。
每个类定义都应该有一个用于描述它的作用,如何使用的注释:
<!-- lang: cpp -->// Iterates over the contents of a GargantuanTable. Sample usage:// GargantuanTableIterator* iter = table->NewIterator();// for (iter->Seek("foo"); !iter->done(); iter->Next()) {// process(iter->key(), iter->value());// }// delete iter;class GargantuanTableIterator { ...};
<!-- lang: cpp -->// Returns an iterator for this table. It is the client's// responsibility to delete the iterator when it is done with it,// and it must not use the iterator once the GargantuanTable object// on which the iterator was created has been deleted.//// The iterator is initially positioned at the beginning of the table.//// This method is equivalent to:// Iterator* iter = table->NewIterator();// iter->Seek("");// return iter;// If you are going to immediately seek to another place in the// returned iterator, it will be faster to use NewIterator()// and avoid the extra seek.Iterator* GetIterator() const;
去掉不必要的注释:
<!-- lang: cpp -->// Returns true if the table cannot hold any more entries.bool IsTableFull();
应该说明这个变量用来干什么,在特定的情况下,需要更多的注释。例如:
<!-- lang: cpp -->private: // Keeps track of the total number of entries in the table. // Used to ensure we do not go over the limit. -1 means // that we don't yet know how many entries the table has. int num_total_entries_;
所有的全局变量都应该给出一个注释,来描述它用来做什么。
<!-- lang: cpp -->// The total number of tests cases that we run through in this regression test.const int kNumTestCases = 6;
一些难懂的,复杂的代码块应该被注释:
<!-- lang: cpp -->// Divide result by two, taking into account that x// contains the carry from the add.for (int i = 0; i < result->size(); i++) { x = (x << 8) + (*result)[i]; (*result)[i] = x >> 1; x &= 1;}
意义不明显的行末应该空两个字符并给出注释:
<!-- lang: cpp -->// If we have enough memory, mmap the data portion too.mmap_budget = max<int64>(0, mmap_budget - index_->length());if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock)) return; // Error already logged.
如果接下来的几行都有注释,最好排列起来增强可读性:
<!-- lang: cpp -->DoSomething(); // Comment here so the comments line up.DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment.{ // One space before comment when opening a new scope is allowed, // thus the comment lines up with the following comments and code. DoSomethingElse(); // Two spaces before line comments normally.}vector<string> list{// Comments in braced lists describe the next element .. "First item", // .. and should be aligned appropriately. "Second item"};DoSomething(); /* For trailing block comments, one space is fine. */
当你传递一个空指针,布尔值或者字面整形值时,你应该考虑添加注释来说明他们是什么,或者使你的代码自我注释:
<!-- lang: cpp -->bool success = CalculateSomething(interesting_value, 10, false, NULL); // What are these arguments??versus:bool success = CalculateSomething(interesting_value, 10, // Default base value. false, // Not the first time we're calling this. NULL); // No callback.Or alternatively, constants or self-describing variables:const int kDefaultBaseValue = 10;const bool kFirstTimeCalling = false;Callback *null_callback = NULL;bool success = CalculateSomething(interesting_value, kDefaultBaseValue, kFirstTimeCalling, null_callback);
切记不要描述代码自身:
<!-- lang: cpp -->// Now go through the b array and make sure that if i occurs,// the next element is i+1.... // Geez. What a useless comment.
完整的句子往往比句子片段更容易理解。
使用TODO注释是一个临时的,短期的解决方案,很好的但不是完美的。
当你创建一个TODO时,总是给出你的名字:
<!-- lang: cpp -->// TODO(kl@gmail.com): Use a "*" here for concatenation operator.// TODO(Zeke) change this to use relations.
使用DEPRECATED注释标记一个弃用的接口。
在DEPRECATED之后写出你的名字,email地址或其他能标识你的信息。
弃用注释必须包含简易的,清楚的指引来帮助使用者修复他们的问题。C++中,你可以将弃用的方法放在内联函数中,然后调用新的接口。
一行最多80个字符。
一些原始字符串可能会超出80个字符,除了测试外,这样的字符串应该出现在该文件的顶部。
#include 语句可能会超过80列。
非ascii字符很少出现,但必须使用UTF-8编码。
十六进制也可以使用,在那些需要加强可读性的地方更建议使用。
你不应该使用C++11提供的char116_t和char32_t,因为他们用于非UTF8得文本。同样的,,你也不应该使用wchar_t,除非你是在用Windows API编写程序。
只用空格并且缩进两个字符。
返回类型出现在函数名的同一行,如果能适应,参数也出现在同一行。如果不能适应,折行书写参数表。
<!-- lang: cpp -->ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); ...}If you have too much text to fit on one line:ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { DoSomething(); ...}or if you cannot fit even the first parameter:ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 4 space indent Type par_name2, Type par_name3) { DoSomething(); // 2 space indent ...}
需要指出:
如果一些参数未使用,在函数声明处注释出来:
<!-- lang: cpp -->// Always have named parameters in interfaces.class Shape { public: virtual void Rotate(double radians) = 0;};// Always have named parameters in the declaration.class Circle : public Shape { public: virtual void Rotate(double radians);};// Comment out unused named parameters in definitions.void Circle::Rotate(double /*radians*/) {}// Bad - if someone wants to implement later, it's not clear what the// variable means.void Circle::Rotate(double) {}
像其他函数一样格式化参数和函数体,捕获表如其他逗号分隔的列表。
<!-- lang: cpp -->int x = 0;auto add_to_x = [&x](int n) { x += n; };
简短的lambda表达式应该作为函数参数内联:
<!-- lang: cpp -->std::set<int> blacklist = {7, 8, 9};std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) { return blacklist.find(i) != blacklist.end(); }), digits.end());
将函数调用写在一行,用小括号包裹参数;或者将参数置于新行,用4个空格缩进。使用最小的行数。
如下格式:
<!-- lang: cpp -->bool retval = DoSomething(argument1, argument2, argument3);
如果参数太多,折行书写,括号左右不要有空格。
<!-- lang: cpp -->bool retval = DoSomething(averyveryveryverylongargument1, argument2, argument3);if (...) { ... ... if (...) { DoSomething( argument1, argument2, // 4 space indent argument3, argument4); }
<!-- lang: cpp -->// Examples of braced init list on a single line.return {foo, bar};functioncall({foo, bar});pair<int, int> p{foo, bar};// When you have to wrap.SomeFunction( {"assume a zero-length name before {"}, some_other_function_parameter);SomeType variable{ some, other, values, {"assume a zero-length name before {"}, SomeOtherType{ "Very long string requiring the surrounding breaks.", some, other values}, SomeOtherType{"Slightly shorter string", some, other, values}};SomeType variable{ "This is too long to fit all in one line"};MyType m = { // Here, you could also break before {. superlongvariablename1, superlongvariablename2, {short, interior, list}, {interiorwrappinglist, interiorwrappinglist2}};
最好括号内无空格。if和else关键字属于分立的行中。
<!-- lang: cpp -->if(condition) { // Bad - space missing after IF.if (condition){ // Bad - space missing before {.if(condition){ // Doubly bad.if (condition) { // Good - proper space after IF and before {.
简短的条件块可以写在一行,如果能强化可读性。
<!-- lang: cpp -->if (x == kFoo) return new Foo();if (x == kBar) return new Bar();
如果含有else块,则不允许写在一行:
<!-- lang: cpp -->// Not allowed - IF statement on one line when there is an ELSE clauseif (x) DoThis();else DoThat();
一般而言,单行条件语句不需要花括号,如果你喜欢也可以加上,特别是在循环中存在复杂的条件时,使用花括号可增加可读性。有些项目还要求if块必须含有完整地括号对。
然而,if-else一部分使用了花括号,那么所有块都必须使用:
<!-- lang: cpp -->// Not allowed - curly on IF but not ELSEif (condition) { foo;} else bar;// Not allowed - curly on ELSE but not IFif (condition) foo;else { bar;}// Curly braces around both IF and ELSE required because// one of the clauses used braces.if (condition) { foo;} else { bar;}
switch块必须包含default,如果default永不会被执行,则写一个assert:
<!-- lang: cpp -->switch (var) { case 0: { // 2 space indent ... // 4 space indent break; } case 1: { ... break; } default: { assert(false); }}
空的case块用花括号括起来。
单语句循环,花括号可以省略,空循环用花括号括起来或者写continue,而不仅仅是分号。
<!-- lang: cpp -->while (condition) { // Repeat test until it returns false.}for (int i = 0; i < kSomeNumber; ++i) {} // Good - empty body.while (condition) continue; // Good - continue indicates no logic.while (condition); // Bad - looks like part of do/while loop.
句号和箭头周围无空格:
<!-- lang: cpp -->x = *p;p = &x;x = r.y;x = r->y;
当声明一个指针类型变量时,保证星号两侧仅有一个空格:
<!-- lang: cpp -->// These are fine, space preceding.char *c;const string &str;// These are fine, space following.char* c; // but remember to do "char* c, *d, *e, ...;"!const string& str;char * c; // Bad - spaces on both sides of *const string & str; // Bad - spaces on both sides of &
当一个布尔表达式超过标准行长度(80)时,保证折行书写的一致性。
<!-- lang: cpp -->if (this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another && last_one) { ...}
不要给return语句加上括号。仅在返回一个逻辑表达式的时候加括号。
<!-- lang: cpp -->return result; // No parentheses in the simple case.// Parentheses OK to make a complex expression more readable.return (some_long_condition && another_condition);return (value); // You wouldn't write var = (value);return(result); // return is not a function!
可以选择=,()或者{}。下面都是正确的:
<!-- lang: cpp -->int x = 3;int x(3);int x{3};string name = "Some Name";string name("Some Name");string name{"Some Name"};
当心{}会调用std::initializer_list 构造函数。为了避免这个问题,使用():
<!-- lang: cpp -->vector<int> v(100, 1); // A vector of 100 1s.vector<int> v{100, 1}; // A vector of 100, 1.
总是在一行的开头书写,无论是不是在代码块内:
<!-- lang: cpp -->// Good - directives at beginning of line if (lopsided_score) {#if DISASTER_PENDING // Correct -- Starts at beginning of line DropEverything();# if NOTIFY // OK but not required -- Spaces after # NotifyClient();# endif#endif BackToNormal(); }// Bad - indented directives if (lopsided_score) { #if DISASTER_PENDING // Wrong! The "#if" should be at beginning of line DropEverything(); #endif // Wrong! Do not indent "#endif" BackToNormal(); }
public,protected,private前面有一个空格:
<!-- lang: cpp -->class MyClass : public OtherClass { public: // Note the 1 space indent! MyClass(); // Regular 2 space indent. explicit MyClass(int var); ~MyClass() {} void SomeFunction(); void SomeFunctionThatDoesNothing() { } void set_some_var(int var) { some_var_ = var; } int some_var() const { return some_var_; } private: bool SomeInternalFunction(); int some_var_; int some_other_var_;};
注意:
初始化列表可以在一行内,也可以折行,前面缩进4个字符:
<!-- lang: cpp -->// When it all fits on one line:MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) {}or// When it requires multiple lines, indent 4 spaces, putting the colon on// the first initializer line:MyClass::MyClass(int var) : some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up ... DoSomething(); ...}
命名空间内的内容不缩进。
<!-- lang: cpp -->namespace {void foo() { // Correct. No extra indentation within namespace. ...}} // namespaceDo not indent within a namespace:namespace { // Wrong. Indented when it should not be. void foo() { ... }} // namespace
水平空格依赖于位置,不要行末添加空格。
<!-- lang: cpp -->void f(bool b) { // Open braces should always have a space before them. ...int i = 0; // Semicolons usually have no space before them.// Spaces inside braces for braced-init-list are optional. If you use them,// put them on both sides!int x[] = { 0 };int x[] = {0};// Spaces around the colon in inheritance and initializer lists.class Foo : public Bar { public: // For inline function implementations, put spaces between the braces // and the implementation itself. Foo(int b) : Bar(), baz_(b) {} // No spaces inside empty braces. void Reset() { baz_ = 0; } // Spaces separating braces from implementation. ...
循环和条件,
<!-- lang: cpp -->if (b) { // Space after the keyword in conditions and loops.} else { // Spaces around else.}while (test) {} // There is usually no space inside parentheses.switch (i) {for (int i = 0; i < 5; ++i) {// Loops and conditions may have spaces inside parentheses, but this// is rare. Be consistent.switch ( i ) {if ( test ) {for ( int i = 0; i < 5; ++i ) {// For loops always have a space after the semicolon. They may have a space// before the semicolon, but this is rare.for ( ; i < 5 ; ++i) { ...// Range-based for loops always have a space before and after the colon.for (auto x : counts) { ...}switch (i) { case 1: // No space before colon in a switch case. ... case 2: break; // Use a space after a colon if there's code after it.
操作符,
<!-- lang: cpp -->// Assignment operators always have spaces around them.x = 0;// Other binary operators usually have spaces around them, but it's// OK to remove spaces around factors. Parentheses should have no// internal padding.v = w * x + y / z;v = w*x + y/z;v = w * (x + z);// No spaces separating unary operators and their arguments.x = -5;++x;if (x && !y) ...
模板和类型转换,
<!-- lang: cpp -->// No spaces inside the angle brackets (< and >), before// <, or between >( in a castvector<string> x;y = static_cast<char*>(x);// Spaces between type and pointer are OK, but be consistent.vector<char *> x;set<list<string>> x; // Permitted in C++11 code.set<list<string> > x; // C++03 required a space in > >.// You may optionally use symmetric spacing in < <.set< list<string> > x;
使纵向空格最小化。
当你没必要时,不要留很多空行。特别的,在函数间不要留超过一行或两行的空白。
函数开始不要有空行,函数结束也不要有空行。
基本原则:一个屏幕上有越多的代码,就越容易理解程序的控制流程。当然,可读性也很重要。
你可能会在不符合本规则的代码上工作,为了保持一致性,你不应该生搬硬套这个规则。
上面的一些规则在Windows上不适用:
符合常识以及保持一致性。
在编辑代码之前,花几分钟看看当前代码的编码风格。并与之保持一致的风格。
]]>