Об «очевидном» и полезном или создание и уничтожение объектов Delphi

    Когда читаешь о той или иной реализации механизмов ООП, то весьма забавно видеть, как та или иная конкретная особенность конкретного языка или библиотеки называется «очевидной». Особенно выделяется в этом отношении описание ООП в C++, в котором по факту реализация одна из самых непрозрачных и запутанных.
    Так вот, я ничего не буду писать про очевидность, а расскажу про способ облегчить себе жизнь в критических точках жизненного цикла объектов, используя не «очевидные», но весьма полезные особенности реализации ООП в Delphi.


    Обработка невозможности завершить создание объекта в конструкторе


    Если конструктор хранит свои данные только в полях объекта, то решение элементарно — достаточно возбудить исключение. RTL Delphi сама его перехватит, вызовет деструктор, высвободит занятую память и возбудит исключение повторно.
    Соответственно, если часть данных для конструирования хранится в глобальных переменных, то достаточно использовать обычный блок try..except с повторным возбуждением исключения.
    Отсюда выводятся два требования к деструкторам: не провоцировать исключения, что означает не пытаться заниматься чем-либо, кроме освобождения ресурсов (например, сохранением настроек) и обязательно поддерживать…

    Удаление частично инициализированного объекта


    В Delphi это не представляет никаких трудностей, так как любой объект еще до передачи управления конструктору инициализируется нулями. Соответственно, в деструкторе достаточно корректно обрабатывать нулевые значения, в чем сильно помогает процедура FreeAndNil, которая освобождает объект, только если ссылка на него уже не nil.

    Порядок вызова конструкторов


    В Delphi он не имеет значения. Можно вызывать конструкторы предка, другие конструкторы того же класса точно так же, как и обычные методы или не вызывать ничего — так что если надо использовать инициализацию предка, то забывать ее вызвать не рекомендуется, зато и проблем сделать что-то до или вместо вызова конструктора предка нет никаких.

    Вызов виртуальных методов в конструкторе


    Так как в Delphi объект сразу инициализируется конечной ссылкой на VMT, то никакой разницы между вызовами виртуальных методов из конструктора от прочих вариантов нет.

    Виртуальные конструкторы и ссылки на класс


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

    Автоматическое управление временем жизни объекта


    Если объект реализует тот или иной явно заданный интерфейс, то его можно привести к ссылке на этот интерфейс, используя присваивание или операцию as. В этом случае Delphi будет сама генерировать вызовы методов IUnknown, что позволяет не запрашивать интерфейсы и не удалять объекты явно.

    Итоги


    С одной стороны, все вышеперечисленное должен знать каждый разработчик; с другой — я встречал не так уж мало программистов со стажем, изобретающих велосипеды только от незнания особенностей реализации своего рабочего инструмента. Надеюсь, что эта статья немного поможет. Из полезных особенностей реализации ООП в Delphi осталась нерассмотренной поддержка делегирования реализации интерфейсов, но это тема для отдельной статьи.

    Комментарии 44

      +2
      А еще есть замечательные методы BeforeDestruction и AfterConstruction
        0
        Есть такие, но из них извлечь реальную пользу, ничего не порушив, куда сложнее.
        Хорошо в них то, что оба имеют дело уже с «боевым» объектом, причем если AfterConstruction возбуждает исключение, то сначала вызовется BeforeDestruction, а уж потом деструктор.
        +1
        Почему-то у меня такое ощущение, что исключения принято вызывать, а возбуждают кой-чего другое. Но, если честно, Object Pascal использовал так давно, что не помню какой там для этого кейворд.
          0
          raise ;)
            0
            значит русский аналог подобран таки удачно :)
            0
            Учитывая что язык давно уже называется Delphi — неудивительно :)
            А «вызов» — слово не слишком удачное, так как после возбуждения исключения в Delphi никакого возврата (как при «нормальных» вызовах) не происходит по определению.
            0
            Зря на C++ бочку катите, посмотрите, как управление памятью в Qt реализовано
              +3
              Никто не отрицает, что на С++ можно и нужно сделать (и неоднократно сделано!) хорошую реализацию управления памятью. Но речь шла о якобы очевидном поведении объектов C++ при создании и уничтожении. Ничего очевидного там, естественно, нет. Жесткий порядок вызова при инициализации, смертельная опасность выброса исключения из конструктора, перезапись указателя VMT, фактическая невиртуальность вызовов оттуда же…
                0
                Всё одна глава в книжке и всё это будете знать. После этого всё кажется очевидно :)
                  0
                  Я это знаю давно. Но очевидным (т.е. однозначно понятным без точных знаний) оно от давности не стало.
                    0
                    Однозначно понятным без точных знаний не является ничего.
                  0
                  смертельная опасность выброса исключения из конструктора
                  W-w-what?
                    0
                    Формулировка и впрямь слишком резкая. Точнее, бросать исключения из конструктора в C++ требует гораздо больше труда и внимательности, так как деструктор при этом вызван не будет, а неинициализированные части объекта содержат мусор. В результате за очистку при прерывании инициализации отвечает конструктор, не имея вдобавок простых средств для отделения зерна от плевел.
                    Со стороны C++-программиста на проблему можно посмотреть по ссылке: alenacpp.blogspot.com/2006/11/blog-post.html
                      0
                      Деструкторы будут вызваны для всех полностью сконструированных членов класса и предков. Собственно главное — не бросать исключение в момент, когда какой-то объект находится в «промежуточном» состоянии. Для этого придумано RAII.
                        0
                        А для не полностью сконструированных конструктор должен все сделать сам. RAII помогает, конечно, но тем не менее дельфийский подход требует намного меньше труда и внимания — достаточно лишь аккуратно написать деструктор.
                +2
                Соответственно, в деструкторе достаточно корректно обрабатывать нулевые значения, в чем сильно помогает процедура FreeAndNil, которая освобождает объект, только если ссылка на него уже не nil.

                Точнее есть метод Free, который вызывает деструктор если Free был вызван для не нулевого объекта.
                А процедура FreeAndNil еще дополнительно обнуляет объект который ей передали.

                  +1
                  Да, имеет смысл упомянуть, что FreeAndNil еще и висячей ссылки за собой не оставляет.
                  Free не поминал специально, так как считаю его применение в деструкторах (и вообще везде, кроме тех мест где FreeAndNil задействовать невозможно) неправильным.
                  Почему — успешно объяснено до меня и без меня по ссылке: gunsmoker.blogspot.com/2009/04/freeandnil-free.html
                    0
                    Про эту ссылочку:
                    Ее уже здесь обсуждали, выяснили, что неполезная

                    delphimaster.ru/cgi-bin/forum.pl?id=1256236718&n=3

                    Если интересно, могу и предыдущее обсуждение найти, конкретно про обсуждение данной ссылки
                      0
                      Прочитал.
                      Участники дискуссии упускают из вида один важный момент (подробно освещенный по ссылке) — двойное удаление есть лишь частный случай использования висячей ссылки на объект, причем самый безобидный. Зато если после удаления будут вызваны любые другие методы объекта, то в случае использования FreeAndNil будет сразу возбуждено исключение, а Free эту ошибку замаскирует, причем наглухо — падать будет не всегда и совсем не там, где ошибка. Это куда более дорогая цена по сравнению с успешным двойным удалением (которое по факту все равно будет успешным однократным).
                  0
                  Почему-то мне кажется что топик несет в себе ноль полезной информации :(
                  В чем смысл-то? Перечисление «особенностей», о которых все знают?
                    0
                    Как ты узнал, что все знают? Я, например, не знал.
                      –1
                      О чем именно из того, что тебе будет полезно, ты не знал? :)
                        0
                        Доктор, вы задаёте слишком много вопросов.
                          –1
                          Один, самый главный.
                      +2
                      отучаемся говорить за всех?
                        –1
                        О чем именно из того, что тебе будет полезно, ты не знал? :)
                      0
                      возможность создания объектов с неизвестным на этапе компиляции классом
                      а можно пример?
                          0
                          Пример использования!
                          Фабрика объектов?
                            0
                            Пример из RTL Delphi. Этот код создает класс при десериализации его из потока.
                              procedure CreateComponent;
                              var
                                ComponentClass: TComponentClass;
                              begin
                                try
                                  ComponentClass := FindComponentClass(CompClass);
                                  Result := nil;
                                  if Assigned(FOnCreateComponent) then
                                    FOnCreateComponent(Self, ComponentClass, Result);
                                  if Result = nil then
                                  begin
                                    Result := TComponent(ComponentClass.NewInstance);
                                    if ffInline in Flags then
                                    begin
                                      Include(Result.FComponentState, csLoading);
                                      Include(Result.FComponentState, csInline);
                                    end;
                                    try
                                      Result.Create(Owner);
                                    except
                                      Result := nil;
                                      raise;
                                    end;
                                  end;
                                  Include(Result.FComponentState, csLoading);
                                except
                                  if not Recover(Result) then raise;
                                end;
                              end;
                            

                              0
                              Точно!
                              Спасибо.
                              Я ещё подумал про сериализацию (a la php), но забыл, что в Delphi тоже используется сериализация (при загрузке форм).
                        0
                        Соответственно, в деструкторе достаточно корректно обрабатывать нулевые значения, в чем сильно помогает процедура FreeAndNil, которая освобождает объект, только если ссылка на него уже не nil.

                        Delphi 7:

                        procedure FreeAndNil(var Obj);
                        var
                         Temp: TObject;
                        begin
                         Temp := TObject(Obj);
                         Pointer(Obj) := nil;
                         Temp.Free;
                        end;
                          0
                          Об «очевидном» и полезном или создание и уничтожение Delphi
                          FIXED
                            +2
                            На лоре пересидели?
                            +1
                            Delphi, ты еще жив?
                            В прошлом веке я так любил тебя. Ты был прекрасен.
                              0
                              Слухи о смерти Delphi оказались несколько преувеличены.
                              Хотя собственный родитель (Borland) приложил очень много усилий к организации «дельфицида».
                                0
                                Одна из самых больших профессиональных ошибок в моей жизни — это то, что в далеком 92-93 году я сориентировался на Borland.
                                А ведь тогда _нужно_ было предолеть рвотный рефлекс и пересесть на MS. Тогда фаворит был неочевиден а среда Turbo/Delphi давала 100 очков форы MS.
                                Кто бы знал, что Borland, с его грандиозными планами, уйдет в маргиналии.
                                  0
                                  Вряд ли ошибка (если это именно ошибка) была совершена в 92-93 — Delphi вплоть до седьмой версии включительно был более чем на уровне.
                                  Оптимальное время миграции — выход Delphi 8 под .NET. Именно тогда был взят гибельный курс. А вообще лично я не вижу проблемы для программиста с переходом на .NET и сейчас (если, конечно, нет жестких ограничений по времени и заработку прямо сейчас). И не наблюдаю реальных альтернатив Delphi для Win32.
                                    0
                                    >А вообще лично я не вижу проблемы для программиста с переходом на .NET и сейчас
                                    Ключевое слово «сейчас». Тогда в 94 найти работу было крайне сложно. Пришлось преквалифицироваться. Потом еще раз, и еще раз и еще раз… Десктопных приложений с тех пор не писал.
                                    А перескочить с php или perl на .NET достаточно сложно. К тому же длительный перерыв в професии дает о себе знать. Но если бы тогда сориентировался на богомерзкий MS VBasic вместо Delphi (1/2/3) жизнь могла бы пойти по другому. Так мне кажется.
                                      0
                                      Сопссно, C# — это прямой наследник Delphi (или младший брат, учитывая общего родителя :)
                                        0
                                        Меня глючит. Кто общий родитель С# и Delphi???????????????

                                        Вспомнился старый анекдот про лекцию Вирта(Хансена) в испанояычном университете.
                                        Вирта: Вам нравится языка Паскаль?
                                        Студенты: Си, синьор! Си! (sí señor! sí!)

                                        Я вообще-то алгол учил в свое время. И до сих пор скобки расставляю по алгольному.
                            0
                            Allen Bauer опубликовал в своём блоге заметку по данному поводу:
                            A case against FreeAndNil
                              0
                              Все верно, от двойного удаления FreeAndNil не лечит, а маскирует.
                              Но любое другое использование висячей ссылки после освобождения маскирует уже Free, причем с куда худшими последствиями. Так что если код падает на Free, то сначала надо устранить двойное удаление, а уже потом заменить Free на FreeAndNil — для демаскировки использования висяка.

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

                            Самое читаемое