Почему сборщик мусора принимает объекты в неправильном порядке?

У меня есть приложение с двумя классами, A и B. Класс A имеет внутри ссылку на класс B. Деструкторы классов выполняют некоторую очистку ресурсов, но их нужно вызывать в правильном порядке, сначала деструктор A и затем деструктор B.

Что происходит, так это то, что сначала вызывается деструктор B, а затем происходит сбой деструктора A, потому что он пытается выполнить методы удаленного объекта.

Правильно ли такое поведение сборщика мусора? Я ожидал, что GC обнаружит, что A имеет ссылку на B, а затем сначала вызовет деструктор A. Я прав?

Спасибо товарищи!

PD: В случае сомнений по поводу деструктора/финализатора/утилизатора и т. д. вот что у нас есть:

~A()
{
    this.Dispose();
}

~B()
{
    this.Dispose();
}    

person Ignacio Soler Garcia    schedule 12.11.2010    source источник
comment
Использование финализатора - это С#, IMO почти всегда неправильно. Для нативных ресурсов вам нужна критическая финализация/безопасные дескрипторы, а для управляемых вещей детерминированное уничтожение с помощью Dispose обычно лучше. Лично я использую только обычные финализаторы, чтобы уведомить меня, что я забыл вызвать Dispose.   -  person CodesInChaos    schedule 12.11.2010
comment
Я просто думаю так же. Мы работаем над ДЕЙСТВИТЕЛЬНО большим приложением (которое развивалось более 15 лет) и похоже на то, которое обнаружило, что Dispose не был вызван правильно, хотя было проще вызвать Dispose из деструктора, чем искать везде, где использовались классы.   -  person Ignacio Soler Garcia    schedule 12.11.2010


Ответы (5)


Сборщик мусора не является детерминированным: нет гарантии, что ваши объекты будут собирать мусор в любом конкретном порядке, в любое время или даже когда-либо --- он может решить оставить их после завершения вашей программы, если захочет. Таким образом, финализаторы очень редко полезны или используются.

Если вам нужна детерминированная финализация, вам следует использовать шаблон IDisposable.

person Domenic    schedule 12.11.2010

Как уже отмечали другие, ваши финализаторы ошибаются, ошибаются, ошибаются. Вы не можете просто вызвать Dispose в финализаторе и ожидать хороших результатов. Прочтите о правильном способе реализации одноразового шаблона.

Получение этого права — это начало, а не конец работы, которую вы должны выполнить. В дополнение ко всем остальным правильным ответам здесь отмечу, что:

  • финализатор может (и обычно работает) работать в другом потоке. Завершение ресурсов, связанных с определенным потоком, опасно, вы можете столкнуться с взаимоблокировками, если не будете осторожны, и так далее.

  • финализация может «воскресить» мертвый объект, присвоив ссылку на него переменной, которая находится в живом объекте. Не делай этого. Это невероятно запутанно.

  • финализаторы могут работать с объектами, которые были частично созданы, когда произошло исключение прерывания потока. Вы не можете предполагать, что какой-либо инвариант, верный для полностью сконструированного объекта, верен и для завершаемого объекта.

По всем этим причинам правильно написать финализатор чрезвычайно сложно. Избегайте, избегайте, избегайте.

person Eric Lippert    schedule 12.11.2010

Из http://msdn.microsoft.com/en-us/magazine/bb985010.aspx :

Среда выполнения не дает никаких гарантий относительно порядка вызова методов Finalize. Например, предположим, что есть объект, который содержит указатель на внутренний объект. Сборщик мусора обнаружил, что оба объекта являются мусором. Кроме того, предположим, что сначала вызывается метод Finalize внутреннего объекта. Теперь методу Finalize внешнего объекта разрешено обращаться к внутреннему объекту и вызывать методы для него, но внутренний объект был финализирован, и результаты могут быть непредсказуемыми. По этой причине настоятельно рекомендуется, чтобы методы Finalize не обращались к каким-либо внутренним объектам-членам.

person WiseGuyEh    schedule 12.11.2010

Вы не должны писать код, который зависит от деконструкторов в C#, они могут никогда не запускаться, поэтому на них нельзя полагаться.

person Hans Olsson    schedule 12.11.2010

Правильный способ написания финализаторов - ничего не вызывать для связанных объектов, вы должны только уничтожать неуправляемые ресурсы через финализатор именно потому, что, как вы заметили, все происходит не по порядку.

Как отмечали другие, это задокументировано как часть спецификации, так что это не ошибка и не то, что изменится, чтобы «делать правильные вещи». В частности, подумайте о двух объектах, имеющих ссылку на другой, среда выполнения не сможет определить, какой здесь правильный порядок.

В любом случае, вот как вы должны реализовать финализатор. Обратите внимание, что вам следует реализовывать финализатор только в том случае, если вам нужно уничтожить неуправляемые ресурсы (обратите внимание, есть некоторые крайние случаи, когда вам может понадобиться финализатор для очень специального класса, но я не буду упомянуть их здесь больше, чем это):

public class Test : IDisposable
{
    ~Test()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        // TODO: Make sure calling Dispose twice is safe
        if (disposing)
        {
            // call Dispose method on other objects
            GC.SuppressFinalize(this);
        }

        // destroy unmanaged resources here
    }
}

Дополнительную информацию можно найти здесь: Реализация метода Dispose.

person Lasse V. Karlsen    schedule 12.11.2010
comment
Спасибо, я уже знаю, как реализовать шаблон Dispose. Просто я нашел код, работающий таким образом. Спасибо, в любом случае! - person Ignacio Soler Garcia; 12.11.2010
comment
Я считаю это антипаттерном (и учтите, что код, написанный под него, отдаляется от него, так что я не одинок). Храните каждый неуправляемый ресурс в классе дескриптора, у которого нет другого состояния. В тех классах Dispose и Finalize — это одно и то же (очистка этого ресурса), в других классах finalize не требуется (поскольку они напрямую не содержат неуправляемых ресурсов). Проще, чище, легче доказать правильность, меньшие финализируемые объектные графы. - person Jon Hanna; 12.11.2010