Объектно-ориентированное программирование
Общие замечания
правитьОбъектно-ориентированная парадигма программирования не нова. Её истоки восходят к Симуле-67, хотя впервые она была полностью реализована в Smalltalk-80. ООП (Объектно-ориентированное программирование) приобрело популярность во второй половине 80-х вместе с такими языками, как С++, Objective C (другое расширение C), Object Pascal и Turbo Pascal, CLOS (объектно-ориентированное расширение Lisp'a), Eiffel, Ada (в её последних воплощениях) и недавно — в Java. В этой статье внимание сосредоточено на C++, Object Pascal и Java, иногда упоминаются и другие языки.
Ключевые черты ООП хорошо известны:
- Первая — инкапсуляция — это определение классов — пользовательских типов данных, объединяющих своё содержимое в единый тип и реализующих некоторые операции или методы над ним. Классы обычно являются основой модульности, инкапсуляции и абстракции данных в языках ООП.
- Вторая ключевая черта, — наследование — способ определения нового типа, когда новый тип наследует элементы (свойства и методы) существующего, модифицируя или расширяя их. Это способствует выражению специализации и генерализации.
- Третья черта, известная как полиморфизм, позволяет единообразно ссылаться на объекты различных классов (обычно внутри некоторой иерархии). Это делает классы ещё удобнее и облегчает расширение и поддержку программ, основанных на них.
Инкапсуляция, наследование и полиморфизм — фундаментальные свойства, которыми должен обладать язык, претендующий называться объектно-ориентированным (языки, не имеющие наследования и полиморфизма, но имеющие только классы, обычно называются основанными на классах). Различные ОО языки используют совершенно разные подходы. Мы можем различать ОО языки, сравнивая механизм контроля типов, способность поддерживать различные программные модели и то, какие объектные модели они поддерживают.
Алан Кей в свое время вывел пять основных черт языка Smalltalk — первого удачного ОО языка:
- Все является объектом. Объект как хранит информацию, так и способен ее преобразовывать. В принципе любой элемент решаемой задачи (дом, собака, услуга, химическая реакция, город, космический корабль и т. д.) может представлять собой объект. Объект можно представить себе как швейцарский нож: он является набором различных ножей и «открывашек» (хранение), но в то же самое время им мы можем резать или открывать что-либо (преобразование).
- Программа — совокупность объектов, указывающих друг другу что делать. Для обращения к одному объекту другой объект «посылает ему сообщение». Как вариант возможно и «ответное сообщение». Программу можно представить себе как совокупность к примеру 3 объектов: писателя, ручки и листа бумаги. Писатель «посылает сообщение» ручке, которая в свою очередь «посылает сообщение» листу бумаги — в результате мы видим текст (посыл сообщения от листа к писателю).
- Каждый объект имеет свою собственную «память» состоящую из других объектов. Таким образом программист может скрыть сложность программы за довольно простыми объектами. К примеру, дом (достаточно сложный объект) состоит из дверей, комнат, окон, проводки и отопления. Дверь, в свою очередь, может состоять из собственно полотна, ручки, замка и петель. Проводка тоже состоит из проводов, розеток и, к примеру, щитка.
- У каждого объекта есть тип. Иногда тип называют еще и классом. Класс (тип) определяет какие сообщения объекты могут посылать друг другу. Например, аккумуляторная батарея может передавать электролампе ток, а вот импульс или физическое усилие - нет.
- Все объекты одного типа могут получать одинаковые сообщения. К примеру у нас есть 2 объекта: синяя и красная кружки. Обе разные по форме и материалу. Но из обеих мы можем пить (или не пить, если они пустые). В данном случае кружка — это тип объекта.
Самое лаконичное описание объекта предложил Буч: «Объект обладает состоянием, поведением и индивидуальностью».
Контроль во время компиляции и во время выполнения
правитьЯзыки программирования можно оценить по тому, насколько они строги к типам. Контроль типов включает проверку существования вызываемых методов, видов их параметров, проверку границ массивов и подобное.
C++, Java, и Object Pascal предпочитают более или менее тщательный контроль типов во время компиляции. С++, возможно, наименее точен в этом отношении (на что указывает, к примеру, возможность присвоения double к float), тогда как Java использует проверку типов наиболее широко. Это оттого, что C++ обеспечивает совместимость с Си, который не очень строго проверяет типы во время компиляции. Например, C и C++ считают, что все арифметические типы совместимы (хотя присвоение float целой переменной вызовет предупреждение компилятора). В Object Pascal и Java логическое значение не целое, а символ - еще один отличный и несовместимый тип.
Тот факт, что виртуальная машина Java интерпретирует байтовый код во время выполнения, не означает, что этот язык отказывается от проверки типов во время компиляции. Наоборот, в этом языке проверка наиболее тщательна. Другие ОО языки, такие как Smalltalk и CLOS, наоборот, склонны большинство проверок типов (если не все) осуществлять во время исполнения.
Чисто объектно-ориентированные и гибридные языки
правитьРазличаются чистые и гибридные объектно-ориентированные языки. Чистые — языки, которые позволяют использовать только одну модель программирования — объектно-ориентированную. Можно объявлять классы и методы, но не можете завести глобальные переменные и обычные функции и процедуры старого типа.
Среди наших четырех языков только Java и C# являются чистыми ОО языками (как Eiffel и Smalltalk). На первый взгляд это кажется положительной идеей. Однако она ведет к тому, что вы используете кучу статических методов и статических данных, что не так уж отличается от использования глобальных функций и данных, за исключением более сложного синтаксиса. Чистые ОО языки дают преимущество новичкам в ООП, потому что программист вынужден использовать (и учить) модель ООП. C++ и Object Pascal, наоборот, - типичные примеры гибридных языков, которые позволяют программистам использовать при необходимости традиционный подход C или Pascal.
Smalltalk расширяет эту идею до уровня «объектирования» таких предопределенных типов данных, как целые и символы, а также языковых конструкций (таких как циклы). Это теоретически интересно, но сильно уменьшает эффективность. Java и C# останавливаются намного раньше, допуская присутствие простых не ОО типов данных (хотя имеются необязательные классы-обертки и для простых типов).
Простая объектная модель и ссылочно-объектная модель
правитьСвойство: Третий элемент, по которому различаются языки ООП - их объектная модель. Некоторые традиционные языки ООП позволяют программистам создавать объекты в стеке, в куче (в хипе - heap) или в статической памяти. В этих языках переменная типа класс соответствует объекту в памяти. Так работает C++.
В последнее время появилась тенденция использовать другую модель, часто называемую ссылочно-объектной моделью. В этой модели каждый объект динамически размещается в куче, а переменная типа класс фактически является ссылкой или хэндлом объекта в памяти (технически это нечто вроде указателя). Java и Object Pascal оба используют эту ссылочную модель. В C# используется преимущественно ссылочно-объектная модель, однако имеется возможность создавать т. н. структуры (по сути дела, структура здесь - специальная разновидность класса), объекты которых будут располагаться в стеке и статической памяти. Как мы увидим, вкратце это значит, что вам необходимо не забыть выделить память для объекта.
Классы, объекты и ссылки
правитьСвойство: Так как мы обсуждаем языки ООП, то после этого введения, начнём обсуждать классы и объекты. Я надеюсь, что каждый ясно понимает разницу между этими двумя терминами. В двух словах, класс - это тип данных, а объект - экземпляр типа класс. "Кружка" - это класс (тип). А уж которая, - синяя или красная, - это два разных объекта (экземпляра), типа "кружка". Как нам теперь использовать объекты в языках, использующих различные объектные модели?
C++: если у нас есть класс MyClass с методом MyMethod, мы можем написать:
MyClass Obj;
Obj.MyMethod();
и получить объект класса MyClass с именем Obj. Память для этого объекта обычно выделяется в стеке, и вы можете сразу начать использовать объект, как это сделано во второй строке.
Также возможно выделить память для объекта в куче и оперировать указателем на объект:
MyClass *obj = new MyClass();
obj->MyMethod();
delete obj;//освобождаем память
Java: подобная инструкция выделяет место только для хэндла объекта, а не для самого объекта:
MyClass obj;
obj = new MyClass();
obj.myMethod();
Прежде чем использовать объект, вы должны вызвать "new
" для выделения под него памяти. Конечно, вы можете объявить и проинициализировать объект в одном предложении, избегая использования неинициализированных объектных хэндлов:
MyClass obj = new MyClass();
obj.myMethod();
Object Pascal: использует подобный подход, но требует отдельных предложений для объявления и инициализации:
var
Obj: MyClass;
begin
Obj := MyClass.Create;
Obj.MyMethod;
В C# работа с объектами классов и структур внешне выглядит похоже:
MyClass obj1 = new MyClass();
obj1.Method1();
MyStruct1 obj2 = new MyStruct("Hello!");
obj2.Method2();
MyStruct2 obj3;
obj3.Method3();
Но все же есть одно принципиальное отличие - память под объект структуры выделяется статически, поэтому вызов конструктора перед вызовом метода не является обязательным - объект уже существует, хотя его поля могут содержать "мусорные" значения.
Замечание: Если вам кажется, что ссылочно-объектная модель требует большей работы от программиста, обратите внимание на то, что в C++ вы часто должны использовать указатели и ссылки на объекты. Только используя указатели и ссылки, вы можете добиться полиморфизма. Ссылочно-объектная модель, наоборот, делает использование указателей подразумеваемым, скрывая от программиста сложность этого подхода. В Java, в частности, официально указателей нет, хотя они там повсюду. Только программисты не имеют над ними прямого контроля, и поэтому, из соображений безопасности, не могут попасть в произвольное место памяти.
Мусорная корзина
правитьСвойство: Если вы создали и не использовали объект, вам нужно уничтожить его, чтобы не занимать неиспользуемую память.
C++: В C++ уничтожить объект, расположенный в стеке, довольно просто. С другой стороны, уничтожение объектов, созданных динамически, зачастую является сложной проблемой. Есть много решений, включая подсчет ссылок и "интеллектуальные" указатели, но ни один из них не даёт простого решения. Первое впечатление для C++ программистов, что использование ссылочно-объектной модели сделает ситуацию только хуже.
Java: Это, конечно, не касается Java, так как виртуальная машина запускает алгоритм сборки мусора (в фоновом процессе, согласно теории Java; или начинает этот процесс после того, как ненадолго остановит программу, как в большинстве реальных JVM). Сбор мусора происходит без участия программиста, но он может неблагоприятно влиять на эффективность выполнения приложения. Отсутствие явно записываемых деструкторов может приводить к ошибкам в завершающем коде (см. также главу о деструкторах и методе finalize() ниже).
OP: В Object Pascal, наоборот, нет механизма сбора мусора. Однако компоненты Delphi поддерживают идею владельца (owner) объекта: владелец становится ответственным за уничтожение всех объектов, которыми он владеет. Это делает управление уничтожением объекта очень простым и прямым. Delphi также использует механизм подсчёта ссылок для строк, динамических массивов и интерфейсов, освобождая объект в памяти, когда на него нет больше ссылок.
C#: В CLR (среде выполнения .NET) имеется сборщик мусора, аналогичный используемому в виртуальной машине Java. Поэтому программисту также не приходится заботиться об удалении созданных объектов. Однако, имеется специальный интерфейс IDisposable, которым "обозначаются" объекты, требующие ручной деинициализации (например, закрытия потоков ввода-вывода).
MyClass obj = new MyClass();//класс MyClass реализует интерфейс IDisposable
obj.Method1();
...
obj.Method2();
//объект obj нам больше не нужен
obj.Dispose();
Определение новых классов
правитьСвойство: Теперь, когда мы рассмотрели, как создавать объекты для существующих классов, мы можем обратиться к определению новых классов. Класс — это просто набор методов, работающих с определёнными локальными данными.
C++: Вот C++ синтаксис определения простого класса:
class Date {
private:
int dd;
int mm;
int yy;
public:
void Init (int d, int m, int y);
int Day ();
int Month ();
int Year ();
};
А вот определение пользовательского метода-инициализатора:
void Date::Init (int d, int m, int y)
{
dd = d;
mm = m;
yy = y;
}
Java: Синтаксис Java похож на синтаксис C++:
class Date {
private int dd = 1;
private int mm = 1;
private int yy = 1;
public void Init ( int d, int m, int y ) {
dd = d;
mm = m;
yy = y;
}
public int getDay () { return dd; }
public int getMonth () { return mm; }
public int getYear () { return yy; }
}
Основная разница состоит в том, что код каждого метода пишется там же, где он объявляется (при этом функции не становятся вставными (inline), как в C++), и в том, что вы можете инициализировать элементы данных класса. Фактически, если вы не сделаете этого, то Java проинициализирует все элементы данных за вас, используя значения по умолчанию.
OP: В Object Pascal синтаксис определения класса другой, но похожий скорее на C++, чем на Java:
type
Date = class
private
dd, mm, yy: Integer;
public
procedure Init (d, m, y: Integer);
function GetMonth: Integer;
function GetDay: Integer;
function GetYear: Integer;
end;
procedure Date.Init (d, m, y: Integer);
begin
dd := d;
mm := m;
yy := y;
end;
function Date.GetDay: Integer;
begin
Result := dd;
end;
...
Как видите, здесь есть синтаксические отличия: методы определяются с ключевыми словами function и procedure, методы без параметров не используют скобок, методы просто объявляются внутри определения класса, тогда как определяются позже, как это обычно делается в C++. Однако Pascal использует нотацию с точкой, а C++ — оператор :: (недоступный в Object Pascal и Java).
C# Синтаксис C# очень похож на Java (в данном примере идентичен):
class Date
{
private int dd;
private int mm;
private int yy;
void init(int d, int m, int y)
{
dd = d;
mm = m;
yy = y;
}
int Day()
{
return dd;
}
...
}
Примечание: Доступ к текущему объекту. В ОО языках методы отличаются от глобальных функций тем, что у них присутствует скрытый параметр, ссылка или указатель на объект, с которым мы работаем. Просто эта ссылка на текущий объект по-разному называется. Это this в C++, Java и C#, Self в Object Pascal.
Создание и уничтожение объектов
правитьКонструкторы
правитьСвойство: Вышеупомянутый класс ужасно прост. Первое, что мы могли бы к нему добавить, это конструктор, что является хорошей техникой для решения проблемы инициализации объекта.
C++: В C++, так же, как и в Java, имя конструктора совпадает с именем класса. Если вы не определили никакого конструктора, компилятор синтезирует конструктор по умолчанию, добавляя его к классу. В обоих этих языках вы можете завести несколько конструкторов благодаря перегрузке функций.
class MyClass
{
private:
int i;
public:
MyClass(int i);
}
MyClass::MyClass(int i)
{
this->i = i;
}
Java: Всё работает как в C++, хотя конструкторы называются также инициализаторами. Это подчеркивает тот факт, что объект создаёт виртуальная машина Java, тогда как код, который вы пишете в конструкторе, просто инициализирует свежесозданный объект. (То же самое фактически происходит и в Object Pascal.)
class MyClass {
private int i;
MyClass(int i) {
this.i = i;
}
}
OP: В Object Pascal вы используете специальное ключевое слово constructor и можете дать конструктору любое имя. Хотя Borland в Delphi 4 добавила поддержку перегрузки методов, программисты всё ещё дают разным конструкторам разные имена. В Object Pascal у каждого класса по умолчанию есть конструктор Create (наследуемый от TObject), если вы не перегрузите его конструктором с тем же именем и, возможно, другими параметрами. Этот конструктор, как мы увидим позднее, просто наследуется от общего базового класса.
type
TMyClass = class
private
I: Integer;
public
constructor Create(AI: Integer);
end;
...
constructor TMyClass(AI:Integer);
begin
I := AI;
end;
C#: Синтаксис объявления конструктора очень похож на Java, а в нашем примере будет идентичен.
Деструкторы и финализация
правитьСвойство: Деструктор играет роль противоположную конструктору и обычно вызывается при уничтожении объекта. Если конструктор нужен большинству классов, то только некоторые из них нуждаются в деструкторе. Деструктор в основном используется для освобождения ресурсов, зарезервированных конструктором (или другими методами во время жизни объекта). Эти ресурсы включают память, файлы, базы данных, ресурсы ОС и т. д.
C++: деструкторы вызываются автоматически, когда объект выходит из области определения или когда вы удаляете объект, заведенный динамически. У каждого класса есть только один деструктор, который объявляется как ~Class(), где Class - имя класса. Если объект создан в куче, то он не может быть автоматически удален и если не объявить деструктор явно в программе, то происходит утечка памяти (в Java данная проблема решена сборщиком мусора).
OP: деструкторы похожи на деструкторы C++. (Для деструкторов используется ключевое слово destructor [мое примечание — В. К.]) Object Pascal использует стандартный виртуальный деструктор, называемый Destroy. Этот деструктор вызывается стандартным методом Free. Все объекты динамические, поэтому предполагается, что вы вызовете Free для каждого объекта, созданного вами, если у того нет владельца, отвечающего за его уничтожение. Теоретически вы можете объявить несколько деструкторов, что имеет смысл, если вы можете вызывать деструкторы в своем коде (это не делается автоматически).
Java: В Java нет деструкторов. Объекты, на которые нет ссылок, уничтожаются сборщиком мусора, который работает в виде фоновой задачи и делает это автоматически (как описывалось ранее). Прежде чем уничтожать объект, сборщик мусора должен вызвать метод finalize(). Однако нет никакой гарантии, что этот метод вызывается в каждой JVM. По этой причине, если вам нужно освободить ресурсы, вы должны добавить какой-нибудь метод, и убедиться, что он вызывается (эти дополнительные усилия не нужны в других ОО языках).
C# Как и в виртуальной машине Java, в CLR используется автоматическая сборка мусора. Как было сказано выше, существует специальный интерфейс для объектов, требующих ручного освобождения ресурсов. В C# также можно создать метод вида ~имя_класса(), который полностью аналогичен методу finalize() в Java.
Инкапсуляция (Private и Public)
правитьСвойство: Общим элементом всех трех языков является присутствие трех спецификаторов доступа, указывающих на различные уровни инкапсуляции класса: public, protected, и private. Public означает: видимый любым другим классом, protected означает: видимый производными классами, private означает: отсутствие видимости извне. В деталях, однако, есть различия.
C++: В C++ вы можете использовать ключевое слово friend для обхода инкапсуляции. Видимость по умолчанию для класса — private, для структур — public.
OP: В Object Pascal private и protected относятся только к классам других юнитов. В терминах C++, класс является дружественным для любого другого класса, определенного в том же юните (или файле исходного кода). В Delphi есть еще один модификатор доступа — published, который генерирует информацию времени выполнения (RTTI) об элементах.
Java: В Java отличие синтаксиса в том, что модификатор доступа повторяется для каждого элемента класса. А конкретнее, по умолчанию в Java используется friendly, это значит, что элемент видим для других классов этого же пакета (или файла исходного кода, как в OP). Подобным образом, protected означает видимость для подклассов.
C#: В CLR используется пять уровней доступа к членам класса, которые в C# обозначаются как private, protected, public, internal, protected internal. Первые три аналогичны соответствующим модификаторам в C++, Object Pascal и Java. Модификатор internal ограничивает видимость текущей сборкой (т. е. исполняемым файлом или файлом динамической библиотеки, к которому относится данный класс), protected internal разрешат также доступ в унаследованных классах из других сборок.
Файлы, модули и пакеты
правитьСвойство: Важное различие между тремя языками заключается в организации исходного кода в файлах. Все три языка используют файлы в качестве стандартного механизма для запоминания исходного кода классов (в отличие от других ОО языков, таких как Smalltalk), но компилятор C++, в отличие от OP или Java, не понимает файлов. Эти же два языка работают с идеей модулей, хотя называют их по-разному.
C++: В C++ программист обычно помещает определение класса в файл объявлений, а определение методов — в отдельный файл кода. Обычно у этих двух файлов одинаковые имена и различные расширения. Компилируемый блок, как правило, ссылается (включает в себя) на свой файл объявлений и на файлы объявлений тех классов (или функций), на которые ссылается код. Все эти соглашения не утруждают компилятор. Это значит, что линкеру предстоит большая работа, потому что компилятор не может знать, в каком другом модуле может быть определен нужный метод. Однако также существуют пространства имен, позволяющие во многих случаях избежать неоднозначностей. Например, классы и прочие элементы библиотеки STL находятся в пространстве имен std.
OP: В Object Pascal каждый файл исходного кода называется unit, и он делится на две части: интерфейс и реализация, отмечаемые соответственно ключевыми словами interface и implementation. Секция интерфейса включает в себя определения классов (с объявлениями методов), а секция реализации должна включать в себя определения методов, объявленных в интерфейсе. Писать фактический код в секции интерфейса нельзя. Вы можете сослаться на объявления другого файла, используя предложение uses. Этим включается в компиляцию интерфейс того файла:
uses Windows, Form, MyFile;
Java: В Java каждый файл исходного кода или единица компиляции компилируется отдельно. Затем вы можете отметить группу единиц компиляции как части одного пакета. В отличие от двух других языков, вы пишете весь код методов тут же при объявлении класса. При включении какого-либо файла предложением import, компилятор читает только public объявления, а не весь код:
import where.Myclass; import where.* // все классы
C#: На платформе CLR (.NET) код разделен на сборки, которые могут представлять собой динамически подключаемую библиотеку либо исполняемый файл. Кроме того, для группировки типов с целью сведения к минимуму вероятности конфликта имен, существуют пространства имен, которые, однако, никак не связаны со сборками или файлами исходного кода.
Примечание: Модули как пространство имён. Другим важным отличием Java и OP является их способность читать откомпилированные файлы и извлекать из них определения, как бы извлекая заголовки из скомпилированного кода. С другой стороны, для преодоления отсутствия модулей C++ включает пространство имен (namespace). В Java и OP, когда два имени конфликтуют, вы можете просто использовать имя модуля в качестве префикса. Это не требует дополнительной работы по определению пространств имен, а просто включено в языки.
Методы/данные класса и объекта класса
правитьСвойство: ОО языки обычно разрешают заводить методы и данные, относящиеся к классу целиком, а не к отдельным объектам. Метод класса обычно может быть вызван как для объекта класса, так и применён к классу в целом. Данные класса не повторяются для каждого объекта, а разделяются между всеми объектами данного типа.
C++: В C++ методы и данные класса отмечаются ключевым словом static. Данные класса должны быть проинициализированы специальным объявлением, ещё одной уступкой отсутствию модулей.
OP: В OP допустимы только методы класса, которые отмечаются словом class. Данные класса можно заменить, так сказать, приватными глобальными переменными в секции исполнения юнита, описывающего класс.
Java: Java использует то же слово, что и C++, static. Статические методы используются очень часто (и даже слишком) из-за отсутствия глобальных функций. Статические данные можно инициализировать прямо в объявлении класса.
C#: Также, как и в C++ и Java, используется ключевое слово static. Кроме того, можно отметить как статический целый класс, что запрещает создание его объектов и определение нестатических членов.
Классы и наследование
правитьСвойство: Наследование у классов — одно из оснований ООП. Оно может быть использовано для выражения генерализации или специализации. Основная идея в том, что вы определяете новый тип, расширяя или модифицируя существующий, другими словами, производный класс обладает всеми данными и методами базового класса, новыми данными и методами и, возможно, модифицирует некоторые из существующих методов. Различные ОО языки используют различные жаргоны для описания этого механизма (derivation, inheritance, sub-classing), для класса, от которого вы наследуете (базовый класс, родительский класс, суперкласс) и для нового класса (производный класс, дочерний класс, подкласс).
C++: C++ использует слова public, protected, и private для определения типа наследования и чтобы спрятать наследуемые методы или данные, делая их приватными или защищёнными. Хотя публичное наследование наиболее часто используется, по умолчанию берётся приватное. Как мы увидим далее, C++ — единственный из этих четырех языков, поддерживающий множественное наследование. Вот пример синтаксиса наследования:
class Dog: public Animal {
...
};
OP: Object Pascal при наследовании использует не ключевые слова, а специальный синтаксис, добавляя в скобках имя базового класса. Этот язык поддерживает только один тип наследования, который в C++ называется публичным. Как мы увидим позднее, классы OP происходят от одного общего базового класса.
type
Dog = class (Animal)
...
end;
Java: Java использует слово extends для выражения единственного типа наследования, соответствующего публичному наследованию в C++. Java не поддерживает множественное наследование. Классы Java тоже происходят от общего базового класса.
class Dog extends Animal {
...
}
C#: C#, как и Java, не поддерживает множественное наследование. Синтаксис наследования несколько отличается от остальных рассматриваемых языков:
class Dog : Animal
{
...
}
Примечание: Конструкторы и инициализация базового класса. В C++, C# и Java у конструкторов наследующих классов сложная структура. В Object Pascal за инициализацию базового объекта отвечает программист. Это довольно сложный раздел, поэтому я пропустил его в этой статье. Вместо этого я сосредоточусь на общем базовом классе, множественном наследовании, интерфейсах, позднем связывании и других родственных предметах.
Предок всех классов
правитьСвойство: В некоторых ОО языках каждый класс происходит по крайней мере от некоторого базового класса по умолчанию. Этот класс, часто называемый Object, или подобно этому, обладает некоторыми основными способностями, доступными всем классам. Фактически, все другие классы в обязательном порядке его наследуют. Этот подход является общим ещё и потому, что так первоначально делалось в Smalltalk.
C++: Хотя язык C++ и не поддерживает такое свойство, многие структуры приложений базируются на нём, вводя идею общего базового класса. Пример тому — MFC с его классом COobject. Фактически это имело большой смысл вначале, когда языку не хватало шаблонов.
OP: Каждый класс автоматически наследует класс TObject. Так как язык не поддерживает множественное наследование, все классы формируют гигантское иерархическое дерево. Класс TObject поддерживает RTTI и обладает некоторыми другими возможностями. Общей практикой является использование этого класса, когда вам нужно передать объект неизвестного типа.
Java: Как и в OP, все классы безоговорочно наследуют класс Object. И в этом языке у общего класса тоже есть некоторые ограниченные свойства и небольшая поддержка RTTI.
C# (CLR): Также имеется класс System.Object (в C# также существует ключевое слово object для его обозначения), являющийся предком не только всех классов, но и любых других типов, даже примитивных, которые также включены в структуру наследования и имеют методы.
Доступ к методам базового класса
правитьСвойство: Когда вы пишете метод класса или перекрываете метод базового класса, вам нередко надо сослаться на методы базового класса. Если этот метод переопределен в производном классе, то, используя его имя, вы получите новую версию. В ОО языках есть некоторые приёмы или ключевые слова, позволяющие решить эту проблему.
C++: В C++ для указания нужного класса можно использовать оператор (::). Вы можете получить доступ не только к методам базового класса, но к классам выше по иерархии. Это очень мощная техника, но она создаёт проблемы, когда вы добавляете в иерархию промежуточный класс.
OP: В Object Pascal для этой цели есть специальное слово inherited. После этого слова вы можете написать имя метода базового класса или (в некоторых случаях) просто использовать это ключевое слово для доступа к соответствующему методу базового класса.
Java: Java для этого использует ключевое слово super. В этом языке, так же, как и в OP, нет возможности сослаться на другой предшествующий класс. На первый взгляд, это может показаться ограничением, но оно позволяет расширять иерархию, вводя промежуточные классы. К тому же, если вы не нуждаетесь в функциях базового класса, вам, наверное, не следует его наследовать.
Совместимость подтипов
правитьСвойство: Как я указывал в начале, не все ОО языки строго типизированы, но все три языка, на которых мы сконцентрировались, обладают этим свойством. В основном это означает, что объекты различных классов несовместимы по типу. Из этого правила есть исключение: объекты производных классов совместимы с типом их базового класса. (Примечание: обратное обычно неверно.)
C++: В C++ правило совместимости подтипов справедливо только для указателей и ссылок, но не для обычных объектов. Фактически, у различных объектов разный размер, поэтому их нельзя расположить на том же месте в памяти.
OP: Благодаря ссылочно-объектной модели, совместимость подтипов возможна для каждого объекта. Более того, все объекты совместимы по типу с TObject.
Java: Java использует ту же модель, что и Object Pascal.
Примечание: Полиморфизм. Совместимость подтипов особенно важна для позднего связывания и полиморфизма, как это показано в следующей секции.
Позднее связывание (и полиморфизм)
правитьСвойство: Когда различные классы в иерархии переопределяют некоторый метод, очень полезна возможность ссылаться на общий объект этих классов (благодаря совместимости подклассов) и вызывать этот метод, результатом чего будет вызов метода надлежащего класса. Для этого компилятор должен поддерживать позднее связывание, то есть не генерировать вызов специфической функции, а ждать, пока во время выполнения не определятся фактический тип объекта и функция, которую нужно вызвать.
C++: В C++ позднее связывание доступно только для виртуальных методов (вызов которых становится немного медленнее). Метод, объявленный в базовом классе как виртуальный (virtual), поддерживает это свойство (но только если описания методов совпадают). Обычные, не виртуальные методы не позволяют позднее связывание, как и OP.
OP: В Object Pascal позднее связывание вводится с помощью ключевых слов virtual и dynamic (разница между ними только в оптимизации). В производных классах переопределённые методы должны быть отмечены словом override (это заставляет компилятор проверять описание метода). Рациональное объяснение этой особенности OP состоит в том, что разрешается больше изменений в базовом классе и предоставляет некоторый дополнительный контроль во время компиляции.
Java: В Java все методы используют позднее связывание, если вы не отметите их явно как final. Финальные методы не могут быть переопределены и вызываются быстрее. В Java написание методов с нужной сигнатурой жизненно важно для обеспечения полиморфизма. Тот факт, что в Java по умолчанию используется позднее связывание, тогда как в C++ стандартом является раннее связывание, — явный признак разного подхода этих двух языков: C++ временами жертвует ОО моделью в пользу эффективности, тогда как Java — наоборот
Примечание: Позднее связывание для конструкторов и деструкторов. Object Pascal, в отличие от других двух языков, позволяет определять виртуальные конструкторы. Все три языка поддерживают виртуальные деструкторы.
Абстрактные методы и классы
правитьСвойство: При построении сложной иерархии, для обеспечения полиморфизма программисты часто вынуждены вводить методы в классы верхнего уровня, даже если эти методы ещё не определены для этой специфической абстракции. Здесь можно было бы оставить пустые методы, но многие ОО языки предлагают такой специфический механизм, как определение абстрактных методов, то есть методов без реализации. Классы, имеющие хотя бы один абстрактный метод, часто называются абстрактными классами.
C++: В C++ абстрактные методы или чисто виртуальные функции получаются добавлением так называемого чистого описателя (=0) в определение метода. Абстрактные классы являются просто классами с одним или более абстрактным методом (или наследующие их). Вы не можете создать объект абстрактного класса.
OP: Object Pascal для выделения этих методов использует ключевое слово abstract. Кроме того, абстрактными классами являются классы, имеющие или наследующие абстрактные методы. Вы можете создать объект абстрактного класса (хотя компилятор выдаст предупреждающее сообщение). Это подвергает программу риску вызвать абстрактный метод, что приведёт к генерации ошибки времени выполнения и завершению программы.
Java: В Java и абстрактные методы, и абстрактные классы отмечаются ключевым словом abstract (действительно, в Java обязательно определять как абстрактный, класс, имеющий абстрактные методы, — хотя это кажется некоторым излишеством). Производные классы, которые не переопределяют все абстрактные методы, должны быть отмечены как абстрактные. Как и в C++, нельзя создавать объекты абстрактных классов.
Множественное наследование и интерфейсы
правитьСвойство: Некоторые ОО языки допускают наследование более чем одному базовому классу. Другие языки позволяют вам наследовать только от одного класса, но дополнительно позволят вам наследовать также от многочисленных интерфейсов или чисто абстрактных классов, то есть классов, состоящих только из виртуальных функций.
C++: C++ — единственный из трех языков, поддерживающий множественное наследование. Некоторые программисты считают положительным фактом, другие — отрицательным, и я не буду вмешиваться сейчас в эту дискуссию. Определенно, что множественное и повторяющееся наследование влечет за собой такие понятия, как виртуальные базовые классы, которые не легко освоить, хотя они предоставляют мощную технику. C++ не поддерживает понятия интерфейсов, хотя их можно заменить множественным наследованием чисто абстрактным классам (интерфейсы можно рассматривать как подмножество множественного наследования).
Java: Java, как и Object Pascal, не поддерживает множественное наследование, но полностью поддерживает интерфейсы. Методы интерфейсов допускают полиморфизм, и Вы можете использовать объект, осуществляющий интерфейс, когда ожидается интерфейсный объект. Класс может наследовать или расширить всего лишь один базовый класс, но может осуществить (это — ключевое слово) многочисленные интерфейсы. Хотя это не было спланировано заранее, интерфейсы Java очень хорошо укладываются в модель COM. Вот пример интерфейса:
public interface CanFly { public void Fly(); } public class Bat extends Animal implements CanFly { public void fly() { /* the bat flies... */ } }
OP: Delphi 3 ввел в Object Pascal понятие, подобное интерфейсам Java, но эти интерфейсы строго соответствуют COM (хотя технически возможно использовать их в обычных не-COM программах). Интерфейсы формируют иерархию, отдельную от классов, но, как и в Java, класс может наследовать одному базовому классу и осуществлять различные интерфейсы. Отображение методов класса на методы интерфейсов, осуществляемых классом, является одной из наиболее сложных частей языка Object Pascal. Delphi 4 добавляет к этой структуре возможность передать реализацию интерфейса подобъекту, делая эту технику почти такой же эффективной, как и множественное наследование.
Другие свойства
правитьПомимо обеспечения объектно-ориентированного программирования, эти языки предлагают другие интересные и мощные характеристики, которые дополняют поддержку ООП. В следующих разделах я просто представлю некоторые из них.
RTTI
правитьСвойство: В строго типизованных ОО языках компилятор осуществляет весь контроль типов, так что нет особой необходимости хранить информацию о классах и типах в работающей программе. Тем не менее, есть случаи (как, например, динамическое преобразование типов), которые требуют информацию о типе. По этой причине все три ОО языка, рассматриваемые здесь, более или менее поддерживают Идентификацию/Информацию о Типе Времени Выполнения (RTTI).
C++: первоначально не поддерживал RTTI. Это было добавлено позже для динамического преобразования типа (dynamic_cast) и сделало доступной некоторую информацию о типе для классов. Вы можете запросить идентификацию типа для объекта, и проверить, принадлежат ли два объекта одному классу.
OP: поддерживает и требует много RTTI. Доступен не только контроль соответствия и динамическое преобразование типов (с помощью операторов is и as). Классы генерируют расширенную RTTI для своих published свойств, методов и полей. Фактически это ключевое слово управляет частью генерации RTTI. Вся идея свойств, механизм потоков (файлы форм — DFM), и среда Delphi, начиная с Инспектора Объектов, сильно опирается на RTTI классов. У класса TObject есть (кроме прочих) методы ClassName и ClassType. ClassType возвращает переменную типа класса, объект специального типа ссылки на класс (который не является самим классом).
Java: как и в Object Pascal, в Java тоже есть единый базовый класс, помогающий следить за информацией о классе. Безопасное преобразование типов (type-safe downcast) встроено в этот язык. Метод getClass() возвращает своего рода метакласс (объект класса, описывающего классы), и Вы можете применить функцию getName() для того, чтобы получить строку с именем класса. Вы можете также использовать оператор instanceof. Java включает в себя расширенную RTTI для классов или интроспекцию, которая была введена для поддержки компонентной модели JavaBeans. В Java существует возможность создавать классы во время исполнения программы.
Пример: Вот синтаксис безопасного преобразования типов на всех трех языках. В случае ошибки в Delphi и Java происходит исключение, а в С++ возвращается нулевой указатель:
// C++ Dog* MyDog = dynamic_cast <Dog*> (myAnimal);
// Java Dog MyDog = (Dog) myAnimal;
// Object Pascal myDog := myAnimal as Dog;
Обработка исключений
правитьСвойство: Основная идея обработки исключений — упростить код обработки ошибок в программе, предоставив стандартный встроенный механизм, с целью сделать программы более устойчивыми. Обработка исключений — это тема, требующая отдельного рассмотрения, поэтому я только очерчу некоторые ключевые элементы и различия.
C++: C++ использует ключевое слово throw для генерации исключения, try для отметки охраняемого блока и catch для записи кода обработки исключения. Исключения — объекты специального класса, которые могут образовывать некоторую иерархию во всех трёх языках. При возникновении исключения C++ выполняет очистку стека до точки перехвата исключения. Перед удалением каждого объекта в стеке вызывается соответствующий деструктор.
OP: Object Pascal использует подобные ключевые слова: raise, try, и except и обладает подобными свойствами. Единственное существенное отличие состоит в том, что опустошение стека не производится, просто потому, что в стеке нет объектов. Кроме того, вы можете добавить в конце блока try слово finally, отмечая блок, который должен выполняться всегда, независимо от того, было или нет вызвано исключение. В Delphi классы исключений — производные Exception.
Java: Использует ключевые слова C++, но ведёт себя как Object Pascal, включая дополнительное ключевое слово finally. (Это общее свойство всех языков со ссылочно-объектной моделью, оно включено Borland также и в C++Builder 3.) Присутствие алгоритма сборки мусора ограничивает использование finally в классе, который распределяет другие ресурсы, кроме памяти. Также Java строже требует, чтобы все функции, которые могут вызвать исключение, описывали в соответствующем блоке, какие исключения могут быть вызваны функцией. Эти описания исключений проверяются компилятором, что является хорошим свойством, даже если оно подразумевает некоторую дополнительную работу для программиста. В классах Java объекты-исключения должны наследовать класс Throwable.
Шаблоны (обобщенное программирование)
правитьСвойство: Обобщенное программирование — это техника написания функций и классов, оставляя некоторые типы данных неопределёнными. Спецификация типа осуществляется, когда эта функция или класс используется в исходном коде. Всё делается под строгим контролем компилятора, и ничего не остаётся без определения во время выполнения. Наиболее типичный пример шаблона класса — это контейнерные классы.
C++: есть шаблонные классы и функции, отмечаемые ключевым словом template. Стандартный C++ включает обширную библиотеку шаблонов, называемую STL (Standart Template Library ,Стандартная библиотека шаблонов), которая поддерживает специфический и мощный стиль программирования: обобщенное программирование. C++ — единственный из рассматриваемых трех языков, который основывается на поддержке обобщенного программирования, помимо ООП.
OP: нет шаблонов. Контейнерные классы обычно строятся как контейнеры объектов класса TObject, а затем уточняются для необходимых объектов.
Java: реализуются в рамках Generics (введенного в JDK 1.5 «Tiger»). Концептуально они не отличаются от шаблонов в C++, но имеют некоторые особенности, которые диктуются свойствами самого языка. В отличие от C++, в Java невозможно во время выполнения получить информацию о конкретном типе шаблона. Предусмотрены контейнеры на все случаи жизни: List (хранение последовательностей элементов), Map или ассоциативные массивы (связывание одних объектов с другими), Set (уникальность значений для каждого типа).
Другие специфические свойства
правитьСвойство: Есть еще другие свойства, не упомянутые мной, хотя они важны, только из-за того, что они специфичны только для одного из трёх языков.
C++: Я уже упомянул множественное наследование, виртуальные базовые классы и шаблоны. Эти свойства отсутствуют в двух других ОО языках. В C++ есть ещё перегрузка операторов, тогда как перегрузка методов присутствует также в Java и была недавно добавлена в Object Pascal. C++ позволяет программистам перегружать и глобальные функции. Вы можете перегрузить операторы преобразования типов, написав конвертирующие методы, которые будут вызываться «за кулисами». Объектная модель C++ требует копировать конструкторы и перегружать операторы присваивания, в чем не нуждаются остальные два языка, поскольку базируются на ссылочно-объектной модели.
Java: Только Java поддерживает многопоточность непосредственно в языке. Объекты и методы поддерживают механизм синхронизации (с ключевым словом synchronized): два синхронизированных метода одного класса не могут выполняться одновременно. Для создания нового потока вы просто наследуете от класса Thread, перегружая метод run(). Как альтернативу вы можете осуществить интерфейс Runnable (что вы обычно делаете в апплетах, поддерживающих многопоточность). Мы уже обсуждали сборщик мусора. Ещё одно ключевое свойство Java, конечно, идея переносимого байтового кода, но это не относится непосредственно к языку. Другое примечательное свойство — это поддержка основанных на языке компонентов, известных как JavaBeans и многие другие свойства, недавно добавленные в этот язык.
OP: Вот некоторые специфические черты Object Pascal: ссылки на классы, легкие для использования указатели на методы (основа модели обработки событий) и, в частности, свойства (property). Свойство — это просто имя, скрывающее путь, которым вы получаете доступ к данным или методу. Свойство может проецироваться на прямое чтение или запись данных, а может ссылаться на метод, обеспечивающий доступ. Даже если вы меняете способ доступа к данным, вам не нужно менять вызывающий код (хотя вам нужно будет его перекомпилировать): это делает свойства очень мощным средством инкапсуляции.
Стандарты
правитьСвойство: Для каждого языка требуется, чтобы кто-то установил его стандарт и проверял все реализации на соответствие ему.
C++: Стандарт ANSI/ISO C++ явился завершением многотрудных усилий соответствующего комитета. Большинство авторов компиляторов, кажется, пытаются подчиняться стандарту, хотя есть ещё много странностей. Теоретически развитие языка должно на этом закончиться. На практике, инициативы вроде компилятора Borland C++Builder, конечно, не способствуют улучшению ситуации, но многие чувствуют, что C++ очень нуждается в визуальном окружении программирования. В то же время, популярный Visual C++ тянет C++ в другом направлении, например, с явным злоупотреблением макросов. (По моему личному мнению, у каждого языка есть собственная модель развития, и поэтому нет большого смысла в попытках использовать язык для того, для чего он не был предназначен.) Много новых возможностей будут введены новым стандартом C++ 0x.
OP: Object Pascal — язык-собственность, поэтому у него нет стандарта. Borland лицензировал язык для пары продавцов небольших компиляторов на OS/2, но это не оказало большого влияния. Borland расширяет язык с каждым новым выпуском Delphi.
Java: Компания-создатель Sun обладает торговой маркой Java. Однако Sun лицензирует его для продавцов других компиляторов, и убедило ISO создать стандарт Java, не создавая специальный комитет, а просто приняв предложения Sun как есть. Кроме формального стандарта, однако, Java требует высокосовместимых JVM. С недавней поры Sun выдвинула инициативу открыть исходные коды Java (OpenJDK) и сделать ее доступной для всех разработчиков в рамках лицензии GPL 2.
Заключение: Языки и программное окружение
правитьКак я упоминал в начале, хотя я пытался исследовать эти языки, только сравнивая синтаксические и семантические характеристики, важно рассмотреть их в соответствующем контексте. Языки нацелены на различные потребности, что означает, что они решают разные проблемы разными способами и используются в очень разных средах программирования. Хотя как языки, так и их среда копируют характеристики друг друга, они были сконструированы для разных потребностей, и в этом вы можете убедиться, сравнивая их характеристики.
Цель C++ — мощность и контроль за счет сложности. Целью Delphi является легкое, визуальное программирование (не отказываясь от мощности) и прочная связь с Windows. Цель Java — мобильность, даже за счет некоторого отказа от скорости, и распределённые приложения или исполняемое содержание WWW (хотя это, конечно, — не Microsoft-овский взгляд на Java!).
Можно определить, что успех этих трех языков зависит не от технических характеристик, которые я включил в эту статью. Финансовый статус Borland, операционная система управления Microsoft, популярность Sun в мире Internet, тот факт, что Java рассматривается как anti-Microsoft-овский язык, будущее браузеров Паутины и Win32 API, роль и признание модели ActiveX (из-за связанной с ней проблемой безопасности) и три уровня архитектуры Delphi — вот показатели, которые могли повлиять на ваш выбор сильнее, чем технические элементы. Например, такой хороший язык как Eiffel, у которого Object Pascal и Java взяли не только некоторое вдохновение, никогда не получит реальной доли рынка, хотя он был популярен во многих университетах земного шара.
Просто имейте в виду, что «модный» становится все более частым словом в компьютерном мире. Как пользователи хотят иметь инструменты этого года (вероятно, по этой причине операционные системы называются по тому году, в котором они выпущены), программисты любят работать с последним языком программирования и первыми овладеть им. Можно наверняка утверждать, что Java — не последний из языков ООП. Через несколько следующих лет найдется кто-то с новым модным языком, и все прыгнут в этот поезд, думая, что нельзя отставать, и забывая, что большинство программистов в мире всё ещё печатают на клавиатуре на добром старом Cobol! (С последним утверждением можно поспорить.)
Ссылки
правитьЛитература
править- Брюс Эккель. Thinking in Java (4th edition) (Философия Java, 4-е издание).