YuMingzhe's blog

Writing and Teaching is my way of Learning

命令式与声明式编程

大家可能都听说过命令式和声明式编程这两个概念,它们都是用于描述一种编程的方式,或者代码风格,如果对这两个概念还不了解的话,百度下就能得到它们的定义:

命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

如果我们对这两种编程风格很熟悉的话,这个定义自然很好理解,但是对于初学者来说,这个定义很抽象,根本想象不到 how 和 what 到底是什么意思。下面我将以生活中的例子做类比,给大家讲解下命令式和声明式编程的概念,最后再辅以代码演示下这两种编程风格。

首先,要知道的是我们使用的大部分高级编程语言都属于命令式编程的范畴,比如我们常用的 C,C++,Java 语言等,而对声明式编程可能比较陌生,其实我们每天也都接触声明式编程,只是大家没有对它有个明确的定义罢了,最常见的声明式编程语言有 SQL,HTML 等,还有一些编程语言这两种风格都支持,属于混合型的,如 JavaScript、Python 等。

想要讲清命令式和声明式编程,我们还是从它们的定义入手。为了便于理解,我们用现实中的例子来类比下:假设你和女友周末晚上约会去一家餐厅吃饭,进入餐厅后要选一个桌子坐下,下面就分别用命令式和声明式的方式来模拟找座位的场景:

  • 命令式:你和女友走进餐厅,环顾了下四周,发现有几个空座,有的空座旁边是几个东北大哥在胡吃海塞,声音吵闹,显然不适合俩人世界,有的位置附近有人在抽烟也不适合,经过多次比较,最终你们发现角落里的那个位置比较幽静,光线柔和,很有气氛,于是你们便选定了那个位置。
  • 声明式:你和女友走进餐厅,对服务员说:“麻烦你,我们想要一个比较幽静的位置”,于是服务员很快的找到了一个符合条件的位置并把你们引导过去。

从上面的例子可以看出命令式想要达成目标(找到合适的座位)需要我们全程的参与,身体力行,比如我们要挨个考察每个空位是否符合我们的要求,即如何去做(how)。而声明式只需将符合我们期望的条件告诉第三人,让他替我们实现目标即可,很省事,这里就是告诉别人你想要的是什么(what)。这就是命令式与声明式编程在“行事”方面的区别。

如果上面的例子说的不够清晰的话,我们再举个例子。假设你的亲戚乘高铁来看望你,到站后你如何指导他到达你的住所呢?

  • 命令式:先检票出站,乘电梯到 2 楼,坐 2 路汽车到 xxx 站下车,然后换成 3 路汽车到 xxx 站下车,下车后沿 xx 路走 300 米到 xx 小区,我家就在一单元四楼。
  • 声明式:出站后打个的,告诉到 xx 小区,我家在一单元四楼

从上面的例子看出,如果使用命令式的话,我们想要达成目标就要把每一步描述的很清楚,执行者按照每一步就能到达目的地;而使用声明式的话,不管你的住所在哪里,我只要能打到车就能到达目的地,如何到达就是司机的事了。所以命令式就是要求我们将实现目标的步骤要描述清楚(how),效率低;声明式要求我们给出最终的目标(what),中间过程由第三方完成,效率高

经过上面例子的铺垫,我们终于要用代码来演示下命令式与声明式编程到底为何物了。 下面使用网上的两个例子来说明。现在要实现一个名叫 double 的函数,接收一个整数数组,返回一个数组,但数组里每个数都是原数组的2倍;再实现一个名为 add 的函数,接收一个整数数组,返回数组所有元素的和。 这两个函数实现起来都很简单,我们可以轻松地写出下面命令式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function double (arr) {
  let results = []
  for (let i = 0; i < arr.length; i++){
    results.push(arr[i] * 2)
  }
  return results
}
function add (arr) {
  let result = 0
  for (let i = 0; i < arr.length; i++){
    result += arr[i]
  }
  return result
}

通过观察上面的代码,我们可以发现,每个函数的实现都是在描述如何去完成目标功能,即我们要迭代数组,然后对数组的每个元素进行操作,最终返回结果。此外,对于声明式编程不熟悉的同学来说,还有一点可能没察觉的就是,每个函数中我们都声明了一个临时变量result(s)来存储中间结果并且这个中间结果在每次迭代时都被不断地修改,而这个变量是一种状态变量,如果逻辑稍微复杂点很容易将其设置为其他的值,导致程序行为异常,而修改状态变量是声明式编程里要绝对避免的。最后从代码的可读性方面来说,命令式编程如果想要读懂的话,需要我们一行一行的读代码,理解程序的执行流程,最终才能将整个代码的实现思想搞清楚,如果代码量很大的话将是非常耗时耗力的一项工作。

那么我们再使用声明式编程风格对上面的例子进行改写,我们马上就能感受到声明式编程的简洁,但却不简单:

1
2
3
4
5
6
function double (arr) {
  return arr.map((item) => item * 2)
}
function add (arr) {
  return arr.reduce((prev, current) => prev + current, 0)
}

上面的代码就是声明式编程的实现,这些实现依赖于 JavaScript 提供的 map 和 reduce 函数,而这两个函数就相当于第三方,我们将最终的目标告诉他们,具体实施就由它们来做了,为我们隐藏了不必要的麻烦。这也是声明式编程的精髓所在——我只关心我要什么(what)而不是如何做(how)。并且从代码量和可读性来说比命令式编程简洁了不少,也很容易理解。这里还需要再提一句声明式编程鲜为人知的好处就是声明式代码通常可以做到编程语言独立,什么意思呢?就是说,因为声明式编程语言关注的是“做什么”,将具体的实现细节隐藏起来,那么我们将声明式代码移植到另一种语言时,只要简单地更改语法即可,而不用修改具体的实现细节,比如中间变量的类型等即可让代码重新运行起来。

讲到这里本篇基本要结束了,需要更正一个错误就是由于 Java 8 增加了 Lambda 表达式这一新特性,允许我们以函数式风格进行编程,所以Java 也算是一种命令式与声明式混合风格的编程语言了,但需要注意的是函数式编程只是声明式编程的一个子集。本文希望用生活中的例子和代码结合的方式尽可能简单地向大家讲解命令式和声明式编程,说了这么多可以用一句话总结:在计算机科学中,声明式编程是一种用于表达计算逻辑而不是靠描述程序控制流程的编程风格,有点像写伪代码一样。