让赋值运算符返回值有什么好处?


27

我正在开发一种语言,打算取代Javascript和PHP。(我看不出有任何问题。这两种语言都不具有庞大的安装基础。)

我要更改的一件事是将赋值运算符转换为赋值命令,从而取消了使用返回值的功能。

x=1;          /* Assignment. */
if (x==1) {}  /* Comparison. */
x==1;         /* Error or warning, I've not decided yet. */
if (x=1) {}   /* Error. */

我知道这将意味着C员工如此钟爱的单行功能将不再起作用。我发现(几乎没有我个人经验的证据),这种情况在绝大多数情况下确实是比较操作。

还是?赋值运算符的返回值是否有任何实际用途,无法轻易重写?(对于具有这种概念的任何语言。)


12
JS和PHP没有很大的“安装基础”吗?
mhr 2014年

47
@mri我怀疑很讽刺。
安迪·亨特

12
我能记住的唯一有用的情况是while((x = getValue()) != null) {}。由于您需要使用break或重复x = getValue分配,因此替换操作会比较麻烦。
CodesInChaos

12
@mri哦,不,我听说这两种语言都是琐碎的事情,根本没有任何重大投资。一旦少数坚持使用JS的人看到了我的语言,他们将切换到我的语言,而不必再次编写===。我同样确定浏览器制造商将立即推出包含我的语言和JS的更新。:)
billpg

4
我会向您建议,如果您打算增强现有语言并且打算将其广泛采用,那么与现有语言100%向后兼容是很好的。请参阅TypeScript作为示例。如果您打算为现有语言提供更好的替代方法,那么您将遇到更加困难的问题。为了支付转换的费用​​,一种新的语言必须比现有的语言更好地解决现有的现实问题。学习语言是一种投资,它需要回报。
埃里克·利珀特

Answers:


25

从技术上讲,即使语法语法糖可以提高一些常规操作的可读性,即使可以简单地替换掉,它也值得保留。但是,分配即表达不属于此范围。在比较中输入错误的危险意味着它很少使用(有时甚至受到样式指南的禁止),并且每次使用时都会引起重复。换句话说,可读性的好处是数量和幅度都较小。

看看这样做的现有语言可能是值得的。

  • Java和C#保留赋值表达式,但通过要求条件评估为布尔值来消除您提到的陷阱。尽管人们偶尔会抱怨说这不允许if (x)在诸如if (x != null)if (x != 0)取决于的类型之类的x
  • Python将赋值指定为正确的语句,而不是表达式。更改此建议的建议偶尔会到达python-ideas邮件列表,但我的主观印象是,与其他“缺少”功能(例如do-while循环,switch语句,多行lambda,等等

但是,Python允许一种特殊情况,即一次分配多个名称:a = b = c。这被视为等同于的语句b = c; a = b,并且偶尔使用,因此也有必要添加到您的语言中(但我不会费力,因为该添加应向后兼容)。


5
+1用于提出a = b = c其他答案未真正提出的问题。
狮子座

6
第三种解决方案是使用其他符号进行分配。Pascal :=用于分配。
布莱恩

5
@Brian:的确如此,C#也是如此。=是分配,==是比较。
Marjan Venema 2014年

3
在C#中,类似的东西if (a = true)会抛出C4706警告(The test value in a conditional expression was the result of an assignment.)。与C的GCC也会抛出一个warning: suggest parentheses around assignment used as truth value [-Wparentheses]。这些警告可以用额外的括号来消除,但是在这里鼓励它们明确表明指派是有意的。
鲍勃

2
@delnan只是有点一般性的注释,但它是由“通过要求条件对布尔值求值来消除您提到的陷阱”引发的- a = true 确实求值为布尔值,因此不是错误,但是在C#中也会引发相关警告。
鲍勃

11

赋值运算符的返回值是否有任何实际用途,无法轻易重写?

一般来说,没有。将赋值表达式的值作为已赋值的想法意味着我们有一个表达式,可用于其副作用和其,并且很多人认为这令人困惑。

常用用法通常是使表达式紧凑:

x = y = z;

在C#中具有“将z转换为y的类型,将转换后的值赋给y,转换后的值是表达式的值,将其转换为x的类型,赋给x”的语义。

但是在声明上下文中,我们已经处于命令性副作用的境界,因此与

y = z;
x = y;

M(x = 123);

简写

x = 123;
M(x);

同样,在原始代码中,我们同时使用了一个表达式来表示其副作用和其值,并且我们正在制作一个具有两个副作用而不是一个副作用的语句。两者都臭。尝试使每个语句具有一个副作用,并使用表达式作为其值,而不是其副作用。

我正在开发一种语言,打算取代Javascript和PHP。

如果您真的想大胆地强调赋值是一个语句而不是一个等式,那么我的建议是:明确指出赋值语句

let x be 1;

在那里,完成。要么

x <-- 1;

甚至更好:

1 --> x;

甚至更好

1 → x;

绝对不可能将它们与混淆x == 1


1
世界是否已准备好使用编程语言编写非ASCII Unicode符号?
billpg 2014年

我很喜欢您的建议,我的目标之一是可以对大多数“写得很好”的JavaScript进行很少或没有修改的移植。
billpg

2
@billpg:世界准备好了吗?我不知道-在Unicode发明问世几十年之前,1964年世界就为APL准备好了吗?这是APL中的程序,该程序从前40个数字中随机选择六个数字:x[⍋x←6?40] APL需要自己的特殊键盘,但这是一种非常成功的语言。
埃里克·利珀特

@billpg:Macintosh程序员工作室使用非ASCII符号进行正则表达式标签或stderr重定向之类的操作。另一方面,MPW的优势在于Macintosh使键入非ASCII字符变得容易。对于为什么美国键盘驱动程序没有提供任何体面的键入任何非ASCII字符的方式,我必须感到困惑。Alt-数字输入不仅需要查找字符代码-在许多应用程序中甚至不起作用。
supercat

嗯,为什么会有人喜欢分配“右边”这样的东西a+b*c --> x?这在我看来很奇怪。
Ruslan

9

许多语言确实选择使赋值成为语句而不是表达式的途径,包括Python:

foo = 42 # works
if foo = 42: print "hi" # dies
bar(foo = 42) # keyword arg

和Golang:

var foo int
foo = 42 # works
if foo = 42 { fmt.Printn("hi") } # dies

其他语言没有赋值,而是有范围的绑定,例如OCaml:

let foo = 42 in
  if foo = 42 then
    print_string "hi"

但是,let是表达本身。

允许赋值的好处是我们可以直接在条件中检查函数的返回值,例如在以下Perl代码段中:

if (my $result = some_computation()) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}

Perl另外将声明的范围限定为该条件,这使其非常有用。如果在没有声明新变量的情况下在条件内部赋值,if ($foo = $bar)也会发出警告- 会警告,if (my $foo = $bar)不会。

通常在另一条语句中进行赋值就足够了,但是会带来范围界定问题:

my $result = some_computation()
if ($result) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}
# $result is still visible here - eek!

Golang在很大程度上依赖于返回值进行错误检查。因此,它允许条件语句执行初始化语句:

if result, err := some_computation(); err != nil {
  fmt.Printf("Failed with %d", result)
}
fmt.Printf("We succeeded, and the result is %d\n", result)

其他语言使用类型系统禁止条件内的非布尔表达式:

int foo;
if (foo = bar()) // Java does not like this

当然,使用返回布尔值的函数会失败。

现在,我们已经看到了防止意外分配的不同机制:

  • 禁止将赋值作为表达式
  • 使用静态类型检查
  • 分配不存在,我们只有let绑定
  • 允许初始化语句,否则不允许分配
  • 禁止在没有声明的条件内分配

我已经按照偏好的升序对它们进行了排序-表达式内的赋值可能是有用的(并且通过使用显式声明语法和不同的命名参数语法来规避Python的问题很简单)。但是也可以禁止它们,因为还有许多其他选择可以达到相同的效果。

无错误的代码比简洁的代码更重要。


+1表示“不允许将其分配为表达式”。表达式即分配的用例并不能证明存在潜在的错误和可读性问题。

7

您说:“我发现(几乎没有我个人经验的证据),这种情况在绝大多数情况下确实是比较操作。”

为什么不解决问题?

为何不使用==进行分配和==进行相等性测试,为什么不使用:=进行赋值并且使用=(甚至==)进行相等性测试?

观察:

if (a=foo(bar)) {}  // obviously equality
if (a := foo(bar)) { do something with a } // obviously assignment

如果要使程序员更难将分配错误等同于相等性,请使其更难。

同时,如果您确实要解决此问题,则可以删除声称布尔值只是具有预定义符号糖名称的整数的C缸。使其完全不同。然后,而不是说

int a = some_value();
if (a) {}

您强迫程序员编写:

int a = some_value();
if (a /= 0) {} // Note that /= means 'not equal'.  This is your Ada lesson for today.

事实是,作为操作员的分配是一个非常有用的结构。我们没有消除剃须刀,因为有人割伤了自己。相反,吉列国王(Kill Gillette)发明了安全剃刀。


2
(1):=分配和=平等可能解决了这个问题,但代价是疏远了所有没有成长的程序员,他们使用的是一小组非主流语言。(2)条件中允许的布尔类型以外的其他类型并不总是由于布尔和整数混合造成的,对其他类型给出正确/错误的解释就足够了。对于整数以外的其他许多类型,不怕偏离C的较新语言(例如,Python认为空集合为false)。

1
关于剃须刀:那些用于需要锋利度的用例。另一方面,我不认为编程很好需要在表达式求值的中间分配变量。如果有一种简单,技术含量低,安全且具有成本效益的方法可以使体毛消失而没有锋利的边缘,那么我敢肯定,剃须刀片会被移位或者至少变得更加稀有。

1
@delnan:一个聪明的人曾经说过:“使它尽可能简单,但不要简单”。如果您的目标是消除绝大多数a = b与a == b错误,则将条件测试的范围限制为布尔值,并消除<other>-> boolean的默认类型转换规则,将使您几乎一路走好那里。那时,仅当a和b均为布尔值且a为合法左值时,if(a = b){}才在语法上合法。
John R. Strohm 2014年

使赋值成为语句至少与您提出的更改一样简单(可以说比它更简单),并且至少可以实现更多(可以说甚至更多)(甚至不允许if (a = b)使用左值a,布尔值a,b)。在没有静态类型的语言中,它还会提供更好的错误消息(解析时与运行时)。另外,防止“ a = b vs. a == b错误”可能不是唯一相关的目标。例如,我还想允许类似if items:mean的代码if len(items) != 0,并且我不得不放弃以将条件限制为布尔值。

1
@delnan Pascal是非主流语言吗?数百万人使用Pascal(和/或从Pascal派生的Modula)学习了编程。而且Delphi在许多国家/地区仍然普遍使用(也许在您所用的国家中不是很多)。
jwenting 2014年

5

要实际回答这个问题,是的,尽管它们有些小众,但它有许多用途。

例如在Java中:

while ((Object ob = x.next()) != null) {
    // This will loop through calling next() until it returns null
    // The value of the returned object is available as ob within the loop
}

不使用嵌入式分配的替代方法需要在ob循环范围之外定义,并使用两个单独的代码位置调用x.next()。

已经提到您可以一步分配多个变量。

x = y = z = 3;

这种事情是最常见的用法,但是富有创造力的程序员总是会想出更多的东西。


那while循环条件会在ob每个循环中释放并创建一个新对象吗?
user3932000 '17

@ user3932000在那种情况下,可能不是,通常x.next()对某个对象进行迭代。当然有可能。
Tim B

1

因为你能弥补所有的规则,为什么现在让交作业的值,根本容许有条件的步骤内分配?这使您可以轻松完成初始化的语法糖,同时仍可以防止常见的编码错误。

换句话说,将其合法化:

a=b=c=0;

但这是非法的:

if (a=b) ...

2
这似乎是一个特别的规则。将赋值声明为一个语句并将其扩展为允许a = b = c看起来更正交,并且也更容易实现。这两种方法在表达式(a + (b = c))中的赋值上存在分歧,但您并未对此表示支持,因此我认为它们无关紧要。

“易于实施”不应该被考虑在内。您正在定义一个用户界面-将用户的需求放在首位。您只需要问自己这个行为是对用户有帮助还是阻碍。
Bryan Oakley

如果您不允许隐式转换为bool,则不必担心条件分配
棘手怪胎2014年

易于实现只是我的观点之一。那剩下的呢?从用户界面的角度来看,我可能会补充说,恕我直言,不连贯的设计和特殊例外通常会妨碍用户浏览和内部化规则。

@ratchetfreak您仍然可能在分配实际布尔值方面遇到问题
jk。

0

听起来,您正在创建一种相当严格的语言。

考虑到这一点,迫使人们写:

a=c;
b=c;

代替:

a=b=c;

似乎可以阻止人们这样做:

if (a=b) {

当他们打算这样做时:

if (a==b) {

但是最后,这种错误很容易检测到并警告它们是否为合法代码。

但是,在某些情况下会这样做:

a=c;
b=c;

并不意味着

if (a==b) {

会是真的。

如果c实际上是函数c(),则每次调用它都可能返回不同的结果。(它也可能在计算上也很昂贵...)

同样,如果c是指向内存映射硬件的指针,则

a=*c;
b=*c;

两者可能会有所不同,并且在每次读取时也会对硬件产生电子影响。

硬件还有许多其他排列方式,您需要精确地读取,写入特定时间限制并在特定时间限制下读取哪些存储器地址,其中在同一行上进行多个分配是快速,简单和明显的,而没有时序风险临时变量介绍


4
相当于a = b = c是不是a = c; b = c,它的b = c; a = b。这避免重复的副作用,也保留的修改a,并b以相同的顺序。同样,所有这些与硬件有关的争论都是愚蠢的:大多数语言都不是系统语言,既不是设计用来解决这些问题的,也不是在发生这些问题的情况下使用的。这对于尝试替换JavaScript和/或PHP的语言来说是双重的。

德南,问题不是这些人为的例子,而是这些。仍然要指出的是,它们显示了写a = b = c很常见的地方,并且在硬件情况下,按照OP的要求,这被认为是良好的实践。我确信他们将能够考虑其与预期环境的相关性
Michael Shaw

回想起来,我对这个答案的问题不是主要集中在系统编程用例上(尽管那会很糟糕,写的方式),而是在于假设错误的重写。这些示例不是a=b=c常见/有用的地方的示例,它们是必须注意副作用的顺序和数量的地方的示例。那是完全独立的。正确重写链接的分配并且两个变体都正确。

@delnan:右值b在一个临时模中转换为类型,在另一个临时模中转换为类型a。实际存储这些值的相对时间未指定。从语言设计的角度来看,我认为要求多重赋值语句中的所有左值都具有匹配类型是合理的,并且可能还要求它们都不是易变的。
supercat 2014年

0

在我看来,将赋值作为一种表达的最大好处是,如果您的目标之一是“一切都是表达”,那么它将使您的语法更加简单-特别是LISP的目标。

Python没有这个。它具有表达式和语句,赋值是一个语句。但是因为Python将lambda表单定义为单个参数化表达式,所以这意味着您无法在lambda中分配变量。有时这很不方便,但不是一个关键问题,这是我的经验中让赋值成为Python语句的唯一缺点。

在不引入if(x=1)C 潜在事故的情况下,使赋值(或赋值的效果)成为表达式的一种方法是使用类似LISP的let构造,例如(let ((x 2) (y 3)) (+ x y))用您的语言将其评估为5let如果您定义let为创建词法范围,则从技术上讲,完全不需要使用这种方法来分配语言。通过这种方式定义,let可以使用与构造和调用带参数的嵌套闭包函数相同的方式来编译构造。

另一方面,如果您只关心if(x=1)大小写,但是希望赋值像C中那样是一个表达式,则也许只需选择不同的标记就足够了。作业:x := 1x <- 1。比较:x == 1。语法错误:x = 1


1
let与赋值的不同之处在于,除了在技术上在新范围内引入新变量之外,它还具有更多的区别。对于初学者来说,它对体外的代码没有影响let,因此需要进一步嵌套所有代码(应使用变量的内容),这是繁重的代码的显着缺点。如果要走这条路,那set!将是更好的Lisp类似物-完全不同于比较,但不需要嵌套或新的作用域。

@delnan:我想看看声明和分配的组合语法,该语法将禁止重新分配,但允许重新声明,但要遵守以下规则:(1)重新声明仅对声明和分配标识符合法,并且(2 )重新声明将在所有封闭范围内“取消声明”变量。因此,任何有效标识符的值将是该名称的先前声明中分配的值。这似乎比必须为仅用于几行的变量添加作用域块或必须为每个临时变量指定新的名称要好一些。
supercat 2014年

0

我知道这将意味着C员工如此钟爱的单行功能将不再起作用。我发现(几乎没有我个人经验的证据),这种情况在绝大多数情况下确实是比较操作。

确实。这并不是什么新鲜事,C语言的所有安全子集都已经得出了这个结论。

MISRA-C,CERT-C等均禁止在内部条件下进行分配,仅因为这很危险。

在任何情况下,都无法重写依赖于内部条件分配的代码。


此外,此类标准还警告不要编写依赖评估顺序的代码。在单个行上进行多次分配x=y=z;就是这种情况。如果具有多个分配的行包含副作用(调用函数,访问易失性变量等),则您将不知道首先发生哪种副作用。

计算操作数之间没有序列点。因此,我们无法知道子表达式是y在之前还是之后进行求值的z:它是C中未指定的行为。因此,此类代码可能不可靠,不可移植且与所提到的C的安全子集不符。

解决方法是将代码替换为y=z; x=y;。这增加了一个顺序点并保证了评估的顺序。


因此,基于这在C中引起的所有问题,任何现代语言都可以很好地禁止在内部条件中进行赋值以及禁止在一行中进行多个赋值。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.