两年前,我和李开复博士等人多次谈论科技公司的兴衰,我们一致认为一个公司的基因常常决定它今后的命运,比如 IBM 很难成为一个微机公司一样。摩托罗拉也是一样,它的基因决定了它在数字移动通信中很难维持它原来在模拟手机上的市场占有率。摩托罗拉并不是没有看出数字手机将来必将代替模拟手机,而是很不情愿看到这件事发生。作为第一代移动通信的最大收益者,摩托罗拉要尽可能地延长模拟手机的生命期,推迟数字手机的普及,因为它总不希望自己掘自己的墓。如果过早地放弃模拟手机,就等于放弃已经开采出来的金矿,而自降身价和诺基亚的公司一同从零开始。尤其在刚开始时,数字手机的语音质量还远不如摩托罗拉砖头大小的大哥大,更使摩托罗拉高估了模拟手机的生命期。和所有大公司一样,在摩托罗拉也是最挣钱的部门嗓门最大,开发数字手机的部门当然不容易盖过正在挣钱的模拟手机部门,因此,摩托罗拉虽然在数字手机研发上并不落后,但是,进展缓慢。一旦各个竞争对手推出各种各样小巧的数字手机时,摩托罗拉才发现自己慢了半拍。
乔布斯和盖茨都意识到了微机及其相关工业将是一个大产业,事实证明这确实是一个万亿美元的大产业。我们在前面已经分析过,计算机工业比任何行业都容易出现垄断公司。乔布斯和盖茨都想做垄断者,但是他们的方式不同。前者是想做原来 IBM 那样的垄断者,从硬件到软件全部垄断,这后来证明是行不通的。而盖茨天才之处在于,它在微机工业刚刚开始的时候,就意识到只要垄断了操作系统,就间接垄断了整个行业,因为操作系统和别的软件不同,是在买计算机时预装好了的,一般用户没有选择权。而其它的软件用户则有选择权。 如果说乔布斯是锋芒毕露,聪明写在脸上;盖茨就是一个平衡木冠军,聪明藏在肚子里。无疑,后者比前者更可怕。
附: 插件开放让第三方实现与官方自己实现并集成的优劣之分参考知乎的一篇文章: Visual Studio Code 有哪些工程方面的亮点。 通过插件来扩展功能的做法已经是司空见惯了,但如何保证插件和原生功能一样优秀呢?历史告诉我们:不能保证。大家可以参考 Eclipse,插件模型可以说是做得非常彻底了,功能层面也是无所不能,但存在几个烦人的问题:不稳定、难用、慢,所以不少用户转投 IntelliJ 的怀抱。可谓成也插件,败也插件。问题的本质在于信息不对称,它导致不同团队写出来的代码,无论是思路还是质量,都不一致。最终,用户得到了一个又乱又卡的产品。所以要让插件在稳定性、速度和体验的层面都做到和原生功能统一,只能是一个美好的愿望。 来看看其他 IDE 是怎么做的,Visual Studio 自己搞定所有功能,并且做到优秀,让别人无事可做,这也成就了其 “宇宙第一 IDE” 的美名;IntelliJ 与之相仿,开箱即用,插件可有可无。这么看起来,自己搞定所有的事情是个好办法,但大家是否知道,Visual Studio 背后有上千人的工程团队,显然,这不是 VS Code 这二十几号人能搞定的。他们选择了让大家来做插件,那怎么解决 Eclipse 所遇到的问题呢? 这里分享一个小知识 ——Eclipse 核心部分的开发者就是早期的 VS Code 团队。嗯,所以他们没有两次踏入同一条河流。与 Eclipse 不同,VS Code 选择了把插件关进盒子里。 这样做首先解决的问题就是稳定性,这个问题对于 VS Code 来说尤为重要。都知道 VS Code 基于 Electron,实质上是个 node.js 环境,单线程,任何代码崩了都是灾难性后果。所以 VS Code 干脆不信任任何人,把插件们放到单独的进程里,任你折腾,主程序妥妥的。 VS Code 团队的这一决策不是没有原因的,正如前面提到的,团队里很多人其实是 Eclipse 的旧部,自然对 Eclipse 的插件模型有深入的思考。Eclipse 的设计目标之一就是把组件化推向极致,所以很多核心功能都是用插件的形式来实现的。遗憾的是,Eclipse 的插件运行在主进程中,任何插件性能不佳或者不稳定,都直接影响到 Eclipse,最终结果是大家抱怨 Eclipse 臃肿、慢、不稳定。VS Code 基于进程做到了物理级别的隔离,成功解决了该问题。实际上进程级别的隔离也带出了另一个话题,那就是界面与业务逻辑的隔离。
智能提示
作为写代码的工具,代码提示已经司空见惯了。但是,就算同样是代码提示,有的代码提示只是简单的代码片段(snippets),而有的却是基于代码语法树分析进行的,甚至于编辑器会学习使用者的习惯,将最常用的提示放在最前面。WebStorm 从始至终一直都是第三种,而 VSCode 最近官方才开发了基于 AI 自动学习的智能提示插件 Visual Studio IntelliCode。
不知你是否曾遇到过,正在编辑着一个文件,突然断电,或者是因为其他什么原因,导致文件内容被清空了。或者是误删了代码之后之前的代码还没提交,又不能撤回那么多次,导致代码重写的经历呢?吾辈就曾经经历过,所以对本地历史记录这个功能相当重视,然而很遗憾,VSCode 依旧需要第三方插件 Local History 才能支持。
排版样式 读者进入网页之后,第一眼看到的绝对不是具体的内容,而是网页的排版大致是什么样子的,这点在读者阅读时能够清晰的感受出来。就像人的外貌,在开口前读者便能藉此看出大概(所谓以貌取人)。即便可能在读者继续阅读内容而扭转形象,但更有可能是读者直接点 X 关闭网页,并且留下了不好的印象。 所以排版真的很重要,下面提供吾辈的几条经验:
我家附近,一辆汽车的保险杠贴着一张粘纸,上面写着 “太麻烦,不如死”(death before inconvenience)。大多数人,在大多数时候,总是选择最省事的做法。如果互联网软件能够击败桌面软件,一定是赢在更方便这一优势上。无论从用户的角度还是从开发者的角度来看都是如此。
是的,现在浏览器已经击败了客户端软件,甚至在原本必须要使用客户端的地方使用 Web 技术进行了入侵(Electron,React Native)。但这并不意味人们知道这件事就会真的对用户更友好,尤其是对于免费的开源程序而言。用户不再是首位要素,动辄要求使用者去看源码,吐槽就会被说 “爱用用不用滚,You can you do?”。你敢相信?但这就是国内开源现状。
exportfunction assign<T, A>(target: T, a: A): T & A exportfunction assign<T, A, B>(target: T, a: A, b: B): T & A & B exportfunction assign<T, A, B, C>(target: T, a: A, b: B, c: C): T & A & B & C exportfunction assign<T, A, B, C, D>( target: T, a: A, b: B, c: C, d: D, ): T & A & B & C & D function assign<T>(target: T, ...objects: any[]): any { objects.forEach(obj => { Object.entries(obj).forEach(([k, v]) => (target[k] = v)) }) return target }
for(j=0; j<array_len; j+ =8) { total += array[j+0 ]; total += array[j+1 ]; total += array[j+2 ]; /* Main body of total += array[j+3]; * loop is unrolled total += array[j+4]; * for greater speed. total += array[j+5]; */ total += array[j+6 ]; total += array[j+7 ]; }
如果不是用绿色标出来,你能注意到这三行代码被注释掉了么?
用连接符隐藏变量
对于下面的定义
1
#define local_var xy_z
可以把 “xy_z” 打散到两行里:
1 2
#define local_var xy\ _z // local_var OK
这样全局搜索 xy_z 的操作在这个文件里就一无所获了。 对于 C 预处理器来说,第一行最后的 “" 表示继续拼接下一行的内容。
文档
任何傻瓜都能说真话,而要把谎编圆则需要相当的智慧。 - Samuel Butler (1835 - 1902)
没错,你的客户会要求向上兼容,那就去做吧。不过一定要确保向下是不兼容的。这样可以阻止客户从新版本回退,再配合一套合理的 bug 修复规则(见上一条),就可以确保每次新版本发布后,客户都会留在新版本。学有余力的话,还可以想办法让旧版本压根无法识别新版本产生的文件。那样的话,老版本系统不但无法读取新文件,甚至会否认这些文件是自己的应用系统产生的!温馨提示:PC 上的 Word 文字处理软件就典型地精于此道。
20. 抵消 Bug
不用费劲去代码里找 bug 的根源。只要在更高级的例程里加入一些抵销它的代码就行了。这是一种很棒的智力测验,类似于玩 3D 棋,而且能让将来的代码维护者忙乎很长时间都想不明白问题到底出在哪里:是产生数据的低层例程,还是莫名其妙改了一堆东西的高层代码。这一招对天生需要多回合执行的编译器也很好用。你可以在较早的回合完全避免修复问题,让较晚的回合变得更加复杂。如果运气好,你永远都不用和编译器前端打交道。学有余力的话,在后端做点手脚,一旦前端产生的是正确的数据,就让后端报错。
functionwarp(obj) { const result = newProxy(obj, { get(_, k) { const v = Reflect.get(_, k) if (v !== undefined) { return v } return'' }, }) return result }
不能直接代理一些需要 this 的对象 这个问题就比较麻烦了,任何需要 this 的对象,代理之后的行为可能会发生变化。例如 Set 对象
1 2
const proxy = newProxy(newSet([]), {}) proxy.add(1) // Method Set.prototype.add called on incompatible receiver [object Object]
是不是很奇怪,解决方案是把所有的 get 操作属性值为 function 的函数都手动绑定 this
1 2 3 4 5 6 7 8 9 10 11
const proxy = newProxy(newSet([]), { get(_, k) { const v = Reflect.get(_, k) // 遇到 Function 都手动绑定一下 this if (v instanceofFunction) { return v.bind(_) } return v }, }) proxy.add(1)