2 мая 2013 г.

Эволюция Delphi: современные возможности

Среда Delphi не стоит на месте. Каждый год выходит новая версия Delphi с новыми возможностями. Компании Borland (Inprise) и Embarcadero всегда стремились сохранять в своих продуктах высокий уровень обратной совместимости, поэтому каждая новая версия Delphi способна почти без проблем компилировать старый код. Тем не менее, некоторые существующие возможности могут не существовать в новом мире и окружении или вести себя иначе.

В этой статье я попробую сделать небольшой обзор современных тенденций развития языка Delphi и изменений в нём. В целом, статья будет сконцентрирована на новейших измененияx в архитектуре Delphi, доступные в XE4.

Not Invented Here: Like the deserts miss the rain

Новый компилятор

Среда разработки состоит из компилятора (переводит исходный текст программы в машинный/виртуальный код), компоновщика/linker (собирает программу из готовых блоков, созданных компилятором), отладчика (debugger), редактора кода (и вообще, в целом - визуальной оболочки) и дополнительных утилит. Ну и, конечно же, среда разработки зависит от языка и библиотек на нём. Всё вместе это называется toolchain (букв. "цепочка утилит") - набор утилит для создания приложений. Слово "цепочка" намекает на то, что результат работы одной утилиты используется следующей (т.е. редактор -> компилятор -> компоновщик -> отладчик).

Среда Delphi является развитием языка Pascal. Toolchain Delphi является закрытой (проприетарной) разработкой Borland. За всю историю Delphi она поддерживала несколько платформ (Win16, Win32, Win64, Linux/CLX, .NET). Под каждую платформу был свой собственный компилятор, который был монолитным. Исходный код компилировался компилятором непосредственно в машинный код целевой платформы (файлы .dcu и .obj).

В этой ситуации добавление новой платформы было непростым делом, поскольку требовалось разрабатывать компилятор для неё с нуля. Дополнительными сложностями был перенос существующего код RTL и VCL, завязанного на конкретную платформу (Win32). Сегодня доля Windows уменьшается, а на сцену выходят более молодые платформы: от Apple и Google. Причём актуальные платформы меняются намного быстрее, чем это происходило в прошлом. В ситуации с таким динамическим изменением имеет смысл упростить разработку компилятора, чтобы более оперативно реагировать на изменения и вносить новые возможности.

Поэтому, центральной идеей ближайшего развития Delphi становится модульный компилятор. Идея заключается в том, чтобы разделить (ранее монолитный) компилятор на две части: т.н. front-end и back-end. Front-end компилятора берёт исходный код программы и переводит его не в машинный код конкретной платформы, а в (универсальный) виртуальный код - т.н. байт-код. Байт-код - это максимально универсальное представление логики программы, не зависящее от языка и платформы. Back-end работает по результату работы front-end: он преобразовывает байт-код уже непосредственно в машинный код конкретной платформы.

Таким образом, вместо того, чтобы делать компилятор полностью для каждой новой платформы, можно оставить front-end неизменным (а ведь именно он отвечает за синтаксис языка), а написать только новый back-end. Более того, вместо того, чтобы использовать собственную проприетарную (и ни с кем не совместимую) разработку, можно использовать широко известное решение (в качестве back-end, конечно же) - получив при этом не только частично готовый код, но и совместимость с некоторыми сторонними утилитами. В качестве такого известного решения разработчики Delphi решили использовать LLVM (Low Level Virtual Machine) - это универсальная система анализа, трансформации и оптимизации программ, реализующая виртуальную машину с RISC-подобными инструкциями.

LLVM используется, в частности, в компаниях Adobe, Apple и Google (например, iPhone SDK использует back-end LLVM). Apple и Google являются одними из основных спонсоров проекта. В настоящее время для LLVM есть back-end-ы для x86-32, x86-64, ARM, PowerPC, SPARC, MIPS, Qualcomm Hexagon и front-end-ы для С, C++, Objective-C, Fortran, Ada, Haskell, Java, Python, Ruby, JavaScript, GLSL (в т.ч. - Clang и GCC). А теперь ещё к front-end добавляется и Delphi. Конечно же, LLVM понятия не имеет про Паскаль и Borland-ский форматы файлов. Но Delphi может иметь свой собственный front-end, который будет компилировать исходный код Паскаль в байт-код LLVM (называемый LLVM IR - "Intermediate Representation", т.е. "промежуточное представление"). А готовый back-end от LLVM может скомпилировать IR от front-end Delphi в машинный код x86-32, x86-64 или ARM. Хотя LLVM IR похож на готовый байт-код для некой виртуальной машины или JIT-компилятора, он всё же нацелен именно на чёткое разграничение front-end и back-end и может рассматриваться как вывод компилятора - аналогично .dcu (Delphi) и .obj (C++ Builder) файлам.

Итак, теперь должно быть очевидным, что в будущем Delphi будет иметь новый компилятор, совместимый с LLVM - и начнётся это уже сейчас, начиная с компилятора для iOS (ARM). А для C++ Builder новая эра началась ещё в прошлом году: 64-битный компилятор C++ Builder сделан уже на новой архитектуре (LLVM). Конечно же, компилятор - это ещё не всё. Нужен ещё компоновщик, отладчик, библиотека поддержки языка (RTL), а для визуального языка - ещё и визуальная библиотека (такая как VCL, CLX, FMX). Также важно отметить, что LLVM в каком-то смысле "подталкивает" разработчиков front-end-ов использовать определённые подходы к управлению памятью, потоками и исключениями. Хотя это и всего лишь "толчок", а не железное ограничение. Стоит отметить, что для мобильных платформ распространена практика использовать LLVM (или виртуальные среды типа Java и .NET), которые поддерживают автоматическое управление памятью: или сборку мусора (garbage collection) или автоматические ссылки (ARC - Automatic Reference Counting). В итоге, вывод: автоматическое управление памятью более предпочтительно, т.к. оно более проработано, поддерживается мобильными устройствами и более привлекательно для новичков.

Итак, сегодня в Delphi (и я говорю про Delphi XE4) есть пять компиляторов: для Win32, Win64, MacOS, эмулятор iOS (компилирует в x86) и iOS (компилирует в ARM). Компиляторы для Win32, Win64, MacOS и эмулятор iOS являются классическими, а компилятор для iOS основан на новой архитектуре LLVM. Как я сказал выше, C++ Builder отличается тем, что компилятор для Win64 у него тоже является новым (LLVM).

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

Изменения в языке

Несмотря на то, что Delphi уже давно поддерживает несколько платформ, до сих пор язык Delphi не претерпел никаких хирургических вмешательств по отсечению старых возможностей. Компилятор для каждой новой платформы создавался полностью Borland/CodeGear/Embarcadero и педантично тащил за собой весь багаж обратной совместимости.

Сейчас ситуация несколько иная. Во-первых, необходимо сделать компилятор (front-end) из Паскаль кода в LLVM IR - что потребует тщательного воспроизведения всего накопленного багажа из обратной совместимости. Во-вторых, ввод нового компилятора совпадает с введением поддержки мобильных платформ. Перенос старого уже написанного кода на мобильную платформу, вероятно, и так потребует пересмотра. В-третьих, добавление новых платформ требует введения в язык новых возможностей. Частично они будут перекрывать старые. В языке будет несколько способов сделать одно и то же. Язык станет слишком сложным сам по себе, не говоря уже о сложностях изучения его для новичков. В четвёртых, уже сегодня в Delphi есть как избыточность (посмотрите, сколько есть в ней типов строк), так и несогласованность (сравните индексацию с 1 для строк, но с 0 - для списков и массивов).

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

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

Итак, современные (и будущие) изменения в языке Delphi заключаются в следующем:
  1. Строки:
    • всего один тип строк
    • индексируются с 0
    • "неизменяемые" строки (immutable strings)
  2. Улучшения классического механизма автоматического подсчёта ссылок:
    • Автоматические ссылки для объектов
    • Слабые (weak) ссылки
  3. Новые классы и процедуры в RTL для кросс-платформенного кода
  4. Отсутствие пакетов и DLL на некоторых платформах
  5. В будущем:
    • with - deprecated
    • object - deprecated
    • указатели - deprecated
    • ассемблер - deprecated

Строки

Больше всего изменений в новых версиях Delphi приходится на строки. Для этого есть несколько причин:
  • Упрощение модели строк (несколько типов строк)
  • Унификация (1-индексация)
  • Оптимизация (требования более слабых мобильных платформ)

Сохранение обратной совместимости со строками из времён Turbo Pascal/Delphi 1 слишком затратно как для разработчиков самой Delphi, так и для разработчиков на Delphi (особенно новичков).

Единый строковый тип

Сегодня в Delphi есть следующие типы строк:
  • Delphi-строки:
    • Родной (string) - псевдоним для UnicodeString
    • UnicodeString (счётчик ссылок, Unicode, длина, размер символа, нуль-терминированная)
    • AnsiString (счётчик ссылок, Ansi, длина, размер символа, нуль-терминированная)
    • AnsiString[кодовая-страница] (счётчик ссылок, кодовая страница, длина, размер символа, нуль-терминированная)
    • RawByteString (счётчик ссылок, длина, размер символа, нуль-терминированная)
  • Pascal-строки:
    • ShortString (Ansi-кодировка, 255 символов, счётчик длины в первом символе)
    • String[число] (Ansi-кодировка, менее 255 символов, счётчик длины в первом символе)
  • C-строки:
    • PChar - псевдоним для PWideChar
    • PAnsiChar (Ansi, нуль-терминированная)
    • PWideChar (Unicode, нуль-терминированная)
  • WideString (BSTR из COM, Unicode, нуль-терминированная, счётчик длины, специальный API)
Если вы посмотрите на этот список, то заметите следующую вещь: всюду в вашей программе вы оперируете со строками типа string. Все прочие типы строк нужны вам исключительно для совместимости со сторонним кодом: вашим же старым кодом (AnsiString или Pascal-строки), ОС (нуль-терминированные или BSTR) и т.п. Такой зоопарк не только вызывает путаницу (вопросы вида "в чём разница между WideString и UnicodeString?"), но и весьма сложен для переноса на другие платформы (чему равен WideString на iOS?). Поэтому идея заключается в том, чтобы оставить один тип строк - самый удобный и универсальный. Гораздо лучше использовать не строковые типы (записи/классы) для коммуникации с внешним миром - так их семантика будет понятнее. А перегрузка операторов сделает безболезненным операции присваивания.

Именно поэтому на новых LLVM компиляторах iOS есть только тип string. Все прочие типы строк там не объявлены и при попытке ими воспользоваться сгенерируют вам ошибку вида "Undeclared identificator AnsiString". Новый тип string в целом равен UnicodeString (т.е. хранит данные строки в UTF-16, имеет счётчик ссылок и длины, а также поле кодовой страницы, которое перманентно равно CP_UTF16 = 1200 ($4B0), и поле размера символа, которое перманентно равно 2 байтам).

Однако сказанное не означает, что вы не сможете работать с данными строк других форматов - просто вы не сможете это делать со встроенными (native) типами данных. Например, предположим, вам нужно использовать текстовые данные в формате UTF-8. Вы можете использовать классы типа TTextReader или TEncoding (которые, кстати, тоже появились в Delphi довольно давно), например:
var
  FileName: string;
  TextReader: TStreamReader;
begin
  FileName := TPath.GetHomePath + TPath.DirectorySeparatorChar + 'Documents' + TPath.DirectorySeparatorChar + 'Utf8Text.txt';
  TextReader := TStreamReader.Create(FileName, TEncoding.UTF8);
  try
    while not TextReader.EndOfStream do
      ListBox1.Items.Add(TextReader.ReadLine);
  finally
    FreeAndNil(TextReader);
  end;
end;
Этот простой код скрывает от вас всю работу с UTF-8 строками. А вот вариант с явным преобразованием:
var
  FileName: string;
  FileStream: TFileStream;
  ByteArray: TArray<Byte>;
begin
  FileName := TPath.GetHomePath + TPath.DirectorySeparatorChar + 'Documents' + TPath.DirectorySeparatorChar + 'Utf8Text.txt';
  FileStream := TFileStream.Create(FileName, fmOpenRead);
  try 
    SetLength(ByteArray, FileStream.Size);
    FileStream.Read(ByteArray[0], FileStream.Size);
  finally
    FreeAndNil(FileStream);
  end;
  ListBox1.Items.Text := TEncoding.UTF8.GetString(ByteArray);
end;

Вам может потребоваться хранить строковые данные в других форматах в памяти (например, при вызове сторонних API функций) - в этом случае вам нужно использовать класс TEncoding и хранить строковые данные в (динамическом) массиве байтов (TBytes). При желании вы можете даже эмулировать поведение старого компилятора путём введения типов с перегрузкой операторов, например:
type
  UTF8String = record
  private
    InternalData: TBytes;
  public
    class operator Implicit(s: string): UTF8String;
    class operator Implicit(us: UTF8String): string;
    class operator Add(us1, us2: UTF8String): UTF8String;
  end;
Реализация этого класса может использовать TEncoding для работы (конкретно - TUTF8Encoding). Используя такую запись, вы можете продолжать использовать старый код вида:
var
  strU: UTF8String;
begin
  strU := 'Hello';
  strU := strU + string(' ăąāäâå');
  ShowMessage(strU);
end;

0-индексируемые строки

Как известно, первый символ в любой строке Delphi имеет индекс 1, а не 0, как может ожидать любой программист, ранее не знакомый со строками в Delphi. Это называется 1-индексацией (или индексацией с единицы). 1-индексация строк усугубляется тем, что другие структуры в Delphi (динамические массивы, списки и т.п., а также не-Delphi строки) индексируются с нуля (используют 0-индексацию). Получается некоторая путаница и непривычные корректировки на +/-1 в коде по работе со строками.

Историческая справка: почему в Delphi строки индексируются с 1?
Delphi является наследником языка Pascal. В Паскале не использовались 0-терминированные строки из C. Вместо этого Паскаль использовал так называемые "короткие" строки: первый байт строки служил счётчиком символов (= "байтов" в Паскале) в строке. Таким образом, в отличие от строк C строки Паскаля могли хранить #0 внутри строки и очень быстро определять длину (не нужно было искать терминатор в строке, не было цикла), но были ограничены 255 символами (т.е. строка занимала максимум 256 байт вместе со счётчиком).

Соответственно, в Паскале строки технически индексировались с нуля, но нулевой символ отводился под счётчик длины строки, а данные строки начинались с символа №1. Т.е. данные строки индексировались с единицы.

Когда Delphi ввела длинные строки (AnsiString в Delphi 2), то, хотя у длинных строк уже не было счётчика длины в первом символе строки (теперь он хранился в скрытом заголовке строки), индексацию с 1 оставили по соображениям обратной совместимости - чтобы не пришлось переделывать уже написанный код, который работал со строками в предположении, что они индексируются с 1.

Таким образом строки в Delphi стали индексироваться с 1.

Совместно с введением одного единственного строкового типа решено было изменить и этот аспект поведения строк. Поскольку подобное изменение весьма значительно для языка, но не привязано к архитектуре компилятора, то было решено контролировать этот аспект директивой компилятора: $ZEROBASEDSTRINGS. Кстати, эта директива впервые появилась ещё в XE3. По умолчанию эта директива выключена в Delphi XE3, а в Delphi XE4 она выключена для Win32, Win64 и OSX и включена для iOS и эмулятора iOS. Поскольку эта опция контролируется директивой, то вы можете включить её для Delphi XE3 (чтобы начать миграцию раньше). Более того, вы можете выключить её для iOS, чтобы компилировать старый код.

На что нужно обратить внимание:
  • Внутренняя структура строк не меняется. Иными словами не существует такого понятия как "0-индексированная строка". Строка - это строка. Индексация - это лишь способ доступа к данным, он не влияет на сами данные. Т.е. вы можете смешивать в одном проекте модули, собранные с разными настройками. Более того, вы можете иметь разные настройки для разных функций в рамках одного модуля.
  • Все новые функции в Delphi (хэлпер TStringHelper, TStringBuilder) используют новую семантику (0-индексацию) вне зависимости от опции $ZEROBASEDSTRINGS и компилятора.
  • Все классические функции RTL (Copy, Pos, Delete и т.п.) всегда используют прежнюю семантику (1-индексацию) вне зависимости от опции $ZEROBASEDSTRINGS и компилятора. Тем не менее, Embarcadero рекомендуют не использовать старые RTL-функции (используйте TStringHelper и TStringBuilder).
Другими словами, опция $ZEROBASEDSTRINGS влияет только на вычисление выражений вида StrVar[число]. Посмотрите на такой код:
procedure TForm1.Button1Click(Sender: TObject);
var
  S: string;
begin
  S := 'Hello';
  S := S + ' foo';
  S[2] := 'O';
  Button1.Caption := S;
end;
В любых предыдущих версиях Delphi (XE2 и ниже), а также в XE3 и выше с выключенной опцией $ZEROBASEDSTRINGS вы получите 'HOllo foo'. Но если вы добавите {$ZEROBASEDSTRINGS ON} перед кодом (либо запустите его на iOS, где эта опция уже включена), то получите 'HeOlo foo'. Единственная разница между этими двумя кусками - способ вычисления S[2]: в первом случае вы обращаетесь ко второму элементу, который имеет индекс 2 (отсчёт с 1), во втором случае вы обращаетесь к третьему элементу, который имеет индекс 2 (отсчёт с 0).

Примечание: в предварительных обсуждениях релиза XE4 было несколько заблуждений относительно строк. Заметьте, что способ интерпретации выражения в квадратных скобках для строк вообще не зависит от структуры строки, а остаётся на усмотрение компилятора. В самом деле, вы и ранее использовали 1 как индекс для первого символа длинных строк, но как второй символ для коротких строк (первый символ занят под счётчик и имеет индекс 0). Т.е. строки остаются теми же самыми, меняется только способ вычисления компилятором выражения StrVar[число]. Вы не передаёте в функцию "0-индексированную строку", вы передаёте "просто строку". Это означает, что вы можете смешивать в одном проекте и модули функции, скомпилированные с разными настройками. Посмотрите на такой код:
var
  s1: string;
begin
  s1 := '1234567890';
  ListBox1.Items.Add('Text: ' + s1);
  ListBox1.Items.Add('Chars[1]: ' + s1.Chars[1]);
  ListBox1.Items.Add('s1[1]: ' + s1[1]);
  ListBox1.Items.Add('IndexOf(2): ' + IntToStr(s1.IndexOf('2')));
  ListBox1.Items.Add('Pos(2): ' + IntToStr(Pos('2', s1)));
end;
По умолчанию, в Delphi XE4 этот код покажет 2/1/1/2 на Windows и 2/2/1/2 на iOS. И снова: единственное отличие - интерпретация выражения в квадратных скобочках. И снова: вы можете изменить поведение на любой платформе на обратное, используя $ZEROBASEDSTRINGS.

Если вы хотите написать универсальный код, который будет работать для обоих вариантов $ZEROBASEDSTRINGS, то вы можете определить константы, зависящие от значения Low(string), которое будет равно 1 и 0 для {$ZEROBASEDSTRINGS OFF} и {$ZEROBASEDSTRINGS ON}, соответственно. Например:
const
  thirdChar = Low(string) + 2; // = 2 или 3, но всегда обозначает третий символ в строке
...
  s1[thirdChar] := 'A';
Этот код будет работать всегда одинаково, вне зависимости от настроек компилятора. А вот как вы можете работать с циклами:
var
  S: string; 
  I: Integer;
  ch1: Char;
begin
  // Классический for 
  // (работает только для 1-индексированных строк, 
  // не будет работать для 0-индексированных строк)
  for I := 1 to Length(S) do
    use(S[I]);

  // "Новый" for, вариант 1 
  // (работает только для 0-индексированных строк, 
  // не будет работать для 1-индексированных строк)
  for I := 0 to Length(S) - 1 do
    use(S[I]);

  // "Новый" for, вариант 2 
  // (работает для любых настроек, 
  // XE3 и выше)
  for I := 0 to S.Length - 1 do
    use(S.Chars[I]);

  // Универсальный, вариант 1
  // (работает для любых настроек, 
  // доступен, начиная с Delphi 2006)
  for ch1 in S do
    use(ch1);

  // Универсальный, вариант 2
  // (работает для любых настроек, 
  // доступен, начиная с Delphi XE3)
  for I := Low(S) to High(S) do
    use(S[I]);
end;
Low(S) возвращает 0 для 0-индексированной строки и 1 - для 1-индексированной. High(s) возвращает Length(S) - 1 для 0-индексированной строки и Length(S) - для 1-индексированной. В случае пустой строки Low, конечно же, возвращает всё то же значение, а High возвращает -1 или 0, соответственно. Вы можете передать тип вместо переменной в Low, но это не сработает для High.

Вместо Low и High вы можете использовать хэлпер для строк, который появился в Delphi XE3. Фактически, в Delphi XE3 появилась новая возможность: возможность добавлять методы любым встроенным типам данным, а не только записям и классам. Хотя синтаксис несколько необычен для Delphi:
type
  TIntHelper = record helper for Integer
    function ToString: string;
  end;

procedure TForm1.Button2Click(Sender: TObject);
var
  I: Integer;
begin
  I := 4;
  Button2.Caption := I.ToString;
  Caption := 400000.ToString;
end;
Кроме самой возможности в Delphi XE3 были введены и некоторые новые конструкции, использующие новую возможность. Среди них: TStringHelper - хэлпер для типа string. Он объявлен в модуле SysUtils и предоставляет методы вида Compare, Copy, IndexOf, Substring, Length, Insert, Join, Replace, Split и многие другие. Поэтому теперь вы можете написать:
procedure TForm1.Button1Click(Sender: TObject);
var
  s1: string;
begin
  // С переменной
  s1 := 'Hello';
  if s1.Contains('ll') then
    ShowMessage (s1.Substring(1).Length.ToString);

  // С константой
  Left := 'Hello'.Length;

  // Можно выстраивать вызовы в цепочку
  Caption := ClassName.Length.ToString;
end;
Заметьте, что все эти методы (включая индексированное свойство Chars) используют индексацию с нуля вне зависимости от настроек компилятора.

Immutable-строки

Несмотря на то, что новый единый тип string по-прежнему эквивалентен бывшему UnicodeString, внутренняя реализация строк может быть изменена в будущем и/или на других мобильных платформах. Уже сейчас предполагается, что строки станут неизменяемыми (т.н. immutable-строки): это означает, что строку нельзя изменить когда она была создана. Этот аспект не влияет на операции типа конкатенации (сложения строк), потому что эти операции создают новую строку из каких-то других строк. Immutable-строки влияют на in-place операции вида S[1] := 'A'; - такие операции "запрещены".

Ещё раз: сегодня строки по прежнему изменяемы в любых компиляторах (в том числе - для iOS). Конструкции вида S[1] := 'A'; полностью разрешены (в том числе - для iOS). Тем не менее, в будущем этот аспект может быть ограничен.

Сегодня все компиляторы Delphi используют семантику copy-on-write (копирование-при-записи): если вы модифицируете строку, а она имеет счётчик ссылок больший 1, то строка копируется в новую, и изменения вносятся в копию, оставляя старую версию неизменной - так что все остальные (кто держит ссылку на строку) не увидят вашего изменения. Иными словами, вместо копирования строки изначально при присваивании, техника copy-on-write копирует строку позже - когда её необходимо изменить. Копирования может и не произойти, если вы не модифицируете строку. Внутренне это достигается (скрытыми) вызовами UniqueString для строк вида S[1] := 'A';. Разумеется, вам нужно вставлять вызовы UniqueString вручную, если вы работаете с содержимым строки напрямую (через указатели).

Immutable-строки предлагают улучшенную модель управления памятью. В будущем вы не сможете модифицировать содержимое строки, либо эта операция будет допустимой, но станет медленнее. В то же время конкатенация строк станет быстрее.

Уже сегодня вы можете найти потенциальные пробные места в вашем коде. Для этого вы можете включить подсказки компилятора директивой {$WARN IMMUTABLE_STRINGS ON}. С включенной опцией компилятор будет выдывать такие предупреждения:
[dcc32 Warning]: W1068 Modifying strings in place may not be supported in the future”
И снова: если вы используете в вашем коде только конкатенацию строк, то immutable-строки вас не коснутся - ни сейчас, ни в будущем (в самом деле, оптимизация конкатенации - это одна из целей для развития Delphi). Разработчики Delphi не ожидают, что конкатенация строк станет медленнее или будет запрещена в будущем. Только изменение индивидуальных символов (содержимого строки) может вызвать проблемы в будущем (а может и не вызвать).

Тем не менее, сегодня операция конкатенации может быть не самым оптимальным способом работы со строками на мобильной платформе. Вы можете знать, что в Delphi уже давно есть специализированный класс для построения строк: TStringBuilder. Несмотря на то, что этот класс присутствует в Delphi уже давно (начиная с Delphi 2009), он не пользуется популярностью. Почему? Посмотрите на такой код:
const
  MaxLoop = 2000000; 

procedure TMainForm.btnConcatenateClick(Sender: TObject);
var
  str1, str2, strFinal: string;
  sBuilder: TStringBuilder;
  I: Integer;
  t1, t2: TStopwatch;
begin
  t1 := TStopwatch.StartNew;
  str1 := 'Hello ';
  str2 := 'World ';
  for I := 1 to MaxLoop do
    str1 := str1 + str2;
  strFinal := str1;
  t1.Stop;

  Memo2.Lines.Add('Length: ' + IntToStr(strFinal.Length));
  Memo2.Lines.Add('Concatenation: ' + IntToStr(t1.ElapsedMilliseconds));

  t2 := TStopwatch.StartNew;
  str1 := 'Hello ';
  str2 := 'World ';
  sBuilder := TStringBuilder.Create(str1, str1.Length + str2.Length * MaxLoop);
  try
    for I := 1 to MaxLoop do
      sBuilder.Append(str2);
    strFinal := sBuilder.ToString;
  finally
    FreeAndNil(sBuilder);
  end;
  t2.Stop;

  Memo2.Lines.Add('Length: ' + IntToStr(strFinal.Length));
  Memo2.Lines.Add('StringBuilder: ' + IntToStr(t2.ElapsedMilliseconds));
end;
На Desktop-платформах подобный код даст следующие результаты:
Length: 12000006
Concatenation: 60 (msec)
Length: 12000006
StringBuilder: 61 (msec)
Иными словами, на мощных платформах нет никакой выгоды от использования TStringBuilder, поскольку умный менеджер памяти (типа FastMM или даже встроенного в ОС) успешно выполняет ту же работу, что и TStringBuilder (работу по динамическому росту блоков памяти).

Но на мобильных платформах ситуация иная: более слабые платформы не имеют такого же сложного и умного менеджера памяти, как и Desktop-ы. Поэтому результат работы такого кода на iOS будет следующим:
Length: 12000006
Concatenation: 2109 (msec)
Length: 12000006
StringBuilder: 1228 (msec)
В этом случае TStringBuilder почти в два раза быстрее простого сложения строк.

В целом же, вы можете не менять свой код, если только у вас не будет показаний профайлера, что ваша программа тормозит именно из-за работы со строками. В большинстве программ (которые не выполняют два миллиона операций изменения строк в секунду) разница в производительности не будет заметна.

Улучшения классического механизма автоматического подсчёта ссылок

Delphi для iOS вводит в язык поддержку ARC (Automatic Reference Counting) - "автоматический подсчёт ссылок". ARC является улучшенным механизмом подсчёта ссылок, который существовал в Delphi со времён Delphi 2 - для строк, вариантов, динамических массивов и интерфейсов. Фактически, единственными данными, управляемыми вручную, в Delphi являлись объекты и указатели. И если указатели уже давно успешно вытесняются управляемыми аналогами, то объекты продолжали оставаться типами с ручным управлением, плодя бесконечные вложенные иерархии try-finally в вашем коде.

До сегодняшнего дня. Сегодня ARM компилятор Delphi вносит автоматическое управление временем жизни и в объекты.

Автоматические ссылки для объектов

ARC является механизмом автоматического учёта памяти. Часто ему противопоставляют реализацию автоматического учёта памяти из .NET, называемую (несколько ошибочно) сборкой мусора (garbage collection). Оба механизма служат одной цели, но делают это разными способами. Напомню, что менеджер памяти .NET периодически запускает подпрограмму очистки памяти, которая пытается найти блоки памяти (или группы блоков), на которые нет внешних ссылок. Здесь же видно, в чём отличие двух подходов: ARC 100% детерминирован - память освобождается всегда в один и тот же момент (когда счётчик ссылок падает до нуля), способ .NET может освобождать память позднее, чем она реально отпускается. Кроме того, освобождение памяти (и, следовательно, объектов) в ARC выполняется текущим же потоком, а не фоновым потоком-уборщиком, как это происходит в .NET. Однако, ARC всё ещё допускает возможность утечек памяти, если вы создадите циклическую ссылку (первый объект указывает на второй, а второй - на первый), в то время как .NET увидит два блока памяти, изолированные от остальных, и удалит их.

Примечание: хотя ARC реализован только в (LLVM) компиляторе для iOS, его эмуляция также доступна на (классическом) компиляторе "эмулятор iOS". ARC не доступен в компиляторах для Win32, Win64 и OSX.

Использовать ARC очень просто - вам практически не нужно думать об управлении памятью. В вашей практике вы постоянно использовали строки (string) и практически никогда не задумывались об управлении памяти для них. Точно так же вы теперь можете поступать и с объектами:
var
  MyObj: TMySimpleClass;
begin
  // создание объекта, счётчик ссылок увеличивается с 0 до 1
  MyObj := TMySimpleClass.Create;  
  // использование объекта, счётчик ссылок = 1 
  MyObj.DoSomething;               
  // ссылка на объект уходит из зоны видимости, уменьшение ссылки с 1 до 0, уничтожение объекта
end;
Ближайший аналог ARC для объектов - это работа с интерфейсами (interface) в Delphi. Если вы когда-либо работали с интерфейсами в Delphi, то теперь точно так же сможете работать и с обычными объектами.

Точно так же, как с интерфейсами (и любыми другими типами с автоматическим управлением памятью в Delphi), вы можете удалить ссылку преждевременно (до выхода переменной за область видимости) путём присвоения переменной значения nil:
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;  
  MyObj.DoSomething;               
  MyObj := nil; // деструктор запустится здесь
end;
Хотя строка "end" по прежнему будет содержать (скрытый) блок finally с очисткой MyObj - в этом варианте кода "магия" компилятора отработает вхолостую, поскольку вы сами освободили ссылку до выхода из подпрограммы. Разумеется, если метод DoSomething вызовет исключение, то строка с присвоением nil будет пропущена, и тогда объект, как и ранее, будет удалён из "подстилки" компилятора в строке "end".

Заметьте, что в этих примерах отсутствуют явные блоки try-finally - и код при этом остаётся 100% корректным. Это благодаря тому, что блок try-finally теперь является скрытым. Теперь вам не нужно писать многоуровневые вложенные блоки try-finally! Фактически, то, что делает сейчас ARC, эквивалентно такому коду (который, впрочем, вы и сами могли писать ранее вручную):
var
  MyObj: TMySimpleClass;
  FileData: TFileStream;
  ImageData: TImageData;
begin
  // "Магия" компилятора из "begin"
  Pointer(MyObj) := nil;
  Pointer(FileData) := nil;
  Pointer(ImageData) := nil;
  try

    // Реальный код подпрограммы
    MyObj := { ... };    

    // ...

    FileData := { ... };    

    // ...

    ImageData := { ... };
  
    // ...
    
  // "Магия" компилятора из "end"
  finally
    Finalize(ImageData); // FreeAndNil - для старого компилятора
    Finalize(FileData);  // FreeAndNil - для старого компилятора
    Finalize(MyObj);     // FreeAndNil - для старого компилятора
  end;
end;

Также обратите внимание, что оба примера кода, будучи запущенными на старом компиляторе, приведут к утечке памяти, поскольку деструктор объекта выполнен не будет (ибо на старом компиляторе он должен вызываться вручную). Так что, если вы хотите написать универсальный код, который можно запускать на обоих компиляторах (с ARC и классический, без него), то вы можете:
  • Использовать {$IFDEF AUTOREFCOUNT}, разделив код на два варианта.
  • Использовать классический подход с Free/FreeAndNil, не используя преимущества ARC. На ARC этот подход формально будет работать благодаря обратной совместимости, хотя его поведение может незначительно отличаться.

По первому пункту: новый компилятор предоставляет следующие (новые) символы условной компиляции (определения для компиляторов даны по состоянию на XE4):

Символ: Условие: Компиляторы:
NEXTGEN Новый компилятор dcciosarm, dccios32
AUTOREFCOUNT Доступен ARC dcciosarm, dccios32
CPUARM Для процессоров с архитектурой ARM dcciosarm
IOS Целевая платформа - iOS dcciosarm, dccios32
WEAKREF Компилятор может использовать слабые ссылки dcciosarm, dccios32

Примечание: все символы условной компиляции можно посмотреть здесь.

По второму пункту: разумеется, разработчики Delphi не могли просто "выбросить на свалку" базилионы написанных сторонними разработчиками строк кода на Delphi, объявив их "устаревшими" и "несовместимыми с новой моделью". К примеру, если рассмотреть такой классический код:
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;  
  try
    MyObj.DoSomething;               
  finally
    FreeAndNil(MyObj); // а также MyObj.Free или MyObj.Destroy
  end;
end;
В классическом компиляторе, где объекты являются неуправляемыми типами данных, вызовы FreeAndNil, Destroy или Free безусловно удаляли существующий объект. В новых компиляторах с поддержкой ARC этот код будет работать немного иначе: вызовы FreeAndNil, Destroy и Free будут эквивалентны ":= nil" (т.е. очистке ссылки). Иными словами, блок кода выше в компиляторе с ARC будет скомпилирован как:
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;  
  try
    MyObj.DoSomething;               
  finally
    MyObj := nil;
  end;
end;
Что является 100% рабочим и корректным кодом, пусть и не самым разумным и эффективным. Иными словами, старые вызовы FreeAndNil/Free/Destroy полностью допустимы и безопасны, хотя и бесполезны в компиляторах с ARC.

Однако это не означает, что вы сможете использовать весь свой старый код без модификаций. В старом коде у вас могут быть более сложные ситуации - например, несколько ссылок на один объект. С классическим компилятором висячая ссылка (вы удалили объект по одной ссылке, но остальные ссылки не были сброшены) ваш объект удаляется, но на него продолжают указывать ссылки. Это - допустимо, если вы не обращаетесь к объекту по висячим ссылкам. Но в новой модели эти висячие ссылки добавят "+1" к счётчику ссылок объекта. Таким образом, очистка ссылки вызовом FreeAndNil/Free/Destroy уменьшит счётчик, но не до 0. Т.е. объект удалён не будет. Само собой, это не означает утечки памяти - объект всё же будет удалён, но позже - когда удалится последняя (ранее "висячая") ссылка. Так что ваш код может работать и как ранее (только изменится картина выделения/освобождения памяти), но, быть может, вам необходимо очистить объект до наступления другого события (такого, как выгрузка библиотеки, из которой объект и получен). В этом случае ваш код может вылететь. Решение заключается в правиле, которому не грех было бы следовать и ранее (ещё с классическим компилятором): не оставляйте висячих ссылок. Очищайте все ссылки на объект при его удалении.

Альтернативным решением задачи гарантированного вызова деструктора может быть вызов (нового) метода DisposeOf:
var
  MyObj1: TMySimpleClass;
  MyObj2: TMyCompleClass;
begin
  MyObj2 := TMyCompleClass.Create;

  MyObj1 := TMySimpleClass.Create;
  try  
    MyObj2.MyObj := MyObj1;
  finally
    MyObj1.DisposeOf;
  end;

  if not Assigned(MyObj2.MyObj) then
    Caption := 'No object'   
  else
  if MyObj2.MyObj.Disposed then
    Caption := 'Zombie object'
  else
    Caption := 'Normal object';

  if Assigned(MyObj2.MyObj) and
     (not MyObj2.MyObj.Disposed) then
    // что-то делаем с MyObj2.MyObj
  else
    // объект MyObj2.MyObj не доступен
end;
Метод DisposeOf безусловно вызывает деструктор - даже несмотря на существующие ссылки на объект. После такого вызова деструктора объект переходит в состояние "зомби" ("zombie state" или "disposed state") - для него был вызван деструктор, объект был очищен, но память для него ещё не была освобождена. Вы можете узнать состояние объекта через свойство Disposed - это аналог Assigned для объектов из классического компилятора.

Разница между вызовами FreeAndNil/Free и DisposeOf заключается в ваших намерениях: вызов FreeAndNil/Free отсоединяет ссылку, но не означает немедленного удаления объекта (он может быть удалён сейчас, но может быть удалён и позднее), а вызов DisposeOf всегда безусловно удаляет объект, даже если на него есть ссылки.

Примечание: "зомби"-объект никак не защищается от возможного ошибочного доступа к нему. Вы можете прочитать/записать свойство, вызывать методы (как обычные, так и виртуальные) - все эти операции будут успешными, но будут оперировать на уже очищенном объекте. И хотя это не приведёт к Access Violation, как в классическом компиляторе с висячими ссылками (потому что память под "зомби" объект всегда гарантировано выделена), но все структуры данных объекта уже были очищены деструктором, что может привести к неожиданному поведению. Всегда проверяйте статус объекта вызовом Disposed, если вы удаляете объект вручную. Кроме того, вы можете проверить доступность объекта в самих методах объекта вызовом protected-метода CheckDisposed - это некий аналог Assert(Disposed);.

Заметьте, что старый Assigned вместе с FreeAndNil больше не имеют смысла в новой архитектуре, потому что объект всегда гарантировано существует (пусть даже и как зомби), пока на него есть хоть одна ссылка - это отличается от классической модели, где вам приходилось записывать в ссылку nil, чтобы указать на уже удалённый объект. (Хотя, конечно, вы можете продолжать использовать Assigned, если вы очищаете ссылки на объекты до их выхода из области видимости.)

К счастью, вам не нужно увлекаться {$IFDEF AUTOREFCOUNT}, потому что и DisposeOf и Disposed доступны и в классических компиляторах (начиная с XE4, конечно же). Код выше будет полностью работоспособен и в Win32, где вызов DisposeOf просто вызывает Free, ну а Disposed всегда возвращает False, поскольку в классическом компиляторе нет состояния "зомби". Поэтому, если у вас есть старый код и вы хотите точно такого же поведения (т.е. удалять объект сразу, а не когда уйдёт последняя висячая ссылка на него), то вы можете просто заменить вызовы FreeAndNil/Free/Destroy на вызов DisposeOf. К несчастью, вместо двух состояний "есть объект"/"нет объекта" у вас теперь появляется три состояния: "есть объект"/"зомби"/"нет объекта" - что, впрочем, не сильно отличается от бывшего "есть объект"/"висячая ссылка - непонятно, есть объект или нет"/"нет объекта" - которое в классическом компиляторе вы должны были сводить к "есть объект"/"нет объекта". В связи с этим, вам может пригодится такая подпрограмма:
function ValidObject(const AObj: TObject): Boolean;
begin
  Result := Assigned(AObj) {$IFDEF AUTOREFCOUNT}and (not AObj.Disposed){$ENDIF};
end;
Эту функцию можно использовать во всех местах, где вы раньше использовали if Assigned(Obj) then - замените их на if ValidObject(Obj) then.

Примечание: деструктор в ARC по прежнему называется Destroy, но он заблокирован для прямого вызова (помещением в секцию protected). Поэтому:
  1. Добавьте {$IFDEF AUTOREFCOUNT}protected{$ENDIF} перед каждым destructor Destroy; override;
  2. Замените все внешние вызовы Destroy (если они вдруг у вас есть) на FreeAndNil/Free или DisposeOf - смотря по тому, согласны ли вы с отложенным удалением объекта или вам нужно немедленное удаление.

Суммируя сказанное, вот современная реализация TObject (показан только код, имеющий отношение к циклу создание-удаления объектов):
type
  TObject = class
  public
    constructor Create;
{$IFDEF AUTOREFCOUNT}
  protected
{$ENDIF}
    destructor Destroy; virtual;

  public
    procedure AfterConstruction; virtual;
    procedure BeforeDestruction; virtual;

    procedure Free;
    procedure DisposeOf; {$IFNDEF AUTOREFCOUNT} inline; {$ENDIF}

    class function InitInstance(Instance: Pointer): TObject {$IFDEF AUTOREFCOUNT} unsafe {$ENDIF};
    procedure CleanupInstance;
    class function NewInstance: TObject {$IFDEF AUTOREFCOUNT} unsafe {$ENDIF}; virtual;
    procedure FreeInstance; virtual;
{$IFDEF AUTOREFCOUNT}
    function __ObjAddRef: Integer; virtual;
    function __ObjRelease: Integer; virtual;
{$ENDIF}

  protected
    function GetDisposed: Boolean; inline;
    procedure CheckDisposed; {$IFNDEF AUTOREFCOUNT} inline; {$ENDIF}

{$IFDEF AUTOREFCOUNT}
  private const
    objDestroyingFlag = Integer($80000000);
    objDisposedFlag = Integer($40000000);
  protected
    [Volatile] FRefCount: Integer;
    class procedure __MarkDestroying(const Obj); static; inline;
    class function __SetDisposed(const Obj): Boolean; static; inline;
  public
    property RefCount: Integer read FRefCount;
{$ENDIF}

    property Disposed: Boolean read GetDisposed;
  end;

constructor TObject.Create;
begin
end;

destructor TObject.Destroy;
begin
end;

procedure TObject.AfterConstruction;
begin
end;

procedure TObject.BeforeDestruction;
begin
{$IFDEF AUTOREFCOUNT}
  if ((RefCount and objDestroyingFlag) = 0) and (RefCount = 0) then
    Error(reInvalidPtr);
{$ENDIF}
end;

procedure TObject.Free;
begin
// under ARC, this method isn't actually called since the compiler translates
// the call to be a mere nil assignment to the instance variable, which then calls _InstClear
{$IFNDEF AUTOREFCOUNT}
  if Self <> nil then
    Destroy;
{$ENDIF}
end;

procedure TObject.DisposeOf;
type
  TDestructorProc = procedure (Instance: Pointer; OuterMost: ShortInt);
begin
{$IFDEF AUTOREFCOUNT}
  if Self <> nil then
  begin
    Self.__ObjAddRef; // Ensure the instance remains alive throughout the disposal process
    try
      if __SetDisposed(Self) then
      begin
        _BeforeDestruction(Self, 1);
        TDestructorProc(PPointer(PByte(PPointer(Self)^) + vmtDestroy)^)(Self, 0);
      end;
    finally
      Self.__ObjRelease; // This will deallocate the instance if the above process cleared all other references.
    end;
  end;
{$ELSE}
  Free;
{$ENDIF}
end;

function TObject.GetDisposed: Boolean;
begin
{$IFDEF AUTOREFCOUNT}
  Result := FRefCount and objDisposedFlag <> 0;
{$ELSE}
  Result := False;
{$ENDIF}
end;

procedure TObject.CheckDisposed;
begin
{$IFDEF AUTOREFCOUNT}
  if Disposed then
    ErrorAt(Byte(reObjectDisposed), ReturnAddress);
{$ENDIF}
end;

class function TObject.NewInstance: TObject;
begin
  Result := InitInstance(_GetMem(InstanceSize));
{$IFDEF AUTOREFCOUNT}
  Result.FRefCount := 1;
{$ENDIF}
end;

procedure TObject.FreeInstance;
begin
  CleanupInstance;
  _FreeMem(Pointer(Self));
end;

class function TObject.InitInstance(Instance: Pointer): TObject;
var
  IntfTable: PInterfaceTable;
  ClassPtr: TClass;
  I: Integer;
begin
  FillChar(Instance^, InstanceSize, 0);
  PPointer(Instance)^ := Pointer(Self);
  ClassPtr := Self;
  while ClassPtr <> nil do
  begin
    IntfTable := ClassPtr.GetInterfaceTable;
    if IntfTable <> nil then
      for I := 0 to IntfTable.EntryCount-1 do
        with IntfTable.Entries[I] do
        begin
          if VTable <> nil then
            PPointer(@PByte(Instance)[IOffset])^ := VTable;
        end;
    ClassPtr := ClassPtr.ClassParent;
  end;
  Result := Instance;
end;

procedure TObject.CleanupInstance;
var
  ClassPtr: TClass;
  InitTable: Pointer;
begin
{$IFDEF WEAKREF}
  _CleanupInstance(Self);
{$ENDIF}
  ClassPtr := ClassType;
  repeat
    InitTable := PPointer(PByte(ClassPtr) + vmtInitTable)^;
    if InitTable <> nil then
      _FinalizeRecord(Self, InitTable);
    ClassPtr := ClassPtr.ClassParent;
  until ClassPtr = nil;
  TMonitor.Destroy(Self);
end;

{$IFDEF AUTOREFCOUNT}
class procedure TObject.__MarkDestroying(const Obj);
var
  LRef: Integer;
begin
  repeat
    LRef := TObject(Obj).FRefCount;
  until AtomicCmpExchange(TObject(Obj).FRefCount, LRef or objDestroyingFlag, LRef) = LRef;
end;

class function TObject.__SetDisposed(const Obj): Boolean;
var
  LRef: Integer;
begin
  repeat
    LRef := TObject(Obj).FRefCount;
  until AtomicCmpExchange(TObject(Obj).FRefCount, LRef or objDisposedFlag, LRef) = LRef;
  Result := LRef and objDisposedFlag = 0;
end;

function TObject.__ObjAddRef: Integer;
begin
  Result := AtomicIncrement(FRefCount);
end;

function TObject.__ObjRelease: Integer;
begin
  Result := AtomicDecrement(FRefCount) and not objDisposedFlag;
  if Result = 0 then
  begin
    __MarkDestroying(Self);
    if __SetDisposed(Self) then
      Destroy
    else
      FreeInstance;
  end;
end;
{$ENDIF}

function _ClassCreate(InstanceOrVMT: Pointer; Alloc: ShortInt): Pointer;
begin
  if Alloc >= 0 then
    InstanceOrVMT := Pointer(TClass(InstanceOrVMT).NewInstance);
  Result := TObject(InstanceOrVMT);
end;

procedure _ClassDestroy(const Instance: TObject);
begin
  Instance.FreeInstance;
end;

function _AfterConstruction(const Instance: TObject): TObject;
begin
  try
    Instance.AfterConstruction;
    Result := Instance;
{$IFDEF AUTOREFCOUNT}
    AtomicDecrement(Instance.FRefCount);
{$ENDIF}
  except
    _BeforeDestruction(Instance, 1);
    raise;
  end;
end;

procedure _BeforeDestruction(const Instance: TObject; OuterMost: ShortInt);
begin
  if OuterMost > 0 then
    Instance.BeforeDestruction;
end;

Слабые (weak) ссылки

Однако поддержка ARC в Delphi касается не только расширением действия счётчиков ссылок на классы/объекты, но и поддержки слабых (weak) ссылок. Слабые ссылки предназначены для решения проблемы циклических ссылок. Наиболее типичный случай возникновения циклических ссылок: контейнер-коллекция, в котором его элементы содержат ссылки на него самого (как на контейнер-владелец). В классической модели ссылок из Delphi подобная конструкция порождает утечку из-за наличия циклической ссылки.

Здесь на сцену выходят слабые ссылки. Слабая ссылка - это ссылка на объект, которая не приводит к изменению счётчика ссылок. Иными словами, при присвоении объекта в переменную со слабой ссылкой не происходит увеличение счётчика ссылок объекта на единицу. Аналогично, при очистке слабой ссылки не происходит уменьшение счётчика объекта на единицу. Создать слабую ссылку очень просто - достаточно пометить переменную атрибутом [weak], например:
type
  TMyComplexClass = class;

  TMySimpleClass = class
  private
    [weak] FOwnedBy: TMyComplexClass;
  protected
    destructor Destroy; override;
  public
    constructor Create;
    procedure DoSomething(bRaise: Boolean = False);
  end;

  TMyComplexClass = class
  private
    FSimple: TMySimpleClass;
  protected
    destructor Destroy; override;
  public
    constructor Create;
    class procedure CreateOnly;
  end;

constructor TMyComplexClass.Create;
begin
  inherited Create;
  FSimple := TMySimpleClass.Create;
  FSimple.FOwnedBy := self;
end;
В этом примере поле FOwnedBy является слабой ссылкой, потому что оно помечено атрибутом [weak]. Это означает, что присвоение этому полю не увеличивает счётчик ссылок присваевомого объекта, а его очистка - не уменьшает счётчик ссылок объекта. Таким образом, создание экземпляра TMyComplexClass не приведёт к утечке памяти, несмотря на наличие циклической ссылки - благодаря тому, что одна из ссылок в составе циклической ссылки является слабой.

Вы можете увидеть, что атрибут [weak] используется и в коде самой Delphi, например:
type
  TComponent = class(TPersistent, IInterface, IInterfaceComponentReference)
  private
    [weak] FOwner: TComponent;

Примечание: вы можете использовать атрибут [weak] и в классических компиляторах, но он будет игнорироваться, поскольку в этих компиляторах нет ARC. Таким образом, если вы пишете универсальный исходный код - вам необходимо как помечать переменные атрибутом [weak], так и использовать FreeAndNil/Free (использование которых допускается в компиляторах с ARC).

Вы также не можете проверить статус объекта по слабой ссылке. Чтобы проверить статус объекта, вам сначала нужно присвоить объект в обычную переменную, например:
  [weak] FOwner: TComponent;

...

var
  TheOwner: TComponent;      // обычная переменная
begin
  TheOwner := FOwner;        // делаем alias
  if Assigned(TheOwner) then // проверяем TheOwner вместо FOwner
    TheOwner.ClassName;      // OK
end;

Диагностика с ARC

Использование ARC упрощает работу с памятью и снижает риск утечек памяти/ресурсов в вашем коде, но поскольку всё же существует вероятность создать циклическую ссылку, то ваш код всё ещё не полностью защищён от утечек памяти.

С целью отладки вы можете использовать свойство RefCount, чтобы узнать число живых ссылок на объект. Не следует использовать это свойство для реализации логики программы. Кроме того, вы можете (крайне редко) использовать __ObjAddRef и __ObjRelease для ручного управления счётчиком ссылок - например, для записи объекта в неуправляемую переменную-указатель (к примеру, свойства типа Tag/Data). Этот приём допустимо использовать в логике кода, хотя его и нужно избегать (предпочтительнее: создание наследника с полем нужного типа).

Вы можете проверить наличие циклических ссылок с помощью функции CheckForCycles (модуль Classes):
procedure CheckForCycles(const Obj: TObject; const PostFoundCycle: TPostFoundCycleProc); overload;
procedure CheckForCycles(const Intf: IInterface; const PostFoundCycle: TPostFoundCycleProc); overload;
Как и выше, эта функция служит для диагностики, её не следует использовать в финальной сборке программы/для реализации логики самой программы. Используйте её только для отладки.

Пример использования:
var
  MyComplex: TMyComplexClass;
begin
  MyComplex := TMyComplexClass.Create;
  MyComplex.FSimple.DoSomething;
  CheckForCycles(MyComplex,
    procedure(const ClassName: string; Reference: IntPtr; const Stack: TStack<IntPtr>)
    begin
      Log('Object ' + IntToHex (Reference, 8) + ' of class ' + ClassName + ' has a cycle');
    end);
end;

Заметьте, что по аналогии со строками и интерфейсами ARC с объектами является потокобезопасным: при работе со счётчиком ссылок используются атомарные interlocked-операции. Заметьте, что это не означает автоматической потокобезопасности самих объектов.

Обратите внимание, что на мобильных платформах Delphi использует функции операционной системы в качестве штатного менеджера памяти. Иными словами, FastMM - штука чрезмерно сложна для мобильной платформы и, более того, написанная на x86-ассемблере (иными словами: непереносимая). Поэтому возможности Delphi по диагностике утечек памяти будут недоступны. Вы можете использовать инструментарий целевой ОС или использовать сторонние фильтры.

Новые классы и процедуры в RTL для кросс-платформенного кода

В целом вы должны избегать прямых платформенных вызовов (т.е. функций Windows/Mac/iOS API) и использовать, предлагаемые Embarcadero обёртки-переходники. Конечно же, вы также должны как чумы избегать ассемблера и, желательно, не использовать указатели.

Например, Embarcadero предлагает вам модуль IOUtils. Он доступен, начиная с Delphi 2010. Вы можете прочитать про него здесь. Как можно догадаться, этот модуль предоставляет вам кросс-платформенные возможности для работы с файлами. В нём есть классы TDirectory, TPath и TFile - для работы с каталогами, именами файлов и файлами соответственно.

К примеру, вы можете получить доступ к папке "Documents" на мобильном устройстве так же, как вы получаете доступ к папке Application Data в Windows:
var
  MyFileName: string;
begin
  MyFileName := TPath.GetHomePath + TPath.DirectorySeparatorChar + 'Documents' + TPath.DirectorySeparatorChar + 'TheFile.txt';
  if TFile.Exists(MyFileName) then
    ...

А вот как вы можете искать файлы: этот код считывает подпапки заданной папки, а затем считывает файлы в найденных подпапках:
var
  PathList, FilesList: TStringDynArray;
  StrPath, StrFile: string;
begin
  if TDirectory.Exists(BaseFolder) then
  begin
    ListBox1.Items.Clear;
    ListBox1.Items.Add('Searching in ' + BaseFolder);
    PathList := TDirectory.GetDirectories(BaseFolder, TSearchOption.soTopDirectoryOnly, nil);
    for StrPath in pathList do
    begin
      ListBox1.Items.Add(StrPath);
      FilesList := TDirectory.GetFiles(StrPath, '*');
      for StrFile in filesList do
        ListBox1.Items.Add ('- ' + strFile);
    end;
    ListBox1.Items.Add('Searching done in ' + BaseFolder);
  end
  else
    ListBox1.Items.Add ('No folder in ' + BaseFolder);
end;

Библиотеки и пакеты

К сожалению, одна из древнейших возможностей Delphi - использование пакетов времени выполнения (run-time packages, BPL) и, более обще, DLL - не поддерживается на платформе iOS. Пакеты и библиотеки представлены DLL на Windows, dylib на MacOS и so (shared object) на Linux. Они позволяют вам создавать модульные приложения. Но на iOS приложение не может устанавливать библиотеки - это может делать только сама Apple, а iOS приложения обязаны быть монолитными программами.

Тем не менее, компилятор Delphi умеет распознавать статические ссылки на DLL (например, на midas.dll) и внедрять их в приложение статически, а не как отдельные библиотеки.

"Плохие" конструкции

with, object, указатели и ассемблер - не рекомендуются к использованию и вполне могут исчезнуть из языка Delphi в ближайшем будущем.

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

Object (старые объекты Паскаля) устарели много лет назад. Замените их на записи (record).

Ассемблерный код уже подвергся ограничению при переходе на Win64 (нельзя внедрять ассемблерный код в середину функции), а в будущем он может быть ограничен ещё больше. Уже сейчас он не поддерживается компилятором iOS. Ассемблерный код привязан к конкретной платформе и не портируется на другую платформу. Постарайтесь не использовать его.

Прямой доступ к указателям есть не на всех платформах. Хотя даже сегодня использование указателей не поощряется (как подверженное ошибкам), но они всё ещё полностью поддерживаются всеми компиляторами Delphi. Только надо иметь в виду, что в будущем их использование может быть ограничено или вовсе отсутствовать для некоторых платформ. Уже сейчас указатели удаляются из языка в пользу ARC (к примеру, в Delphi для iOS отсутствует модуль System.Contnrs, поскольку он основан на TList с указателями). Поэтому если у вас есть выбор, использовать указатели или безопасный аналог - не используйте указатели.

К примеру, TList и TStringList являются своеобразными "швейцарскими ножами": они используются как универсальный контейнер на все случаи жизни, благодаря способности хранить произвольные ссылки (для TStringList - через свойство Objects). Но новые версии Delphi поддерживают дженерики (generics) и имеют более узкоспециализированные классы - и их использование будет предпочтительнее по двум причинам: меньше ошибок (нет приведений типов) и быстрее выполнение (может использоваться хэш-таблица).

Рассмотрим такой код с двумя идентичными списками:
sList: TStringList;
sDict: TDictionary<string, TMyObject>;
Списки заполняются случайными (но идентичными для обоих списков) значениями в цикле:
sList.AddObject(aName, anObject);
sDict.Add(aName, anObject);
Попробуем получить каждый объект в обоих списках по его имени (ключу). Оба списка содержат идентичный набор данных, а имена ключей (объектов) хранятся в отельном списке (sList):
theTotal := 0;
for I := 0 to sList.Count - 1 do
begin
  aName := sList[I];

  // Поиск объекта по имени
  anIndex := sList.IndexOf(aName);
  anObject := sList.Objects[anIndex] as TMyObject;

  Inc(theTotal, anObject.Value);
end;

theTotal := 0;
for I := 0 to sList.Count - 1 do
begin
  aName := sList[I];

  // Поиск объекта по имени
  anObject := sDict.Items[aName];

  Inc(theTotal, anObject.Value);
end;
Сколько времени займёт поиск в отсортированном списке строк (который использует двоичный поиск в случае отсортированного списка) по сравнению со словарём (который использует хэш-ключи)?
StringList: 2839 ms
Dictionary: 686 ms
Результат работы обоих вариантов кода идентичен (предполагая, что на вход поступил один и тот же набор данных), но скорость выполнения значительно отличается: TStringList работает в четыре раза медленнее словаря (пример дан для миллиона записей).

Этот пример показывает только верхушку айсберга (новых конструкций в Delphi). Конечно, он также с пользой может быть использован в классических приложениях Delphi, но наиболее ценен именно для iOS - из-за изменившейся модели памяти.

Заключение

С выходом первого компилятора Delphi для ARM, основанного на архитектуре LLVM, язык Delphi переживает важный переходный период. Хотя разработчиками Delphi были предприняты усилия для поддержания обратной совместимости, разработчикам рекомендуется использовать новые возможности языка и двигаться вперед.

Описанные в этой статье возможности языка (в частности - поддержка ARC) будут формировать будущее Delphi. Эти изменения частично обусловлены поддержкой новой платформы, а частично предназначены для исправления некоторых "плохих" мест в языке Delphi.

Суммирующая табличка по компиляторам для Delphi XE4 (компиляторы для C++ Builder не показаны, за исключением Win64):

НазваниеWin32Win64OSXЭмулятор iOS
(работает на OSX)
iOS
DelphiC++ Builder
Имя компилятораDCC32DCC64CPP64DCCOSXDCCIOS32DCCIOSARM
Архитектура компилятораClassicClassicLLVMClassicClassicLLVM
Целевая платформаx86-32x86-64x86-32x86-32ARM
Целевая ОСWindowsWindowsOSXiOSiOS
FastMM
(встроенный)
ДаДаНет
(возможен)
Нет
(вероятно, возможен)
Нет
FastMM
(FullDebugMode)
ВозможенВозможенНетНетНет
ARCНетНетНетДаДа
0-индексируемые строки
(по умолчанию)
Нет
(возможны)
Нет
(возможны)
Нет
(возможны)
ДаДа
Всего один тип строк
(...undeclared identificator AnsiString...)
НетНетНетДаДа
Символы условной компиляции
(применимо только к Delphi)
MSWINDOWS
WIN32
CPUX86
ASSEMBLER
MSWINDOWS
WIN64
CPUX64
ASSEMBLER
MACOS
MACOS32
POSIX
POSIX32
CPUX86
ALIGN_STACK
ASSEMBLER
IOS
POSIX
POSIX32
CPUX86
ASSEMBLER
NEXTGEN
AUTOREFCOUNT
WEAKREF
IOS
POSIX
POSIX32
CPUARM
NEXTGEN
AUTOREFCOUNT
WEAKREF

P.S. Пожалуйста, обратите внимание, что эта статья не носит характер "что нового в XE4". Она описывает лишь ключевые изменения в Delphi, задающие тон её дальнейшего развития. За кадром статьи осталось много нововведений XE4.

40 комментариев :

  1. Спасибо за статью - подробное и интересное описание пути, по которому движется Delphi. Забегая вперед, отмечу, что я вижу этот путь скатыванием в УГ. Единственное что - хотелось бы увидеть более глубокий анализ причин, из-за которых происходят изменения. Благо, квалификация позволяет...

    По порядку.

    1. Каждая строка нового типа будет безполезно отжирать 3-4 байта для хранения константных кодировки и длины символа? Зачем?

    2. Ansi/RawByteString-строк больше не будет - тоже фейл, кому они мешали, спрашивается? Как теперь под виндой (не все же кинутся программить под 100500 платформ, подавляющему большинству только винда и нужна) общаться с интерфейсами, которые оперируют BSTR, как вызывать ansi-функции из API/DLL? Надеюсь, им хватит ума не урезать строки во всех платформах в будущих релизах.

    3. Immutable-строки. Я так и не увидел ответа на 2 основных вопроса:
    3.1. Если иммутабельность введут - будет ли легитимным код вида:
    var
    Str : string;
    Run : PChar;
    Step: Integer;
    begin
    Str:='123';
    Run:=Pointer(Str);
    for Step:=1 to Length(Str)
    do begin
    Run^:='-';
    Inc(Run)
    end
    end;

    3.2. На чем планируется достигнуть ускорения, что послужило заигрыванием с иммутабельностью? Вроде, упоминалась конкатенация - в каком месте там профит будет при неизменяемых строках? "Тяжелую" UniqueString вызывать не надо, атомарный инкремент (если он останется в новых строках) - только 1 раз для новой строки (но, пардон, при использовании якобы быстрого TStringBuilder с ARC будут вызываться атомарные инкремент и декремент).

    4. Что касается быстрого StringBuilder. Твой код под D2010 выдает 53 мс у простого сложения и 76 - у билдера. Зная, в какую сторону меняется качество RTL в новых версиях, у меня подозрение, что в твоей Delphi вовсе не билдер ускорен)))

    ОтветитьУдалить
  2. 5. Что касается дженериков. Удобства они, безусловно, добавляют. Правда, тут 2 серьезные оговорки:
    5.1. Если отбросить глюкавость компилятора
    5.2. Удобство очень часто выходит боком, пример: пробуем заменить TStringList велосипедом из записи
    TRec = record
    Str: string;
    Obj: TObject;
    end;
    и списка этих записей TList. Отныне про элементарную операцию List[Index].Str:='123' можно забыть.

    Ну а скорость дженериков, как и всего новомодного RTL, вызывает лишь печаль. Продвинутый пример замены TStringList:
    type
    TRec = record
    Str: string;
    Obj: T;
    end;
    const
    Size = 10000000;
    Str1 = '123';
    Str2 = '456';
    var
    SList: TStringList;
    GList: TList>;
    Timer: TStopwatch;
    Step : Integer;
    Rec : TRec;
    begin
    Timer:=TStopwatch.StartNew;
    SList:=TStringList.Create;
    for Step:=0 to Size-1
    do SList.Add(Str1);
    for Step:=0 to Size-1
    do SList[Step]:=Str2;
    SList.Free;
    Timer.Stop;
    Memo2.Lines.Add('StringList: '+IntToStr(Timer.ElapsedMilliseconds));

    Timer:=TStopwatch.StartNew;
    GList:=TList>.Create;
    Rec.Str:=Str1;
    for Step:=0 to Size-1
    do GList.Add(Rec);
    for Step:=0 to Size-1
    do begin
    Rec:=GList[Step];
    Rec.Str:=Str2;
    GList[Step]:=Rec
    end;
    GList.Free;
    Timer.Stop;
    Memo2.Lines.Add('Generic list: '+IntToStr(Timer.ElapsedMilliseconds))
    end;

    Итог - 806 мс против 1898. Думаю, не надо уточнять в чью пользу. Даже если оставить тупо добавление-очистка списка (убрав тяжелое и неудобное изменение) - дженерики проиграют 776 мс против 512.
    Быстро наговнить небольшой список с ограниченным удобством - самое то. Написать производительный код - нет, спасибо.

    ОтветитьУдалить
  3. 6. Ссылки для объектов. Тут смешанные чувства, преимущественно - негативные. Задача, стоящая перед разработчиками - максимально подсластить жизнь говнокодерам, в т.ч. пришедшим из других джав и прочих дотнетов (где ручным освобождением памяти предпочитают не утруждать программиста). Мозговой штурм подсказывает _кучу_ проблем и неопределенностей, которые принесет ARC, вместо отказа от затеи героически придумываются костыли. Совет "Очищайте все ссылки на объект при его удалении в классическом компиляторе" (с намеком, что так код писать надо было с самых начал) вообще откровенно позабавил. Ну да, ведь вручную удалять объекты - это чересчур тяжело, гораздо проще занулять разошедшиеся повсюду ссылки даже когда в этом нет никакого смысла.
    Возвращаясь к способу реализации ARC - тут возможен только один вариант: атомарные инкременты/декременты для объектов на каждый чих. Это конгениально: выбросить их из строк для ускорения процесса и тут же запихнуть в объекты. Попытка сделать замену TStringList на дженериках не с помощью TList<>, а с помощью TObjectList<> (чтобы иметь возможность удобно менять поля) с атомарным подсчетом ссылок приведет к еще большим тормозам.

    7. По библиотекам непонятно: те, что установлены, вызывать можно? Если нет - как же приложение работает?)) Тот же браузер как в прогу добавить - статически линковать весь Chromium?


    Ну и как общий итог. Если ARC и новые строки введут как основу во всех компиляторах - то через пять лет можно будет вместо "Сейчас with в Delphi спроектирована не слишком удачно" уверенно говорить "Сейчас половина языка в Delphi спроектирована не слишком удачно" :(

    ОтветитьУдалить
  4. Моё ИМХО.

    1. Про байты странный вопрос. В целях обратной совместимости, конечно же.

    2. Почему убрали строки.

    Смотри, сейчас строки встроены в язык. Вообще все. Хотя реально в языке ты работаешь только со string, а остальное используешь только как переходник к внешнему миру. Теперь посмотри на мобильные платформы, которые сплошняком используют Unicode, а ANSI там вообще нет. На некоторых платформах вообще могут отсутствовать функции ОС для перекодировки в/из ANSI. Ну и как под такое делать строковые типы, встроенные в язык? Таскать с программами все возможные таблицы перекодировок? Логичнее все сторонние строки сделать обычным user-типом. Кому надо - сам подключит.

    3.1. Легитимность этого кода не зависит от immutable-строк. Легитимность этого кода зависит от указателей. Указатели и immutable-строки - разные вещи.

    3.2. Как можно было увидеть из статьи - конкатенация строк сейчас напрямую зависит от того, как менеджер памяти умудриться оптимизировать перераспределение памяти под строки. Хорошо умудрится - будет хорошая скорость. Плохо умудриться - будет плохая скорость. Вон там пример с iOS это показывает. Так вот цель будущей доработки - улучшить этот момент. Типа, реализовать часть логики TStringBuilder для конкатенации.

    4. Ты бы глянул под отладчиком, что там происходит. Ты же понимаешь, что для preallocated-буфера для TStringBuilder там будет просто серия Move-в.

    ОтветитьУдалить
  5. А провинился with? Tго нет, кажется, в других языках, но чем он архитектурно плох?

    ОтветитьУдалить
  6. 1. Да, это надо было 5 лет назад спрашивать))

    2. Если надо нуль-терминацию или вообще отдельный менеджер памяти - реализация user-типа выйдет кудрявой или не выйдет вовсе.
    Потом, однобайтовые строки в _куче_ библиотек используются в качестве буферов для бинарных данных. Используются неспроста, получая все возможные плюшки: ARC, copy-on-write, куча встроенных функций (элементарный PosEx для TBytes где взять?), возможность удобного задания констант, совместимость между собой (смешно ведь - вся куча: TBytes, TByteDynArray, TArray, array of byte, array [x..y] of Byte с точки зрения компилятора между собой несовместима, скопировать напрямую не получится)

    3.1. Представим, что указатели пока еще легитимны. Строки уже иммутабельны. И тут хитрый я делаю их вполне себе мутабельными через "черный ход". Какие последствия? Если никаких - почему запретили изменение через Str[]?

    3.2. Вопрос вот в чем: "На чем планируется достигнуть ускорения, что послужило заигрыванием с иммутабельностью?". Ты не в курсе?))

    4. Нет смысла спускаться так глубоко - в отладчик. Понимание "как надо сделать" иногда расходится с тем, что реально сделано. Когда я смотрю на самый примитивный метод:

    function TStringBuilder.Append(const Value: string): TStringBuilder;
    begin
    Length := Length + System.Length(Value);
    Move(PChar(Value)^, FData[Length - System.Length(Value)], System.Length(Value) * SizeOf(Char));
    Result := self;
    end;

    мне становится смешно. Это ведь самый простой кусок, я даже не смотрел, как сделано работа с буфером. Но когда в итоге узкоспециализированное решение (StringBuilder) проигрываем общему (менеджер памяти) 50% - уже не до смеха. Сравнив, к примеру, стандартный билдер с решением от Synopse - понимаешь уровень нового RTL. И эти люди проповедуют, что в их реализации иммутабельные строки принесут какую-то скорость.

    зы. там еще и другие вопросы были :)

    ОтветитьУдалить
  7. 2 Анонимный:

    >>> А провинился with? Tго нет, кажется, в других языках, но чем он архитектурно плох?

    В предложении про with, вообще-то, была ссылка на объяснение.

    2 fd00ch:

    >>> Потом, однобайтовые строки в _куче_ библиотек используются в качестве буферов для бинарных данных.

    А потом авторы этих библиотек задают вопросы вида "почему мой код сломался в Delphi 2009".

    3.1. Перефразирую тебя: "Если я могу взять через указатель private-поле класса, то зачем ввели private?"

    3.2. По-моему, тут кто-то что-то не понимает. Я уже два раза ответил на этот вопрос разными словами.

    4. Я не зря тебя в отладчик тыкаю. В приведённом тобой коде НЕТ тормозов.

    (Да, можно было сделать микрооптимизацию вида сохранения длины в промежуточную переменную (экономим аж одно вычитание). Но погоды оно не делает.)

    >>> там еще и другие вопросы были :)

    Там скорее философские рассуждения.

    7. Библиотеки в iOS может устанавливать только Apple. Если тебе нужна библиотека, то ты можешь только статически её включить в себя.

    ОтветитьУдалить
  8. 2. Речь про либы, работающими под Delphi 2009 и позже

    3. При таком изменении private-поля я понимаю, что могу нарушить логику класса. И, потом, разработчики не заявляли, что private-поля каким-то чудесным способом работают быстрее, чем public

    С иммутабельными стрингами все наоборот:
    3.1. я не понимаю - нарушу ли я что-то, если буду таким образом менять строку?
    3.2. на чем _именно_ будет выигрыш в скорости, что послужило поводом "заигрывать" с иммутабельностью?

    Ты пока ответил что-то типа "когда-нибудь на какой-нибудь платформе StringBuilder будет сцеплять стринги быстрее, чем оператор сложения". Это все, безусловно, прекрасно, но каким боком именно _иммутабельные_ стринги повысят скорость _конкатенации_? Там же тупо считывание Length и последующий Move в бОльший буфер, счетчик ссылок не проверяется и не изменяется, отдельные символы через конструкцию Str[] - тоже.

    ОтветитьУдалить
  9. 2. Пример дашь?

    3. Т.е. при таком коде ты логику не нарушишь?

    S := 'A';
    H := S;
    P := Pointer(S);
    P^ := 'B';
    Assert(H = 'A');

    3.1. Я, может, не очень понял, что ты подразумеваешь под "легитимный". Компилироваться он будет. Но если на строку есть более одной ссылки, то изменением строки через указатель ты "сломаешь" все её копии. Это и сейчас так, так в будущем будет ровно так же.

    3.2. А, теперь понял про что ты. В тексте статьи не сказано, что из иммутабельности строк следует ускорение конкатенации. Это два независимых явления. Они перечислены вместе, т.к. влияют на скорость выполнения. Я не знаю, почему ты связал их вместе, если в статье дан конкретный пример на скорость конкатенации, которая никак не связана с immutable-строками, а зависит от менеджера памяти программы.

    Ещё раз:
    а). Разработчики Delphi предполагают, что некоторые платформы могут вводить ограничения на изменения строк, либо такие изменения могут быть неэффективны. Цитирую white paper от Марко Канту: "The research the R&D team in currently doing in this direction is looking for optimizations to common operations". Поэтому уже сегодня есть Warning, чтобы отследить потенциальные места в коде. Я много раз в тексте выделял жирным слова, чтобы это подчеркнуть. Строки в Delphi НЕ являются immutable.

    б). Ускорение конкатенации в будущем будет достигаться не за счёт immutable-строк, а за счёт специализированного решения - как и было показано в статье.

    в). Я думаю, что immutable-строки могут быть предпочтительнее изменяемых по следующим причинам (дальше - чисто моё ИМХО, сильно я не вникал):
    - Возможно, что immutable-строки могут быть более потокобезопасны, чем простые строки - за счёт своей неизменности. Я слышал, как кто-то ругался на реализацию работы со строками в Delphi в контексте многопоточного программирования. Может быть, здесь можно что-то улучшить...
    - Теоретически возможна оптимизация экономии памяти. Например, создаём новую строку. Если она уже равна какой-то ранее созданной - делаем ссылку на старую, а не создаём новую (типа, "глобальное хранилище строк"). Вероятно, это могло бы быть полезно на устройствах с малым количеством памяти.
    - Возможно, здесь есть поле для оптимизации компилятора, но мы этого не видим, т.к. не являемся разработчиками Delphi.

    ОтветитьУдалить
  10. Любопытно, а как будет разруливаться ситуация с циклическими ссылками в массивах?
    FComponents: array of TComponent;

    ОтветитьУдалить
  11. >> типа, "глобальное хранилище строк"
    Я об этом в первую очередь и подумал, когда читал статью. В своей практике я делал такую штуку, когда надо было сэкономить на памяти - все строки создавались через некоторую хэш-таблицу - чуть медленнее, чем просто выделение памяти, но зато на большом объёме данных (порядка 5 млн. записей из БД) удалось сократить расход памяти более чем в три раза.

    ОтветитьУдалить
  12. Отлично! Как же я рад за Delphi, молодцы парни из Embarcadero!
    Скорей бы эти прелести пришли на x86.

    ОтветитьУдалить
  13. >>> Любопытно, а как будет разруливаться ситуация с циклическими ссылками в массивах?

    А где там циклические ссылки?

    ОтветитьУдалить
  14. Меня, безусловно, радует такое количество нововведений. Не вижу ничего плохого в with, это довольно удобная конструкция, которую я по инерции часто искал в других языках, например, C# и PHP. Спасибо за перевод!

    ОтветитьУдалить
  15. Это не строго перевод, это "материал на основе". Здесь кое-что добавлено от меня, а кое-что я не рассказывал вообще (в основном - по теме "что нового в XE4", не относящееся к фундаментальным изменениям). Порядок подачи информации тоже отличается. В целом я говорил своими словами, перевода здесь - только несколько абзацев.

    Так что если вы ищите именно перевод white paper от Марко Канту, то это не он. Пока перевода нет, можно читать только в оригинале.

    ОтветитьУдалить
  16. >>>>> Любопытно, а как будет разруливаться ситуация с циклическими ссылками в массивах?
    >>А где там циклические ссылки?
    Имелось ввиду как делать массив слабых ссылок?
    TMySimpleClass = class
    private
    [weak] FOwnedBy: TMyComplexClass;
    // скорее всего здесь будет массив со
    // списком всех "детей"
    [weak] FChildrens: array of TMySimpleClass;
    Как я понимаю такое объявление не правильное?

    ОтветитьУдалить
  17. Я вот с либами не могу вкурить. Например, я юзаю библиотеку libpq для доступа к Постгресу. Там ясен пень все функции объявлены с параметрами const char *varname. Я их импортирую как varname: PAnsiChar. Внимание три вопроса:
    1. Как мне теперь их определять? TBytes, PByte или еще как?
    2. А если параметр тупо указатель? А их собираются убить.
    3. Как в принципе мне протащить либу на IOS? или куда там еще жизнь пожелает...

    ОтветитьУдалить
  18. Про атомарные (т.е. с локом) изменения счетчиков ссылок при работе с объектами не такой уж праздный вопрос. Интерфейсные ссылки значительно медленнее работали, чем классовые. Теперь любые медленные?

    Ну и несовместимость между win32 компилятором и IOS это сложно понять. Как же один код под множество платформ?

    ОтветитьУдалить
  19. [weak]... OMG! Осталось только "+=" прикрутить и по специальной директиве begin/end на фигурные скобки заменить. Зачем нам еще один шарп?

    ОтветитьУдалить
  20. Наконец-то можно будет обойтись без кучи try/finally блоков
    function TdmData.AddUser(AUser, APwd: string; IsAdmin: Boolean = False; CanSeeAll: Boolean = False): Integer;
    var
    PwdHash: string;
    md5: TIdHashMessageDigest5;
    CanCommit: Boolean;
    begin
    md5 := TIdHashMessageDigest5.Create;
    Result := 0;
    try
    TSStart(CanCommit);
    try
    PwdHash := md5.AsHex(md5.HashValue(APwd));
    spAddUser.ParamByName('l').AsString := (AUser);
    spAddUser.ParamByName('p').AsString := (PwdHash);
    spAddUser.ParamByName('a').AsInteger := FBool(IsAdmin);
    spAddUser.ParamByName('c').AsInteger := FBool(CanSeeAll);
    spAddUser.ExecProc;
    TSCommit(CanCommit);
    Result := spAddUser.ParamByName('USERSID').AsInteger;
    except
    on E: Exception do
    begin // тут осмыслененное сообщение для пользователя
    dmData.TSRollBack(CanCommit);
    raise;
    end;
    end;
    finally
    md5.Free;
    end;
    end;

    ОтветитьУдалить
    Ответы
    1. Да, только теперь в ЛЮБОЙ подпрограмме, использующей объекты, будет неявная try-finally!

      Удалить
    2. Ну, try-finally не стоит вообще ничего на x86-64, и крайне мало (несколько push/pop и копирование регистра) - на x86-32.

      Удалить
  21. 2. Ни одна из _сторонних_ либ для работы с бинарными данными, что я использую, не юзает array of byte (или любой другой псевдоним) в качестве единственного способа обмена. Большинство используют Raw/Ansi-стринги (по понятными всем причинам, которые похерятся при переходе на TBytes). Еще бывают нетипизированные параметры вроде Read(const Buffer)

    Конкретные примеры: Synapse для скачивания данных, ZLibEx для (де)компрессии, DEC для (де)шифровки

    3.1. Единственная причина, которую я могу принять как разумную для ввода иммутабельных строк - железные ограничения какой-либо платформы на in-place изменение строк (или действительно сильный оверхед для этого действия). Но ведь приведенный код выше делает то самое изменение - что, он перестанет работать, выкидывая AV, или приведет к мегатормозам? Разумеется, явный вызов UniqueString там будет при необходимости.

    3.2.
    > Возможно, что immutable-строки могут быть более потокобезопасны, чем простые строки - за счёт своей неизменности
    Я, раскидывая мозгами, не смог прийти к выводу, что иммутабельность сможет избавить строки от атомарного счетчика ссылок - это единственная причина тормозов _стрингов_ в многопоточном программировании (есть еще тормоза от плохо масштабируемого FastMM)

    > Теоретически возможна оптимизация экономии памяти
    Т.е. экономия памяти за счет процессора. В период, когда на всех платформах памяти уже хватает, а вот процессоры такими же темпами в росте производительности не могут похвастать

    > Возможно, здесь есть поле для оптимизации компилятора
    Конечно, оно там есть (и в RTL еще), но оптимизациями никто чето не занимается и без иммутабельных стрингов

    ОтветитьУдалить
  22. На форуме Embarcadero пользователи Delphi активно обсуждают immutable-строки, пока никто не смог понять, для чего это нужно, кроме как "так делают в функциональных языках и это модно".

    forums.embarcadero.com/thread.jspa?threadID=87171

    ОтветитьУдалить
  23. Ну и несовместимость между win32 компилятором и IOS это сложно понять. Как же один код под множество платформ?

    Все просто - мобильные платформы отдельно, стационарные отдельно. Один код для стационарных платформ - поддержка Windows, Mac OS и т.д., один код для мобильных платформ - поддержка iOS, Android и т.д.

    ОтветитьУдалить
  24. В плане увеличения производительности ИМХО очень странные оптимизации.
    Современные процессорные архитектуры очень плохо (медленно) работают со ссылочными типами данных. В силу того что блоки предсказания не знают что он них потребуется дальше и не подгружают в кеш нужную информацию для обработки заранее. Зато они прекрасно предсказывают массивы и последовательности данных...
    Как перевод на ссылочные рельсы ускорит работу на процессорах которые этого не любят непонятно...

    ОтветитьУдалить
  25. Мне не нравятся все нововведения делфи, потому что они сводятся к "наговнять по быстренькому". Это и множество багов, из-за которых новыми возможностями пользоваться мягко говоря невозможно :) И качество реализации того нового функционала, который нам предлагают использовать.

    Ввели RTTI для всего. Замечательно, начинаем использовать. Внезапно для интерфейса:
    {$M+}
    IMyNode = interface
    function GetNode(index: Integer): IMyNode;
    end;
    {$M-}
    в RTTI видим, что это у нас процедура, а возвращаемый тип nil о_О. Бага с д2010, в xe3 не фиксед. Слава богу, хоть в xe4 они её фиксед( http://qc.embarcadero.com/wc/qcmain.aspx?d=108551 ), ну или по крайней мере говорят что фиксед.

    Или вот еще на вскидку в том же RTTI (ага далеко ходить не надо)
    {$M+}
    IMyNode = interface
    procedure DoWork;
    procedure DoSome(var Data);
    end;
    {$M-}
    Внезапно мы обнаружим, что у данного интерфейса вообще методов нету, и класть компилятор хотел на M+ ^_^. Все из-за параметра var Data, стоит хоть в один метод включить var что-то, и вся метаинформация пропадает для этого интерфейса. Не знаю пофиксили в xe4 или нет.
    И там еще over100500 проблем было, которые тянутся достаточно долго уже.

    Пойдем к дженерикам?
    Всякие синтаксические подводные камни, типа круглых скобок для коструктора без параметра: http://qc.embarcadero.com/wc/qcmain.aspx?d=115437
    Нерабочие SizeOf-ы на этапе компиляции: http://qc.embarcadero.com/wc/qcmain.aspx?d=103277
    Не обошлось и без глупых ограничений в плане синтаксиса:
    Отсутсвие возможности "гуидизации" дженериковских интерфейсов. Т.е. я описал:
    IMyIntf = interface
    ['{B9BA889F-A12F-43EE-9EE0-2217A16BE842}']
    end;
    И далее если я в классе имплементирую:
    TMyObj = class (TInterfacedObject, IMyIntf, IMyIntf)
    то оно даст мне скомпилировать, но при работе с IMyIntf очень вероятно всего рухнет, т.к. оно вернет мне IMyIntf, а реализацию будет юзать от IMyIntf. В xe3 не пофикшено, и фиксить надо, расширяя синтаксис, т.е. добавить гуидизацию в списки к дженерикам.

    Нельзя использовать типизированные указатели для дженерик типов:
    TMyStruct = record
    Param: T;
    end;
    PMyStruct = ^TMyStruct;
    не даст скомпилировать :) Это кстати ставит крест на микроменеджменте памяти при аллокациях. А когда вообще откажутся от указателей - мне что, менеджер памяти (например пул одинаковых объектов) писать на другом языке программирования? :D

    FireMonkey? Тут вообще говорить нечего. Снаружи все красиво, внутри никаких абсолютно оптимизаций, всюду for i := 0 to Count - 1 do... Самая великая оптимизация пожалуй, это если больше 20 (почему 20?) invlalidate rect-ов - объединить их в 1, и перерисовать всю область :D
    Просто берем отсюда пример: http://blogs.embarcadero.com/yaroslavbrovin/2012/10/11/listboxitem_styling/
    ставим в нем 1000 итемов в листбокс, скролим этот TListBox и наслаждаемся тормозами. Чтобы в FireMonkey все бегало быстро - надо выкинуть целиком ту реализацию что сейчас, и написать заново :) Ну а чтобы они поняли, насколько гавно ихняя реализация - надо чтобы они IDE написали на ней :) А то FMX продвигаем, пользуйтесь, а IDE под мак толковой до сих пор нет.

    В общем мне абсолютно не нравится, что вместо качественного продукта мы получаем какой-то написанный по быстрому на коленке кусок кода. И все новые и новые модные фичи которыми нельзя пользоваться. В общем чисто маркетинговый ход.

    Вот взять те же неизменяемые строки. Сейчас FastMM дает отличные скорости, и в их неизменности смысла нет. Да, FastMM сильно завязан на x86 архитектуру, и на ОС в которой он работает. Дык напишите FastMM для iOS. Пусть даже с нуля. Почему им проще запретить посимвольное редактирование строки, а оптимизировать только конкатенацию? Кроме как проще (а значит быстрее) реализовать - на ум не приходит ничего.

    ОтветитьУдалить
  26. p.s. Кстати, очень хочется чтобы на вашем блоге было нормальное поле для ввода комментария. А то sizegrip у поля то есть, но выходит вот такая красота: http://screenup.org/51948847b325d
    Приходится набирать комментарии в блокноте, а потом копипастить в эту щель для ввода комментария.

    у < div class="comment-form"> убрать из стилей max-width: 425px; (он в http://www.blogger.com/static/v1/widgets/1832531788-widget_css_bundle.css)

    внутри < div class="comment-form"> лежит < iframe>, у которого жестко задан height="216px", увеличить до разумных размеров, хотя бы 400px, а можно и все 600. Скорее всего высота задается яваскриптами, не смотрел.

    И того, если все это сделать, то выглядеть это будет вот так: http://screenup.org/5194899b86eb2

    ОтветитьУдалить
  27. К сожалению, я не смог изменить высоту блока комментариев, но добавил ширины.

    ОтветитьУдалить
  28. Класс, сегодня напоролся при попытке откомпилировать некоторый код в XE3:
    E2382 Cannot call constructors using instance variables
    Оказывается теперь нельзя вызывать конструктор как метод. Более того, я не смог найти директивы, как это отключить, и никакой инфы, почему они так сделали, увы.

    ОтветитьУдалить
  29. уф, осилил :) У вас замечательные и, порой, просто огромные по объему записи.

    Кажется Embarcadero осознавая быстрый рост мобильных платформ решила уже сложившийся язык поддерживать на дополнительных платформах в угоду меньшим растратам на поддержку, а в результате получается чертовщина. Такое чувство что они в лепешку разобьются, весь язык перелопатят чтобы занять хотя бы процент на мобильном рынке. Эти их нововведения выглядят странно на desktop-ах.

    MrShoor, с другой стороны эти нововведения делают Delphi похожим на Java и C#. Если проникнуться панике Embarcadero (а они, на мой взгляд, трезво боятся потерять как desktop так и мобильные платформы), то они идут по своему пути правильно, только вот.. не качественно совсем. тикеты не закрываются старые, зато новых с каждой версией можно описать) Все не в угоду старому родному Delphi.

    ОтветитьУдалить
  30. Конечно, указатели и асм - это плохо. Даешь DVM (Delphi Virtual Machine)! Только так обойдем Java и C# ! И чо уж там, выкиньте goto до кучи...

    По делу:
    - ассемблер сами же используют в исходниках
    - руки прочь от AnsiString (да, типов многовато, но тут же где-то был перевод, зачем нужны PChar и прочие, плюс в исходниках этих PChar'ов куча). AnsiChar соответственно тоже.
    - индексация строк с нуля - ну хз, вроде не смертельно, переучусь, хотя у меня и самодельные массивы чаще всего с 1
    - неизменяемые строки - нет уж, спасибо, повидал этого говнеца в яве, больше не тянет
    - указатели deprecated - совсем сдурели что-ли??? Если кто-то не умеет пользоваться, то есть повод научиться или решить, что они не нужны лично тебе, но никак не выбрасывать из языка. Вообще, был бы стнадарт на язык, по типу C, наверно, не допустили бы такого маразма. Есть ведь еще FreePascal, Lazarus как-никак, им как быть? Делать как в делфе, делать по-старому, делать по-своему? Ну и опять же, в исходниках их тонны.
    - with - ну хрен с ним, сделайте deprecated, но не убирайте (кому-то нравится, где-то действительно тупо короче получается, хотя сам стараюсь не использовать). Вот кстати что странно - goto живет в языке десятилетиями, хотя, по идее, совсе-совсем не рекомендуется, однако ж. В это же время with - ай-яй-яй, бида-бида.
    - object - хз что это, видимо не застал уже, может и можно выкинуть.
    И еще, кому нахрен сдалась эта iOS, запилите под Android наконец!

    ОтветитьУдалить
  31. Ассемблер и указатели это прямо не комильфо... :( Нужны по настоящему редко, но метко... Допуститм есть реализация siphash, что с ней делать в новых версиях? Использовать чисто паскалёвый вариант и молиться, что бы компилятор сам векторизировал?

    ОтветитьУдалить
  32. Delphi не поддерживает ASM-вставки для ARM процессоров. Мне кажется, что это ограничение - скорее, следствие "лени" (не нужно делать компилятор ассемблера ARM).

    Но это не означает, что ассемблер нельзя использовать вообще. Если прям так сильно нужен ассемблерный код (для той же мега-оптимизации), то нужно вынести его в отдельный файл, собрать каким нибудь бесплатным ассемблером, получить объектник (.a для ARM - аналог .obj) и подключить его через external. Именно так в Delphi подключается поддержка JPEG и ZIP (ZLib).

    ОтветитьУдалить
  33. Замечу, что ARM-дизассеблер, равно как и CPU-отладчик для ARM в Delphi всё же есть.

    ОтветитьУдалить
  34. Вот я так и не понял, что они задумали с указателями. Останется ли Pointer, как тип, останутся ли GetMem/FreeMem и т.д. Побайтовый доступ к памяти объектов, возможность создавать обычные структуры данных, типа списков, деревьев и т.д. Спросил на форуме - молчат.

    ОтветитьУдалить
  35. Я так думаю, что будет зависеть от платформы. Держаться за Pointer будут до последнего. Но если на платформе его не будет, то не будет и в Delphi.

    ОтветитьУдалить
  36. Доброго времени суток.

    Подскажите пожалуйста:
    Что надежнее в работе Delphi 6 или 7?
    Какие отличительные преимущества одного над другим?

    Искал в сети информацию, мнения везде разные.
    Или подскажите статью об этом вопросе, подкрепленную не предположениями, а проверенными фактами.
    Но интересует именно Ваше мнение.

    ОтветитьУдалить
  37. > Object (старые объекты Паскаля) устарели много лет назад. Замените их на записи (record).
    Из чего сделан такой вывод? Есть ли какие-то первоисточники по данному вопросу?

    ОтветитьУдалить
  38. >>> Из чего сделан такой вывод? Есть ли какие-то первоисточники по данному вопросу?

    1. object появился в Паскале для реализации ООП. Для реализации ООП в Delphi появился class. Если для одной вещи появляется что-то новое, перекрывающее старые возможности, то старые возможности становятся deprecated.
    2. record, которые обрастают возможностями, аналогичными object.
    3. Документация: "Object types are supported for backward compatibility only. Their use is not recommended on Win32".
    4. Отчёты в QC о багах в object будут закрыты c "Won't do: Object types are deprecated and should no longer be used".

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.