https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5_%D1%80%D0%B5%D1%81%D1%83%D1%80%D1%81%D0%B0_%D0%B5%D1%81%D1%82%D1%8C_%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F
Хочется написать "что-то умное", поэтому попробую добавить "немного от себя".
Приведу "надуманный пример". Но надеюсь, что он будет понят.
(Предвижу вопрос - "зачем передавать критическую секцию". Отвечу - "ни зачем". Но я и такое в реальном коде видал. В чужом)
Итак.
Пусть есть такой код:
Приведу ещё ссылки:
- http://www.delphimaster.net/view/14-1089121849/all
- http://www.rsdn.ru/forum/delphi/411835.flat
- http://objectmix.com/delphi/402814-exception-constructor.html
Из последней ссылки процитирую:
"No, the destructor is automatically called when an exception is raised
in the constructor."
Это написано и в документации по Delphi, но к сожалению ссылку в интернете - я не нашёл.
Поверьте мне "на слово".
Процитирую лишь место из help к Delphi XE6:
"When an exception is raised during the creation of an object, Destroy is automatically called to dispose of the unfinished object. This means that Destroy must be prepared to dispose of partially constructed objects. Because a constructor sets the fields of a new object to zero or empty values before performing other actions, class-type and pointer-type fields in a partially constructed object are always nil. A destructor should therefore check for nil values before operating on class-type or pointer-type fields. Calling the Free method (defined in TObject) rather than Destroy offers a convenient way to check for nil values before destroying an object."
- что мы тут получим?
А то, что мы попытаемся выйти из критической секции в которую не входили.
И это вообще говоря - проблема. Подробности зависят от версии Windows.
Что можно сделать?
Можно например написать так:
- что мы тут сделали?
Мы поменяли местами строчки:
SomeInitCode;
fCS.Enter;
- тут "всё срастётся".
Но повторю - "пример надуманный".
В реальном коде - "может и не срастись". Да и не факт, что "ресурс не захвачен" и SomeInitCode как раз и приведёт к его освобождению.
Это тоже - "звучит бредом", но так в "реальной жизни" - тоже бывает.
Тут тоже можно сказать - "присваивайте fCS непосредственно перед fCS.Enter".
Можно!
Но повторю - "пример надуманный".
Как "я считаю" было бы правильно?
А вот примерно так:
(Об "оверхеде" на время - забудем)
- тут "всё срастается".
Всё?
Да нет - не всё.
Напишем так:
- что мы сделали?
Мы перенесли исключение из SomeInitCode в SomeDoneCode.
И опять переставили местами: SomeInitCode; fCS := TLocker.Create(aCS);
Как было рассказано "о решении проблемы" ранее.
- в чём проблема?
А в том, что до строчки:
FreeAndNil(fCS); - мы не дойдём.
И критическую секцию - мы не освободим.
Это понятно? Или я что-то напутал?
Как можно "поправить ошибку"?
Можно написать так:
- так опять - "все срастётся".
Всё? Вроде - да.
Но! "Эта ужасная лестница из try".
Что можно сделать?
Однозначного рецепта у меня - нет.
Но!
Могу предложить - "лишь беглый взгляд". Он - далеко не совершенный.
Но это путь - "куда думать" и отчасти ответ на вопрос - "почему Embarcadero так настойчиво продвигает ARC". Хотя я сам с этим и не согласен (Про ARC).
Но! Один из вариантов:
Что мы получили?
Мы получили тот факт, что fCS - будет однозначно освобождён.
И как следствие - мы попадём в fCS.Leave;
Зачем написана вот эта строчка:
fCS := nil;
?
Ну скажем так - "чтобы гарантировать порядок выполнения" хотя бы в случае отсутствия исключений.
Криво? Да - "местами криво".
Но - "лучше, чем ничего".
Какое решение является серебряной пулей?
Я пока - не знаю.
Есть один вариант.
Он описан тут - Черновик. Написать о том как использование "шаблонов" и "примесей" избавляет от "косвенности" и лишнего распределения памяти
На что стоит обратить внимание?
А вот на это:
А вот что:
В конструкторе вызываются процедуры вида InitProcXXX и они взводят в "маске состояния" биты означающие, что "этот метод был вызван".
А в деструкторе вызываются процедуры вида DoneProcXXX. И сбрасывают биты в "маске состояний".
Причём про DoneProcXXX есть два момента:
1. Они вызываются только если взведён соответствующий бит.
2. Они обрамлены блоком try..except. Т.е. они позволяют пройти следующим процедурам DoneProcXXX даже если в предыдущих произошло исключение.
Что сказать?
Этот вариант - железобетонный. И в нём - проблем нет.
И - не я его придумал. А другие умные люди.
Он реально - железобетонный.
Но!
Чем он мне не нравится?
А тем, что он не читабельный. Вообще.
Не знаю кому как, но от него мне лично - крышу рвёт. Но он - работает.
Когда подобный код "генерируется из UML" (Зачем UML) то это "куда ни шло.
А если это "руками писать" и потом читать - это - беда. Но зато - работает.
В общем - подведу итоги. Проблему я обозначил - "возбуждение исключений в конструкторах и деструкторах" и как следствие - неполная инициализация и деинициализация объектов. И как следствие - негарантированное получение/освобождение ресурсов.
Свои пути решения - я также перечислил. Они - не идеальны. Но! "Лучше чем ничего".
Если у кого-то есть лучшие варианты - я бы с удовольствием их бы посмотрел.
Спасибо за внимание. Надеюсь, что "что-то умное" написать таки - удалось.
P.S. Кстати есть "ещё два слова" - AfterConstruction и BeforeDestruction.
Процитирую документацию к Delphi:
"Responds after the last constructor has executed.
AfterConstruction is called automatically after the object's last constructor has executed. Do not call it explicitly in your applications.
The AfterConstruction method implemented in TObject does nothing. Override this method when creating a class that performs an action after the object is created. For example, TCustomForm overrides AfterConstruction to generate an OnCreate event."
"Responds before the first destructor executes.
BeforeDestruction is called automatically before the object's first destructor executes. Do not call it explicitly in your applications.
The BeforeDestruction method implemented in TObject does nothing. Override this method when creating a class that performs an action before the object is destroyed. For example, TCustomForm overrides BeforeDestruction to generate an OnDestroy event.
"Note: BeforeDestruction is not called when the object is destroyed before it is fully constructed. That is, if the object's constructor raises an exception, the destructor is called to dispose of the object, but BeforeDestruction is not called."
sic!
P.P.S. И вот ещё связанная вещь - BeforeRelease.
Ну и вот ещё ссылки:
http://18delphi.blogspot.ru/2013/04/3.html
http://18delphi.blogspot.ru/2013/04/iunknown.html
P.P.P.S. Есть ещё одна вещь - AutoPtr - они "сходны подсчёту ссылок", но имеют "особенное применение". Свои мысли об "умных указателях" для Delphi я попробую как-нибудь потом рассказать.
Хочется написать "что-то умное", поэтому попробую добавить "немного от себя".
Приведу "надуманный пример". Но надеюсь, что он будет понят.
(Предвижу вопрос - "зачем передавать критическую секцию". Отвечу - "ни зачем". Но я и такое в реальном коде видал. В чужом)
Итак.
Пусть есть такой код:
type TA = class private fCS : TCriticalSection; procedure SomeInitCode; procedure SomeDoneCode; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TA ... constructor TA.Create(aCS: TCriticalSection); begin inherited Create; fCS := aCS; SomeInitCode; fCS.Enter; end; destructor TA.Destroy; begin fCS.Leave; fCS := nil; SomeDoneCode; inherited; end; procedure TA.SomeInitCode; begin raise Exception.Create('Some fake error'); end; procedure TA.SomeDoneCode; begin // - do nothing end; ... var A : TA; begin A := TA.Create(SomeCriticalSection); ... end;
Приведу ещё ссылки:
- http://www.delphimaster.net/view/14-1089121849/all
- http://www.rsdn.ru/forum/delphi/411835.flat
- http://objectmix.com/delphi/402814-exception-constructor.html
Из последней ссылки процитирую:
"No, the destructor is automatically called when an exception is raised
in the constructor."
Это написано и в документации по Delphi, но к сожалению ссылку в интернете - я не нашёл.
Поверьте мне "на слово".
Процитирую лишь место из help к Delphi XE6:
"When an exception is raised during the creation of an object, Destroy is automatically called to dispose of the unfinished object. This means that Destroy must be prepared to dispose of partially constructed objects. Because a constructor sets the fields of a new object to zero or empty values before performing other actions, class-type and pointer-type fields in a partially constructed object are always nil. A destructor should therefore check for nil values before operating on class-type or pointer-type fields. Calling the Free method (defined in TObject) rather than Destroy offers a convenient way to check for nil values before destroying an object."
- что мы тут получим?
А то, что мы попытаемся выйти из критической секции в которую не входили.
И это вообще говоря - проблема. Подробности зависят от версии Windows.
Что можно сделать?
Можно например написать так:
type TA = class private fCS : TCriticalSection; procedure SomeInitCode; procedure SomeDoneCode; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TA ... constructor TA.Create(aCS: TCriticalSection); begin inherited Create; fCS := aCS; fCS.Enter; SomeInitCode; end; destructor TA.Destroy; begin SomeDoneCode; fCS.Leave; fCS := nil; inherited; end; procedure TA.SomeInitCode; begin raise Exception.Create('Some fake error'); end; procedure TA.SomeDoneCode; begin // - do nothing end; ... var A : TA; begin A := TA.Create(SomeCriticalSection); ... end;
- что мы тут сделали?
Мы поменяли местами строчки:
SomeInitCode;
fCS.Enter;
- тут "всё срастётся".
Но повторю - "пример надуманный".
В реальном коде - "может и не срастись". Да и не факт, что "ресурс не захвачен" и SomeInitCode как раз и приведёт к его освобождению.
Это тоже - "звучит бредом", но так в "реальной жизни" - тоже бывает.
Тут тоже можно сказать - "присваивайте fCS непосредственно перед fCS.Enter".
Можно!
Но повторю - "пример надуманный".
Как "я считаю" было бы правильно?
А вот примерно так:
(Об "оверхеде" на время - забудем)
type TLocker = class private fCS : TCriticalSection; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TLocker TA = class private fCS : TLocker; procedure SomeInitCode; procedure SomeDoneCode; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TA ... constructor TLocker.Create(aCS: TCriticalSection); begin inherited Create; fCS := aCS; fCS.Enter; end; destructor TLocker.Destroy; begin fCS.Leave; fCS := nil; inherited; end; constructor TA.Create(aCS: TCriticalSection); begin inherited Create; SomeInitCode; fCS := TLocker.Create(aCS); end; destructor TA.Destroy; begin FreeAndNil(fCS); SomeDoneCode; inherited; end; procedure TA.SomeInitCode; begin raise Exception.Create('Some fake error'); end; procedure TA.SomeDoneCode; begin // - do nothing end; ... var A : TA; begin A := TA.Create(SomeCriticalSection); ... end;
- тут "всё срастается".
Всё?
Да нет - не всё.
Напишем так:
type TLocker = class private fCS : TCriticalSection; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TLocker TA = class private fCS : TLocker; procedure SomeInitCode; procedure SomeDoneCode; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TA ... constructor TLocker.Create(aCS: TCriticalSection); begin inherited Create; fCS := aCS; fCS.Enter; end; destructor TLocker.Destroy; begin fCS.Leave; fCS := nil; inherited; end; constructor TA.Create(aCS: TCriticalSection); begin inherited Create; fCS := TLocker.Create(aCS); SomeInitCode; end; destructor TA.Destroy; begin SomeDoneCode; FreeAndNil(fCS); inherited; end; procedure TA.SomeInitCode; begin // - do nothing end; procedure TA.SomeDoneCode; begin raise Exception.Create('Some fake error'); end; ... var A : TA; begin A := TA.Create(SomeCriticalSection); ... end;
- что мы сделали?
Мы перенесли исключение из SomeInitCode в SomeDoneCode.
И опять переставили местами: SomeInitCode; fCS := TLocker.Create(aCS);
Как было рассказано "о решении проблемы" ранее.
- в чём проблема?
А в том, что до строчки:
FreeAndNil(fCS); - мы не дойдём.
И критическую секцию - мы не освободим.
Это понятно? Или я что-то напутал?
Как можно "поправить ошибку"?
Можно написать так:
destructor TA.Destroy; begin try SomeDoneCode; finally try FreeAndNil(fCS); finally inherited; end; end; end;
- так опять - "все срастётся".
Всё? Вроде - да.
Но! "Эта ужасная лестница из try".
Что можно сделать?
Однозначного рецепта у меня - нет.
Но!
Могу предложить - "лишь беглый взгляд". Он - далеко не совершенный.
Но это путь - "куда думать" и отчасти ответ на вопрос - "почему Embarcadero так настойчиво продвигает ARC". Хотя я сам с этим и не согласен (Про ARC).
Но! Один из вариантов:
type ILocker = interface(IUnknown) end;//ILocker TLocker = class(TInterfacedObject, ILocker) private fCS : TCriticalSection; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TLocker TA = class private fCS : ILocker; procedure SomeInitCode; procedure SomeDoneCode; public constructor Create(aCS: TCriticalSection); destructor Destroy; overide; end;//TA ... constructor TLocker.Create(aCS: TCriticalSection); begin inherited Create; fCS := aCS; fCS.Enter; end; destructor TLocker.Destroy; begin fCS.Leave; fCS := nil; inherited; end; constructor TA.Create(aCS: TCriticalSection); begin inherited Create; fCS := TLocker.Create(aCS); SomeInitCode; end; destructor TA.Destroy; begin SomeDoneCode; fCS := nil; inherited; end; procedure TA.SomeInitCode; begin // - do nothing end; procedure TA.SomeDoneCode; begin raise Exception.Create('Some fake error'); end; ... var A : TA; begin A := TA.Create(SomeCriticalSection); ... end;
Что мы получили?
Мы получили тот факт, что fCS - будет однозначно освобождён.
И как следствие - мы попадём в fCS.Leave;
Зачем написана вот эта строчка:
fCS := nil;
?
Ну скажем так - "чтобы гарантировать порядок выполнения" хотя бы в случае отсутствия исключений.
Криво? Да - "местами криво".
Но - "лучше, чем ничего".
Какое решение является серебряной пулей?
Я пока - не знаю.
Есть один вариант.
Он описан тут - Черновик. Написать о том как использование "шаблонов" и "примесей" избавляет от "косвенности" и лишнего распределения памяти
На что стоит обратить внимание?
А вот на это:
constructor Tm3CustomHeaderStream.Create(const AStream: IStream; const AAccess: LongInt); begin inherited; m2InitOperation(_Status,InitProc00000001($00000001)); m2InitOperation(_Status,InitProc00000002($00000002)); m2InitOperation(_Status,InitProc00000004($00000004)); m2InitOperation(_Status,InitProc00000008($00000008)); m2InitOperation(_Status,InitProc00000010($00000010)); end; procedure Tm3CustomHeaderStream.Cleanup; begin m2DoneOperation(_Status,$00000010,DoneProc00000010); m2DoneOperation(_Status,$00000008,DoneProc00000008); m2DoneOperation(_Status,$00000004,DoneProc00000004); m2DoneOperation(_Status,$00000002,DoneProc00000002); m2DoneOperation(_Status,$00000001,DoneProc00000001); inherited; end; ... procedure m2InitOperation(var AStatus: LongWord; const ABitMask: LongWord); begin Assert((AStatus and ABitMask) = 0); AStatus:=AStatus or ABitMask; end; procedure m2DoneOperation(var AStatus: LongWord; const ABitMask: LongWord; const AClassDoneProc: Tm2ClassDoneProc); begin if ((AStatus and ABitMask) <> 0) then begin try AClassDoneProc(); except m2ExcErrHandler(); end; AStatus:=AStatus and not(ABitMask); end; end;Что тут написано?
А вот что:
В конструкторе вызываются процедуры вида InitProcXXX и они взводят в "маске состояния" биты означающие, что "этот метод был вызван".
А в деструкторе вызываются процедуры вида DoneProcXXX. И сбрасывают биты в "маске состояний".
Причём про DoneProcXXX есть два момента:
1. Они вызываются только если взведён соответствующий бит.
2. Они обрамлены блоком try..except. Т.е. они позволяют пройти следующим процедурам DoneProcXXX даже если в предыдущих произошло исключение.
Что сказать?
Этот вариант - железобетонный. И в нём - проблем нет.
И - не я его придумал. А другие умные люди.
Он реально - железобетонный.
Но!
Чем он мне не нравится?
А тем, что он не читабельный. Вообще.
Не знаю кому как, но от него мне лично - крышу рвёт. Но он - работает.
Когда подобный код "генерируется из UML" (Зачем UML) то это "куда ни шло.
А если это "руками писать" и потом читать - это - беда. Но зато - работает.
В общем - подведу итоги. Проблему я обозначил - "возбуждение исключений в конструкторах и деструкторах" и как следствие - неполная инициализация и деинициализация объектов. И как следствие - негарантированное получение/освобождение ресурсов.
Свои пути решения - я также перечислил. Они - не идеальны. Но! "Лучше чем ничего".
Если у кого-то есть лучшие варианты - я бы с удовольствием их бы посмотрел.
Спасибо за внимание. Надеюсь, что "что-то умное" написать таки - удалось.
P.S. Кстати есть "ещё два слова" - AfterConstruction и BeforeDestruction.
Процитирую документацию к Delphi:
"Responds after the last constructor has executed.
AfterConstruction is called automatically after the object's last constructor has executed. Do not call it explicitly in your applications.
The AfterConstruction method implemented in TObject does nothing. Override this method when creating a class that performs an action after the object is created. For example, TCustomForm overrides AfterConstruction to generate an OnCreate event."
"Responds before the first destructor executes.
BeforeDestruction is called automatically before the object's first destructor executes. Do not call it explicitly in your applications.
The BeforeDestruction method implemented in TObject does nothing. Override this method when creating a class that performs an action before the object is destroyed. For example, TCustomForm overrides BeforeDestruction to generate an OnDestroy event.
Note: BeforeDestruction is not called when the object is destroyed before it is fully constructed. That is, if the object's constructor raises an exception, the destructor is called to dispose of the object, but BeforeDestruction is not called. "Внимание стоит обратить вот на что:
"Note: BeforeDestruction is not called when the object is destroyed before it is fully constructed. That is, if the object's constructor raises an exception, the destructor is called to dispose of the object, but BeforeDestruction is not called."
sic!
P.P.S. И вот ещё связанная вещь - BeforeRelease.
Ну и вот ещё ссылки:
http://18delphi.blogspot.ru/2013/04/3.html
http://18delphi.blogspot.ru/2013/04/iunknown.html
P.P.P.S. Есть ещё одна вещь - AutoPtr - они "сходны подсчёту ссылок", но имеют "особенное применение". Свои мысли об "умных указателях" для Delphi я попробую как-нибудь потом рассказать.
Про ARC интересная мысль, действительно многое объясняет.
ОтветитьУдалить