编程中声明式和命令式范式有什么区别?

805

我一直在搜索关于声明式命令式编程的定义,希望能让我更加明白。然而,我发现一些资源中使用的语言让我感到有些困难 - 比如在维基百科上。 请问是否有人可以给我展示一个真实世界的例子,以便更好地理解这个主题(也许是用C#)?


57
“命令句”去餐厅点了一份罕见熟度的6盎司牛排,配薯条(加番茄酱),配沙拉(加牧场沙拉酱),以及一杯不加冰的可乐。服务员按照他的要求送来了食物,收费14.50美元。另一方面,“陈述句”去餐厅告诉服务员他只想花大约12美元吃晚餐,而且他想吃牛排。服务员回来端来了一份熟度为中等的6盎司牛排,搭配土豆泥、蒸西兰花、一个面包和一杯水。收费11.99美元。 - cs_pupil
3
我找到的另一个很好的例子可能是关于如何编写Docker文件和命令... 例如,一种命令式方法是在命令行中逐步定义所有步骤,例如在AWS中创建容器、创建网络,然后将资源组合在一起... 声明式方法则是:您声明一个Dockerfile(或docker-compose.yaml)文件,在其中基本上放置所有命令(或只是命名要执行的操作),然后简单地执行该文件。它事先全部声明,因此应始终具有相似的行为。 - MMMM
1
如果你将声明式程序和声明式编程看作是配置和配置过程,那么你会更好地理解它们的本质。 - AnatolyG
3
那个去餐厅点一份生熟的牛排的人肯定是直截了当的。他明确表示他想要一份生的6盎司牛排。这和那个宣称他愿意支付12美元的人没有什么不同。抽象程度可能略有不同,但在这里,“命令式”和“陈述式”的区别完全是随机的。 - undefined
22个回答

1069

一个很好的C#声明式编程和命令式编程的例子是LINQ。

使用命令式编程,你会一步步地告诉编译器你想要发生什么。

例如,让我们从这个集合开始,并选择奇数:

List<int> collection = new List<int> { 1, 2, 3, 4, 5 };

使用命令式编程,我们会逐步进行并决定我们想要什么:

List<int> results = new List<int>();
foreach(var num in collection)
{
    if (num % 2 != 0)
          results.Add(num);
}

这里我们说的是:

  1. 创建一个结果集合
  2. 遍历集合中的每个数字
  3. 检查数字,如果它是奇数,则将其添加到结果中

然而,在声明式编程中,您编写描述您想要的内容的代码,但不一定需要说明如何实现(声明所需的结果,而不是逐步说明):

var results = collection.Where( num => num % 2 != 0);

在这里,我们所说的是“给我们所有奇数的东西”,而不是“遍历集合。检查这个项目,如果它是奇数,则将其添加到结果集合中。”

在许多情况下,代码将是两种设计的混合体,因此并不总是非黑即白。


15
但是,你首先提到了LINQ,但这些示例与LINQ有什么关系呢? - Zano
21
集合使用了声明式的LINQ扩展方法,而不是使用C#语言特性,而是使用声明式API。我不想混淆信息,这就是为什么我避免了构建在声明式方法之上的语言扩展的原因。 - Reed Copsey
452
对我来说,声明式编程似乎仅仅是一层抽象。 - Drazen Bjelovuk
9
这是一个很好的回答,但它回答了不纯函数式和命令式编程之间的区别。collection.Where没有使用Linq提供的声明性语法 - 参见https://msdn.microsoft.com/en-us/library/bb397906.aspx中的示例,`from item in collection where item%2 != 0 select item` 是声明式形式。仅仅因为调用的函数在System.Linq命名空间中,并不能将其变成声明式编程。 - Pete Kirkham
40
@PeteKirkham 你使用的语法不是问题所在 - 声明式与命令式更多地涉及声明您想要发生的事情,而不是解释它需要如何发生。使用集成语法或扩展方法语法是一个单独的问题。 - Reed Copsey
显示剩余12条评论

201

声明式编程是指你说明 想要什么,而命令式语言则是说明 如何 获得你想要的结果。

下面是 Python 的一个简单示例:

# Declarative
small_nums = [x for x in range(20) if x < 5]

# Imperative
small_nums = []
for i in range(20):
    if i < 5:
        small_nums.append(i)
第一个示例是声明性的,因为我们没有指定构建列表的任何"实现细节"。 为了举个C#的例子,通常使用LINQ会以声明式风格呈现,因为你不是在说明如何获得想要的东西,而是仅在说明你想要什么。 对于SQL也可以这样说。
声明式编程的一个好处是它允许编译器做出决策,这可能会导致比您手动制作的代码更好的结果。 以SQL为例,如果您有一个查询,类似于
SELECT score FROM games WHERE id < 100;

SQL的“编译器”可以对这个查询进行“优化”,因为它知道id是一个索引字段——或者可能它不是索引字段,在这种情况下,它将不得不遍历整个数据集。或者也可能SQL引擎知道现在正是利用全部8个核心进行快速并行搜索的最佳时机。作为一个程序员,并不需要关心这些条件,也不需要编写处理任何特殊情况的代码。


61
这个Python示例不是声明式的。 - Juanjo Conti
22
@Juanjo:它确实是声明性的。 - missingfaktor
8
这里的第一个陈述比第二个更加陈述性吗? - zenna
32
同意Juanjo和zenna的看法 - 循环结构在重构为更短的符号表示法时,并不会神奇地转变为声明式程序。 - Felix Frank
16
对 @FelixFrank 的观点持不同意见,更倾向于 @missingfaktor 的“大胆”陈述。传统的、“完全”的声明式方法是 filter(lambda x: x < 5, range(20)),只是另一种缩写形式。从任何有意义的角度来看,这与列表推导表达式(具有明确的“map”和“filter”部分)没有什么不同,而该列表推导表达式是为了创建更简洁的表示法而创建的(请参见 pep 202)。在这种情况下,使用列表推导表达式会更清晰/符合惯用语。 - yoniLavi
显示剩余11条评论

175

声明式编程 vs. 命令式编程

编程范式是计算机编程的基本风格。有四种主要的编程范式:命令式、声明式、函数式(被认为是声明式范式的子集)和面向对象。

声明式编程:是一种表达计算逻辑(做什么)而不描述其控制流程(如何做)的编程范式。一些著名的声明式领域特定语言(DSLs)包括CSS,正则表达式和SQL的子集(例如SELECT查询)。许多标记语言,如HTML、MXML、XAML、XSLT等,通常也是声明式的。声明式编程试图模糊程序作为指令集和程序作为所需答案断言之间的区别。

命令式编程:是一种以改变程序状态的语句来描述计算的编程范式。命令式程序可以被双重地视为编程命令或数学断言。

功能编程:是一种将计算视为数学函数评估并避免状态和可变数据的编程范式。它强调应用函数,与强调状态更改的命令式编程风格形成对比。 在纯函数语言(例如Haskell)中,所有函数都没有副作用,并且状态更改仅表示为转换状态的函数。
下面是一个命令式编程的例子,在 MSDN 中循环遍历1到10之间的数字,并查找偶数。
var numbersOneThroughTen = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//With imperative programming, we'd step through this, and decide what we want:
var evenNumbers = new List<int>();
foreach (var number in numbersOneThroughTen)
{    if (number % 2 == 0)
    {
        evenNumbers.Add(number);
    }
}
//The following code uses declarative programming to accomplish the same thing.
// Here, we're saying "Give us everything where it's even"
var evenNumbers = numbersOneThroughTen.Where(number => number % 2 == 0);

这两个示例产生相同的结果,一个并不比另一个更好或更差。第一个示例需要更多的代码,但是该代码是可测试的,并且命令式方法使您完全控制实现细节。在第二个示例中,代码可能更易读;然而,LINQ不会让您掌控背后发生的事情。您必须相信LINQ将提供所请求的结果。


15
可以,请问您需要为面向对象添加描述吗? - mbigras
4
好的例子:https://tylermcginnis.com/imperative-vs-declarative-programming/ - user3245268

106

以下是网络帖子和本文提到的:

  • 使用声明式编程时,您编写描述您想要什么的代码,但不一定知道如何实现它
  • 应该选择声明式编程而不是命令式编程

他们没有告诉我们如何实现。为了使程序的一部分更具声明性,其他部分必须提供抽象以隐藏实现细节(即命令式代码)。

  • 例如,LINQ比循环(for、while等)更具声明性,例如,您可以使用list.Where()获取一个新的已过滤列表。为了使其工作,Microsoft在LINQ抽象背后完成了所有繁重的工作。

事实上,函数式编程和函数式库更具声明性的原因之一是它们已经抽象掉了循环和列表创建,将所有实现细节(很可能有循环的命令式代码)隐藏在幕后。

在任何程序中,您始终会同时拥有命令式和声明式代码,并且您应该致力于将所有命令式代码隐藏在领域特定的抽象后面,以便程序的其他部分可以以声明性方式使用它们。

最后,尽管函数式编程和LINQ可以使您的程序更具声明性,但您始终可以通过提供更多的抽象来使其更加声明性。例如:

// JavaScript example

// Least declarative
const bestProducts = [];
for(let i = 0; i < products.length; i++) {
    let product = products[i];
    if (product.rating >= 5 && product.price < 100) {
        bestProducts.push(product);
    }
}


// More declarative
const bestProducts = products.filter(function(product) {
    return product.rating >= 5 && product.price < 100;
});

// Most declarative, implementation details are hidden in a function
const bestProducts = getBestProducts();

P.S. 声明式编程的极致是发明新的领域特定语言(DSL):

  1. 字符串搜索:使用正则表达式代替自定义命令式代码
  2. React.js:使用JSX代替直接DOM操作
  3. AWS CloudFormation:使用YAML代替CLI
  4. 关系型数据库:使用SQL代替旧的读写API,如ISAM或VSAM。

1
有许多优秀的声明式编程示例:ReactCloudFormationTerraform - engineforce
1
那么,“声明式”编程只是将执行任务的代码移动到函数中吗? - Guillaume F.
6
@GuillaumeF。这是关于创建特定领域抽象的内容,例如:
  • 在银行业:您应该创建函数,如“debit”、“deposit”等,而不是重复命令式代码“account.balance += depositAmount”。
- engineforce
@engineforce 但你仍然会同时拥有两者,是吗?在你的抽象结束时,你将拥有 account.balance += depositAmount。那么,声明式编程风格只是为了倡导在正确的抽象层次上创建良好的API吗? - Nevermore
基本上是的。当然,这样做可以提供更好的测试实践和弹性,以及其他一些好处。 - undefined

52

我再举一个在声明式/命令式编程讨论中很少出现的例子: 用户界面!

在C#中,您可以使用各种技术构建UI。

在命令式端,您可以使用DirectX或OpenGL以非常命令式的方式逐行(或实际上是三角形)画出按钮、复选框等。绘制用户界面的方式由您决定。

在声明式端,您有WPF。你基本上编写一些XML(是的,是“XAML”技术),框架会为您完成工作。您说出用户界面的外观,系统会找出如何完成它。

无论如何,这只是另一个需要考虑的问题。仅仅因为一种语言是声明式还是命令式,并不意味着它没有另一种语言的某些特性。

此外,声明式编程的好处之一是,目的通常更容易从读取代码中理解,而命令式编程则可以更精细地控制执行。

总的来说:

声明式->你想要做什么

命令式->你想要怎样做


44

这主要与抽象层次有关。在声明式编程中,你离具体步骤越远,程序在得到结果时就有更多的自由度。


你可以将每个指令视为处于一个连续性范围的某个位置:

抽象度:

Declarative <<=====|==================>> Imperative

声明式现实世界示例:

  1. 图书管理员,请帮我借一本《白鲸记》的副本。(图书管理员自行决定执行请求的最佳方法)

命令式现实世界示例:

  1. 进入图书馆。
  2. 找到图书组织系统(卡片目录 - 老派的)。
  3. 研究如何使用卡目录(你也忘了,对吧)。
  4. 弄清楚书架是如何标记和组织的。
  5. 弄清楚书籍在书架上的组织方式。
  6. 将卡片目录中的书籍位置与组织系统进行交叉参考以查找所需书籍。
  7. 将书籍带到借还系统。
  8. 借阅书籍。

1
这不更多关于抽象化而非声明式/命令式吗?你仍然在指示图书管理员去取书。 - kamathln
更新答案以使其更完整,并在解决方案中包括此方面。 - Lucent Fox
4
这个说法有一定道理,但并不是完整的定义。在声明式编程中,你陈述的是最终目标,而不考虑起点。而在命令式编程中,定义好的起点很重要,就像给地址和给方向的区别一样。无论你在哪里,地址都是有用的。而方向如果你从其他地方开始就无效了。 - Cthutu
1
@Cthutu,我希望"这就像[声明]地址和指示方向的区别。无论你在哪里,地址都是有用的。而如果你从其他地方开始,方向就是无效的。" 能够成为唯一被选中的答案。;^) - ruffin
@ruffin 这是一个非常棒的描述方式。 - Cthutu
1
这真的是唯一有意义的答案。通常所说的“陈述句”和“命令句”的区别在编程中并不适用,除非你在看一个谱系,唯一真正的命令式编程是机器码。 - undefined

38

我喜欢剑桥课程上的一个解释和他们的例子:

  • 声明式 - 指定要做什么,而不是如何做
    • 例如:HTML描述了网页上应该出现什么,而不是它应该如何在屏幕上绘制
  • 命令式 - 同时指定要做什么如何做
    • int x; - 什么(声明式)
    • x=x+1; - 如何

“不是应该在屏幕上绘制的方式”…那么这是否意味着CSS命令式的呢? - Chef_Code
19
不是这样的。它也可以被认为是陈述句,因为你只是说出你想要的 - "使这个单元格的边框变成蓝色"。试想一下,如果你想用命令式的方法(例如JavaScript)来画同样的边框,那么你需要说“前往点(x1, y1),在这一点和(x2, y1)之间画一条蓝线,从(x2, y1)到(x2, y2)画一条蓝线,从(x2, y2)到(x1, y2)画一条蓝线,从(x1, y2)到(x1, y1)画一条蓝线”。 - ROMANIA_engineer
@ROMANIA_engineer,请问我在哪里可以找到这样的剑桥课程? - test team
@testteam,请在Google上搜索“cl.cam.ac.uk teaching ooprog”。您可以更改URL中的年份。 - ROMANIA_engineer

30
命令式编程要求开发者逐步定义代码应如何执行。要以命令式的方式给出指示,你会说:“去第一条街道,向左转进入Main大街,行驶两个街区,右转进入Maple路,在左侧第三所房屋停下来。” 声明式版本可能听起来像这样:“开车到Sue的房子”。一个是说明如何做某事;另一个则说明需要做什么。
声明式风格比命令式风格有两个优点:
  • 它不强迫旅客记忆一长串指令。
  • 当可能时它允许旅客优化路线。
参考文献:Calvert,C Kulkarni,D(2009)。 Essential LINQ. Addison Wesley. 48.

11

借用Philip Roberts的说法

  • 命令式编程告诉计算机如何做某事(从而导致你想要的结果)
  • 声明式编程告诉计算机你想要发生什么(然后计算机找出如何做到这一点)

以下是两个例子:

1. 将数组中的所有数字翻倍

命令式:

var numbers = [1,2,3,4,5]
var doubled = []

for(var i = 0; i < numbers.length; i++) {
  var newNumber = numbers[i] * 2
  doubled.push(newNumber)
}
console.log(doubled) //=> [2,4,6,8,10]

声明性地:

var numbers = [1,2,3,4,5]

var doubled = numbers.map(function(n) {
  return n * 2
})
console.log(doubled) //=> [2,4,6,8,10]

2. 对列表中所有项目求和

命令式编程

var numbers = [1,2,3,4,5]
var total = 0

for(var i = 0; i < numbers.length; i++) {
  total += numbers[i]
}
console.log(total) //=> 15

声明式地

var numbers = [1,2,3,4,5]

var total = numbers.reduce(function(sum, n) {
  return sum + n
});
console.log(total) //=> 15

注意命令式示例涉及创建一个新变量、改变它并返回该新值(即如何使某事发生),而声明式示例执行给定的输入并根据初始输入返回新值(即我们想要发生什么)。


9
和这个问题的许多令人惊恐的答案一样,你提供的“声明式”编程示例是函数式编程的一个例子。“map”的语义是“按顺序将此函数应用于数组的元素”。这不允许运行时在执行顺序方面有任何自由裁量。 - Pete Kirkham

11

命令式编程是直接告诉计算机要做什么以及如何执行,比如指定顺序等。

C#:

for (int i = 0; i < 10; i++)
{
    System.Console.WriteLine("Hello World!");
}

声明式编程是告诉计算机做什么,但并不是真正的如何做。在这方面,Datalog / Prolog是首先想到的语言。基本上所有东西都可以是声明式的。你无法真正保证顺序。

C#是一种更加命令式的编程语言,但某些C#特性是更加声明式的,比如Linq。

dynamic foo = from c in someCollection
           let x = someValue * 2
           where c.SomeProperty < x
           select new {c.SomeProperty, c.OtherProperty};

同样的事情可以用命令式的方式来写:

dynamic foo = SomeCollection.Where
     (
          c => c.SomeProperty < (SomeValue * 2)
     )
     .Select
     (
          c => new {c.SomeProperty, c.OtherProperty}
     )

(来自维基百科Linq的示例)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接