Вы попадаете сюда и делаете очень неправильные выводы, потому что используете отладчик. Вам нужно будет запустить свой код так, как он работает на компьютере вашего пользователя. Сначала перейдите к сборке выпуска с помощью диспетчера сборки + конфигурации, измените комбо «Конфигурация активного решения» в верхнем левом углу на «Выпуск». Затем перейдите в Инструменты + Параметры, Отладка, Общие и снимите флажок «Подавить оптимизацию JIT».
Теперь снова запустите вашу программу и возитесь с исходным кодом. Обратите внимание, что дополнительные скобки не имеют никакого эффекта вообще. И обратите внимание, что установка переменной на ноль вообще не имеет значения. Он всегда будет печатать «1». Теперь это работает так, как вы надеялись и ожидали, что это сработает.
Что и объясняет, почему он работает по-другому, когда вы запускаете сборку Debug. Это требует объяснения того, как сборщик мусора обнаруживает локальные переменные и как на это влияет наличие отладчика.
Прежде всего, джиттер выполняет две важные обязанности, когда компилирует IL для метода в машинный код. Первый очень хорошо виден в отладчике, вы можете увидеть машинный код в окне Debug + Windows + Disassembly. Вторая обязанность, однако, совершенно невидима. Он также генерирует таблицу, которая описывает, как используются локальные переменные внутри тела метода. В этой таблице есть запись для каждого аргумента метода и локальная переменная с двумя адресами. Адрес, где переменная будет сначала хранить ссылку на объект. И адрес инструкции машинного кода, где эта переменная больше не используется. Также, хранится ли эта переменная в кадре стека или в регистре процессора.
Эта таблица важна для сборщика мусора, она должна знать, где искать ссылки на объекты при выполнении сбора. Довольно легко сделать, когда ссылка является частью объекта в куче GC. Определенно нелегко сделать, когда ссылка на объект хранится в регистре процессора. В таблице указано, где искать.
Адрес «больше не используется» в таблице очень важен. Это делает сборщик мусора очень эффективным . Он может собирать ссылку на объект, даже если он используется внутри метода и этот метод еще не завершен. Что является очень распространенным, ваш метод Main (), например, когда-либо прекратит выполнение только перед завершением вашей программы. Ясно, что вы бы не хотели, чтобы какие-либо ссылки на объекты, используемые внутри этого метода Main (), жили в течение всей программы, что привело бы к утечке. Джиттер может использовать таблицу, чтобы обнаружить, что такая локальная переменная больше не нужна, в зависимости от того, как далеко продвинулась программа внутри этого метода Main () до того, как она сделала вызов.
Почти магический метод, связанный с этой таблицей, - это GC.KeepAlive (). Это очень особенный метод, он вообще не генерирует никакого кода. Его единственная обязанность - изменить эту таблицу. Она простираетсявремя жизни локальной переменной, предотвращая сбор мусора в хранимой ссылке. Единственный раз, когда вам нужно его использовать, это не дать GC чрезмерно увлечься сбором ссылки, что может произойти в сценариях взаимодействия, когда ссылка передается на неуправляемый код. Сборщик мусора не может видеть, что такие ссылки используются таким кодом, поскольку он не был скомпилирован джиттером, поэтому у него нет таблицы, в которой указано, где искать ссылку. Передача объекта делегата в неуправляемую функцию, такую как EnumWindows (), является типичным примером того, когда вам нужно использовать GC.KeepAlive ().
Итак, как вы можете заметить из своего примера фрагмента после запуска его в сборке Release, локальные переменные могут быть собраны раньше, до того, как метод завершит выполнение. Еще мощнее, объект может быть собран во время работы одного из его методов, если этот метод больше не ссылается на это . Есть проблема с этим, очень неудобно отлаживать такой метод. Так как вы можете поместить переменную в окно Watch или проверить ее. И он исчезнет во время отладки, если произойдет сборщик мусора. Это было бы очень неприятно, поэтому джиттер знает о подключенном отладчике. Затем он модифицируеттаблица и изменяет «последний использованный» адрес. И меняет его с обычного значения на адрес последней инструкции в методе. Который поддерживает переменную живым, пока метод не вернулся. Что позволяет вам продолжать смотреть его, пока метод не вернется.
Теперь это также объясняет, что вы видели ранее и почему вы задали вопрос. Он печатает «0», потому что вызов GC.Collect не может собрать ссылку. В таблице сказано, что переменная используется после вызова GC.Collect (), вплоть до конца метода. Вынужден сказать это, подключив отладчик и выполнив сборку Debug.
Установка переменной в null теперь имеет эффект, потому что GC будет проверять переменную и больше не будет видеть ссылку. Но убедитесь, что вы не попадете в ловушку, в которую попали многие программисты C #, на самом деле писать этот код было бессмысленно. Не имеет значения, присутствует ли этот оператор при запуске кода в сборке выпуска. Фактически, оптимизатор джиттера удалит это утверждение, поскольку оно никак не повлияет. Поэтому не пишите подобный код, хотя, похоже, он дал эффект.
Последнее замечание по этой теме - вот что доставляет программистам неприятности, которые пишут небольшие программы, чтобы что-то делать с приложением Office. Отладчик обычно выводит их на неверный путь, они хотят, чтобы программа Office выходила по требованию. Надлежащий способ сделать это - вызвать GC.Collect (). Но они обнаружат, что это не работает, когда они отлаживают свое приложение, приводя их в никогда и никогда не приземляясь, вызывая Marshal.ReleaseComObject (). Ручное управление памятью, оно редко работает должным образом, потому что они легко пропустят невидимую ссылку на интерфейс. GC.Collect () на самом деле работает, только не при отладке приложения.