Thinking In Programming Language

0x00 前言

笔者对于涉猎的语言都是草草了解,深入不敢谈。能用罢了。

即便是用过几种语言,得出的经验依然是片面的。

很多人得出一些结论,往往是样本就一个。比如说一个只用过 PHP 的人叫嚣 PHP 是最好的语言。

所以,我得出的结论,不过是一家之言罢了。

本文文章就是总结一下,我使用的一些语言的经验,我整理出一个大致的框架,方便我以后切换到其他编程语言可以更顺滑一些。

0x01 语言胡论

在我看来,一个人说他会且只精通一门编程语言是很让我不能理解的事情,在我看来,什么编程语言框架上手两三天就能去写,但是这严格意义上应该叫做能用,和精通相差甚远。那是不是说写的时间长的人经验就老道了呢?也不见得。下棋下了一辈子是烂棋篓子的人比比皆是。

那我认为什么样子的人才是懂写代码的人。

拥有良好的组织代码的能力的人。

恩,组织代码的能力,一个文件排布混乱的人,不太可能写出整齐严谨的代码,当然,人也可能是复杂的,比如,这个人写的代码挺好,但是生活住处一团糟。

当然这是题外话了,依个人经验而言,学习任何一个复杂的系统,也需要像组织一个东西一样,系统性的学习。

系统化的东西往往像《如何阅读一本书》里面描述一本书的复杂架构那样。

没有一种物质或产品是绝对简单的。所有的东西都是复杂的组合体。当你看一个整体组成复杂的东西的时候,如果只看它如何呈现一体的面貌,那是还没有掌握精髓,你还必须要明白它怎样呈现多个的面貌,但不是各自为政、互不相干的多个面貌,而是互相融合成有机体的多个面貌。如果组成的各个部分没有有机的关联,仅仅是一个集合体罢了。

这就像是一堆砖头跟一栋又砖头建造起来的房子是有区别的。而单一的房子与建造的房子也不相同。一本书就像是一栋单一的房子。一栋大厦,拥有许多房间,每层楼都有房间,有不同的尺寸和形状,不同的外观,不同的用途,这些房间是独立的,分离的,每个房间都有自己的架构和装潢设计,但却不是完全独立与分离的,这些房间使用普通门 / 拱门 / 走廊 / 楼梯串联起来的,即建筑师错位的动线架构,因为这些架构师彼此联结的,因此每一个部分在整体的使用功能上要贡献出自己的力量。否则这栋房子便是不适合居住的。

独一无二

有的人认为所有的编程语言都是一样的。

依我看,所有的编程语言都是有特点的,也是有优缺点的。

举一个我朋友的爱说的很粗俗的例子:

虽然说关了灯全都一个样,但是每一个女孩子都有是独一无二的存在。

编程语言也是一样。

  • 有的偏向于运行效率,有的偏向于开发效率。
  • 有的据说是让人编程时候感到快乐 (ruby),有的说你生命苦短,为什么不用 Python。
  • 有的偏向于 Web 开发 (PHP),有的偏向于并发操作
  • 有的是 Windows 上面自动化的小白工具 (Autohotkey)
  • 有的是据说是一次编译,到处运行 (Java)
  • 有的是亲妈平台万金油 (C#), 当然,最近也在亲妈的带领下往其他方向前进了。

编程语言往往是上面这些因素的取舍。

  • 你要运行效率,往往开发效率就会打折。
  • 你要开发效率,往往就需要堆更多的机器来提升性能。
  • 你要充分利用某个平台,往往就没有极高兼容性。
  • 你要编程语言帮你处理掉不需要考虑的问题,有的程序员就站出来了,你是不相信我控制
    内存的能力么?

有的语言生态好,比如 对于我定位于全栈工程师(其实是全干工程师)的程序员来说:

文能写虫爬数据
武能后端写网站
进能数据搞 AI
退能机器跑运维

十八般武艺武艺样样稀疏的 API 搬运工,那,那就 Python 好了。你还要啥自行车?

0x02 语言的工具链

工欲善其事必先利其器

编辑器 && IDE

大学里有个老师喜欢用 Notepad 手撸 Pascal 代码,然后拖到 IDE 里面运行。

集成开发环境 (IDE) = 编辑器 + 编译器 + 构建系统 + 调试 + 其他编辑提升(补全,重构,格式化)

到底是 IDE 好,还是 Editor 好。其实你看到这里就差不多明白了。一般情况下,选择
JetBrain 的 IDE 总是没问题的。

那么什么情况下,会选择编辑器呢?

多个

对于,这个经典的问题,可以这么回答:抓到好猫的猫,你管他是黑猫还是白猫。

那么,这个问题就变成了另一个问题。编辑器或者 IDE 可以满足我当前的开发工作流么?

Workflow

那么,我们从编辑器 + 集成环境

  • Mac Homebrew
  • 开发环境里面的 shell
  • cmake

0x03 语言的学习资源

官网的文档是最应该反复查看的东西,这是我现在依然喜欢强调的。

而官网的文档也分为四种

  • 一种是 tutorial – 用于上手对应的软件 / 编程语言
  • 一种是 guide – 用于 Topic Reading
  • 一种是 api document – 用于查看细节
  • 一种是 RTFSC ( Read The Fucking Source Code ) 阅读源码

注:把 StackOverFlow 中某个标签的 Most Votes 的答案,是除了大略看看 tutorial 之外的另一种快速熟悉入门时候的痛点的手段。

当然,其实代码写的足够好的话本身就是一种注释。

社区

社区一般情况下都会有的,但有几个网站特别值得提出来

  • 官网上一般都会放一些比较出色的社区
  • Github
  • reddit
  • 某个技术对应的 Weekly 订阅

书籍

特别值得一提的是有一个持续不断阅读到有趣的文章的方法:

  1. Google 出对应的 书 / Weekly 周报 / 博客,比如 Python Weekly 然后订阅
  2. 接着不断的查看列入优质文章的作者的文章,Github 地址啦,他关注的 Repo 啦等等等等。
  3. 然后去阅读他的代码 / 博客。

0x04 基础概念

程序 = 算法 + 数据结构

这句话当然是不全面的,这句话经典就经典在高度概括了程序中算法和数据结构的重要性,但并不影响这句话在计算机世界里面的地位。

依我看来,对我的启发是:

我会把 API 的调用和数据结构以及算法想清楚,然后才动手把代码分解成伪代码。最后写成代码。

数据类型

按照复杂性可划分为:

  • 简单类型
  • 复杂类型

按照复杂性可划分为:

  • 基本类型
  • 引用类型

按照数据结构可划分为:

  • 集合结构 : 串
  • 线性结构 : 线性表 (单链表,静态链表,循环链表,双向链表,栈,队列)
  • 树形结构 : 树(二叉树,B+ 树,红黑树)
  • 图形结构 : 图

对于一些基本的数据类型,操作为 加减乘除取余数位运算等等

对于复杂的一些数据类型,则需要对数据结构多一些了解。

比如,对队列而言,增删改查在算法复杂度上意味着什么?对机器的性能会不会有很多影响呢?
比如,对 hash 而言,增删改查在算法复杂度上意味着什么?对机器的性能会不会有很多影响呢?
比如,对字典而言,增删改查在算法复杂度上意味着什么?对机器的性能会不会有很多影响呢?
比如,对字符串而言,增删改查在算法复杂度上意味着什么?对机器的性能会不会有很多影响呢?

那字符串来说,Java 推荐使用 StringBuilder 来合并多个字符串,Python 推荐 join 多个字符串等等。

语句

  • 声明语句
  • 赋值语句
  • 条件语句

    1. 判断的时候不确定操作符优先级的时候,加括号
    2. 尽量显式判断,不要用隐式判断。
  • 循环语句

    1. Break 和 Continue

函数

  1. 传值还是传引用
  2. 参数

函数或者叫做方法,叫法不同。

函数,我有个很私人的称呼,称它为最小操作模块。

实际上,在编程的过程中,程序员用面向对象的思想进行编码的人可能真的不是很多。把一段长程序按照自己的需求进行切分成若干个函数的倒是比比皆是。

不过按照什么样子的标准来切分一段程序为多个函数,仁者见仁智者见智。

这里面需要注意的事情是:

    1. 注意传值(基本类型)和传引用(引用类型)
    1. 函数重载

对于不同的编程语言,传值(基本类型)和传引用(引用类型)基本上达成了共识。
但在实现函数重载的时候则是有所不同,

比如,Java 里面选择了多写几个函数,Python 则没有这个机制,不过,通过默认参数却可以曲线救国,实现这个机制。

递归
函数式编程
高阶函数 mapreduce/ filter / sorted / 返回函数 / 匿名函数 / 装饰器 / 偏函数

作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def outer():
a = 1
def inner():
a = 2
print(a)
outer()
# 1
def outer():
a = 1
def inner():
a = 2
print(a)
inner()
print(a)
outer()
# 1
# 1
def outer():
a = [1]
def inner():
a.append(2)
print(a)
inner()
print(a)
outer()
# [1]
# [1, 2]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var outer = function(){
var a = 1;
var inner = function(){
a = 2
};
console.log(a);
inner();
console.log(a);
}
outer()
# 1
# 2
var outer = function(){
var a = 1;
var inner = function(){
var a = 2
};
console.log(a);
inner();
console.log(a);
}
outer()
# 1
# 1

高阶函数

  • map
  • reduce
  • filter

递归

0x05 中级概念

类和对象

面向对象有三大概念:

  • 封装
  • 继承
  • 多态

模块与包

模块,这个概念,可大可小,大的时候,把一个程序说成是模块,小的时候,可以把一个文件,甚至你说这一个函数是一个模块,也行。

这里的模块指的是一个包下的函数。

错误 / 调试测试

异常处理实际上可以考验一个程序员编写代码的健壮性。

事实上来说,代码写的健壮是一个程序员必备的素养。但其实在开发过程中,出于对项目进行赶工上线,需要对程序的健壮性做出一定的取舍。并且,在编写客户端,服务端,网页前端的时候基本上都会遇到这个问题。什么时候选择健壮的程序,什么时候选择是还可以的程序。需要自己的经验。

IO 编程

正则表达式

0x06 高级概念

元编程

进程

Spawn - 产卵,为什么叫做产卵呢,因为生出大量的 child process
Spawn = fork + exec

并发 / 并行

并行架构

并行并不完全等同与多核并行

  1. bit-level 并行 : 即 8bit 与 32bit 的区别
  2. instrction-level 并行:CPU 的并行度:流水线,乱序执行,猜测执行
  3. data-level 并行:比如 GPU 在调整图像亮度的时候。
  4. task-level 并行:即多处理器,按照内存模型氛围共享内存模型与分布式内存模型

七个并发 / 并行模型

  1. 线程与锁
  2. 函数式编程
  3. 分离标志与状态
  4. Actor
  5. 通信顺序进程 (CSP)
  6. 数据级并行
  7. Lambda 架构

模型 1. 线程与锁

互斥
竞态条件
死锁

内置锁

当若干个线程进行对某个变量进行一个非原子性的操作的时候,比如
(read-modify-write), 就吹出现竞态条件。解决方式就是进行对某个变量使用同步访问 (java 中的 synchronized)。

内存可见性

当一个线程使用了多把锁的时候,就可能出现死锁。简单避开死锁的方式就是总是按照一个
全局的固定思路获取多把锁

哲学家进餐:五个哲学家围绕着桌子,两边五双筷子,如果饿了,就拿起两边的筷子吃饭。则迟早会出现一个情
况,所有的哲学家在同一时刻决定吃饭。于是两边都拿不到筷子。

最终的解决方案就是按照全局的顺序来获取多把锁。

可重入锁 Reentrant Lock

可以用 try lock 来和超时时间来避免无尽死锁。

交替锁,避免锁整个链表,而是锁上下结点。

模型 2. 函数式编程

模型 3. 分离标志与状态

模型 4. Actor

模型 5. 通信顺序进程 (CSP)

模型 6. 数据级并行

模型 7. Lambda 架构

0x05 标准库

数据结构与算法

字符串与文本

数字日期与时间

迭代器与生成器

文本处理

0x07 番外篇

0x08 调试和 Profile

8.1 测试

hash 算法

rehash | redis 渐进式 rehash

0xEE 参考


ChangeLog:

  • 2017-03-21 初始化本文
  • 2017-05-11 增加代码质量模块
  • 2018-08-29 重修文字
  • 2018-09-08 阅读七周七并发之后,针对并发章节做了补充