主页

索引

模块索引

搜索页面

临时

定义:

范即模范之意,范式即模式、方法,是一类典型的编程风格,是指从事软件工程的一类典型的风格

编程语言发展到今天,本质是: 如何写出更为通用、更具可重用性的代码或模块

C 语言特性:

1. C 语言是一个静态弱类型语言,在使用变量时需要声明变量类型,但是类型间可以有隐式转换
2. 不同的变量类型可以用结构体(struct)组合在一起,以此来声明新的数据类型
3. C 语言可以用 typedef 关键字来定义类型的别名,以此来达到变量类型的抽象
4. C 语言是一个有结构化程序设计、具有变量作用域以及递归功能的过程式语言
5. C 语言传递参数一般是以值传递,也可以传递指针
6. 通过指针,C 语言可以容易地对内存进行低级控制,然而这加大了编程复杂度
7. 编译预处理让 C 语言的编译更具有弹性,比如跨平台

泛型

抽象类型,这就是所谓的泛型编程

C语言-小实例:

void swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}

C语言-泛型实现-void*:

void swap(void* x, void* y, size_t size)
{
     char tmp[size];
     memcpy(tmp, y, size);
     memcpy(y, x, size);
     memcpy(x, tmp, size);
}

C语言-泛型实现-用宏定义来做泛型:

#define swap(x, y, size) {\
  char temp[size]; \
  memcpy(temp, &y, size); \
  memcpy(&y,   &x, size); \
  memcpy(&x, temp, size); \
}

一个良好的泛型编程需要解决如下几个泛型编程的问题:

1. 算法的泛型;
2. 类型的泛型;
3. 数据结构(数据容器)的泛型

C语言-泛型实现-search()函数:

int search(void* a, size_t size, void* target,
  size_t elem_size, int(*cmpFn)(void*, void*) )
{
  for(int i=0; i<size; i++) {
    if ( cmpFn (a + elem_size * i, target) == 0 ) {
      return i;
    }
  }
  return -1;
}

C++语言-泛型实现-search()函数:

template<typename T, typename Iter>
Iter search(Iter pStart, Iter pEnd, T target)
{
  for(Iter p = pStart; p != pEnd; p++) {
    if ( *p == target )
      return p;
  }
  return NULL;
}

在编程世界中,我们需要处理好两件事:

第一件事是编程语言中的类型问题
第二件事是对真实世界中业务代码的抽象、重用和拼装

程序语言的『类型』系统主要提供如下的功能:

1. 程序语言的安全性
    使用类型可以让编译器侦测一些代码的错误
    例如:
      可以识别出一个错误无效的表达式,如“Hello, World” + 3
    强类型语言提供更多的安全性,但是并不能保证绝对的安全

2. 利于编译器的优化
    静态类型语言的类型声明,可以让编译器明确地知道程序员的意图。
    因此,编译器就可以利用这一信息做很多代码优化工作。
    例如:
      如果我们指定一个类型是 int ,那么编译就知道,这个类型会以 4 个字节的倍数进行对齐,
      编译器就可以非常有效地利用更有效率的机器指令。

3. 代码的可读性
    有类型的编程语言,可以让代码更易读和更易维护,代码的语义也更清楚,
      代码模块的接口(如函数)也更丰富和清楚
4. 抽象化
    类型允许程序设计者对程序以较高层次的方式思考,而不是烦人的低层次实现。
    例如:
      我们使用整型或是浮点型来取代底层的字节实现,我们可以将字符串设计成一个值,而不是底层字节的数组。
    从高层上来说,类型可以用来定义不同模块间的交互协议,比如函数的入参类型和返回类型,
      从而可以让接口更有语义,而且不同的模块数据交换更为直观和易懂。

动态&静态类型语言

两类语言:

1. 一类是静态类型语言,如 C、C++、Java
2. 一种是动态类型语言,如 Python、PHP、JavaScript 等

泛型技术是静态系统所独有的特性
  本质上我觉得还是为了兼顾执行效率和编程灵活性,实现零成本抽象这一刀尖上跳舞的巨大挑战。

在弱类型或是动态类型的语言中,下面代码的执行会有不确定的结果:

x = 5;
y = "37";
z = x + y;

1. 像 Visual Basic 语言
    给出的结果是 42:系统将字符串 "37" 转换成数字 37,以匹配运算上的直觉
2. 像 JavaScript 语言
    给出的结果是 "537":系统将数字 5 转换成字符串 "5" 并把两者串接起来
3. 像 Python 这样的语言
    则会产生一个运行时错误

类型检查系统:

1. 静态类型检查是在编译器进行语义分析时进行的
    如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换),
    那么称此处理为强类型,反之称为弱类型。
2. 动态类型检查系统更多的是在运行时期做动态类型标记和相关检查
    所以,动态类型的语言必然要给出一堆诸如这样的运行时类型检查函数:
        is_array(), is_int(), is_string() 或是 typeof()

类型是对底层内存布局的一个抽象,会让我们的代码要关注于这些非业务逻辑上的东西

对于静态类型的语言也开了些“小后门”:比如,类型转换,还有 C++、Java 运行时期的类型测试

这些小后门也会带来相当讨厌的问题,比如下面这个 C 语言的示例:
  int x = 5;
  char y[] = "37";
  char* z = x + y;
  结果可能和你想的完全不一样

备注

静态类型语言的支持者和动态类型自由形式的支持者,经常发生争执。前者主张,在编译的时候就可以较早发现错误,而且还可增进运行时期的性能。后者主张,使用更加动态的类型系统,分析代码更为简单,减少出错机会,才能更加轻松快速地编写程序。与此相关的是,后者还主张,考虑到在类型推断的编程语言中,通常不需要手动宣告类型,这部分的额外开销也就自动降低了。

备注

任何语言都有类型系统,只是动态类型语言在运行时做类型检查。动态语言的代码复杂度比较低,并可以更容易地关注业务,在某些场景下是对的,但有些情况下却并不见得。

比如:在 JavaScript 中,我们需要做一个变量转型的函数,可能会是下面这个样子:

function ToNumber(x) {
    switch(typeof x) {
        case "number": return x;
        case "undefined": return NaN;
        case "boolean": return x ? 1 : 0;
        case "string": return Number(x);
        case "object": return NaN;
        case "function": return NaN;
    }
}

备注

用过一段时间的动态类型语言,一旦代码量比较大了,我们就会发现,代码中出现“类型问题”而引发整个程序出错的情况实在是太多太多了。而且,这样的出错会让整个程序崩溃掉,太恐怖了。这个时候,我们就很希望提前发现这些类型的问题。静态语言的支持者会说编译器能帮我们找到这些问题,而动态语言的支持者则认为,静态语言的编译器也无法找到所有的问题,想真正提前找到问题只能通过测试来解决。其实他们都对。

泛型的本质

先了解下类型的本质:

类型是对内存的一种抽象
    不同的类型,会有不同的内存布局和内存分配的策略
不同的类型,有不同的操作
    所以,对于特定的类型,也有特定的一组操作

要做到泛型,我们需要做下面的事情:

1. 标准化掉类型的内存分配、释放和访问
2. 标准化掉类型的操作
    比如:比较操作,I/O 操作,复制操作…
3. 标准化掉数据容器的操作
    比如:查找算法、过滤算法、聚合算法…
4. 标准化掉类型上特有的操作
    需要有标准化的接口来回调不同类型的具体操作…

C++ 动用了非常繁多和复杂的技术来达到泛型编程的目标:

1. 通过类中的构造、析构、拷贝构造,重载赋值操作符,标准化(隐藏)了类型的内存分配、释放和复制的操作
2. 通过重载操作符,可以标准化类型的比较等操作
3. 通过 iostream,标准化了类型的输入、输出控制
4. 通过模板技术(包括模板的特化),来为不同的类型生成类型专属的代码
5. 通过迭代器来标准化数据容器的遍历操作
6. 通过面向对象的接口依赖(虚函数技术),来标准化了特定类型在特定算法上的操作
7. 通过函数式(函数对象),来标准化对于不同类型的特定操作

备注

Generic programming centers around the idea of abstracting from concrete, efficient algorithms to obtain generic algorithms that can be combined with different data representations to produce a wide variety of useful software.—Musser, David R.; Stepanov, Alexander A., Generic Programming(1985 年). 本质就是 —— 屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。

小结

备注

编程语言本质上帮助程序员屏蔽底层机器代码的实现,而让我们可以更为关注于业务逻辑代码。但是因为,编程语言作为机器代码和业务逻辑的粘合层,是在让程序员可以控制更多底层的灵活性,还是屏蔽底层细节,让程序员可以更多地关注于业务逻辑,这是很难两全需要 trade-off 的事。

不同的语言在设计上都会做相应的取舍,比如:C 语言偏向于让程序员可以控制更多的底层细节,而 Java 和 Python 则让程序员更多地关注业务功能的实现。而 C++ 则是两者都想要,导致语言在设计上非常复杂。

函数式编程

函数式编程的理念来自于数学中的代数。

备注

对于函数式编程来说,它只关心定义输入数据和输出数据相关的关系,数学表达式里面其实是在做一种映射(mapping),输入的数据和输出的数据关系是什么样的,是用函数来定义的。

特征:

1. stateless
    函数不维护任何状态。
    函数式编程的核心精神是 stateless,
    简而言之就是它不能存在状态,打个比方,你给我数据我处理完扔出来。里面的数据是不变的。
2. immutable
    输入数据是不能动的,动了输入数据就有危险,所以要返回新的数据集。

优势:

1. 没有状态就没有伤害
2. 并行执行无伤害
3. Copy-Paste 重构代码无伤害
4. 函数的执行没有顺序上的问题

函数式编程还带来了以下一些好处:

1. 惰性求值
    这需要编译器的支持,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。也就是说,语句如 x:=expression;  (把一个表达式的结果赋值给一个变量) 显式地调用这个表达式被计算并把结果放置到 x 中,但是先不管实际在 x 中的是什么,直到通过后面的表达式中到 x 的引用而有了对它的值的需求的时候,而后面表达式自身的求值也可以被延迟,最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树。
2. 确定性
    所谓确定性,就是像在数学中那样,f(x) = y 这个函数无论在什么场景下,都会得到同样的结果,而不是像程序中的很多函数那样。同一个参数,在不同的场景下会计算出不同的结果,这个我们称之为函数的确定性。所谓不同的场景,就是我们的函数会根据运行中的状态信息的不同而发生变化。

劣势:

数据复制比较严重。
注:有一些人可能会觉得这会对性能造成影响。其实,这个劣势不见得会导致性能不好。因为没有状态,所以代码在并行上根本不需要锁(不需要对状态修改的锁),所以可以拼命地并发,反而可以让性能很不错。比如:Erlang 就是其中的代表。

对于纯函数式(也就是完全没有状态的函数)的编程来说,各个语言支持的程度如下:

1. 完全纯函数式 Haskell
2. 容易写纯函数 F#, Ocaml, Clojure, Scala
3. 纯函数需要花点精力 C#, Java, JavaScript

函数式编程用到的技术:

1. first class function(头等函数)
    这个技术可以让你的函数就像变量一样来使用。
    也就是说,你的函数可以像变量一样被创建、修改,并当成变量一样传递、返回,或是在函数中嵌套函数。
2. tail recursion optimization(尾递归优化)
    我们知道递归的害处,那就是如果递归很深的话,stack 受不了,并会导致性能大幅度下降。因此,我们使用尾递归优化技术——每次递归时都会重用 stack,这样能够提升性能。当然,这需要语言或编译器的支持。Python 就不支持。
3. map & reduce
    这个技术不用多说了,函数式编程最常见的技术就是对一个集合做 Map 和 Reduce 操作。这比起过程式的语言来说,在代码上要更容易阅读。(传统过程式的语言需要使用 for/while 循环,然后在各种变量中把数据倒过来倒过去的)这个很像 C++ STL 中 foreach、find_if、count_if 等函数的玩法。
4. pipeline(管道)
    这个技术的意思是,将函数实例成一个一个的 action,然后将一组 action 放到一个数组或是列表中,再把数据传给这个 action list,数据就像一个 pipeline 一样顺序地被各个函数所操作,最终得到我们想要的结果。

5. recursing(递归)
    递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。

6. currying(柯里化)
    将一个函数的多个参数分解成多个函数, 然后将函数多层封装起来,每层函数都返回一个函数去接收下一个参数,这可以简化函数的多个参数。在 C++ 中,这很像 STL 中的 bind1st 或是 bind2nd。

7. higher order function(高阶函数)
    所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。这个技术用来做 Decorator 很不错。

函数式编程的理念:

1. 把函数当成变量来用,关注描述问题而不是怎么实现,这样可以让代码更易读
2. 因为函数返回里面的这个函数,所以函数关注的是表达式,关注的是描述这个问题,而不是怎么实现这个事情

other

备注

当我们说一门编程语言是图灵完备的语言时,说明这门语言所拥有的编程能力,是现代计算机语言所能拥有的最高能力。

主页

索引

模块索引

搜索页面