写在前面
偶尔会有同学来问我:“飞哥,我在学校是学java,来公司却安排搞c++,我不喜欢,怎么办?”
让人信服地回答这个问题其实很难。我的理解是无论什么语言,本质是要服务业务,无论是java还是c++,他们都是实现业务软件的工具。在工作中,我们要尽可能多的去掌握一些固化不变的基础,越基础的知识点越具有通用性,掌握会让自己变得更有竞争力。各种编程语言在语法上可能有着相同又有不同的地方,有着他们各自最佳适合的场景。透过语法层面来看,似乎我们总是能找到一些共同的基础知识体系。当你了解并熟悉这些知识点之后,可能不会再纠结是选java还是其它了。
当然并不是语言层面的知识不再需要深入掌握,而是当工作选择需要你掌握哪种语言时,就去学习了解哪种语言。有了一些公共基础,也会触类旁通,学习起来更轻松。
本文试图抛开具体的语言,列出提纲梳理背后共同的技术栈,希望能给正在处于纠结选择语言的同学一些帮助。由于笔者能力、精力都有限,部分内容也是从网上收集整理,其中可能存在理解上偏差与错误。
数据结构与算法
曾有人总结,程序=数据结构+算法。毫无疑问,他们是基础,是根本。
学习他们最好的方法之一是刷LeetCode题。我之前比较反感刷题,因为觉得编程语言的体系都实现大多数的数据结构和多种算法,大部分算法也在实际工作中使用不到。在某一次的软件教练聚餐上,意识到优秀的人往往越爱学习。其中一位知名教练说他会每周刷一两道题,目的是锻炼解决问题的思考能力。
下面以应用的视角来看数据结构与算法,是不是觉得他们既陌生又熟悉?不经意间发现我们从未离开他们。
- 数据结构
- 集合:链表,栈,队列,编码时可能就是不停地与他们打交道
- 二叉树
- 红黑树:应用在Java/C++ STL的tree set/map,Linux虚拟内存管理
- 霍夫曼树:常应用在无损压缩编码
- B树:分B-树,B+树,B+-树,用在磁盘文件组织,数据索引和数据库索引
- AVL树:平衡二叉搜索树,Win系统对进程地址空间的管理
- 字典树:用在统计和排序大量字符串
- 图
- 二分图:有两顶点集且图中每条边的的两个顶点分别位于两个顶点集中,每个顶点集中没有边直接相连接。如应用在匈牙利算法,解决指派任务
- 有向无环图:如果一个有向图无法从某个顶点出发经过若干条边回到该点则这个图是一个有向无环图。如应用在流程编排,Spark的DAG图
- 欧拉图:通过图(无向图或有向图)中所有边且每边仅通过一次通路,相应的回路称为欧拉回路,如应用在判断定理,邮路问题
- 哈密顿图:图的一个回路,通过图的每一个节点一次且仅一次,就是哈密顿回路,存在哈密顿回路的图就是哈密顿图。可以解决旅行售货员,排座位问题等
- 算法
- 贪心:局部最优策略能导致产生全局最优解
- 动态规划:将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解
- 深度优先搜索:解决连通性问题,通常采用递归
- 广度优先搜索:解决最短路径问题,通常采用栈
- 最大流最小割:网络流基础,网络流的最大流等于其最小割
- 均摊分析:少量昂贵操作的成本均摊到所有的操作上
离散数学
昨天刷手机刷到我司交付的瑞士5G网络优化项目荣获2020 GLOTEL最佳交付奖。前一段时间GTS的创新论坛,正好有一位教授演讲的Topic与之相关,听完不得不感慨数学太重要了。因为这个获奖的背后并不是传统的工程化,而是通过数学建模来解决复杂网络的优化场景,数学公式是复杂事物的抽象。
不过大多数程序员不是数学科班出身,过多的数学理论对我们来说太难了。编程的基础是计算机科学,而计算机科学的基础是数学。学习数学有助于巩固编程的基础,写出更健壮的程序。
计算机只能懂得离散,作为程序员就有必要了解一些离散数学知识。离散数学是数学和计算机之间的桥梁。算法的基础都是离散数学,加密的理论基础也是离散数学,编码中很多奇怪而又高效的小技巧核心也是离散数学。
笔者去年偶然刷手机发现一本日本人写的《程序员的数学》,一读就停不下来,其中不少也与离散数学相关,写得太有意思了:
- 0的故事:无即是有,其意义更多的是空、占位符
- 逻辑:真与假的二元世界,写代码需注意边界值,也需要兼顾完整性和排他性
- 余数:大问题通过余数规律简化为小问题
- 归纳法:一条腿可以往前迈一步,另一条腿若也能迈出一步,那就能够行进到无限的远方
- 排列组合:计数的关键就在于注意“遗漏”和“重复”
- 递归:从一般性前提推出个别性结论
- 指数爆炸:极力求解,变相求解,近似求解,概率求解
- 不可解问题:不可数的集合,用对角论证法和反证法
数学如何解决问题:
- 认清模式,进行抽象化:用简单的数字进行规则的探寻,对问题进行抽象化的思考
- 由不擅长催生出的智慧:处理大规模数字、复杂判断,复杂问题可以找到规律简化为小问题
- 幻想法则:把问题换一种思考方式,不按套路去出牌
语言理论
以前看软件工程的材料,会看到形式化建模、形式化验证与形式化方法。由于非CS科班出身,对形式化这一词难以理解。一搜索才发现还有形式语言,自动机理念,进程代数等。它们是形式化方法基础,也是程序语言(PL)理论基础。
程序语言理论就是编程语言各自特征的设计、实现、分析、表征和分类。而形式语言是经过数学定义的语言,从数学方法的严谨性解决各类计算问题。解决这些问题需要多少资源,存储的空间与花费的时间要达到怎样程度。
公司iLearning上有一门南大教授讲《程序设计语言概念》的课程,或许你听完成之后有种上帝的视角,选择什么编程语言那不再是事儿。
下面关于PL的知识点,感兴趣的同学可以搜索相关材料。
- 形式语言理论
- 自动机理论
- 下推自动机
- 有限状态自动机
- 线性有界自动机
- 图灵机
- 文法
- 正则文法
- 上下文无关文法
- 无限制文法
- 形式系统
- 形式证明
- 自动机理论
- 形式语义学
- 操作语义
- 指称语义
- 代数语义
- 公理语义
- 编译原理
- 中间代码(IR)
- 编译器前端
- 编译器后端
- 编译方式
- 实时编译(JIT)
- 预先编译(AOT)
编程范式
编程语言有上百种,能归纳如下编程范式:
- 指令式:使用流程化语句和过程直接控制程序的运行与状态数据
- 过程式:核心在于模块化,在实现过程中使用了状态,依赖了外部变量,导致很容易影响附近的代码,可读性较差,后期维护成本也较高。
- 面向对象:核心在于事物抽象,提供清晰的对象边界。结合封装、集成、多态特性,降低了代码的耦合度,提升了系统的可维护性。
- 声明式:定义计算的逻辑而不是定义具体的流程控制
- 函数式:核心在于“避免副作用”,不改变也不依赖当前函数外的数据。结合不可变数据、函数是第一等公民等特性,使函数带有自描述性,可读性较高。
- 逻辑式:核心在于强调的是我们事先知道一系列事实,然后通过这些事实自动推出合理的结果
- 数据流式:核心在于将数据视作数据流的形式,并用pipeline的形式做处理。典型代表是响应式编程
- 元编程:用代码生成代码
- 静态元编程:在编译期展开生成或者执行代码
- 宏:分为基于文本替换的宏(C/C++),基于语法的宏(Rust)
- 泛型:在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。Java叫泛型,C++叫模板。
- 动态元编程:
- 反射式:在运行时可以访问、检测和修改它本身状态或行为的一种能力
- 静态元编程:在编译期展开生成或者执行代码
并发编程
软件的性能提升是一个永恒话题,并发编程是提升性能常见有效的方式。现有计算资源都是多核,从早期的多进程,到多线程,再到现在流行的协程,并发调度的粒度越来越细,为的是更有效地发挥多核的价值。但并发编程需要面对的问题是:并发实体之间的如何协同,资源竞争时如何同步?个人觉得并发编程是编码中最不好掌握且容易引发问题的点。
多进程编程主涉及到进程间通讯,在单体系统中应用较多,多与操作系统相关。协程可以理解微线程,近几年已逐渐普及。多线程编程是各种语言并发基础,下面是对多线程的知识点梳理:
- 并发模型
- CSP:独立的并发实体间通过共享的通讯channel(管道)进行通信,注重的是消息传送方式(channel),不关心发送的人和接收的人是谁
- Actor:独立的并发实体间通过消息传递避免数据竞争,注重的处理单元,也就是Actor,而不是消息传送方式,发送消息时,都需要知道对方是谁
- 协作方式
- Semaphone:信号量,维护了一组许可证,以约束访问被限制资源的线程数
- Monitor:管程,使用条件变量(Condition Variable)提供的等待队列(Waiting Set)实现线程间协作
- CountDownLatch:计数器,通过倒计数器控制线程是否要等待
- CyclicBarrier:循环屏障,用于一组固定数目的线程互相等待
- Phaser:相位器,一种同步器能够让多个线程分别在不同阶段进行同步
- Exchanger:交换器,提供了线程之间能够交换对象的同步点
- 设计模式
- Single Threaded Execution模式:同一时刻只允许一个线程操作
- Immutable模式:变量赋值一次后只能读取,不能改变
- Fork/Join:单机版的MapReduce
- Feature:给您一张提货单,下午来取
- Producer-Consumer模式:你生产蛋榚我来吃
- Balking模式:有人在做了?太好了,我就不过去了
- Guarded Suspension模式:要等到我准备好,没准备好就在门口等着,准备好了再叫你
- Thread per Message模式:一任务一线程
- Worker thread模式:工作没来就一直等,工作来了就干活
- Thread-Specific Storage模式:线程私有物品保管箱
- Two-Phase Termination模式:优雅安全的关闭线程
- 互斥锁
- 同步资源决策方式
- 悲观锁: 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
- 乐观锁: 认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
- 同步资源表现方式
- 阻塞锁
- 非阻塞锁
- 自旋锁: 指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
- 适应性自旋锁:自适应意味着自旋的时间(次数)不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
- 资源竞争方式
- 公平锁:排队,多个线程按照申请锁的顺序来获取锁
- 非公平锁:插队,多个线程获取锁的顺序并不是按照申请锁的顺序
- 资源竞争结果:
- 无锁:不锁住资源,多个线程只有一个锁能修改资源,其它线程重试
- 偏向锁:同一线程执行同步资源时自动获取资源
- 轻量级锁:多个线程竞争同步资源时,没有获得资源的线程自旋等待释放
- 重量级锁:多个线程竞争同步资源时,没有获得资源的线程阻塞等待唤醒
- 锁是否多线程持有
- 共享锁:锁可被多个线程所持有
- 独享锁:锁一次只能被一个线程所持有
- 锁是否可重入
- 可重入锁:递归锁,锁支持一个线程对资源的重复加锁
- 不可重入锁:锁不支持一个线程 对资源的重复加锁,同一线程重入会导致死锁
- 同步资源决策方式
设计模式
设计模式是从许多优秀系统中总结出的成功、可复用的经验,提供一套通用的设计词汇与形式来描述。
设计模式的六大原则:
- 开闭原则:对扩展开放,对修改关闭
- 里氏替换原则:任何基类可以出现的地方,子类一定可以出现
- 依赖倒置原则:针对接口编程,依赖于抽象而不依赖于具体
- 接口隔离原则:使用多个隔离的接口,比使用单个接口要好
- 迪米特法则:一个实体应当尽量少地与其他实体之间发生相互作用
- 合成复用原则:尽量使用合成/聚合的方式,而不是使用继承
面向对象设计原则:
- 对接口编程而不是对实现编程
- 优先使用对象组合而不是继承
应对变化,正交四原则:
- 消除重复:重复意味着耦合,最小化重复
- 分离变化:识别变化方向,并对变化预留出扩展接口
- 缩小依赖范围:依赖接口,不要依赖实现
- 向稳定的方向依赖:定义的API应该关注What,而不是How
经典 23 种设计模式:
- 创建型模式:隐藏创建对象的逻辑,包括:工厂模式,抽象工厂模式,单例模式,建造者模式,原型模式
- 结构型模式:组合接口和定义组合对象获得新功能,包括:适配器模式,桥接模式,过滤器模式,组合模式,装饰器模式,外观模式,享元模式,代理模式
- 行为型模式:使对象之间的通信方式松耦、可扩展,包括: 责任链模式,命令模式,解释器模式,迭代器模式,迭代器模式,中介者模式,备忘录模式,观察者模式,状态模式,空对象模式,策略模式,模板模式,访问者模式
结语
上面的提纲仅仅是从编程语言层次来发现背后的理论、数学、范式、模型、原则来展开。整个软件开发的技术栈太广太深,这些只是冰山一角,但却是最为基础。掌握基础知识点,或许会让你切换编程语言体系不再是那么困难。