DBObject и IDisposable

Автор Тема: DBObject и IDisposable  (Прочитано 5175 раз)

0 Пользователей и 1 Гость просматривают эту тему.

Оффлайн Андрей БушманАвтор темы

  • ADN Club
  • *****
  • Сообщений: 2000
  • Карма: 163
  • Пишу программки...
    • Блог
  • Skype: Compositum78
DBObject и IDisposable
« : 11-03-2014, 11:28:53 »
Доброго времени суток

- AutoCAD 2009-2014 x86/x64

Известно, что класс DBObject реализует IDisposable. Соответственно, по философии .NET, по завершению использования такого объекта нужно вызывать Dispose либо напрямую, либо оборачивать инициализацию объекта в блок using. Однако во всех примерах по AutoCAD .NET API, это соблюдается лишь применительно к Transaction и DocumentLock (насколько я вижу). Т.о. обычно в примерах код пишется как-то так:
Код - C# [Выбрать]
  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. using cad = Autodesk.AutoCAD.ApplicationServices.Application;
  5. using Ap = Autodesk.AutoCAD.ApplicationServices;
  6. using Db = Autodesk.AutoCAD.DatabaseServices;
  7. using Ed = Autodesk.AutoCAD.EditorInput;
  8. using Gm = Autodesk.AutoCAD.Geometry;
  9. using Rt = Autodesk.AutoCAD.Runtime;
  10.  
  11. [assembly: Rt.CommandClass(typeof(Bushman.Samples.TestClass))]
  12.  
  13. namespace Bushman.Samples {
  14.  
  15.         public sealed class TestClass {
  16.  
  17.                 [Rt.CommandMethod("test", Rt.CommandFlags.Session)]
  18.                 public static void Test() {
  19.                         Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
  20.                         Db.Database db = doc.Database;
  21.                         Ed.Editor ed = doc.Editor;
  22.                         using (doc.LockDocument()) {
  23.                                 using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
  24.                                         Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForRead) as Db.BlockTable;
  25.                                         const String blockName = "test_block";
  26.                                         const String attName = "MY_ATTRIBUTE";
  27.                                         if (!bt.Has(blockName)) {
  28.                                                 ed.WriteMessage("Определение блока \"{0}\" не найдено.\n", blockName);
  29.                                                 return;
  30.                                         }
  31.                                         List<Db.ObjectId> bRefIds = new List<Db.ObjectId>();
  32.                                         Db.ObjectId bDefId = bt[blockName];
  33.                                         Db.BlockTableRecord bDef = tr.GetObject(bDefId, Db.OpenMode.ForRead) as Db.BlockTableRecord;
  34.                                         foreach (Db.ObjectId id in bDef.GetBlockReferenceIds(false, true)) bRefIds.Add(id);
  35.                                         if (bDef.IsDynamicBlock) {
  36.                                                 foreach (Db.ObjectId anDefId in bDef.GetAnonymousBlockIds()) {
  37.                                                         Db.BlockTableRecord anDef = tr.GetObject(anDefId, Db.OpenMode.ForRead) as Db.BlockTableRecord;
  38.                                                         foreach (Db.ObjectId id in anDef.GetBlockReferenceIds(false, true)) bRefIds.Add(id);
  39.                                                 }
  40.                                         }
  41.                                         foreach (Db.ObjectId id in bRefIds) {
  42.                                                 Db.BlockReference br = tr.GetObject(id, Db.OpenMode.ForRead) as Db.BlockReference;
  43.                                                 foreach (Db.ObjectId attId in br.AttributeCollection) {
  44.                                                         Db.AttributeReference ar = tr.GetObject(attId, Db.OpenMode.ForRead) as Db.AttributeReference;
  45.                                                         if (String.Equals(ar.Tag, attName, StringComparison.CurrentCultureIgnoreCase)) {
  46.                                                                 ar.UpgradeOpen();
  47.                                                                 ed.WriteMessage("{0}\n", new String('*', 15));
  48.                                                                 ed.WriteMessage("TextString = {0}\n", ar.TextString);
  49.                                                                 ed.WriteMessage("getTextWithFieldCodes = {0}\n", ar.getTextWithFieldCodes());
  50.                                                                 ed.WriteMessage("меняем значение атрибута...\n");
  51.                                                                 ar.TextString = "Новое значение";
  52.                                                                 ed.WriteMessage("TextString = {0}\n", ar.TextString);
  53.                                                                 ed.WriteMessage("getTextWithFieldCodes = {0}\n\n", ar.getTextWithFieldCodes());
  54.                                                                 break;
  55.                                                         }
  56.                                                 }
  57.                                         }
  58.                                         tr.Commit();
  59.                                 }
  60.                         }
  61.                 }
  62.         }
  63. }

Однако, с точки зрения философии .NET, правильней было бы написать такой вариант (инициализация каждого DBObject обёрнута в using, в т.ч. инициализация Document и Database):

Код - C# [Выбрать]
  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. using cad = Autodesk.AutoCAD.ApplicationServices.Application;
  5. using Ap = Autodesk.AutoCAD.ApplicationServices;
  6. using Db = Autodesk.AutoCAD.DatabaseServices;
  7. using Ed = Autodesk.AutoCAD.EditorInput;
  8. using Gm = Autodesk.AutoCAD.Geometry;
  9. using Rt = Autodesk.AutoCAD.Runtime;
  10.  
  11. [assembly: Rt.CommandClass(typeof(Bushman.Samples.TestClass))]
  12.  
  13. namespace Bushman.Samples {
  14.  
  15.         public sealed class TestClass {
  16.  
  17.                 [Rt.CommandMethod("test", Rt.CommandFlags.Session)]
  18.                 public static void Test() {
  19.                         Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
  20.                         Db.Database db = doc.Database;
  21.                         Ed.Editor ed = doc.Editor;
  22.                         using (doc.LockDocument()) {
  23.                                 using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
  24.                                         using (Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForRead) as Db.BlockTable) {
  25.                                                 const String blockName = "test_block";
  26.                                                 const String attName = "MY_ATTRIBUTE";
  27.                                                 if (!bt.Has(blockName)) {
  28.                                                         ed.WriteMessage("Определение блока \"{0}\" не найдено.\n", blockName);
  29.                                                         return;
  30.                                                 }
  31.                                                 List<Db.ObjectId> bRefIds = new List<Db.ObjectId>();
  32.                                                 Db.ObjectId bDefId = bt[blockName];
  33.                                                 Db.BlockTableRecord bDef = tr.GetObject(bDefId, Db.OpenMode.ForRead) as Db.BlockTableRecord;
  34.                                                 foreach (Db.ObjectId id in bDef.GetBlockReferenceIds(false, true)) bRefIds.Add(id);
  35.                                                 if (bDef.IsDynamicBlock) {
  36.                                                         foreach (Db.ObjectId anDefId in bDef.GetAnonymousBlockIds()) {
  37.                                                                 using (Db.BlockTableRecord anDef = tr.GetObject(anDefId, Db.OpenMode.ForRead) as Db.BlockTableRecord)
  38.                                                                         foreach (Db.ObjectId id in anDef.GetBlockReferenceIds(false, true)) bRefIds.Add(id);
  39.                                                         }
  40.                                                 }
  41.                                                 foreach (Db.ObjectId id in bRefIds) {
  42.                                                         using (Db.BlockReference br = tr.GetObject(id, Db.OpenMode.ForRead) as Db.BlockReference)
  43.                                                                 foreach (Db.ObjectId attId in br.AttributeCollection) {
  44.                                                                         using (Db.AttributeReference ar = tr.GetObject(attId, Db.OpenMode.ForRead) as Db.AttributeReference)
  45.                                                                                 if (String.Equals(ar.Tag, attName, StringComparison.CurrentCultureIgnoreCase)) {
  46.                                                                                         ar.UpgradeOpen();
  47.                                                                                         ed.WriteMessage("{0}\n", new String('*', 15));
  48.                                                                                         ed.WriteMessage("TextString = {0}\n", ar.TextString);
  49.                                                                                         ed.WriteMessage("getTextWithFieldCodes = {0}\n", ar.getTextWithFieldCodes());
  50.                                                                                         ed.WriteMessage("меняем значение атрибута...\n");
  51.                                                                                         ar.TextString = "Новое значение";
  52.                                                                                         ed.WriteMessage("TextString = {0}\n", ar.TextString);
  53.                                                                                         ed.WriteMessage("getTextWithFieldCodes = {0}\n\n", ar.getTextWithFieldCodes());
  54.                                                                                         break;
  55.                                                                                 }
  56.                                                                 }
  57.                                                 }
  58.                                         }
  59.                                         tr.Commit();
  60.                                 }
  61.                         }
  62.                 }
  63.         }
  64. }


Оба варианта работают.

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

Вопросы:
1. Почему все примеры пишутся в стиле первого примера и не вызываются Dispose для всех DBObject (за исключением Transaction и DocumentLock)?
2. Чревато ли в AutoCAD .NET API подобное игнорирование вызовов Dispose (помимо уничтожения транзакции и блокировки документа) какими-то последствиями (например не освобождением ресурсов и, как следствие, утечкой памяти)?

Спасибо.
« Последнее редактирование: 11-03-2014, 16:01:24 от Андрей Бушман »

Оффлайн Андрей БушманАвтор темы

  • ADN Club
  • *****
  • Сообщений: 2000
  • Карма: 163
  • Пишу программки...
    • Блог
  • Skype: Compositum78
Re: DBObject и IDisposable
« Ответ #1 : 11-03-2014, 11:55:43 »
Нашел несколько заметок по теме у Kean Walmsley, почитаю:

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

P.S. Не плохо бы и их перевести, раз уж такое здесь практикуется.

Оффлайн Андрей БушманАвтор темы

  • ADN Club
  • *****
  • Сообщений: 2000
  • Карма: 163
  • Пишу программки...
    • Блог
  • Skype: Compositum78
Re: DBObject и IDisposable
« Ответ #2 : 11-03-2014, 14:35:52 »
По первой ссылке:
Первые два случая, обозначенные Kean Walmsley требуют "ручного" вызова Dispose:
Цитата: Kean Walmsley
Temporary objects - such as those provided by Autodesk.AutoCAD.Geometry - which are never Database-resident
Temporary objects with the potential to be database-resident but which never actually get added to a Database

Несмотря на то, что транзакция (третий случай) автоматом вызывает Dispose для всех объектов, использовавшихся в её рамках, обозначенное "НО" заставляет задуматься о перестраховке:
Цитата: Kean Walmsley
There is no need to explicitly dispose of the objects managed by a Transaction (unless there is a failure between the time the object is created and when it is added to the transaction via AddNewlyCreatedDBObject(), of course).
Т.о. если объект был создан в рамках транзакции (не в блоке using) и после этого произошло необработанное исключение до того, как должен был быть вызван AddNewlyCreatedDBObject(), то в этом случае Dispose не будет вызван. Соответственно, имеет смысл каждый такой DBObject упаковывать в блок using - в этом случае Dispose будет вызван гарантированно в любом случае.

Вот только не обозначен такой важный момент: если транзакция изначально упакована в using и произойдёт исключение, то в этом случае Dispose будет гарантированно вызван для транзакции, а вот будет ли в блоке using нашей транзакции, в этой ситуации, вызваны Dispose и для всех объектов, используемых в рамках данной транзакции? Т.е. вызываются ли методы Dispose объектов базы данных в коде метода TransactionDispose? Позднее, в третьей записи блога, Kean Walmsley всё же дал ответ на этот вопрос (см. ниже первую цитату в разделе "По третьей ссылке").

Вывод по первой ссылке я делаю такой: Dispose нужно вызывать самостоятельно, причём посредством упаковки каждого DBObject (а так же Document и Database) в блок using, дабы в случае исключения не пропустить ручной вызов Dispose.

По второй ссылке:

Во второй обозначенной выше ссылке Kain Walmsley вроде как и написал чего-то, но по сути - никакой конкретики... Автор сообщает, что в составе API присутствуют некоторые общие ресурсы, и при вызове Dispose, эти ресурсы освобождаются без каких либо проверок на то, используется ли этот ресурс кем-то ещё... Насколько я понял, реализация кода такова, что при работе с общими ресурсами не используются счётчики ссылок, на основании значения которых в коде метода Dispose следовало бы определять, можно ли освобождать общий ресурс или же нет (хотя этот способ обозначен в любом букваре). Т.е. говоря по простому: код написан через задницу... Причём всех таких "весёлых" мест не знают даже разработчики Autodesk и, как я понял, исправляться это уже не будет "в целях обратной совместимости", как это нередко мотивируется. Единственное, чем нам могут помочь, это посоветовать "быть внимательными" (от оно чё, Михалыч...). Иными словами - флаг нам в руки и паровоз навстречу... Ну и "метод научного тыка": комментировать\раскомментировать Dispose...

Цитата: Kean Walmsley
Fenton’s assertion is that you really need to call Dispose() on all AutoCAD objects that you create yourself, unless they are managed by AutoCAD’s transaction system (i.e. you’ve passed responsibility across to AutoCAD by calling Transaction.AddNewlyCreatedDBObject()). Which means that while you don’t need to call Dispose() on objects such as the AutoCAD Editor or the active Document (and you really shouldn’t), you really should call Dispose() on various objects you’ve been used to letting the .NET garbage collector (GC) dispose of, such as those belonging to the Autodesk.AutoCAD.Geometry namespace.
Красным выделено то, что по мнению Kean Walmsley является очевидным следствием предыдущего предложения. Вот только я, почему-то, в этом не вижу никакой логической связи... И почему нельзя вызывать Dispose для переменной, указывающей на активный документ? По выходе из метода переменная всё равно утилизируется. Помещаю Document и Database в блоки using (каждая в свой) и никаких проблем не наблюдаю. Может чего упустил?

Для Editor вызывать Dispose и не получится даже при сильном желании, поскольку данный класс не реализует IDisposable (во всяком случае в AutoCAD 2009).

Цитировать
So, to summarise… I don’t suggest people necessarily go back through and systematically call Dispose() on every geometry object they’ve ever created within their AutoCAD .NET code, but it’s certainly worth paying attention to this, moving forwards, as well as keeping in mind the problematic areas (such as BRep) should unexplained crash reports start to come in from users.
В первой ссылке Kean Walmsley рекомендовал вызывать Dispose для всех случаев, кроме работы в контексте транзакции (мол там это необязательно, ибо будет зачищено автоматом, при условии отсутствия исключений). А сейчас, получается, что "политика партии" резко изменила своё направление и теперь автор "не рекомендует возвращаться назад" к ручному вызову Dispose. "Так чего, Василий??? Дёргать, али не дёргать??? Ты уж определись..."(c).

По третьей ссылке:

Цитировать
The short answer is that anything managed by a transaction – whether newly created and added to the transaction or opened by it – will be disposed of automatically by the transaction. So in general you shouldn’t even need a using block around such entities.
Вооот... Значит достаточно в блок using запихнуть только транзакцию и все объекты, оперируемые в её рамках будут зачищены в любом случае, даже при возникновении исключения (это радует).


Вывод:
1. Транзакцию нужно инициализировать только в блоке using, тем самым снимая множество проблем со своей головы.
2. Объекты, прочтённые, изменённые или добавленные в рамках этой транзакции не нуждаются в Dispose, т.к. они будут автоматом вызваны в методе Dispose транзакции.
3. Все объекты, инициализированные не при помощи транзакции, должны зачищаться вручную, посредством размещения его в собственном блоке using.
4. Если автоматический Dispose какой-то переменной вызывает Fatal Error, значит для этой переменной не следует её инициализацию упаковывать в свой блок using. Это определяется только "методом научного тыка".