函数调用后,“静态”值似乎已重置

Eme*_*erG 88 c#

我找到了很多有关静态的文章(MSDNMSDN 2Stack Overflow等等),但是我仍然不明白为什么此代码返回-1

class Program
{
    static int value = 0;

    static int foo()
    {
        value = value - 7;
        return 1;
    }

    static void Main(string[] args)
    {
        value -= foo();
        Console.WriteLine(value);
        Console.ReadKey();
    }
}
Run Code Online (Sandbox Code Playgroud)

这是调试器在foo()运行后但从中减去结果之前显示的内容value

foo = 1,value = -7

但是,下一步value-1

值= -1

我希望-8因为静态字段存储在内存中一次。

当我将其更改为

var x = foo();
value -= x;
Run Code Online (Sandbox Code Playgroud)

表明 -8

究竟如何运作?

shi*_*ngo 132

这个问题与静态无关。这是关于减法的工作原理。

value -= foo(); 可以扩展到 value = value - foo()

编译器将解释为四个步骤:

  1. 将的值加载value到堆栈上。
  2. 调用该方法foo并将结果放入堆栈。
  3. 用堆栈上的这两个值相减。
  4. 将结果设置回value字段。

因此,value字段的原始值已经加载。无论您value对方法进行什么更改,foo减法的结果都不会受到影响。

如果将顺序更改为value = - foo() + value,则将在调用value后加载field 的值foo。结果是-8;这就是您期望得到的。

感谢Eliahu的评论。

  • 如果在最后一句中将“值= foo()-值”更改为“值= -foo()+值;”,您将得到“预期”结果“ -8”。 (14认同)
  • @NormanGray,您提醒我从规范中搜索定义,链接为c ++标准,我发现[适用于c#](https://github.com/dotnet/csharplang/blob/master/spec/expressions.md#operators) ,它是从左到右。 (12认同)
  • @EliahuAaron ...可能 操作数的求值顺序[未指定/未排序](/sf/ask/497859771/),因此将允许编译器以任何顺序或实际上并行求值两个参数。但是您的评论有用地强调了-=比起初看起来要复杂得多。 (7认同)
  • @AlbertoSantini由于[副作用](https://en.wikipedia.org/wiki/Side_effect_(computer_science)),所以不是。“ A + B”表示先评估“ A”(可能会引起副作用),保留结果,然后“评估+ B”(副作用),最后添加结果。 (6认同)
  • @AlbertoSantini附加部分是。您不会在数学上得到类比,因为在一个不变的世界中,这个问题不存在。它来自可变性-根据执行value和foo()的顺序,您会得到不同的结果。最后的加法a和b是可交换的,但是如何获得a和b却不是。 (3认同)
  • @AlbertoSantini(另一件事:`+的某些重载可能由于其他原因而无法交换,例如,没有副作用““ Hello” +“ world”`与“” world“ +” Hello“`不同,但是我猜想您即使使用运算符`+`也不会将字符串连接称为“加法”,用户定义的+运算符重载可以执行任何操作,例如`DateTime.Now + TimeSpan.FromHours(0.5)'是可以的,但`TimeSpan.FromHours(0.5)+ DateTime.Now`不匹配任何重载。) (2认同)

Bah*_*rom 63

该声明

value -= foo(); // short for value = value - foo();
Run Code Online (Sandbox Code Playgroud)

相当于

var temp = value; // 0
var fooResult = foo(); // 1
value = temp - fooResult; // -1
Run Code Online (Sandbox Code Playgroud)

这就是为什么你得到 -1

  • @EliahuAaron:因为C#规范是这样说的。为什么C#规范这么说呢?因为(a)具有确定性的评估顺序,以及(b)a-= b的行为类似于a = a-b(而仅对a进行评估)都是好主意。您会指定不同的名称吗?如果是,如何,为什么? (16认同)
  • @Bahrom:好像您声称的那样工作,但是为什么呢? (11认同)

SᴇM*_*SᴇM 34

只需查看生成的CIL:

.method private hidebysig static int32  foo() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 V_0)
  IL_0000:  nop
  IL_0001:  ldsfld     int32 Program::'value'
  IL_0006:  ldc.i4.7
  IL_0007:  sub
  IL_0008:  stsfld     int32 Program::'value'
  IL_000d:  ldc.i4.1
  IL_000e:  stloc.0
  IL_000f:  br.s       IL_0011
  IL_0011:  ldloc.0
  IL_0012:  ret
} // end of method Program::foo
Run Code Online (Sandbox Code Playgroud)
  • IL_0001:-将静态字段的值压入堆栈。s:[值(0)]
  • IL_0006:-推7入堆栈。s:[7,值(0)]
  • IL_0007:- 7从value1(0)中减去value2(),返回新值(-7)。
  • IL_0008:-用val (值= -7)替换静态字段的
  • IL_000d:-推1入堆栈。s:[1,7,value(-7)]
  • IL_000e:-从堆栈中弹出一个值到局部变量0。(lv = 1)
  • IL_0011:-将局部变量0加载到堆栈上。s:[lv(1),7,value(-7)]
  • IL_0012:-返回(lv(1))

Main方法:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       29 (0x1d)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldsfld     int32 Program::'value'
  IL_0006:  call       int32 Program::foo()
  IL_000b:  sub
  IL_000c:  stsfld     int32 Program::'value'
  IL_0011:  ldsfld     int32 Program::'value'
  IL_0016:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_001b:  nop
  IL_001c:  ret
} // end of method Program::Main
Run Code Online (Sandbox Code Playgroud)
  • IL_0001:-推入value堆栈(即0
  • IL_0006:-通话foo(将返回1
  • IL_000b:- value2(1)value1(0)value(0) - value(1) = -1)中减去值:

结果是-1


kov*_*oli 18

您可以使用菜单调试Windows反汇编并检查后台发生了什么:

我评论了最有趣的部分。

    //static int value = 0;
    05750449  mov         ebp,esp
    0575044B  push        edi
    0575044C  push        esi
    0575044D  push        ebx
    0575044E  sub         esp,2Ch
    05750451  xor         edx,edx
    05750453  mov         dword ptr [ebp-10h],edx
    05750456  mov         dword ptr [ebp-1Ch],edx
    05750459  cmp         dword ptr ds:[15E42D8h],0
    05750460  je          05750467
    05750462  call        55884370
    05750467  xor         edx,edx
    05750469  mov         dword ptr ds:[15E440Ch],edx  // STEP_A place 0 in ds register
 somewhere
    0575046F  nop
    05750470  lea         esp,[ebp-0Ch]
    05750473  pop         ebx
    05750474  pop         esi
    05750475  pop         edi
    05750476  pop         ebp
    05750477  ret

    //value -= foo();
    057504AB  mov         eax,dword ptr ds:[015E440Ch]   // STEP_B places (temp) to eax. eax now contains 0
    057504B0  mov         dword ptr [ebp-40h],eax
    057504B3  call        05750038



    057504B8  mov         dword ptr [ebp-44h],eax
    057504BB  mov         eax,dword ptr [ebp-40h]
    057504BE  sub         eax,dword ptr [ebp-44h]   //STEP_C substract the return(-1) of call from the temp eax
    057504C1  mov         dword ptr ds:[015E440Ch],eax  // STEP_D moves eax (-1) value to our ds register to some memory location

    //Console.WriteLine(value);
    015E04C6  mov         ecx,dword ptr ds:[015E440Ch]  // Self explanatory; move our ds(-1) to ecx, and then print it out to the screen.
    015E04CC  call        54CE8CBC
Run Code Online (Sandbox Code Playgroud)

因此,在编写时value -= foo(),它确实会生成如下代码:

value = 0; // In the beginning STEP_A

//... main
var temp = value; //STEP_B
temp -= foo(); // STEP_C
value = temp; // STEP_D
Run Code Online (Sandbox Code Playgroud)


小智 7

我认为这与它value在汇编级别上的减法有关,并且导致程序中的某些不一致。我不知道它是否与静态有关。但是根据我的直觉,会发生以下情况:

让我们专注于 value -= foo()

  1. 旧的value保存(推入堆栈)
  2. foo()函数返回1
  3. 现在,value-7由于foo()操作
  4. 这里是问题所在:将OLD value(这是较早保存的OLD 0)减去,1并将结果分配给current value

  • 这不仅是偶然的-C#规范专门定义了这种情况下的执行顺序-不允许编译器生成将输出除-1之外的任何东西的代码,这与C语言不同。 (5认同)

小智 5

value -= foo(); // value = value - foo();
Run Code Online (Sandbox Code Playgroud)

foo()会回来的1

value最初是0这样的:0 = 0 - 1

现在value-1

所以问题在于回报1

  • 这个答案忽略了调用foo会改变value的事实。 (3认同)
  • @Theraot:这个答案是正确的;foo更改value的事实是无关紧要的。不管foo的值有什么变化,事实是C#中的减法从左到右进行。 (2认同)