В.Э. Карпов
Москва 2006
ОГЛАВЛЕНИЕ
1. ОБЩИЕ СВЕДЕНИЯ О ЯЗЫКЕ СМОЛТОК..
1.1. Объектно-ориентированные языки.
1.2. Основные понятия языка Смолток.
2. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА СМОЛТОК..
2.5. Иерархия классов. Наследование и полиморфизм.
2.6. Вызов метода через сообщения.
2.7. Классы Object и Class. Метаклассы..
2.9. Условные конструкции и булевские величины..
2.10. Циклические конструкции.
2.11. Создание новых объектов.
3. ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ СМОЛТОК..
3.1. Этапы разработки программы..
3.2. Системный генератор путей.
5. ВИРТУАЛЬНАЯ МАШИНА СМОЛТОК-80. БАЙТ-КОД..
5.2. Семантика и система команд.
Современные системы управления сложными объектами основаны на применении высокотехнологичных приемов и методов программирования, одним из которых является объектно-ориентированное программирование (ООП). Несмотря на то, что история ООП насчитывает уже более 20 лет, основные тенденции современного программирования остаются неизменными. Изучению общих принципов объектно-ориентированного программирования и получению навыков практического применения ООП при создании сложных программных комплексов и посвящено настоящее пособие. В первой части пособия рассматриваются основы программирования на, пожалуй, единственном "настоящем" на данный момент времени объектно-ориентированном языке – языке Смолток.
Язык Смолток – очень элегантен и прост с точки зрения синтаксиса (попробуйте найти еще один язык, в котором было бы всего три оператора). Тем не менее это одно из наиболее эффективных средств для моделирования сложных систем. На этом языке создаются надежные сверхсложные программные комплексы и не случайно, по некоторым оценкам, специалисты по языку Смолток являются сегодня наиболее высокооплачиваемыми и дефицитными. К сожалению, все это не относится к нашей стране, в которой язык Смолток не получил широкого распространения (возможно, из-за засилия продукции от Microsoft и Borland).
Вариантов, версий и клонов Смолтока к настоящему времени создано немало. Однако стандартом системы считается версия Смолток-80, и все существующие версии так или иначе имеют общие черты как с точки зрения внутреннего устройства, так и с точки зрения внешнего интерфейса. Изучая основы программирования на языке Смолток, мы будем использовать примеры из двух систем – системы Smalltalk Express и русскоязычной системы Смолток, созданной в ИПИ РАН. Переходы от англоязычной нотации конструкций языка к русскоязычной не должны вводить читателя в заблуждение. Пожалуй, это, напротив, может демонстрировать мобильность системы и укреплять во мнении, что суть объектно-ориентированного программирования не зависит от того, какими буквами обозначаются языковые конструкции.
Учебное пособие предназначено для студентов, обучающихся по специальности "Управление и информатика в технических системах" в соответствии с программой курса "Объектно-ориентированное программирование". На основании изучения этой дисциплины студент должен уметь применять приемы и методы ООП в своей практической деятельности, знать основные принципы организации сложных объектно-ориентированных систем, иметь представление об основных тенденциях развития современных информационных технологий. Этот курс является составной частью цикла специальных дисциплин, определяющих подготовку студентов в области современных информационных технологий и может быть полезен учащимся и других специальностей.
Язык Смолток начал разрабатываться с начала 70-х гг. в исследовательском центре автоматизации учрежденческого труда ПАРС в Пало-Альто (Калифорния, США) фирмой "Ксерокс" (Xerox). В основу его были положены идеи алгоритмического языка Simula, а также идеи Алана Кея (Alan Kay), суть которых сводилась к созданию однородной объектно-ориентированной среды программирования, основанной на малом числе взаимосвязанных понятий. В 1983 г. система Смолток была выпущена на рынок и поначалу применялась лишь на АРМ "Ксерокс", обладающем высокими функциональными возможностями. Первая версия была названа Смолток-72. Целью было создание системы программирования для эффективной связи человека с машиной. При этом старались максимально использовать высокие графические и коммуникационные возможности АРМ ALTO.
АРМ ALTO было создано еще в 1973. Для того времени это была поистине пионерская разработка. Позднее на его основе были разработаны система STAR фирмы Xerox и система McIntosh фирмы Apple. Характеристики системы ALTO:
· Графический дисплей с побитовым отображением в память. Разрешение - 600x800 точек. Независимое управление яркостью каждой точки. Представление как символов, так и графических изображений. Многочисленные редакторы и устройства машинной графики.
· Управление курсором с помощью манипулятора типа "мышь".
· Возможность подсоединения к системе быстродействующей сети Ethernet. Благодаря этому системы ALTO получили возможность обмена файлами и осуществления общего пользования дорогостоящими печатающими устройствами и накопителями. Система Ethernet стала основой для построения ЛВС.
Всем этим хотелось эффективно управлять. И решение этой задачи было возложено на Смолток. Итак, говоря о Смолтоке, следует понимать, что:
· Смолток – это язык, основанный на небольшом числе простых понятий, каждое из которых определяется в терминах остальных.
· Смолток – это язык программирования, являющийся основой создания мощной информационной системы, в которой пользователь может хранить, получать и обрабатывать информацию так, что система развивается и расширяется вместе с развитием представлений и требований пользователя к системе.
· Смолток – это графическая, интерактивная среда программирования, включающая поддержку визуального языка для представления каждого объекта.
· Смолток – это система, объекты которой обеспечивают функции, присущие операционным системам – управление памятью и файловой системой, управление внешними устройствами, управление и планирование процессов, компиляция и т.д.
В общем случае для выполнения вычислений необходима вычислительная процедура и данные к ней. В языках типа Алгола, Кобола или Фортрана, разработанных до начала 60-х гг., главную роль играли процедуры, снабжающиеся необходимыми данными при выполнении вычислений. Однако с расширением области применения ЭВМ все большую роль стали приобретать задачи, связанные с обработкой символов, формул, структур и т.п. Возрастало число программ, в которых требовалась обработка сложных структур данных. Например, при моделировании аппаратного обеспечения ЭВМ необходимо определять непосредственно в виде структур данных такие устройства, как процессор, память, устройства ввода-вывода и т.п., а также описывать потоки команд и данных между ними. Стало ясно, что процесс программирования можно облегчить, если ввести абстрактное понятие объекта вычисления и представлять эти объекты в виде программ, допускающих вычисления над ними. Однако существовавшие в то время языки программирования были малопригодны для решения этой задачи в силу ограниченности типов данных в них. Не спасали положения и разработанные позже процедурные языки типа PL или Pascal, в которых абстрактные типы данных были развиты достаточно хорошо.
Дальнейшее развитие языков программирования привело к появлению такого элемента языков программирования, как "объект".
Объект является основной конструкцией в ООП. В качестве объектов могут выступать программистские абстракции (числа, символы, файлы, и т.д.) или сущности моделируемой предметной области и их взаимосвязь. Объект содержит и процедурную, и понятийную части (говорят, что "объект=данные+процедуры"), причем для представления понятийной части используется аппарат абстрактных типов данных.
При этом объекты взаимодействуют друг с другом, посылая и принимая сообщения.
Представляется, что происхождение понятия "объект" восходит к работам М.Минского, который ввел в шестидесятых годах концепцию т.н. фреймов для представления знаний. Фрейм, согласно Минскому, - это то минимальное описание, которое еще сохраняет сущность описываемого явления. С программной точки зрения фрейм содержит как данные (слоты), так и процедуры (демоны), вызываемые при манипулировании данными.
Ключевые понятия языка Смолток - объект, класс, сообщение и метод.
Объект обладает собственной памятью (переменные экземпляра), где хранится информация о его свойствах и состоянии. Доступ к указанной информации имеет только сам этот объект.
Кроме того, объект обладает множеством процедур, описывающих поведение объекта – набором методов - операций для манипулирования хранящейся в объекте информацией.
Для активизации метода объекту посылается сообщение - аналог вызова процедур с данными в виде параметров в традиционных языках программирования процедурного типа.
С каждым объектом связан протокол сообщений, которые он понимает. Всякому сообщению из протокола соответствует реализующая его процедура (метод).
Метод состоит из операций над своими переменными экземпляра и из посылок сообщений другим объектам. В конечном счете метод должен выдать ответ на посланное объекту сообщение.
Объекты взаимодействуют друг с другом путем посылки сообщений. Для того чтобы послать сообщение некоторому объекту, указывается имя объекта - получателя сообщения, затем имя сообщения и, возможно, аргументы.
Синтаксис языка чрезвычайно прост. В нем всего три оператора: посылка сообщения, выдача ответа и присваивание значения переменной.
Вся сложность программы определяется смыслом посланных сообщений, поэтому главный оператор Смолтока - посылка сообщения.
Синтаксис этого оператора напоминает предложение естественного языка (подлежащее – сказуемое - дополнение):
объектПолучатель имяСообщения [объектыАргументы]
Например:
а – 3 объекту "а" посылается сообщение с именем "-" и аргументом 3;
5 factorial объекту 5 посылается сообщение с именем "factorial";
table inPos: 5 locate: 7 объекту "table" посылается сообщение с именем "inPos: locate:" и аргументами: 5 и 7.
Как уже говорилось, объект обладает свойствами, поведением и состоянием. Ему можно присвоить имя. Объекты с одинаковыми свойствами и поведением объединены в классы, причем каждый объект входит в один класс и называется экземпляром класса. Объекты одного класса обладают одинаковым протоколом методов. Экземпляры отличаются друг от друга именами и состояниями.
Таким образом, объекты имеют различные свойства, и эти свойства определяются тем, какие сообщения может получать объект. Описание совокупности сообщений, которые может получать объект, называют его спецификацией. Спецификация объекта, зарегистрированная в системе, представляет собой класс, и классы также являются объектами.
Существует множество объектов, соответствующих спецификации одного и того же класса. Таким образом, в окружающем мире существуют объекты, называемые классами, а также объекты - экземпляры классов:
Чтобы создать объект, классу посылается сообщение о создании нового экземпляра. Класс создает экземпляр с присущей всем объектам этого класса структурой - набором переменных экземпляра. Если нужно, он их инициализирует и выдает созданный экземпляр в качестве ответа на сообщение.
Классы в Смолтоке организованы иерархически, в виде дерева. Каждый класс имеет одного предка, называемого суперклассом, и может иметь несколько потомков, называемых подклассами. Всякий класс наследует переменные экземпляра и методы своего суперкласса. Кроме того, он может содержать новые переменные экземпляра и методы, может переопределять унаследованные.
Знание иерархии классов важно для понимания процесса выдачи ответа на сообщение. При получении сообщения объект ищет в своем классе метод с именем, совпадающим с именем сообщения. Если такого метода нет, объект ищет его в списке методов суперкласса и так далее. Когда метод обнаружен, он выполняется и выдает ответ. Если нигде, вплоть до корня дерева, метод не будет обнаружен, выдается ответ с сообщением об ошибке.
Как уже говорилось, посылка сообщения служит для активизации того или иного метода. Такая посылка содержит указание адресата - имя получателя сообщения, т.е. имя объекта, к которому посылается сообщение, и само сообщение, представляющее собой требование выполнить процедуру над объектом - получателем сообщения. При этом вызывается соответствующая программа (метод). Сообщение представляет собой имя сообщения и, возможно, несколько объектов-параметров, с которыми может манипулировать метод.
Сообщение определяет, какая требуется операция, но не определяет, каким образом эта операция должна выполняться. Только получатель – объект, которому посылается сообщение, определяет, как выполнить эту операцию. Выполняемая операция, если она возможна, рассматривается как встроенная способность объекта, которую можно единообразно активизировать посылкой ему сообщения.
Программа в системе ООП, таким образом, - последовательность сообщений к различным объектам.
При описании посылок сообщений используют так называемые селекторы.
Посылка сообщения с унарным селектором может записываться, например, следующим образом: stack pop. Здесь stack указывает на объект - получатель сообщения, а pop является унарным селектором, который и образует сообщение.
Посылка сообщения с бинарным селектором, например, арифметическое выражение, записывается в виде: x+y. Здесь x - получатель сообщения, а +y - сообщение. При этом y – единственный параметр, а "+" - бинарный селектор.
В общем случае бинарные селекторы должны оформляться специальными символами.
Есть возможность создания посылок с несколькими параметрами в сообщении. В выражении, реализующем эту возможность, используется селектор с ключевыми словами. Например, в посылке сообщения
pen move:east by:10
pen - получатель сообщения, move:east by:10 - само сообщение. Два ключевых слова move: и by: представляют собой селекторы ключевых слов, а east и 10 - значения параметров. В селекторе с ключевыми словами каждое ключевое слово должно заканчиваться двоеточием. Таким образом, имеется возможность создания посылок сообщений без использования специальных символов селекторов.
Сообщение анализируется получателем, и поэтому два синтаксически одинаковых предложения, будучи посланы разным получателям, могут вызвать разные действия. Например, согласно сообщению
cursor moveTo:locationA
курсор будет передвинут в положение A (locationA), а согласно сообщению
window moveTo:locationA
в положение A будет перемещен выделенный фрагмент экрана - окно. В некоторых случаях разбор сообщения может окончиться неудачей. Например, в сообщении table+5 к столу нельзя прибавить 5: объекту table посылать сообщение +5 некорректно. В этом случае системой фиксируется ошибка по невозможности выполнения.
Унарное сообщение имеет более высокий приоритет, чем бинарные. Приоритет может задаваться скобками.
Например, x - y size = x - (y size).
Замечание. Если попробовать выполнить сообщение
а) "1–2",
то большинство Смолток–систем выдаст сообщение об ошибке. То же произойдет и с сообщением
б) "1 –2"
Однако сообщения
в) "1– 2" и
г) "1+(–2)"
будут обработаны правильно.
Дело в том, что, например, в случае б) система обнаруживает два различных объекта – число "1" и число "–2", не находя имени сообщения. А ситуация а) обычно и вовсе считается неоднозначной.
Приведем некоторые примеры сообщений и их результатов:
Сообщение |
Тип |
Объект-адресат |
Имя сообщения |
Объект-параметр |
Результат |
1 + 2 |
Бинарное сообщение |
1 (целое) |
+ |
2 (Целое) |
Целое |
'привет' inPos: 5 |
Ключевое сообщение |
'привет' (строка) |
inPos: |
5 (Целое) |
Символ |
12 factorial |
Унарное сообщение |
12 (целое) |
factorial |
Нет |
Целое |
5 in:3 and:7 |
Ключевое сообщение |
5 (целое) |
in: and: |
3, 7 (Целое) |
Истина |
Если зарегистрировать в системе спецификацию экземпляра класса и спецификацию самого класса, то получим объявление класса.
Спецификация экземпляра состоит из объявления переменных экземпляра и объявления методов экземпляра. Спецификация самого класса также состоит из объявления переменных класса и объявления методов класса.
Экземпляры из одного класса распознают одни и те же сообщения и имеют одинаковую структуру собственной памяти. Объект состоит из данных (имеет собственную память) и методов. Собственная память объекта (или переменные экземпляра) служит для хранения информация о его свойствах и состоянии. Переменные экземпляра определены внутри экземпляра, и прямые ссылки на них других экземпляров не разрешены, т.е. доступ к указанной информации имеет только сам объект.
У каждого класса существует два типа методов:
· методы класса;
· методы экземпляра.
Методы класса используются, когда необходимо произвести какие-либо действия с целым классом, например, добавить к классу новый экземпляр. Сообщения к экземплярам инициализируют методы, оперирующие с конкретными экземплярами данного класса.
Объявление метода экземпляра состоит из схемы сообщения и тела метода. Схема сообщения состоит из имени сообщения и формальных параметров. Тело метода представляет собой программу, выполняемую по сообщению, посылаемому экземпляру этого класса или экземпляру подкласса.
Метод определяет реакцию объекта на данное сообщение, т.е. его поведение. Он состоит из операций над своими переменными экземплярами и из посылок сообщений другим объектам. В конечном счете объект возвращает ответ на посланное ему сообщение. Поскольку все объекты одного класса обладают одинаковым набором методов, последний хранится в одном месте - в самом классе.
Переменные класса являются глобальными: на них разрешена ссылка из всех экземпляров класса. Методы класса выполняются по сообщениям, посылаемым этому классу.
В языке Смолток существует пять типов переменных:
· временные переменные (temporary variable);
· переменные экземпляра (instance variable);
· переменные класса (class variable);
· глобальные переменные (global variable);
· общие переменные (pool variable).
Все они различаются временем своего существования и областью действия.
Временные переменные объявляются внутри методов, создаются в момент вызова метода и уничтожаются по возвращении из метода.
Например:
| path speed time |
path := 200.
speed := 5.
time := path/speed.
^time.
Переменные экземпляра хранятся в памяти внутри каждого экземпляра. Ссылки на них допускаются только внутри данного экземпляра, и они существуют до тех пор, пока существует сам экземпляр.
Переменные класса. Они доступны любому экземпляру этого класса по чтению и записи. Описываются эти переменные при объявлении свойств класса.
К переменным класса, глобальным и общим переменным имеется доступ более чем из одного объекта. Они зарегистрированы в общих словарях. Переменные классов регистрируются в словаре по имени Class, глобальные переменные зарегистрированы в словаре по имени SmallTalk ( Смолток), общие переменные зарегистрированы в словарях, объявляемых специальным образом.
Глобальные переменные. В системе есть каталог, содержащий все глобальные переменные программной среды. Он имеет имя SmallTalk (Смолток) и сам является глобальной переменной. Глобальные переменные доступны любому объекту по чтению и записи. Для определения новой глобальной переменной необходимо вычислить следующее выражение:
SmallTalk at: #<name> put: nil
(Смолток вПозиции: #<имя> разместить: nil)
Имя глобальной переменной начинается с прописной буквы. Символ "#" используется в языке для определения системного имени. Речь здесь идет о том, чтобы система Смолток, встретив указанное имя, не пыталась найти по нему существующий объект, а воспринимала бы его именно как последовательность символов, которая должна быть помещена в словарь.
Значение глобальной переменной можно изменить, присвоив ей новое значение. Например, введем новую глобальную переменную NewVar:
SmallTalk at: #NewVar put: nil.
NewVar := 3.1415.
Эти две строки можно заменить одной:
SmallTalk at: #NewVar put: 3.1415
Еще один способ ввода новых глобальных переменных – это использование возможностей интегрированной среды Смолток. Дело в том, что, когда интерпретатор встречает неизвестное имя, то он выдает запрос пользователю об объявлении новой глобальной переменной.
Общие переменные. Для чтения и записи в общие переменные в описании класса должны быть объявлены имена словарей, содержащих эти переменные.
Например:
class name HUMAN
superclass ANIMAL
instance variable names SEX
class variable names POPULATION
shared pools BiologicalFacts -- общие переменные
При таком объявлении словарь переменных BiologicalFacts будет общим для класса Human, и все переменные из словаря BiologicalFacts будут доступны по чтению и записи всем экземплярам класса Human.
В системе Смолток существуют переменные, которым нельзя присвоить значение. Это – так называемые системные псевдопеременные:
nil (нуль) ссылка на пустой объект;
self (сам),
super (супер) используются в качестве адресатов при обращении из метода какого-либо объекта к самому себе или к суперклассу соответственно.
Применение данных системных псевдопеременных в левой части оператора присваивания запрещено.
Кроме того, существуют обычно еще две псевдопеременные: true (истина)false (ложь) - ссылки на объекты, представляющие "истину" и "ложь". Эти переменные не являются системными (они определяются в классах явным образом), однако являются очень важными для системы, позволяя организовывать условные и циклические конструкции. Таким образом, эти переменные можно назвать псевдопеременными лишь условно.
Замечания
1. Работая с переменными, необходимо учитывать то, как Смолток реализует операцию присваивания. Дело в том, что Смолток работает только с указателями на объекты. Это позволяет избегать какой бы то ни было типизации переменных, т.е. понятие типа переменной в языке лишено смысла. Имеет смысл лишь говорить о том, на какой объект указывает переменная.
Поэтому можно реализовывать последовательности присваиваний переменной значений указателей на различные объекты:
|a b| -- Введенные переменные указывают на
-- nil - экземпляр класса UndefinedObject (см.ниже)
a := 1. -- a указывает на экземпляр класса МалоеЦелое
a := "qwerty". -- a указывает на экземпляр класса МалоеЦелое
a := b. -- a вновь указывает на nil
2. Зачем необходимо объявлять временные переменные. Это далеко не праздный вопрос. Дело в том, что, встретив объявление временной переменной, Смолток порождает экземпляр специального класса, который называется НеопределенныйОбъект (UndefinedObject). У этого класса есть только один экземпляр – нуль (nil). Это можно продемонстрировать, выполнив следующую последовательность сообщений.
|a|
a class. -- Результатом будет выдача имени класса «UndefinedObject»
a isNil. -- Проверка того, что переменная ссылается на nil
В Смолтоке существует возможность введения иерархии классов с целью получения более компактных описаний и облегчения понимания отношений между классами.
Как уже говорилось, каждый класс имеет одного предка (суперкласс) и может иметь несколько потомков, называемых подклассами.
Классы в Смолтоке организованы иерархически в виде дерева. Иерархия классов устанавливается объявлением суперкласса внутри описания класса. Если объявлено, что класс A является суперклассом класса B, то это означает, что спецификация класса B включает в себя как спецификацию класса B, так и спецификацию класса A. При этом говорят, что класс B наследует спецификации из класса A (переменные экземпляра и методы своего суперкласса).
Кроме того, он может содержать новые переменные экземпляра и методы, может переопределять унаследованные.
Например:
В данном примере класс A является суперклассом класса B. Реализация класса B будет содержать в качестве переменных класса переменные a, b c, в качестве переменных экземпляра – переменные h, x, y, в качестве методов класса - процедуры f1, f2, f3, а в качестве методов экземпляра - процедуры g1, g2, g3.
Одним из положительных эффектов иерархии классов является возможность объединения совпадающих по лексике объявлений методов, встречающихся в разных местах. При этом программы становятся более ясными и легко читаемыми.
Например, пусть имеются классы IntegerFloat, описывающие целые и действительные числа соответственно. Если при этом создать класс Number, являющийся суперклассом для них обоих, то те методы, которые определены в классах IntegerFloat и при этом совпадают лексически, могут быть определены в классе Number, после чего их можно удалить как из класса Integer, так и из класса Float. Например, метод ³ можно описать в виде
³ y
^(self<y) not
и включить в класс Number (напомним, что здесь ключевым словом self обозначена псевдопеременная, указывающая на получателя сообщения вида ³e). Сообщение <y вызывает два разных метода в зависимости от того, на что указывает псевдопеременная self: на экземпляр класса Integer или на экземпляр класса Float:
Во многих случаях иерархия классов позволяет также легко объявлять новые классы. Это связано с тем, что часто новые классы можно создавать как подклассы уже созданных классов с введением в них лишь особенностей, присущих данному подклассу. Рассмотрим, например, класс "Поток", используемый для ввода-вывода информации. Будем считать, что уже определен класс WriteStream, используемый только для выходного потока. При этом класс ReadWriteStream, используемый и для выходного, и входного потоков, можно создать как подкласс класса WriteStream, определив при этом еще лишь метод чтения.
В языке Смолток сообщения анализируются их получателем. При этом в зависимости от того, посылается ли (лексически) одно и то же сообщение экземпляру различных классов, результаты будут, вообще говоря, различными.
Например, при печати может использоваться единственное сообщение print. Оно используется как для печати экземпляров класса String, так и для экземпляров класса Integer. Говорят при этом, что сообщение print - сообщение родового (generic) класса. При печати экземпляра класса Integer сообщение X print дает вывод числа, а при печати экземпляра класса String сообщение S print выводит строку. Это происходит из-за того, что каждый объект анализирует сообщение по-разному.
В каждом классе имеется словарь методов (или протокол). Он представляет собой таблицу имен сообщений и тел процедур, вызываемых этими сообщениями. Поиск ведется по имени сообщения, и если в этой таблице это имя имеется, то результатом поиска будет тело процедуры.
При посылке сообщения определяется прежде всего класс получателя. Происходит проверка того, имеется ли имя сообщения в словаре методов этого класса. Если оно имеется, то выполняется соответствующее тело процедуры. Если такого имени в словаре нет, то определяется суперкласс этого класса, и в словаре этого суперкласса снова ищется имя сообщения. Если его и там не оказывается, то ищется его суперкласс, и эта операция повторяется до тех пор, пока система не доходит до класса, не имеющего суперклассов (класс ObectОбъект). Если и в нем имя сообщения не будет найдено, то выдается диагностическое сообщение о том, что данный получатель не может разобрать это сообщение:
В языке Смолток классы одновременно являются объектами. У классов есть переменные и классам также можно посылать сообщения, например, сообщение new (НовыйЭкземпляр) создания экземпляра. Следовательно, будучи объектами, они должны принадлежать некоторому классу.
Все классы, будучи объектами, принадлежат классу Класс (Class). Класс Объект (Object) тоже является экземпляром класса Класс, который, в свою очередь, является подклассом корневого класса Объект.
Условно связь экземпляров класса и связь подклассов можно изобразить так:
Мы уже говорили о том, что сообщение, посылаемое экземпляру, анализируется его классом. Следовательно, сообщение, посылаемое классу, должно анализироваться его классом (классом класса). Этот класс классов называется метаклассом. Таким образом, получается единый унифицированный механизм вызова сообщений, обеспечивающий ясность программ и логическую завершенность системы в целом.
Классам присваиваются имена, а метаклассам – нет. При обращении к метаклассу ему посылается сообщение class (класс). Например, пусть есть класс целых чисел Integer и метакласс Integer class. Класс Integer является единственным экземпляром метакласса Integer. При этом некоторые из методов класса могут быть зарегистрированы в словаре методов метакласса, а методы экземпляра - в словаре методов класса. Имя сообщения, посланного классу, можно отыскать в словаре сообщений метакласса с помощью тех же механизмов, которые применяются для общего метода поиска:
Между метаклассами также существует иерархия, образованная тем же отношением "суперкласс - подкласс". Иерархия классов и иерархия метаклассов изоморфны друг другу:
Классы КлассМетаклас расположены в иерархии недалеко от корня – обычно они являются подклассами класса Поведение (Behavior). Классы эти весьма нетривиальны. Они отвечают за инициализацию, поиск методов и переменных, за загрузку и выгрузку, за компиляцию и выполнение и многое другое.
Блок (block) - это, формально говоря, описание отложенной последовательности действий. Иными словами, блок-сообщение – это группа сообщений, заключенная в квадратные скобки. Блок – объект специального вида (экземпляр класса Context (Контекст)), вычисление которого (т.е. выполнение списка внутренних операторов) производится путем посылки к нему сообщения - value (значение). Если оператор блока находится внутри программы, то создается автоматически экземпляр класса Context, и в нем накапливается вся необходимая информация для работы блока, однако оценка содержимого оператора блока при этом не выполняется. Чтобы она выполнялась, экземпляру класса Context и посылается сообщение value.
Например:
| x |
x := [ 'строка' inPos: 5 ]
^x. -- результат - экзКонтекст
Заменив последнюю строку на "^x value", получим результат "к".
Блок может иметь аргументы. В этом случае сообщение для его вычисления будет ключевым:
[ :<аргумент>| <сообщения> ] value: <значение аргумента>
Например:
^[:i | i+5] value: 5 -- 10
^[:i :j| i*j ] value:5 value: 3 -- 15
Аргументы блока описываются в начале блока (а не в начале программы как все остальные переменные). Символ '|' в данном случае - это символ окончания декларации параметров блока.
Например:
[ :i :j | i + j ]
i , j - параметры блока
| - символ окончания декларации параметров
i + j - "тело" блока
Аргумент блока (block argument) - это параметр, поставляемый при выполнении конкретного блока.
Например:
[:i | i+5 ] value:15 -- i - аргумент блока, принимающий значение 15
Простота и лаконичность Смолтока особенно четко проявляется в том, что в этом языке отсутствуют такие необходимые, на первый взгляд, операторы, как условные и циклические. Оказывается, эти операторы и в самом деле излишни. Дело не в том, что они не нужны, а в том, что их можно сконструировать самостоятельно. Начнем с условных операторов.
а) Без сравнения объектов в Смолтоке, конечно, не обойтись. Конструкции вида
3 > 4
#(1 2 3 )= #(1 2 3)
'привет' >'пока'
5=2+3
используются в программистской практике повсюду. Результатом выполнения этих операция являются либо истина, либо ложь.
б) Условные выражения служат для обработки результатов сравнения.
Условная конструкция имеет вид:
< условие >
если условие истинно: [ <блок выражений> ]
если условие ложно: [ <блок выражений> ]
На языке ООП эта конструкция записывается таким образом:
<объект-адресат класса True (Истина) или False (Ложь)>
ifTrue: <объект-параметр>
ifFalse: <объект-параметр>
В данном случае <объект-параметр> представляет собой блок сообщений (напомним, что блок сообщений - это группа сообщений, заключенная в квадратные скобки).
Естественно, возможны и следующие конструкции:
< условие > ifTrue: [ <группа сообщений> ].
< условие > ifFalse: [ <группа сообщений> ].
Для реализации этих конструкций существует класс булевских величин Boolean. Класс Boolean реализует значения истинности. Он имеет два подкласса: класс True и класс False. Значение true является единственным экземпляром класса True, а false – единственным экземпляром класса False. Эти конструкции введены для обеспечения возможности описания условных операторов и условных конструкций:
Классы Boolean, TrueFalse являются надстраиваемыми (или конструируемыми) классами. Поэтому весьма полезно продемонстрировать их внутреннее устройство, которое столь просто, что далее можно обойтись без комментариев (еще один пример того, что хорошие объектно-ориентированные программы должны быть легко читаемыми).
Класс Boolean (Логический)
class name Boolean
superclass Object
class methods
"Оба следующих метода выдают ошибку: создание новых логических значений невозможно, так как значения 'истина' и 'ложь' уникальны"
new
^self неправильноеСообщение
new: intVal
^self неправильноеСообщение
instance methods
занестиВ: экзПоток
"Добавить последовательность символов приемника к заданному потоку, из
которого приемник может быть создан заново"
self печататьНа: экзПоток
малаяКопия
"Выдает копию приемника, разделяющую с ним переменные экземпляра. Так как есть только одно значение 'истина' и одно значение 'ложь', выдается приемник"
^self
печататьНа: экзПоток
"Добавить представление приемника в символьном виде к заданному потоку"
экзПоток поместитьВсеПоследующие:
(self ifTrue: ['истина'] ifFalse:['ложь'])
полнаяКопия
"Выдает копию приемника с малыми копиями каждой переменной экземпляра. Так как есть только одно значение 'истина' и одно значение 'ложь', выдается приемник"
^self
Класс True (Истина)
class name True
superclass Boolean
-- class methods отсутствуют
instance methods
& boolInstance
^boolInstance
| boolInstance
^true
ifTrue: Block
^Block value
ifTrue: Block1 ifFalse: Block2
^Block1 value
ifFalse: Block
^nil
ifFalse: Block1 ifTrue: Block2
^Block2 value
and: Block – И с блоком
^Block value
or: Block -- ИЛИ с блоком
^true
xor: boolInstance -- исключающее или
^boolInstance not
not
^false
equal: boolInstance -- эквивалентность приемника и аргумента
^boolInstance
Класс False (Ложь)
class name False
superclass Boolean
-- class methods отсутствуют
instance methods
& boolInstance
^false
| boolInstance
^boolInstance
ifTrue: Block
^nil
ifTrue: Block1 ifFalse: Block2
^Block2 value
ifFalse: Block
^Block value
ifFalse: Block1 ifTrue: Block2
^Block1 value
and: Block -- И с блоком
^false
or: Block -- ИЛИ с блоком
^Block value
xor: boolInstance -- исключающее ИЛИ
^boolInstance
not
^true
equal: boolInstance -- эквивалентность приемника и аргумента
^boolInstance not
Например:
| maximum a b |
a:= 5 sqr.
b:= 5 factorial.
a>b ifTrue: [maximum:= a] ifFalse :[maximum:= b].
^maximum
Создадим на основе данной программы метод с именем 'max'. Для начала определимся со спецификацией.
1. Объект-адресат - это целое число. Следовательно, данный метод будет принадлежать классу целых чисел.
2. Объект-параметр - целое число.
3. Результат - это тоже целое число.
Значит, сообщение будет выглядеть следующим образом:
<экзЦелоеЧислоМакс> := <экзЦелоеЧислоA> maximum: <экзЦелоеЧислоB>
В Смолтоке любой метод имеет следующий вид:
имяСообщения
" комментарии "
| временные переменные |
выражения
В нашем случае:
max: arg -- имя метода с параметром
"вычисление максимума( a^2, b!)"
| a b maximum |
a:= self sqr. -- псевдопеременная 'self' -
-- значение объекта-адресата
b:= arg factorial.
a>b
ifTrue: [maximum:= a]
ifFalse: [maximum:= b].
^maximum
Теперь можно добавить данный метод в класс Integer (суперкласс Number - Величина). Примеры использования метода:
5 max: 4
10 max: 7
и т.п.
В Смолтоке нет циклических операторов. Циклы приходится "конструировать" вручную. Ниже приведена таблица с некоторыми возможными циклическими конструкциями. Обычно именно эти или подобные конструкции предлагаются разработчиками пользователям в виде уже реализованных методов.
<число> timesRepeat: [ блок сообщений ] |
Повторить блок заданное <число> раз |
[блок условия] whileFalse: [ блок сообщений ] |
Пока условие ложно, выполняются сообщения |
[блок условия] whileTrue: [ блок сообщений ] |
Пока условие истинно, выполняются сообщения |
<число1> TO:<число2> BY:<шаг> DO: [ :<переменная> | блок сообщений ] |
Выполнить блок сообщений, пока значение <переменной>, изменяющее свое значение с заданным шагом, принадлежит промежутку (число1,число2) |
<объект> выполнить: [ :<переменная> | блок сообщений ] |
Значение <переменной> присваивается последовательно элементам <объект> |
<объект> выбрать: [ :<переменная> | <условие> ] |
Изменяет <объект>, удаляя элементы, не удовлетворяющие условию |
<объект> исключить: [ :<переменная> | <условие> ] |
Изменяет <объект>, удаляя элементы, удовлетворяющие условию |
<объект> собрать: [ :<переменная> | сообщение] |
Заменяет каждый элемент <объекта> на результат выполненного сообщения |
Интересно посмотреть, как выглядят "изнутри" некоторые из этих методов.
а) Условия whileTrue (покаИстина), whileFalse (покаЛожь):
whileTrue: BLOCK
"Выполняет заданный блок до тех пор, пока приемник не
будет иметь значение 'ложь'. Выдает нуль"
self value ifTrue: [BLOCK value. self whileTrue: BLOCK ].
^nil!
whileFalse: BLOCK
"Выполняет заданный блок до тех пор, пока приемник не
будет иметь значение 'истина'. Выдает нуль"
self value ifFalse: [BLOCK value. self whileFalse: BLOCK ].
^nil!
Пример программы, копирующей файл SOURCE.TXT в файл DEST.TXT:
| ввод вывод |
ввод:= Файл полноеИмя: 'SOURCE.TXT'.
вывод:=Файл полноеИмя: 'DEST.TXT'.
[ввод вКонце] whileFalse: [ вывод поместитьСледующий: ввод следующий ].
ввод закрыть.
вывод закрыть.
б) Простые циклы (выполнить заданный блок столько раз, сколько задано приемником):
timesRepeat: Block
| n |
n := self.
[n > 0] whileTrue: [n := n - 1. Block value]
Например:
3 timesRepeat: [ Черепашка переместитьНа: 100; повернутьНа: 120 ]
в) Простые итерации (арифметические циклы) можно определить в виде следующего метода:
TO:limit BY:step DO:block
self<=limit ifTrue: [block value: self. (self+step) TO: limit BY step DO: block]
Например:
sum := 0.
1 to:100 by:2 do: [:each | sum := sum + each].
Чтобы создать объект, классу посылается сообщение о создании нового экземпляра. Класс создает экземпляр с присущей всем объектам этого класса структурой - набором переменных экземпляра.
Если нужно, он их инициализирует и выдает созданный экземпляр в качестве ответа на сообщение.
Например: x := Array new: 10. (создается новый объект, с именем x, класса Array, размера 10).
Для того чтобы обратиться к нужному методу экземпляра класса, необходимо, чтобы уже существовал экземпляр класса.
Например, при выполнении следующего выражения ('выполнить')
ДемоКласс запустить.
компилятор выдает ошибку, т.к. здесь сообщение посылается классу, а в протоколе сообщений класса такого метода нет. Метод 'запустить' находится в протоколе сообщений экземпляра класса. Поэтому необходимо сначала создать экземпляр класса:
ДемоКласс новыйЭкземпляр запустить.
Возможно создание нового экземпляра в результате выполнения некоторых сообщений. Например:
'Привет,',' мартышка' (создается новый экземпляр класса "Строка", имеющий значение 'Привет, мартышка');
1 / 2 (объект-адресат и объект-параметр экземпляры класса "Целое", а результат - экземпляр класса "Дробь");
1 > 2 (результат - экземпляр класса False).
При программировании на языке Смолток необходимо прежде всего решить, каким образом будет посылаться сообщение, т.е. ответить на вопросы:
- Что будет являться объектом - адресатом?
- Что будет являться объектом - параметром?
- Что будет являться результатом?
Иными словами, необходимо разработать спецификацию метода.
Программирование в системе Смолток состоит из следующих этапов, обычно выполняемых с помощью окна просмотра иерархии классов (кроме П.4):
1. Создаются новые классы объектов, необходимых для решения задачи некоторой прикладной области.
Для каждого нового класса определяется место в иерархии прикладных классов:
· его суперкласс;
· возможные подклассы.
Для каждого класса определяется протокол сообщений, понимаемых экземплярами этого класса, и набор переменных, составляющих внутреннюю структуру экземпляров.
В системе на этом этапе создаются так называемые "абстрактные классы". Этот этап соответствует анализу предметной области.
2. Алгоритм решения задачи записывается на языке взаимодействия объектов:
· объекты порождаются,
· инициализируются,
· обмениваются сообщениями с другими объектами в целях получения результата решения задачи.
Для этого некоторые сообщения из протокола классов верхних уровней иерархии "реализуются" с помощью методов, состоящих из объектов и сообщений классов более низких уровней. Этот этап соответствует проектированию прикладной системы.
3. Новые классы реализуются с помощью имеющихся классов системы Смолток. Для каждого класса находится место в иерархии классов системы Смолток. Для каждого сообщения из протокола реализуется метод, состоящий из программы - реакции на данное сообщение с выдачей результирующего объекта.
Метод обычно строится из обмена сообщений между переменными экземпляра данного объекта и, возможно, другими объектами. При создании и инициализации переменных экземпляра новых классов выбираются объекты уже реализованных классов более низкого уровня.
Этот этап соответствует программированию задачи.
4. Вновь реализованные классы и методы тестируются сначала отдельно, потом совместно с другими классами и методами. Для этого используются рабочие окна, где набираются тексты тестовых примеров; инспекторы, позволяющие узнать статическое состояние объекта в произвольной точке программы; отладчик, позволяющий проследить за динамикой получения ответа на сообщение.
5. Созданные и отлаженные классы предметной области могут быть повторно использованы для решения сходных задач из этой области, а также для построения новых классов других предметных областей.
Одной из основных составных частей системы Смолток является библиотека классов. В нее входят классы, которые представляют традиционные средства программирования:
· арифметические и символьные вычисления;
· обработка файлов;
· обработка сложных структур данных;
· графика.
Поскольку сама система написана на языке Смолток, в библиотеку классов входят также и средства объектно-ориентированного программирования (компилятор, отладчик, инспекторы объектов, диспетчеры окон).
Классы в библиотеке расположены в порядке иерархии, в которой подклассы наследуют структуру и поведение суперклассов.
Независимо от того, с какой реализацией системы мы имеем дело, основные принципы интерфейса Смолтока остаются сходными. Для доступа к библиотеке нужно открыть окно "Просмотр иерархии классов" (выбрав в системном меню пункт "просмотр классов").
Обычно окно состоит из пяти рамок. В левой верхней списковой рамке представлена иерархия классов. Место класса в иерархии обозначается величиной отступа имени класса от левого края рамки.
Если за именем класса стоит многоточие, это означает, что класс имеет подклассы, которые скрыты с целью экономии места в списке. Раскрыть их можно выбором пункта "скрыть/показать" в меню рамки.
В правой верхней рамке располагается список методов выбранного класса. В зависимости от выбора одной из маленьких рамок, расположенных под ней, это будут или методы экземпляра, или методы класса.
В самой нижней, текстовой рамке будет показано определение выбранного класса, как оно записывается на языке Смолток.
Если выбрать один из методов в списке методов, то в текстовой рамке появится исходный текст этого метода:
· Категории - группы классов, выделенные для некоторых областей применения.
· Классы - классы системы Смолток.
· Категории методов - группы методов классов или методов экземпляров, выделенные для удобства по признаку применения или по иному свойству.
· Методы - методы системы Смолток.
Рекурсия - это основной механизм программирования итерационных конструкций языка Смолток. Рассмотрим реализацию метода Fibo класса Integer.
-- Фибоначчи
-- выдать N число Фибоноччи, где N - объект-адресат
-- Числа Фибоначчи задаются следующей последовательностью:
-- x1=1, x2=1, xi=xi-1+xi-2
Fibo
self<3 ifTrue: [ ^1 ] ifFalse: [ ^ (self - 1) Fibo + (self - 2) Fibo ]
Теперь можно выполнить следующее выражение:
#(1 2 3 4 5 6 7 10 20) собрать: [ :m| m Fibo ]
Здесь сообщение "собрать" приводит к созданию нового набора, содержащего в качестве элементов результаты выполнения блока.
В качестве следующего примера рассмотрим программу для решения известной головоломки "Ханойские башни".
Постановка задачи. Имеется 3 штырька, на штырьке #1 расположены кольца разного диаметра в порядке убывания их размера снизу вверх. Требуется переложить кольца со штырька #1 на штырек #2, пользуясь штырьком #3 как промежуточным, таким образом, чтобы кольца на штырьке #2 располагались в порядке убывания их диаметра и чтобы в процессе перекладывания кольцо большего диаметра никогда не накладывалось на кольцо меньшего диаметра.
1 Этап. Спецификация метода
a) объект-адресат - это целое число (количество колец). Следовательно, данный метод будет принадлежать классу целых чисел.
b) объекты-параметры - символы '1', '2', '3' (информация: с какого штырька на какой нужно переложить кольцо и какой использовать как дополнительный)
c) результат - вывод на экран информации о последовательности выполнения данной задачи.
Следовательно, вызов метода будет осуществляться следующим образом:
10 ханойC: '1' на: '2' через: '3'
2 Этап. Реализация метода ханойС: x на: y через: z.
Данная задача решается рекурсивно.
a) Базис. Если число колес равно 1, то нужно просто переложить это кольцо c х на y.
b) Если число колес равно К, то нужно переложить со штырька х на штырек z K-1 колец, а затем переложить самое большое K-е кольцо на y и переложить все остальные K-1 колец на y.
ханойС: x на: y через: z
"головоломка 'Ханойские башни'"
self := 1
ifTrue: [ СистемнаяИнформация поместитьВсеПоследующие:
'Переложить со штырька ',x,' на штырек ',y;
символВК. ]
ifFalse: [ (self - 1) ханойС: x на: z через: y.
СистемнаяИнформация поместитьВсеПоследующие:
'Переложить со штырька ',x,' на штырек ',y;
символВК.
(self - 1) ханойС: z на: y через: x ]
Теперь можно добавить полученный метод в класс Integer (Целое) и выполнить программу для числа колец, равного, например, 10:
10 ханойС: '1' на: '2' через: '3'
Реализация этого метода в системе Smalltalk Express выглядит следующим образом:
hanoiFrom: x to: y by: z result: aStream
"головоломка 'Ханойские башни'"
self == 1
ifTrue: [ 'Переложить со штырька ' printOn: aStream.
x printOn: aStream.
'на штырек ' printOn: aStream.
y printOn: aStream.
(10 asCharacter) printOn: aStream.]
ifFalse: [ (self - 1) hanoiFrom: x to: z by: y result: aStream.
'Переложить со штырька ' printOn: aStream.
x printOn: aStream.
'на штырек ' printOn: aStream.
y printOn: aStream.
(10 asCharacter) printOn: aStream.
(self - 1) hanoiFrom: z to: y by: x result: aStream.]
Пример работы (пункт Show – "Показать"):
|aStream|
aStream := WriteStream on: String new.
10 hanoiFrom: 1 to: 2 by: 3 result: aStream.
aStream contents.
И, для полноты картины, приведем пример этой программы на языке Паскаль:
Program Han;
const Step : integer = 0; {Глобальный счетчик итераций- шагов}
procedure Hanoi(N : integer; X, Y, Z : char);
{ X-откуда, Y-куда, Z-через}
begin
if N=1 then
begin
Step:= Step +1;
writeln(Step:3,' Переложить со штырька ',X,' на штырек ',Y);
end
else
begin
Hanoi(N-1, X,Z,Y);
Step:= Step +1;
writeln(Step:3,' Переложить со штырька ',X,' на штырек ',Y);
Hanoi(N-1, Z,Y,X);
end;
end;
var N : integer;
begin
writeln('ХАНОЙСКИЕ БАШНИ');
write('Введите число колец: ');
readln(N);
Hanoi(N,'A','B','C');
end.
Основной областью применения языков ООП является моделирование - представление знаний в ЭВМ о процессах и явлениях окружающего мира.
Для адекватного моделирования необходима реализация механизма, обеспечивающего параллельную работу системы. В Смолтоке такая работа обеспечивается использованием трех механизмов: параллелизма (parallelism), планирования (scheduling) и синхронизации (synchronisation).
Параллелизм обеспечивает синхронное выполнение двух и более процессов (программ), планирование - управление распределением процессорного времени между этими независимо выполняющимися программами, а синхронизация - правильный обмен информацией между отдельными выполняющимися программами. В языке Смолток для реализации этих функций имеются три класса: Process, ProcessSchedulerSemaphore, которые и выполняют работу по организации параллелизма, планирования и синхронизации соответственно.
Параллелизм. Класс Process реализует механизм распараллеливания. В общем случае при создании экземпляра класса Process оператору блока посылается сообщение fork. Например, если программа computeFunc, вычисляющая значение некоторой функции, определяется как метод экземпляра f некоторого класса и выполняется
[f computeFunc] fork,
то создается процесс вычисления функции, начинающий свою работу независимо от других процессов. Поскольку программа, породившая этот процесс, продолжает по-прежнему работать, одновременно с вычислением функции может продолжаться и другая работа в порождающей программе. Основными сообщениями, посылаемыми другими процессами, являются следующие:
newProcess создать новый процесс, но не выполнять его;
resume выполнить остановленный процесс;
suspend остановить процесс;
terminate закончить процесс.
Например, если выполнить выражение
FProcess := [f computeFunc] newProcess,
то сформируется новый процесс, однако он будет находиться в состоянии ожидания и выполняться не будет. Выполнение его начнется только после того, как будет выполнено сообщение:
FProcess resume.
Диспетчеризация. Система Смолток (Смолток-80) построена так, что она может работать только на однопроцессорной машине. Поэтому при наличии нескольких работающих одновременно процессов могут возникать конфликтные ситуации (например, при одновременном обращении к внешним устройствам или к файлам).
Для предотвращения конфликтных ситуаций необходима реализация методов организации вычислительных процессов (диспетчеризация или планирование). Для этого создается класс ProcessScheduler. Этот класс имеет единственный экземпляр, указателем на который является глобальная переменная Processor.
Алгоритм диспетчеризации реализуется через простую систему приоритетов (priority system). Каждому процессу назначается приоритет. Если процесс, выполняющийся в данный момент, останавливается или завершается, то из процессов, находящихся в состоянии ожидания, выбирается и запускается тот, который обладает наивысшим приоритетом. Чтобы остановить выполняющийся в данный момент процесс, посылается сообщение suspend либо внутри самого выполняемого процесса вызывается процедура с наименованием yield. Работает yield так: если имеется остановленный процесс с приоритетом, равным приоритету процесса, который выполняется в настоящий момент, то он запускается. Если же такого процесса нет, то продолжается выполнение текущего процесса. Приоритет вычисляется системой, и если необходимо программу вычисления функции из предыдущего примера запустить как фоновую задачу, то это запишется в следующем виде:
[Float computeFunc] forkAt:Processor userBackgroundPriority
Пример сеанса работы (рабочее окно Smalltalk Express):
|aStream n block1 block2 w nfibo flimit|
w := TextWindow windowLabeled: 'Fibo' frame: (100@100 extent: 400@200).
flimit := 25.
aStream := WriteStream on: String new.
block1 := [ n := 0. [n<flimit] whileTrue: [n:=n+1. nfibo := ( n fibo).
nfibo printOn: aStream. ' ' printOn: aStream.
w nextPutAll: ' P-1 '.
w nextPutAll: (n radix:10).
w nextPutAll: ' : '.
w nextPutAll: (nfibo radix:10); cr.].
w nextPutAll: 'Process 1 done.'; cr.].
block2 := [ n := 0. [n<flimit] whileTrue: [n:=n+1. nfibo := ( n fibo).
nfibo printOn: aStream. ' ' printOn: aStream.
w nextPutAll: ' P-2 '.
w nextPutAll: (n radix:10).
w nextPutAll: ' : '.
w nextPutAll: (nfibo radix:10); cr.].
w nextPutAll: 'Process 2 done.'; cr.].
block1 forkAt: (ProcessScheduler new lowUserPriority).
block2 forkAt: (ProcessScheduler new lowUserPriority).
Результаты работы отображаются как в окне, так и в потоке
aStream (aStream contents).
Процессы можно породить и вызовами
Processor fork: block1.
block1 fork.
Приоритеты процессов в Smalltalk Express определяются методами класса ProcessScheduler
userPriority
highUserPriority
lowUserPriority
topPriority
Методы эти просты и выглядят (в SmallTalk Express, например) так:
userPriority
^4!
highUserPriority
^6!
lowUserPriority
^2!
topPriority
^7!
Синхронизация. Методы управления и синхронизации процессов (обрабатывающих единиц), находящихся между собой в конкурентных отношениях, делятся на две большие группы. Одна из них включает в себя методы, основанные на общих переменных, вторая - на посылке заявок. Метод семафоров Дейкстры, рассматриваемый ниже, относится к методу управления и синхронизации, основанному на общих переменных. Для реализации семафоров Дейкстры используют переменные семафоров и функции P и V.
Пусть sem - переменная семафора. Ее значения - целые неотрицательные числа. Если при выполнении функции P(sem) в работающем в данный момент процессе значение sem окажется равным нулю, то процесс приостанавливается (переводится в состояние ожидания), ставится в очередь, связанную с данным семафором, и будет находиться в этом состоянии до тех пор, пока переменная sem не станет больше нуля (пока не откроется семафор). Если при этом имеется какой-либо другой процесс, готовый к выполнению, то он выполняется. Если sem > 0, то оператор P уменьшает значение sem на единицу (процесс закрывает семафор).
При выполнении функции V к значению sem прибавляется единица (семафор открывается) и если в очереди к семафору имеются процессы, то выбирается один из них, делается активным, а от значения sem вычитается единица (семафор вновь закрывается).
Естественно, что операторы P и V являются взаимоисключающими. Кроме того, функции P и V для одного семафора должны быть доступны внешним прерываниям.
Аналогичный механизм реализован в объектах языка Смолток. Экземпляры класса Semaphore являются переменными семафора. Сообщение signal соответствует операции V, а сообщение wait - операции P. Создаваемому семафору изначально присваивается значение 0.
Рассмотрим решение задачи создания кольцевого буфера на N ячеек с использованием механизма семафоров. Кольцевой буфер – это очень интересная структура. Суть его сводится к тому, что имеется ограниченное число ячеек памяти, соединенных в кольцо. Информация последовательно записывается в кольцо и также последовательно из него считывается. При этом запись очередной порции осуществляется в ячейку, на которую указывает т.н. голова буфера. После записи этот указатель сдвигается на следующую свободную ячейку. А считывание происходит из ячейки, адресуемой другим указателем – т.н. хвостом. После считывания хвост будет указывать на следующую ячейку. Таким образом при считывании/записи информации из кольцевого буфера данные "циркулируют" в нем. На этом принципе построены многие управляющие структуры. В том числе и такие, как каналы.
Схема кольцевого буфера на 10 ячеек
Этому буферу посылаются сообщения fetch (принести) и store:data (сохранить). При этом считается, что при пустом буфере должны останавливаться все процессы, посылающие ему сообщения fetch до тех пор, пока в буфер не будет занесено значение. При полном буфере должны останавливаться те процессы, которые посылают сообщения store:data. Во всех остальных случаях процессы чтения и записи должны действовать независимо друг от друга.
Для этого создаются два семафора: EMPTY и FULL. Семафору EMPTY присваивается начальное значение 0, семафору FULL - значение N - число ячеек в буфере (в нашем примере N = 10). Таким образом, при сообщении fetch посылаются сообщения EMPTY wait и FULL signal, а при сообщении store:data посылаются сообщения FULL wait и EMPTY signal. В результате содержимое семафора EMPTY оказывается равным количеству данных, содержащихся в буфере, а содержимое семафора FULL - количеству свободных мест в буфере. При пустом буфере процесс, пытающийся осуществить доступ к нему, останавливается сообщением EMPTY wait, при полном - сообщением FULL wait. Кроме того, нельзя допускать одновременного доступа к переменным экземпляра HEAD и TAIL. Доступ должен быть взаимоисключающим. Для этого используются переменные FetchProtect и StoreProtect.
Описание классов BUFFER и RINGBUFFER выглядит так:
" Класс BUFFER "
Object subclass: #Buffer
instanceVariableNames:
'next val '
classVariableNames: ''
poolDictionaries: '' !
!Buffer methods !
next
^next.!
next: link
next := link.!
put: data
val := data.!
val
^val.! !
" Класс RINGBUFFER "
Object subclass: #RingBuffer
instanceVariableNames:
'empty full head tail fetchprotect storeprotect '
-- семафоры:
-- empty - количество данных в буфере
-- full - количество свободных мест
-- head, tail - указатели на начало и конец
-- fetchprotect и storeprotect - вспомогательные переменные, обеспечивающие
-- взаимоисключение
classVariableNames: ''
poolDictionaries: '' ! !
!RRingBuffer class methods !
create: size
^self new init: size.! !
!RRingBuffer methods !
init: size
head := tail := Buffer new.
1 to: size - 1 do: [ :i | tail next: Buffer new. tail := tail next].
tail next: head.
tail := head.
empty := Semaphore new.
full := Semaphore new.
1 to: size do: [:i | full signal].
fetchprotect := Semaphore new signal.
storeprotect := Semaphore new signal.!
fetch
|data|
empty wait.
fetchprotect wait.
data := tail val.
tail := tail next.
fetchprotect signal.
full signal.
^data.!
store: data
full wait.
storeprotect wait.
head put: data.
head := head next.
storeprotect signal.
empty signal.! !
Ниже приведен фрагмент рабочего окна:
|n block1 block2 w nfibo rb d flimit|
flimit := 25.
rb := RingBuffer new init: 20.
-- или так: rb := RingBuffer create: 20.
w := TextWindow windowLabeled: 'Ring Buffer' frame: (100@100 extent: 400@200).
block1 := [ n := 0. [n<flimit] whileTrue: [n:=n+1. nfibo := ( n fibo).
rb store: nfibo. ].
w nextPutAll: 'Process 1 done.'; cr.].
block2 := [ n := 0. [n<flimit] whileTrue: [n := n+1. d := rb fetch. w nextPutAll: (d radix:10); cr. ].
w nextPutAll: 'Process 2 done.'; cr.].
block1 forkAt: Processor lowUserPriority. -- или block1 forkAt: 1.
block2 forkAt: Processor lowUserPriority. -- или block1 forkAt: 1.
Идея реализации персонального компьютера как специализированной машины с аппаратно реализованным языком высокого уровня была впервые воплощена Л.Петером Дойчем в Лисп-машине. В этом проекте используется система команд, в которой минимальной единицей является один байт, и поэтому ее называют байт-кодом.
В байт-коде имеется тенденция реализовывать все команды по возможности в одном байте (при высокой частоте выполнения команд), что позволяет создавать компактный объектный код.
Все это нужно, во-первых, для того, чтобы можно было реализовать специальный Смолток-процессор. Дело в том, что Смолток – это интерпретируемая система, и для повышения быстродействия очень хорошо было бы создать интерпретатор на аппаратном уровне, благо язык Смолток очень прост. Во-вторых, речь идет об "аппаратно-независимом" объектном коде Смолток-команд (как бы неожиданно это не выглядело). Представьте себе системы, работающие на различных программных и аппаратных платформах, но могущие выдавать эквивалентные объектные коды. Тогда мы можем говорить о полной переносимости программ даже без использования всяческих кросс-систем. Для всего этого и существует модель системы Смолток, называемая виртуальной Смолток-машиной.
Виртуальная машина Смолток-80 выполняет три функции:
1) Функция интерпретации. Интерпретатор считывает команды языка (байт-код) и выполняет их.
2) Управление объектами. Блок управления объектами создает необходимые объекты и передает их интерпретатору, а ставшие ненужными объекты собирает и использует для дальнейшей работы.
3) Система базовых операций. В нее входят операции ввода-вывода, управления процессами и другие базовые операции. В системе также регистрируются в качестве элементарных методов (primitive method - примитивы) те операции, которые нельзя реализовать на самом Смолтоке (или их реализация неэффективна) и которые реализуются в виде программ вне системы Смолток-80.
При посылке сообщения создается объект, называемый контекстом метода. В него входит следующая информация:
1) контекст вызова;
2) адрес команды;
3) указатель стека;
4) обрабатывающая процедура метода (объекты с байт-программами);
5) получатель;
6) аргументы;
7) временные переменные;
8) стек.
Эта информация необходима для выполнения метода.
Приведем некоторые примеры групп команд виртуальной Смолток-машины.
1) Проталкивание в стек переменных экземпляра-получателя. Переменные экземпляра фиксируются для каждого экземпляра, и в каждом объекте для них отводится область памяти. Данная команда проталкивает в стек считанные переменные экземпляра, в частности – получателя.
Байт-коды 0-15, 128:
0-15 [0000iiii] Помещение в стек переменной-экземпляра получателя с номером #iiii.
128 [10000000] [jjkkkkkk] Помещение в стек переменной экземпляра получателя, временной переменной, литерала, глобальной переменной, указываемой литералом [jj] с номером #kkkkkk.
2) Проталкивание в стек временной переменной. Временные переменные создаются в момент вызова метода.
Байт-коды 16-31, 128:
16-31 [0001iiii] Помещение в стек временной переменной с номером #iiii.
3) Проталкивание символов в стек. Символ - это селектор сообщения или константа с объектным указателем.
Байт-коды 32-63, 128:
32-63 [001iiiii] Помещение в стек литерала с номером #iiiii.
4) Вызов метода с использованием селектора сообщения, находящегося в области литералов. Команда производит поиск селектора сообщения, начиная со словаря класса получателя. Если поиск успешен, то производится вызов соответствующего метода.
Байт-коды 131, 132, 134, 208-255.
5) Помещение в стек активного контекста. Команда помещает в поля текущего контекста значения регистров и затем помещает в стек указатель этого контекста.
Байт-код 137.
6) Команды перехода и условного перехода.
Байт-коды 144-175:
144-151 [10010iii] Переход по адресу iii+1.
152-159 [10011iii] Выталкивание из стека, переход по адресу iii+1 при значении false вытолкнутой вершины.
160-167 [10100iii] [jjjjjjjj] Переход по адресу (iii-4)*256+jjjjjjjj.
7) Посылка заявки на вычисление. Команда реализует арифметические операции "+" и "-". Если получатель не является целым числом, то выполняются действия, аналогичные обычной посылке заявки.
Байт-коды 192-207:
192-207 [1100iiii] Посылка специальной заявки #iiii.
1. Бадд Тимоти. Объектно-ориентированное программирование в действии /Пер. с англ. СПб.:Питер, 1997. -464 с.
2. Фути К., Судзуки Н. Языки программирования и схемотехника СБИС /Пер. с японского, М.:Мир, 1988. –224 с.
Кроме того, полезной может оказаться документация системы Smalltalk Express: Smalltalk Express. ParcPlace-Digitalk Inc, 1992-1996. www.parcplace.com