我一直在网上搜索声明式和命令式编程的定义,这将为我带来一些启发。但是,我发现的一些资源中使用的语言令人生畏——例如在Wikipedia中。有没有人有一个真实的例子,他们可以向我展示这可能会给这个主题带来一些观点(也许在 C# 中)?
19 回答
声明式与命令式编程的一个很好的 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);
}
在这里,我们说:
- 创建结果集合
- 遍历集合中的每个数字
- 检查数字,如果是奇数,将其添加到结果中
另一方面,使用声明式编程,您编写的代码描述了您想要的东西,但不一定是如何获得它(声明您想要的结果,但不是一步一步):
var results = collection.Where( num => num % 2 != 0);
在这里,我们说的是“给我们所有奇怪的地方”,而不是“逐步浏览集合。检查这个项目,如果它很奇怪,将它添加到结果集合中。”
在许多情况下,代码也将是两种设计的混合体,因此并不总是黑白分明的。
声明式编程是你说你想要什么,而命令式编程是你说如何得到你想要的。
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 个内核进行快速并行搜索的最佳时机。 作为程序员,您不关心任何这些条件,并且您不必编写代码来以这种方式处理任何特殊情况。
声明式与命令式
编程范式是计算机编程的基本风格。有四种主要范式:命令式、声明式、函数式(被认为是声明式范式的子集)和面向对象。
声明式编程:是一种编程范式,它表达了计算的逻辑(做什么)而不描述其控制流(如何做)。声明性领域特定语言 (DSL) 的一些著名示例包括 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 会提供请求的结果。
此处和其他在线帖子中的答案提到以下内容:
- 使用声明式编程,您编写的代码描述了您想要的,但不一定是如何获得它
- 您应该更喜欢声明式编程而不是命令式编程
他们没有告诉我们的是如何实现它。为了使程序的一部分更具声明性,其他部分必须提供抽象来隐藏实现细节(这是命令式代码)。
- 例如,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();
PS声明式编程的极端是发明新的领域特定语言(DSL):
- 字符串搜索:正则表达式而不是自定义命令式代码
- React.js:JSX 而不是直接的 DOM 操作
- AWS CloudFormation:YAML 而不是 CLI
- 关系数据库:SQL 代替旧的读写 API,如 ISAM 或 VSAM。
我将添加另一个在声明式/命令式编程讨论中很少出现的示例:用户界面!
在 C# 中,您可以使用各种技术构建 UI。
在命令式结束时,您可以使用 DirectX 或 OpenGL 来非常命令式地绘制按钮、复选框等……逐行(或者实际上是逐个三角形)。如何绘制用户界面由您决定。
在声明式结束时,您有 WPF。您基本上编写了一些 XML(是的,是的,技术上是“XAML”),框架为您完成工作。你说用户界面是什么样子的。由系统决定如何做到这一点。
无论如何,只是另一件事要考虑。仅仅因为一种语言是声明式或命令式的并不意味着它没有另一种语言的某些特征。
此外,声明式编程的一个好处是通常通过阅读代码更容易理解目的,而命令式可以让您更好地控制执行。
这一切的要点:
声明式 ->what
你想要完成
命令式 ->how
你想要它完成
我喜欢剑桥课程的解释+他们的例子:
- 声明式- 指定要做什么,而不是如何做
- 例如:HTML 描述的是网页上应该出现的内容,而不是应该如何在屏幕上绘制
- 命令式- 指定内容和方式
int x;
- 什么(声明性)x=x+1;
- 如何
差异主要与抽象的整体水平有关。使用声明性,在某些时候,您与各个步骤相距甚远,以至于程序在如何获得结果方面有很大的自由度。
您可以将每条指令视为落在一个连续统一体的某个地方:
抽象程度:
Declarative <<=====|==================>> Imperative
声明式真实世界示例:
- 图书管理员,请给我看一本《白鲸记》。(图书馆员自行决定选择执行请求的最佳方法)
命令式现实世界示例:
- 走进图书馆
- 找书组织系统(卡片目录-老派)
- 研究如何使用卡片目录(你也忘了,对吧)
- 弄清楚货架的标签和组织方式。
- 弄清楚书是如何在书架上组织的。
- 从卡片目录与组织系统交叉引用书籍位置以查找所述书籍。
- 带书到结账系统。
- 看看书。
命令式编程要求开发人员逐步定义代码的执行方式。以命令式的方式给出指示,你说,“去 1st Street,左转进入 Main,开两个街区,右转进入 Maple,然后在左边的第三个房子停下。” 声明式版本可能听起来像这样:“开车去苏家。” 一个说如何做某事;另一个说需要做什么。
声明式风格比命令式风格有两个优点:
- 它不会强迫旅行者记住一长串指令。
- 它允许旅行者尽可能优化路线。
卡尔弗特,C 库尔卡尼,D(2009 年)。基本 LINQ。艾迪生卫斯理。48.
命令式编程明确地告诉计算机要做什么以及如何去做,例如指定顺序等
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 的示例)
在这里从菲利普罗伯茨那里偷东西:
- 命令式编程告诉机器如何做某事(导致你想要发生的事情)
- 声明式编程告诉机器你想要发生什么(并且计算机会弄清楚如何去做)
两个例子:
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
请注意命令式示例如何涉及创建新变量、对其进行变异并返回该新值(即,如何使某事发生),而声明性示例在给定输入上执行并根据初始输入返回新值(即,我们想要发生的事情)。
在计算机科学中,声明式编程是一种编程范式,它表达了计算的逻辑而不描述其控制流。
来自http://en.wikipedia.org/wiki/Declarative_programming
简而言之,声明性语言更简单,因为它缺乏控制流的复杂性(循环、if 语句等)
一个很好的比较是 ASP.Net '代码隐藏' 模型。您有声明性“.ASPX”文件,然后是命令性“ASPX.CS”代码文件。我经常发现,如果我可以在脚本的声明性部分中做所有我需要的事情,那么更多的人可以遵循正在做的事情。
命令式编程 - 您编写完成工作的代码
声明式编程 - 其他人编写完成工作的代码
据我了解,这两个术语都源于哲学,有陈述性和命令性的知识。陈述性知识是对真理的断言,是对数学公理的事实陈述。它告诉你一些事情。命令式或程序性知识,一步一步地告诉你如何达到目标。这就是算法本质上的定义。如果愿意,请将计算机编程语言与英语进行比较。陈述句说明了一些事情。一个无聊的例子,但这是一种在 Java 中显示两个数字是否相等的声明方式:
public static void main(String[] args)
{
System.out.print("4 = 4.");
}
另一方面,英语中的祈使句发出命令或提出某种请求。因此,命令式编程只是一个命令列表(做这个,做那个)。这是在 Java 中接受用户输入时显示两个数字是否相等的命令式方法:
private static Scanner input;
public static void main(String[] args)
{
input = new Scanner(System.in);
System.out.println();
System.out.print("Enter an integer value for x: ");
int x = input.nextInt();
System.out.print("Enter an integer value for y: ");
int y = input.nextInt();
System.out.println();
System.out.printf("%d == %d? %s\n", x, y, x == y);
}
本质上,声明性知识跳过某些元素以形成对这些元素的抽象层。声明式编程也是如此。
声明性程序只是其一些或多或少的“通用”命令式实现/虚拟机的数据。
优点:仅以某种硬编码(和检查)格式指定数据比直接指定某些命令式算法的变体更简单且不易出错。一些复杂的规范不能直接编写,只能以某些 DSL 形式编写。DSL 数据结构中使用的 best 和 freq 是集合和表。因为您在元素/行之间没有依赖关系。当您没有依赖项时,您可以自由修改和轻松支持。(例如,将模块与类进行比较 - 使用您满意的模块和使用脆弱的基类问题的类)所有声明性和 DSL 的优点都立即从该数据结构(表和集)的好处中受益。另一个优点 - 如果 DSL 或多或少是抽象的(设计良好),您可以更改声明性语言 vm 的实现。例如,进行并行实现。
缺点:你猜对了。通用(并由 DSL 参数化)命令式算法/vm 实现可能比特定的更慢和/或内存占用。在某些情况下。如果这种情况很少见 - 忘记它,让它慢慢来。如果它很频繁 - 你总是可以为这种情况扩展你的 DSL/vm。在某个地方减慢所有其他情况,当然......
PS 框架介于 DSL 和命令式之间。和所有半途而废的解决方案一样……它们结合了缺陷,而不是好处。它们不是那么安全,也不是那么快:) 看看万事通的haskell——它介于强大的简单机器学习和灵活的metaprog Prolog 之间,而且......它是多么的怪物。您可以将 Prolog 视为具有仅布尔函数/谓词的 Haskell。以及它对 Haskell 的灵活性是多么简单......
只是在移动应用程序开发方面添加另一个示例。在 iOS 和 Android 中,我们有 Interface Builders,我们可以在其中定义应用程序的 UI。
使用这些 Builder 绘制的 UI 本质上是声明性的,我们在其中拖放组件。实际绘图发生在框架和系统之下并由框架和系统执行。
但是我们也可以在代码中绘制整个组件,这在本质上是必不可少的。
此外,Angular JS 等一些新语言专注于以声明方式设计 UI,我们可能会看到许多其他语言提供相同的支持。就像 Java 没有任何好的声明方式来在 Java swing 或 Java FX 中绘制本机桌面应用程序一样,但在不久的将来,它们可能会。
我只是想知道为什么没有人提到 Attribute 类作为 C# 中的声明性编程工具。这个页面的流行回答刚刚谈到了 LINQ 作为一种声明式编程工具。
根据维基百科
常见的声明性语言包括数据库查询语言(例如 SQL、XQuery)、正则表达式、逻辑编程、函数式编程和配置管理系统。
所以LINQ作为函数式语法肯定是声明式的方法,但是C#中的Attribute类作为配置工具也是声明式的。这是阅读更多相关信息的一个很好的起点:C# 属性编程快速概述
已经添加了很多代码示例,所以我不会再添加一个。
相反,我将尝试以一种我认为比大多数浮动定义更清楚它们的本质的方式来解释这两种方法之间的区别:
声明式方法侧重于特定算法的目的,通常隐藏算法本身。
命令式方法专注于特定目的的算法,这通常隐藏目的本身。
我发现基于idempotent和commutative更容易区分声明式和命令式。使用参考资料来了解它们。
查看此简化版本以了解幂等性。
然后我引入“什么”和“如何”的定义来理解“什么”和“如何”的真正含义。在声明式中,您通过定义它们之间的关系将一个数据与另一个数据连接起来。您没有提到应该如何实现这种关系,而是说这种关系是“什么”。通过一种关系,您描述“什么”您的输出数据看起来像,而不是“如何”来实现这个输出数据。
开始在我们的脑海中绘制一些图表,绘制一些点(数据)并将它们与线(关系)连接起来。以所有可能的方式绘制一对多、多对一和一对一。给这些行加箭头,像这样<----------。所有箭头都应朝左,因为必须首先计算特定数据所基于的所有数据,然后再向左移动以计算该特定数据。
如果数据a
基于数据b
,数据c
和数据d
又可能基于其他一些数据。然后b
,c
和d
应该先计算,然后a
才会计算。a
行的左侧也是如此,而其他所有的人都在右侧。将有 3 条线从和中到达a
一条线。b
c
d
该图具有一些属性:
- 没有数据会违反它与所有其他数据的关系
b
当然,控制流或顺序无关紧要,c
应该d
先计算,但a
两者之间没有偏好b
,即先计算这三个中的哪一个并不重要(可交换)c
d
a
仅基于b
,c
而d
没有其他人。因此,无论使用多少次计算并执行关系操作,a
都应该实现相同b
(幂等)。是这里关系运算的最终结果。基本上,每个正在影响的人都应该有一条线指向。c
d
a
a
a
a
这些关系(线)就像函数(数学函数和非编程函数)。毫无疑问,函数式编程在学术界是有名的。纯函数(我们的编程,因此不是粗体)就像函数(数学,因此是粗体)。
到现在为止,对您来说,声明式可能已经开始听起来像 PURE 和 IMMUTABLE(通常用于函数式编程),如果是 GOOD,如果不是 GREAT。因为这不是这里的目标,所以这是从这种模式中自动出现的。
如果你的一段代码可以转换成这个图表,那么它完全是声明性的,否则它就位于规模的其他地方。
声明式接近数学。
现在让我们放大这些关系(线),看看在程序执行期间计算机内部发生了什么。
势在必行。这是完成基础工作的地方。当务之急,您一步一步地提到它需要完成的“如何”b
c
d
,并且您知道这一系列步骤将在一个数据(输入)和另一个数据(输出a
)之间创建所请求的关系。在这里,您可以创建变量、对其进行变异、遍历数组和所有其他内容。
命令式接近编程。
与其说一个程序是声明性的还是命令性的,我更喜欢看到它的规模,在最左边我是完全声明性的,而在最右边是完全命令性的。请记住,声明式是建立在命令式之上的,因此您看到的任何声明式事物实际上都是命令式的。通常,程序是声明式和命令式的混合体。
现在,让我们举两个例子:
第二个示例可以像这样转换为图表:
reduce_r
map_r
filter_r
a
<--------- b
<--------- c
<------ d
- filter_r(关系):
c
只有偶数d
- map_r(关系):
b
是所有数字乘以 10c
- reduce_r(关系):
a
是所有数字相加b
这应该看起来像数学的复合函数reduce_r
: (map_r
(filter_r
(d
)))
在声明式中,开发人员的工作是将最终目标 ( a
) 分解为有助于实现最终目标的子目标 ( b
, )。c
当然,在程序map的引擎盖下,reduce和filter是命令式代码运行。
深思熟虑:如果您需要假设map
函数从左到右以使您的代码按预期工作,那么您实际上是以声明性的名义执行命令。