为什么赋值语句返回值?


126

这是允许的:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

据我了解,赋值s = ”Hello”;应该只导致“Hello”被赋值给s,但操作不应返回任何值。如果是这样,((s = "Hello") != null)则将产生错误,因为null将其与没有进行比较。

允许赋值语句返回值的背后原因是什么?


44
作为参考,此行为在C#规范中定义:“简单赋值表达式的结果是分配给左操作数的值。该结果与左操作数具有相同的类型,并且始终归为一个值。”
Michael Petrotta 2010年

1
@Skurmedel我不是拒绝投票的人,我同意你的看法。但是也许是因为这被认为是一个修辞问题?
jsmith

在pascal / delphi中,:=不返回任何内容。我讨厌它。
安德烈2010年

20
C分支中的语言标准。分配是表达式。
汉斯·帕桑

“赋值...仅应将“ Hello”赋值给s,但该操作不应返回任何值” –为什么?“允许赋值语句返回值的背后原因是什么?” -因为它很有用,如您自己的示例所示。(嗯,您的第二个例子很愚蠢,但while ((s = GetWord()) != null) Process(s);不是)。
Jim Balter

Answers:


160

据我了解,赋值s =“ Hello”; 应该只将“ Hello”分配给s,但是该操作不应返回任何值。

您的理解是100%错误的。你能解释为什么你相信这假话吗?

允许赋值语句返回值的背后原因是什么?

首先,赋值语句不产生值。赋值表达式产生一个值。作业表达是法律声明;在C#中只有少量的表达式是合法的语句:等待表达式,实例构造,增量,减量,调用和赋值表达式可用于需要语句的地方。

C#中只有一种表达式不会产生某种值,即,调用键入为return void的某种东西。(或者等效地,等待没有相关结果值的任务。)每种其他类型的表达式都会产生值或变量,引用或属性访问或事件访问,等等。

请注意,所有合法的作为语句的表达式对于其副作用都是有用的。这是这里的关键见解,我想也许是你的直觉认为分配应该是语句而不是表达式。理想情况下,每个语句只具有一个副作用,而表达式中没有副作用。它一个有点奇怪副作用的代码可以在所有的表达式上下文中使用。

允许使用此功能的原因是因为(1)它经常很方便,并且(2)在类似C的语言中是惯用语言。

可能有人注意到这个问题已经被乞求了:为什么这种类似C语言的语言很惯用?

不幸的是,丹尼斯·里奇(Dennis Ritchie)不再可以询问,但是我的猜测是,赋值几乎总是留下刚刚在寄存器中赋值的值。C是一种非常“接近机器”的语言。似乎很合理,并且与C的设计保持一致,语言特性基本上意味着“继续使用我刚刚分配的值”。为此功能编写代码生成器非常容易。您只需继续使用存储已分配值的寄存器。


1
不知道任何返回值的东西(甚至实例构造也被视为表达式)
user437291 '09

9
@ user437291:如果实例构造不是表达式,则无法对构造的对象执行任何操作-无法将其分配给某些对象,无法将其传递给方法,并且无法调用任何方法在上面。会变得毫无用处,不是吗?
Timwi's

1
更改变量实际上是“仅”赋值运算符的副作用,赋值运算符创建表达式后会产生一个值作为其主要用途。
mk12

2
“您的理解是100%错误的。” -虽然OP对英语的使用不是最大的,但是他/她的“理解”是关于应该是什么情况,使它成为一种意见,所以这不是“ 100%错误”的事情。一些语言设计师同意OP的“应该”,并且使赋值没有价值,或者禁止它们成为子表达式。我认为这是对=/ ==错字的过度反应,可以通过禁止使用/的值(=除非带括号)来轻松解决。例如,if ((x = y))if ((x = y) == true)允许,但if (x = y)不允许。
吉姆·巴尔特

“不知道任何返回值的东西都被认为是一个表达式” –嗯……所有返回值的东西都被认为是一个表达式-两者实际上是同义词。
Jim Balter

44

您没有提供答案吗?这是为了完全启用您提到的结构。

使用赋值运算符的此属性的常见情况是从文件中读取行...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...

1
+1这必须是此构造的最实际用途之一
Mike Burton 2010年

11
对于非常简单的属性初始值设定项,我真的很喜欢它,但是您必须要小心,将行设置为“简单”,以降低可读性:return _myBackingField ?? (_myBackingField = CalculateBackingField());比检查null和赋值要少得多。
汤姆·梅菲尔德,2010年

34

我最喜欢的赋值表达式用法是用于延迟初始化的属性。

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}

5
这就是为什么这么多人建议??=语法的原因。
配置器

27

首先,它允许您链接分配,如示例所示:

a = b = c = 16;

另外,它允许您在单个表达式中分配和检查结果:

while ((s = foo.getSomeString()) != null) { /* ... */ }

两者都有可能是可疑的原因,但是肯定有人喜欢这些结构。


5
+1链接不是关键功能,但是很高兴看到它在很多事情都不起作用时“可以正常使用”。
Mike Burton 2010年

+1也用于链接;我一直认为这是一种由语言处理的特殊情况,而不是“表达式返回值”的自然副作用。
卡莱布·贝尔

2
它还允许您在收益中使用分配:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz 2013年

14

除了已经提到的原因(分配链,在while循环内进行设置和测试等)之外,要正确使用该using语句,您还需要以下功能:

using (Font font3 = new Font("Arial", 10.0f))
{
    // Use font3.
}

MSDN不鼓励在using语句之外声明可抛弃对象,因为即使将其处置后,它仍将保留在范围内(请参阅我链接的MSDN文章)。


4
我认为这本身并不是分配链。
肯尼2010年

1
@kenny:嗯...不,但我也从未声称过。就像我说的那样,除了已经提到的原因(包括赋值链接)之外,如果不是因为赋值运算符返回赋值操作的结果,那么using语句将很难使用。这与分配链根本不相关。
MartinTörnwall10年

1
但是正如Eric上面指出的那样,“赋值语句不返回值”。这实际上只是using语句的语言语法,这证明不能在using语句中使用“赋值表达式”。
肯尼2010年

@kenny:抱歉,我的用语显然是错误的。我确实意识到,可以在没有通用支持返回值的赋值表达式的语言的情况下实现using语句。在我看来,那将是非常不一致的。
MartinTörnwall10年

2
@kenny:其实,一个可以使用的定义和赋值语句,在任何表情using。所有这些都是合法的:using (X x = new X())using (x = new X())using (x)。但是,在此示例中,using语句的内容是一种特殊的语法,它完全不依赖于返回值的赋值- Font font3 = new Font("Arial", 10.0f)不是表达式,并且在任何需要表达式的地方均无效。
配置器

10

我想详细说明埃里克·利珀特(Eric Lippert)在回答中提出的具体观点,并把焦点放在任何其他人都没有触及的特定场合。埃里克(Eric)说:

[...]分配几乎总是留下刚在寄存器中分配的值。

我想说的是,赋值将始终留下我们尝试分配给左操作数的值。不只是“几乎总是”。但是我不知道,因为我没有在文档中发现此问题。从理论上讲,“离开”而不重新评估左操作数可能是一个非常有效的实现过程,但是有效吗?

到目前为止,针对此线程答案构建的所有示例均“有效”。但是对于使用get和set访问器的属性和索引器而言,效率很高吗?一点也不。考虑以下代码:

class Test
{
    public bool MyProperty { get { return true; } set { ; } }
}

这里有一个属性,它甚至不是私有变量的包装。每当有人要求他返回真相时,每当有人试图设定自己的价值时,他就什么都不做。因此,每当评估此属性时,他就应该诚实。让我们看看发生了什么:

Test test = new Test();

if ((test.MyProperty = false) == true)
    Console.WriteLine("Please print this text.");

else
    Console.WriteLine("Unexpected!!");

猜猜它打印了什么?它打印Unexpected!!。事实证明,确实调用了set访问器,它什么也不做。但是此后,根本不会调用get访问器。分配只是留下了false我们尝试分配给属性的值。而此false值是if语句求值的值。

我将以一个使我研究这个问题的真实示例作为结尾。我创建了一个索引器,该索引器是我List<string>的类作为私有变量的集合()的便捷包装。

发送到索引器的参数是一个字符串,该字符串将被视为我的集合中的值。如果该值存在于列表中,则get访问器将仅返回true或false。因此,get访问器是使用该List<T>.Contains方法的另一种方法。

如果以字符串作为参数调用了索引器的set访问器,而正确的操作数是bool true,则他会将该参数添加到列表中。但是,如果将相同的参数发送给访问器,并且正确的操作数是bool false,他将改为从列表中删除该元素。因此set访问被用作方便的替代都List<T>.AddList<T>.Remove

我以为我有一个简洁紧凑的“ API”,其中包含我自己的逻辑(作为网关)来包装列表。仅依靠索引器的帮助,我就可以通过几次击键来完成很多事情。例如,如何尝试将值添加到列表中并验证其是否在其中?我认为这是唯一需要的代码行:

if (myObject["stringValue"] = true)
    ; // Set operation succeeded..!

但是,正如我之前的示例所示,甚至没有调用应该查看该值是否确实在列表中的get访问器。该true值始终被有效地破坏了我在get访问器中实现的任何逻辑。


非常有趣的答案。这使我对测试驱动的开发产生了积极的思考。
Elliot

1
尽管这很有趣,但这是对Eric的回答的评论,而不是对OP问题的回答。
Jim Balter

我不希望尝试的赋值表达式首先获取目标属性的值。如果变量是可变的,并且类型匹配,为什么旧​​值是什么呢?只需设置新值即可。不过,我不喜欢IF测试中的作业。它是由于分配成功而返回true,还是因为它是某个值(例如,强制为true的正整数),或者是因为您正在分配布尔值?不管执行什么,逻辑都是模棱两可的。
达沃斯

6

如果赋值未返回值,则该行a = b = c = 16也将不起作用。

while ((s = readLine()) != null)有时也可以写类似的东西。

因此,让分配返回分配的值的原因是让您执行这些操作。


没有人提到过它的缺点-我来这里想知道为什么赋值不能返回null,以使常见的赋值与比较错误“ if(something = 1){}”无法编译,而不是总是返回true。我现在看到那会造成其他问题!
2014年

1
@Jon可以通过禁止或警告=没有括号的表达式中的错误(不计算if / while语句本身的括号)来防止该错误。gcc发出此类警告,从而从根本上消除了与其一起编译的C / C ++程序中的此类错误。遗憾的是,其他编译器作者却很少关注此问题以及gcc中的其他好点子。
Jim Balter

4

我认为您误会了解析器将如何解释该语法。分配将被评估第一,然后将结果进行比较,以NULL,即语句相当于:

s = "Hello"; //s now contains the value "Hello"
(s != null) //returns true.

正如其他人指出的那样,赋值的结果就是赋值。我很难想象拥有

((s = "Hello") != null)

s = "Hello";
s != null;

等同...


从实际的意义上说,您的两行是正确的,但您的说法是赋值未返回值在技术上是不正确的。我的理解是赋值的结果是一个值(根据C#规范),从这个意义上说,它与1 + 2 + 3
Steve Ellinger 2010年

3

我认为主要原因是与C ++和C的(故意)相似。使赋值运算符(以及许多其他语言构造)的行为像其C ++对应物一样,仅遵循最小惊奇的原则,并且任何来自另一个卷曲的程序员方括号语言可以使用它们而无需花很多时间。对于C ++程序员而言,易于掌握是C#的主要设计目标之一。


2

出于以下两个原因,您需要在帖子
1)中添加内容,以便可以进行a = b = c = 16
2),以便测试作业是否成功 if ((s = openSomeHandle()) != null)


1

'a ++'或'printf(“ foo”)'既可以用作独立语句,也可以用作较大表达式的一部分,这一事实意味着C必须考虑表达式结果可能是也可能不是的可能性。用过的。鉴于此,人们普遍认为,可能有用地“返回”值的表达式也可以这样做。如果所有有问题的变量都不具有完全相同的类型,则分配链在C中可能有点“有趣”,在C ++中甚至更有趣。最好避免使用这种用法。


1

另一个很好的示例用例,我一直使用:

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once

0

我没有在此处的答案中看到的另一个优点是赋值的语法基于算术。

现在x = y = b = c = 2 + 3在算术上的含义不同于C风格的语言。在算术上是一个断言,我们声明 x等于y等,而在C语言中,这是一条使 x等于y等的指令。

这就是说,算术和代码之间仍然存在足够的关系,除非有充分的理由,否则不允许算术中的自然现象是没有道理的。(C风格语言使用equals符号的另一件事是使用==进行相等比较。这里,尽管由于最右边的==返回值,所以这种链接是不可能的。)


@JimBalter我想知道五年后您是否真的会反对。
乔恩·汉娜

@JimBalter不,您的第二点评论实际上是一个反驳的论点,也是一个合理的论点。我的思想反驳是因为您的第一条评论没有。不过,在学习算术时,我们学习的a = b = c意思是abc是同一回事。在学习一个C语言风格我们得知后a = b = cab有同样的事情c。正如我的回答本身所言,语义上肯定存在差异,但是还是在我小的时候,我第一次学习用一种确实=用于赋值的语言编程,但a = b = c对我来说却不允许这样做,但是……
Jon Hanna

…不可避免,因为该语言也用于=相等性比较,因此,这a = b = c必须a = b == c表示C风格语言的含义。我发现C中允许的链接更加直观,因为我可以对算术进行类比。
乔恩·汉娜

0

当我需要更新一堆东西并返回是否有任何更改时,我喜欢使用赋值返回值:

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

不过要小心。您可能会认为可以将其缩短为:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

但这实际上会在找到第一个为真后停止评估or语句。在这种情况下,这意味着一旦分配了第一个不同的值,它将停止分配后续值。

看到https://dotnetfiddle.net/e05Rh8玩这个

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.