本文为《代码之髓:编程语言核心概念- [日] 西尾泰和》的读书笔记。(书的内容深度不够,不过还是能扩充一下思想的,速读)
学习中需要做到以下三点。
• 在比较中学习
• 在历史中学习
• 在实践中学习
第一条是指通过比较多种语言,总结出某种语言的独有特点,以及多种语言的共有特点。
第二条是指通过追溯语言的发展历史,了解语言是如何产生、变化和消失的,探寻语言发展演变的轨迹。
第三条是指亲自进行程序设计。边实践边思考如何编程,才能深入理解语言设计者的意图,同时也能发现自己原先理解不到位之处。
如何深入高效地学习语言
多种语言共通的知识才是要点。在比较中学习多种语言时,一些知识能理解得更深刻。所谓语言不同,规则不同。
学习适用于各种语言的知识。通过比较不同的语言、了解语言的发展历史及其变化原因,培养对不同语言都适用的理解能力,是非常重要的。
懒惰:程序员的三大美德之一
Perl 语言的设计者 Larry Wall 提出,优秀的程序员具有三大美德: 懒惰、急躁和傲慢(Laziness, Impatience and Hubris)。“急躁”的意思是,程序员忍受不了程序执行的低效。“傲慢”的意思是,程序员容不得对错误不管不顾。
Laziness 有懒惰、懒散、慵懒等不同的翻译方式,总的来说就是让自己轻松、方便。但这不是追求一时轻松,而是选择能将轻松便捷最大化的方法。也就是说,在能达到相同目的的多种方法中,选取一种效率最高、效果最好的方法。
程序设计语言的选用因使用者目的不同而不同。不同语言致力于达成不同的目的。
语法的诞生
语法就是程序语言设计者规定的解释程序编写方式的一系列规则。同样是处理 1 加 2 乘以 3 这样的运算,不同语言的表达方式大相径庭。但是基本上都是用语法树来表达。语言之间的这种差异就是语法的差别,它决定了怎样的代码对应怎样的语法树。
程序的流程控制
使用if…else , while, break,continue 代替 过于强大、结构差、不便阅读的goto。
同时增加 for, switch,以及后续的foreach 让代码更简洁、清晰。
虽然不使用这些语句也可以编写程序,但是使用它们会让我们的程序变得更容易理解。
函数
随着程序变得越来越庞大,把握全局逐渐地变得困难起来。同时,有可能需要多次用到非常相似的操作。
函数就是为解决这个问题产生的。通过在语义上把一整块代码切分出来为之命名,理解这段代码变得更加容易。此外,通过在其他地方调用这个函数,实现了代码的再利用。
伴随着函数的使用产生了递归调用这一编程技巧,它非常适合处理嵌套形式的数据。
错误处理
错误处理的方法大体可分为两种:使用返回值和使用异常(异常处理)。
C++ ,JAVA,C# 使用throw , try … catch。
JAVA,C# 中 可以使用try … catch … finally。使用finally,出口只有一个,实现成对操作的无遗漏执行。
其他语言中所谓的异常,Java 语言中的 throw 语句也能抛出,并进一步分为三类:不应该做异常处理的重大问题、可做异常处理的运行时异常和可做异常处理的其他异常。这里的其他异常叫做检查型异常,如果在方法之外抛出,就需要在定义方法时声明。
JAVA 采用检查型异常,要明确声明可能抛出的异常,因为”The Trouble with Checked Exceptions”,所以C#未引人。这种机制并没有很好地普及到其他语言中,就是因为它太麻烦。一旦 throws 或 try/catch 中异常的数目增多,或者某一方法需要追加一种异常,就不得不修改调用了该方法的所有方法,特别麻烦。
通过返回值传达出错信息有两个问题:
1 遗漏错误
程序员忘记了对返回值做检查,从而遗漏了错误。
2 错误处理导致代码可读性下降
名字和作用域
(动态作用域、全局作用域、静态作用域),命名空间。
类型
类型是人们给数据附加的一种追加数据。
CPU对操作数的存放方式:Little_endian(从低字节到高字节)和Big_endian(从高字节到低字节)。
以2进制表示10进制,约3.32(log10 / log2)个bit位表示一位。
整形:最高位为符号位。负数采用补码的方式,从而将减法转变为加法。(补码:除符号位外 按位取反 再 加 1 )
浮点:以32位为例,最高位为符号位,之后8位是表示位数的指数部分(0~255,减去127,得到-127~128,-127表下溢出:0,128表上溢出:无穷小或无穷大,剩下的为位数),剩下23位为尾数部分,表示小数点以下的部分(通过移动小数点使整数部分变成1后的小数部分)。
自定义类型:结构,联合,枚举,类。使用基本数据类型通过组合定义新的类型。
泛型、模版。
动态类型。
容器和字符串
不同语言中名称表达的差异是导致混乱的根源。因此,本章把这种存放多个元素的东西称为容器。因为在内存上存储数据的方式不一样,各种容器的性能也不同,没有一种容器在各方面都是最优的,而是优缺点兼具。多数语言都支持数组和链表两种容器。
- 数组与链表
元素较少时,使用数组还是链表的差别并不大。但是随着元素个数的增加,数组所需时间不断增加,而链表所需时间却没有变化。所以对于元素多、插入操作频繁的情况,链表是更适合的。 - 链表的长处与短处
链表的长处是插入元素的计算量为 O(1),而对于数组是 O(n)。元素的删除同样如此。另一方面,链表也有其短处,要获得第 n 个元素需要花费较长时间。
字符串就是字符并列的结果,但在不同的语言中,字符串列的表现方式各不相同。并发处理
细分后再执行:在人们察觉不到的极短间隔内交替进行多项处理。尽管在某一瞬间实际只进行一项处理,但人们会觉得似乎有多项处理在同时进行。交替的两种方法
“何时交替”可以分为两种情况。
- 竞态条件成立的三个条件
并行执行的两个处理之间出现竞态条件必须同时满足以下三个条件。
a 两个处理共享变量
b 至少一个处理会对变量进行修改
c 一个处理未完成之前另一个处理有可能介入进来
反之,只要三个条件中有一个不具备,就可以编写适于并发处理的安全的程序。没有共享——进程和 actor 模型
不同的进程不会共享内存,所以在多个程序之间不会在内存上出现竞态条件。只需要注意与数据库连接或文件读写时共享数据的情形就够了。
actor 模型:发布于 1973 年,是为实现并发处理而出现的一种模型。它认为可以通过不共享内存而是传递消息的方法 来在并发处理时进行信息交互。这种模型中处理是非同步的。不修改——const、val、Immutable
有一种方法是通过规避条件b,即使共享内存,只要不作修改也不会有任何问题。使一部分变更无法作修改。比如在 C++ 语言中,使用 const 声明变量时,这个变量就是无法修改的。再如在 Scala 语言中,有 var 和 val 两种声明变量的方法,val 声明的变量就无法作修改。
Java 语言经常使用到 Mark Grand 提出的设计模式 9 之一的 Immutable 模式。这种模式下,类中定义了 private 字段,同时定义了读取这些字段的 getter 方法,但不定义对这些字段作修改的 setter 方法。因为没有准备用于修改的方法,所以实现了只能读取但不能改写的效果。不介入
线程的协调——fibre、coroutine、green thread
:由于是协作式多任务模式,如果有某个线程独占 CPU,其他处理就只能停止。说到底,这种方法的前提是各个线程能保证合理的执行时间在合适的时候做出让步。
表示不便介入的标志——锁、mutex、semaphore
:锁这个名字很容易让人误解为只要上了锁其他人就进不来了,然而实际上它只是一个表示“使用中”的状态牌。如果有线程不去检查状态牌的状态,那它也就变得没有意义了。为了不让其他处理在中间介入进来,就有必要使用一种能将值的检查和修改同时执行的命令。因为 Java 语言处理器实现了这一功能,所以 Java 语言用户无需烦恼,只要直接使用synchronized lock
就可以轻松地使用锁的功能。锁的问题及对策
- 锁的问题
- 陷入死锁
为了避免这一问题,程序员就需要在程序的整体上注意上锁的顺序,不仅要把握应该对什么上锁,还要把握好按什么顺序去上锁。 - 无法组合
在线程安全的程序库中,程序员无需担心锁的控制方式,内部机制可以保证使用锁后删除操作或写入操作不会被中间介入。但这个锁无法保证将从 X 删除值往 Y 中写入时不会被中间介入。要防止中间介入,程序员必须用新的锁将这两个处理步骤包括起来,用 synchronized lock 把所有这些与 X 和 Y 读写相关的代码包括起来。
- 对策-借助事务内存来解决
这种方法把数据库中事务的理念运用到内存上,做法是先试着执行,如果失败则回退到最初状态重新执行,如果成功则共享这一变更。假设有写入操作在中间介入进来,那么临时创建的版本就会被丢弃,重新回退到最初状态开始执行。这样一来,即使不上锁也可以顺利地进行并发处理。要注意的是,当写入的频率太高时,回退重 新执行的操作就会多次执行到,这样会导致性能下降。对象与类
面向对象编程里的 对象是现实世界的模型。归纳并建立模型的方式多种多样,语言不同,选择也不同,类是最方便的,却不是必须的。
语言中的用语并不是共通的,在不同语言中,同一个用语的含义可能会有很大差别。大部分语言的程序设计中,类并不是不可或缺的,但 Java 语言是例外。Java 语言“把类定义为部件,将其组装起来即是程序设计”。因此,在用 Java 语言编写程序时类是必要的。 - 归集变量与函数建立模型的方法
方法 1:模块、包
模块是一种归集的方法。
方法 2:把函数也放入散列中
方法 3:闭包
它是创建具有对象性质的事物的一种技术。很多语言都支持定义带有某种状态的函数。一个包含了自由变量的开放表达式,它和该自由变量的约束环境组合在一起后,实现了一种封闭的状态。
方法 4:类
C++ 语言和 Java 语言的类具有以下几个作用:
❶ 整合体的生成器
❷ 可行操作的功能说明
❸ 代码再利用的单位继承与代码再利用
继承的不同实现策略
- 一般化与专门化
第一种策略是在父类中实现那些一般化的功能,在子类中实现那些专门的个性化的功能。 - 共享部分的提取
第二种策略是从多个类中提取出共享部分作为父类。 - 差异实现
第三种策略认为继承之后仅实现有变更的那些属性会带来效率的提高。它把继承作为实现方式再利用的途径,旨在使编程实现更加轻松。继承是把双刃剑
尤其是第三种使用方法——继承已有的类并实现差异部分,这种编程风格会造成多层级的继承树,很容易导致代码理解困难。通过使用继承实现代码的再利用,对于编写程序来说代码编写量减少了,工作变轻松了。但是反复使用继承后代码的影响范围便变大了,理解起来也困难了。因此,为了保证理解的简易性,就要防止继承树的层级过多。里氏置换原则
这一原则现在常常在创建子类时作为注意点被提及。这一原则也可以表达为继承必须是 is-a 关系。把子类 S 的所有对象都看作是父类 T 的对象而不会有任何问题,必须要做到这一点。多重继承
多重继承对于实现方式再利用是一种非常便利的方法。
多重继承的问题——还是有冲突:
- 解决方法 1:禁止多重继承
Java 语言中就禁止了类的多重继承。只要不认可类的多重继承这种方式,就不会有上述问题。
委托:
委托(delegation)也叫做聚集(aggregation)或者咨询(consultation)。这种方法定义了具有待使用实现方式的类的对象,然后根据需要使用该对象来处理。使用继承后,从类型到命名空间都会被一起继承,从而导致问题的发生,这种方法只是停留在使用对象的层面上。对于委托的使用,也不需要在源代码中写死,而是可以通过配置文件在合适的时候注入运行时中去。这个想法催生了依赖注入(Dependency Injection)的概念。
接口:
Java 语言中类的继承用 extends,接口的继承用 implements 来区别表示。另外接口的继承也称为实现。接口是没有实现方式的类。它的功能仅仅在于说明继承了该接口的类必须持有某某名字的方法。多重继承中发生的问题是多种实现方式相冲突时选取哪个的问题。而在接口的多重继承中,尽管有多个持有某某方法的信息存在,但这仅仅表明持有某某方法,不会造成任何困扰。
Java 语言中可以对名字相同但参数类型不同的方法进行重载。 - 解决方法 2:按顺序进行搜索
- 解决方法 3:混入式处理
- 解决方法 4:Trait
类具有两种截然相反的作用。一种是用于创建实例的作用,它要求类是全面的、包含所有必需的内容的、大的类。另一种是作为再利用单元的作用,它要求类是按功能分的、没有多余内容的、小的类。Trait 的设计使得即使顺序改变了程序的行为也不会变。发生名字冲突时,程序会明确地发布错误信息。