【问题标题】:C# loss of intermediate values in multiple assignmentC#在多重赋值中丢失中间值
【发布时间】:2012-11-24 10:49:43
【问题描述】:

鉴于...

int a = 1, b = 4;

那么……

a += b += a += b;  

在 C++ 中评估...

(a += (b += (a += b))); // a = 14[5 + 9] (b = 9[4 + 5] (a = 5[1 + 4]))  

...以及在 C# 中...

(a += (b += (a += b))); // a = 10[1 + 9] (b = 9[4 + 5] (a = 5[1 + 4]))  
                        //        ^ re-use of original value of a (1)  
                        //          instead of expected intermediate right-to-left  
                        //          evaluation result (5)  

以上内容已在 Visual Studio 2008 和 2012 中进行了测试,因此不会怀疑最近引入的语言错误。

但是,我确实希望 C# 的行为模仿 C++ 的行为,并假设我需要接受教育。我了解表达式评估逻辑的 HOW,不需要 MSIL 解释。在进行了相当广泛的搜索后,未能在语言规范中找到相关的细则,我希望语言专家能解释为什么会这样。

对于那些想知道我到底为什么要这样做的好奇者...有一个方便的小 C++ 技巧可以实现两种整数类型的非常有效和整洁的内联交换...

a ^= b ^= a ^= b;  

我很失望地发现它在 C# 中不起作用并且很好奇为什么。这是关于理解 C# 的低级机制及其背后的基本原理。没有更多,也没有更少,特别是没有可读性宗教。

注意 请各位,这不是把所有东西都挤在一条线上的认真努力!!!
对于 C(和后来的 C++),运算符优先级和关联性总是被精确定义。 赋值运算符的标准从右到左关联性使 C/C++ 版本的行为 100% 清晰且可预测。它在多个编译器和平台上都对我有用,我认为没有这样行为的编译器是有问题的。
我承认代码行被混淆了,这是一种好奇心,并被用作我可能给初级 C/C++ 程序员的“脑筋急转弯”。 显然 C# 是不同的 - 显然是故意的。
我寻求解释导致 C# 与其一种祖先语言的行为差异的设计考虑因素。 甚至受过教育欢迎猜测。

已回答 感谢 Alexander Stepaniuk。
从操作数的从左到右求值开始

a        = 1 + theRest1     // (1)  
theRest1 = b = 4 + theRest2 // (2)  
theRest2 = a = 1 + 4        // (3)  
(3) into (2): theRest1 = b = 4 + 5  
(2) into (1): a = 1 + 9  

“为什么”的解释仍将受到赞赏。
但是 C# 规范清楚地表明以上是正确的评估。
下面是尽可能接近(我认为)使用 2 个变量的 C++ 技巧...

b ^= a ^= b;
a ^ = b;

我不喜欢它——为了记录——因为它打破了我的直觉 ;-)

【问题讨论】:

  • 如果您正在寻找这些类型的性能提升,您到底在使用 C# 做什么?我很欣赏这种好奇心,但是....
  • 无论如何,这对任何现代架构都没有性能提升。它可能更慢,而且还有一个等待发生的错误。
  • 虽然优先级和关联性在 C++ 中得到了很好的定义,但计算顺序却没有。

标签: c#


【解决方案1】:

也不保证可以在 C/C++ 中工作。在单个语句中多次赋值的变量的值是未定义的。

它的工作原理取决于实现——你不妨找到一个编译器或架构,它根本不工作。

C# 只是对它严格,这不是坏事。

无论如何,异或技巧在现代架构上毫无意义。底层实现的效率将低于仅使用临时变量。现代 CPU 上有超过 2 个寄存器。

【讨论】:

  • 你可能教会了我一些东西。我一直理解赋值链接(在 C++ 中)精确地(并且仅)起作用,因为赋值语句总是返回其赋值的规范。我不知道有一条规则禁止在一个语句中对单个变量进行多次赋值。请问你有这个花絮的来源/链接吗?
【解决方案2】:

它甚至可以追溯到 c。

K&R 有这个例子:

a[i++] = i;

这是未定义的行为,在一行中修改和使用变量。一种编译器可以一种方式工作,另一种方式工作。该标准故意将其保留为未定义,以便您不依赖它。

K&R 建议使用单独的行或中间变量。不是为了人类可读性,而是为了使编译器的解释明确。

a ^= b ^= a ^= b;

所以这变成了明确的:

a ^= b; b ^= a; a ^= b; // can still on one line if that is important to you!

查看此处了解更多未定义行为:Why are these constructs (using ++) undefined behavior?

【讨论】:

  • C/C++ 中赋值运算符的从右到左的关联性是明确定义的,我的示例没有混合具有不同优先级的运算符(++、[])。
【解决方案3】:

你实际上已经自己解释了一切:)

“重用a(1)的原值”

为什么会这样? 你可以找到很好的解释here

简而言之
a += anything 等价于a = a + anything
所以编译器需要计算表达式a + anything的两个操作数,然后将结果赋值给a:
1. 评估第一个操作数(即a)。评估结果为 1
2. 计算第二个操作数(即expression)。评估结果为 9。评估的副作用是 a 包含 5
3. 计算第一个和第二个操作数的和。结果是 10
4. 结果分配给a。现在a 包含 10 个。

希望对您有所帮助。

更新
根据c# Specification计算表达式中操作数的顺序是从左到右请参阅第 150 页的 §14.2。
因此,此行为在 C# 中已指定且正确。

【讨论】:

  • 我的问题不是发生了什么,而是为什么?为什么 C# 似乎从右到左工作... 为 a 分配一个新值,然后使用 a 的新值为 b 分配一个新值,然后将 a 的旧值添加到 b 的新值以进行最终分配? C# 运算符优先级和关联性规则与 C/C++ 的赋值规则相同。那么为什么会有行为差异呢?
  • 我已经参考规范更新了我的答案。 C# 计算二进制表达式从左到右,这就是为什么使用旧的a 值。
  • 谢谢。这就是我需要的。根据这种新的理解,我已经更新了我原来的问题。 (不能说这对我来说很直观,但可以理解。)
猜你喜欢
  • 2015-12-09
  • 2019-10-26
  • 2021-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多