0,0
рейтинг
19 декабря 2012 в 23:25

Разработка → Расставляем точки над i в Delphi RAII

Тема RAII в Delphi обычно замалчивается или же информация по этому вопросу ограничивается обсуждением полезности интерфейсов. Но интерфейсы поодиночке не дают многих желаемых возможностей. Когда в Delphi 2006 появилась перегрузка операций, приватные поля записей, собственные конструкторы и методы в записях и, казалось, было бы логично увидеть и автоматически вызываемый деструктор. И run-time позволяет, и в разделе запроса новых фич Delphi на протяжении нескольких лет в ТОП–10 висит запрос №21729 «Record Operator Overloading: Please implement «Initialize» and «Finalize» operators». Наверное, не судьба. Ничего, я покажу, как обойтись без несостоявшихся фич. Так как Delphi 7 живее всех живых, будут рассмотрены решения, совместимые с Delphi 7 в том числе

Времени найти обходные пути было достаточно. Эта статья — не tutorial и рассчитана на продвинутых разработчиков на Delphi, пишущих собственные библиотеки или делающих привязки к библиотекам на других языках программирования

Зачем хочется RAII в Delphi?



  • Автоматическое управление памятью. Лестница из try… finally — это не серьёзно. TList, TComponentList и прочие требуют дисциплины от того, кто будет их применять. Особенно сложно, не используя автоматику, сделать корректное освобождение памяти для переменных, используемых из восходящего замыкания
  • Copy-on-write и счётчики ссылок
  • Другое особое поведение при копировании
  • Copy-on-write и счётчики ссылок для объектов, созданных в сторонних библиотеках (например, CFString)


Для каких типов Delphi действует автоматическое управление?



  • AnsiString, WideString, UnicodeString — строки
  • array of… — динамические массивы
  • reference to… — замыкания в Delphi 2009+
  • интерфейсы
  • Variant, OleVariant — варианты


Среди всех этих возможностей программируемыми являются только интерфейсы и варианты

Чем плохи интерфейсы?


  • Инициализируются nil, у которого нельзя вызывать методы.
    Не подходит, если вы хотите реализовать собственный тип строки или собственную длинную арифметику. Неинициализированная переменная должна вести себя как пустая строка или 0, соответственно
  • Методы не могут изменить содержимое переменной–указателя, у которого они были вызваны
  • Нет контроля за тем, что происходит при копировании объекта. Только AddRef, который не может изменить содержимое переменной–указателя
  • Нет встроенной возможности сделать copy-on-write
  • Нет перегрузки операций


Чем плохи варианты?


  • Инициализируются Unassigned, у которого также нельзя вызвать методы
  • Вызовы нетипизированы. Реализация IDispatch или диспетчеризации у вариантов — нетривиальная и слабо документированная область знаний
  • Необходимость реализации муторных конверсий между другими типами вариантов, всяческих вспомогательных методов, которые могут быть вызваны


Как решить большинство этих проблем?



Решение, которое я предлагаю — заворачивать интерфейсы или варианты внутрь приватной части записей (record). Объявляем тип записи. Объявляем тип интерфейса. Дублируем все методы и в интерфейсе, и в записи. Методы записи перенаправляют все вызовы внутреннему объекту, при этом можно сделать то, что сама по себе переменная интерфейсного типа сделать не способна

В реализации каждого метода записи предусматриваем случай, когда в приватном поле nil — может потребоваться автоматически инициализировать объект перед тем, как что–либо вызывать у него. Если нужно реализовать Copy-on-write, в интерфейсе объявляется метод

procedure Unique(var Obj: IOurInterface);


Этот метод определяет по счётчику ссылок свою уникальность. Если объект не уникален, объект должен создать свою копию и записать этот указатель в Obj вместо себя. Каждый метод записи, который может что–либо изменить, перед передачей управления методу интерфейса должен убедиться в уникальности указателя. Для внутренних нужд можно и у других методов интерфейса предусмотреть var Obj: IOurInterface. Например, по аналогии со встроенными строками может возникнуть желание сделать так, чтобы, когда в строке собственного типа не остаётся символов, динамически размещённый объект удалялся, а внутренний указатель становился nil

В целях оптимизации при реализации собственных строк или длинной арифметики может потребоваться предусмотреть специальный случай a := a + b. Не гарантирую, что это сработает, но можно попробовать при реализации операции + сравнивать указатели @ Self и @ Result

Принципиально неразрешима проблема безусловной автоматической инициализации внутреннего поля, но можно инициализировать при первом обращении. Остальные проблемы разрешимы либо завёртыванием интерфейса в запись, либо завёртыванием варианта в запись, об этом далее

Вариант в записи — это как зефир в глазури, но вариант в записи



Собственный тип варианта даёт более полное управление по сравнению с интерфейсом. Так как вариантное поле приватно и наружу этот вариант не должен утекать, можно реализовать лишь минимальный набор методов собственного (custom) типа варианта. Если не считать отладчика, пытающегося привести (CastTo) вариант к строке при наведении курсора, потребуется реализовать копирование (Copy) и уничтожение (Clear) варианта. В оперативной памяти собственные типы варианта, как правило, состоят из маркера типа варианта и указателя (например, наследник TObject). Как это делается, предлагаю посмотреть на примере реализации комплексных чисел (VarCmplx.pas), который присутствует, по крайней мере, начиная с Delphi 7

Использование вариантов пригодилось бы для однозвенной обёртки CFString. Если делать обёртку для интерфейсов, Delphi будет вызывать AddRef и Release у интерфейса, но CFString — не интерфейс, и потребуется либо обернуть CFString в дополнительный слой косвенности из интерфейса, либо использовать собственный тип варианта, который вызывает CFRetain и CFRelease, требуемые для нормального управления памятью CFString. Это работало бы гораздо лучше, чем та обёртка CFString, которую предлагает Embarcadero в Delphi XE2

Эй, а как же Delphi 7?



Delphi — язык с длинной историей, и до того, как появилась объектная система Delphi, в Borland Pascal with Objects была другая объектная система. В Delphi 7 и Delphi 2005 она по–прежнему функционирует. Вместо record пишется ключевое слово object, и получившийся тип во многом похож на record в Delphi 2006: у него могут быть приватные поля, у него могут быть методы. object'ы одного типа можно присваивать друг другу, в этом смысле они тоже аналогичны record. Как раз то, что нам нужно. Компилятор будет ругаться на небезопасный тип, нет перегрузки операций, но это единственные неудобства. Сходство object и record настолько велико, что можно, используя условные директивы компилятора, на старых версиях Delphi объявлять тип как object, а на новых — как record. Именно так я поступил в своей небольшой библиотеке коллекций Delphi-CVariants

Проблемы могут возникнуть, если пытаться объявить несколько таких типов, использующих друг друга. Цикличные зависимости в исходном коде предусмотрены для классов, интерфейсов и указателей, но не для object'ов as is. Предпочтительнее объявлять object'ы так, чтобы каждый следующий знал про предыдущие, но не наоборот. Поэтому, например, в моей библиотечке CMapIterator знает про CVariant, но CVariant не знает про CMapIterator
Иван Левашев @OCTAGRAM
карма
22,0
рейтинг 0,0
Фрилансер
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (56)

  • –36
    Без обид, возможно я ничего не понимаю, но кому нужно сейчас Delphi? Я понимаю что есть куча старого софта на нем написаного, но че действительно он так востребованный?
    • +21
      господи, давайте хоть в этом топике обойдемся без дурацких вопросов
      • –25
        И в чем вопрос дурацкий? Мне действительно интересно узнать где он востребованный. Можете Вы сможете объяснить? Я Вас с удовольствием выслушаю.
        • +21
          В каждом топике по Delphi находится кто-то вроде вас, кто задает этот вопрос. Я лично не хочу в сотый раз подымать эту тему
        • +2
          А что в вашем понимании значит «востребованный». Если на нем уже более 20 лет (включая Pascal) пишут ПО для автоматизации телевещания для лидера отрасли — это «востребован»? И, чем Delphi так плох, что не может быть востребован?
        • +16
          Что вы вообще делаете в хабе "Delphi"? Отпишись от него и вам будет спокойнее )
        • +1
          Найдите холиварный топик по Делфи и там можно сраться до конца скролла.
          А тут — не мешайте ностальгировать :)
        • +4
          Вы скайпом пользуетесь? Наверное будете удивлены, если узнаете, что он написан на Delphi.
          • +3
            И в каждом холиваре всегда вижу этот аргумент :-).
      • 0
        .
        • 0
          Эта ваша «точка» зрения меня наводит на неоднозначные мысли по поводу сказанного.
          • 0
            Ошибся веткой, а удалить комент возможности нет :(. Так что извините, впредь буду осторожней.
            • 0
              Согласен, удаления порой сильно не хватает.
              • +1
                Особенно возможности удалить чужой комент :).
                • 0
                  Quod licet Iovi, non licet bovi!
    • 0
      *введите сюда язык (не C++\Delphi), компилируемый в нативный код и с удобным визуальным фреймворком (+дизайнером форм)*
    • +1
      Хотя-бы с теоретической точки зрения интересно (дизайн языков программирования). Я именно с этой точки зрения прочитал.
      • –9
        Так я же не спорю. У меня вопрос, где востребован сейчас Delphi?
        • +2
          Будь у меня достаточно денег, я бы создал компанию в которую нанимал делфи-программистов и учил их С++, С# или Java. // второй раз за день опасно шуткую
          • +7
            А после обучения этим языкам предполагается, что они и дальше пишут на Delphi? Вы думаете, им понравится C++, в котором приведение типа из char* в std::string вызывает Segmentation Fault в случае NULL? Или Java, в которой при сравнении строк можно получить NullPointerException, если не использовать equals из стороннего пакета Apache Commons.

            Delphi и Ada — нативные языки программирования, позволяющие работать в том числе с сишными библиотеками, но при этом изолированные от них языковым барьером. Нет ситуации, когда я делаю инклуд одного файла, этот файл инклудит заголовки Win32, и через все эти инклуды в препроцессор и пространство имён заливается неконтроллируемый объём дефайнов и объявлений. И Delphi, и, особенно, Ada имеют свои механизмы определения области видимости, отличные от C и C++ в лучшую сторону.

            Кроме того, в Delphi и Ada для примитивных типов можно включить проверку диапазонов, и это штатная фича, в отличие от костылей типа Electric Fence или libmudflap в C/C++. И, наоборот, в отличие от Java и C#, эти проверки можно отключать.
            • 0
              Или Java, в которой при сравнении строк можно получить NullPointerException, если не использовать equals из стороннего пакета Apache Commons.
              Неубедительно.
              При вызове метода объекта по нулевому указателю в Delphi тоже может быть NPE AV.
              Что бы обезопасить себя от NPE достаточно одной проверки на null, безо всяких невизуальных компонентов сторонних пакетов.

          • +1
            Не бывает делфи-программистов или джава-программистов. Бывает программист — человек, знающий минимум три языка. Один чтобы «от зубов», а второстепенные «read only», и то только потому, что он их редко использует. А все остальные — это студенты, нубы, быдлокодеры…
        • +3
          М.б. у мелкомягких? Скайп-то они купили...)
    • +2
      Мне нужен. Уходите.
    • 0
      Я делаю на дельфи $300k в год. Без обид.
      • 0
        Что разрабатываете, если не секрет?
        • 0
          Всяким — z3x-team.com
          • 0
            Знакомая штука! Вам надо над информативностью поработать, я так и не понял, что к чему, и как разлочить телефон. закрыл страницу и пошел гуглить дальше.
      • 0
        Та же тема. К томуже, я делаю на нем все, что хочется — ISAPI Extension, exe, dll… разве что, драйверы писать нельзя. Для этого нужен обычный C. И любой другой разработчик может всязть мою dll и его не будет беспокоить, на каком языке она написана.
    • +1
      Без обид, просто не нужно заходить, а тем более комментировать тему, если она Вам не интересна.
      • –3
        Я сказал что тема мне не интересна? Мне интересно, где сейчас востребован Delphi? И да почитайте ниже комментарии, там больше наезда на Delphi, чем как вы посчитали в моем вопросе
    • +1
      Погуглите, пожалуйста, Embarcadero Showcase
      Многие (не) риторические вопросы легко гуглятся.
  • 0
    Аналог DriverPackSolution -DriverX написан на дельфи, скайп, тотал, один из создателей Delphi Придумал C#.

    Чем больше программируешь на Visual C++, тем больше понимаешь что он не визуальный. Очень популярный МЕМ
    Тем более что Все плюшки Delphi утекли в C# после ухода Хейлсберга из Borland в Microsoft.

    ЛЯ ЛЯ ????
    • –1
      Что такое Visual C++?
      • +5
        чую скоро появится поколение людей которые будут спрашивать: «что такое Delphi?»
        • +1
          Уже сталкивался. Рожденные в середине 90-х.
          • +1
            Простите, что? Я начинал с Delphi, например.

            И да, у меня все друзья о нем знают, т.к. он (в начале обучения) — один из наиболее простых языков.
            • 0
              У меня большинство тоже знают, включая 90-тиков. Но есть и такие, для кого это как для нас какой-нибудь PL/M. Такое впечатление, что их уже в школе не учат паскалю, а на большинстве веб-ресурсов о программировании чаще упоминаются совсем другие инструменты.
        • 0
          Сарказма значит вы не разглядели в посте, увы… Нет такого языка Visual C++, это древнее название Visual Studio.
  • +1
    Я всё понимаю, но Delphi 7 в 2012 году?
    К чему эта некрофилия? Его даже поставить без плясок с бубнами на Win 7 не получится.
    Давно пора переползти на более поздние версии.
    • +4
      — Ставится без проблем.
      — В режиме совместимости XP работает хорошо.
      — Если вы компоненто-писатель, то какая у вас альтернатива? Как еще вы будете поддерживать эту армию программистов, которым все еще нужен D7?
    • 0
      Скорость скачивания обновлений по gprs может быть от 1.5 до 5 Кбайт/сек. Тариф — 7 руб. за Мб

      .bpl'ки улучшают ситуацию, но само их появление на платежных терминалах можно приурочить только к крупным обновлениям
    • +1
      никогда не задумывались, что есть старые проекты, которые надо саппортить, а рефакторинг под новые версии — нереальный геморрой?:) Плюс это последняя действительно быстрая и стабильная IDE из всей линейки. Да, там нет кучи современных вещей, мне там не хватает дженериков как воды, но в целом, работать можно.
      Ставлю на вин7 без единой пляски с бубнами, кстати:)
      • –1
        По-моему, вы кривите душой. ;) Режим совместимости с ХР таки включать надо. И ставить в Program Files ни в коем случае нельзя, иначе задолбает «разреши, хозяин, писаться в ту папочку»
        • +2
          не ставил режим совместимости:) ставлю на несистемный раздел и все:)
      • 0
        Так прямо и «нереальный геморрой»?
        Тогда и его поддержка на D7 тоже не сахар, видимо :)
        Перетаскивал проект (около миллиона строк) с D7 на D2009, затруднения были только с Indy.
        • 0
          всякое бывает, знаете ли. не скажу, что разработка на д7 доставляет острые анальные боли, работа как работа:) Синтаксический сахар это хорошо, но на качестве кода это редко сказывается.

          Свой проект перенести проще, вы попробуйте перенести чужой, да с кучей зависимостей, да с библиотеками, которые не обновлялись уже с пяток лет;) Вот это приключение, я вам скажу. И проблемы с юникодом это так, фигня, решается быстрее всего. Сложнее становится, когда логика перестает работать как раньше в силу особенностей новой платформы, каких-то новых багов или потому что старые хаки не работают. Вот это действительно превращает процесс перевода в крупную головную боль.
  • +3
    Маловато примеров. А без примеров все эти пляски с бубном выглядят подозрительно. Попробую предложить решения некоторых проблем:
    Автоматическое управление памятью. Лестница из try… finally — это не серьёзно
    obj1 := nil;
    obj2 := nil;
    try
      obj1 := TClass1.Create;
      obj2 := TClass2.Create;
     ....
    finally
      obj1.Free;
      obj2.Free;
    end;
    решение масштабируется на любое количество объектов.

    Copy-on-write и счётчики ссылок
    Счетчики ссылок встроены в TInterfacedObject. готовое Copy-on-write реализовано только в string, остальное да, ручками.

    Другое особое поведение при копировании
    Непонятно каким образом объектная модель Delphi мешает реализовать любое копирование, как вам нужно.

    Неинициализированная переменная должна вести себя как пустая строка или 0, соответственно
    Это почему еще? Даже в антагонистическом С++ это не так.

    Методы не могут изменить содержимое переменной–указателя, у которого они были вызваны
    Зачем такие извращения?

    Нет контроля за тем, что происходит при копировании объекта.
    Встроенного копирования объектов в Delphi нет, соответственно нет и контроля. Копируйте и контролируйте сами.

    Подозреваю, что до Delphi вы имели богатый опыт на каком-то высокоуровневом языке вроде C# или Java. У Delphi'ста с рождения, таких проблем возникать не должно :)
    • 0
      Непонятно каким образом объектная модель Delphi мешает реализовать любое копирование, как вам нужно

      a := b; — объектная модель не мешает, но и не помогает. Только, используя варианты, можно вклиниться в процесс копирования

      Неинициализированная переменная должна вести себя как пустая строка или 0, соответственно

      Это почему еще? Даже в антагонистическом С++ это не так

      Имелось в виду, что string(nil) в Delphi — это пустая строка, и собственные типы могли бы быть устроены так же…

      Методы не могут изменить содержимое переменной–указателя, у которого они были вызваны

      Зачем такие извращения?

      … в том числе, становясь nil, если их занулить. Например, если сделать строке .Delete по всему диапазону. А также это необходимо для copy-on-write. MyCustomString.Append('12') может захотеть стать другим указателем, если будет знать, что не уникален.

      Нет контроля за тем, что происходит при копировании объекта.

      Встроенного копирования объектов в Delphi нет, соответственно нет и контроля. Копируйте и контролируйте сами.

      В некоторых случаях это крайне нежелательно, особенно, когда объекты начинают выстраиваться в иерархию и иметь пересекающиеся части.
      • 0
        полагаю, что немного примеров из реального кода помогли бы:)
  • +1
    Я для себя написал вот такой класс:

    TkaGarbageCollector
    unit kaGarbageCollector;
    
    interface
    
    {$REGION 'interface uses'}
    uses
     // Стандартные модули:
     Generics.Collections;
    {$ENDREGION}
    
    type
     ///<summary> Класс позволяющий упростить освобождение памяти.</summary>
     TkaGarbageCollector = class
     public
      const
       DEFAULT_TAG = 'DEFAULT_TAG';
     private
      items: TDictionary<TObject, string>;
     public
      constructor Create();
      destructor Destroy; override;
    
      ///<summary> Добавить элемент в список и вернуть свежедобавленный элемент.</summary>
      function add<T:class>(item: T): T; overload;
    
      ///<summary> Добавить помеченый элемент в список и вернуть свежедобавленный элемент.</summary>
      function add<T:class>(const tag: string; item: T): T; overload;
    
      /// <summary> Произвести сборку мусора с указанной меткой. </summary>
      procedure gc(const tag: string);
     end;
    
    implementation
    
    {$REGION 'TGarbageList'}
    constructor TkaGarbageCollector.Create();
    begin
     items := TObjectDictionary<TObject, string>.Create([doOwnsKeys]);
    end;
    
    destructor TkaGarbageCollector.Destroy;
    begin
     items.free();
    
     inherited Destroy;
    end;
    
    function TkaGarbageCollector.add<T>(item: T): T;
    begin
     result := add(DEFAULT_TAG, item);
    end;
    
    function TkaGarbageCollector.add<T>(const tag: string; item: T): T;
    begin
     items.add(item, tag);
     result := item;
    end;
    
    procedure TkaGarbageCollector.gc(const tag: string);
    var key: TObject;
        item: TPair<TObject, string>;
        gcList: TList<TObject>;
    begin
     gcList := TList<TObject>.Create();
     try
      for item in items do
       begin
        if (item.Value = tag) then
         gcList.add(item.Key);
       end;
    
      for key in gcList do
       items.remove(key);
     finally
      gcList.free();
     end;
    end;
    {$ENDREGION}
    



    Его можно применять начиная с Delphi 2009, так как он использует generics.

    Применять вот так:
      gc := TkaGarbageCollector.Create();
      try
       log := gc.add(TLogStrings.Create());
       warnings := gc.add(TLogStrings.Create());
       partialTrips := gc.add(TLogStrings.Create());
      .... code ....
      finally
       gc.free();
      end;
    

    • +1
      ну сравни это с:
      var
        FileStreamPtr: TScopedPtr<TFileStream>;
      begin
        FileStreamPtr := TFileStream.Create('C:\1.data');
        FileStreamPtr.Value.WriteBuffer(..);
        ...
      end;
      
      
  • 0
    у raii на рекордах в дельфи по сравнению с плюсами — есть ряд минусов

    1) удобство использования.
    у рекорда должно быть некое свойство Value для доступа к методам объекта
    2) производительсность.
    какой нить интерфейсы/варианты дают оверхед. Правда на уровне общей слабости дельфийского компилятора/RTL — это не так и важно.
    3) невозможность реализовать что нить типа unique_ptr (http://en.wikipedia.org/wiki/Smart_pointer#unique_ptr). невозможно в компил тайм запретить присваивать один указатель другому. можно только в ран тайм просигнализировать об этом.

    ну и если касается в дельфи7, то там невозможно создать универсальное решение, только затачиваться под конкретный класс.
    а так, в целом пользоваться можно.
    • 0
      Для доступа к методам объекта используются методы record (или методы object в Delphi 7 и 2005)

      По поводу оверхеда — да, это действительно так. Смысл статьи — показать, что кое–какие фичи в Delphi реализуемы и показать, куда копать

      Кстати, System.Rtti.TValue — это как раз интерфейс в записи
  • 0
    >>Для доступа к методам объекта используются методы record (или методы object в Delphi 7 и 2005)
    это работает только если твой врайпер заточен под строго определенный класс объектов. Для общего случая — это невозможно реализовать.

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