代码之髓
2021-07-21 10:20:52 0 举报
AI智能生成
如何深入高效地学习语言?是在比较中学习?还是在历史中学习?
作者其他创作
大纲/内容
学习多种语言的共通点并理解
如真假值问题
在比较中学习
如提出这种语言的目的,应用范围及特性等
理解语言设计者的意图
如基本的流程控制
培养适用于各种语言的知识
在历史中学习
1.如何深入高效地学习语言
1.连接电缆(ENIAC)
机器语言
EDSAC 纸带打点,读取纸带数据
2. 程序内置
Formula Translating System 公式翻译系统
公式转化为机器语言
3. FORTRAN语问世
历史
Perl设计者 Larry Wall 《Programming Perl》
懒惰: 让自己轻松方便
急躁: 无法容忍低效
傲慢: 无法容忍对错误不管不顾
程序员三大美德
1. 懒惰
如C++重视执行速度
如Scheme 重视语言规则
如Python 为代码阅读变得容易
程序语言为便捷而生
每个语言都有擅长的领域
每种语言致力于达成不同的目的
2. 语言各有各便捷
产生的原因
语言更多是工具属性,为解决现实问题提供更多的便捷
程序设计语言提供工作效率
解决现实问题时要根据自己的情况去选择相应的语言
总结
2. 程序设计语言诞生史
定义: 语法就是程序设计者规定的解释程序编写方式的一系列规则
运算符的优先级
语法因语言而异
运算符的存在类型也因语言而异
什么是语法
Forth语言 开发于1958年,几乎没有语法的语言
Forth语言设计者 Charles H. Moores
Forth语言使用了被称为栈的数值预存空间
1 2 + 3 * 表示1加2再乘以3
运算符在计算的数值后
Forth 如何表达计算顺序
Forth 基于栈
当前很少使用直接基于栈的语言直接编写代码
基于栈的语言
栈机器和FORTH语言
Lisp语言使用 括号表示完整的意思单元
结构及 FORTH和LISP语法树对比
通过Python 自带库文件ast(abstract syntax tree 抽象语法树) 查看特定代码被转换成的语法树 例如: import ast; ast.dump(ast.parse("1+2"))
如图
Python 和Lisp 语法树对比
语法树
要确认理解是否正确,首要要表达清楚
要说出自己的观点,能让第三方检验
编写程序有误,则机器会准确指出
专栏
语法树和LISP语言
如 LISP 语言 (+ 1 2) 运算符放在运算对象的前面
前缀表达式
如 日常数学表达式 1+2 运算符放在运算对象的中间
中缀表达式
如 FORTH 语言 1 2 + 运算符放在运算对象的后面
后缀表达式
表达式
把源代码作为字符串读入,解析,并建立语法树的程序
语法的设计和 语法分析器的实现是决定语言外在表现的重要因素
语法分析器
vvv<vector<int>>一侧的>>符号就会被解析为既有的移位运算符>>
规则竞争
学习目的不明确时,先从无下手
宏大的想法也要从简单的事情做起
学习和积累的过程中逐渐明白能做什么,不能做什么,还需要学习什么
中缀语法
不同的语言表达方式大相径庭,但基本上都是使用语法树来表达
语言的差异就是语法树的差别,其决定了怎样的代码对应怎样的语法树
现代大多数语言崇尚FORTRAN语言风格,追求简单便利
融入新语法时不与现有语法发生冲突很困难
现实中程序设计语言仍保留有不少别扭复杂的编写规则
设计不存在任何解析矛盾的语法体系是十分困难的
小结
3.语法的诞生
初衷是为了使代码结构更简单
1. 结构化程序设计的诞生
汇编语言没有if语句
更多是使用跳转的方式
goto语句
使用if...else 能是编程更容易理解
2. if语句诞生之前
增强了程序的易读性和易写性
3. while语句
while 不直观 for语句更加紧凑
通过处理的对象来控制循环操作
为了方便编写对某对象内所有元素进行某种处理的代码
foreach 句型
4. for语句
程序流程控制语句 主要目的是为了写出更简洁易懂的程序
5.小结
4.程序的流程控制
定义: 把代码的一部分视作有机整体,然后切分处理并为之命名的程序设计机制
便于理解和重复使用
减少代码冗余,封装成整体更易理解
1. 函数的作用
goto 语句无法将程序返回原来的位置
变量的诞生,就是为了用字符串替代数值开始时的内存的位置
存储于多个值的数据结构
后入先出
栈
2. 返回命令
函数内部再次调用当前函数的过程
更以处理嵌套结构
递归结束条件很关键
3. 递归调用
函数 可以减少重复代码
递归适合处理嵌套形式的数据
函数将一部分代码切分出来更容易理解
4. 小结
5.函数
C语言不支持异常处理机制
使用返回值
使用异常处理
错误处理的两种方式
1.程序也会出错
即是 出错时把信息写入返回值,接着做返回值检查
容易遗漏错误
错误处理会导致代码可读性下降
问题
C语言中没有异常机制,而使用goto语句
从形式上 把错误处理代码和业务处理代码分开
通过跳转集中进行错误处理
通过返回值传达出错信息
UNIVAC I,是1951年使用晶体管制造的计算机,是第二代计算机的代表
UNIVACI
1954年出现的程序设计语言FORTRAN语言中还没有异常处理机制
1959年COBOL语言才设计了两种类型的错误处理机制
COBOL
为实现灵活的错误处理,引入了ON语句结构
能够让语言处理器自动检查是否出错
PL/I可以追加定义新的错误类型
程序可以主动触发新定义的错误类型
可追加,可自主触发出错,这两项功能为现代异常处理所继承
PL/I
C语言诞生前已经存在其他错误处理的方式
C语言等语言主要是通过返回值来传达错误
出错则跳转
2. 如何传达错误
Java等语言是先编写代码再编写错误处理
明确声明可能抛出何种异常
需要将可能出错的操作括起来的语句结构
Java语言异常检查使用了这个方针
John Goodenough 的观点
CLU语言引入了异常处理机制
追加了置于后面的错误处理语句结构except
引入CLU语言
1983年 C++语言诞生
斯特劳斯特卢普 (Bjarne Stroustrup) :引入关键字try只是为了方便理解
引入C++语言
导入了结构化异常处理 (SEH Structured Exception Handling)概念
引入Windows NT 3.1
3. 将可能出错的代码括起来的语句结构
可以提高代码的可靠性
为什么引入finally
如 文件打开后,执行操作后要关闭,这些都要成对出现
成对操作的无遗漏执行
1990 微软开始使用finally
1995年 Java发布,也引入了finally
使用finally解决方案
使用RAII 资源获取即初始化技术
设计者认为比使用fianlly方法更为优雅
没有finally的C++语言的解决方案
可以事先定义从某一作用域跳出时执行的操作
引入了作用域守护(scope guard)的概念
D语言中scope(exit)的解决方案
4. 出口只要一个
JavaScript把缺失的参数当作未定义的特殊值(undefined)继续执行
函数调用时参数不足的情况
数组越界的情况
"错误优先"思想(fail fast)
出错就要立刻抛出异常
5. 何时抛出异常
Java等语言可以将异常传递给调用方
Java语言的检查型异常(要明确地声明可能要抛出的异常)
太麻烦
检查型异常没有得到普及的原因
6. 异常传递
错误传达的两种类型
两种方法长短处兼具,要根据需要去选择
7.小结
有意识的学习应用范围更广抽象的概念
将抽象概念和自己具体经验相结合
学习讲求细嚼慢咽
从需要的地方开始阅读
读书要针对性的阅读
6.错误处理
编号(存储地址)->用名字指定对象
使用内存地址 不直观,不利于阅读,故使用名字
建立名字与内容的对照表
早期整个程序共享同一个对照表,容易造成名字冲突
取更长更有具有特殊标记的名字
使用作用域
避免冲突:
为什么要取名
作用域指名字的有效范围,范围越小,名字越不易冲突
具体操作: 把变量原来的值先保存在函数入口,在出口处再写回变量
早期是人工处理,现在基本程序自动处理
改写变量后,再调用其他函数,被调用函数会受到影响
问题点:
动态作用域
每调用一个函数时,就创建一个新的对照表
动态作用域的对照表能被全部代码读取
动态作用域的问题点: 多个函数共用一张对照表
现在很多语言都是采用了静态作用域
静态作用域
作用域的演变
内置的
全局的
局部的
python采用静态作用域
Python 赋值即定义的语言
一图胜千言
嵌套函数问题
nonlocal 关键字: 非本地变量声明,通过此关键字修改外部作用域
外部作用域再绑定
问题点
静态作用域是完美的吗
当前大部分计算机语言使用的是静态作用域
动态作用域是从进入该作用域开始直到离开这一时间轴上的完整独立的范围
不要把单例模式作为全局变量的替代来使用
7. 名字与作用域
类型是人们给数据附加的的一种追加数据
计算机中保存的数据是有on和off或0和1的组合来表达
伯特兰罗素 注意到对集合的定义会产生悖论
1. 什么是类型
数位的发明
七段数码管显示器
算盘
2. 数值的on和off的表达方式
从十进制到二进制
八进制与十六进制
3. 一个数位上需要几盏灯泡
定点数——小数位置确定
IEEE 754标准
浮点数——数值本身包含小数部分何处开始的信息
4. 如何表达实数
7和7.0在计算机中 是完全不同的
整数和浮点数之间运算的之间转换问题
隐性类型转换
5.为何会出现类型
用户定义型和面向对象
作为功能的类型
总称型、泛型和模板
动态类型
类型推断
6.类型的各种展开
0-10 最原始的计数方法
发展到进位法
定点数和浮点数
类型
先掌握概要再阅读细节
先浏览目录了解构造,再根据需要选择阅读
阅读源码 要先了解基本结构及切入口
从头开始逐章抄
8.专栏
8.类型
如LISP语言和Haskell语言中的列表 与Java语言和Python语言中的列表内部构造完全不同
不同语言中名称表达的差异是导致混乱的根源
1. 容器种类多样
由于各种容器兼具长处和短处
容器数据实际存放在内存中
数组存放方式是数值按顺序排列存放的
链表是在内存中同时存放表示下一个数据存放位置的信息
链表适合元素多的插入操作
数组适合查询操作
插入删除计算量为O(1),而查找计算量为O(n)
用大O表示链表的长处和短处
数据量n翻倍,计算所花费的时间也翻倍 O(n), n的数量级
数据量n翻倍,计算时间不变吃 常数的数量级
当数据量变为2倍、3倍时,计算时间增加到4倍、9倍,用O(n2)表示
当数据量变成2倍时增加的计算时间,和数据量从2倍增加4倍时增加的计算时间相同 用O(logn) 表示
随着n的增大,大致关系如下: O(1)< O(log n)< O(n)<O(n2)
大O表示法
数组和链表
Java,Python,Ruby等 数组是最基本的容器
大多数语言中的容器都是在库中提供的
语言差异
2. 为什么存在不同种类的容器
数组是整数和值的对应
以字符串为参数返回整数的散列函数,实现了字符串与值的对应
散列表
基本的数据结构
从树中读取元素的所需时间是O(logn)
树
散列表所需的时间数量级是最小,但占用比较多的内存空间
合理利用容器,根据不同的需求进行不同的调整
3.字典、散列、关联数组
是人们约定好的命名为字符的一系列字符的集合
字符集合称为字符集或字符包
字符集因国家和文化不同千差万别
编码必须编码方和解码方共同所有
效率优先,满足个别需求的原则
标准化
字符编码方式的发展历程,是两种观点相互角力的历史
字符要通过数字化显示需要进行编码
摩斯码
博多码
EDSAC的字符编码
ASCII时代和EBCDIC时代(IBM)
计算机诞生前的编码
告诉编译器源码的字符编码方式
# -*- coding utf-8 -*- #
魔术注释符
Unicode带来了统一
4. 什么是字符
就是字符并列的结果
C语言中 字符串不知道自身的长度,是最原始的字符串
一个字符为16比特的Java字符串
既支持java 16bit的Unicode字符
又支持8bit的字符串列的字符串
Python语言引入的设计变更
5.什么是字符串
性能
各容器优缺点
链表和数组
容器
历史演变
标准
字符串
UTF-8 8-bit Unicode Transformation Format
UTF-16等
字符串编码方式
6.小结
9. 容器和字符串
重叠的时间段内同时进行的多个处理
EDSAC等古老计算机不能进行并行处理
进程和线程
锁和光纤
1.什么是并行处理
在人们察觉不到的极短时间内,交替进行多项处理
在人们看来,多项任务同时进行,其实是在切分为小的时间段内串行执行
2.细分后再执行
协作式多任务模式——在合适的节点交替
当前CPU已经装载多个处理线路,既多核
抢占式多任务模式——定时间后进行交替
3.交替的两种方法
实例说明
两个处理共享变量
至少一个会对变量进行修改
一个处理未完成之前另一个处理有可能介入进来
竞态条件的成立的必须满足的三个条件
只要一个不满足,就可以编写适合并行处理的安全的程序
主要是抢占式线程
Ruby中的Fibre类
Python和Javascript中的generator
这种方法的前期是各个线程能保证合理的执行时间在合适的时候做出让步
使用时先检查是否在使用,有则等待,无则进入
Java实现了synchronized lock
表示不便于介入的标志——锁、mutex、semaphore
不介入
通过规避条件,即使共享内存,只要不修改也不会有问题(Haskell语言提倡这种方式)
Haskell的所有变量都不可修改
C++ 的const
Scala 的val
变量定义成private 字段,只提供getter方法,不提供setter方法
实现读取不能修改的效果
Java 的Immutable模式(不变模式是多线程设计模式之一)
更多的语言采用折衷策略,部分可以修改
不修改——const,val,Immutable
进程间没有内存共享
1969年 Multics系统 进程是共享内存的
Multics 太复杂 ,简化运动,最终 1970年 UNICS 被设计出来 即后来的UNIX
UNIX保证每个进程所需的内存空间,不同的内存空间不会共享内存
UNIX的机制一个进程只能并行处理一个
多线处理必须启动多个进程
没有共享内存依然有解决不了并行处理问题,同样使用线程依然有很多问题无法解决
线程诞生
非同步
适合信息交互的处理场景
Erlang、Scalca等使用了actor模型
actor模型
没有共享——进程和actor模型
4.如何避免竞态条件
死锁
无法组合
锁的问题
数据库的理念运用到内存上
先尝试解决,失败则回退,成功则共享该变量,创建临时版本,完整不可以分割的过程执行完毕后才反映在最终结果上
借助事务内存来解决
商业不成功 Symbolics公司
硬件事务内存
1995年 在软件中实现事务内存的论文发表
2005年 微软发表使用Concurrent Haskell 在软件中实现事务内存的论文
此后 很多编程语言都实现了软件事务内存
2007 年基于Java VM的Clojure发布(一种运行在Java平台上的 Lisp 方言)
软件事务内存
事务内存历史
2010 年 .net 平台终止了搭载事务内存的实验
Azul Systems在2009年推出的 Vega 2 微处理器支援硬件事务内存
事务内存能成功吗
5.锁的问题及对策
并行是个比较复杂的问题
并行处理对每一种语言都很重要
共享->非共享->共享
协调->非协调->协调
硬件->软件->硬件
观念摇摆
兼顾两面,灵活运用才是最重要的
10.并行处理
class是一种创建用户自定义类型的功能
Simula(1967年诞生于挪威,最早的面向对象程序设计语言) 的继承机制是解决问题的关键
面向对象程序设计是使用了用户定义类型和继承的程序设计
C++设计者 本贾尼 斯特劳斯特卢普
通过不同状态的对象相互传递消息来通信的程序设计就是面向对象
不喜欢Simula的继承方式
对类和继承持否定立场
面向对象的发明者 艾伦·凯(Alan kay ) 同时是Smalltalk 语言设计者
分支主题
面向对象语言历史
面向对象所指的内容因语言而异
将现实世界的“物”的模型在计算机中建立起来(ALGOL60 语言设计者霍尔)
对象是现实世界的模型
Java将类定义为部件,将其组装起来即是程序设计
C++ 将类定义为用户自定义类型
什么是类
1. 什么是面向对象
将相关联的函数集中到一起的功能
Perl中叫包 把归集函数的包和归集变量的散列绑定在一起的方法
模块(module)
JavaScript语言
把函数和变量放入散列中
主要在函数式语言中使用
闭包(closure)
2. 归集变量与函数建立模型的方法
1978年 Modula-2语言导入了模块的概念
Python和Ruby继承了Modula-2的模块概念
而Java和Perl语言则称为包 Package
3. 方法1: 模块、包
Javascript 使用另一种归集方法,这种方法将函数也放入散列中
而Fortran 66 中不能把字符串赋值给变量
C语言不可以将数组作为函数参数
大部分语言可以字符串赋值给变量,可以做函数参数或返回值
像这种不受限制,可以赋值给变量,也可以作为函数的参数传递,又可以做函数的返回值的值被称为first class
在Python,Java,Perl中 字符串 就是first class(第一级/一阶元素或头等元素)的值
如果一个对象自己没有当前变量的值,会尝试到它的原型中去查找
new 运算符实现高效表达
把共享的属性放入原型中
把函数也放入散列中
类并不是程序设计诞生开始就存在的
类的存在更多是为了提供便利性而提出的
类更多是一种约定事项,在每种语言学习中,要充分理解程序语言设计者引入类的意图
面向对象
4. 方法2:把函数也放入散列中
创建具有对象性质的事物的一种技术
可以在函数中定义函数,允许嵌套的静态作用域,可以把函数作为返回值传递给变量,那么通过嵌套就可以实现带有某种状态的函数
并没有闭包的特殊语法结构
一个包含了自由变量的开放表达式,它和该自由变量的约束环境组合在一起后,实现了一种封闭的状态
为什么称为闭包
5.方法3: 闭包
最初是分类
Java,C++ 包含了模板意思
霍尔设想的类
用户自定义类型
直接延续了Simula语言中的名词
C++中的类
Smalltalk语言中,方法调用是向对象传送一个消息,告诉执行某个方法,具体执行与否,有执行方决定
艾伦·凯 自由度是面向对象的重要元素之一
功能说明的作用
Java和Perl主要是集中在生成器功能上
整合体的生成
Java 接口的定义
动态类型语言对这点不太重视
可行操作的功能说明
继承
代码再利用的单元
类的三大作用
6. 方法4:类
面对对象产生的原因: 为创建现实世界中物的模型而产生
不同语言对类的实现方式以及面向对象这一术语意义也不相同
11.对象和类
父类实现一般的功能
子类是父类的专门化,实现专门化的个性功能
一般化与专门化
从多个类中提取出共享部分作为父类
不考虑是否是父类的一种
是习惯了函数的一种考虑问题的方法
共享部分的提取
继承之后仅实现有变更的那些属性会带来效率的提高
继承作为实现方式再利用的途径,旨在使编程实现更轻松
通常子类都不是父类的一种
差异实现
继承的不同实现策略
继承机制因语言而异
自由度高
尤其第三种 差异实现 容易造成多层级的继承树,容易导致代码理解困难
继承树的层级不易过多
继承是把双刃剑
CLU语言 设计者 芭芭拉·科斯科夫 (Barbara Liskov)等1987年提出
在创建子类时会被重点提及
对于类T的对象一定成立的条件,对于类T的子类S的对象也必须成立
父类出现的地方可以用子类替换
继承必须是is-a的关系
这约束条件很严格,在属性不断增加的情况下,是可能被打破的
是否遵守里氏替换原则要看具体情形
里氏替换原则
1. 什么是继承
多重继承的初衷
一种事物在多个分类中
多重继承对于实现方式再利用非常便利
2.多重继承
多个父类中变量名字重名的问题
Java语言采用的这种方式
失去了多重继承的良好便利性
委托 也叫聚集 或者咨询
与从多个类中实现强耦合相比,使用委托进行耦合更好些
委托可以通过配置文件在合适的时候注入运行时去,后来出现了依赖注入的概念(Dependency Injection)
委托
Java 通过接口具备实现多重继承功能
Java语言为了仅实现功能上的多重继承引入了接口
接口
解决方法1:禁止多重继承
试图通过按明确定义的搜索顺序来解决冲突的问题
菱形继承
深度优先搜索法
父类不比子类被检查
如果是从多个子类中继承下来则优先检查先书写的类
Python2.1 使用的是深度优先搜索法,Python2.3 以后采用C3
C3线性化(1993年提出的算法)确定顺序
解决方法2:按顺序进行搜索
可以消除菱形继承
而且继承树的深度也减少了一层
Mix-in编程风格本身并不依赖语言处理器
Ruby语言采用的规则是: 类是单一继承的而模块则可以任意数量地做混入式处理
解决方法3: 混入式处理
一种是用于创建实例的作用,要求大而全
另一种是作为再利用单元的作用,要求小而精
类具有两种截然相反的作用
利用已有的Trait,通过改写某些方法定义新的Trait实现继承
Squeak语言引入了Trait
Scala语言也引入了Trait
PHP 5.4也引入了这种功能
Ruby2.0版引入了mix method
Trait逐渐被广泛使用
解决方法4:Trait(特性 )
3.多重继承的问题--还是有冲突
解决方法很多,具体情况具体分析
Trait正在被各个语句逐渐接受
类 具有再利用单元和实例生成器的两种作用是相反的
4.小结
先掌握细节在阅读细节
从头开始逐章手抄
对庞大信息心力交瘁时
要有明确的要做的事情
可以按时间段衡量学习的效果
12.继承和代码再利用
仅讨论程序设计语言的一些重要话题
对某些话题感兴趣可以自己去详细了解
具体问题具体分析,很难说最好
一个问题有多种解决方法
富豪式 : 图形界面专家增井俊之 为实现图形界面不惜代码的投入
YAGNI 主张不应该添加任何当前认为不需要的功能
富豪式程序设计和YAGNI
更容易关注How的问题,实际上,What(要实现什么)和Why(为什么要实现)的问题也同样重要
时间宝贵,要多用在真正需要的地方
后记
1. 增长见闻
2. 对一门语言的功能点有更清晰的认识
3. 学习编程语言,要理解语言设计者的意图,应用领域以及其特点等
个人总结
代码之髓
0 条评论
下一页
为你推荐
查看更多