本文提到的绝大多数错误,都是作者历经一番艰辛才得以发现,要么是因为自己犯过,要么是在别人的工作中见过。
本文并非意图对程序员划分等级 ,只是适合某些程序员阅读,他们相信自己有能力判断一件事情在什么情况下是不良习惯的迹象,在什么情况下则是特殊环境导致的结果。
写这个系列是为了迫使作者自省,而发布出来,是因为觉得大家也可能会从中找到感兴趣的地方。
对代码进行推理意味着能跟随代码的执行路径(“在脑子里运行程序”),同时清楚地知道代码执行的目标。
程序猿可以通过实践来克服这个缺点,如果 IDE 自带的调试器能单步调试,就把它作为助手使用。比如说在 Visual Studio 里,这就意味着要在问题区域的起始处打上断点,然后按下‘ F11 ’单步调试,查看变量的值(变化前后都要查看),直到你明白了代码正在做什么。如果你的目标环境不具备这种特性,那就找一个拥有这种特性的环境去实践。
这么做的目的是,让你做到不再需要调试器就能在脑子里跟随代码的流程,而且有足够的耐心去思考代码正在对整个程序的状态做什么。这么做的好处就是能够识别出冗余且无用的代码,而且不需要从头执行整个路径就能在当前代码中找出 bug。
面向对象编程( Object Oriented Programming )就是一种语言模型,正如函数式编程( Functional programming )或声明式编程( Declarative programming )一样。它们每一个都和过程式或命令式编程有着显著不同,就像过程式编程明显不同于汇编或基于 GOTO 的编程。此外,虽然有很多语言都跟随同一个主流编程模型(如面向对象的编程),但它们都只介绍自己的改进,例如递推式构造列表( list comprehensions )、泛型( generics )、鸭式分类( duck-typing )等等。
译者:duck-typing 是动态语言的一种程序设计风格,用以实践方法多态。Duck-typing 并不关注对象的实际类型,而是关注其表现。概念提出者 James Whitcomb Riley 这样描述这个风格:当看到一只鸟走起来像鸭子,游起泳来像鸭子,叫起来也像鸭子,那这只鸟就可以看出是鸭子。
如果你的技能不足,是因为别人教得不好或是自己没学好,那编译器自身就是一位备选老师。学习一个新的编程模型,最有效的办法莫过于创建一个新工程,不管都有哪些新的构造方法,强迫自己去使用它们,无论在工程中的使用是否明智。你也需要练习用自己最熟悉且通俗易懂的措辞来解释模型特性,然后递归地创建自己的新词汇表,直到你对模型理解入微。举个例子:
阶段一:“OOP 就是方法的集合”
阶段二:“OOP 里的方法就是函数,它们运行在自带全局变量的小程序中”
阶段三:“全局变量被称为字段,其中有些是私有字段,在小程序外不可见”
阶段四:“拥有私有和公有元素是为了隐藏实现细节,暴露干净整洁的接口,这就叫封装”
阶段五:“封装意味着实现细节不会破坏业务逻辑”
对所有编程语言来说阶段五看起来都一样,因为所有语言在阶段五都试图让程序猿能表达出程序的 意图 ,而不需要将其隐藏在 如何 实现的细节之中。拿函数式编程再举个例子:
如今,现代语言和框架都带有非常了不起的内置命令和特性,一些主要的框架(像 Java 、 . Net 、 Cocoa)由于本身结构庞大,任何一个程序猿(甚至是一个很优秀的程序猿)都要花费好几年时间去学习。但是,一个优秀的程序猿在自己开始构造所需函数之前,会先搜索有没有满足需求的内置函数。而杰出的程序猿们则能够分解并识别出任务中的抽象问题,接着在实际开始设计程序之前,去搜索适用的现有框架、模式、模型和语言。
如果在应该掌握新平台很久以后,这些特征还继续出现,那它们就暗示着存在问题。
译者:“ comfort zone ”就是使人感到安全、舒服或在其掌控之下的形式或状态。
也会偶尔复制代码,复制的频率和框架大小成比例,因此,按自己的程度来判断吧。手写链表的人 也许知道自己正在做什么 ,但手写 StrCpy() 的人可能就不知道了。
一个程序猿如果不放慢速度,就不可能学到这类知识。而且很有可能,这个人一直都在火急火燎地用任何需要的手段让每个函数都工作起来。他需要在手边放一本平台的技术参考手册,并且能够花最小的代价浏览它,这就是说要么在桌上的键盘右边放一本打印稿,要么还有一个屏专门用来打开浏览器。为了开始培养这种习惯,他应该重构旧代码,目标是减少十分之一以上的指令数量。
如果你不能理解指针,那你能写的程序类型就非常有限,因为指针的概念创造出了很多复杂的数据结构和有效的 APIs。托管类语言使用引用来代替指针,两者很像,但引用增加了自动解引用功能并禁止指针运算,从而消除特定类型的 bug。无论如何,它们还是非常相似,不能掌握这个概念就会导致数据结构的设计很差劲,并且出现一些由于不理解方法调用中值传递和引用传递的区别而导致的问题。
“我有一个叫 Joe 的朋友待在宾馆的某个房间里,而我不知道他的房间号。但我知道他的熟人 Frank 待在哪个房间”,因此我跑去敲门问他‘Joe 在哪个房间?’,Frank 表示他也不知道,但他知道 Joe 的同事 Theodore 在哪个房间,并给了我 Theodore 的房间号。因此我又跑到Theodore的房间问 Joe 在哪,Theodore 告诉我 Joe 在414房间。实际上,Joe 就是在那个房间。”
对于指针,可以用很多种不同的隐喻来描述,而数据结构则可以描述成多种比喻。上面是对链表的简单类比,而且任何人都能发明自己的版本,即使他们不是程序猿。提到指针大家都能理解,因此,你的描述不会比现有的描述还更全面。当程序猿试图想象计算机的内存里正在发生什么,并把这个想象和他们对普通变量的理解融合时,虽然这两者很相似,但这个时候就会无法理解。也许将代码解释成一个简单的故事有利于推理当前的状况,直到发现其中的区别,直到程序猿可以像面对标量值和数组一样直观地想象指针和数据结构。
递归的思想很容易理解,但程序猿们经常在自己脑子里想象一次递归操作的结果时遇到困难,或想不通一个简单函数是怎么计算出复杂结果的。这些不解使得要设计一个递归函数变得难上加难,因为当你要对初始条件或递归调用的参数进行测试时,你想象不出“当前走到哪一步了”。
先体会一下,准备好迎接某种堆栈溢出吧。首先,在代码里只写一个初始条件检测并只调用一次递归,递归中使用同一个被传递的未修改参数。即使你觉得写得不够好也要停下来,无论如何,让代码运行一下。它抛出了一个堆栈溢出的异常,那么现在返回去继续写,在递归调用中传递参数的已修改拷贝。产生了更多的堆栈溢出错误?输出过度?那就接着反复修改代码再运行,从修改初始条件测试转向修改递归调用,直到你开始凭直觉就知道函数怎么转换它的输入参数。忍住冲动,使用的初始条件测试或递归调用不要超过一次,除非你真的 知道自己在做什么 。
你的目标是勇于进行递归调用,即使在这条想象中的递归路径上,你没有完全搞清楚“自己在哪里”。那么,等你需要为一个真正的项目去写一个函数时,你会从写单元测试开始,并且运用上面提到的相同技术来一步步推进。
别人是按代码行数付钱给你吗?这些旧习惯是不是你从一个拥有弱类型体系的语言中延续下来的?如果两种都不是,那这种情况就类似于“无法推理代码”,但是似乎不是推理能力受损,而是无法信任和适应编程语言。有些特征更像是经不起逻辑分析的“comfort code”,但程序猿非要强迫自己这么写。唯一的补救措施就是,多花时间熟悉编程语言。