Учебник по программированию.

Первые шаги. Язык программирования PascalABC.

 

Предыдущий параграф Назад в содержание Следующий параграф


§28. Свойства. Видимость членов класса.

Свойства.

Ещё одно неоспоримое достоинство ООП это свойства. Свойства это не поля и не методы. Можно сказать, что это совмещение поля с методом. Работа со свойством похожа на работу с полем, однако при присвоении свойству какого либо значения происходит ещё вызов определённого кода, описанного как бы в данном свойстве.

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

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

 

  ...........

  Ob.Stroka:= Ob.Stroka+ch;

  Ob.Draw;

  ...........

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

 

  ...........

  Ob.Stroka:= Ob.Stroka+ch;

  ...........


Как видите, код стал «проще» на одну строку. Теперь что бы осознать достоинства использования свойств в полной мере, усложним ситуацию. Мы являемся разработчиками библиотеки классов объектов управления программ под операционную систему Windows. В данный момент нам необходимо разработать класс объекта ввода числа. В такой ситуации плюсом к предыдущим методам необходимо дописать метод, который будет фильтровать, вводимые символы, следить за количеством символов вообще, будет отслеживать, не ввели ли случайно вторую точку, а так же будет следить за знаком минус перед числом. Такой метод может носить название, например, SetStroka(ch), и для записи строки необходимо будет не напрямую обратиться к полю строка, а вызвать данный метод.

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

 

  ...........

  Ob.Stroka:= Ob.SetStroka(ch);

  Ob.Draw;

  Chislo:=StroToFloat(Ob.Stroka);

  ...........


Если при разработке данного класса мы будем использовать свойства, то программисту необходимо будет знать, только само свойство Stroka и больше ничего. При присвоении этому свойству какого-либо значения фильтрация данных, а так же перерисовывание изменений на экране будет происходить автоматически. Далее код, который должен будет написать программист для изменения и считывания данного свойства:


  ...........

  Ob.Stroka:=Ob.Stroka+ch;

  Chislo:=StrToFloat(Ob.Stoka);

  ...........


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

Теперь рассмотрим, как описывается свойство в коде программы:


property Stroka:string read GetStroka write SetStroka;


Для описания свойства используется специальное слово property, после которого идёт имя свойства, затем после двоеточия тип этого свойства. Затем используется обязательное слово read, после которого указывается имя функции, которая вызывается при считывании значения свойства. Далее идёт обязательное слово write и имя процедуры, которая вызывается при присвоении свойству какого-либо значения.

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

Имя процедуры так же может быть любым, но в его формировании, как правило, используется слово Set (установить) и имя свойства. В нашем случае SetStroka. Эта процедура должна быть методом класса и должна быть описана выше по тексту. В качестве параметра ей должна передаться переменная типа данного свойства. Данная процедура является процедурой записи свойства. Поэтому перед ней должно находиться слово write.

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

Наглядно всё это иллюстрирует следующий код программы:


type TOb = class

       sStr:string;

       procedure SetStroka(setStr:string);

         begin

           sStr:=setStr;

           writeln('Сработала процедура SetStroka.');

         end;

       function GetStroka:string;

         begin

           GetStroka:=sStr;

           writeln('Сработала функция GetStroka.');

         end;

       property Stroka:string read GetStroka write SetStroka;

     end;


var Ob:TOb:=new TOb;


begin

  Ob.Stroka:='Привет всем!';

  writeln(Ob.Stroka);

end.

_____________________________________________________________

Сработала процедура SetStroka.

Сработала функция GetStroka.

Привет всем!


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

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

Пример:


type TOb = class

       sStr:string;

       procedure SetStroka(setStr:string);

         begin

           sStr:=setStr;

           writeln('Сработала процедура SetStroka.');

         end;

       function GetStroka:string;

         begin

           GetStroka:=sStr;

           writeln('Сработала функция GetStroka.');

         end;

      property Stroka:string read sStr write sStr;

      end;


var Ob:TOb:=new TOb;


begin

  Ob.Stroka:='Привет всем!';

  writeln(Ob.Stroka);

end.

_____________________________________________________________

Привет всем!


В данном примере методы SetStroka и GetStroka так и не были вызваны.

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

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


program ObjectVvoda;

uses GraphABC;


type TOb = class

       x,y:integer;

       sStr:string;

       procedure Draw;

         begin

           SetBrushColor(clWhite);

           Rectangle(x,y,x+100,y+20);

           TextOut(x+100-TextWidth(sStr),y+2,sStr);

         end;

       procedure SetStroka(setStr:string);

         begin

           if (setStr[Length(setStr)] < '0')

                        or (setStr[Length(setStr)] > '9')

              then exit;

           sStr:=setStr;

           if Length(sStr)>12 then Delete(sStr,1,1);

           Draw;

         end;

       property Stroka:string read sStr write SetStroka;

       constructor (cx,cy:integer;s:string);

         begin

           x:=cx; y:=cy;

           sStr:=s;

           Draw;

         end;

     end


var Ob:TOb:=new TOb(10,10,'');


procedure KeyPress(ch:char);

begin

  Ob.Stroka:=Ob.Stroka+ch;

end;


begin

OnKeyPress:=KeyPress;

end.


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

Ещё одно замечание. Можно сказать, что класс TOb реализует работу поля ввода вывода. В данном подразделе было выбрано название TOb для того, что бы не называть одним и тем же словом разные вещи. В данном случае получилось бы, что сам объект называется полем и у этого объекта есть поля. Понимать такой учебный текст было бы сложнее.


Видимость членов класса.

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

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

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

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

В некоторых случаях бывает необходимо специально определить видимость члена класса. Таким примером может быть программа ObjectVvoda из предыдущего раздела. Изменение свойства Stroka происходит методом SetStroka, который производит фильтр вводимых символов и следит за их количеством. При этом можно изменить поле sStr напрямую, что приведёт к автоматическому изменению свойства Stroka. Если не позаботиться о фильтре этого поля, то в дальнейшей работе программы может появиться ошибка. Выход из ситуации прост надо запретить прямое обращение к полю sStr, т.е. сделать это поле не видимым.

Как раз об организации видимости членов класса и пойдёт речь в данном подразделе.

Всего существует четыре типа видимости:

  • public (открытый). Член класса может быть виден везде и всеми.
  • private (закрытый). Член класса может быть виден только методами этого же класса. В производном классе он виден уже не будет.
  • protected (защищённый). Член класса может быть виден методами этого класса и методами производных от этого классов.
  • internal (внутренний). В рамках данной книги этот тип рассматриваться не будет.

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

Для того, что бы присвоить члену тот или иной атрибут видимости, его нужно описать определённой секции. Другими словами при описании класса создаются разделы видимости, которые называются секциями, и каждый член описывается в той или иной секции. Секции видимости начинаются со слов public, private, protected или internal:


type TOb = class

     internal

       .........

     private

       .........

     protected

       .........

     public

       .........

     end;


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

Теперь, используя полученные знания, можем защитить программу ObjectVvoda от ошибок. Для этого опишем класс TOb в отдельном модуле, а поле sStr  в секции protected. Конечно, можно описать в секции private, но в таком случае, если мы захотим усовершенствовать класс, то в производном классе это поле мы уже не увидим. Далее код получившейся программы:


unit MTOb;

uses GraphABC;


type TOb = class

     private

       sStr:string;

     public

       .................

     end

end.

============================================================

program ObjectVvoda;

uses GraphABC,MTOb;


var Ob:TOb:=new TOb(10,10,'');


procedure KeyPress(ch:char);

begin

  Ob.Stroka:=Ob.Stroka+ch;

end;


begin

OnKeyPress:=KeyPress;

// Ob.sStr:='345';  Ошибка sStr не объявлен в типе TOb

end.


Обратите внимание, в закомментированной строчке находится код, который приведёт к ошибке во время компиляции.

Для того, что бы освоиться с атрибутами видимости, предлагаю поэкспериментировать с данной программой. Опишите самостоятельно производный класс от класса TOb. Опишите в нём метод, который напрямую обращается к полю sStr, например, он может просто выводить содержание этого поля на экран. Данный метод можно вызывать, например, в процедуре KeyPress. Откомпилируйте программу. Если всё правильно, то ошибок ни каких не будет. Затем поменяйте в модуле MTOb секцию protected на private и снова откомпилируйте программу.  Должна выйти ошибка, в строке, в которой ваш новый метод обращается к полю sStr.

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


В данном параграфе мы познакомились со свойствами и узнали о видимости членов класса.


Задачи.

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

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

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


Решение.

unit ClassKvadrat;

uses GraphABC;


type

  TKvadrat = class

    private

    PoleX,PoleY:integer;//Координаты

    procedure Draw;

      begin

        SetBrushColor(clRed);

        FillRect(PoleX,PoleY,PoleX+20,PoleY+20);

      end;

    procedure Clear;

      begin

        SetBrushColor(clWhite);

        FillRect(PoleX,PoleY,PoleX+20,PoleY+20);

      end;

    procedure SetX(xx:integer);

      begin

        if (xx<0) or (xx>620) then exit;

        Clear;

        PoleX:=xx;

        Draw;

      end;

    procedure SetY(yy:integer);

      begin

        if (yy<20) or (yy>460) then exit;

        Clear;

        PoleY:=yy;

        Draw;

      end;

    public 

    constructor (xx,yy:integer);

      begin

        x:=xx;

        y:=yy;

      end;

    property x:integer read PoleX write SetX;

    property y:integer read PoleY write SetY;

  end;

end.

=========================================================================

program IspolzovaniyeSvoystv;


uses GraphABC,ClassKvadrat;


var Kvadrat:TKvadrat:=new TKvadrat(100,200);


procedure KeyDown(key:integer);

begin

  case key of

    VK_Left :Kvadrat.x:=Kvadrat.x-4;

    VK_Right:Kvadrat.x:=Kvadrat.x+4;

    VK_UP   :Kvadrat.y:=Kvadrat.y-4;

    VK_Down :Kvadrat.y:=Kvadrat.y+4;

    VK_Enter:

      begin

        SetBrushColor(clWhite);

        FillRect(250,0,200,20);

        TextOut(250,0,'x='+IntToStr(Kvadrat.x)+' y='+IntToStr(Kvadrat.y))

      end;

  end;

end;


begin

  OnKeyDown:=KeyDown;

end.




Предыдущий параграф Назад в содержание Следующий параграф