Эволюция 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 довольно давно), например:

1

2

3

4

5

6

7

8

9

10

11

12

13

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 строками. А вот вариант с явным преобразованием:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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). При желании вы можете даже эмулировать поведение старого компилятора путём введения типов с перегрузкой операторов, например:

1

2

3

4

5

6

7

8

9

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). Используя такую запись, вы можете продолжать использовать старый код вида:

1

2

3

4

5

6

7

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[число]. Посмотрите на такой код:

1

2

3

4

5

6

7

8

9

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-индексированную строку", вы передаёте "просто строку". Это означает, что вы можете смешивать в одном проекте и модули функции, скомпилированные с разными настройками. Посмотрите на такой код:

1

2

3

4

5

6

7

8

9

10

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}, соответственно. Например:

1

2

3

4

const

  thirdChar = Low(string) + 2; // = 2 или 3, но всегда обозначает третий символ в строке

...

  s1[thirdChar] := 'A';

Этот код будет работать всегда одинаково, вне зависимости от настроек компилятора. А вот как вы можете работать с циклами:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

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:

1

2

3

4

5

6

7

8

9

10

11

12

13

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 и многие другие. Поэтому теперь вы можете написать:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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), он не пользуется популярностью. Почему? Посмотрите на такой код:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

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) и практически никогда не задумывались об управлении памяти для них. Точно так же вы теперь можете поступать и с объектами:

1

2

3

4

5

6

7

8

9

var

  MyObj: TMySimpleClass;

begin

  // создание объекта, счётчик ссылок увеличивается с 0 до 1

  MyObj := TMySimpleClass.Create; 

  // использование объекта, счётчик ссылок = 1

  MyObj.DoSomething;              

  // ссылка на объект уходит из зоны видимости, уменьшение ссылки с 1 до 0, уничтожение объекта

end;

Ближайший аналог ARC для объектов - это работа с интерфейсами (interface) в Delphi. Если вы когда-либо работали с интерфейсами в Delphi, то теперь точно так же сможете работать и с обычными объектами.

Точно так же, как с интерфейсами (и любыми другими типами с автоматическим управлением памятью в Delphi), вы можете удалить ссылку преждевременно (до выхода переменной за область видимости) путём присвоения переменной значения nil:

1

2

3

4

5

6

7

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, эквивалентно такому коду (который, впрочем, вы и сами могли писать ранее вручную):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

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, объявив их "устаревшими" и "несовместимыми с новой моделью". К примеру, если рассмотреть такой классический код:

1

2

3

4

5

6

7

8

9

10

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 будет скомпилирован как:

1

2

3

4

5

6

7

8

9

10

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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

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. К несчастью, вместо двух состояний "есть объект"/"нет объекта" у вас теперь появляется три состояния: "есть объект"/"зомби"/"нет объекта" - что, впрочем, не сильно отличается от бывшего "есть объект"/"висячая ссылка - непонятно, есть объект или нет"/"нет объекта" - которое в классическом компиляторе вы должны были сводить к "есть объект"/"нет объекта". В связи с этим, вам может пригодится такая подпрограмма:

1

2

3

4

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 (показан только код, имеющий отношение к циклу создание-удаления объектов):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

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], например:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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, например:

1

2

3

4

type

  TComponent = class(TPersistent, IInterface, IInterfaceComponentReference)

  private

    [weak] FOwner: TComponent;


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

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

1

2

3

4

5

6

7

8

9

10

11

  [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):

1

2

procedure CheckForCycles(const Obj: TObject; const PostFoundCycle: TPostFoundCycleProc); overload;

procedure CheckForCycles(const Intf: IInterface; const PostFoundCycle: TPostFoundCycleProc); overload;

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

Пример использования:

1

2

3

4

5

6

7

8

9

10

11

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:

1

2

3

4

5

6

var

  MyFileName: string;

begin

  MyFileName := TPath.GetHomePath + TPath.DirectorySeparatorChar + 'Documents' + TPath.DirectorySeparatorChar + 'TheFile.txt';

  if TFile.Exists(MyFileName) then

    ...


А вот как вы можете искать файлы: этот код считывает подпапки заданной папки, а затем считывает файлы в найденных подпапках:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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) и имеют более узкоспециализированные классы - и их использование будет предпочтительнее по двум причинам: меньше ошибок (нет приведений типов) и быстрее выполнение (может использоваться хэш-таблица).

Рассмотрим такой код с двумя идентичными списками:

1

2

sList: TStringList;

sDict: TDictionary<string, TMyObject>;

Списки заполняются случайными (но идентичными для обоих списков) значениями в цикле:

1

2

sList.AddObject(aName, anObject);

sDict.Add(aName, anObject);

Попробуем получить каждый объект в обоих списках по его имени (ключу). Оба списка содержат идентичный набор данных, а имена ключей (объектов) хранятся в отельном списке (sList):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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):
 

Название Win32 Win64 OSX Эмулятор iOS
(работает на OSX)
iOS
Delphi C++ Builder
Имя компилятора DCC32 DCC64 CPP64 DCCOSX DCCIOS32 DCCIOSARM
Архитектура компилятора Classic Classic LLVM Classic Classic LLVM
Целевая платформа x86-32 x86-64 x86-32 x86-32 ARM
Целевая ОС Windows Windows OSX iOS iOS
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.

Тэги кроссплатформенность , Статья , Delphi

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

  1. fd00ch2 мая 2013 г., 23:52

    Спасибо за статью - подробное и интересное описание пути, по которому движется 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. fd00ch2 мая 2013 г., 23:53

    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. fd00ch2 мая 2013 г., 23:54

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

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


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

    Ответить
  4. Александр Алексеев3 мая 2013 г., 0:08

    Моё ИМХО.

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

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

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

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

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

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

    Ответить
  5. Анонимный3 мая 2013 г., 1:53

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

    Ответить
  6. fd00ch3 мая 2013 г., 19:37

    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. Александр Алексеев4 мая 2013 г., 13:20

    2 Анонимный:

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

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

    2 fd00ch:

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

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

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

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

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

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

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

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

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

    Ответить
  8. fd00ch4 мая 2013 г., 15:57

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

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

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

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

    Ответить
  9. Александр Алексеев5 мая 2013 г., 16:33

    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. Анонимный5 мая 2013 г., 21:22

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

    Ответить
  11. Николай Зверев5 мая 2013 г., 21:59

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

    Ответить
  12. Coriolis6 мая 2013 г., 10:48

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

    Ответить
  13. Александр Алексеев6 мая 2013 г., 16:28

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

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

    Ответить
  14. xPhoenix7 мая 2013 г., 16:45

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

    Ответить
  15. Александр Алексеев7 мая 2013 г., 16:57

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

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

    Ответить
  16. Анонимный9 мая 2013 г., 0:40

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

    Ответить
  17. pgolub9 мая 2013 г., 0:45

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

    Ответить
  18. ARGB10 мая 2013 г., 1:38

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

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

    Ответить
  19. Анонимный10 мая 2013 г., 18:42

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

    Ответить
  20. Vladimir Srednikh12 мая 2013 г., 16:26

    Наконец-то можно будет обойтись без кучи 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;

    Ответить
  21. fd00ch13 мая 2013 г., 1:18

    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. Chaa13 мая 2013 г., 8:20

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

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

    Ответить
  23. Анонимный14 мая 2013 г., 17:05

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

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

    Ответить
  24. Анонимный15 мая 2013 г., 16:33

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

    Ответить
  25. MrShoor16 мая 2013 г., 13:10

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

    Ввели 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. MrShoor16 мая 2013 г., 13:11

    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. Александр Алексеев16 мая 2013 г., 14:26

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

    Ответить
  28. MrShoor27 мая 2013 г., 22:56

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

    Ответить
  29. ...so why?2 сентября 2013 г., 11:29

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

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

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

    Ответить
  30. Анонимный24 сентября 2013 г., 10:35

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

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

    Ответить
  31. pda26 ноября 2013 г., 20:51

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

    Ответить
  32. Александр Алексеев27 ноября 2013 г., 11:31

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

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

    Ответить
  33. Александр Алексеев27 ноября 2013 г., 11:31

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

    Ответить
  34. pda28 ноября 2013 г., 16:39

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

    Ответить
  35. Александр Алексеев28 ноября 2013 г., 18:05

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

    Ответить
  36. Анонимный7 ноября 2014 г., 10:03

    Доброго времени суток.

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

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

    Ответить
  37. Hol Man13 февраля 2015 г., 11:53

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

    Ответить
  38. Александр Алексеев13 февраля 2015 г., 18:32

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

    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".








ip: 46.161.14.99 Дата и время: 2018.11.18 20:30:56 Имя - eyCqVJY5WPMB Комментарий:
Begun, the great internet edoictaun has.