Понимание ООП на примере Паскаля
05-06-2020Время чтения ~ 7 мин.PHP/ООП 7275
В продолжении предыдущей темы, что база программирования важнее самого языка, покажу на примере 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 выглядит достаточно просто: обычные подпрограммы и классы, которые в большинстве случаев нужны только для ограничения области видимости. На мой взгляд, после Паскаля, большинство вещей в программировании становятся простыми и понятными, что позволяет перейти к использованию любого другого современного языка.