#25 ООП Паскале. Введение: Класс, объект, конструктор, метод, поле

Что такое ООП

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

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

Далее мы начнем знакомиться с основными из этих понятий, активно используя примеры кода

Привет ООП! Начало работы с классами. Методы, конструктор, создание объекта

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

В отличие от записей и массивов классы не просто хранят данные, они могут действовать ;)
Давайте начнем именно с этого их свойства и рассмотрим пример:

 type
  THelloWorld = class // объявляем класс
  public // секция публичных - общедоступных элементов класса
    procedure sayHello; // c единственным методом: процедурой
  end;

procedure THelloWorld.sayHello; // описываем реализацию процедуры
begin
 writeln('Привет Мир!');
end;

var
  primer: THelloWorld; // переменная типа класса THelloWorld
begin
  { создаем объект = "экземпляр класса" THelloWorld
   вызовом стандартного метода-конструктора create() }
  primer := THelloWorld.create();

  // вызываем метод, который должен здоровься
  primer.sayHello();
end. 

-- добавим следующее:

  1. Метод - это подпрограмма (в Паскале - процедура или функция), которая относится к какому-либо классу и его объектам.
  2. Как связаны "класс" и "объект": класс - это тип для объекта, по аналогии, напр. с тем, что конкретный автомобиль является экземпляром класса своей модели. Объект также называют "экземпляром класса" (эти термины в документации часто значат одно и то же).
  3. Конструктор -- специальный метод, который создает экземпляр класса. Можно сказать, что конструктор как бы строит объект в памяти компьютера, выделяя ее в нужных количествах и заполняя необходимымми структурами, а чертежом/проектом для этого строительства как раз и служит класс, в которому конструируемый объект относится.
  4. В примере выше использован стандартный конструктор create() (еще такие конструкторы называют конструкторами по умолчанию) - мы нигде не определяли его, но изначально есть у любого класса в Паскале, потому и называется стадартным
    (такой же подход с наличием стандарного неявно определяемого конструктора используется и во многих других ЯП).

Поля класса - хранение данных. Обращение к собственным полям из методов класса

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

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

 type
  THelloWorld = class
  public
    whatToSay: string; // поле строкового типа
    procedure sayHello;
  end;

procedure THelloWorld.sayHello;
begin
 writeln(self.whatToSay); // поле класса в качестве аргумента процедуре печати
end;

var
  primer: THelloWorld; // переменная типа класса THelloWorld
begin
  primer := THelloWorld.create();
  primer.whatToSay := 'Привет Вася!!!';
  primer.sayHello();
end.  

в этом примере:

  • программа делает все то же самое, но тут мы используем добавленное нами же свойство whatToSay (переводится как "что говорить"), в которое можно записать любую фразу, которую мы хотим использовать для приветствия, это позволяет менять поведение объекта после создания из тела программы.
  • Поля класса - это переменные, связанные с классом (и созданными по его схеме объектами), в них могут лежать данные любого, допустимого для данной программы типа

Использование нестандартного конструктора. Передача параметров конструируемому объекту

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

 type
  THelloWorld = class
  public
    whatToSay: string;
    // объявляем нестандартный конструктор
    constructor create(whatToSayValue: string);
    procedure sayHello;
  end;

procedure THelloWorld.sayHello;
begin
 writeln(self.whatToSay);
end;

// Добавляем реализацию собственного конструктора
constructor THelloWorld.create(whatToSayValue: string);
begin
 self.whatToSay := whatToSayValue;
end;

var
  primer: THelloWorld; // переменная типа класса THelloWorld
begin
  primer := THelloWorld.create('Привет мир!!!');
  primer.sayHello();
end.  

В этом примере:

  1. Мы сразу, прямо в конструктор передаем аргумент строкового типа
  2. Конструктор реализован таким образом, что записывает переданный аргумент в поле объекта whatToSay и именно это поле использует в своей реализации метод sayHello()

Конфликты имен между аругументами метода класса и публичными полями класса

В данном разделе для объявления полей и элементов класса мы рассматривали только одну область видимости - public. Подробнее о том что это мы поговорим в следующих уроках, но сразу заметим что в freepascal для кода вида:

type
  Cat = class
  public
    name: string; // поле класса
    constructor create(name: string); // публичный (открытый) аргумент метода с имененем как и у поля выше
    procedure sayHello();
  end;

constructor Cat.create(name: string);
begin
   self.name := name;
end;

procedure Cat.sayHello();
begin
 writeln('Привет, я '  + self.name + '!');
end;

var
  CatItem: Cat;
begin
  CatItem := Cat.create('Мурка');
  CatItem.sayHello();
end. 

-- при попытке его запустить получим ошибку:

Compile Project, Target: .. Exit code 1, Errors: 1, Hints: 1
project1.lpr(5,24) Error: Duplicate identifier "name"

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

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

type
  Cat = class
  public
    name: string;
    constructor create(nameValue: string); // изменим имя аргумента
    procedure sayHello();
  end;
 
constructor Cat.create(nameValue: string);
begin
   self.name := nameValue; // используем новое имя аргумента
end;
 
procedure Cat.sayHello();
begin
 writeln('Привет, я '  + self.name + '!');
end;
 
var
  CatItem: Cat;
begin
  CatItem := Cat.create('Мурка');
  CatItem.sayHello();
end.

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

Пример №1 - разбиение/разделение решения на методы внутри класса

Пользователь передает целое положительное число $N$, выведете на экран последовательность от $1$ до $N$ "ёлочкой", например для $N = 18$:

1
2 3
4 5 6
7 8 9 10
11 12 13 14 15
16 17 18

-- оформить решения задачи (способом "одним циклом") в виде класса, который:

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

Решение:



type
  Elka = class
    public
      count: integer;
      constructor create(n: integer);
      procedure newLineIfNeeded(var k, m: integer);
      procedure show();
  end;

constructor Elka.create(n: integer);
begin
   self.count := n;
end;

procedure Elka.newLineIfNeeded(var k, m: integer);
begin
  if (k = m) then
  begin
    writeln();
    m+=1; // ожидаем на 1 один элемент больше в следующей строке
    k:=0;
  end;
end;

procedure Elka.show();
var i,
  m, // ожидаемая длина строки
  k  // текущая длина строки
  :integer;
begin
  m:=1;
  k:=0;
  for i:=1 to self.count do
  begin
    write(i, ' ');
    k+=1;
    self.newLineIfNeeded(k, m);
  end;
end;

var n, i,
  m, // ожидаемая длина строки
  k  // текущая длина строки
  :integer;
  ElkaItem: Elka;
begin
  n:= 10; // вводит пользователь

  // ----- Решение с ООП:
  ElkaItem := Elka.create(n);
  ElkaItem.show();

  // -----Решение без ООП (одним циклом):
  m:=1;
  k:=0;
  for i:=1 to n do
  begin
    write(i, ' ');
    k+=1;
    if (k = m) then
    begin
      writeln();
      m+=1; // ожидаем на 1 один элемент больше в следующей строке
      k:=0;
    end;
  end;

end.

-- для лучше понимания можно посмотреть видео-разбор этого решения.

Видео-материалы

Задачи для самостоятельного решения

    Ответьте на вопросы:
  1. Что такое класс? Что такое объект? Как они связаны и чем отличаются?
  2. Что такое метод класса? Чем именно могут быть методы в Паскале?
  3. Что такое поля класса? Для чего нужны?
  4. Что такое конструктор?
  5. Решите задачи:

  6. Напишите программу, которая использует объект класса Posledovatelnost для вывода на экран числа от 1 до N, при это пусть последовательность выводится самим кноструктором класса, который создает объект
  7. Напишите программу, которая использует объект класса Posledovatelnost для вывода на экран числа от 1 до N, для вывода последовательности реализуйте метод doIt(), который в качестве аргумента принимает значение N
  8. Напишите программу, которая использует объект класса Posledovatelnost для вывода на экран числа от 1 до N, при этом пусть класс на этапе конструирования объекта сохраняет значение N во внутреннее поле, а для вывода последовательности реализуйте метод doIt()
  9. Напишите программу, которая выводит матрицу из единиц размерами M на N, напр. для M=2 и N=4 мы должны получить результат:
    1 1 1 1
    1 1 1 1
    

    -- оформите решение программы в виде класса, который (должны быть выполнены все пункты):

    • решает задачу вызовом метода doIt() (без параметров)
    • в конструктор принимает значения M и N (и хранит их в виде собственных полей)
    • для вывода строки с указанным количеством единиц использует отдельный метод str(), работающий с ранее сохраненными параметрами.
  10. Выведете последовательность следующего вида:
    Пользователь вводил число $N$ - максимальное значение и число $M$, которое отвечало бы за длину возрастающего фрагмента, например для $M=4$:

    $ \underbrace{8 \;10 \;12 \;14}_{\text{четыре числа}} \;3 \underbrace{\;16 \;18 \;20 \;22}_{\text{четыре числа}} \;3 \; .... \;3 \;.... \;\text{и т.д.} $

    Заметьте. что в предыдущей задаче $M$ было зафиксировано $=2$:
    $ \underbrace{8 \;10}_{\text{два числа}} \;3 \underbrace{\;14 \;16}_{\text{два числа}} \;3 \; .... \;3 \;.... \;\text{и т.д.} $

    Как оформлять решение:

    1. Напишите класс, который будет принимать в конструкторе, параметры, которые нужны для вывода последовательности
    2. Пусть за вывод возрастающего фрагмента отвечает отдельный метод
    3. За вывод тройки тоже пусть отвечает отдельный метод
    4. Клиентский код, должен иметь возможность вывести последовательность в консоль вызовом одного метода doIt() без параметров

Что еще почитать

  1. Freepascal Class docs: https://wiki.freepascal.org/Class
  2. Castle Engine - Modern Pascal Classes: https://castle-engine.io/modern_pascal#_...
vedro-compota's picture

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

Часто под свойствами подразумеваются те же поля, для которых есть механизм контроля процессов чтения и записи

// ----------------------

type
  THelloWorld = class
  public
    whatToSay: string; // поле строкового типа
    procedure sayHello;
  end;

procedure THelloWorld.sayHello;
begin
 writeln(self.whatToSay); // поле класса в качестве аргумента процедуре печати
end;

var
  primer, primer2: THelloWorld; // переменная типа класса THelloWorld
begin
  primer := THelloWorld.create();
  primer2 := THelloWorld.create();
  primer.whatToSay := 'Привет Вася!!!';
  primer2.whatToSay := 'Привет!';

  primer.sayHello();
  primer2.sayHello();
end.
//-----------

_____________
матфак вгу и остальная классика =)

vedro-compota's picture

Пример неработающего кода с конфликтом имен между аргументом метода и именем поля


type
  Cat = class
  public
    name: string; 
    constructor create(name: string); 
    procedure sayHello();
  end;

var
  CatItem: Cat;
begin
  CatItem := Cat.create('Мурка');
  CatItem.sayHello();
end. 

_____________
матфак вгу и остальная классика =)