学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,会让人感到痛苦,并随时想找理由放弃。
主动学习和被动学习
人的学习分为「被动学习」和「主动学习」两个层次。
- 被动学习:如听讲、阅读、视听、演示,学习内容的平均留存率为 5%、10%、20% 和 30%。
- 主动学习:如通过讨论、实践、教授给他人,会将原来被动学习的内容留存率从 5% 提升到 50%、75% 和 90%。
学习不是努力读更多的书,盲目追求阅读的速度和数量,这会让人产生低层次的勤奋和成长的感觉,这只是在使蛮力。要思辨,要践行,要总结和归纳,否则,你只是在机械地重复某件事,而不会有质的成长的。
浅度学习和深度学习
只要你注意观察,就会发现,少数的精英人士,他们在训练自己获取知识的能力,他们到源头查看第一手的资料,然后,深度钻研,并通过自己的思考后,生产更好的内容。而绝大部分受众享受轻度学习,消费内容。
可见深度学习十分重要,但应该怎样进行深度学习呢?下面几点是关键。
- 高质量的信息源和第一手的知识。
- 把知识连成地图,将自己的理解反述出来。
- 不断地反思和思辨,与不同年龄段的人讨论。
- 举一反三,并践行之,把知识转换成技能。
换言之,学习有三个步骤。
- 知识采集。信息源是非常重要的,获取信息源头、破解表面信息的内在本质、多方数据印证,是这个步骤的关键。
- 知识缝合。所谓缝合就是把信息组织起来,成为结构体的知识。这里,连接记忆,逻辑推理,知识梳理是很重要的三部分。
- 技能转换。通过举一反三、实践和练习,以及传授教导,把知识转化成自己的技能。这种技能可以让你进入更高的阶层。
学习不仅仅是为了知道,而更是为了思考和理解。在学习的过程中,我们不是为了知道某个事的表面是什么,而是要通过表象去探索其内在的本质和原理。
一旦理解和掌握了这些本质的东西,你就会发现,整个复杂多变的世界在变得越来越简单。你就好像找到了所有问题的最终答案似的,一通百通了。
学习不仅仅是为了开拓眼界,而更是为了找到自己的未知,为了了解自己。开拓眼界的目的就是发现自己的不足和上升空间,从而才能让自己成长。
学习不仅仅是为了成长,而更是为了改变自己。学习是为了改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长。
挑选知识和信息源
信息源要有下面几个特质。
- 应该是第一手资料,不是被别人理解过、消化过的二手资料。尤其对于知识性的东西来说,更是这样。应该是原汁原味的,不应该是被添油加醋的。
- 应该是有佐证、有数据、有引用的,或是有权威人士或大公司生产系统背书的资料。应该是被时间和实践检验过的,或是小心求证过的,不是拍脑袋野路子或是道听途说出来的资料。
- 应该是加入了一些自己的经验和思考,可以引发人深思的,是所谓信息的密集很大的文章。
注重基础和原理
最最关键的是,这些基础知识和原理性的东西和技术,都是经历过长时间的考验的,所以,这些基础技术也有很多人类历史上的智慧结晶,会给你很多启示和帮助。比如:TCP 协议的状态机,可以让你明白,如果你要设计一个异步通信协议,状态机是一件多么重要的事,还有 TCP 拥塞控制中的方式,让你知道,设计一个以响应时间来限流的中件间是什么样的。
当学习算法和数据结构到一定程度的时候,你就会知道,算法不仅对于优化程序很重要,而且,会让你知道,该如何设计数据结构和算法来让程序变得更为健壮和优雅。
有时候,学习就像拉弓蓄力一样,学习基础知识感觉很枯燥很不实用,工作上用不到,然而学习这些知识是为了未来可以学得更快。基础打牢,学什么都快,而学得快就会学得多,学得多,就会思考得多,对比得多,结果是学得更快……这种感觉,对于想速成的人来说,很难体会。
使用知识图
比如,在学习 C++ 的时候,面对《C++ Primer》这种厚得不行的书,我就使用联想记忆法。
我把 C++ 分成三部分。
- 第一部分是 C++ 是用来解决 C 语言的问题的,那么 C 语言有什么问题呢?指针、宏、错误处理、数据拷贝…… C++ 用什么技术来解决这些问题呢?
- 第二部分是 C++ 的面向对象特性:封装、继承、多态。封装,让我想到了构造函数、析构函数等。构造函数让我想到了初始化列表,想到了默认构造函数,想到了拷贝构造函数,想到了 new……多态,让我想到了虚函数,想到了 RTTI,RTTI 让我想到了 dynamic_cast 和 typeid 等。
- 第三部分是 C++ 的泛型编程。我想到了 template,想到了操作符重载,想到了函数对象,想到 STL,想到数据容器,想到了 iterator,想到了通用算法,等等。
于是,我通过“顺藤摸瓜”的方式,从知识树的主干开始做广度或是深度遍历,于是我就得到了一整棵的知识树。这种“顺藤摸瓜”的记忆方式让我记住了很多知识。最重要的是,当出现一些我不知道的知识点时,我就会往这棵知识树上挂,而这样一来,也使得我的学习更为系统和全面。
这种画知识图的方式可以让你从一个技术最重要最主干的地方出发开始遍历所有的技术细节,也就是画地图的方式。如果你不想在知识的海洋中迷路,你需要有一份地图,所以,学习并不是为了要记忆那些知识点,而是为了要找到一个知识的地图,你在这个地图上能通过关键路径找到你想要的答案。
系统地学习
- 这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题。这个问题非常关键,也就是说,你在学习一个技术的时候,需要知道这个技术的成因和目标,也就是这个技术的灵魂。如果不知道这些的话,那么你会看不懂这个技术的一些设计理念。
- 这个技术的优势和劣势分别是什么,或者说,这个技术的 trade-off 是什么。任何技术都有其好坏,在解决一个问题的时候,也会带来新的问题。另外,一般来说,任何设计都有 trade-off(要什么和不要什么),所以,你要清楚这个技术的优势和劣势,以及带来的挑战。
- 这个技术适用的场景。任何技术都有其适用的场景,离开了这个场景,这个技术可能会有很多槽点,所以学习技术不但要知道这个技术是什么,还要知道其适用的场景。没有任何一个技术是普适的。注意,所谓场景一般分别两个,一个是业务场景,一个是技术场景。
- 技术的组成部分和关键点。这是技术的核心思想和核心组件了,也是这个技术的灵魂所在了。学习技术的核心部分是快速掌握的关键。
- 技术的底层原理和关键实现。任何一个技术都有其底层的关键基础技术,这些关键技术很有可能也是其它技术的关键基础技术。所以,学习这些关键的基础底层技术,可以让你未来很快地掌握其它技术。可以参看我在 CoolShell 上写的 Docker 底层技术那一系列文章。
- 已有的实现和它之间的对比。一般来说,任何一个技术都会有不同的实现,不同的实现都会有不同的侧重。学习不同的实现,可以让你得到不同的想法和思路,对于开阔思维,深入细节是非常重要的。
举一反三
我觉得一个人的举一反三能力,可以分解成如下三种基本能力。
- 联想能力。这种能力的锻炼需要你平时就在不停地思考同一个事物的不同的用法,或是联想与之有关的别的事物。对于软件开发和技术学习也一样。
- 抽象能力。抽象能力是举一反三的基本技能。平时你解决问题的时候,如果你能对这个问题进行抽象,你就可以获得更多的表现形式。抽象能力需要找到解决问题的通用模型,比如数学就是对现实世界的一种抽象。只要我们能把现实世界的各种问题建立成数据模型(如,建立各种维度的向量),我们就可以用数学来求解,这也是机器学习的本质。
- 自省能力。所谓自省能力就是自己找自己的难看。当你得到一个解的时候,要站在自己的对立面来找这个解的漏洞。有点像左右手互博。这种自己和自己辩论的能力又叫思辨能力。将自己分裂成正反方,左右方,甚至多方,站在不同的立场上来和自己辩论,从而做到不漏过一个 case,从而获得完整全面的问题分析能力。
在这方面,我对自己的训练如下。
- 对于一个场景,制造出各种不同的问题或难题。
- 对于一个问题,努力寻找尽可能多的解,并比较这些解的优劣。
- 对于一个解,努力寻找各种不同的测试案例,以图让其健壮。
总结和归纳
我们积累的知识越多,在知识间进行联系和区辨的能力就越强,对知识进行总结和归纳也就越轻松。
学习的开始阶段,可以不急于总结归纳,不急于下判断,做结论,而应该保留部分知识的不确定性,保持对知识的开放状态。当对整个知识的理解更深入,自己站的位置更高以后,总结和归纳才会更有条理。总结归纳更多是在复习中对知识的回顾和重组,而不是一边学习一边就总结归纳。
把你看到和学习到的信息,归整好,排列好,关联好,总之把信息碎片给结构化掉,然后在结构化的信息中,找到规律,找到相通之处,找到共同之处,进行简化、归纳和总结,最终形成一种套路,一种模式,一种通用方法。
如果你在 Coolshell 上看过我写的《TCP 的那些事儿》,你就能知道我对《TCP/IP 详解》这本这么厚的书以及一些日常工作经验的总结,我写成了两篇比较简单的博客。你需要像我一样扩大自己的知识面,然后学会写博客,就能慢慢地拥有这种能力了。这种将信息删减、精炼和归纳的方法,可以让你的学习能力得到快速的提升。当你这么做的时候,一方面是在锻炼你抓重点的能力,另一方面是在锻炼你化繁为简的能力。这两种能力都是让你高效学习的能力。
实践出真知
所谓实践出真知,也就是学以致用,不然只是纸上谈兵,误国误民。只有实践过,你才能对学到的东西有更深的体会。就像我看 《Effective C++》和《More Effective C++》这两本书一样,一开始看的时候,我被作者的那种翻来覆去不断找到答案又否定自己的求知精神所折服。但是,作者的这种思维方式只有在我有了很多的实践和经验(错误)后,才能够真正地体会为什么是这样的。
这两书不厚,但是,我看了十多年,书中的很多章节我都可以背出来,但是我想得到的不是这些知识,而是这种思维方式,这需要我去做很多的编程工作才能真正明白,才会有斯科特·迈耶斯(Scott Meyers)那样的思维方式,这才是最宝贵的。
另外,实践出真知也就是英文中的 Eat your own dog food。吃自己的狗粮,你才能够有最真实的体会。那些大公司里的开发人员,写完代码,自己不测试,自己也不运维,我实在不知道他们怎么可能明白什么是好的设计,好的软件?不吃自己的狗粮,不养自己的孩子,他们就不会有痛苦,没有痛苦,就不会想改进,没有改进的诉求也就不会有学习的动力,没有学习,就不会进步,没有进步就只会开发很烂的软件……不断地恶性循环下去。
实践是很累很痛苦的事,但只有痛苦才会让人反思,而反思则是学习和改变自己的动力。
Grow up through the pain, 是非常有道理的。
坚持不懈
想一想,如果全中国有 100 万个程序员,只要你能坚持学习技术 2-3 年,你就可以超过至少 99 万人了(可能还更多)
一方面你要把你的坚持形成成果晒出来,让别人来给你点赞,另一方面,你还要把坚持变成一种习惯,就像吃饭喝水一样,你感觉不到太多的成本付出。只有做到这两点,你才能够真正坚持。
读文档还是读代码
- 如果你想了解一种思想,一种方法,一种原理,一种思路,一种经验,恐怕,读书和读文档会更有效率一些,因为其中会有作者的思路描述。像 Effective C++ 之类的书,里面有很多对不同用法和设计的推敲,TCP/IP 详解里面也会有对 TCP 算法好坏的比较……这些思维方式能让你对技术的把握力更强,而光看代码很难达到这种级别。(现在你知道什么样的书是好书了吧 ;-))
如果你想了解的就是具体细节,比如某协程的实现,某个模块的性能,某个算法的实现,那么你还是要去读代码的,因为代码中会有更具体的处理(尤其是对于一些 edge case 或是代码技巧方面的内容)。
很多时候,我们去读代码,那是因为没有文档,或是文档写得太差。
- 很多时候,在 Google、Stack Overflow、GitHub 过后,你会发现,你掌握的知识就是一块一块的碎片,既不系统,也不结构化,更别说融汇贯通了。你会觉得自己需要好好地读一本书,系统地掌握知识。你的这种感觉一定很强烈吧。
- 很多时候,在读别人代码的时候,你会因为基础知识或是原理不懂,或是你在不知道为什么的情况下,要么完全读不懂代码,要么会误解代码。比如,如果你没有 C 语言和 TCP 原理方面的基础知识,就根本读不懂 Linux 下 TCP 的相关代码。我们因为误解代码用意而去修改代码造成的故障还少吗?
- 很多时候,看到一个算法或是一个设计时,比如 Paxos,你是不是会想去看一下这个算法的实现代码是什么样的?思考一下如何才能实现得好?(但是如果你没看过 Paxos 的算法思想,我不认为你光看代码实现,就能收获 Paxos 的思想。)
- 很多时候,当你写代码的时候,你能感觉得到自己写的代码有点别扭,怎么写都别扭,这个时候,你也会有想去看别人的代码是怎么实现的冲动。
- 如果你是个新手,那应该多读代码,多动手写代码,因为你需要的是“感性认识”,这个时候“理性认识”你体会不到。一是因为,你没有切身的感受,即便告诉你 Why 你也体会不到。另一方面,这个阶段,你要的不是做漂亮,而是做出来。所以,在新手阶段,你会喜欢 GitHub 这样的东西。
- 如果你是个老手,你有多年的“感性认识”了,那么你的成长需要更多的“理性认识”。因为这个阶段,一方面,你会不满足于做出来,你会想去做更牛更漂亮的东西;另一方面,你知道的越多,你的问题也越多,你迫切地需要知道 Why!这时,你需要大量地找牛人交流(读牛人的书,是一种特殊的人与人的交流),所以,这个阶段,你会喜欢读好的书和文章。
如何阅读源代码
首先,在阅读代码之前,我建议你需要有下面的这些前提再去阅读代码,这样你读起代码来会很顺畅。
- 基础知识。相关的语言和基础技术的知识。
- 软件功能。你先要知道这个软件完成的是什么样的功能,有哪些特性,哪些配置项。你先要读一遍用户手册,然后让软件跑起来,自己先用一下感受一下。
- 相关文档。读一下相关的内部文档,Readme 也好,Release Notes 也好,Design 也好,Wiki 也好,这些文档可以让你明白整个软件的方方面面。如果你的软件没有文档,那么,你只能指望这个软件的原作者还在,而且他还乐于交流。
- 代码的组织结构。也就是代码目录中每个目录是什么样的功能,每个文档是干什么的。如果你要读的程序是在某种标准的框架下组织的,比如:Java 的 Spring 框架,那么恭喜你,这些代码不难读了。
接下来,你要了解这个软件的代码是由哪些部分构成的,我在这里给你一个列表,供你参考。
- 接口抽象定义。任何代码都会有很多接口或抽象定义,其描述了代码需要处理的数据结构或者业务实体,以及它们之间的关系,理清楚这些关系是非常重要的。
- 模块粘合层。我们的代码有很多都是用来粘合代码的,比如中间件(middleware)、Promises 模式、回调(Callback)、代理委托、依赖注入等。这些代码模块间的粘合技术是非常重要的,因为它们会把本来平铺直述的代码给分裂开来,让你不容易看明白它们的关系。
- 业务流程。这是代码运行的过程。一开始,我们不要进入细节,但需要在高层搞清楚整个业务的流程是什么样的,在这个流程中,数据是怎么被传递和处理的。一般来说,我们需要画程序流程图或者时序处理图。
- 具体实现。了解上述的三个方面的内容,相信你对整个代码的框架和逻辑已经有了总体认识。这个时候,你就可以深入细节,开始阅读具体实现的代码了。对于代码的具体实现,一般来说,你需要知道下面一些事实,这样有助于你在阅读代码时找到重点。
- 代码逻辑。代码有两种逻辑,一种是业务逻辑,这种逻辑是真正的业务处理逻辑;另一种是控制逻辑,这种逻辑只是用控制程序流转的,不是业务逻辑。比如:flag 之类的控制变量,多线程处理的代码,异步控制的代码,远程通讯的代码,对象序列化反序列化的代码等。这两种逻辑你要分开,很多代码之所以混乱就是把这两种逻辑混在一起了(详情参看《编程范式游记》)。
- 出错处理。根据 2:8 原则,20% 的代码是正常的逻辑,80% 的代码是在处理各种错误,所以,你在读代码的时候,完全可以把处理错误的代码全部删除掉,这样就会留下比较干净和简单的正常逻辑的代码。排除干扰因素,可以更高效地读代码。
- 数据处理。只要你认真观察,就会发现,我们好多代码就是在那里倒腾数据。比如 DAO、DTO,比如 JSON、XML,这些代码冗长无聊,不是主要逻辑,可以不理。
- 重要的算法。一般来说,我们的代码里会有很多重要的算法,我说的并不一定是什么排序或是搜索算法,可能会是一些其它的核心算法,比如一些索引表的算法,全局唯一 ID 的算法,信息推荐的算法、统计算法、通读算法(如 Gossip)等。这些比较核心的算法可能会非常难读,但它们往往是最有技术含量的部分。
- 底层交互。有一些代码是和底层系统的交互,一般来说是和操作系统或是 JVM 的交互。因此,读这些代码通常需要一定的底层技术知识,不然,很难读懂。
- 运行时调试。很多时候,代码只有运行起来了,才能知道具体发生了什么事,所以,我们让代码运行进来,然后用日志也好,debug 设置断点跟踪也好。实际看一下代码的运行过程,是了解代码的一种很好的方式。
总结一下,阅读代码的方法如下。
- 一般采用自顶向下,从总体到细节的“剥洋葱皮”的读法。
- 画图是必要的,程序流程图,调用时序图,模块组织图……
- 代码逻辑归一下类,排除杂音,主要逻辑才会更清楚。
- debug 跟踪一下代码是了解代码在执行中发生了什么的最好方式。
如何面对枯燥的知识
我列举我的这个学习过程,就是想说,如果你发现有些知识太过于枯燥,那么可以通过下面的方法解决。
- 这个知识对于你来说来太高级了,你可能不知道能用在什么地方。
- 人的认知是从感性认识向理性认识转化的,所以,你可能要先去找一下应用场景,学点更实用的,再回来学理论。
- 学习需要有反馈,有成就感,带着相关问题去学习会更好。
- 当然,找到牛人来给你讲解,也是一个很不错的手段。
如何面对大量的知识
东西太多了,怎么学。我给你的建议是,一点一点学,一口一口吃。你可以使用我前面说过的那些方法,注重基础,画知识图,多问为什么,多动手,然后坚持住,哪怕你每周就学一个知识点,你一年也可以学到 50 个知识点。只要你在进步,总有一天可以把这些知识学到手的。
当然,你的目的不是学完这些知识,因为学无止境,你永远也学不完,所以你在学习时,一定不要学在表面上,一定要学到本质,学到原理上,那些东西是不容易变的,也是经得住时间考验的。把学习当成投资,这是这个世界上回报最好的投资。
带着问题去学习,带着要解决的东西去学习,带着挑战去学习,于是每当你解决了一个问题,做了一个功能,完成了一个挑战,你就会感到兴奋和有成就感。这样,你也就找到了源源不断的学习驱动力。
把你学习的心得、过程、笔记、代码分享出来,找到和你一同学习的人,因为一个人长跑很辛苦,有人同行就会好很多,就算没有人同行,你的读者,你的观众也会为你鼓掌加油,这些也是让你持续前行的动力。
其它几个实用的技巧
- 用不同的方式来学习同一个东西。比如:通过看书,听课,创建脑图,写博客,讲课,解决实际问题,等等。
- 不要被打断。被打断简直就是学习的天敌,所以,你在学习的时候,最好把手机设置成勿扰模式放在一边,然后把电脑上的所有通知也关掉,最好到一个别人找不到你的地方。
- 总结压缩信息。当你获得太多的信息时,你需要有一个“压缩算法”。我常用的压缩算法是只关心关键点,所以,你需要使用表格、图示、笔记或者脑图来帮助你压缩信息。
- 把未知关联到已知。把你新学的知识点关联到已知的事物上来。比如,你在学习 Go 语言,你就把一些知识关联到自己已经学过的语言上比如 C 和 Java。通过类比,你会学得更扎实,也会思考得更多。
- 用教的方式来学习。你想想,如果你过几天要在公开场合对很多人讲一个技术,那么这个压力会让你学得更好。因为要教给别人,所以,这么高的标准需要你不但要把自己已掌握的东西学好,还要把周边的也一并学了,才可能做到百问不倒。你才敢去教别人,不是么?(试试教 6 岁的孩子编程,如果你掌握了这种技能,那么你一定是把知识吃得非常透彻了。)
- 学以致用。把学到的东西用起来,没有什么比用起来能让你的知识更巩固的了。在实践中,你才会有更为真实的体会,你才会遇到非常细节和非常具体的问题,这些都会让你重新思考,或深化学习。
- 不要记忆。聪明的人不会记忆知识的,他们会找方法,那些可以推导出知识或答案的方法。这也是为什么外国人特别喜欢方法论。
- 多犯错误。犯错会让你学得到更多,通过错误总结教训,你会比没有犯过错的人体会得更深。但是千万不要犯低级错误,也不要同一个错误犯两次。