表达式与语句的区别

477
我询问的是关于c#的问题,但我认为在大多数其他编程语言中都是相同的。
请问有人有“表达式”和“语句”的良好定义,并且它们之间的区别是什么?

4
我认为你所选择的答案含糊不清。一个表达式也会执行某些操作——它会被求值得出一个值。我给出了一个明确无误的答案。 - Shelby Moore III
4
@ShelbyMooreIII-“ 非歧义但却是错误的。被接受的答案措辞非正式,但这种措辞使得它易于理解 —— 更重要的是,它所传达的意思是准确的。” - Justin Morgan
@JustinMorgan 很遗憾,被接受的答案中的定义对于大多数现代语言(包括类C语言)也显然是错误的(“评估为一个值”/“一行代码”):表达式可以在未评估的上下文中使用,而语句与行无关。即使有一些解释,简短的答案仍然令人困惑和误导。 - FrankHB
另外需要补充的是,这个问题实际上是一个虚假二元论。表达式具有可求值的属性,语句也是如此,但不需要输入任何内容,而C#支持ExpressionStatement的概念,例如a=b而不是c=(a=b),这是一个不返回值的表达式。无论您将ExpressionStatement称为语句还是表达式都可以。 - Frank
值得注意的是,表达式具有副作用。如果您正在评估二元表达式(例如a+b),并且a是一个属性getter,它设置b的值,则需要知道二元表达式从左到右进行评估。 - Frank
显示剩余2条评论
21个回答

582

表达式:能够计算出值的一种东西。例如:1+2/x
语句:执行某项操作的一行代码。例如:GOTO 100

在最早期的通用编程语言(如FORTRAN)中,这个区别非常明显。在FORTRAN中,语句是一个执行单元,是你所做的事情。之所以它不被称为“行”,仅仅是因为有时候它跨越了多行。一个独立的表达式没法做任何事情……你必须将其分配给一个变量。

1 + 2 / X

FORTRAN中的错误,因为它什么也不做。你必须对那个表达式进行某些操作:

X = 1 + 2 / X

FORTRAN并没有像我们今天所知道的语法,那个想法是作为Algol-60定义的一部分而被发明的,连同Backus-Naur Form(BNF)一起。在那时,“语义”区别(“具有值”与“执行某些操作”)已经被奉为“语法”:一种短语是表达式,另一种是语句,解析器可以将它们区分开来。

后来的语言设计者模糊了这种区别:他们允许语法表达式执行某些操作,并且允许具有值的语法语句。仍然存在的最早流行的语言示例是C语言。C语言的设计者意识到,如果允许评估一个表达式并丢弃结果,则不会造成任何伤害。在C语言中,每个语法表达式都可以通过在末尾添加分号来变成语句:

1 + 2 / x;

这虽然毫无实际效果,但是完全是真实的陈述。同样,在 C 语言中,一个表达式可能会有 副作用 — 它会改变某些东西。

1 + 2 / callfunc(12);

因为callfunc可能会执行一些有用的操作。

一旦你允许任何表达式都可以成为语句,你也可以在表达式中使用赋值运算符(=)。这就是为什么C语言允许你做类似下面的事情:

callfunc(x = 2);

这里首先将表达式 x = 2(将值2赋给x)进行评估,然后将其(即2)传递给函数callfunc

在所有C派生语言中(包括C,C ++,C#和Java),表达式和语句的模糊化都存在,虽然仍有一些语句(如while),但几乎可以将任何表达式用作语句(在C#中,只能使用赋值、调用、增量和减量表达式作为语句;请参见Scott Wisniewski的答案)。

对于具有两个“语法类别”(这是语句和表达式此类事物的技术名称)的编程语言来说,可能会导致重复劳动。例如,C有两种条件形式,其中之一是语句形式。

if (E) S1; else S2;

以及表达式的形式

E ? E1 : E2

有时人们希望存在并不存在的重复:例如,在标准C中,只有语句才能声明新的局部变量 - 但是这种能力非常有用,因此GNU C编译器提供了一个GNU扩展,使表达式也能声明局部变量。

其他语言的设计者不喜欢这种重复,并且他们很早就看到,如果表达式除了值之外还具有副作用,那么语句和表达式之间的语法区别并不是很有用 - 因此他们取消了它。 Haskell、Icon、Lisp和ML都是没有语法语句的语言 - 它们只有表达式。 即使是类结构化的循环和条件形式也被视为表达式,并且它们具有值 - 但不是非常有趣的值。


9
如果我没有误解你的意思,你似乎声称"(setf (third foo) 'goose)"是一个表达式而不是语句,这是因为它是Lisp,Lisp "没有语句",而且Lisp比C要早十多年,C是"最早模糊表达式和语句之间界限的流行语言"。你能否向我解释一下其中的细节? 如果我没有理解错误,你似乎声称"(setf (third foo) 'goose)"是一个表达式而不是语句,原因是它是Lisp,而Lisp“不包含语句”,此外,Lisp比C还要早十多年,而C是“最早模糊表达式和语句之间界限的流行语言”。你能否向我详细解释一下这个问题? - cjs
2
@Curt Sampson,你有没有把这个问题作为一个单独的问题来问过? - Kelly S. French
6
如果我没记错的话,callfunc(x = 2);x 传递给了 callfunc,而不是传递了数字 2。如果 x 是浮点数,则调用 callfunc(float) 而不是 callfunc(int)。在 C++ 中,如果你把 x=y 传递给了 func,并且 func 接收一个引用并对其进行更改,那么它会更改 x,而不是 y - Gabriel
在上面的答案中,写道:“Haskell,...都是没有语法语句的语言 - 它们只有表达式”。我很好奇为什么Haskell中的where子句被认为是一个表达式而不是语句。 http://learnyouahaskell.com/syntax-in-functions#where - skgbanga
您可以查看以下答案:https://dev59.com/77Dla4cB1Zd3GeqP7Fwb#62608498 - Pluto65
显示剩余3条评论

26
  • 表达式是任何产生值的内容:2 + 2
  • 语句是程序执行的基本“块”之一。

请注意,在C中,“=”实际上是一个运算符,它有两个作用:

  • 返回右边子表达式的值。
  • 将右边子表达式的值复制到左侧变量中。

这是ANSI C语法的一部分。您可以看到,C没有太多不同类型的语句...程序中大多数语句都是表达式语句,即在末尾带有分号的表达式。

statement
    : labeled_statement
    | compound_statement
    | expression_statement
    | selection_statement
    | iteration_statement
    | jump_statement
    ;

expression_statement
    : ';'
    | expression ';'
    ;

http://www.lysator.liu.se/c/ANSI-C-grammar-y.html


2
语句是什么的逻辑错误。声明式程序也可以执行,但声明式程序没有语句。语句是有副作用的(即是命令式的)。参见我的回答 - Shelby Moore III

16

表达式是返回值的东西,而语句则不是。

例如:

1 + 2 * 4 * foo.bar()     //Expression
foo.voidFunc(1);          //Statement

两者的重要区别在于表达式可以链接在一起,而语句无法链接。


6
肯定语句可以链接在一起形成一个链式结构,例如{stmt1; stmt2; stmt3;}就是一个链式结构,并且它本身也是一个(复合)语句。 - Hugh Allen
8
foo.voidFunc(1);是一个返回void类型值的表达式。 whileif是语句。 - tzot
我对语句的非链接性很好奇。像“if(x>1)return;”这样的语句是否被认为是将两个语句链接在一起? - Simon Elms
1
@SimonTewsi 我认为 return 被视为子语句。 - RastaJedi
1
@SimonTewsi 这里的返回语句隐式地位于 if 语句块内部,因此它是 if 语句的一部分,而不是与其链接。编译器允许我们在这里省略大括号,因为它是一个单行块。 - user2597608

11
您可以在维基百科上找到相关内容,但表达式会被计算出一个值,而语句没有计算出的值。
因此,表达式可以用于语句中,但语句不能用于表达式中。
请注意,一些编程语言(如Lisp、Ruby等)不区分语句和表达式…在这些语言中,所有内容都是表达式,并且可以与其他表达式链接使用。

9

关于表达式与语句的可组合性(链式调用)之间的重要差异,我最喜欢的参考资料是约翰·巴克斯的图灵奖论文:Can programming be liberated from the von Neumann style?

命令式语言(Fortran、C、Java等)强调使用语句来构建程序,并将表达式作为一种附带思想。函数式语言则强调表达式。纯粹的函数式语言具有如此强大的表达式,以至于语句可以完全被消除。


7
我不太满意这里的任何答案。我查看了C++ (ISO 2008)的语法。但出于教学和编程的目的,也许这些答案已足以区分这两个元素(然而现实更为复杂)。
语句由零个或多个表达式组成,但也可以是其他语言概念。以下是该语法的扩展Backus Naur格式(摘录自语句部分):
statement:
        labeled-statement
        expression-statement <-- can be zero or more expressions
        compound-statement
        selection-statement
        iteration-statement
        jump-statement
        declaration-statement
        try-block

我们可以看到C++中被视为语句的其他概念。
- 表达式语句(expression-statement)很容易理解(一个语句可以由零个或多个表达式组成,请仔细阅读语法,这有点棘手)。 - 例如,case是一个标记语句(labeled-statement)。 - 选择语句(selection-statement)包括if、if/else和case。 - 迭代语句(iteration-statement)包括while、do...while和for(...)。 - 跳转语句(jump-statement)包括break、continue、return(可以返回表达式)和goto。 - 声明语句(declaration-statement)是一组声明。 - try块(try-block)是表示try/catch块的语句。 - 除此之外,在语法中可能还有更多。
以下是关于表达式部分的摘录:
expression:
        assignment-expression
        expression "," assignment-expression
assignment-expression:
        conditional-expression
        logical-or-expression assignment-operator initializer-clause
        throw-expression
  • 表达式通常包含赋值语句
  • 条件表达式(听起来有点误导)指的是使用运算符(+-*/&|&&||等)
  • 抛出表达式 - 嗯?throw子句也是一种表达式

1
最近我开始对这个话题产生兴趣,但不幸的是,在stackoverflow上经常出现这种情况,即低票答案可能是列表中唯一“正确”的答案。简单地说,“a=b”可以根据上下文是表达式或语句。换句话说,它不是语法元素的某种固有属性,而是由编译器使用的语法(隐含或显式)定义的上下文来确定表达式与语句。 - Frank

7

表达式可以被计算得到一个值,而语句则不返回任何值(它们的类型是void)。

函数调用表达式也可以被视为语句,但除非执行环境有一个特殊的内置变量来保存返回值,否则无法检索它。

面向语句的语言要求所有过程都是语句列表。面向表达式的语言,可能所有函数式语言,都是表达式列表,或者在LISP的情况下,是表示表达式列表的一个长S表达式。

虽然两种类型都可以组合,但大多数表达式可以任意组合,只要类型匹配即可。每种语句类型都有自己组合其他语句的方式,如果它们能够这样做的话。foreach和if语句要求单个语句或所有下属语句放在一个语句块中,一个接一个地排列,除非子语句允许它们自己的子语句。

语句也可以包含表达式,其中表达式实际上不包含任何语句。一个例外是lambda表达式,它表示一个函数,因此可以包含任何函数可以包含的内容,除非语言只允许有限的lambda,比如Python的单表达式lambda。

在基于表达式的语言中,函数只需要一个单一的表达式,因为所有控制结构都返回一个值(其中很多返回NIL)。不需要return语句,因为函数中最后计算的表达式就是返回值。


语句的类型是底部类型。Void不是底部类型。请参见我的答案 - Shelby Moore III
1
“_null type_” 不是底层类型(只有一个值 null)吗?void 不更像是 _unit type_(但其单一值无法访问)吗? - Mark Cidade
如果void是一个从不返回的函数(例如,抛出错误的函数)的返回类型,那么它就是底部类型。否则,void单元类型。你说得对,一个不可能分歧的语句具有单元类型。但是,一个可能分歧的语句就是底部类型。由于停机定理,我们通常无法证明一个函数不会分歧,所以我认为单元类型是虚构的。底部类型不能有值,因此它不能有一个null的单一值。 - Shelby Moore III
1
关于我三年前所说的,我不知道我是否仍然认为语句具有“void”类型或任何类型。在我熟悉的基于语句的语言中,只有值和任何存储或返回值的东西(例如表达式、变量、成员和函数)可以具有类型。我通常将底部类型视为空集(没有值),因此任何本体上不存在的东西都将具有此类型。 “null”值实际上是一个__伪值__,表示引用指向不存在的东西。 - Mark Cidade
1
Mark,我很欣赏你回复的理性。你基本上已经说出了我的心里话,而且我希望你清楚地知道,我承认你提出单元点是正确的。我认为我们达成了一致。我本来不想提及这个问题,但似乎有些人认为我很消极。我只是试图客观陈述事实。 - Shelby Moore III
显示剩余8条评论

5

关于表达式语言的一些事情:


最重要的是:所有东西都返回一个值


在基于表达式的语言中,花括号和大括号没有区别,用于分隔代码块和表达式,因为所有东西都是表达式。但这并不妨碍词法作用域:例如,可以为其定义所包含的表达式以及其中包含的所有语句定义本地变量。


在基于表达式的语言中,所有东西都返回一个值。一开始可能会有点奇怪——(FOR i = 1 TO 10 DO (print i))返回什么?

以下是一些简单的例子:

  • (1) 返回 1
  • (1 + 1) 返回 2
  • (1 == 1) 返回 TRUE
  • (1 == 2) 返回 FALSE
  • (IF 1 == 1 THEN 10 ELSE 5) 返回 10
  • (IF 1 == 2 THEN 10 ELSE 5) 返回 5

以下是一些更复杂的例子:

  • 某些东西,如某些函数调用,实际上没有有意义的返回值(只产生副作用的东西?)。调用OpenADoor(),FlushTheToilet()TwiddleYourThumbs()将返回某种平凡的值,例如OK、Done或Success。
  • 当在一个较大的表达式中评估多个未链接的表达式时,最后一个被评估的东西的值成为较大表达式的值。以(FOR i = 1 TO 10 DO (print i))为例,for循环的值是“10”,它会导致(print i)表达式被评估10次,每次返回i作为字符串。最后一次返回10,是我们的最终答案。

要充分利用基于表达式的语言,通常需要稍微改变一下思维方式,因为所有东西都是表达式,所以可以内联很多东西。

以下是一个快速示例:

 FOR i = 1 to (IF MyString == "Hello, World!" THEN 10 ELSE 5) DO
 (
    LotsOfCode
 )

对于非表达式的替代,<c:out>是一个完全有效的选择。

IF MyString == "Hello, World!" THEN TempVar = 10 ELSE TempVar = 5 
FOR i = 1 TO TempVar DO
(    
    LotsOfCode  
)
在某些情况下,基于表达式的代码所允许的布局对我来说感觉更自然。当然,这可能会导致疯狂。作为一种基于表达式的脚本语言MaxScript的业余项目的一部分,我设法想出了这个超长的代码行。
IF FindSectionStart "rigidifiers" != 0 THEN FOR i = 1 TO (local rigidifier_array = (FOR i = (local NodeStart = FindsectionStart "rigidifiers" + 1) TO (FindSectionEnd(NodeStart) - 1) collect full_array[i])).count DO
(
    LotsOfCode
)

5
这些概念的实际基础是:
表达式:一种语法类别,其实例可被计算为一个值。
语句:一种语法类别,其实例可能与表达式的计算相关联,但计算结果(如果有)不能保证可用。
除了FORTRAN在早期几十年的最初上下文之外,所接受答案中关于表达式和语句的定义显然是错误的。
  • 表达式可以是未求值的操作数,从来不会产生值。
    • 在非严格求值中,子表达式可以明确未求值。
      • 大多数类 C 语言都有所谓的短路求值规则,可以有条件地跳过一些子表达式求值,尽管具有副作用但不改变最终结果。
    • C 和一些类 C 语言有未求值操作数的概念,这甚至可能在语言规范中得到规范定义。这样的构造被用于避免明确求值,因此可以静态区分剩余的上下文信息(例如类型或对齐要求),而不改变程序转换后的行为。
      • 例如,作为 sizeof 运算符操作数的表达式永远不会求值。
  • 语句与行结构无关。它们可以执行比表达式更多的操作,具体取决于语言规范。
    • 现代 Fortran 作为老 FORTRAN 的直接继承者,拥有可执行语句和非可执行语句的概念。
    • 同样,C++ 将声明定义为翻译单元的顶级子类别。在 C++ 中,声明是一条语句。(在 C 中并非如此。)还有类似 Fortran 可执行语句的表达式语句。
    • 为了与表达式进行比较,只有“可执行”语句很重要。但是你不能忽略这样一个事实:语句已经被泛化为形成这些命令式语言中的翻译单元构造。因此,正如你所见,该类别的定义差异很大。这些语言中(可能)唯一保留的共同属性就是期望按词法顺序解释语句(对于大多数用户而言,从左到右、从上到下)。
(BTW,关于C语言材料的答案,我希望加上[citation needed],因为我不记得DMR是否有这样的观点。如果没有,那么在C的设计中保留功能重复是没有理由的:特别是逗号运算符与语句相对应。)
(以下的原理不是对原始问题的直接回答,但我认为有必要澄清已经在这里回答过的一些事情。)
然而,值得怀疑的是,我们在通用编程语言中需要一个特定类别的“语句”:
  • 通常情况下,语句在语义能力上并不一定比表达式更强大。
    • 许多语言已经成功地放弃了语句的概念,以获得整洁、简明和一致的总体设计。
    • 在这些语言中,表达式可以做到旧式语句所能做的一切:在表达式被评估时,只需丢弃未使用的结果,要么通过明确未指定结果(例如在 RnRS Scheme 中),要么通过具有特殊值(作为单位类型的值)的方式。
    • 表达式的词法顺序规则可以由显式的序列控制运算符(例如 Scheme 中的 begin)或单子结构的语法糖来替换。
    • 其他种类的“语句”的词法顺序规则可以通过语法扩展(例如使用卫生宏)派生出来,以获得类似的语法功能。(实际上它可以 做更多。)
  • 相反,语句不能有这样的常规规则,因为它们在评估时无法组合:没有“子语句评估”的共同概念。(即使有任何,我怀疑除了从现有的表达式评估规则中复制和粘贴之外,不可能有更多。)
    • 通常,保留语句的语言也会有表达式来表示计算,并且保留了一个子类别的语句来进行该子类别的表达式评估。例如,C++ 有所谓的表达式语句作为子类别,并使用废弃值表达式评估规则来指定这种上下文中的完整表达式评估的一般情况。一些语言(如 C#)选择细化上下文以简化使用情况,但这使规范变得更加臃肿。
  • 对于编程语言的用户来说,语句的重要性可能会进一步困扰他们。
    • 语言中表达式和语句的规则分离需要更多的学习语言的努力。
    • 天真的词法顺序解释隐藏了更重要的概念:表达式评估。(这可能是最具问题性的方面。)
      • 即使语句中的完整表达式的评估受到词法顺序的限制,子表达式也不是(必然)。除了与语句耦合的任何规则之外,用户应该最终学会这一点。(考虑如何让新手明白在 C 中 ++i + ++i 是无意义的。)
      • 一些语言(如 Java 和 C#)进一步限制子表达式的评估顺序,以容忍对评估规则的无知。这可能会更加问题。
        • 这似乎对已经了解表达式评估思想的用户来说是过度指定的。它还鼓励用户社区遵循模糊的语言设计心理模型。
        • 它使语言规范变得更加臃肿。
        • 它对于通过在引入更复杂的原语之前利用非确定性的表达能力来进行优化是有害的。
      • 一些
        所以为什么要使用语句?反正历史已经一团糟了。似乎大多数语言设计者并不认真考虑自己的选择。
        更糟糕的是,它甚至给一些类型系统爱好者(他们对PL历史不太熟悉)带来了一些误解,即类型系统必须与操作语义规则的更基本设计有重要关联。
        严肃地说,在许多情况下,基于类型的推理并不那么糟糕,但在这种特殊情况下并非建设性。即使是专家也可能会搞砸事情。
        例如,有人强调良好的打字特性作为反对未限定续延传统治疗的中心论点。尽管结论有些合理,关于组合函数的见解也还可以(但仍然过于天真到本质),但这个论点不成立,因为它完全忽略了实践中的“侧信道”方法,比如在C11中编码Falsum_Noreturn any_of_returnable_types严格来说,具有不可预测状态的抽象机器并不等同于“崩溃的计算机”。

  • 5
    简单来说,表达式会计算出一个值,而语句则不会。

    那么一个语句是做什么的?什么都不做吗? - Shelby Moore III
    1
    它可以执行某些操作,但不会返回任何值。也就是说,你不能将其结果赋值给一个变量,而表达式可以这样做。 - Matthew Schinckel
    因此,正如我那个被严重踩的回答所述,语句必须具有副作用。语句可能还有什么其他用途呢?即使将NO-OP视为语句(仅在语法上是“语句”,但在语义层面上它被解析后就被擦除了,而我们正在讨论的是语义),这也无法解释语句的一般效用。 - Shelby Moore III
    1
    @ShelbyMooreIII 语句不需要做任何事情或产生副作用。例如,{}是一个语句。加上引号并不会改变这一点。语句是具有语义的语法结构。不存在所谓的“语义层”——您似乎在指的是执行。您说您试图准确,但您失败了。您对“投票者无知”的抱怨纯粹是以人身攻击为主;您没有关于投票者心理状态的信息。 - Jim Balter
    是的,除了那些不诚实的人之外,每个人都是错的。在 C# 语言规范中,{} 被定义为一个语句。 - Jim Balter
    显示剩余4条评论

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