Понимание ООП на примере Паскаля

В продолжении предыдущей темы, что база программирования важнее самого языка, покажу на примере Turbo Pascal 7.0, что такое объекты в объектно-ориентированном программировании. (Чтобы было понятно, ООП в Паскале появился в 1989 году в версии TP 5.5.)

Если начинать изучать объекты, например с PHP, то не будет понимания, что это вообще такое. Зачем, например оператор new, что такое поля или методы? Такие вещи воспринимаются как часть языка, а всё что внутри — чёрный ящик, где происходят какие-то скрытые процессы. Вообще ООП — это парадигма, то есть описывает вроде как только общие принципы, а конкретная реализация зависит от языка. Но, на самом деле ООП в итоге всё равно превращается в обычный исполняемый код. На мой взгляд важно понимать откуда он берётся.

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

В ООП объект — это тип данных. Есть простые типы данных. Например тип Byte используется для хранения информации размером в один байт. Тип Word — занимает 2 байта и служит для хранения чисел от 0 до 65535.

Когда объявляется переменная, программа выделяет область памяти и именует её для идентификации. При этом компилятор обязательно проверит тип данных, которые пытаются в эту область записать. Если это тип Char (символьный), то в него никак не получится записать число.

В Паскале переменные объявляются в секции var (от «variables»):

var
   a: integer;
   s: string;

В данном случае типы integer и string — простые, о них знает компилятор Паскаля. Но, можно сделать и свои типы данных. Например объявим тип myInteger, который будет целочисленным LongInt.

type
   myInteger: LongInt;
 
var
   a: myInteger;

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

Процедурный тип данных

Вначале я приведу полный код программы.

program sample_type_function;
{$F+}
 
{ TYPES }
type
    TMyFunc = function: string;
 
{ FUNCTIONS }
function f1: string;
begin
     f1 := 'this f1';
end;
 
function f2: string;
begin
     f2 := 'this f2';
end;
 
function f3(s: string): string;
begin
     f3 := 'this f3';
end;
 
{ VARIABLES }
var
    myFun: TMyFunc;
 
{ START PROGRAM }
begin
    writeln('=== MyFun ===');
 
    myFun := f1;
    writeln(myFun);
 
    myFun := f2;
    writeln(myFun);
 
    { ! ERROR: TYPE MISMATCH }
    { myFun := f3; }
 
    writeln('... press Enter ...');
    readln;
end.

Здесь объявляется процедурный тип TMyFunc.

В Паскале принято идентификаторы типов именовать с большой буквы «T».
Ключ {$F+} указывает компилятору использовать отложенное определение функций и процедур. За это отвечает опция компилятора «Force far calls».

Сама функция при этом вообще никак не определена. То есть глядя на это объявление, единственное, что мы можем увидеть — только то, что функция не имеет параметров и возвращает тип string.

Мы объявляем несколько функций: f1, f2, которые точно также не имеют параметров и возвращают строку. То есть по сути они полностью соответствуют по параметрам типу TMyFunc.

А вот функция f3, уже имеет входящий параметр, что делает её несовместимой с TMyFunc.

Имя функции, её параметры и тип возвращаемого результата определяют т.н. сигнатуру функции. В последующих версиях Паскаля (и других ЯП), сигнатура используется для перегрузки (перекрытия), то есть когда можно определить функцию с одним именем, но разными параметрами. Компилятор проверит входящие параметры и вызовет нужную функцию. Перекрытие функций есть не во всех языках, например его нет в PHP.

Теперь, чтобы использовать тип TMyFunc, нужно объявить переменную — у нас это myFun. А дальше она используется как обычно, путем присваивания. В первом примере вывода она будет f1, а во втором f2.

А вот присвоить f3 не получится. Компилятор сразу ругнётся с ошибкой «TYPE MISMATCH» (несовместимость типов).

Функции (и процедуры) — это подпрограммы. Они располагаются отдельными блоками в памяти и имеют точку входа. То есть вызывая f1, программа обратится к ячейке памяти, где начнётся выполняться эта подпрограмма. После выполнения произойдёт возврат из подпрограммы в основную программу.

Когда мы присваиваем myFun := f1;на самом деле создаётся лишь ссылка на адрес подпрограммы. Компилятор понимает, что myFun — процедурный тип, а значит в этой переменной будет храниться только адрес подпрограммы. Если мы попытаемся вызвать myFun без предварительного присваивания, то программа вывалится с ошибкой, поскольку переменная изначально ничего не содержит и ни на что не ссылается.

Тип Record (запись)

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

program sample_rec;
{$F+}
 
{ TYPES }
type
    TMyFunc = function: string;
 
    TMyRec = record
           a: integer;
           c: char;
           f: TMyFunc;
    end;
 
{ FUNCTIONS }
function f1: string;
begin
    f1 := 'this f1';
end;
 
{ VARIABLES }
var
    myRec: TMyRec;
 
{ START PROGRAM }
begin
    writeln('=== MyRec ===');
 
    myRec.a := 1;
    myRec.c := 'a';
    myRec.f := f1;
 
    writeln(myRec.a);
    writeln(myRec.c);
    writeln(myRec.f);
 
    writeln('... press Enter ...');
    readln;
end.

Здесь объявляется тип TMyRec, который состоит из полей a, с и f. Работать с таким типом данных нужно аналогично: объявляется переменная (myRec), а после используется в коде. Чтобы получить доступ к полям используется символ точки «.».

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

Нетрудно заметить, что процедурный тип и запись уже сильно напоминают объекты. Вот если мы их объединим, то и получим тип Объект.

Тип Object

Исходный код:

program sample_obj;
{$F+}
 
{ TYPES }
type
    TMyObj = object
         a: integer;
         c: char;
         procedure p;
    end;
 
{ FUNCTIONS }
procedure TMyObj.p;
begin
     a := 1;
     c := 'a';
end;
 
{ VARIABLES }
var
    MyObj: ^TMyObj;
 
{ START PROGRAM }
begin
    writeln('=== MyObj ===');
 
    new(MyObj);
 
    MyObj^.p;
    writeln(MyObj^.a);
    writeln(MyObj^.c);
 
    MyObj^.a := 2;
    MyObj^.c := 'b';
    writeln(MyObj^.a);
    writeln(MyObj^.c);
 
    writeln('... press Enter ...');
    readln;
end.

Для объявления объектов используется ключевое слово object. Внутри описываются поля так же, как и в записи. А процедуры/функции объявляются в полном виде (параметры и тип результата) и уже называются методы. Для доступа к полям и методам используется аналогичная нотация в виде точки.

В методах, в отличие от обычных процедур, автоматически доступны поля объекта. В других языках для этого приходится использовать специальное ключевое слово «this», указывающее на текущий объект. Именно поэтому в TMyObj.pнет секции var: мы сразу обращаемся к полям объекта.

Для работы с объектом нужно также создать переменную. Но здесь уже следует использовать «шляпки» — символ «^»: в Паскале это означает указатель, который ссылается на тип данных. Если по простому, то это ссылка на область памяти. В данном случае переменная MyObj как бы ссылается на TMyObj. Это довольно типовое поведение для ЯП. Например в PHP когда вы присваиваете какой-то переменной существующий объект, то обе переменные будут ссылаться на один и тот же объект в памяти. В Паскале переменная также указывает именно на объект в памяти, только делается это явно.

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

Обращение к полям и функциям объекта происходит также через точку и указатель. Точно такое же поведение и в других языках программирования, только не везде требуется указывать «шляпку». Это говорит о том, что объект — это в первую очередь ссылка на структуру данных в памяти.

Соберём воедино

Итак, объект — это некая область памяти, содержащая поля данных и подпрограммы. Перед работой с объектом, необходимо определить его тип данных. В Паскале это осуществляется в секции type, как и для любого типа данных. В более поздних версиях Паскаля (и в других языках) для декларирования типа объекта используется ключевое слово class. Так например в PHP:

// TYPES
class TMyObj {
   var $a;
   var $c;
 
   function p() {
      $this->a = 2;
      $this->c = 'b';
   } 
}
 
// START PROGRAM
$myObj = new TMyObj();
 
$myObj->p();
 
echo $myObj->a;
echo $myObj->c;

Фактически это тот же самый код из Паскаля, только в PHP часть работы оказалась скрыта: это делает код проще и удобней. Но при этом из кода PHP мы никогда не узнаем, что на самом деле есть указатели и переменные, которые также ссылаются на выделенную память объекта. Причём это отличается от поведения обычных php-переменных. Но узнать об этом можно только в справке. :-)

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

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)