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

OCTAGRAM 19 декабря 2012 в 23:25 16,6k
Тема 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
Проголосовать:
+26
Сохранить: