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