"статическое" значение сбрасывается после вызова функции

Я нашел много статей о статике (MSDN, MSDN 2, Qaru и многое другое), но я до сих пор не могу понять, почему этот код возвращает -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();
    }
}

Вот что показывает отладчик после запуска foo(), но до того, как результат вычитается из value:

foo=1, value=-7

Но на один шаг позже value становится -1:

value = -1

Я ожидаю -8 из-за статического поля, которое хранится в памяти один раз.

Когда я изменил его на

var x = foo();
value -= x;

это показывает -8

Как это работает точно?

Ответы

Ответ 1

Эта проблема не о статических; это о том, как работает вычитание.

value -= foo(); может быть расширен до value = value - foo()

Компилятор объяснит это в четыре этапа:

  1. Загрузите значение value в стек.
  2. Вызовите метод foo и поместите результат в стек.
  3. Делайте вычитание с этими двумя значениями в стеке.
  4. Установите результат обратно в поле value.

Таким образом, исходное значение поля value уже загружено. Независимо от того, что вы измените value в методе foo, результат вычитания не будет затронут.

Если вы измените порядок на value = - foo() + value, то поле значения value будет загружено после foo. Результат -8; то, что вы ожидаете получить.

Спасибо за комментарий Элиаху.

Ответ 2

Заявление

value -= foo(); // short for value = value - foo();

эквивалентно

var temp = value; // 0
var fooResult = foo(); // 1
value = temp - fooResult; // -1

Вот почему вы получаете -1

Ответ 3

Просто посмотрите на сгенерированный 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
  • IL_0001: - IL_0001: значение статического поля в стек. с: [значение (0)]
  • IL_0006: - Вставьте 7 в стек. s: [7, значение (0)]
  • IL_0007: - вычитает значение 2 (7) из значения 1 (0), возвращая новое значение (-7).
  • IL_0008: - Заменяет значение статического поля на val (value = -7).
  • IL_000d: - Вставить 1 в стек. s: [1, 7, значение (-7)]
  • IL_000e: - IL_000e: значение из стека в локальную переменную 0. (lv = 1)
  • IL_0011: - загрузить локальную переменную 0 в стек. s: [lv (1), 7, значение (-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
  • IL_0001: - IL_0001: value в стек (0)
  • IL_0006: - вызывает foo (который вернет 1)
  • IL_000b: - вычесть значения: value2(1) из значения1 value1(0) (value(0) - value(1) = -1).

Таким образом, результат -1.

Ответ 4

Вы можете использовать меню Debug → Windows → Disassembly и проверить, что происходит в фоновом режиме:

Я прокомментировал самые интересные части.

    //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

Так что это правда, что при записи value -= foo(), он генерирует код примерно так:

value = 0; // In the beginning STEP_A

//... main
var temp = value; //STEP_B
temp -= foo(); // STEP_C
value = temp; // STEP_D

Ответ 5

Я думаю, что это как-то связано с тем, как вычитается value на уровне сборки, и это вызывает некоторую несогласованность в программе. Я не знаю, имеет ли это какое-то отношение к статике или нет. Но из моей интуиции вот что происходит:

Позвольте сосредоточиться на value -= foo()

  1. Старое value сохраняется (помещается в стек)
  2. Функция foo() возвращает 1
  3. Теперь value равно -7 из-за операции foo()
  4. ЗДЕСЬ проблема: старое value (которое было сохранено ранее, то есть 0) вычитается из 1 а результат присваивается текущему value.

Ответ 6

value -= foo(); // value = value - foo();

foo() вернет 1.

value изначально равно 0, поэтому: 0 = 0 - 1.

Теперь value имеет -1.

Так что проблема с возвратом 1.

Ответ 7

Кажется, использование метода приращения неверно. Если вы хотите увеличить, вы не должны вызывать ту же самую переменную для увеличения. Это статическая переменная, но при использовании += есть кеш, я имею в виду, что значение вычисляется перед вычислением), потому что это одна строка для потока на компьютере, поэтому GET слева направо, а затем SET вправо для оставил на той же линии.

демонстрация

public static int value
{
    get
    {
        Console.WriteLine($"get value: {_value}");
        return _value;
    }
    set
    {
        var old = _value;
        _value = value;
        Console.WriteLine($"set value> old: {old}, new: {value}");
    }
}
static int _value = 0;

static int foo()
{
    Console.WriteLine($"########   foo() is called   ######## ");
    value = value - 7;
    Console.WriteLine($"new value is now '-7'");
    Console.WriteLine($"########   foo() is returning '1' ######## ");
    return 1;
}

static void Main(string[] args)
{
    Console.WriteLine($"STARTING");
    //value -= foo(); // TOTALLY EQUALS WITH BELOW
    value = value - foo();
    Console.WriteLine($"FINISHED");
    Console.WriteLine(value);
    Console.ReadKey();
}

Выход

STARTING
get value:0
########   foo() is called   ########
get value:0
set value> old: 0, new: -7
new value is now '-7'
########   foo() is returning '1' ########
set value> old: -7, new: -1
FINISHED
get value: -1
-1

Как видите, вторая строка получает value переменной value и затем вызывает foo(); метод, поэтому он перезаписывает -7 в -1, потому что компьютер знает, прежде чем начинать вычислять значение 0. Даже если вам нужно увеличить, компьютер все еще использует формат value = NEWvalue сложения, например value = NEWvalue, но где-то value = NEWvalue текущее значение.

Примечание: если вы работаете с асинхронным приращением для одной и той же переменной, вы увидите, что некоторые значения пропали.