目录
这篇文章的作者是 Avram Joel Spolsky (汉文:周思博),是 Fog Creek Software 的 CEO,其中一个最出名的产品有 Stack Overflow 。
使错误的代码更容易看出错误,文章以作者面包店工作清洁机器为例子,讲述了内行人和外行人的不同见解,来思考代码编码的时候的规范以及一些不明显的,容易误导的错误的解决方案。之后在一系列例子中引出了代码约定的作用以及风靡一时的匈牙利表示法,介绍了这个表示法的来源,发展,以及最后为什么会消失。总而言之,最后的结论也就是在编写代码或在查阅代码的时候,能在眼睛所到处能获得更多的信息,而不仅仅只是运行之后才知道,虽然匈牙利表示法在现在强类型检查和一些智能 IDE 的代码提示下变得有些不必要了,不过其中的思想也值得学习。
原文
术语表
原文 | 译文 | 注释 |
---|---|---|
dough rounder | 面团整型机 | 就是搅拌面团切成球的机器 |
Oddly-Capitalized variables | 奇怪的大写变量 | 就是变量命名不规范 |
cleanliness、clean | 代码整洁 | 原本单词意思是干净的意思,在代码层面上更趋向整洁的意思,参考 Clean code (代码整洁之道) |
code-robustness | 代码鲁棒性 | 就是健壮性的意思 |
Hungarian Notation | 匈牙利表示法 | 由 Microsoft 公司的程序设计人员 Charles Simonyi 首创的编码风格 |
Cross Site Scripting | 跨站点脚本漏洞 | 一种网站攻击技术 |
a.k.a | 即 | a.k.a 是 Also Known As 的缩写 |
pseudocode | 伪代码 | 又称为虚拟代码,是高层次描述算法的一种方法 |
bug | Bug | 特指代码异常,错误,不做翻译 |
Intellisense | 语法提示 | 直译是智能感知,文中特指代码提示 |
翻译
早在1983年9月,我就开始了我的第一份工作,是在以色列的一家大型面包工厂 Oranim 工作,这家工厂有六个巨大的烤箱中,并且每晚需要制作大约10万个面包,这些烤箱的大小与航空母舰一样大。
我第一次走进面包店的时候,简直不敢相信这是多么糟糕。烤箱的两侧发黄,机器生锈,到处都是油脂。
我问道:“这总是这么乱吗?”
“什么?你在说什么?“经理说。 “我们刚刚完成清洁工作。这是几个星期清理的最干净的时候。“
好家伙。
在这几个月期间,每天早上我都在清洗面包店,这时候才明白到清洁的真正含义。在面包店里,清洁就意味着机器上没有面团,意味着垃圾中没有发酵的面团,意味着地板上没有面团。
干净并不意味着烤箱上的油漆又白又好。油漆烤箱是你每十年做的事,而不是每天。清洁并不意味着没有油脂。事实上,有很多机器都需要定期润滑或上油,而薄薄的一层干净的机油通常是刚刚清洗过的机器的标志。
面包店清洁这整个概念是你必须学习的东西。而对于局外人来说,是不可能走进来判断这个地方是否干净的。一个局外人永远不会想到去看面团整型机的里面(如右图所示,它是一种将正方形的面团卷成球的机器),看它们是否被刮干净了。一个局外人会关注于那些老烤箱有变色的外表,因为那些外表很大。但面包师并不在乎烤箱外面的油漆是否开始变黄了。面包的味道仍然很好。
在面包店待了两个月后,你就学会了如何 “看清” 干净。
代码也是一样的。
当你从一个初级程序员开始,或者当你尝试用一种的新语言阅读代码时,它们看起来同样难以理解。在你理解编程语言本身之前,你甚至看不到明显的语法错误。
在学习的第一阶段,你开始认识到我们通常称之为 “编码风格” 的事物。因此,你开始注意到代码不符合缩进标准以及一些奇怪的大写变量。
关于这点,你通常会说:“Blistering Barnacles,我们必须在这里得到一致的编码约定!”之后在第二天,你花了一天的时间为你的团队写一些编码约定,并且在接下来的六天时间争论 One True Brace 风格和在接下来的三个星期重写旧代码以符合这个 One True Brace 风格,直到有个经理抓着你大喊大叫,说你浪费时间在一些无法赚钱的事情上。但是当你在重新审视这件事的时候,你并不觉得格式化代码是一件坏事情,因此,你有大约一半的代码是 One True Brace 风格,之后你很快就会忘记这些事情,并且开始沉迷于其他与赚钱无关的事,比如用另一种字符串类替换一种字符串类。
当你在一个特定的环境中变得更加熟练地编写代码时,你就开始学会看到其他事物,虽然根据编码约定,可能是完全合法和完全正常的事情,但这会让你担心。
举个例子,在 C 语言中:
char* dest, src;
这是合法的代码。他可能符合你的编码习惯,甚至达到你的预期,但是当你在 C 语言编码方面有足够的经验的时候,你会发现该代码将 dest 申明为 char
指针类型,而将 src 申明为 char
类型,即使这样是你需要的,当然也可能不是。这段代码看起来糟糕透了。
还有更加微妙的:
if (i != 0) foo(i);
在这种情况下,这个代码百分百正确。他符合大部分的代码规范,并且也没有任何错误,但是其中的 if
条件语句后面的单个语句没有用大括号括起来,这会令你感到不安,因为你会在想,天呀,有人可能会在那边插入另外一行代码。
if (i != 0) bar(i); foo(i);
忘了加上大括号,从而意外地使 foo(i)
成为无条件的!所以,当你看到不在大括号中的代码块的时候,你会感觉到少许的不舒服,这会让你感到不安。
好吧,到目前为止,作为一名程序员,我已经提到了三个层次的成就:
- 你不清楚代码哪里不整洁。
- 你对代码整洁只有一个肤浅的概念,主要是在符合编码规范的级别上。
- 你开始嗅到表面下一些细微的不整洁的地方,它们就足够让你抓到并修复代码了。
不过,还有一个更高的层次,这就是我真正想说的:
- 你故意设计一种编写代码的方式使得一些不整洁的的地方能更容易被发现,进而增加代码的准确性。
这才是真正的艺术:通过逐字逐句的编写,让错误在屏幕上凸显出现,使得代码更加健壮。
所以现在,我给你介绍一个小例子,之后我在和你展示一个可以用来创造使得代码具有鲁棒性的一般规则,可能最后有人会对这种匈牙利表示法的进行辩护。那个表示方法不会让人头晕,并且在某些情况下是对异常状态的一种警醒,虽然在大多数情况下可能不是你发现自己的那种情况。
但是,如果你十分确信匈牙利符号是一件坏事,就如自巧克力奶昔以来是最好的发明一样,你甚至不想听到任何其他的意见,那么,请继续前往 Rory 家里并读一读这部优秀的漫画吧,无论如何,你可能不会错过这么多。而实际上,在一分钟之内我就会给出实际的代码样例,甚至在他们有机会让你生气之前就可能让你睡着。是的,我想象中的计划是在你几乎快要睡着的时候,偷偷灌输匈牙利表示法就是好的,代码例子就是不好的这种观点,而不是真正的吵架。
一个例子
好的,在这个例子上,让我们假装你正在构建某种基于 Web 的应用程序,因为这些天这个似乎与孩子们一样风靡一时。
现在,有一个称为跨站点脚本漏洞的安全漏洞,也就是 XSS。我不会在这里详细介绍,但是你必须知道的是,当你在构建 Web 应用程序时,必须小心,不要重复用户输入表单中的任何字符串。例如,如果你有一个网页上写着 “你的名字是什么?”,之后带有一个编辑框,然后提交那个页面就会带你到另一个页面,上面写着 “你好,Elmer!”(假设用户的名字是 Elmer )。那么,这就是一个安全漏洞,因为用户可以输入各种奇怪的 HTML 和 JavaScript 而不是 “Elmer” ,他们输入的那些奇怪的 JavaScript 可以做很多麻烦的事情,并且现在,那些麻烦的东西似乎来自于你,例如他们可以阅读你放在那里的 cookies 并将它们转发给邪恶博士邪恶的网站。
我们把它放在伪代码中。设想:
s = Request("name")
从 HTML 表单中读取输入(POST参数)。如果你写过这段代码:
Write "Hello, " & Request("name")
你的网站已经做好了受到 XSS 攻击的准备,而这就是全部代码。
所以你必须在将其复制回 HTML 之前对其进行编码的工作。对它进行编码意味着将 "
替换成 "
,将 >
替换成 >
等等,所以就变成:
Write "Hello, " & Encode(Request("name"))
这样就安全很多了。
所以,来自用户输入的字符串是不安全的,如果没有编码的话,就不输出任何不安全的字符串。
让我们尝试提出一个编码约定,确保如果你犯了错,就会使得代码看起来像是错的。如果是一个错误的代码,至少看起来不对的话,那么它就有可能被那些从事该代码或审查该代码的人发现。
可能的解决方案 #1
一种解决方案就是立即编码所有字符串,从用户进入的那一刻:
s = Encode(Request("name"))
所以我们的惯例也正好说明了这一点:如果你看到 Request 没有被 Encode 包围 ,那么这代码必定是错误的。
你开始训练你的眼睛寻找裸露的 Requests,因为它们违反了惯例。
这很有效,因为只要你遵守了这个约定,你将永远不会遇到 XSS 错误,但是这不一定是一个好的框架。例如,你可能希望将用户输入的字符串存储到某个地方的数据库中,因此对于那些将要存储到数据库的用户输入的字符串进行 HTML 编码是没有意义的,因为那些存储的数据可能会用在不是 HTML 的页面中,例如信用卡的处理应用程序中,如果进行 HTML 编码估计会很困惑。大多数 Web 应用程序的开发原则是所有内部字符串在发送到 HTML 页面前的最后一刻才进行编码,这可能才是正确的架构。
我们确实需要能够以不安全的格式保留一段时间。
好吧,我再试一次。
可能的解决方案 #2
如果我们制定一个编码规定,当我们需要执行写操作的时候就调用它怎样?
s = Request("name") // much later: Write Encode(s)
现在,无论何时我们只要见到裸露的 Write 没有进行编码操作 Encode 的时候,我们就可以认为有什么地方不对劲。
额,这感觉不太奏效,有时候你的代码中只有很少量的 HTML ,你无法对它们进行编码。
If mode = "linebreak" Then prefix = "<br>" // much later: Write prefix
根据我们的惯例,这看起来是错误的,因为我们要求在执行写操作的时候对字符串进行编码:
Write Encode(prefix)
当时现在,这个 <br>
标签,是可以支持换行的操作的,编码之后就变成了 <br>
并且对用户所显示的文字就为 < b r >
,这样就不对了。
因此,有些字符串在你读的时候是不能进行编码的,并且有些时候在写操作的时候也不能编码,所以这些提议都不起作用。如果没有惯例,我们仍然冒着你这样做的风险:
s = Request("name") ...pages later... name = s ...pages later... recordset("name") = name // store name in db in a column "name" ...days later... theName = recordset("name") ...pages or even months later... Write theName
我们还记得需要编码字符串的操作吗?这里没有任何一个你能发现有 bug 的地方,你甚至察觉不出来。如果你有很多像这样的代码,那么就需要大量的侦查工作来追踪每个字符串的来源,以确保它已被编码。
真正的解决方案
所以,让我给你介绍一个真正有效的编码约定。我们只有一条规则:
对于来自用户输入的所有字符串,必须存储在名称以前缀 “us” 开头的变量(或数据库列)中(是 Unsafe String 的意思)。对于所有已编码的 HTML 或来自已知安全位置的字符串必须存储在名称以前缀 “s” 开头的变量中(表示 Safe )。
让我重写相同的代码,只更改变量名称来匹配我们的新规则。
us = Request("name") ...pages later... usName = us ...pages later... recordset("usName") = usName ...days later... sName = Encode(recordset("usName")) ...pages or even months later... Write sName
我希望你能注意到这个新约定的事情是:现在,如果你犯了使用不安全的字符串这样的错误,在符合编码约定的前提下,你总是可以在一行代码中就可以看到它:
s = Request("name")
这种是先验错误,因为你看到 Request 的结果被分配给名称以 s 开头的变量,这违反了规则。因此,Request 的结果总是不安全,所以必须将其分配给名称以 “us” 开头的变量中。
us = Request("name")
这就对了。
usName = us
这种也是对的。
sName = us
这个就绝对错了。
sName = Encode(us)
这种就是对的。
Write usName
这样是错的。
Write sName
这样写就是对的,也可以写成这样:
Write Encode(usName)
这样的话,每行代码都可以自行检查,如果每行代码都是正确,则代表整个代码都是正确的。
最终,使用这种编码约定的话,你的眼睛就要去学会去看 Write usXXX 这种形式的代码,要明白这种写法是错的,并且也要能立马知道如何去修复它。我知道,一开始去寻找错误代码有些困难,但是如果这样坚持三个星期的话,你的眼睛就会适应的,就像已经学会看一个巨大的面包厂的面包店工人,然后立马说:“天啊,没有人打扫圆桶里面的吗!怎么还有这么一大块东西在里面?”( 这里是这句话的大致翻译,原文用的是网络俚语,单词和字母打乱了部分顺序,这里贴出原文:jay-zuss, nobody cleaned insahd rounduh fo-ah! What the hayl kine a opparashun y’awls runnin’ heey-uh? )
事实上,我们可以稍微扩展一下这个规则,并将 Request 和 Encodefunctions 函数重命名(或包装)为 UsRequest 和 SEncode…换句话来说,返回不安全字符串或安全字符串的函数将以 Us 和 S 开头,就像变量一样。现在看代码:
us = UsRequest("name") usName = us recordset("usName") = usName sName = SEncode(recordset("usName")) Write sName
看到我怎么做了吗?现在,你可以看到等号的两侧都是以相同的前缀开头,以此来检查错误。
us = UsRequest("name") // ok, both sides start with US s = UsRequest("name") // bug usName = us // ok sName = us // certainly wrong. sName = SEncode(us) // certainly correct.
哎呀,其实还可以更进一步,重命名 Write 为 WriteS 并且命名 SEncode 为 SFromUs :
us = UsRequest("name") usName = us recordset("usName") = usName sName = SFromUs(recordset("usName")) WriteS sName
这使得错误看起来更显著。你的眼睛将学会 “看到” 这种有异味的代码,这将有助于你通过正常的写代码和读代码的过程中找到模糊的安全漏洞。
使错误的代码看起来是错误的,这很好,但它不一定是每个安全问题的最佳解决方案。它不会捕获所有可能的 Bug 或错误,因为你可能并不会查看每一行代码。但是,这肯定比什么都没有要好很多,我宁愿有一个编码约定,使得其中错误的代码,至少看起来是错的。每次,程序员的眼睛扫过这一行行代码时,你都会立即获得增量优势,该特定的 Bug 都会被检查并加以预防。
一个通用规则
让错误的代码看起来是错误的,这取决于把正确的东西放在屏幕上的同一个地方。当我在查看一个字符串的时候,为了能写出正确的代码,所以无论在任何地方,只要我看到这字符串,就需要知道这到底是安全的还是不安全的。我不希望这些信息出现在另一个文件或需要滚动到的另一个页面上。我必须能够在那里看到它,这意味着一个变量命名的约定。
有很多其他的例子,你可以通过移动彼此旁边的东西来改进代码。大多数编码约定包括以下规则:
- 保持函数功能简短。
- 将变量的声明尽可能靠近你需要使用到它们的位置。
- 不要使用宏创建你自己的个人编程语言。
- 不要使用
goto
语句。 - 不要将右大括号放在距离匹配的左大括号超过一个屏幕的地方。
所有这些规则的共同点是,它们都试图在一行代码中或者是物理位置接近的地方,尽可能获取到所需要的相关信息。这提高了你的眼球能够弄清楚所有事情的机会。
总的来说,我不得不承认,我有点害怕一些编程语言的隐藏功能。当你看到这个代码:
i = j * 5;
至少在你熟悉的 C 语言中,就是把变量 j 的值乘上 5 ,将结果保存到变量 i 中。
但是,如果这是你在 C# 中看到的相同的代码段,则你就什么都不知道。了解 C# 中真正发生的情况的唯一方法就是找出 i 和 j 的类型,而这些类型可能完全声明在其他地方。这是因为 j 可能是操作符 *
重载的类型,并且当你尝试进行乘法运算的时候,可能会出现及其糟糕的事情。这其中 i 也可能是操作符 =
的重载类型,并且可能会出现类型不兼容而导致自动类型强制函数最终可能会被调用。找到这个问题的唯一方法不仅仅是检查变量的类型,而是找到实现该类型的代码,如果某处有继承话老天会帮你,因此,现在你必须自己去一路沿着类的层次结构向上走,寻找代码的真正位置。但是,如果代码中到处都是多态,那你就真的有麻烦,因为你没有足够的证据去判断 i 和 j 会被申明为什么类型,你必须得在当前就要确定这变量的类型,这意味着需要涉及检查判断大量的代码,而且由于这中断的问题,你永远无法真正确定是否到处都找过。
当你在 C# 中看到 i=j*5
这个代码,并且你真的只有你一个人,朋友,在我看来,仅仅通过查看代码就会降低了发现其中可能出现问题的能力。
当然,这一切都无关紧要。当你像做例如重写运算符 *
这样的聪明学生做的事情时,这是为了帮助你提供一个很好的防水抽象。天啊!j 是 Unicode 字符串类型,将 Unicode 字符串乘以整数,这显然是将繁体中文转换为简体中文的一个很好的抽象,对吧?
当然,问题在于这不是防水抽象。我已经在《泄漏抽象定律》中广泛谈论过这个问题,所以我不会在这里重复。
Scott Meyers 已经完成了一个完整的职业生涯,向你展示了他们所有的失败方式而吸引人,至少在 C# 中。(顺便说一下,Scott 的第三版《Effective C++》刚刚出版,它完全重写了,今天我要拿到你的拷贝版本!)
好吧。
我快失去方向了。我最好现在就总结一下:
查找使错误代码看起来错误的一种编码规定。在代码中,将正确的信息放在屏幕上相同的位置,这样你就可以看到某些类型的问题,并立刻解决它们。
我是匈牙利人
所以现在我们回到臭名昭着的匈牙利标记法。
匈牙利标记法是由微软程序员 Charles Simonyi 发明的。Simonyi 在微软工作的一个主要项目是 Word,事实上,他领导该项目创造了世界上第一个所见即所得的文字处理器,在 Xerox Parc 上称为 Bravo。
在 WYSIWYG 文字处理中,你有可滚动的窗口,因此每个坐标都必须被解释为相对于窗口或相对于页面,这会产生很大的不同,并保持它们笔直是非常重要的。
我推测,这是 Simonyi 开始使用匈牙利表示法的众多原因之一。它看起来像匈牙利语,并且 Simonyi 也来自匈牙利,因此得名。在 Simonyi 的匈牙利表示法的版本中,每个变量都以小写标签为前缀,表示变量包含的内容(indicated the kind of thing that the variable contained.)。
我在那里故意使用 “类型(kind)” 这个词,因为 Simonyi 在他的论文中,他错误地使用了这个单词类型,而之后的几代程序员都误解了他的意思。
如果你仔细阅读 Simonyi 的论文,他所得到的与我在上面的例子中使用的命名约定是相同,我们都一致认为 us 的意思是 “不安全的字符串” ,而 s 意味着 “安全字符串” 。他们都是 string 类型的,所以如果你反过来赋值的话(s 赋值为不安全,us 赋值为安全),编译器也没办法帮你,并且代码提示也不会提示任何东西。但是它们在语义上是不同的,所以它们需要以不同的方式进行解释并进行不同的处理,如果你将它们两反过来赋值的话,要不你会有个运行时错误,要不就是某个转换函数就会执行,当然,你够幸运的话。
Simonyi 最初的匈牙利表示法的概念在微软内部被称为 Apps Hungarian,因为它被用于应用程序部门,即 Word 和 Excel 。在 Excel 的源代码中,你会看到很多 rw 和 col,所以当你看到它们时,你就知道它们引用了行和列。当然,它们都是整数,在它们之间分配赋值是没有意义的。在 Word 中,据我所知,你会看到很多的 xl 和 xw ,其中 xl 表示 “相对于布局的水平坐标” ,而 xw 表示 “相对于窗口的水平坐标” 。它们都是 int 类型。不能互换。在这两个应用程序中,你都会看到很多的 cb 表示 “字节数”。是的,这个也是一个 int 类型,并且你只要通过查看这个变量的变量名就可以知道,它是一个表示缓冲区的大小的字节数(count of bytes)。如果你看到 xl = cb 的话,好吧,你可以吹嘘 Bad Code Whistle 了,这显然是错误的代码,因为即使 xl 和 cb 都是整数,但是将像素的水平偏移设置为字节数也是够疯狂的。
在 Apps Hungarian 中的前缀通常用于函数表示,以及变量。所以,说实话,我从来没有见过 Word 的源代码,但是我敢和你打赌一个甜甜圈,那里面一定有个叫 YlFromYw 的功能,可以从垂直窗口坐标转换为垂直布局坐标。Apps Hungarian 需要一个叫 TypeFromType 的符号,而不是传统的 TypeToType ,这样,每个函数名都可以以返回的类型开头,就像我在前面重命名 Encode SFromUs 的样例中所做的那样。事实上,在适当的 Apps Hungarian 中,编码函数必须命名为 SFromUs 。Apps Hungarian 并不能让你选择如何命名这个函数。这是一件好事,因为你需要记住的东西少了一些,而且你不必怀疑 Encode 这个词所指的是什么样的编码:你有更精确的东西。
Apps Hungarian 非常有价值,特别是在 C 编程时代中,当时的编译器并没有提供一些非常有用的类型检查系统。
但后来出现了一些错误。
黑暗的一面接管了匈牙利表示法。
似乎没有人知道为什么或什么原因,似乎是 Windows 团队的文档编写者在无意中发明了后来被称为 Systems Hungarian 的东西。
有人在某处阅读过 Simonyi 的论文,在那里他使用了 “type” 这个词,并认为他的类型就像类一样,类似于类型系统,就像编译器所做的类型检查一样。但他没有,他非常仔细地解释了“类型”一词的含义,最后却没有用。损坏已经造成了。
Apps Hungarian 有非常有用,有十分有意义的前缀,如 “ix” 表示数组的索引,“c” 表示计数,“d” 表示两个数字之间的差异(例如 “dx” 表示“宽度”),等等。
Systems Hungarian 也有很多有用的前缀,如 “l” 表示长,“ul” 表示 “unsigned long”,“dw” 表示双字,实际上,呃,是无符号长整数。在 Systems Hungarian 中,前缀告诉你的唯一内容是变量的实际数据类型。
这是对 Simonyi 的意图和实践的一个微妙但完全的误解,它只是告诉你,如果你写出错综复杂的学术散文,没有人会理解它,你的想法会被误解,然后这被误解的想法就遭到嘲笑,即使他们并不是你的想法。因此,在 Systems Hungarian 中,你有很多 dwFoo 意思是 “双字foo”,而且还有 it ,这个变量是一个双字的事实告诉你几乎没有任何用处。所以难怪人们反对 Systems Hungarian。
Systems Hungarian 被广泛传播,它是整个 Windows 编程文档的标准。它就像《 Charles Petzold 的 Windows 编程 》这本用于学习 Windows 编程的圣经一样广泛传播,迅速成为匈牙利表示法的主导形式,即使在微软内部,除了 word 和 excel 团队之外,很少有程序员知道它们犯了什么错误。
然后发生了大叛乱。最终,那些一开始就不理解匈牙利表示法的程序员注意到,他们使用的被误解的子集是 Pretty Dang Annoying 和 Well-Nigh Useless,他们对此表示反感。现在, Systems Hungarian 仍然有一些很好的特性,可以帮助你看到 Bug 。至少,如果你使用 Systems Hungarian 的话,你就可以在你使用它的位置知道变量的类型,但它并不像 Apps Hungarian 那样有价值。
大叛乱在第一版的 .NET 发布时达到顶峰。微软最终开始告诉人们:“不推荐使用匈牙利表示法”。这多欢乐。我想他们都懒得说为什么。他们只是浏览了文档的命名指南部分,然后在每个条目中写下 “不要使用匈牙利表示法”。匈牙利表示法在这一点上非常不受欢迎,没有人真正抱怨过,除了 Excel 和 Word 外,世界上的每个人都松了一口气,不再需要使用一个尴尬的命名约定,他们认为,在强类型检查和代码提示的日子里,这种命名约定是不必要的。
但是,Apps Hungarian 仍然有很大的价值,因为它增加了代码的搭配,这使得代码更易于读,写,调试和维护,最重要的是,它使错误的代码看起来是错误的。
在我们离开之前,还有一件我承诺过要做的事情,那就是再一次抨击异常(exceptions,值得是代码中的异常处理)。上次我那样做的时候我惹了很多麻烦。在 Joel on Software 主页上的一个即兴评论中,我写道我不喜欢异常,因为它们实际上是一个看不见的 goto,我推断,它甚至比你能看到的 goto 还要糟糕。当然,数百万人愤怒的回击了我。然而,世界上唯一一个站出来为我辩护的人当然是 Raymond Chen,顺便说一下,他是世界上最好的程序员,所以这不得不说些什么,对吧?
在本文的上下文中,有一个例外。只要有东西可以看,你的眼睛就需要学会看错误的东西,这可以防止 Bug 。为了使代码真正,非常健壮,在你对代码进行审查时,你需要搭配代码约定来进行。换句话说,关于代码正在做什么这样的的信息越多出现在眼前,就越有助于发现错误。当你的代码显示:
dosomething(); cleanup();
你的眼睛就会告诉你,这是怎么回事?我们总是清醒的!但 dosomething 可能会抛出异常,而这可能就意味着 cleanup 不会被调用。这很容易修复,使用 finally 或 whatnot,但这不是我的意思。我的观点是,知道 cleanup 肯定被调用的唯一方法是调查 dosomething 的整个调用树,看看里面、任何地方是否有可以引发异常的东西,这是可以的,还有一些类似检查异常的东西可以让它不那么痛苦,但真正的问题是,异常消除了搭配。你必须寻找其他地方来回答一个问题:代码是否做了正确的事情,所以你不能利用你的眼睛内置的能力来学习看错误的代码,因为没有什么可以看的。
现在,当我写一个小小的脚本收集大量数据并每天打印一次时就会发现,哎呀,异常(exceptions)很是很不错的。我最喜欢的就是忽略所有可能发生的错误,把整个该死的程序打包成一个大的 try/catch 中,如果出了什么问题就给我发邮件。对于写快速和脏的代码(quick-and-dirty code),脚本以及既不是关键任务也不是维持生命的这些代码时,异常是可以接受的。但是,如果你正在编写一个操作系统或一个核电站,或者是一个控制心脏直视手术中使用的高速圆锯的软件,则异常非常危险。
我知道人们会认为我是一个蹩脚的程序员,因为如果只是我成天这么在意异常,那么就无法正确理解异常并且无法理解他们对改善生活的所做的事,但是,还是太糟糕了。编写真正可靠的代码的方法是尝试使用考虑到典型人类弱点的简单工具,而不是具有隐藏的副作用和假定程序员不会出错的泄漏抽象的复杂工具。
更多阅读
如果你仍然对异常这种情况感兴趣,请阅读Raymond Chen的文章 Cleaner, more elegant, and harder to recognize。 “看到基于异常的错误代码和基于异常的错误代码之间的差异非常困难……异常太难了,而且我不够聪明,无法处理它们。”
Raymond 对宏死亡的咆哮, A rant against flow control macros,是另一种情况,即无法在同一个地方获取信息使代码无法维护。 “当你看到使用[宏]的代码时,你必须深入研究头文件以找出它们的作用。”
关于匈牙利表示法的历史背景,请从 Simonyi 的原著《Hungarian Notation》开始。Doug Klound在一篇更清晰的文章中向 Excel 团队介绍了这一点。要了解更多关于匈牙利的故事以及它是如何被文档作者破坏的,请阅读 Larry Osterman 的文章,特别是 Scott Ludwig 的评论,或者是 Rick Schaut 文章。
生词
ferment:n. 酵素;发酵;动乱;vi. 发酵;动乱;vt. 使发酵;使动乱;酝酿
outsider:n. 外人;无取胜希望者
inscrutable:adj. 不可理解的;谜一样的
syntactic:adj. 句法的
consistent:adj. 一贯的, 始终如一的;一致的, 符合的
obsess:vt. 时刻困扰;缠住
irrelevant:adj. 不相干的,不相关的
conformance:n. 顺应,一致
subtle:adj. 微妙的; 难以捉摸的; 细微的;狡猾的, 狡诈的;敏感的, 敏锐的, 有辨别力的
deliberately:adv. 慎重地;谨慎地;故意地,蓄意地;从容不迫地,不慌不忙地
robust:adj. 强健的;健康的;粗野的;粗鲁的
carsick:adj. 晕车的
circumstance:n. 环境, 条件, 情况;境遇, 经济状况
vulnerability:n. 弱点,攻击
pseudocode:伪代码
amiss:adj. 有毛病的,有缺陷的;出差错的,adv. 错误地
polymorphism:n. 多型现象, 多态性
donuts:n. 油炸圈饼;甜甜圈(donut的复数)
jump down sb’s throat:愤怒的回击某人,猛烈回击某人
lame:adj. 瘸的,站不住脚的, 差劲的, 蹩脚的
(文章比较长,有很多翻译不到位的,欢迎指出)
0 条评论