作者: songtianyi, 2018-07-09
毕业之后一直混迹在创业公司,经历过很多次产品从 0 到 1 的过程。产品在开发之前都会面临一个常见问题,即技术选型,首当其冲的是采用哪门编程语言来开发我们的产品。我的第一家公司是 UCloud,三位老板都来自于腾讯,所以第一批员工不少都是腾讯兵,腾讯的主要开发语言是C++,有很多现成的轮子可以用,自然 UCloud 的产品都是用C++开发的,前端则采用的是当时流行的PHP。2014 年Node大火,nodejs的非阻塞 IO 在非计算密集的高并发业务场景效果出众,开发起来也会比较快;nodejs 让前端程序员也能参与到后端开发中来,在人力分配上面自由度会高一些;运行时有 gc, 心智负担比C++小。UCloud也顺势采用了node, 先是前端部门用它重写了自己的产品逻辑,之后后端也陆续采用,包括我自己也用node重写了整个 uhost 产品的后端。2016 年golang热度很高,当时正好有个新产品要基于 docker 来开发,我们就开始尝试用golang来写后端服务, golang虽然类型特性不多,也不支持函数式编程,但是其极佳的工程实践和强劲的并发性能还是深得大家喜爱。现在这家公司,因为规模尚小,考虑到招人的难度就采用了主流的Java, 用golang来做 ssh client。在做编程语言选型的时候,除了技术本身还有很多其他因素,这些因素要针对具体场景来分析,所以本文从通用性的角度考虑,只讲技术层面的内容。
编程语言往大了讲是很复杂的,语法,类型系统,编译原理,编译器,解释器,内存模型,并发模型,工具链等等,单拿一点出来都能写一本书。限于篇幅,本文要介绍的内容限定在类型系统(type systems),语法(syntax)和编程范式(programming paradigms)上。
什么是类型?在软件执行的过程中,变量可以为很多值,定义变量的边界的描述即类型。变量可以被赋予类型(即变量有边界)的语言称为类型语言(typed language),无类型语言(untyped language)没有类型,或者说只有一个全局类型,能够存储所有的值。 类型语言我们见得多了,无类型的呢?lambda 演算(pure λ-calculus)是无类型的,汇编和 LISP 也是无类型的.
变量类型的指定可以是显式的
// golang
var foo int
也可以是隐式的
-- haskell
fac :: Int -> Int -- 这一行可以省略
fac 0 = 1
fac n = n * fac (n - 1)
fac 0 = 1
fac n = n * fac (n - 1)
类型系统会自动赋予变量类型,这种行为称为类型推断(type inference)。
类型系统是类型语言的首要组成部分。类型系统的职责之一是跟踪变量的类型,判断代码是否满足类型约束,这种行为称为类型检查(typechecking), 类型检查是保证程序稳定运行的手段,同时又分为运行时检查(runtime checks)和静态检查(static checks), 运行时检查也叫动态检查(dynamic checking).
类型系统做了静态检查,还有必要做动态检查嘛?有,比如数组的边界检查,就必须在运行时做。运行时的类型检查会导致程序运行终止(fail-stop),那为什么还要检查呢?让它运行到无法继续执行为止不就好了?类型检查虽然会出错,但是阻止了更恶劣的错误(untrapped errors)的发生,比如保证 gc 等机制能够正常运转,让程序能够更平滑地退出。动态检查的缺点是会导致 fail-stop,也会消耗资源,影响性能,所以通常我们认为拥有静态检查的类型系统的语言会更稳定高效。但是静态检查就足够安全了吗?不一定,因为某些语言在静态检查时没有检查一些危险操作,比如C语言中指针的运算和转换,这类语言称为weakly checked, 反之, 程序在编译期间能够尽可能发现所有的类型错误, 称为strongly checked.
那么延伸一下,怎么区分一门语言是weakly checked, 还是strongly checked? 以下几点可以作为判断的依据。
可以进行隐式类型转换的语言属于weakly checked, 如 c++
int a = 3;
double b = 4.5;
a + b; // a 将会被自动转换为 double 类型,转换的结果和 b 进行加法操作
允许指针运算的语言属于weakly checked, 比如 c
int num [] = {1,3,6,8,10,15,22};
int *pointer = num;
printf("*pointer:%d\n",*pointer);
pointer++;
printf("*pointer(p++):%d\n",*pointer);
union type即联合类型,之后的内容会介绍。联合类型中的不同类型的值会被存储在同一地址,这也是不稳定因素之一,所以拥有untagged unions的语言属于weakly checked. 和untagged相对的是tagged union type, tagged union会用 tag 字段显式地标记当前正在使用的类型,因此要相对安全一些。
一般弱类型语言属于weakly checked
除此之外,往往是通过语言之间的比较来进行判断,并没有明显的界限。类型系统除了提供类型检查,保证程序的安全性之外,还可以提供信息给编译器,以优化执行效率。同时,类型系统是对现实世界的抽象,可读性高,也是更高级别抽象(如泛型,OOPs)的基础。所以,无类型的语言在选型时基本会被排除掉。
在语言的抉择上, 静态检查
, 动态检查
和 检查范围
这几个角度影响的是程序的稳定性和执行效率,那么开发效率呢?此时要引入另外一个维度, 强类型
和 弱类型
,强类型是指一旦被指定类型,不可变,弱类型则可变,可变也分为隐式和显式两种。
// js 弱类型,隐式可变
x = 1
y = "2"
z = x + y
弱类型提升了我们的开发效率。
总结,在语言的抉择上,我们可以从类型系统的静态检查,动态检查,检查范围及类型是否可变这几个维度来考虑,在开发效率,执行效率及安全性之间做权衡。无类型的语言可以很安全,但是可维护性差,基本被排除在系统开发之外。
看到这里,你会发现文中并没有讲什么是动态类型语言,静态类型语言。因为这个维度并不是影响我们抉择的本质因素。动态类型语言其实指的就是只有动态检查的语言,静态类型语言指拥有静态检查的语言,此类型非彼类型,个人觉得是翻译的锅。
类型推断(type inference)是类型系统中推测一段代码(声明,表达式等)的类型的过程。类型推断让我们在编写代码时能够略去类型的声明,并且不会影响到类型检查。如果值的类型仅在运行时才能被确定,这类语言被称为动态语言(dynamically typed), 同样的,如果值的类型仅在编译时被确定,这类语言被称为静态语言(statically typed).
类型理论涉及到的内容和类型系统会有部分重叠,你可以理解为,类型理论是服务于类型系统的,即,我们使用了哪些理论来构建我们的类型系统,或者说该语言的类型系统实现了哪些特性。一门语言的类型系统一般只会实现其中的某几种特性,这也是语言之争的根源。
polymorphism 翻译为多态性,但不单单指面向对象里的多态,而是指类型系统里的多态性质。编译时多态,是在编译时就能推导出类型或调用关系,宏也是一种编译时多态。运行时多态的实现依赖于虚函数机制(virtual function), 是在运行时确定调用关系。多态性质的引入可以提高代码的复用率。
// java
public int Add(int a, int b) {
return a + b;
}
public String Add(String a, String b, String c) {
return a + b +c;
}
// java
...
public class ObjectsServiceFactory<T> {
public T save(T o) throws Exception {
try {
return repository.save(o);
} catch (Exception e) {
throw new DatabaseOperationException(String.format("save object %s failed, %s", o.toString(), e.getMessage()));
}
}
public void delete(T o) {
repository.delete(o);
}
}
...
ObjectsServiceFactory s = new ObjectsServiceFactory<String>();
ObjectsServiceFactory i = new ObjectsServiceFactory<Integer>();
s.save("demo");
i.save(100)
S <: T
, 意味着在任何需要使用 T 类型的环境中,都可以安全地使用 S 类型的对象。但要区分的是,这里说的子类型并不是面向对象继承概念里的子类,子类型和父类型(super type)描述的是类型之间的关系,而继承反应的是一类对象可以从另一类对象创造出来,是语言特性的实现,主要目的是重用代码。之所以混淆是因为在 C++等语言中,通常将两者结合起来以实现 OOP。子类型也称为接口(interface)继承,对象继承则称作实现继承,在 Java 中,这两者在语法上是有明显的区分的(interface 和 class)。从多态性质实现的角度讲,subtyping 属于运行时多态(dynamic polymorphism).// c++
#include<iostream>
using namespace std;
class Animal {
public :
virtual void shout() = 0;
virtual ~Animal(){}
};
class Dog :public Animal {
public:
virtual void shout(){ cout << "dog"<<endl; }
};
class Cat :public Animal {
public:
virtual void shout(){ cout << "cat"<<endl; }
};
class Bird : public Animal {
public:
virtual void shout(){ cout << "bird"<<endl; }
};
int main() {
Animal * animal1 = new Dog;
Animal * animal2 = new Cat;
Animal * animal3 = new Bird;
animal1->shout();
animal2->shout();
animal3->shout();
delete(animal1);
delete(animal2);
delete(animal3);
return 0;
}
关于 subtyping 还有更细致的划分,从类型系统的技术实现角度分为nominal subtyping和structural subtyping, 在阐述这两个概念之前,我们先理解类型系统是如何区分两个不同类型的。
一种方式是通过类型名称来区分,即,当且仅当两个类型的命名相同时,它们属于同一类型。例如:
// c
struct A { int a;}
struct B { int a;}
在上面的 C 代码中,类型名称显然不同,类型 A 和类型 B 属于不同类型,尽管它们的成员(property/field/member)是完全相同的, 使用这种区分规则的类型系统属于nominal type system。至于,当我们为类型定义一个别名的时候,类型系统是否认为这是一个新的类型要看类型系统的具体实现,如果别名仅仅是语法糖的话,此时不同的名称仍然代表同一个类型。
与nominal type system相对的是structural type system(property-based type system), 从字面的意思我们大概能猜出,类型的区分是通过类型的定义即类型的结构来区分的:
// pseudo
class Foo {
method(input: string): number { ... }
}
class Bar {
method(input: string): number { ... }
}
let foo: Foo = new Bar(); // OK
上面的代码中,Foo 和 Bar 的类型是相同的,因为他们的结构相同。
接着,我们来看这两种类型区分方式和 subtyping 的关系。
S <: T
,使用这种规则的 subtyping 属于nominal subtyping。比如 Julia: abstract type Graph <: Any end
struct Circle <: Graph
radius ::Float64
x ::Int64
y ::Int64
end
struct Rec <: Graph
width ::Float64
height ::Float64
x ::Int64
y ::Int64
end
function area(r ::Circle) ::Float64
return pi * r.radius^2
end
function area(g ::Rec)
return g.x*g.y
end
println(area(Circle(1.0, 1, 1)))
println(area(Rec(1.0, 1.0, 2, 2)))
```
上述代码通过 `<:` 显式地声明了这种关系, `Circle <: Graph` 。C++, Java 也是*nominal subtyping*
* *Structural subtyping*: 参照*structural typing*的规则,类型之间的子/父关系是通过类型的结构来区分的,使用这种规则的 subtyping 属于*structural subtyping*。Golang 的 interface 属于此类,interface 里声明的函数(feature)在它的子类型中都能找到对应的实现,至于匹配的规则,依赖于类型系统的具体实现。*structural subtyping*相对*nominal subtyping*要更加灵活。
```go
// golang
package main
import "fmt"
type A interface {
Shout()
}
type Girl struct {}
func (s *Girl) Shout() {
fmt.Println("greetings!")
}
type Boy struct {}
func (s *Boy) Shout() {
fmt.Println("66666")
}
func do(who A) {
who.Shout()
}
func main() {
do(&Girl{})
do(&Boy{})
}
```
上面的 Go 代码中,Girl 和 Boy 都没有被显式地声明为 A 的子类型,但它们都实现了 `Shout()` , 从结构上对比,他们都属于 A 的子类型。
04. *Row polymorphism*: 也叫 duck typing,针对结构体类型,从功能(purpose)的角度对类型归类。通常,对象是根据它们的类型来确定彼此之间的关系,比如 subtyping 中的父类/子类关系,而 duck typing 是通过函数,如果它们实现了相同的函数,就认为它们是同一类。
> If it walks like a duck and it quacks like a duck, then it must be a duck.
如果它走路像鸭子(实现了 walk()函数),也会像鸭子一样发出嘎嘎声(实现了 gaga())函数,那它就是一只鸭子(属于同一类型)。
```python
# python
class Duck:
def fly(self):
print("Duck flying")
class Airplane:
def fly(self):
print("Airplane flying")
class Whale:
def swim(self):
print("Whale swimming")
def lift_off(entity):
entity.fly()
duck = Duck()
airplane = Airplane()
whale = Whale()
lift_off(duck) # prints `Duck flying`
lift_off(airplane) # prints `Airplane flying`
lift_off(whale) # Throws the error `'Whale' object has no attribute 'fly'`
Golang 的 interface 和 duck typing 有点像,但是严格来说并不算,这里引用 Rob Pike 的在 Twitter 上的原话来佐证:
Go has structural typing, not duck typing. Full interface satisfaction is checked and required.
Golang interface 勉强算作编译时的 duck typing,因为传统定义的 duck typing 发生在运行时,且没有显式的interface声明,上面的 python 示例就是典型的 duck typing。区分的原则是,duck typing 不以类型来确定关系,而是通过函数来确定,和 subtyping 是截然不同的。
依赖类型(或依存类型,dependent type)是指依赖于值的类型, 此特性通过极其丰富的类型表达能力使得程序得以借助类型的形式被检查,从而有效减少程序错误。依赖类型的两个常见实例是依赖函数类型(又称依赖乘积类型, Π-类型)和依赖值对类型(又称依赖总和类型、Σ-类型)。[4]
一个依赖函数的返回值的类型可以依赖于某个参数的具体值,而非仅仅参数的类型,例如,一个输入参数为整型值 n 的函数可能返回一个长度为 n 的数组
// Idris
// 连接两个列表
// Vect n a 是依赖函数类型,a 是列表元素的类型,n 是输入参数,Vect n a 返回一个长度为 n 的列表
app : Vect n a -> Vect m a -> Vect (n + m) a
一个依赖值对类型中的第二个值可以依赖于第一个值,例如,可表示一对这样的值:它由一对整数组成,其中的第二个数总是大于第一个数。
def do(i : {i:Int | i<=j}, j : Int) :=
// do something
do(10, 1) // compile error
do(1, 10) // ok
以依赖类型系统为基础的编程语言大多同时也作为构造证明与可验证程序的辅助工具而存在,如 Coq 和 Agda(但并非所有证明辅助工具都以类型论为基础)。近年来,一些以通用和系统编程为目的的编程语言被设计出来,如 Idris。
Linear types 的思想来源于 Linear Logic, 它确保对象在程序运行期间有且仅有一个它的引用,这种类型用来描述不能被修改的值,比如文件描述符。linear 类型系统允许引用,但不允许别名(被多个变量引用), 类似于 C++的unique_ptr指针, 只能被移动,不能被复制。
一个 intersection type(交叉类型)是多个 type 的结合, 以此,你能够得到一个包含多个类型的所有成员(members)的新类型!比如,现有三个类 Person, Serializable 和 Loggable, 新的类型 T = Person & Serializable & Loggable, 那么类型 T 拥有 Person,Serializable 及 Loggable 的所有成员。
// TypeScript mixin example
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
console.log("papapa!");
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
学过 C 语言的对此类型并不陌生,和 intersection type 类似,一个 union type 可以为多个类型,但是在任意时刻,它的值的类型只能是其中所有类型中的一种。
//c
union a_bc {
int i;
char mm;
};
TypeScript里的联合类型用竖线 |
来分隔每个类型,所以 value : number | string | boolean
表示一个值可以是 number
,或 string
,或 boolean
。
/**
* TypeScript
* Takes a string and adds "padding" to the left.
* If 'padding' is a string, then 'padding' is appended to the left side.
* If 'padding' is a number, then that number of spaces is added to the left side.
*/
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
}
let indentedString = padLeft("Hello world", true); // errors during compilation
let ok = padLeft("Hello world", 0) // compile ok
在理解存在类型(existential type)之前,我们先看下 java 的类型通配符 ?
, 它代表一个未知类型.
通过声明通配的上限(父类)来匹配,如果你的函数入参可能是 List<Integer>, List<double>或者 List<number> ,你可以使用 ?
声明
public static void add(List<? extends Number> list)
Number 的所有子类都可以作为入参, 例如:
//Java program to demonstrate Upper Bounded Wildcards
import java.util.Arrays;
import java.util.List;
class WildcardDemo
{
public static void main(String[] args)
{
//Upper Bounded Integer List
List<Integer> list1= Arrays.asList(4,5,6,7);
//printing the sum of elements in list
System.out.println("Total sum is:"+sum(list1));
//Double list
List<Double> list2=Arrays.asList(4.1,5.1,6.1);
//printing the sum of elements in list
System.out.print("Total sum is:"+sum(list2));
}
private static double sum(List<? extends Number> list)
{
double sum=0.0;
for (Number i: list)
{
sum+=i.doubleValue();
}
return sum;
}
}
省略写法也是Upper Bounded
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // compile error
当类型不可推断时,上限是Object 即
List<? extends Object>
通过声明通配的下限(子类)来匹配, 比如函数的入参声明为:
List<? super Integer>
那么,Integer 类型的所有父类都可以作为入参。例如:
//Java program to demonstrate Lower Bounded Wildcards
import java.util.Arrays;
import java.util.List;
class WildcardDemo
{
public static void main(String[] args)
{
//Lower Bounded Integer List
List<Integer> list1= Arrays.asList(4,5,6,7);
//Integer list object is being passed
printOnlyIntegerClassorSuperClass(list1);
//Number list
List<Number> list2= Arrays.asList(4,5,6,7);
//Integer list object is being passed
printOnlyIntegerClassorSuperClass(list2);
}
public static void printOnlyIntegerClassorSuperClass(List<? super Integer> list)
{
System.out.println(list);
}
那 java 的类型通配符和存在类型有什么关系呢?先看看 scala 的缔造者Martin Odersky 对 scala 引入存在类型的回答
Bill Venners: Existential types were added to Scala relatively recently. The justification I heard for existentential types was that they allow you to map all Java types, in particular Java's wildcard types, to Scala types. Are existential types larger than that? Are they a superset of Java's wildcard types? And is there any other reason for them that people should know about?
Martin Odersky: It is hard to say because people don't really have a good conception of what wildcards are. The original wildcard design by Atsushi Igarashi and Mirko Viroli was inspired by existential types. In fact the original paper had an encoding in existential types. But then when the actual final design came out in Java, this connection got lost a little bit. So we don't really know the status of these wildcard types right now.
Martin Odersky 在 scala 邮件组里的回答
The original Java wildcard types (as described in the ECOOP paper by Igarashi and Viroli) were indeed just shorthands for existential types. I am told and I have read in the FOOL ’05 paper on Wild FJ that the final version of wildcards has some subtle differences with existential types. I would not know exactly in what sense (their formalism is too far removed from classical existential types to be able to pinpoint the difference), but maybe a careful read of the Wild FJ paper would shed some light on it.
可见,java 类型通配符的设计思想来源于存在类型,但有些不同。scala 引入存在类型是为了兼容 java 和 jvm,所以会比 ?
更强大。那么既然区别不大,理解了类型通配即理解了存在类型。这是一个曲线救国的方式。
接下来,我们从数学定义的角度来理解。
existential type里的 existential 来源于存在量词 ∃
, ∃
在谓词逻辑中的解释:
∃ x: P(x) 表示存在至少一个 x 使得 P(x) 为真。
存在类型的公式化表示:
T = ∃ X { X a; int f(X); }
类型 X 是存在类型, 即存在一个类型 X,满足此表达式,在编程语言里我们称之为可实现。存在类型适合用来定义接口,不论是模块之间还是语言之间。
这里要提下泛型(即前面讲到的Parametric polymorphism, 也叫Universal type), 以避免混淆。Universal type中的 universal 来源于全称量词 ∀
, ∀
在谓词逻辑中的解释:
∀ x: P(x) 表示 P(x) 对于所有 x 为真。
泛型的公式化表示:
T = ∀ X { X a; int f(X); }
即,对于所有类型 X,满足此表达式。
在语言学里有三个基本概念(也是三个分支),syntax, semantics, pragmatics. 即语法,语义(编译结果)和语用(最佳实践, 标准库, 生态)。这里主要讲语法层面。
一门语言的规范,首先是类型(type), 声明(statements), 表达式(expressions)等, 然后是作用域(scoping)。前面的内容介绍了类型系统, 那么该类型系统定义了哪些类型,实现了哪些特性是我们首先要了解的。通常一门语言的语法规范会以 grammar[6]的形式来定义,例如 golang 中对于浮点数字的描述:
float_lit = decimals "." [ decimals ] [ exponent ] |
decimals exponent |
"." decimals [ exponent ] .
decimals = decimal_digit { decimal_digit } .
exponent = ( "e" | "E" ) [ "+" | "-" ] decimals .
这段文本描述了浮点数字的书写规则,也是一棵语法树。grammar对于没有学过的人来说有些难懂,所以本文中用类型实例的方式表示。
即基础类型, 或者叫内置(builtin)类型, 是程序以及复合类型的创建基础。
基础类型 | 解释 |
---|---|
int, short, int8, uint8, byte, int16, uint16, int32, rune, uint32, int64, uint64, long | 整数, 包含不同进制的整数, 包含不同表示范围的整数, 包含有符号和无符号。 ex . int a = 100; int b = 0xff; uint8 c = 34; PS. Rust 的 i8, u8 等写法不赘述. |
float, float32, float64, double, long double | 浮点数, 包含不同的计数方式, 不同的范围。ex. float a = 5.6; float64 f = .12345E+5 |
bool, boolean | 布尔型, true or false. ex . bool b = false |
char, signed char, unsigned char, char16_t, char32_t, wchar_t, string, symbol | 字符/字符串,string 在某些语言里是复合类型. ex. char a = 'c'; string b = "wango"; symbol 是 ruby 里不可修改的字符串类型 |
complex64, complex128 | 复数,部分语言有. ex . var x complex128 = complex(1, 2) |
pointer, reference | 指针/引用. ex . int *p = 0x22ae4f; var p *int; *p= 1; q := &p |
error | 错误信息,少部分语言有,比如 golang. ex : var e error |
null, nil, undefined, void | 空值, 在不同的语言里要做区分, 这里仅作归类 |
即复合类型, 也叫Composite types. 复合类型可以由基础类型和复合类型所构成。
复合类型 | 解释 |
---|---|
array, slice, list, vector | 数组, ex . int *a = new array[3] |
struct, class | 结构体/类. ex . struct{a:b, c:d} |
map | key-value, 包含不同的实现方式. ex . map[string]int |
interface | 接口. ex . interface a { f()} |
channel | 信道. ex . var a chan int |
function | 函数 |
enum | 枚举, 在某些语言里是基础类型, 比如 TypeScript, 某些不是, 比如 Java |
union | 联合, let a : string | number = 10; a = '10' |
table | hash, 在某些语言里是基础类型,比如 Lua,在某些语言里是复合类型, 比如 Java |
set | 集合. ex . const s = new Set([1, 2, 3, 4, 5]); |
Object | 对象 |
tuple | 元组, let x: [string, number]; x 是个元组,包含不同类型的元素, scala, typescript 都有这种元组类型 |
statement 的直译是声明,但在这里按照代码的逻辑单元来理解,一个 statement 是逻辑单元的开始或结束。在书籍中,通常描述为 xxx 语句,比如if语句。
空语句, 不做任何事情。grammar 规则:
EmptyStatement:
;
标签语句, 通常作为goto, break, continue 的目标,例如 c/c++里的goto语句,一个单词加上冒号即是labeled statement。下面代码中的标签语句*LOOP:*即是goto的目标。
`
/*c*/
#include <stdio.h>
int main () {
/* local variable definition */
int a = 10;
/* do loop execution */
LOOP:do {
if( a == 15) {
/* skip the iteration */
a = a + 1;
goto LOOP;
}
printf("value of a: %d\n", a);
a++;
}while( a < 20 );
return
它的 grammar 规则为:
LabeledStmt = Label ":" Statement .
Label = identifier .
labeled statement作为goto的目标大家应该见的很多,这里再举一个作为continue的目标的例子,便于理解。
// golang
guestList := []string{"bill", "jill", "joan"}
arrived := []string{"sally", "jill", "joan"}
CheckList:
for _, guest := range guestList {
for _, person := range arrived {
fmt.Printf("Guest[%s] Person[%s]\n", guest, person)
if person == guest {
fmt.Printf("Let %s In\n", person)
continue CheckList
}
}
}
某些expression(表达式), 比如赋值,函数调用等,可以作为一个statement,称之为expression statement
我们常说的if条件语句,也叫If-then-else, 如果条件满足则执行此逻辑, 否则执行它的else(如果存在)逻辑.
// golang
if x > max {
x = max
}
断言语句,assert 加真假值表达式, 如果表达式结果为false, 程序退出。
// c
assert( size <= LIMIT );
不是所有语言都有断言语句.
switch条件语句,判断表达式的值,满足不同的条件执行时执行不同的逻辑, 当所有条件都不满足时,执行默认逻辑(如果存在).
switch (expression) {
case n:
code block
break;
case n:
code block
break;
default:
code block
}
对于 golang, switch statement分成了两类, 一类是常规的expression switch(上面的例子), 一类是type switch(下面的例子)
// golang type switch
switch i := x.(type) {
case nil:
printString("x is nil") // type of i is type of x (interface{})
case int:
printInt(i) // type of i is int
case float64:
printFloat64(i) // type of i is float64
case func(int) float64:
printFunction(i) // type of i is func(int) float64
case bool, string:
printString("type is bool or string") // type of i is type of x (interface{})
default:
printString("don't know the type") // type of i is type of x (interface{})
}
while循环语句, 重复判断 bool 表达式的值,如果为真则执行,直到表达式的值为假
// c
while(condition)
{
statement(s);
}
tip: 在 golang 里,for语句加上 bool 表达式,可以实现while语句
// golang
for {
// code block
}
for a < b {
f()
}
do语句,和while配合使用,先执行一次逻辑,再判断条件
// c
#include <stdio.h>
int main(void){
int sum=0,i;
scanf("%d",&i);
do{
sum=sum+i;
i++;
}
while(i<=10);
printf("sum=%d",sum);
return 0;
}
for语句, 一般由三部分组成,初始化表达式,条件表达式和一个末尾表达式(post statement),初始化表达式只在开始时执行一次,条件表达式用来判断本次执行是否退出,末尾表达式在每次for语句的代码逻辑执行完后都会执行。
// golang
for i := 0; i < 10; i++ {
f(i)
}
for语句常用来迭代,于是出现了多个变种,for-in常见于脚本语言,如TypeScipt,Groovy, 用于迭代可枚举的(enumerable types)数据类型
// JavaScript
var person = {
fname: "John",
lname: "Doe",
age: 25
};
var text = "";
var x;
for (x in person) {
text += person[x] + " ";
}
这里,x 被赋值为 person 这个 map 的 key。当遍历的对象是 map 时,x 为 key,当遍历的对象是数组时,x 为索引下标
for语句变种, 类似于for-in, 用来迭代可迭代的(iterable)数据类型, 比如数组和字符串
// JavaScript
function* foo() {
yield 1;
yield 2;
}
for (let v of foo()) {
console.log(v);
// expected output: 1
break; // closes iterator, triggers return
}
这里变量 v 被赋值为 foo 函数返回的对象。被赋值的变量总是可迭代类型里的元素。
For-in vs For-of
for..in |
for..of |
|
---|---|---|
Applies to | Enumerable Properties | Iterable Collections |
Use with Objects? | Yes | No |
Use with Arrays? | Yes, but not advised | Yes |
Use with Strings? | Yes, but not advised | Yes |
for语句变种,在 golang 中用来迭代数组, 或者 map
// golang
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
跳出语句,用于立即跳出一个逻辑单元,当不配合labeled statement使用时,立即(abruptly)跳出最里层的一个封闭(enclosing)逻辑单元, 如switch, do, while, for. 当配合labeled statement使用时,立即跳出 label 标定的层级的封闭逻辑单元。
// c
#include <stdio.h>
int main ()
{
/* 局部变量定义 */
int a = 10;
/* while 循环执行 */
while( a < 20 )
{
printf("a 的值: %d\n", a);
a++;
if( a > 15)
{
/* 使用 break 语句终止循环 */
break;
}
}
return 0;
}
labeled break已在labeled statement中说明,这里不再赘述.
continue语句,立即结束当前层级的逻辑,跳转到for循环的末尾表达式,开始下一次循环.
// golang
package main
import "fmt"
func main() {
rows := []int{1, 3, 5}
colunms := []int{2, 4, 6}
for _, row := range rows {
for _, column := range colunms {
if column == 4 {
continue
}
fmt.Printf("%d-%d\n", row, column)
}
}
}
上述代码的输出为:
1-2
1-6
3-2
3-6
5-2
5-6
continue和labeled statement配合使用时,不仅会结束当前层级的逻辑,还会跳转到label标签指定的位置。我们看下下面的代码逻辑.
// golang
package main
import "fmt"
func main() {
rows := []int{1, 3, 5}
colunms := []int{2, 4, 6}
RowLoop:
for _, row := range rows {
for _, column := range colunms {
if column == 4 {
continue RowLoop
}
fmt.Printf("%d-%d\n", row, column)
}
}
}
当 column 等于 4 时,结束逻辑,此时不是跳转到当前层级的post statement, 而是跳转到 RawLoop, 所以输出结果应该为:
1-2
3-2
5-2
不仅 4 没有输出,6 也被continue掉了.
return语句跳出当前函数,回到函数的调用方, 同时将一个或者多个返回值传给调用方。本应出现在最后一行的return语句,在没有返回值的情况下,可以省略。Groovy语言的return语句是可选的。
// sum method
def static sum(n, closure) {
for(int i = 2; i <= n; i += 2) {
closure(i)
}
// return 2, 可以简写成 2
2
}
在一些语言中比如Java, JavaScript等使用throw语句来抛出错误,以便上层的调用方能够通过try-catch-throw的方式捕捉并处理。未捕捉的throw语句会导致线程/进程终止。对于Java, throw的的对象必须是Exception或者其子类,对于JavaScript, throw 的对象可以是任意类型.
// JavasScript
function getRectArea(width, height) {
if (isNaN(width) || isNaN(height)) {
throw "Parameter is not a number!";
}
}
try {
getRectArea(3, 'A');
} catch (e) {
console.log(e);
// expected output: "Parameter is not a number!"
}
goto语句和labeled statement配合使用,用于逻辑跳转,程序执行流程会直接跳转到标签处. goto statement只有部分语言提供,而且写法也有不同,比如对于golang, labeled statement必须在goto statement之前, 而C语言则无此限制。
// c
#include <stdio.h>
int main ()
{
/* 局部变量定义 */
int a = 10;
/* do 循环执行 */
LOOP:do
{
if( a == 15)
{
/* 跳过迭代 */
a = a + 1;
goto LOOP;
}
printf("a 的值: %d\n", a);
a++;
}while( a < 20 );
return 0;
}
以上都是常见的语句,除此之外,语言也会有实现一些非常规的语句,比如golang的defer语句。
在介绍表达式之前,先介绍它的组成元素之一,操作符。其中,操作符分为一元(unary)操作符,二元(binary)操作符和三元操作符. 优先级决定了在多个操作符同时出现时,先使用哪个来求值。数字越大,优先级越高。
符号 | 解释 |
---|---|
+ | 加号, a := 1 + 4 |
- | 减号, b := 1 - 4 |
++ | 累加 1, int b = 0; int a = b++; |
-- | 累减 1, ini b = 0; int a = b—; |
~ | 按位取反, ~ 0111(十进制 7) = 1000(十进制 8) |
! | 非, !isEmpty(o) |
^ | 异或, 2 ^ 3 |
& | 按位与, 0101 & 0011 = 0001 |
<- | 赋值, a <- 1, 在 golang 中用于操作 channel |
| | 按位或, 0101(十进制 5) | 0011(十进制 3) = 0111(十进制 7) |
符号 | 解释 |
---|---|
* | 乘法, c = 2 * 10 = 20 |
/ | 除法, c = 10 / 2 = 5 |
% | 取模, c = 10 % 2 = 0 |
<< | 位运算左移, 在 c++中也用来操作输入流 |
>> | 位运算右移, 在 c++中也用来操作输出流 |
& | 按位与 |
&^ | |
== | 等于, 1 == 2 |
!= | 不等于, 1 != 2 |
< | 小与 |
<= | 小于等于 |
> | 大于 |
>= | 大于等于 |
|| | 或 |
&& | 且 |
+ | 加法 |
- | 减法 |
| | 按位或 |
^ | 按位异或 |
在计算机中也叫条件运算符(conditional operator)
符号 | 解释 |
---|---|
? : | a > b ? a : b |
在编程语言里一个表达式(expression[7])是由一个或多个常量,变量,操作符或函数组成。通过对表达式求值(evaluate)来得到一个新的值,这个新的值可以是基础类型,也可以是复合类型。表达式在运算时,会进行类型推断和类型检查。从之前讲的内容可以看出,statements的作用主要是控制代码逻辑的执行顺序,expressions则是具体的代码逻辑.
// golang
c := a + b
2 + 3
3 != 4
// golang
v := f(1)
a[x]
表达式并没有严格的分类,各个语言也不尽相同,这里仅列举了部分例子来说明。
即作用域, 作用域是名称(比如变量的声明)和其实体(entity, 比如变量的定义)的绑定规则。作用域约束了实体的作用范围,保证程序是无歧义的。
实体仅在表达式内可用。
// c
({ int x = f(); x * x; })
临时变量x接受函数的返回值并平方,这样避免两次调用函数f.
通常编程语言都会使用花括号 {}
来将代码包裹成块(block), 在 block 内声明的实体,仅在 block 内有效。
// golang
{
var a int
b := a
}
b := a // compile error
// golang
{
var a int
b := a
}
b := 1 // ok
在函数内声明的实体,仅在函数内有效。
def square(n):
return n * n
def sum_of_squares(n):
total = 0
i = 0
while i <= n:
total += square(i)
i += 1
return total
为了不和block scope混淆,这里用python的例子。
在代码文件内声明的全局变量,仅在当前文件内有效。
在某些现代语言中,一个实体可以在一个模块内的各个文件内有效,比如golang. 部分语言,一个文件就是一个独立的 module,此时,也属于file scope.
在所有模块,所有文件内都有效的实体称为全局实体,此类作用域属于global scope. 在编程实践当中,应尽量避免使用。
一个复杂程序一般会由多个包(或者叫模块)组成, 这种机制能让程序的结构和逻辑更加清晰可读,提高代码的复用能力,也可以借助module scope来避免同名之间的冲突。这里仅列举几种常见的包引入方式。
// Java 包名是一个层级结构
import org.apache.commons.net.util.SubnetUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
Subnetutils sbt = new Subnetutils("xx")
// python
from sys import argv
顺便说下 python 里from和常规import之间,在包的的使用上会有差异。
>>> from os import path
>>> path.
File "<stdin>", line 1
path.
^
SyntaxError: invalid syntax
>>> path
<module 'posixpath' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/posixpath.pyc'>
>>> os.path
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'os' is not defined
>>> import os.path
>>> os.path
<module 'posixpath' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/posixpath.pyc'>
>>>
// node
var circle = require('./circle.js');
console.log('The area of a circle of radius 4 is ' + circle.area(4));
// golang
import (
"fmt"
"github.com/xx/xx"
)
// TypeScript
import { cube, foo } from './mylib';
console.log(cube(3));
console.log(foo);
// c/c++
#include "my_header.h"
int foo(char* name) {
//do stuff
return 0;
}
编程范式(programming paradigms),是在编程的理论与实践当中提炼出的概念模型。
Programming Paradigm: A conceptual model underlying the theories and practice of programming
即指令式编程。程序由一系列指令和流程控制语句组成,运行过程中指令不断改变程序的状态,由此达到最终的编码意图。IP范式会显式地指定代码的执行流程,以及运算逻辑。汇编是典型的使用IP范式的编程语言。
result = []
i = 0
start:
numPeople = length(people)
if i >= numPeople goto finished
p = people[i]
nameLength = length(p.name)
if nameLength <= 5 goto nextOne
upperName = toUpper(p.name)
addToList(result, upperName)
nextOne:
i = i + 1
goto start
finished:
return sort(result)
即结构化编程,在IP的基础上,我们可以将用goto来控制流程的代码,以for语句,while语句等此类结构化的代码块(block structure)组织起来,使得代码的可读性更高,那么此种编码方式即为结构化范式。SP是现代语言都支持的一种基础范式。
result = [];
for i = 0; i < length(people); i++ {
p = people[i];
if length(p.name)) > 5 {
addToList(result, toUpper(p.name));
}
}
return sort(result);
即过程式编程,单看中文可能难以理解,procedure来源于procedure call, 即函数调用,主要是因为PP在IP的基础上引入了函数及函数调用, 将可提炼的逻辑用函数封装起来,以复用代码和提高可读性。
OOP(Object-oriented Programming)
即我们常说的面向对象编程。在SP和PP的范畴里,数据类型是松散的,数据结构和算法之间也是松散的,而OOP则提供了一种类似于人类对现实世界建模的方法,对二进制世界的类型和逻辑进行建模和封装,并在此基础上提供了更多的类型和语法特性。OOP的优点简列如下(封装,继承,多态):
即,描述性范式。和IP相反,此类语言只描述最终的编码意图,不描述达到意图的过程。举个例子,如何用程序来回答你是怎么回家的?
IP:
Go out of the north exit of the parking lot and take a left. Get on I-15 south until you get to the Bangerter Highway exit. Take a right off the exit like you’re going to Ikea. Go straight and take a right at the first light. Continue through the next light then take your next left. My house is #298.
DP:
My address is 298 West Immutable Alley, Draper Utah 84020
典型的DP范式语言如SQL, 仅描述目的,达到目的的逻辑被隐藏。
SELECT * FROM Users WHERE Country=’Mexico’;
即逻辑编程,它属于DP的范畴。逻辑编程的要点是将数学中的逻辑风格带入计算机程序设计之中。它设置匹配规则来解决问题(rule-based),而非设置步骤来解决问题, 即事实+规则=结果。Prolog是典型的LP范式语言,此类语言主要应用在人工智能,专家系统等领域。
即函数式编程,FP属于DP的子集, 在函数式编程里,函数和其他数据类型一样,可以作为类型定义变量,可以作为入参和返回值,代码里的SP逻辑尤其是控制流逻辑,被隐藏了起来。 假设我们要编写一个函数,将一个数组的每个元素都乘以 2,PP风格的代码如下:
// TypeScript
function double (arr) {
let results = []
for (let i = 0; i < arr.length; i++){
results.push(arr[i] * 2)
}
return results
}
上述代码,详细地写明了整个计算过程,包括迭代过程和计算方法,所以IP范畴的范式会详细描述计算机是如何完成这件事的
First do this, then do that.
FP则不会描述数组是如何迭代的,也不会显式地修改变量, 仅仅描述了我们想要什么,我们想要将元素乘以 2, item * 2
就是核心逻辑,不需要开发者关心它是怎么迭代和修改变量的。
function double (arr) {
return arr.map((item) => item * 2)
}
FP将开发者从机器的执行模型切换到了人的思维模型上,可读性会更高。需要注意的是,某些支持FP的语言本身是属于IP的,同时也可以认为其属于DP, 不必过于纠结。
即元编程, 也写做Metaprogramming。元编程是一种可以将程序当作数据来操作的技术,元编程能够读取,生成,分析或转换其他的程序代码,甚至可以在运行时修改自身. C++的 template 即属于metaprogramming的范畴,编译器在编译时生成具体的源代码。在 web 框架Ruby on Rails里,元编程被普遍使用。比如,在 SQL 数据库的表里,有一个表 users,在 ruby 中用类User表示,你需要根据 user 的 email 字段来获取相应的结果,通常的做法是写个 sql 查询语句去完成,但是Ruby on Rails在元编程的加持下,会让这件事变得异常简单。
User.find_by_email('songtianyi630@163.com')
find_by_email并不是我们自己的定义的函数,它是如何完成这件事的呢?框架会根据函数名find_by_email动态生成一个查询函数去查询数据库。除了这种黑魔法,元编程还能够动态修改一个类的成员函数,在运行时创建函数等等,这里不展开讲,在Ruby或者Groovy的相关书籍里会有详细介绍。语言的反射特性和对模板的支持是实现元编程的主要基础,虽然**c++并不支持反射,但c++**的模板提供了元编程的能力。
编程范式还有很多细分项,比如Event-driven programming, Distributed programming[10]等, 这里不再一一列举。
Lang | Typed | Static and dynamic checks | Strongly checked | Weakly or strongly typed | Dynamically or statically typed | Type theories | Paradigms |
---|---|---|---|---|---|---|---|
Assembly | ❌ | ❌ | ❌ | - | - | - | IP |
Java | ☑️ | ☑️ | ☑️ | strongly | statically | generic, subtype, overloading | IP, SP, PP, OOP, FP, MP |
C | ☑️ | ☑️ | ❌ | strongly | statically | - | IP, SP, PP |
C++ | ☑️ | ☑️ | ❌ | strongly | statically | generic, subtype, overloading | IP, SP, PP, OOP, FP, MP(template) |
Python | ☑️ | ❌ | ☑️ | weakly | dynamically | duck, subtype, overloading | IP, PP, OOP, FP, MP |
C# | ☑️ | ☑️ | ☑️ | strongly | statically | generic, subtype, overloading | IP, SP, PP, OOP, FP, MP |
PHP | ☑️ | ❌ | ❌ | weakly | dynamically | duck, subtype | IP, PP, OOP, FP, MP |
JavaScript | ☑️ | ❌ | ❌ | weakly | dynamically | duck | IP, PP, OOP, FP, MP |
Ruby | ☑️ | ❌ | ☑️ | weakly | dynamically | duck | IP, OOP, FP, MP |
R | ☑️ | ❌ | ❌ | weakly | dynamically | generic, overloading, subtype | IP, PP, OOP, FP, MP |
Go | ☑️ | ☑️ | ☑️ | strongly | statically | subtype | IP, PP, MP |
Objective-C | ☑️ | ☑️ | ❌ | strongly | statically | duck, generic, subtype | IP, OOP, MP |
Perl | ☑️ | ❌ | ❌ | weakly | dynamically | generic, duck, subtype, overloading | IP, PP, OOP, FP, MP |
Swift | ☑️ | ☑️ | ☑️ | strongly | statically | duck, generic, subtype, overloading | IP, PP, OOP, FP, MP |
Scala | ☑️ | ☑️ | ☑️ | strongly | statically | duck, generic, subtype, overloading | IP, OOP, FP, MP |
Lisp | ❌ | ❌ | ☑️ | - | - | - | FP |
Prolog | ☑️ | ☑️ | ❌ | weakly | statically | - | DP, LP |
Erlang | ☑️ | ❌ | ☑️ | strongly | dynamically | overloading | FP |
Lua | ☑️ | ❌ | ☑️ | weakly | dynamically | duck, generic | IP, PP, OOP, FP, MP |
Haskell | ☑️ | ☑️ | ☑️ | strongly | statically | generic, duck, overloading | FP |
Kotlin | ☑️ | ☑️ | ❌ | strongly | statically | generic, subtype, overloading | IP, OOP, FP, MP |
TypeScript | ☑️ | ☑️ | ❌ | weakly | statically | generic, duck, overloading | IP, SP, PP, OOP, FP, MP |
Rust | ☑️ | ☑️ | ☑️ | strongly | statically | generic, overloading, subtype | IP, SP, PP, OOP, FP, MP |
Julia | ☑️ | ❌ | ☑️ | weakly | dynamically | generic, overloading, subtype | IP, SP, PP, OOP, FP, MP |
Lang | Typed | Static and dynamic checks | Strongly checked | Weakly or strongly typed | Dynamically or statically typed | Type theories | Paradigms |
整理完这个对比表才发现一些有意思的事情:
附: 树形对比图 http://on-img.com/chart_image/5b20fa53e4b06350d462d78b.png