вторник, 11 марта 2014 г.

AutoCAD .NET API & IDisposable

В данной заметке речь пойдёт об использовании IDisposable в AutoCAD .NET API. Если не освобождать ресурсы, то будет происходить утечка памяти, которая может привести к серьёзным проблемам. Дабы этого не происходило, нужно понимать, для каких объектов и когда следует вызывать Dispose(), а так же для каких объектов этого делать категорически нельзя.

Для начала несколько ссылок, откуда черпалась информация:
    1. Cleaning up after yourself: how and when to dispose of AutoCAD objects in .NET
    2. Calling Dispose() on AutoCAD objects
    3. Examples of calling Dispose() on AutoCAD objects
    В AutoCAD класс DBObject реализовывает интерфейс IDisposable. Соответственно, указанный интерфейс реализуется и всеми классами, унаследованными от DBObject. Этот же интерфейс реализован и классами Document и Database, однако для Document  вызывать Dispose() никогда не следует (это делает сам AutoCAD), а так же нельзя вызывать Dispose() для объекта, хранящегося в свойстве Document.Database.

    "Вручную" вызывать Dispose() следует в следующих случаях:
    1. Для временных объектов, которые никогда не будут добавлены в базу данных чертежа. Например, это могут быть нерезидентные объекты, предоставленные через Autodesk.AutoCAD.Geometry.
    2. Для временных объектов, которые потенциально могут быть добавлены в базу данных чертежа, но тем не менее не были в неё добавлены.
    3. Если экземпляр класса Database был вами создан способом "new Database(...)" без открытия документа. При работе с созданным подобным образом объектом Database, необходимо не забывать предварительно инициализировать им свойство HostApplicationServices.WorkingDatabase, иначе можно получить исключение eWrongDatabase. По завершению работы с объектом, следует возвращать свойству HostApplicationServices.WorkingDatabase предыдущее значение.
    Внимание!
    Некоторые ресурсы, используемые экземплярами различных классов, могут быть разделяемыми, т.е. одним и тем же ресурсом пользуется сразу несколько разных объектов. К сожалению, в AutoCAD .NET API подобные ситуации могут привести к Fatal Error, если будет произведено освобождение такого общего ресурса в то время, как им продолжают пользоваться другие объекты. 

    Примечание 1
    Скорее всего, возникновение обозначенной выше проблемы с освобождением совместно используемых ресурсов обусловлено плохо написанным кодом разработчиков Autodesk: по-нормальному, для каждого совместно используемого ресурса должен вестись свой счётчик ссылок. В коде метода Dispose() значение счётчика должно сначала декрементироваться (т.е. уменьшаться на 1) и затем проверяться на равенство 0. Если равенство не выполняется, значит ресурс используется кем-то ещё - в этом случае общий ресурс не освобождается. Но если текущее значение счётчика равно 0, то ресурс можно смело освобождать. 

    Учитывая возможные проблемы при освобождении совместно используемых ресурсов, в подобных ситуациях компания Autodesk рекомендует пользоваться "методом научного тыка"  (см. вторую из обозначенных выше ссылок): если вдруг, добавив очередной вызов Dispose() вы обнаруживаете возникновение Fatal Error, то попробуйте закомментировать этот вызов. Если ошибка исчезла, значит в данном случае вызов Dispose() выполнять не следует (логично). Всех мест в API, где наблюдается проблема освобождения совместно используемых ресурсов, не знает даже Autodesk. Поэтому данные места вы порой будете определять экспериментально, методом проб и ошибок (к сожалению).

    Ресурсы всех объектов, полученных при помощи объекта Transaction, посредством метода GetObject(), а так же ресурсы новых объектов, добавленных в базу данных чертежа посредством метода AddNewlyCreatedDBObject(), будут освобождены автоматически в коде метода Dispose() этого объекта транзакции. Т.е. для них не нужно вызывать Dispose() персонально.

    Настоятельно не рекомендую инициализировать объект Transaction следующим образом:

       1:  // db - экземпляр Database
       2:  Transaction tr = db.TransactionManager.StartTransaction();
       3:  ...
       4:  tr.Commit();
       5:  tr.Dispose();

    При такой форме записи, если между инициализацией экземпляра транзакции и вызовом его метода Dispose() произойдёт необработанное исключение, то его Dispose() вызван не будет. Соответственно не будут освобождены ресурсы, связанные с этим объектом, а так же не будут вызваны методы Dispose() для всех объектов, полученных из базы данных при помощи этого экземпляра транзакции, а так же методы Dispose() новых объектов, с его помощью добавленных в базу данных чертежа.

    Дабы избежать обозначенной выше опасности, следует инициализацию объекта Transaction выполнять так:

       1:  // db - экземпляр Database
       2:  using(Transaction tr = db.TransactionManager.StartTransaction()){
       3:      ...
       4:      tr.Commit();
       5:  }

    При таком способе, Dispose() гарантированно будет вызван при выходе из блока using, как для объекта транзакции, так и для всех объектов, работающих в её контексте, даже если произойдёт необработанное исключение

    Примечание 2
    Использовать блок using следует и для инициализации объектов, созданных вне контекста транзакции, поскольку для них метод Dispose() вызывать необходимо вручную.

    1 комментарий:

    Anton Alalaev комментирует...

    Замечательная статья, помогла понять почему при некоторых манипуляциях возникают ошибки.