Object Pascal для программистов Michalis Kamburelis · множество...

86
Краткое введение в современный Object Pascal для программистов Michalis Kamburelis Содержание 1. Введение: Для чего это нужно? ......................................................................... 2 2. Основы ................................................................................................................. 4 2.1. Программа "Hello world!" ........................................................................... 4 2.2. Функции, процедуры, простейшие типы .................................................. 5 2.3. Условные операторы (if) ........................................................................... 7 2.4. Логические операторы, операторы отношений и побитовые (поразрядные) операторы ................................................................................ 8 2.5. Проверка одного выражения на множественные значения (оператор case) ................................................................................................................. 10 2.6. Перечисляемый и порядковый типы, наборы и массивы постоянной длины ............................................................................................................... 11 2.7. Циклы (for, while, repeat, for .. in) ............................................................ 13 2.8. Вывод информации и логов ................................................................... 16 2.9. Преобразование данных в строчный тип .............................................. 17 3. Модули (Unit-ы) .................................................................................................. 19 3.1. Перекрёстные ссылки между unit-ами ................................................... 20 3.2. Определение идентификаторов с указанием имени unit-а ................... 21 3.3. Передача идентификаторов одного unit-а через другой ....................... 24 4. Классы ................................................................................................................ 26 4.1. Основы ..................................................................................................... 26 4.2. Наследование (Inheritance), проверка (is), и приведение типов (as) ... 26 4.3. Свойства ................................................................................................... 29 4.4. Исключения .............................................................................................. 33 4.5. Уровни видимости ................................................................................... 34 4.6. Предок по умолчанию ............................................................................. 35 5. Освобождение памяти классов ........................................................................ 35 5.1. Всегда необходимо освобождать экземпляры класса .......................... 35 5.2. Каким образом освобождать память ..................................................... 35 5.3. Ручное и автоматическое освобождение памяти ................................. 36 5.4. Free notification ......................................................................................... 40 1

Transcript of Object Pascal для программистов Michalis Kamburelis · множество...

Page 1: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современный

Object Pascal для программистов

Michalis Kamburelis

Содержание

1. Введение: Для чего это нужно? ......................................................................... 22. Основы ................................................................................................................. 4

2.1. Программа "Hello world!" ........................................................................... 42.2. Функции, процедуры, простейшие типы .................................................. 52.3. Условные операторы (if) ........................................................................... 72.4. Логические операторы, операторы отношений и побитовые(поразрядные) операторы ................................................................................ 82.5. Проверка одного выражения на множественные значения (операторcase) ................................................................................................................. 102.6. Перечисляемый и порядковый типы, наборы и массивы постояннойдлины ............................................................................................................... 112.7. Циклы (for, while, repeat, for .. in) ............................................................ 132.8. Вывод информации и логов ................................................................... 162.9. Преобразование данных в строчный тип .............................................. 17

3. Модули (Unit-ы) .................................................................................................. 193.1. Перекрёстные ссылки между unit-ами ................................................... 203.2. Определение идентификаторов с указанием имени unit-а ................... 213.3. Передача идентификаторов одного unit-а через другой ....................... 24

4. Классы ................................................................................................................ 264.1. Основы ..................................................................................................... 264.2. Наследование (Inheritance), проверка (is), и приведение типов (as) ... 264.3. Свойства ................................................................................................... 294.4. Исключения .............................................................................................. 334.5. Уровни видимости ................................................................................... 344.6. Предок по умолчанию ............................................................................. 35

5. Освобождение памяти классов ........................................................................ 355.1. Всегда необходимо освобождать экземпляры класса .......................... 355.2. Каким образом освобождать память ..................................................... 355.3. Ручное и автоматическое освобождение памяти ................................. 365.4. Free notification ......................................................................................... 40

1

Page 2: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

6. Run-time library ................................................................................................... 436.1. Ввод/вывод с помощью потоков ............................................................. 436.2. Списки ...................................................................................................... 446.3. Клонирование классов: TPersistent.Assign ............................................ 45

7. Различные полезные возможности языка ....................................................... 477.1. Местные (вложенные) процедуры .......................................................... 477.2. Колбэки#—#они же события, они же указатели на функции, они жепроцедурные переменные ............................................................................. 487.3. Generic-и ................................................................................................... 517.4. Overload .................................................................................................... 537.5. Предобработка кода ................................................................................ 547.6. Record ....................................................................................................... 577.7. Устаревшие object ................................................................................... 597.8. Pointer-ы ................................................................................................... 597.9. Перегрузка операторов ........................................................................... 60

8. Дополнительные возможности классов ........................................................... 638.1. Различие private и strict private ............................................................... 638.2. Дополнительные возможности объявления классов и локальныеклассы .............................................................................................................. 648.3. Class method ............................................................................................ 658.4. Ссылки на класс ...................................................................................... 668.5. Class helper .............................................................................................. 688.6. Виртуальные constructor-ы, destructor-ы ................................................ 708.7. Ошибки при исполнении constructor-а ................................................... 71

9. Интерфейсы ....................................................................................................... 729.1. Хорошие (CORBA) интерфейсы ............................................................. 729.2. CORBA и COM интерфейсы ................................................................... 749.3. GUID-ы интерфейсов .............................................................................. 769.4. Некрасивые (COM) интерфейсы ............................................................ 779.5. Использование COM интерфейсов с отключённым reference-counting ............................................................................................................ 809.6. Приведение типов интерфейсов ............................................................ 82

10. Про этот документ ........................................................................................... 86

1. Введение: Для чего это нужно?

В мире существует множество книг о Паскале, однако, многие из них говорят об"устаревшем" Паскале, без классов, модулей, generic-ов (дженериков) и многихдругих современных методов и приёмов программирования.

2

Page 3: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

С целью заполнения этой бреши и было написано это краткое введение отом, что часто называется современным объектным Паскалем. Чаще егоназывают просто "Паскаль" или "Наш Паскаль". Впрочем, представляя язык,важно подчеркнуть, что это современный, объектно-ориентированный язык,который был существенно усовершенствован по сравнению со старым (Turbo)Pascal, который когда-то давно изучали в школах и институтах. Сегодня еговполне можно сравнивать с такими известными языками программирования, какC++, Java или C#.

• Паскаль имеет все современные особенности, которые можно ожидать-- классы, модульную структуру, интерфейсы, generic-и…

• Паскаль компилируется в быстрый машинный код.

• Паскаль является типобезопасным языком, что в некоторых случаяхсущественно упрощает отладку.

• В основном Паскаль - высокоуровневый язызк, однако он имеет возможностьиспользовать низкоуровневые подходы если это необходимо.

Он так же имеет превосходный кроссплатформенный портативный компилятор соткрытым исходным кодом: Free Pascal Compiler (http://freepascal.org/). Для негосуществует несколько IDE (включающих в себя редактор, отладчик, библиотекикомпонентов, дизайнер форм), одна из наиболее известных называется Lazarus(http://lazarus.freepascal.org/). Автора данной книги является создателем CastleGame Engine (https://castle-engine.io/) - который является мощным портативнымдвух- и трёхмерным игровым движком, использующим этот язык, для созданияигр под многие платформы: Windows, Linux, MacOSX, Android, iOS, web плагины.

Данное краткое введение в первую очередь ориентировано на программистов,которые уже имеют некоторый опыт в других языках программирования. Здесьне будет раскрываться значение некоторых универсальных концепций, таких как"что такое класс", лишь будет показано как они реализуются в современномПаскале.

Прим.перев. Здесь и далее мы будем использовать оригинальные английскиепонятия такие как unit, constructor, override, reference-counting в случаях, еслиони являются ключевыми словами языка либо не имеют общепринятыхпереводов на русский язык. Также часто предпочитается использоватьоригинальное английское слово его транслитерации, как в случае с понятием"generic" выше.

3

Page 4: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

2. Основы

2.1. Программа "Hello world!"

По доброй традиции знакомство с языком начинают с самой базовой и простойпрограммы "Hello world". На Паскале она выглядит следующим образом:

{$mode objfpc}{$H+}{$J-} // Эту строку необходимо использовать во всех современных программах

program MyProgram; // Сохраните этот файл под названием myprogram.lprbegin WriteLn('Hello world!');end.

Это — полноценная программа, которую можно скомпилировать и запустить.

• Это можно сделать с помощью командной строки FPC, просто создав новыйфайл myprogram.lpr с кодом программы внутри и выполнив команду fpcmyprogram.lpr .

• Если используется Lazarus, то необходимо создать новый проект (в строкеменю: Project → New Project → Simple Program). Сохраните его как myprogramи скопируйте в него этот исходный код. Компиляция выполняется используяпункт Run → Compile в меню.

• Данная программа является консольной, то есть, в обоих случаяхскомпилированный исполняемый файл нужно будет запустить из терминалакомандной строки, чтобы увидеть результат.

Остальная часть этой книги рассказывает о самом объектном Паскале, поэтомуне ожидайте чего-либо большего, чем консольных приложений. Если хочетсявзглянуть на что-либо более "крутое", можно просто создать новый GUIпроект в Lazarus (Project → New Project → Application). Вуаля! — рабочеекроссплатформенное GUI приложение, с нативным видом, использующееудобные визуальные библиотеки. Lazarus и Free Pascal Compiler имеютмножество готовых компонент для сетей, GUI, баз данных, чтения и записиразличных форматов файлов (XML, json, изображения…), управления потокамии всем, что только может понадобиться программисту. Ярким тому примеромявляется Castle Game Engine, о котором упоминалось ранее :)

4

Page 5: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

2.2. Функции, процедуры, простейшие типы

{$mode objfpc}{$H+}{$J-}

program MyProgram;

procedure MyProcedure(const A: Integer);begin WriteLn('A + 10 составляет: ', A + 10);end;

function MyFunction(const S: string): string;begin Result := S + 'строки управляются автоматически';end;

var X: Single;begin WriteLn(MyFunction('примечание: ')); MyProcedure(5);

// деление с помощью оператора "/" всегда даёт результат с плавающей запятой // для целочисленного деления необходимо использовать "div" X := 15 / 5; WriteLn('X составляет: ', X); // отобразить в научном формате вида 3.0000000E+000 WriteLn('X составляет: ', X:1:2); // отобразить 2 знака после запятойend.

Чтобы задать возвращаемое значение функции, необходимо присвоить какое-либо значение "волшебной" переменной Result в процессе выполненияфункции. Её можно свободно читать и устанавливать новое значение так жепросто как и любую локальную переменную.

function MyFunction(const S: string): string;begin Result := S + ' Добавим что-нибудь'; Result := Result + ' и ещё что-нибудь!'; Result := Result + ' И ещё немножко!';end;

5

Page 6: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Можно рассматривать имя функции ( MyFunction в примере выше) какпеременную, которую можно использовать как самую обычную переменную. Нолично я бы не советовал так делать, поскольку это выглядит не очень "прозрачно",особенно в случае, если это значение используется с правой стороны выражения.Лучше всегда использовать Result , в случае, если необходимо использоватьили устанавливать возвращаемое значение функции.

Естественно, при необходимости можно вызывать функцию рекурсивно. Нопри этом в случае, если вызывается беспараметрическая функция, следует незабывать указать пустые скобки () (которые в обычных случаях в Паскалеможно опускать) - иначе будет просто выполнен доступ к результату текущейфункции. Например:

function ReadIntegersUntilZero: string;var I: Integer;begin Readln(I); Result := IntToStr(I); if I <> 0 then Result := Result + ' ' + ReadIntegersUntilZero();end;

Функция Exit служит для завершения выполнения процедуры или функциидо того, как она достигнет завершающего end; . Если Exit вызватьбез параметров, она вернёт последнее значение, присвоенное Result .Так же можно использовать конструкцию Exit(X) , чтобы установитьрезультат функции и завершить её исполнение немедленно — это эквивалентноконструкции return X в С-подобных языках.

function AddName(const ExistingNames, NewName: string): string;begin if ExistingNames = '' then Exit(NewName); Result := ExistingNames + ', ' + NewName;end;

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

6

Page 7: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

побочные операции (например, изменяет глобальные переменные), а не простовычисляет некоторый результат. Например:

var Count: Integer; MyCount: Integer;

function CountMe: Integer;begin Inc(Count); Result := Count;end;

begin Count := 10; CountMe; // результат функции будет отброшен, однако функция выполняется, Count станет равен 11. MyCount := CountMe; // запоминаем результат выполнения функции будет, Count теперь 12.end.

2.3. Условные операторы (if)

Конструкции if .. then или if .. then .. else запускают определённыйкод, когда выполняется указанное условие. В отличии от C-подобных языков, вПаскале нет строгого требования ставить условие в скобки.

var A: Integer; B: boolean;begin if A > 0 then DoSomething;

if A > 0 then begin DoSomething; AndDoSomethingMore; end;

if A > 10 then DoSomething else

7

Page 8: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

DoSomethingElse;

// идентично предыдущему примеру B := A > 10; if B then DoSomething else DoSomethingElse;end;

Оператор else всегда относится только к последнему условию if . Поэтомуможно вполне рассчитывать на однозначность выполнения такого кода:

if A <> 0 then if B <> 0 then AIsNonzeroAndBToo else AIsNonzeroButBIsZero;

Впрочем, заключение вложенного if внутри блока begin … end; являетсялучшим вариантом, чем предыдущий пример, поскольку он более читабельный,даже если нарушены отступы или большой объём кода и комментариевзатрудняет понимание. Таким образом, всегда очевидно к какому if относитсяданный else - относительно A или относительно B - и, соответственно, кудаменьше шансов допустить ошибку при написании кода или при его правках.

if A <> 0 thenbegin if B <> 0 then AIsNonzeroAndBToo else AIsNonzeroButBIsZero;end;

2.4. Логические операторы, операторы отношений и

побитовые (поразрядные) операторы

Логические операторы представлены в Паскале операторами and , or , not ,xor . Их значение в большинстве случаев очевидно для людей, знакомых скомпьютерной грамотой. Разве что, за исключением оператора xor , которыйв русской литературе обычно называется "исключающее или". Эти операторы

8

Page 9: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

принимают булевские аргументы (boolean), и возвращаемое значение имеет тотже тип boolean. Они также могут работать как побитовые операторы, в случае,если оба аргумента целого типа (integer, byte или подобные), в этом случае онитакже возвращают значение идентичного целого типа.

Операторы отношения (сравнения) - представлены следующими комбинациямисимволов: = , <> , > , < , <= , >= , значение которых вполне очевидно. Следуетотметить, что в отличии от синтаксиса С-подобных языков, в Паскале операторсравнения выглядит как один знак "равно" A = B (в отличии от С, гдеиспользуется код A == B ). Специальным оператором присваивания в Паскалеявляется := .

Логический (или побитовый) оператор имеет более высокий приоритет, чемоператоры отношения. Поэтому, может понадобиться использовать круглыескобки вокруг сравниваемых выражений для указания правильного порядкавыполнения операторов.

Следующий пример вызовет ошибку компиляции:

var A, B: Integer;begin if A = 0 and B <> 0 then ... // так делать НЕЛЬЗЯ

Ошибка связана с тем, что компилятор в первую очередь пытается выполнитьпобитовый оператор and в середине выражения. В результате получается (0and B) - побитная операция, возвращающая целочисленную величину. Далеекомпилятор выполняет оператор "равно" и получает булевскую величину A = (0and B) . И в конце концов появляется ошибка type mismatch, в результате попыткисравнения булевской величины A = (0 and B) с целочисленной величиной 0 .

Правильно записывать это условие в следующем виде:

var A, B: Integer;begin if (A = 0) and (B <> 0) then ...

Паскаль использует "короткую оценку (short-circuit evaluation)" - мощнуюоптимизацию, позволяющую не вычислять выражение целиком, если какая-либоего часть полностью определяет результат. Рассмотрим пример:

9

Page 10: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

if MyFunction(X) and MyOtherFunction(Y) then...

• Значение функции MyFunction(X) всегда рассчитывается первым.

• И если MyFunction(X) вернёт значение false , это означает, что мы ужезнаем результат всего выражения - какое бы ни было второе значение, привыполнении false and что-нибудь мы всегда получим false . Такимобразом MyOtherFunction(Y) вообще не будет выполняться.

• Идентичная ситуация и с выражением or . В данном случае, если мы наперёдзнаем, что результат будет true потому что первый аргумент имеет значениеtrue , второй аргумент не влияет на результат и не будет рассчитываться.

• Это особенно полезно если нужно записать выражение типа:

if (A <> nil) and A.IsValid then...

В данном случае не возникнет ошибки даже в случае если Aимеет значение nil . Ключевое слово nil это указатель, который вчисленном представлении указывает на "нулевой" адрес. Во многих языкахпрограммирования он называется нулевым указателем (null pointer).

2.5. Проверка одного выражения на множественные

значения (оператор case)

Если в зависимости от разных значений определённого выражения должныбыть выполнены разные действия, тогда может оказаться удобной конструкцияcase .. of .. end .

case SomeValue of 0: DoSomething; 1: DoSomethingElse; 2: begin IfItsTwoThenDoThis; AndAlsoDoThis; end; 3..10: DoSomethingInCaseItsInThisRange; 11, 21, 31: AndDoSomethingForTheseSpecialValues; else DoSomethingInCaseOfUnexpectedValue;end;

10

Page 11: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Условие else опционально (и соответствует default в C-подобных языках).В случае, если текущее значение анализируемого выражения не совпадает ни содним из описанных случаев и нет условия else , то программа просто пропуститвсю конструкцию case и будет выполняться далее.

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

2.6. Перечисляемый и порядковый типы, наборы и

массивы постоянной длины

Перечисляемый тип (enumerated) в Паскале является очень удобным ипрозрачным. Возможно, Вам он понравится и Вы будете использовать его чащечем перечисляемые типы в других языках :)

type TAnimalKind = (akDuck, akCat, akDog);

Общепринято, что префикс перечисляемого типа состоит из двух буквсокращения имени типа, следовательно ak = сокращение для "Animal Kind".Это полезное соглашение, так как имена перечисляемых типов находятсяв глобальном пространстве переменных unit-а. Таким образом, с помощьюпрефикса ak автоматически уменьшаются шансы на случайный конфликт сдругими идентификаторами.

Конфликты в именах не приводят к неработоспособностипрограммы. Вполне допустимо в различных unit-ах определятьодинаковые идентификаторы. Однако, желательно избегатьподобных конфликтов везде, где это возможно, чтобы код былпрост для понимания и анализа.

Можно избежать попадания имён перечисляемых типов вглобальное пространство с помощью директивы компилятора{$scopedenums on} . В таком варианте будет необходимообращаться к ним через имя типа следующим образом:

11

Page 12: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

TAnimalKind.akDuck . В результате отпадает необходимостьв префиксе ak , и можно просто оставить исходные названияDuck, Cat, Dog . Такое исполнение подобно тому, какработают перечисляемые списки в C#.

Прозрачность перечисляемого типа означает, что он не совместим напрямуюс целочисленными величинами. Тем не менее, если такая совместимостьнеобходима, можно использовать Ord(MyAnimalKind) , чтобы вручнуюпривести список к целочисленному типу. Обратная операция будет выглядетькак приведение типа TAnimalKind(MyInteger) и превратит целое числов соответствующий перечисляемый тип. В последнем случае необходимотакже быть уверенным, что MyInteger является частью диапазона от 0 доOrd(High(TAnimalKind))) .

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

type TArrayOfTenStrings = array [0..9] of string; TArrayOfTenStrings1Based = array [1..10] of string;

TMyNumber = 0..9; TAlsoArrayOfTenStrings = array [TMyNumber] of string;

TAnimalKind = (akDuck, akCat, akDog); TAnimalNames = array [TAnimalKind] of string;

Они также могут использоваться для создания наборов (они же set-ы, они жевнутренние битовые поля):

type TAnimalKind = (akDuck, akCat, akDog); TAnimals = set of TAnimalKind;var A: TAnimals;begin A := []; A := [akDuck, akCat]; A := A + [akDog]; A := A * [akCat, akDog]; Include(A, akDuck); Exclude(A, akDuck);

12

Page 13: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

end;

2.7. Циклы (for, while, repeat, for .. in)

{$mode objfpc}{$H+}{$J-}{$R+} // включаем проверку на диапазон величин, очень полезно для отладкиvar MyArray: array [0..9] of Integer; I: Integer;begin // инициализация for I := 0 to 9 do MyArray[I] := I * I;

// отображение for I := 0 to 9 do WriteLn('Квадрат составляет ', MyArray[I]);

// делает то же самое, что и предыдущий вариант for I := Low(MyArray) to High(MyArray) do WriteLn('Квадрат составляет ', MyArray[I]);

// делает то же самое I := 0; while I < 10 do begin WriteLn('Квадрат составляет ', MyArray[I]); I := I + 1; // это идентично "I += 1" или "Inc(I)" end;

// делает то же самое I := 0; repeat WriteLn('Квадрат составляет ', MyArray[I]); Inc(I); until I = 10;

// делает то же самое // обратите внимание, тут переменная I перечисляет значения элементов массива, а не его индексы for I in MyArray do WriteLn('Квадрат составляет ', I);end.

13

Page 14: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Примечания:

• Может показаться, что различие между циклами while и repeat лишь"косметические" с единственным отличием, что условие записано "с точностьюдо наоборот": в случае while .. do выполнение продолжается, покаусловие истинно, а при repeat .. until - выполнение прекращается,когда условие истинно. Впрочем, есть ещё одно важное отличие: в случаеrepeat , условие проверяется не в начале, а в конце цикла. Поэтомусодержимое цикла repeat всегда выполняется как минимум один раз.

• Конструкция for I := .. to .. do … похожа на C-подобный цикл for . Темне менее, она более ограничена, поскольку невозможно указать произвольноедействие/условие чтобы контролировать итерации цикла. В Паскале forиспользуется строго для итерации через последовательные числа (илидругие порядковые типы). Единственной уступкой является возможностьиспользования downto вместо to , чтобы производить счёт в обратномпорядке.

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

Следует также обратить внимание, что переменная, которая использоваласьдля цикла (в примере выше - I ) становится неопределённой после окончанияцикла кроме случая досрочного выхода из цикла с помощью команд Breakили Exit . Цикл for I in .. do .. такой же как конструкция foreach вмногих других языках и хорошо понимает организацию всех встроенных типов:

• Он может перебирать все значения массива (см. пример выше).

• Он может перебирать все возможные значения перечисляемого типа:

var AK: TAnimalKind;begin for AK in TAnimalKind do...

◦ Он может перебирать все элементы набора:

var Animals: TAnimals; AK: TAnimalKind;begin

14

Page 15: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Animals := [akDog, akCat]; for AK in Animals do ...

◦ И так же работает на всех пользовательских типах, включая generic-и,например, TObjectList или TFPGObjectList .

{$mode objfpc}{$H+}{$J-}uses SysUtils, FGL;

type TMyClass = class I, Square: Integer; end; TMyClassList = specialize TFPGObjectList<TMyClass>;

var List: TMyClassList; C: TMyClass; I: Integer;begin List := TMyClassList.Create(true); // значение true означает, что List владеет всеми дочерними объектами try for I := 0 to 9 do begin C := TMyClass.Create; C.I := I; C.Square := I * I; List.Add(C); end;

for C in List do WriteLn('Квадрат ', C.I, ' составляет ', C.Square); finally FreeAndNil(List); end;end.

Мы ещё не рассматривали концепцию классов, поэтому последний примерможет показаться неочевидным. Но мы обязательно рассмотрим этотвопрос чуть позже :)

15

Page 16: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

2.8. Вывод информации и логов

Для простого вывода строки в Паскале используется процедура Write илиWriteLn . Во втором случае в конце автоматически добавляется символпереноса строки.

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

WriteLn('Hello world!');WriteLn('Можно вывести целое число: ', 3 * 4);WriteLn('Отформатировать его: ', 666:10);WriteLn('А также вывести число с плавающей запятой: ', Pi:1:4);

Чтобы явным образом добавить перенос строки можно использовать константуLineEnding (из библиотеки FPC RTL). (Castle Game Engine имеет также болеекраткий вариант NL ). В отличии от HTML и других подобных синтаксисовразметок в Паскале обратная косая ( \ ) не позволяет вставлять специальныесимволы в строке, например:

WriteLn('Первая строка.\nВторая стока.'); // НЕВЕРНЫЙ пример

не вставит перенос строки, а просто выдаст все символы этой строки в однойстрочке. Правильно делать следующим образом:

WriteLn('Первая строка.' + LineEnding + 'Вторая строка.');

или так:

WriteLn('Первая строка.');WriteLn('Вторая строка.');

Стоит отметить, что функции Write / WriteLn будут работать только вконсольных приложениях. Для этого необходимо указывать {$apptypeCONSOLE} (но не {$apptype GUI} ) в главном файле программы. На некоторых

16

Page 17: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

ОС консоль явно или скрыто присутствует всегда (Unix) и эта директива неиспользуется. А в некоторых системах попытка выполнения Write / WriteLn изGUI приложения приведёт к ошибке (например, в Windows).

В Castle Game Engine не советуется использовать WriteLn , поскольку дляэтого есть специальная функция WriteLnLog или WriteLnWarning длявывода логов и отладочной информации. Их результат всегда будет направленв полезном направлении: для Unix-подобных систем это будет стандартныйвывод в консоль. Для Windows GUI приложений это будет лог-файл. В Androidвывод будет направлен в Android logging facility (инструмент логов Андроида),который можно просматривать с помощью команды adb logcat . ИспользоватьWriteLn есть смысл лишь в ограниченном наборе случаев, например, дляконсольных приложений (исполняемых из командной строки) в которых можнобыть точно уверенным, что стандартный вывод определён. Например, такможно делать в конвертере или генераторе трёхмерных моделей, которыйпредназначен для запуска из командной строки.

2.9. Преобразование данных в строчный тип

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

• Некоторые конкретные типы можно преобразовать в строку используяспециальные функции, такие как IntToStr и FloatToStr . В дальнейшемсуммировать (concatenation) строки в Паскале можно просто используя знаксложения. Таким образом можно создавать составные строки: 'Моё целоечисло ' + IntToStr(MyInt) + ', а значение числа пи составляет' + FloatToStr(Pi) .

◦ Преимущество: это очень удобно. Существует множество готовых функцийтипа XxxToStr и им подобных (например, FormatFloat ), для множестваразличных типов данных.

◦ Второе преимущество: они почти всегда имеют обратную функцию. Чтобыпреобразовать строку (например, введённую пользователем) в целое илидробное число можно использовать StrToInt , StrToFloat и подобные(например, StrToIntDef ).

◦ Недостаток: длинная сумма множества XxxToStr и строк выглядитнекрасиво.

17

Page 18: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

• Функция Format используется в виде Format('%d %f %s', [MyInt,MyFloat, MyString]) . Она подобна функции sprintf в C-подобныхязыках. Она вставляет аргументы в соответствующие placeholder-ы согласнозаданному образцу. Эти placeholder-ы могут использовать специальныйсинтаксис, влияющий на форматирование, например %.4f это дробныйформат с 4 знаками после запятой.

◦ Преимущество: отделение строки от аргументов выглядит чисто икрасиво. Легко изменить текст строки, не изменяя аргументов, например,если необходимо выполнить перевод.

◦ Второе преимущество: не используются "волшебные" свойствакомпилятора. Можно использовать идентичный синтаксис дляпередачи произвольного количества аргументов произвольного типав пользовательских процедурах (для этого определите принимаемыйпараметр как array of const ). Затем можно передать эти аргументыфункции Format , или разобрать на список параметров и делать с ним всё,что угодно.

◦ Недостаток: компилятор не проверяет, соответствует ли строка-образецаргументам. Используя неверный тип placeholder-а приведёт к ошибкеEConvertError , впрочем, гораздо более понятной, чем segmentation fault(наиболее часто SIGSEGV).

• В Паскале также существует функция WriteStr(TargetString, …) вомногом подобна базовой функции Write(…) , с одним отличием - результатсохраняется в TargetString .

◦ Преимущество: эта функция имеет все возможности функции Write , втом числе и специальный "волшебный" синтаксис для форматирования, какнапример Pi:1:4 .

◦ Недостаток: такой специальный синтаксис для форматирования является"волшебным", т.е. он написан специально для конкретной процедуры. Частоэто приводит к проблемам, например, невозможно создать свою функциюMyStringFormatter(…) которая бы принимала такой синтаксис, какPi:1:4 . Именно по этому, а также из-за того, что эта функция долгое времяне была доступна в основных компиляторах, такая конструкция не оченьпопулярна.

18

Page 19: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

3. Модули (Unit-ы)

Unit-ы позволяют группировать общие функции и объекты (любые элементыязыка, которые могут быть объявлены), для использования другими unit-ами ипрограммами. Они эквивалентны модулям и пакетам в других языках. Они имеютсекцию interface , где объявляются доступные для других unit-ов и программпеременные, функции и т.п., и секцию implementation , где описано, как ониработают. Unit MyUnit можно сохранить под именем myunit.pas (названиедолжно состоять из строчных букв с расширением .pas ).

{$mode objfpc}{$H+}{$J-}unit MyUnit;interface

procedure MyProcedure(const A: Integer);function MyFunction(const S: string): string;

implementation

procedure MyProcedure(const A: Integer);begin WriteLn('A + 10 составляет: ', A + 10);end;

function MyFunction(const S: string): string;begin Result := S + 'строки управляются автоматически';end;

end.

Файл основной программы чаще всего сохраняется в виде файлов типаmyprogram.lpr ( lpr = Lazarus program file; в Delphi используются .dpr ).Следует отметить, что возможны и другие расширения, например, некоторыепроекты используют расширение .pas для основного файла программы. Дляunit-ов изредка используются расширение .pp . Лично же я предпочитаюиспользовать стандартные .pas для unit-ов и .lpr для FPC/Lazarus программ.

Программа может подключать unit с помощью ключевого слова uses :

{$mode objfpc}{$H+}{$J-}

19

Page 20: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

program MyProgram;

uses MyUnit;

begin WriteLn(MyFunction('Примечание: ')); MyProcedure(5);end.

Unit может также содержать секции initialization и finalization . В нихразмещается код, который выполняется при запуске и завершении выполненияпрограммы, соответственно.

{$mode objfpc}{$H+}{$J-}unit initialization_finalization;interface

implementation

initialization WriteLn('Hello world!');finalization WriteLn('Goodbye world!');end.

3.1. Перекрёстные ссылки между unit-ами

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

{$mode objfpc}{$H+}{$J-}unit AnotherUnit;interface

uses Classes;

{ Тип класса "TComponent" определён в unit-е Classes.

20

Page 21: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

По этому необходимо использовать uses Classes, как видно выше. }procedure DoSomethingWithComponent(var C: TComponent);

implementation

uses SysUtils;

procedure DoSomethingWithComponent(var C: TComponent);begin { Процедура FreeAndNil определена в unit-е SysUtils. Поскольку мы лишь ссылаемся на её имя в разделе implementation, вполне допустимо использовать SysUtils в секции "implementation". } FreeAndNil(C);end;

end.

Запрещено применять кольцевую взаимозависимость (cyclic reference) вразделе interface. Т.е. два unit-а не могут использовать друг друга в разделеinterface. Причина такого ограничения заключается в том, что для того,чтобы "понять" секцию interface unit-а, компилятор анализирует и "понимает"все unit-ы, перечисленные в uses в секции interface. В Паскале этоправило придерживается строго, что позволяет достичь высокой скоростикомпиляции и полностью автоматическое определение компилятором чтоименно необходимо перекомпилировать. В Паскале нет необходимостисоздания сложных Makefile для выполнения простой задачи компиляции, атакже нет нужды перекомпилировать всё лишь для того, чтобы удостовериться,что все зависимости правильно обновились.

Вполне возможно создавать кольцевые зависимости между unit-ами когда одиниз них "используется" только в implementation. Поэтому нормально для Aиспользовать B в interface, и затем unit B использует A в implementation.

3.2. Определение идентификаторов с указанием имени

unit-а

Различные unit-ы могут определять одинаковые идентификаторы. Чтобыподдерживать код простым для чтения и правки, обычно следует избегать такихсовпадений, но не всегда это возможно. В таких случаях, последний unit в спискеuses "перетягивает одеяло на себя", т.е. идентификаторы определённые в нёмскрывают одноимённые идентификаторы введённые другими unit-ами ранее.

21

Page 22: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Однако, возможно недвусмысленно определить unit предоставляющийидентификатор, с помощью конструкции MyUnit.MyIdentifier . Этостандартное решение ситуации, когда используемый идентификатор из MyUnitскрыт идентификатором из другого unit-а. Для достижения этой же цели можнопросто перестроить порядок unit-ов в списке uses, однако это повлияет на всеидентификаторы, что не всегда желательно.

{$mode objfpc}{$H+}{$J-}program showcolor;

// unit-ы Graphics и GoogleMapsEngine определяют свои типы, которые называются одинаково - TColor.uses Graphics, GoogleMapsEngine;

var { Это сработает не так, как ожидается, поскольку TColor определяется последним unit-ом в списке - GoogleMapsEngine. } // Color: TColor; { А так будет правильно. } Color: Graphics.TColor;begin Color := clYellow; WriteLn(Red(Color), ' ', Green(Color), ' ', Blue(Color));end.

В случае unit-ов следует также помнить, что они могут иметь два списка uses :один - в секции interface , другой - в implementation . Основное правилов этом случае звучит как: "позднейшие unit-ы скрывают все что было доэтого" и применяется последовательно, что в свою очередь означает, что unit-ы использованные в секции implementation могут скрывать идентификаторы изunit-ов использованных в секции interface. Кроме того, не стоит забывать, что впроцессе обработки секции interface данного unit-а компилятором, влияют лишьunit-ы использованные в секции interface. Это может сбить с толку в ситуациях,когда два на вид одинаковых объявления обрабатываются компилятором по-разному:

{$mode objfpc}{$H+}{$J-}unit UnitUsingColors;

// НЕВЕРНЫЙ пример

interface

22

Page 23: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

uses Graphics;

procedure ShowColor(const Color: TColor);

implementation

uses GoogleMapsEngine;

procedure ShowColor(const Color: TColor);begin // WriteLn(ColorToString(Color));end;

end.

Unit Graphics (из набора библиотек Lazarus LCL) определяет тип TColor .Но компилятор указывает на ошибку в этом unit-е, указывая на то,что заявленная в секции Interface процедура ShowColor не описана.Проблема в том, что unit GoogleMapsEngine также определяет тип TColor ,который используется только в секции implementation , следовательноон перекрывает определение TColor в секции implementation . Т.е.компилятор видит это буквально как:

{$mode objfpc}{$H+}{$J-}unit UnitUsingColors;

// НЕВЕРНЫЙ пример// демонстрирующий, как предыдущий пример "видит" компилятор

interface

uses Graphics;

procedure ShowColor(const Color: Graphics.TColor);

implementation

uses GoogleMapsEngine;

procedure ShowColor(const Color: GoogleMapsEngine.TColor);begin // WriteLn(ColorToString(Color));end;

23

Page 24: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

end.

В данном случае решение тривиальное: необходимо просто изменитьimplementation , чтобы явно использовать TColor из unit-а Graphics . Этотакже можно исправить, переместив GoogleMapsEngine в секцию interface доunit-а Graphics . Впрочем, это может привести к другим последствиям внутриunit-а UnitUsingColors , так как коснётся всех его определений.

{$mode objfpc}{$H+}{$J-}unit UnitUsingColors;

interface

uses Graphics;

procedure ShowColor(const Color: TColor);

implementation

uses GoogleMapsEngine;

procedure ShowColor(const Color: Graphics.TColor);begin // WriteLn(ColorToString(Color));end;

end.

3.3. Передача идентификаторов одного unit-а через

другой

Иногда возникает необходимость взять идентификатор из одного unit-а ипередать его через другой unit. Т.е. в результате использование нового unit-адолжно сделать доступным старый идентификатор в пространстве имён.

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

Это может быть осуществлено с помощью повторного объявленияидентификатора в новом unit-е.

24

Page 25: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

{$mode objfpc}{$H+}{$J-}unit MyUnit;

interface

uses Graphics;

type { Используем TColor из unit-а Graphics для определения TMyColor. } TMyColor = TColor;

{ Как вариант, можно переопределить его под тем же именем. В таком варианте необходимо будет явно указать наименование unit-а, иначе получится несогласованное определение "TColor = TColor". } TColor = Graphics.TColor;

const { С константами это тоже работает. } clYellow = Graphics.clYellow; clBlue = Graphics.clBlue;

implementation

end.

Стоит отметить, что данный трюк не пройдёт с глобальными процедурами,функциями и переменными. В таком случае возникнет необходимость объявитьпостоянный указатель на процедуру в другом unit-е (см. Раздел 7.2, «Колбэки#—#они же события, они же указатели на функции, они же процедурныепеременные»), но такой код выглядит не совсем чисто.

Более оптимальным решением является создание тривиальной "функции-обёртки", которая под видом простого вызова функции из внешнего unit-а, простопередаёт ему параметры и возвращает принимаемые значения обратно.

Чтобы проделать то же с глобальными параметрами иногда используютсяглобальные (на уровне unit-а) свойства, см. Раздел 4.3, «Свойства».

25

Page 26: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

4. Классы

4.1. Основы

В Паскале для объектно-ориентированного программирования чаще всегоиспользуются классы (classes). На базовом уровне класс просто является"контейнером", который может вмещать в себя:

• поля (field) (иными словами "переменная внутри класса"),

• методы (method) (иными словами "процедура или функция внутри класса"),

• свойства (property) (удобный синтаксис для конструкции подобной полю,однако в действительности являющейся парой методов, используемых длячтения (getter) и записи (setter) чего-либо; детальнее см. ???).

• Вообще говоря, в классах можно разместить очень много различных вещей,см. Раздел 8.2, «Дополнительные возможности объявления классов илокальные классы», но об этом пойдёт речь чуть позже.

type TMyClass = class MyInt: Integer; // это "поле" property MyIntProperty: Integer read MyInt write MyInt; // это "свойство" procedure MyMethod; // это "метод" end;

procedure TMyClass.MyMethod;begin WriteLn(MyInt + 10);end;

4.2. Наследование (Inheritance), проверка (is), и

приведение типов (as)

Паскаль поддерживает наследование и виртуальные методы объектно-ориентированного программирования.

{$mode objfpc}{$H+}{$J-}program MyProgram;

26

Page 27: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

uses SysUtils;

type TMyClass = class MyInt: Integer; procedure MyVirtualMethod; virtual; end;

TMyClassDescendant = class(TMyClass) procedure MyVirtualMethod; override; end;

procedure TMyClass.MyVirtualMethod;begin WriteLn('TMyClass отображает MyInt + 10: ', MyInt + 10);end;

procedure TMyClassDescendant.MyVirtualMethod;begin WriteLn('TMyClassDescendant отображает MyInt + 20: ', MyInt + 20);end;

var C: TMyClass;begin C := TMyClass.Create; try C.MyVirtualMethod; finally FreeAndNil(C); end;

C := TMyClassDescendant.Create; try C.MyVirtualMethod; finally FreeAndNil(C); end;end.

По умолчанию методы не являются виртуальными и для объявления ихвиртуальными необходимо использовать ключевое слово virtual . Перекрытиеили замещение виртуального метода осуществляется с помощью ключевого

27

Page 28: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

слова override , иначе компилятор выдаст ошибку. Чтобы скрыть метод безперекрытия используется ключевое слово reintroduce , впрочем, без особыхна то причин, так делать не стоит.

Чтобы узнать, является ли некоторый класс из семейства классов конкретнымего наследником можно использовать оператор is . Для выполнения приведениятипа класса к конкретному классу следует использовать оператор as .

{$mode objfpc}{$H+}{$J-}program is_as;

uses SysUtils;

type TMyClass = class procedure MyMethod; end;

TMyClassDescendant = class(TMyClass) procedure MyMethodInDescendant; end;

procedure TMyClass.MyMethod;begin WriteLn('Это MyMethod')end;

procedure TMyClassDescendant.MyMethodInDescendant;begin WriteLn('Это MyMethodInDescendant')end;

var Descendant: TMyClassDescendant; C: TMyClass;begin Descendant := TMyClassDescendant.Create; try Descendant.MyMethod; Descendant.MyMethodInDescendant;

{ производные классы сохраняют все функции родительского класса TMyClass, по этому можно таким образом создавать ссылку }

28

Page 29: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

C := Descendant; C.MyMethod;

{ так не сработает, поскольку в TMyClass не определён этот метод } //C.MyMethodInDescendant; { правильно записать следующим образом: } if C is TMyClassDescendant then (C as TMyClassDescendant).MyMethodInDescendant;

finally FreeAndNil(Descendant); end;end.

Вместо приведения типа в виде X as TMyClass , можно использоватьприведение типа без проверки с помощью выражения TMyClass(X) . Такойкод будет работать чуть-чуть быстрее, но может привести к неопределённомуповедению в случае если X не является наследником TMyClass . По этомуконструкцию TMyClass(X) лучше не применять кроме тех случаев, когдаабсолютно очевидно, что X действительно является наследником TMyClass ,например, если до этого тип класса был проверен с помощью оператора is :

if A is TMyClass then (A as TMyClass).CallSomeMethodOfMyClass;// вариант ниже - работает незначительно быстрееif A is TMyClass then TMyClass(A).CallSomeMethodOfMyClass;

4.3. Свойства

Свойства (Properties) являются "синтаксическим сахаром" (прим. перев.syntax sugar - жаргон, означающий синтаксические возможности, применениекоторых не влияет на поведение программы, но делает использование языкаболее удобным для программиста) который можно использовать с целью:

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

29

Page 30: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

2. Сделать что-то внешне похожее на поле, но доступное только для чтения -нечто, похожее на константу или функцию без параметров.

type TWebPage = class private FURL: string; FColor: TColor; function SetColor(const Value: TColor); public { Значение URL невозможно установить напрямую. Следует вызвать метод вроде Load('http://www.freepascal.org/'), для загрузки страницы и установки значения этого свойства. } property URL: string read FURL; procedure Load(const AnURL: string); property Color: TColor read FColor write SetColor; end;

procedure TWebPage.Load(const AnURL: string);begin FURL := AnURL; NetworkingComponent.LoadWebPage(AnURL);end;

function TWebPage.SetColor(const Value: TColor);begin if FColor <> Value then begin FColor := Value; { Например, требовать обновления класса, каждый раз, когда изменяется значение его цвета: Repaint; { Ещё пример: обеспечить чтобы нечто изменялось синхронно с установкой цвета, например } RenderingComponent.Color := Value; end;end;

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

30

Page 31: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

setter-метод SetColor . Однако, для получения значения свойство Colorнапрямую ссылается на private поле FColor . Прямая ссылка на конкретное поле,очевидно, быстрее, чем написание дополнительных методов getter или setter - какс точки зрения разработки, так и с точки зрения скорости исполнения программы.

При объявлении свойства указывается:

1. Может ли оно быть прочитано, и как (с помощью прямого чтения поля, или сиспользованием метода getter ).

2. Может ли оно быть установлено, и как (с помощью прямой записи поля, иливызовом метода setter ).

Компилятор следит за тем, чтобы типы и параметры соответствующих полей иметодов совпадали с типом свойства с которым они работают. Например, чтобыпрочитать свойство Integer следует или предоставить поле Integer , илибеспараметрический метод (функцию), который возвращает Integer .

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

• Функция getter не должна иметь невидимых побочных эффектов (например,она не должна читать некоторый ввод из файла / с клавиатуры). Еёзначение должно быть детерминистическим (без рандомизации или псевдо-рандомизации :)) - чтение свойства должно всегда иметь смысл и возвращатьодинаковый результат, если между операциями чтения ничего не изменилось.

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

• Функция setter должна всегда устанавливать значение таким образом,чтобы getter вернул его же обратно. Не стоит автоматически отбрасыватьневерные значения "setter", а в случае, когда это необходимо, следуетвызвать exception. Также не желательно конвертировать или масштабироватьзапрашиваемое значение. Главная идея заключается в том, чтобы послеустановки MyClass.MyProperty := 123; можно было с уверенностьюсказать, что MyClass.MyProperty = 123 .

31

Page 32: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

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

• Поле, к которому обращаются свойства почти всегда находится в разделеprivate, поскольку главная идея свойств - служить обёрткой и методом доступак нему.

• Технически, возможно создать свойства, которые только устанавливаютзначение, но не читают его. Впрочем, хороших примеров такой реализациилично мне ещё не встречалось :)

Свойства так же могут быть определены вне класса, на уровнеunit-а. Они служат аналогичной цели: они внешне выглядяткак глобальные переменные, но доступ к ним вызываетсоответствующие функции getter и setter.

Сериализация (Serialization) свойств

Свойства, имеющие уровень Published являются основой для сериализации(serialization) (также называемой передачей компонент в потоке (streamingcomponents)) в Паскале. Слово "Serialization" происходит от слова "Series" - "ряд",т.е. сериализация подаёт свойства объекта в виде некоторого линейного рядаданных, такого как например, запись в памяти или в файле.

Собственно, именно сериализация и происходит в момент, когда Lazarusчитает или записывает состояние компонент из/в файл xxx.lfm . ВDelphi эквивалентный файл имеет расширение .dfm . Этот механизмможно использовать и в своих целях, с помощью процедур таких какReadComponentFromTextStream из unit-а LResources . Также можноиспользовать другие алгоритмы сериализации, например unit FpJsonRttiпредставляет возможность сериализации в популярном формате JSON.

В Castle Game Engine можно использовать unit CastleComponentSerialize(созданный на основе FpJsonRtti ) для сериализации иерархии нашихсобственных компонент, таких как user-interface и transformation.

При каждом свойстве можно указать дополнительные параметры, которые будутполезны при использовании любого алгоритма сериализации:

32

Page 33: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

• Можно указать значение свойства "по умолчанию" с помощью ключевого словаdefault . Обратите внимание, что всё равно следует инициализироватьсвойство с помощью значения по умолчанию - это не происходитавтоматически. Значение default это лишь указание алгоритмусериализации: "когда закончится выполнение constructor-а, то данноесвойство имеет это значение".

• Сохранять ли это свойство вообще - с помощью ключевого слова stored .

4.4. Исключения

В паскале можно вызывать и использовать исключения. Их можно "ловить" спомощью конструкции try … except … end , также можно применять секцию"выполнить в конце" try … finally … end .

{$mode objfpc}{$H+}{$J-}

program MyProgram;

uses SysUtils;

type TMyClass = class procedure MyMethod; end;

procedure TMyClass.MyMethod;begin if Random > 0.5 then raise Exception.Create('Вызываем exception!');end;

var C: TMyClass;begin Randomize; C := TMyClass.Create; try C.MyMethod; finally FreeAndNil(C) end;

33

Page 34: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

end.

Обратите внимание, что раздел finally будет выполнен даже в случае, есливыполнение будет прекращено командой Exit (из функции, процедуры илиметода), операторами Break или Continue (внутри тела цикла).

4.5. Уровни видимости

Как и в большинстве других объектно-ориентированных языков, в Паскалеимеются визуальные спецификаторы для ограничения "видимости" полей /методов / свойств.

Основные уровни видимости являются следующими:

publicпредоставлен доступ из любого участка кода, включая код в других unit-ах.

privateдоступен только в этом классе.

protectedдоступен только в этом классе и всех его наследниках.

Краткое описание private и protected , данное выше, не полностью верно.Код в текущем unit-е может преодолевать эти границы, и получать доступ ксекции private и protected . Иногда это полезная особенность, позволяющаяреализовывать тесно связанные классы. В остальных же случаях следуетиспользовать strict private или strict protected для организацииполной недоступности данных методов, полей или свойств извне класса.Детальнее этот вопрос рассматривается в разделе Раздел 8.1, «Различие privateи strict private».

По умолчанию, если видимость не указана явно, то она соответствует public .исключение составляют классы, которые объявляются при включённой директиве{$M+} , либо наследники классов, которые были скомпилированы при {$M+} , что включает в себя всех потомков TPersistent , включая потомковTComponent , который сам является потомком TPersistent . Для такихклассов по умолчанию видимость принимается published , которая подобнаpublic , однако позволяет работать с ними с помощью потоковой (stream)системы.

Однако, не каждому типу поля или свойства позволено быть в секцииpublished - не каждый тип может быть конвертирован в поток (stream), и лишь

34

Page 35: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

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

4.6. Предок по умолчанию

Если явно не объявить родительский класс, то по умолчанию каждый classнаследует TObject .

5. Освобождение памяти классов

5.1. Всегда необходимо освобождать экземпляры класса

Чтобы избежать утечек памяти, все экземпляры класса должны бытьосвобождены вручную. Хорошей практикой является использование опциикомпилятора FPC -gl -gh, чтобы обнаруживать утечки памяти (более подробно см.https://castle-engine.io/manual_optimization.php#section_memory).

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

5.2. Каким образом освобождать память

Самым удобным методом освобождения класса является операцияFreeAndNil(A) из unit-а SysUtils вызванная для данного экземпляракласса. Она проверяет, не имеет ли A значение nil , и если нет — вызываетего деструктор (destructor), и устанавливает значение A в nil . Таким образомповторный вызов данной процедуры не приведёт к ошибке.

Приблизительно это соответствует следующему:

if A <> nil thenbegin A.Destroy; A := nil;end;

Впрочем, эта аналогия немного упрощена, поскольку процедура FreeAndNilсовершает ещё одно полезное действие, сразу устанавливая A значение nil до

35

Page 36: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

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

Иногда можно заметить, что применяется метод A.Free который соответствуетследующему коду:

if A <> nil then A.Destroy;

Т.е. освобождает класс A , если он не равен nil .

Стоит отметить, что в нормальных условиях никогда не стоит вызывать методкласса, ссылка на который может оказаться nil . По этому A.Free можетвыглядеть подозрительно на первый взгляд, поскольку A вполне может иметьзначение nil . Однако, метод Free является исключением из этого правила.Это выглядит немного "грязновато" — а именно, выполняется проверка Self <>nil . Такой фокус работает только для не-виртуальных методов (т.е. в случае,если не вызываются виртуальные методы и не требуется доступ к полям класса).

По этому лучше всегда использовать FreeAndNil(A) , без исключений, иникогда не использовать метод Free или напрямую деструктор Destroy . Такойконцепции придерживается Castle Game Engine. Это позволяет быть уверенным,что все ссылки либо равны nil , либо указывают на существующий рабочийэкземпляр класса.

5.3. Ручное и автоматическое освобождение памяти

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

Например:

uses SysUtils;

36

Page 37: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

type TGun = class end;

TPlayer = class Gun1, Gun2: TGun; constructor Create; destructor Destroy; override; end;

constructor TPlayer.Create;begin inherited; Gun1 := TGun.Create; Gun2 := TGun.Create;end;

destructor TPlayer.Destroy;begin FreeAndNil(Gun1); FreeAndNil(Gun2); inherited;end;

Чтобы избежать необходимости каждый раз явным образом освобождатьэкземпляр класса, можно использовать полезную особенность TComponent ,которая называется "owner" (владение дочерним классом). Класс у которого естьowner (владелец) будет автоматически освобождён его owner-ом. Механизм оченьгибкий и никогда не освобождает классы, которые уже освобождены, т.е. всё будетработать правильно в случае, если класс был освобождён ранее. Таким образомпредыдущий пример можно переписать следующим образом:

uses SysUtils, Classes;

type TGun = class(TComponent) end;

TPlayer = class(TComponent) Gun1, Gun2: TGun; constructor Create(AOwner: TComponent); override; end;

constructor TPlayer.Create(AOwner: TComponent);

37

Page 38: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

begin inherited; Gun1 := TGun.Create(Self); Gun2 := TGun.Create(Self);end;

Следует обратить внимание, что также необходимо override виртуальныйconstructor от TComponent . Это, в свою очередь, означает, что нельзя изменятьпараметры constructor-а. Впрочем, всё-таки это возможно — объявив новыйconstructor с ключевым словом reintroduce . Однако здесь стоит бытьосторожным, так как некоторый функционал, например, streaming, всё равнобудет использовать виртуальный constructor, по этому следует удостовериться,что во всех возможных случаях всё будет работать корректно.

Обратите внимание, что всегда можно использовать nil в качестве owner-а.Таким образом для данного компонента не будет owner-класса и класс не будетосвобождён автоматически. Это может оказаться полезным, если необходимоиспользовать класс на основе TComponent , сохранив при этом возможностьосвобождать его вручную. Таким образом просто создавать наследникакомпонента следующим образом: ManualGun := TGun.Create(nil); .

Ещё один удобный механизм автоматического освобожденияпамяти — функционал OwnsObjects (который по умолчанию равен true )классов-списков, таких, как TFPGObjectList или TObjectList . Т.е. можносделать следующим образом:

uses SysUtils, Classes, FGL;

type TGun = class end;

TGunList = specialize TFPGObjectList<TGun>;

TPlayer = class Guns: TGunList; Gun1, Gun2: TGun; constructor Create; destructor Destroy; override; end;

constructor TPlayer.Create;

38

Page 39: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

begin inherited; // Вообще говоря, параметр OwnsObjects и так true по умолчанию Guns := TGunList.Create(true); Gun1 := TGun.Create; Guns.Add(Gun1); Gun2 := TGun.Create; Guns.Add(Gun2);end;

destructor TPlayer.Destroy;begin { Здесь достаточно освободить сам список. Он сам автоматически освободит всё содержимое. } FreeAndNil(Guns);

{ Таким образом нет нужды освобождать Gun1, Gun2 отдельно. Правда, хорошей практикой будет теперь установить значение "nil" соответствующим значениям ссылок на них, поскольку мы знаем, что они освобождены. В этом простом классе с простым destructor-ом, очевидно, что к ним не произойдёт доступа, однако в случае сложных destructor-ов это может оказаться полезно.

Альтернативно, можно избежать объявления Gun1 и Gun2 отдельно и использовать напрямую Guns[0] и Guns[1] в коде. Можно также создать метод Gun1, который возвращает ссылку на Guns[0]. } Gun1 := nil; Gun2 := nil; inherited;end;

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

Например, в Castle Game Engine все наследники класса TX3DNodeавтоматически управляют памятью при добавлении другой TX3DNode в списокchildren. Корневая X3DNode называется TX3DRootNode и, в свою очередь,обычно имеет своим owner-ом класс TCastleSceneCore . Другие классы также

39

Page 40: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

имеют простой механизм owner-а — обычно это обозначено параметром илисвойством под названием вида OwnsXxx .

5.4. Free notification

Если на экземпляр класса создано несколько ссылок, это эквивалентно тому,что две ссылки указывают на одну и ту же область памяти. Если освободитьодну из них, вторая окажется "болтающимся" pointer-ом. Нельзя пытатьсяполучить доступ к области памяти, которая была освобождена. Это можетпривести к runtime ошибке, либо может быть получено неопределённое значение("мусор") — в случае, если эта область памяти уже была повторно выделена длядругих элементов внутри текущей программы.

В таком случае не достаточно просто вызывать FreeAndNil поскольку этафункция установит nil лишь для переданной ей ссылки — автоматическогометода для подобных задач не существует. Рассмотрим следующий пример:

var Obj1, Obj2: TObject;begin Obj1 := TObject.Create; Obj2 := Obj1; FreeAndNil(Obj1);

// что произойдёт, если попытаться получить доступ к классу Obj1 или Obj2?end;

1. В конце данного блока ссылка Obj1 является nil . Если необходимополучить доступ к ней в коде программы, для надёжности следуетиспользовать проверку if Obj1 <> nil then … чтобы случайно не вызватьметод уже освобождённого экземпляра класса, например:

if Obj1 <> nil then WriteLn(Obj1.ClassName);

Попытка доступа к ссылке nil на экземпляр класса приведёт кпредсказуемой и понятной ошибке. Таким образом, даже если код не будетпроверять Obj1 <> nil , и попытается вслепую получить доступ к Obj1 ,возникнет достаточно ясное сообщение об ошибке.

40

Page 41: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

То же самое происходит и при попытке вызова виртуального, илине-виртуального метода который пытается получить доступ к полюосвобождённого экземпляра класса.

2. Ситуация с Obj2 — куда сложнее. Её значение не nil , однако оно ужеошибочно. Попытка доступа к не- nil ссылки на несуществующий экземпляркласса приводит к непредсказуемому результату — это может быть и ошибкаaccess violation, а может и просто какое-то случайное значение.

К решению такой проблемы есть несколько путей:

• Первое решение - внимательно читать документацию к классу. Непредполагать ничего о длительности жизни ссылки, если она создана чужимкодом. Если в классе TCar есть поле wheel , указывающее на экземпляркласса типа TWheel , то есть правило что ссылка на wheel верна, покасуществует класс car , и сам car освободит все его wheel используясвой destructor. Но это правило не всегда возможно выполнить. В болеесложных случаях, в документации следует сделать упоминание о том, что икак происходит со ссылками.

• В примере выше, сразу после освобождения экземпляра класса Obj1 , можнопросто вручную установить Obj2 значение nil . В данном конкретномпримере тривиально.

• Однако, наиболее перспективным решением будет применение специальногомеханизма класса TComponent под названием "free notification" (извещениеоб освобождении). Таким образом один компонент может получить извещениев случае освобождения одной из компонент, и далее установить ссылку на неёв nil .

Таким образом можно получить слабую ссылку. Использовать эту механикуможно в различных задачах, например, позволить коду извне изменять ссылки,в том числе, возможность освобождать память в любой момент.

Для этого оба класса должны наследовать TComponent . Обычно это сводитсяк использованию FreeNotification , RemoveFreeNotification , иoverride Notification .

Следующий пример демонстрирует как использовать этот подход вместе сconstructor-ом / destructor-ом и setter-ом. Иногда можно всё сделать намногопроще, но здесь демонстрируется полномасштабная версия, которая будетверной в любом случае.

41

Page 42: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

type TControl = class(TComponent) end;

TContainer = class(TComponent) private FSomeSpecialControl: TControl; procedure SetSomeSpecialControl(const Value: TControl); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; public destructor Destroy; override; property SomeSpecialControl: TControl read FSomeSpecialControl write SetSomeSpecialControl; end;

implementation

procedure TContainer.Notification(AComponent: TComponent; Operation: TOperation);begin inherited; if (Operation = opRemove) and (AComponent = FSomeSpecialControl) then { установить значение nil для SetSomeSpecialControl чтобы всё аккуратно подчистить } SomeSpecialControl := nil;end;

procedure TContainer.SetSomeSpecialControl(const Value: TControl);begin if FSomeSpecialControl <> Value then begin if FSomeSpecialControl <> nil then FSomeSpecialControl.RemoveFreeNotification(Self); FSomeSpecialControl := Value; if FSomeSpecialControl <> nil then FSomeSpecialControl.FreeNotification(Self); end;end;

destructor TContainer.Destroy;begin

42

Page 43: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

{ Установить значение nil для SetSomeSpecialControl, чтобы запустить notification про освобождение памяти } SomeSpecialControl := nil; inherited;end;

6. Run-time library

6.1. Ввод/вывод с помощью потоков

Для ввода / вывода в современных программах на Паскале используется классTStream . У него также есть множество полезных производных классов, такихкак TFileStream , TMemoryStream , TStringStream и т.п.

{$mode objfpc}{$H+}{$J-}uses SysUtils, Classes;

var S: TStream; InputInt, OutputInt: Integer;begin InputInt := 666;

S := TFileStream.Create('my_binary_file.data', fmCreate); try S.WriteBuffer(InputInt, SizeOf(InputInt)); finally FreeAndNil(S); end;

S := TFileStream.Create('my_binary_file.data', fmOpenRead); try S.ReadBuffer(OutputInt, SizeOf(OutputInt)); finally FreeAndNil(S); end;

WriteLn('Из файла прочитано целое число: ', OutputInt);end.

В Castle Game Engine следует использовать функцию Download для созданияпотока, который загружает данные с любого заданного URL. Это могут быть

43

Page 44: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

обычные файлы, ресурсы, хранящиеся на HTTP и HTTPS, Android assets и многодругих. Более того, для однозначного кросс-платформенного указания на папкуигровых данных (хранящихся в папке data ) следует использовать функциюApplicationData или особый тип URL castle-data:/xxx URL. Например:

EnableNetwork := true;S := Download('https://castle-engine.io/latest.zip');

S := Download('file:///home/michalis/my_binary_file.data');

S := Download(ApplicationData('gui/my_image.png'));

Чтобы прочитать обычный текстовый файл лучше использовать классTTextReader из CastleClassUtils . Он предоставляет построчный API,как надстройку над TStream . URL можно задать через конструктор классаTTextReader , либо можно передать ему готовый TStream вручную.

Text := TTextReader.Create(ApplicationData('my_data.txt'));while not Text.Eof do WriteLnLog('NextLine', Text.ReadLine);

6.2. Списки

Для создания различных списков переменной длинны, лично я советуюиспользовать generic классы из unit-а FGL . Можно использовать TFPGListдля простых типов данных (включая record-ы или устаревшие object-ы), иTFPGObjectList для списка экземпляров классов. В Castle Game Engine:можно также использовать TGenericStructList из CastleGenericListsдля создания списков record-ов или устаревших object-ов. Это позволяет обойтипроблему невозможности override их операторов в старых версиях FPC.

Применение таких списков является хорошей идеей по нескольким причинам:они типо-безопасны и их API имеет много полезных функций, таких как поиск,сортировка, итерация и т.п. Вообще говоря, не стоит использовать динамическиемассивы ( array of X , SetLength(X, …) ) поскольку их API очень неудобный -можно применить лишь SetLength . Также не желательно использовать TListили TObjectList поскольку это потребует преобразование типа из общегоTObject в конкретный тип класса при каждом обращении.

44

Page 45: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

6.3. Клонирование классов: TPersistent.Assign

Стандартным подходом для создания копии экземпляра класса являетсянаследование этим классом TPersistent с последующим override его методаAssign . Здесь нет ничего сложного, просто нужно в методе Assign прописатькопирование полей, которые Вам необходимы.

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

{$mode objfpc}{$H+}{$J-}uses SysUtils, Classes;

type TMyClass = class(TPersistent) public MyInt: Integer; procedure Assign(Source: TPersistent); override; end;

TMyClassDescendant = class(TMyClass) public MyString: string; procedure Assign(Source: TPersistent); override; end;

procedure TMyClass.Assign(Source: TPersistent);var SourceMyClass: TMyClass;begin if Source is TMyClass then begin SourceMyClass := TMyClass(Source); MyInt := SourceMyClass.MyInt; // Xxx := SourceMyClass.Xxx; // добавить необходимые поля здесь end else { Вызываем inherited ТОЛЬКО если не получается вручную обработать Source } inherited Assign(Source);end;

procedure TMyClassDescendant.Assign(Source: TPersistent);

45

Page 46: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

var SourceMyClassDescendant: TMyClassDescendant;begin if Source is TMyClassDescendant then begin SourceMyClassDescendant := TMyClassDescendant(Source); MyString := SourceMyClassDescendant.MyString; // Xxx := SourceMyClassDescendant.Xxx; // добавить необходимые поля здесь end;

{ ВСЕГДА вызываем inherited, чтобы TMyClass.Assign сама обработала все остающиеся поля. } inherited Assign(Source);end;

var C1, C2: TMyClass; CD1, CD2: TMyClassDescendant;begin // тестируем TMyClass.Assign C1 := TMyClass.Create; C2 := TMyClass.Create; try C1.MyInt := 666; C2.Assign(C1); WriteLn('C2 состояние: ', C2.MyInt); finally FreeAndNil(C1); FreeAndNil(C2); end;

// тестируем TMyClassDescendant.Assign CD1 := TMyClassDescendant.Create; CD2 := TMyClassDescendant.Create; try CD1.MyInt := 44; CD1.MyString := 'что-нибудь'; CD2.Assign(CD1); WriteLn('CD2 состояние: ', CD2.MyInt, ' ', CD2.MyString); finally FreeAndNil(CD1); FreeAndNil(CD2); end;end.

46

Page 47: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Иногда более удобно написать альтернативный override метода AssignTo вкопируемом классе, а не делать override метода Assign в классе, в которыйвыполняется копирование.

Следует быть осторожным с inherited при написании Assign . Inherited изTPersistent.Assign должен вызываться исключительно в случаях, если Выне можете самостоятельно выполнить копирование в своём коде (это позволитиспользовать метод AssignTo и создавать сообщения об ошибках, в случае,если копирование не может быть выполнено). С другой стороны, если данныйкласс является производным от класса, в котором уже есть метод Assign , то вданном случае следует обязательно вызывать inherited из TMyClass.Assign .См. пример выше.

Обратите внимание, что наследуя класс TPersistent поумолчанию видимость полей становится published , чтобыдать возможность переводить в поток (streaming) производныеот TPersistent классы. Однако, не все типы полей исвойств разрешены в секции published . Если возникаютсвязанные с этим ошибки и нет необходимости передавать этиданные в поток, просто вручную измените уровень видимостина public . Детальнее см. раздел Раздел 4.5, «Уровнивидимости».

7. Различные полезные возможности языка

7.1. Местные (вложенные) процедуры

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

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

Данные два примера абсолютно эквивалентны:

47

Page 48: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

function SumOfSquares(const N: Integer): Integer;

function Square(const Value: Integer): Integer; begin Result := Value * Value; end;

var I: Integer;begin Result := 0; for I := 0 to N do Result := Result + Square(I);end;

И второй вариант, в котором Square получает прямой доступ к переменной I :

function SumOfSquares(const N: Integer): Integer;var I: Integer;

function Square: Integer; begin Result := I * I; end;

begin Result := 0; for I := 0 to N do Result := Result + Square;end;

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

7.2. Колбэки — они же события, они же указатели на

функции, они же процедурные переменные

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

48

Page 49: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

исполнения кода для указания на любую функцию с указанными типамипараметров и возвращаемых величин.

Колбэки могут быть:

• Обычными, что означает, что они могут указывать на любую нормальнуюфункцию (т.е. не на метод или локальную функцию)

{$mode objfpc}{$H+}{$J-}

function Add(const A, B: Integer): Integer;begin Result := A + B;end;

function Multiply(const A, B: Integer): Integer;begin Result := A * B;end;

type TMyFunction = function (const A, B: Integer): Integer;

function ProcessTheList(const F: TMyFunction): Integer;var I: Integer;begin Result := 1; for I := 2 to 10 do Result := F(Result, I);end;

var SomeFunction: TMyFunction;begin SomeFunction := @Add; WriteLn('1 + 2 + 3 ... + 10 = ', ProcessTheList(SomeFunction));

SomeFunction := @Multiply; WriteLn('1 * 2 * 3 ... * 10 = ', ProcessTheList(SomeFunction));end.

• Указывающими на метод: для этого в конце добавляется of object .

49

Page 50: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

{$mode objfpc}{$H+}{$J-}uses SysUtils;

type TMyMethod = procedure (const A: Integer) of object;

TMyClass = class CurrentValue: Integer; procedure Add(const A: Integer); procedure Multiply(const A: Integer); procedure ProcessTheList(const M: TMyMethod); end;

procedure TMyClass.Add(const A: Integer);begin CurrentValue := CurrentValue + A;end;

procedure TMyClass.Multiply(const A: Integer);begin CurrentValue := CurrentValue * A;end;

procedure TMyClass.ProcessTheList(const M: TMyMethod);var I: Integer;begin CurrentValue := 1; for I := 2 to 10 do M(I);end;

var C: TMyClass;begin C := TMyClass.Create; try C.ProcessTheList(@C.Add); WriteLn('1 + 2 + 3 ... + 10 = ', C.CurrentValue);

C.ProcessTheList(@C.Multiply); WriteLn('1 * 2 * 3 ... * 10 = ', C.CurrentValue); finally

50

Page 51: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

FreeAndNil(C); end;end.

Следует заметить, что невозможно передать обычные процедуры или функциикак методы. Они несовместимы. Если Вам необходимо использовать колбэкof object , но не хотите создавать экземпляр-пустышку для класса, естьвозможность передавать Раздел 8.3, «Class method» в качестве метода.

type TMyMethod = function (const A, B: Integer): Integer of object;

TMyClass = class class function Add(const A, B: Integer): Integer class function Multiply(const A, B: Integer): Integer end;

var M: TMyMethod;begin M := @TMyClass(nil).Add; M := @TMyClass(nil).Multiply;end;

К сожалению, в данном случае приходится писать громоздкую конструкцию@TMyClass(nil).Add , а не просто @TMyClass.Add .

• Может указывать на локальную процедуру: для этого её нужно объявитьс is nested в конце, а также установить директиву {$modeswitchnestedprocvars} для данного участка кода. Детальнее см. Раздел 7.1,«Местные (вложенные) процедуры».

7.3. Generic-и

Generic-и - это очень мощная функция современных языков. Определение чего-либо (обычно - класса) может быть конкретизировано другим типом. Наиболееестественным примером является необходимость создать контейнер (список,словарь, дерево, граф…): можно определить список типа T, а потом specialize(конкретизировать) его чтобы сразу получить список из integer, список из string,список из TMyRecord и т.д.

51

Page 52: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Generic-и в Паскале реализованы весьма подобно generic-ам в C++. Это означает,что они "конкретизируются" когда происходит specialize, что немного похоже наповедение макросов (однако, когда тип "конкретизирован", используются толькоэти конкретные определения, и таким образом невозможно "добавить" какое-либонеожиданное поведение в generic после его "конкретизации"). Преимуществомих является высокая скорость выполнения (оптимизированная для каждогоконкретного типа) и поддержка типов любого размера. Можно использоватьпримитивные типы (integer, float) а также record, class и т.п. при specialize данногоgeneric-а.

{$mode objfpc}{$H+}{$J-}uses SysUtils;

type generic TMyCalculator<T> = class Value: T; procedure Add(const A: T); end;

procedure TMyCalculator.Add(const A: T);begin Value := Value + A;end;

type TMyFloatCalculator = specialize TMyCalculator<Single>; TMyStringCalculator = specialize TMyCalculator<string>;

var FloatCalc: TMyFloatCalculator; StringCalc: TMyStringCalculator;begin FloatCalc := TMyFloatCalculator.Create; try FloatCalc.Add(3.14); FloatCalc.Add(1); WriteLn('Сложение величин типа Float: ', FloatCalc.Value:1:2); finally FreeAndNil(FloatCalc); end;

StringCalc := TMyStringCalculator.Create; try

52

Page 53: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

StringCalc.Add('что-нибудь'); StringCalc.Add(' ещё'); WriteLn('Сложение величин типа String: ', StringCalc.Value); finally FreeAndNil(StringCalc); end;end.

Возможности generic-ов не ограничиваются классами, можно создать genericфункции и процедуры:

{$mode objfpc}{$H+}{$J-}uses SysUtils;

{ Примечание: этот пример требует FPC 3.1.1 и не скомпилируется в FPC 3.0.0 или более ранних версиях. }

generic function Min<T>(const A, B: T): T;begin if A < B then Result := A else Result := B;end;

begin WriteLn('Min (1, 0): ', specialize Min<Integer>(1, 0)); WriteLn('Min (3.14, 5): ', specialize Min<Single>(3.14, 5):1:2); WriteLn('Min (''a'', ''b''): ', specialize Min<string>('a', 'b'));end.

7.4. Overload

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

По умолчанию overload использует стиль FPC. Это означает, что абсолютновсе имена в пространстве имён (в классе или unit-е) равны, и скрывает всеостальные методы в пространстве имён с более низким приоритетом. Например,если создать класс с методом Foo(Integer) и Foo(string) , и при этомкласс наследует класс в котором уже есть Foo(Float) , то метод Foo(Float)

53

Page 54: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

невозможно будет вызвать напрямую, только с помощью приведения типаданного класса к родительскому классу. Чтобы избежать такого поведения,при объявлении процедур и функций следует использовать ключевое словоoverload .

7.5. Предобработка кода

Можно использовать простые директивы предобработки кода с целью:

• компиляции на основании различных условий (например, если код зависит отплатформы, или других заданных вручную параметров),

• чтобы включить один файл внутри другого,

• для вызова беспараметрических макросов.

Обратите внимание, макросы с параметрами запрещены. В общем, следуетизбегать предобработки кода кроме случаев, когда она действительнонеобходима. Предобработка происходит перед парсингом, что значит, что онавполне может "ломать" обычный синтаксис языка Паскаль. Это мощно, нонемного "грязно".

{$mode objfpc}{$H+}{$J-}unit PreprocessorStuff;interface

{$ifdef FPC}{ всё что идёт внутри данного условия ifdef определено только для FPC, а не других компиляторов (например, Delphi). }procedure Foo;{$endif}

{ Определить константу NewLine. Это пример того, как "нормальный" синтаксис Паскаля "поломан" директивами предобработки. Если компилировать на Unix-системах (включая Linux, Android, Mac OS X), компилятор увидит следующее:

const NewLine = #10;

Если компилировать на Windows, компилятор увидит так:

const NewLine = #13#10;

Однако, на других операционных системах, код не скомпилируется, поскольку компилятор увидит следующее:

54

Page 55: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

const NewLine = ;

Вообще, это *хорошо* что в данном случае возникает ошибка -- если возникнет необходимость портировать программу на другую операционную систему, которая не является ни Unix, ни Windows, то компилятор "напомнит", что необходимо выбрать правильное значение NewLine для такой системы. }

const NewLine = {$ifdef UNIX} #10 {$endif} {$ifdef MSWINDOWS} #13#10 {$endif} ;

{$define MY_SYMBOL}

{$ifdef MY_SYMBOL}procedure Bar;{$endif}

{$define CallingConventionMacro := unknown}{$ifdef UNIX} {$define CallingConventionMacro := cdecl}{$endif}{$ifdef MSWINDOWS} {$define CallingConventionMacro := stdcall}{$endif}procedure RealProcedureName; CallingConventionMacro; external 'some_external_library';

implementation

{$include some_file.inc}// $I это удобное сокращение от $include, они идентичны{$I some_other_file.inc}

end.

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

• Включаемый файл может содержать лишь другие директивы компилятора,которые "конфигурируют" исходный код программы. Например, можно создатьфайл myconfig.inc со следующим содержанием:

55

Page 56: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

{$mode objfpc}{$H+}{$J-}{$modeswitch advancedrecords}{$ifndef VER3} {$error Этот код может быть скомпилирован только в версии FPC не ниже 3.x.}{$endif}

Теперь можно включить этот файл {$I myconfig.inc} в каждом unit-еисходного кода.

• Второй важный пример использования - разделить unit на несколько файлов,и при этом всё же оставить его одним unit-ом (с учётом всех особенностейПаскаля). Не стоит злоупотреблять таким подходом. В первую очередь стоитдумать о том, как разделить программу на несколько unit-ов, а не разделятьеё на множество include файлов. Однако, в общем, это довольно полезная иудобная техника.

1. Такой подход позволяет избежать огромного количества unit-ов, и приэтом исходные файлы не окажутся слишком длинными. Например, кудаудобнее иметь один файл с "часто используемыми GUI элементами", чемсоздавать отдельные unit-ы для каждого GUI элемента", поскольку второйвариант сделает обычную "uses" неоправданно длинной (поскольку в GUI-приложении будут использоваться множество GUI элементов). Однако,размещение всех этих классов в одном файле myunit.pas приведёт кего большому размеру, что в свою очередь затруднит навигацию по файлу.Таким образом, разбить этот файл на несколько include файлов будетразумным.

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

{$ifdef UNIX} {$I my_unix_implementation.inc} {$endif}{$ifdef MSWINDOWS} {$I my_windows_implementation.inc} {$endif}

Иногда это более удобно, чем писать длинный код со множеством{$ifdef UNIX} , {$ifdef MSWINDOWS} вперемешку с нормальнымкодом (определение переменных, описание процедур). Повышается и

56

Page 57: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

читабельность кода. Эту технику можно применять более агрессивно,используя опцию командной строки -Fi при вызове FPC, чтобывключить целые папки только для конкретных платформ. Таким образомможно организовать множество версий include файлов типа {$I myplatform_specific_implementation.inc} . При этом компиляторавтоматически найдёт правильную версию.

7.6. Record

Record является своеобразным контейнером для других переменных. Онаявляется очень, очень упрощённым классом: в record-ах нет наследования, нетвиртуальных методов. Они несколько подобны structure в C-подобных языках.

Применяя директиву {$modeswitch advancedrecords} , появляетсявозможность добавить методы и уровни видимости в record и использоватьлюбые особенности языка, доступные классам, которые не нарушают простуюи предсказуемую раскладку памяти для record.

{$mode objfpc}{$H+}{$J-}{$modeswitch advancedrecords}type TMyRecord = record public I, Square: Integer; procedure WriteLnDescription; end;

procedure TMyRecord.WriteLnDescription;begin WriteLn('Квадрат числа ', I, ' равен ', Square);end;

var A: array [0..9] of TMyRecord; R: TMyRecord; I: Integer;begin for I := 0 to 9 do begin A[I].I := I; A[I].Square := I * I; end;

57

Page 58: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

for R in A do R.WriteLnDescription;end.

В современном Паскале, в первую очередь лучше использовать class , ане record , поскольку классы имеют множество дополнительных полезныхвозможностей, как конструкторы и наследование. Однако, record-ы всё ещё могутоказаться полезными если необходима скорость или предсказуемая раскладкапамяти:

• Record-ам не требуется constructor или destructor. Они определяются какпеременные. Их содержимое не определено в начале (так называемый "мусорв памяти"), кроме случаев автоматически управляемых типов, таких какstring, которые всегда инициализируются пустыми. Таким образом следуетбыть более аккуратным при их использовании, однако преимуществом такогоподхода будет увеличение скорости исполнения программы.

• Массивы, состоящие из record-ов однородно располагаются в памяти и такимобразом их удобно кешировать.

• Разметка памяти (размер и расстояние между каждым полем) для record-овчётко определена в некоторых ситуациях: когда запрашивается C layout илиесли используется packed record . Это может быть полезным в следующихслучаях:

◦ для связи между библиотеками, написанными в других языкахпрограммирования и предоставляющих API, который основывается наданных типа record,

◦ для чтения и записи бинарных файлов,

◦ для выполнения "грязных" низкоуровневых оптимизаций (как небезопасноеприведение одного типа в другой, на основании их однозначногопредставления в памяти).

• В record-ах можно также использовать часть case , которая работает как unionв C-подобных языках и позволяет интерпретировать одну и ту же областьпамяти, как различные типы данных, в зависимости от необходимости. Такимобразом можно достичь более высокой эффективности использования памятив некоторых случаях. В том числе, можно выполнять некоторые "грязные"низкоуровневые оптимизации.

58

Page 59: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

7.7. Устаревшие object

Давным-давно, в Turbo Pascal был введён новый тип синтаксиса с функционалом,похожим на классы, который задавался ключевым словом object . По сути, это- нечто среднее между record и современным class .

• Такие object-ы можно создавать / освобождать, и в процессе этого можновызвать их constructor / destructor.

• Но их можно просто объявить и использовать как обычную record. Простыетипы record или object не являются указателями (pointer-ами) на что-либо, они, собственно, являются данными. Это делает их весьма удобными вслучаях небольших объёмов информации, когда многократное распределениеили освобождение памяти не всегда оправдано.

• Устаревшие object-ы имеют наследование и виртуальные методы, впрочем,в несколько отличном виде от современных классов. Следует бытьвнимательным: если попытаться использовать object, имеющий виртуальныеметоды, без вызова его constructor-а, могут возникнуть ошибки.

В большинстве случаев крайне не советуется использовать устаревшие objects-ы. Современные class-ы имеют гораздо более широкий функционал. А в случаеесли необходимо повысить скорость выполнения, можно использовать record-ы (включая advanced records). Такие подходы обычно лучше, чем устаревшиеobject-ы.

7.8. Pointer-ы

В Паскале возможно создать pointer (указатель) на любой тип данных. Указательна тип TMyRecord определяется с помощью синтаксиса ^TMyRecord , и чащевсего такие pointer-ы называют PMyRecord . В качестве классического примерарассмотрим связанный список целых чисел, организованный с помощью record:

type PMyRecord = ^TMyRecord; TMyRecord = record Value: Integer; Next: PMyRecord; end;

Следует обратить внимание, что здесь было использовано рекурсивноеопределение (тип PMyRecord определяется с помощью TMyRecord , а

59

Page 60: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

TMyRecord использует в своём определении PMyRecord ). Можно определитьpointer на тип, который ещё не был объявлен, в том случае, если он будетопределён в том же самом блоке type .

Можно распределять и освобождать память для pointer-ов с помощьюметодов New / Dispose , или (более низкоуровневых, но не типобезопасных)методов GetMem / FreeMem . Для доступа к данным, на которыеpointer указывает, после него следует добавить оператор ^ (в видеMyInteger := MyPointerToInteger^ ). Обратная операция (получить pointerна существующую переменную) выполняется с помощью оператора-префикса @(например, MyPointerToInteger := @MyInteger ).

Существует также самый общий тип Pointer , который не указывает наконкретный тип данных и подобен void* в C-подобных языках. Он совершенноне типобезопасен, и его можно привести его тип в любой другой тип pointer-а.

Следует помнить, что экземпляр класса также является pointer-ом, хоть дляработы с ним и не нужно использовать дополнительных операторов ^ и @ .Рекурсивные определения также возможны и для классов, и в данном случае всёбудет выглядеть ещё проще:

type TMyClass = class Value: Integer; Next: TMyClass; end;

7.9. Перегрузка операторов

В Паскале существует возможность "перегрузить" (overload) значениемногих операторов языка, например определить сложение и умножениепользовательских типов данных. Рассмотрим следующий пример:

{$mode objfpc}{$H+}{$J-}uses StrUtils;

operator* (const S: string; const A: Integer): string;begin Result := DupeString(S, A);end;

60

Page 61: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

begin WriteLn('повтор' * 10);end.

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

{$mode objfpc}{$H+}{$J-}uses SysUtils;

type TMyClass = class MyInt: Integer; end;

operator* (const C1, C2: TMyClass): TMyClass;begin Result := TMyClass.Create; Result.MyInt := C1.MyInt * C2.MyInt;end;

var C1, C2: TMyClass;begin C1 := TMyClass.Create; try C1.MyInt := 12; C2 := C1 * C1; try WriteLn('12 * 12 = ', C2.MyInt); finally FreeAndNil(C2); end; finally FreeAndNil(C1); end;end.

Можно перегружать и операции над record-ами. Обычно это проще, чем в случаеклассов, поскольку нет необходимости выполнять операции по распределениюили освобождению памяти.

61

Page 62: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

{$mode objfpc}{$H+}{$J-}uses SysUtils;

type TMyRecord = record MyInt: Integer; end;

operator* (const C1, C2: TMyRecord): TMyRecord;begin Result.MyInt := C1.MyInt * C2.MyInt;end;

var R1, R2: TMyRecord;begin R1.MyInt := 12; R2 := R1 * R1; WriteLn('12 * 12 = ', R2.MyInt);end.

При работе с record-ами лучше использовать режим {$modeswitchadvancedrecords} и перегружать операторы в виде class operatorвнутри объявления record-а. Такой подход позволяет использовать generic классыкоторые зависят от существования какого-либо оператора (например TFPGList ,который зависит от доступности оператора равенства). В противоположномслучае "глобальное" определение оператора (не внутри объявления даннойrecord) не будет найдено (поскольку оно не доступно коду, которыйиспользуется в TFPGList ), и не удастся specialize список в виде specializeTFPGList<TMyRecord> .

{$mode objfpc}{$H+}{$J-}{$modeswitch advancedrecords}uses SysUtils, FGL;

type TMyRecord = record MyInt: Integer; class operator+ (const C1, C2: TMyRecord): TMyRecord; class operator= (const C1, C2: TMyRecord): boolean;

62

Page 63: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

end;

class operator TMyRecord.+ (const C1, C2: TMyRecord): TMyRecord;begin Result.MyInt := C1.MyInt + C2.MyInt;end;

class operator TMyRecord.= (const C1, C2: TMyRecord): boolean;begin Result := C1.MyInt = C2.MyInt;end;

type TMyRecordList = specialize TFPGList<TMyRecord>;

var R, ListItem: TMyRecord; L: TMyRecordList;begin L := TMyRecordList.Create; try R.MyInt := 1; L.Add(R); R.MyInt := 10; L.Add(R); R.MyInt := 100; L.Add(R);

R.MyInt := 0; for ListItem in L do R := ListItem + R;

WriteLn('1 + 10 + 100 = ', R.MyInt); finally FreeAndNil(L); end;end.

8. Дополнительные возможности классов

8.1. Различие private и strict private

Спецификатор видимости private означает что поле или метод не доступныизвне данного класса. Однако из данного правила есть одно исключение: любойкод в данном unit-е может преодолеть это ограничение и иметь полный доступк private полям и методам. Программист на C++ сказал бы, что в Паскале всеклассы в одном unit-е являются "друзьями". Часто это весьма удобно.

63

Page 64: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

Однако, при создании unit-ов большого размера со множеством не очень близкоинтегрированных классов, более безопасным является применение видимостиstrict private . Как можно легко догадаться, это означает что поле или методне доступны извне данного класса. Точка. Никаких исключений.

Таким же образом различаются спецификаторы видимости protected(видимый наследникам данного класса или "друзьям" в том же unit-е) и strictprotected (видимы только и исключительно наследникам данного класса.Точка).

8.2. Дополнительные возможности объявления классов и

локальные классы

Внутри определения класса можно создавать константы ( const ) илитипы ( type ). Таким образом можно даже создать класс внутри класса.Спецификаторы видимости будут работать как обычно, и такие локальные классымогут быть private (не видимые "внешнему миру"), что иногда удобно.

Следует обратить внимание, чтобы определить поле после константы или типабудет необходимо использовать блок var :

type TMyClass = class private type TInternalClass = class Velocity: Single; procedure DoSomething; end; var FInternalClass: TInternalClass; public const DefaultVelocity = 100.0; constructor Create; destructor Destroy; override; end;

constructor TMyClass.Create;begin inherited; FInternalClass := TInternalClass.Create; FInternalClass.Velocity := DefaultVelocity;

64

Page 65: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

FInternalClass.DoSomething;end;

destructor TMyClass.Destroy;begin FreeAndNil(FInternalClass); inherited;end;

{ Обратите внимание на префикс "TMyClass.TInternalClass." }procedure TMyClass.TInternalClass.DoSomething;beginend;

8.3. Class method

Существуют методы, которые можно вызвать из класса вообще ( TMyClass ), необязательно из его конкретного экземпляра.

type TEnemy = class procedure Kill; class procedure KillAll; end;

var E: TEnemy;begin E := TEnemy.Create; try E.Kill; finally FreeAndNil(E); end; TEnemy.KillAll;end;

Обратите внимание, что они также могут быть виртуальными — иногда это весьмаудобно, особенно, если применяется совместно с понятием Раздел 8.4, «Ссылкина класс».

Следует отметить, что constructor всегда работает, как class method когдавызывается "обычным образом" ( MyInstance := TMyClass.Create(…

65

Page 66: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

); ). Впрочем, можно вызвать конструктор в данном конкретном экземплярекласса как метод — и он тогда и сработает как обычный метод. Такимобразом можно сделать удобную "цепочку" constructor-ов, когда один constructor(например, перегруженный для принятия дополнительных параметров)выполняет определённый код, а затем вызывает другой constructor (например,беспараметрический).

8.4. Ссылки на класс

Ссылки на класс позволяют выбрать класс в процессе исполнения программы,например, для вызова class method-а или constructor-а не определив заранее,какой именно класс будет использоваться. Такой тип объявляется следующимобразом: class of TMyClass .

type TMyClass = class(TComponent) end;

TMyClass1 = class(TMyClass) end;

TMyClass2 = class(TMyClass) end;

TMyClassRef = class of TMyClass;

var C: TMyClass; ClassRef: TMyClassRef;begin // Можно сделать так:

C := TMyClass.Create(nil); FreeAndNil(C); C := TMyClass1.Create(nil); FreeAndNil(C); C := TMyClass2.Create(nil); FreeAndNil(C);

// А с помощью ссылки на класс можно сделать следующим образом:

ClassRef := TMyClass; C := ClassRef.Create(nil); FreeAndNil(C);

ClassRef := TMyClass1; C := ClassRef.Create(nil); FreeAndNil(C);

66

Page 67: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

ClassRef := TMyClass2; C := ClassRef.Create(nil); FreeAndNil(C);end;

Ссылки на класс можно комбинировать с виртуальными class method-ами.Работает это подобно классам с виртуальными методами — конкретный метод,который необходимо выполнить определяется уже в процессе выполненияпрограммы.

type TMyClass = class(TComponent) class procedure DoSomething; virtual; abstract; end;

TMyClass1 = class(TMyClass) class procedure DoSomething; override; end;

TMyClass2 = class(TMyClass) class procedure DoSomething; override; end;

TMyClassRef = class of TMyClass;

var C: TMyClass; ClassRef: TMyClassRef;begin ClassRef := TMyClass1; ClassRef.DoSomething;

ClassRef := TMyClass2; ClassRef.DoSomething;

{ А следующая строка приведёт к ошибке выполнения, поскольку DoSomething является abstract в TMyClass. } ClassRef := TMyClass; ClassRef.DoSomething;end;

Если есть экземпляр класса и необходимо создать ссылку на этот класс (нена какой-либо объявленный класс, а на конкретного наследника, который был

67

Page 68: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

использован при создании данного экземпляра класса), можно использоватьсвойство ClassType . Вообще говоря, объявленный тип ClassType являетсяTClass , который в свою очередь является class of TObject . Его тип можнопривести во что либо более конкретное, когда есть информация, чем именноявляется данный экземпляр.

ClassType также можно использовать для вызова виртуальных методов,включая виртуальные constructor-ы. Такой подход позволяет создать такиеметоды, как Clone которые создают экземпляр как точную копию данногокласса в конкретный момент исполнения программы. Можно совместить такойподход с Assign (см. Раздел 6.3, «Клонирование классов: TPersistent.Assign»)для того, чтобы метод возвращал новую, готовую к работе копию текущегоэкземпляра.

Следует обратить внимание, что подобный подход сработает только еслиconstructor данного класса виртуальный. Например, его можно использоватьс наследниками стандартного класса TComponent , поскольку все онивыполняют override виртуального constructor-а TComponent.Create(AOwner:TComponent) .

type TMyClass = class(TComponent) procedure Assign(Source: TPersistent); override; function Clone(AOwner: TComponent): TMyClass; end;

TMyClassRef = class of TMyClass;

function TMyClass.Clone(AOwner: TComponent): TMyClass;begin // Таким образом будет создан класс конкретного типа TMyClass: // Result := TMyClass.Create(AOwner); // А такой подход может создать класс как типа TMyClass, так и его наследников: Result := TMyClassRef(ClassType).Create(AOwner); Result.Assign(Self);end;

8.5. Class helper

Метод является лишь процедурой внутри конкретного класса. Извне классаон вызывается с помощью специального синтаксиса MyInstance.MyMethod(…

68

Page 69: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

) . И через некоторое время приходит привычка, что если нужно произвестидействие над классом X, следует писать X.Action(…) .

Однако, иногда возникает необходимость выполнить что-либо, что концептуальноявляется действием на класс TMyClass, однако при этом не изменяяисходный код TMyClass. Причин тому может быть несколько. Например, этоможет быть исходный код, написанный другим программистом, который неследует или невозможно изменять. Также иногда причиной тому могут бытьзависимости — добавление метода Render к классу TMy3DObject кажетсявполне логичным, однако, возможно, имплементация класса TMy3DObjectдолжна быть независимой от кода рендера? В таких случаях удобнее "расширить"существующий класс, добавив к нему функционал, при этом не изменяя егоисходный код.

Наиболее простой путь сделать это - создать глобальную процедуру, котораябудет принимать ссылку на TMy3DObject как параметр.

procedure Render(const Obj1: TMy3DObject; const Color: TColor);var I: Integer;begin for I := 0 to Obj1.ShapesCount - 1 do RenderMesh(Obj1.Shape[I].Mesh, Color);end;

И это действительно сработает. Однако, недостаток такого подхода - он выглядитне очень красиво. Ведь обычно мы вызываем действия над классом с помощьюX.Action(…) , а тут нам приходится использовать иной синтаксис: Render(X,…) . Было бы куда удобнее записать X.Render(…) , даже для случаев, когдаRender не описан в unit-е, в котором находится TMy3DObject .

Для этого и существуют class helper-ы, дающие возможность создать процедуры/функции, которые оперируют данным классом и вызываются таким же образом,как и остальные методы класса. Однако они не являются "обычными"методами — они "добавляются" извне определения класса TMy3DObject .

type TMy3DObjectHelper = class helper for TMy3DObject procedure Render(const Color: TColor); end;

69

Page 70: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

procedure TMy3DObjectHelper.Render(const Color: TColor);var I: Integer;begin { Обратите внимание, мы получаем доступ к ShapesCount, Shape без дополнительных указаний типа TMy3DObject.ShapesCount } for I := 0 to ShapesCount - 1 do RenderMesh(Shape[I].Mesh, Color);end;

Более общая концепция является "type helper", используякоторую становится возможным добавлять методыдаже к самым примитивным типам, например integer.Можно также создать "record helper" чтобы… ну, Выпоняли. Детальнее см. здесь: http://lists.freepascal.org/fpc-announce/2013-February/000587.html.

8.6. Виртуальные constructor-ы, destructor-ы

Имя destructor-а всегда должно быть Destroy , и он всегда является virtual(поскольку он вызывается без указания конкретного класса при компиляции) ибеспараметрический.

В качестве имени constructor-а принято использовать Create . Можноиспользовать и другое имя, однако здесь следует быть аккуратным — например,создав CreateMy , всегда создайте и Create , иначе constructor Create будетвсё равно доступен из родительского класса и может быть вызван в обходконкретного CreateMy конструктора.

В базовом классе TObject constructor не является виртуальным, и при созданиинаследников его можно изменить. Новый constructor просто спрячет constructorродительского класса (примечание: не используйте overload , в данном случаеэто не сработает).

В наследниках же класса TComponent , следует выполнять constructorCreate(AOwner: TComponent); override; . Для решения задач потоковойпередачи данных, а также для создания класса без указания конкретного типа принаписании программы, виртуальные constructor-ы являются очень удобными (см.раздел Раздел 8.4, «Ссылки на класс» выше).

70

Page 71: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

8.7. Ошибки при исполнении constructor-а

Что произойдёт если в процессе выполнения constructor-а возникнет ошибка?Строка

X := TMyClass.Create;

не будет выполнена до конца, и X не может быть присвоено какое-либо значение.Кто будет выполнять очистку после частично сконструированного класса?

В объектном Паскале существует следующее решение. Если возникла ошибкапри исполнении constructor-а, то сразу вызывается destructor. Именно по этойпричине этот destructor должен быть "дубовым", т.е. сработать в любом случае,даже на частично сконструированном классе. Обычно это не сложно, еслиосвобождать всё безопасным образом, например, с помощью FreeAndNil .

Также можно полагаться на факт, что перед вызовом constructor-а вся памятьгарантированно обнуляется. Таким образом, при создании все ссылки внутрикласса являются nil , а числа равны 0 и т.п.

Т.е. следующий код сработает без утечек памяти:

{$mode objfpc}{$H+}{$J-}uses SysUtils;

type TGun = class end;

TPlayer = class Gun1, Gun2: TGun; constructor Create; destructor Destroy; override; end;

constructor TPlayer.Create;begin inherited; Gun1 := TGun.Create; raise Exception.Create('Вызваем exception из constructor-а!'); Gun2 := TGun.Create;end;

71

Page 72: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

destructor TPlayer.Destroy;begin { В данном случае в результате ошибки в constructor-е, у нас может оказаться Gun1 <> nil и Gun2 = nil. Смиритесь. В таком случае, FreeAndNil справится с задачей без каких-либо дополнительных действий с нашей стороны, поскольку FreeAndNil проверяет является ли экземпляр класса nil перед вызовом соответствующего destructor-а. } FreeAndNil(Gun1); FreeAndNil(Gun2); inherited;end;

begin try TPlayer.Create; except on E: Exception do WriteLn('Ошибка ' + E.ClassName + ': ' + E.Message); end;end.

9. Интерфейсы

9.1. Хорошие (CORBA) интерфейсы

Интерфейс, так же как и класс, объявляет API, но не определяет его конкретнуюреализацию. Класс может иметь только один родительский класс, однако можетреализовать множество интерфейсов.

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

Интерфейсы CORBA в Object Pascal работают в значительной степени так,как и интерфейсы в Java (https://docs.oracle.com/javase/tutorial/java/concepts/interface.html) или в C# (https://msdn.microsoft.com/en-us/library/ms173156.aspx).

{$mode objfpc}{$H+}{$J-}

72

Page 73: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

{$interfaces corba}

uses SysUtils, Classes;

type IMyInterface = interface ['{79352612-668B-4E8C-910A-26975E103CAC}'] procedure Shoot; end;

TMyClass1 = class(IMyInterface) procedure Shoot; end;

TMyClass2 = class(IMyInterface) procedure Shoot; end;

TMyClass3 = class procedure Shoot; end;

procedure TMyClass1.Shoot;begin WriteLn('TMyClass1.Shoot');end;

procedure TMyClass2.Shoot;begin WriteLn('TMyClass2.Shoot');end;

procedure TMyClass3.Shoot;begin WriteLn('TMyClass3.Shoot');end;

procedure UseThroughInterface(I: IMyInterface);begin Write('Стреляем... '); I.Shoot;end;

var

73

Page 74: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

C1: TMyClass1; C2: TMyClass2; C3: TMyClass3;begin C1 := TMyClass1.Create; C2 := TMyClass2.Create; C3 := TMyClass3.Create; try if C1 is IMyInterface then UseThroughInterface(C1 as IMyInterface); if C2 is IMyInterface then UseThroughInterface(C2 as IMyInterface); if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); finally FreeAndNil(C1); FreeAndNil(C2); FreeAndNil(C3); end;end.

9.2. CORBA и COM интерфейсы

Почему интерфейсы названы "CORBA"?Название CORBA крайне неудачное. Куда лучший термин был бы обычныйинтерфейс. Такие интерфейсы являются "чистой особенностью языка".Их можно использовать, если возникает необходимость приведения типовразличных классов в виде одинакового интерфейса, поскольку у ниходинаковая API.

Не смотря на то, что эти интерфейсы могут использоваться совместнос технологией CORBA (Common Object Request Broker Architecture) (см.https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture), онине имеют к ней никакого отношения.

Требуется ли задавать {$interfaces corba} ?Да, поскольку по умолчанию будут созданы COM интерфейсы. Последнееможно указать явно с помощью директивы {$interfaces com} , но необязательно, так как это случай по умолчанию.

Лично я не советую использовать COM интерфейсы. Особенно в случае, еслиВам знакомы интерфейсы из других языков программирования. ИнтерфейсыCORBA в Паскале — это именно то, что ожидается от интерфейсов, они

74

Page 75: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

идентичны интерфейсам в C# и Java. При этом COM интерфейсы привносятдополнительные "особенности", которые, скорее всего, не понадобятся.

Обратите внимание, что {$interfaces xxx} определяеттолько интерфейсы, родительский интерфейс которых явно неуказан (лишь используется ключевое слово interface , а неinterface(ISomeAncestor) ). Если интерфейс имеет родительскийинтерфейс, он имеет такой же тип, как и родительский интерфейс и не зависитот директивы {$interfaces xxx} .

Что такое COM интерфейсы?Понятие COM интерфейс является синонимом понятия интерфейс,наследующий некоторый интерфейс IUnknown . Такое наследование отIUnknown :

• Требует, чтобы классы определяли методы _AddRef и _ReleaseRef .Правильная имплементация этих методов позволит управлять классом спомощью подсчёта ссылок (reference-counting).

• Добавляет метод QueryInterface .

• Позволяет взаимодействовать с технологией COM (Компонентная модельобъектов).

Почему я не советую использовать COM интерфейсы?Дополнительные возможности, привнесённые COM интерфейсами с моейточки зрения проблематичны. Не поймите превратно — идея reference-counting очень хороша. Однако переплетение такого функционала синтерфейсами (вместо того, чтобы делать их "поперечными" свойствами),на мой взгляд, является очень грязным. И явно не соответствует задачам,которые я встречал в своей практике.

• Иногда возникает необходимость передать обычные классы (не имеющиминичего общего) через обычный интерфейс.

• Иногда возникает желание управлять памятью с помощью технологииreference-counting.

• Возможно когда-нибудь лично мне тоже пригодится COM технология.Но всё это - ничем не связанные задачи. Переплетать их в единойособенности языка, на мой личный взгляд, нехорошо. И это не просто вопросэстетики, такой подход причиняет реальные проблемы: механизм reference-counting интерфейса COM, даже если отключён с помощью специальныхимплементаций _AddRef и _ReleaseRef , всё равно может привести кошибкам. Придётся быть внимательным и следить, чтобы нигде не осталась

75

Page 76: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

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

Именно по этому, мой совет: всегда использовать интерфейс в стиле CORBAи соответствующую {$interfaces corba} директиву в современном коде,который работает с интерфейсами. На мой взгляд, COM интерфейсы этонекоторое "недоразумение" языка.

Однако, чтобы быть честными, чуть дальше будет детально идти речь и о COMинтерфейсах.

Можно ли использовать reference-counting совместно с интерфейсомCORBA?

Естественно. Необходимо лишь добавить методы _AddRef / _ReleaseRef .Нет необходимости наследовать интерфейс IUnknown . Впрочем, вбольшинстве случаев, если возникает необходимость использовать reference-counting в интерфейсах, можно просто использовать COM интерфейсы.

9.3. GUID-ы интерфейсов

GUID-ы это на первый взгляд случайная последовательность букв ицифр ['{ABCD1234-…}'] которую можно увидеть при каждом объявленииинтерфейса. В действительности, они и являются случайными. Но, к сожалению,в них есть необходимость. Никакого особого смысла в них не вкладывается(если не планируется интеграция с технологиями COM или CORBA). Однако, дляправильного исполнения программы они обязательны. И пусть не сбивает с толкукомпилятор, который, увы, позволяет создавать интерфейсы без GUID-ов.

Без присваивания (уникального) GUID-а, все интерфейсы будут идентичнымидля оператора is . Таким образом, он всегда будет возвращать true еслиданный класс поддерживает любой из используемых интерфейсов. "Волшебная"функция Supports(ObjectInstance, IMyInterface) работает нескольколучше в данном случае, поскольку выдаст ошибку компиляции для интерфейсовбез GUID. Это касается и CORBA, и COM интерфейсов, для версии FPC 3.0.0.

Таким образом, необходимо обязательно объявлять GUID для каждогоинтерфейса. Можно использовать встроенный генератор GUID-ов Lazarus GUIDgenerator (горячая клавиша Ctrl + Shift + G в режиме редактирования). Либовоспользоваться он-лайн сервисом, например https://www.guidgenerator.com/.

А можно вообще написать свой инструмент, использующий CreateGUID иGUIDToString функции из RTL. Например, следующим образом:

76

Page 77: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

{$mode objfpc}{$H+}{$J-}uses SysUtils;var MyGuid: TGUID;begin Randomize; CreateGUID(MyGuid); WriteLn('[''' + GUIDToString(MyGuid) + ''']');end.

9.4. Некрасивые (COM) интерфейсы

Использование COM интерфейсов даёт две дополнительных возможности:

1. Интеграция с технологией COM (технология, которая используется в Windows,также доступна в Unix-системах через XPCOM, который применяется Mozilla),

2. Подсчёт ссылок - reference counting (что позволяет автоматическиосвобождать экземпляр класса, когда все ссылки на его интерфейс уходят изполя переменных).

На мой взгляд, неправильно переплетать интерфейсы с такими возможностями.Это усложняет использование интерфейсов для того, для чего онипредназначены: когда у многих классов один и тот же API, однако они не могутпроисходить от одного родительского класса. Используя же COM интерфейс,следует помнить о механизме автоматического уничтожения и его отношениик технологии COM.

На практике это означает следующее:

• В классе необходимо создать "волшебные" методы _AddRef , _Release иQueryInterface . Или наследовать класс, который уже имеет их. Конкретнаяимплеметация этих методов позволяет включить или отключить такуювозможность COM интерфейсов, как reference-counting. Впрочем, отключатьеё достаточно опасно — см. следующий подраздел.

◦ Стандартный класс TInterfacedObject имеет эти методывыполненными в таком виде, чтобы включить reference-counting.

◦ Стандартный класс TComponent имеет эти методы выполненнымив таком виде, чтобы выключить reference-counting. В CastleGame Engine предоставлены дополнительные классы, от которых

77

Page 78: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

можно выполнять наследование: TNonRefCountedInterfacedObjectи TNonRefCountedInterfacedPersistent для этих целей,см. детальнее: https://github.com/castle-engine/castle-engine/blob/master/src/base/castleinterfaces.pas.

• Необходимо быть аккуратным при освобождении класса, когда на негомогут ссылаться некоторые переменные интерфейса. Поскольку интерфейсыосвобождаются с помощью виртуального метода (потому что он можетиспользовать reference-counting, даже если _AddRef написан такимобразом, чтобы отключить эту возможность), то нельзя освобождатьнизлежащий экземпляр класса из-за того, что какая-либо переменнаяинтерфейса может на него указывать. См. раздел "7.7 Reference counting" вруководстве FPC (http://freepascal.org/docs-html/ref/refse47.html).

Чтобы безопасно использовать COM интерфейсы необходимо

• осознавать факт, что в них используется reference-counting,

• наследовать соответствующие классы от TInterfacedObject

• и избегать прямого обращения к экземпляру класса, вместо чего всегдаиспользовать экземпляр через интерфейс, оставляя алгоритму reference-counting управление освобождением памяти.

Ниже представлен пример использования такого интерфейса:

{$mode objfpc}{$H+}{$J-}{$interfaces com}

uses SysUtils, Classes;

type IMyInterface = interface ['{3075FFCD-8EFB-4E98-B157-261448B8D92E}'] procedure Shoot; end;

TMyClass1 = class(TInterfacedObject, IMyInterface) procedure Shoot; end;

TMyClass2 = class(TInterfacedObject, IMyInterface) procedure Shoot; end;

78

Page 79: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

TMyClass3 = class(TInterfacedObject) procedure Shoot; end;

procedure TMyClass1.Shoot;begin WriteLn('TMyClass1.Shoot');end;

procedure TMyClass2.Shoot;begin WriteLn('TMyClass2.Shoot');end;

procedure TMyClass3.Shoot;begin WriteLn('TMyClass3.Shoot');end;

procedure UseThroughInterface(I: IMyInterface);begin Write('Стреляем... '); I.Shoot;end;

var C1: IMyInterface; // COM управляет освобождением памяти C2: IMyInterface; // COM управляет освобождением памяти C3: TMyClass3; // Здесь управлять освобождением памяти придётся ВАМbegin C1 := TMyClass1.Create as IMyInterface; C2 := TMyClass2.Create as IMyInterface; C3 := TMyClass3.Create; try UseThroughInterface(C1); // Нет необходимости в операторе "as" UseThroughInterface(C2); if C3 is IMyInterface then UseThroughInterface(C3 as IMyInterface); // Так не сработает finally { Переменные C1 и C2 выходят из поля зрения и будут автоматически уничтожены сейчас.

а переменная C3 является экземпляром класса и не управляется интерфейсом,

79

Page 80: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

по этому её необходимо совободить вручную. } FreeAndNil(C3); end;end.

9.5. Использование COM интерфейсов с отключённым

reference-counting

Как уже было упомянуто в прошлом разделе, в классах,наследующих TComponent (либо подобные классы, такие какTNonRefCountedInterfacedObject иTNonRefCountedInterfacedPersistent ) отключено reference-counting дляCOM интерфейсов. Это позволяет использовать COM интерфейсы и при этомдаёт возможность освобождать классы вручную.

Всё же, необходимо быть аккуратным в этом случае, чтобы не освободить класс,когда какая-либо переменная интерфейса ссылается на него и не забывать,что каждая операция приведения типа в виде Cx as IMyInterface такжесоздаёт временную переменную интерфейса, которая может существоватьвплоть до конца текущей процедуры. Этот пример иллюстрирует процедуруUseInterfaces , которая освобождает классы вне этой процедуры (когда мыможем быть уверены, что временная переменная интерфейса вышла из текущегополя переменных).

Чтобы избежать этих неудобств, лучше использовать интерфейсы CORBA, еслив данной программе нет необходимости параллельного использования reference-counting и интерфейсов.

{$mode objfpc}{$H+}{$J-}{$interfaces com}

uses SysUtils, Classes;

type IMyInterface = interface ['{3075FFCD-8EFB-4E98-B157-261448B8D92E}'] procedure Shoot; end;

TMyClass1 = class(TComponent, IMyInterface) procedure Shoot;

80

Page 81: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

end;

TMyClass2 = class(TComponent, IMyInterface) procedure Shoot; end;

TMyClass3 = class(TComponent) procedure Shoot; end;

procedure TMyClass1.Shoot;begin WriteLn('TMyClass1.Shoot');end;

procedure TMyClass2.Shoot;begin WriteLn('TMyClass2.Shoot');end;

procedure TMyClass3.Shoot;begin WriteLn('TMyClass3.Shoot');end;

procedure UseThroughInterface(I: IMyInterface);begin Write('Стреляем... '); I.Shoot;end;

var C1: TMyClass1; C2: TMyClass2; C3: TMyClass3;

procedure UseInterfaces;begin if C1 is IMyInterface then //if Supports(C1, IMyInterface) then // эта строчка идентична проверке "is" выше UseThroughInterface(C1 as IMyInterface); if C2 is IMyInterface then UseThroughInterface(C2 as IMyInterface); if C3 is IMyInterface then

81

Page 82: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

UseThroughInterface(C3 as IMyInterface);end;

begin C1 := TMyClass1.Create(nil); C2 := TMyClass2.Create(nil); C3 := TMyClass3.Create(nil); try UseInterfaces; finally FreeAndNil(C1); FreeAndNil(C2); FreeAndNil(C3); end;end.

9.6. Приведение типов интерфейсов

Этот раздел касается как CORBA, так и COM интерфейсов. Впрочем, такжеотмечены некоторые особенности CORBA интерфейсов.

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

UseThroughInterface(Cx as IMyInterface);

Этот вариант скомпилируется для всех экземпляров C1 , C2 , C3 из примерав предыдущем подразделе. При выполнении программы возникнет ошибкадля C3 , у которого не описан IMyInterface .

Использование оператора as работает правильно независимо от того,объявлен ли Cx как экземпляр класса (например, TMyClass2 ) или какинтерфейс (например, IMyInterface2 ).

However, it is not allowed for CORBA interfaces.

2. Однако, возможно привести тип данного экземпляра самымнепосредственным образом:

UseThroughInterface(Cx);

82

Page 83: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

В таком случае проверка выполняется в момент компиляции. Такойкод скомпилируется для C1 и C2 (которые определены, как классы,использующие IMyInterface ), но не скомпилируется для C3 .

По большому счёту, такое приведение типа работает как для обычныхклассов. В случае, если требуется экземпляр класса TMyClass , всегдаможно использовать переменную, объявленную как TMyClass , которойподойдёт любой наследник TMyClass. Точно такая же ситуация и дляинтерфейсов. Не требуется выполнять явные приведения типов в такойситуации.

3. Можно также выполнить приведение типа IMyInterface(Cx) следующимобразом:

UseThroughInterface(IMyInterface(Cx));

Обычно, такой синтаксис приведения типа является небезопасным, непроверяемым. Если привести ошибочный тип интерфейса подобным образом,могут возникнуть ошибки, как в случае приведения типа класса к классу, или_интерфейса к интерфейсу_с помощью данного синтаксиса.

Можно сделать небольшую оговорку: если Cx объявлен как class (напримерTMyClass2 ), то такая операция приведения типа должна быть верной прикомпиляции. Иными словами, приведение типа класса к интерфейсу такимобразом - безопасно и быстро (проверка выполняется при компиляции).

Можете всё это попробовать, поиграв со следующим примером:

{$mode objfpc}{$H+}{$J-}

// {$interfaces corba} // обратите внимание, что приведение типа с помощью "as" для интерфейсов типа CORBA не скомпилируется

uses Classes;

type IMyInterface = interface ['{7FC754BC-9CA7-4399-B947-D37DD30BA90A}'] procedure One; end;

83

Page 84: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

IMyInterface2 = interface(IMyInterface) ['{A72B7008-3F90-45C1-8F4C-E77C4302AA3E}'] procedure Two; end;

IMyInterface3 = interface(IMyInterface2) ['{924BFB98-B049-4945-AF17-1DB08DB1C0C5}'] procedure Three; end;

TMyClass = class(TComponent, IMyInterface) procedure One; end;

TMyClass2 = class(TMyClass, IMyInterface, IMyInterface2) procedure One; procedure Two; end;

procedure TMyClass.One;begin Writeln('TMyClass.One');end;

procedure TMyClass2.One;begin Writeln('TMyClass2.One');end;

procedure TMyClass2.Two;begin Writeln('TMyClass2.Two');end;

procedure UseInterface2(const I: IMyInterface2);begin I.One; I.Two;end;

procedure UseInterface3(const I: IMyInterface3);begin I.One; I.Two; I.Three;

84

Page 85: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

end;

var My: IMyInterface; MyClass: TMyClass;begin My := TMyClass2.Create(nil); MyClass := TMyClass2.Create(nil);

// Следующий код не скомпилируется, так как в момент компиляции неизвестно является ли My интерфейсом IMyInterface2. // UseInterface2(My); // UseInterface2(MyClass);

// Это скомпилируется и работает правильно. UseInterface2(IMyInterface2(My)); // А это не скомпилируется. Приведение типа InterfaceType(ClassType) проверяется в момент компиляции. // UseInterface2(IMyInterface2(MyClass));

// Это скомпилируется и работает правильно. UseInterface2(My as IMyInterface2); // Это скомпилируется и работает правильно. UseInterface2(MyClass as IMyInterface2);

// Это скомпилируется, но приведёт к непонятной ошибке "Access violation" при выполнении программы. // UseInterface3(IMyInterface3(My)); // Это не скомпилируется. Приведение типа InterfaceType(ClassType) проверяется в момент компиляции. // UseInterface3(IMyInterface3(MyClass));

// Это скомпилируется, но приведёт к понятному сообщению об ошибке "EInvalidCast: Invalid type cast" и укажет на проблему. // UseInterface3(My as IMyInterface3); // Это скомпилируется, но приведёт к понятному сообщению об ошибке "EInvalidCast: Invalid type cast" и укажет на проблему. // UseInterface3(MyClass as IMyInterface3);

Writeln('Готово');end.

85

Page 86: Object Pascal для программистов Michalis Kamburelis · множество готовых компонент для сетей, gui, баз данных, чтения

Краткое введение в современныйObject Pascal для программистов

10. Про этот документ

Copyright Michalis Kamburelis.

Исходные файлы этого документа в формате AsciiDoc можно скачать поадресу: https://github.com/michaliskambi/modern-pascal-introduction. Автор будетрад любым пожеланиям, исправлениям, расширениям материала, доработками pull request-ам :). С автором можно связаться через профиль GitHub либопо e-mail: [email protected] . Домашняя страничка автора: https://michalis.ii.uni.wroc.pl/~michalis/.

Этот документ можно свободно распространять и изменять согласнолицензи идентичной лицензии Wikipedia, см. https://en.wikipedia.org/wiki/Wikipedia:Copyrights:

• Creative Commons Attribution-ShareAlike 3.0 Unported License (CC BY-SA)

Либо

• GNU Free Documentation License (GFDL) (unversioned, with no invariant sections,front-cover texts, or back-cover texts) .

Благодарю за прочтение!

Перевод на русский выполнен: Александр Скворцов и Евгений Лоза 2016-2019

Благодарность за помощь в поиске и исправлении ошибок и очепяток: vitaly_l иpupsik @ freepascal.ru

1 mailto:[email protected]

86