|
Критический анализ языка PERLИван Головинов (johnheadlong@yahoo.com)Аннотация
Литература АннотацияСтатья посвящена популярному языку программирования PERL. В данной статье представлен детальный анализ и критика основных языковых средств PERL. Подробно рассмотрены все элементы синтаксиса, начиная с лексем и заканчивая средствами поддержки ООП. Особое внимание уделено понятию типа данных и механизму типизации, а также базовым понятиям и концепциям, лежащим в основе модульного и объектно-ориентированного программирования. Осознание изложенных в статье идей не предполагает знакомства с языком PERL, однако необходимо знание какого-либо языка и понимание основных понятий, лежащих в основе программирования, например, подпрограмма, модуль, класс, тип данных и проч. Статья предназначена для всех, интересующихся программированием вообще и языком PERL в частности. Автор надеется, что данная статья будет интересна и полезна как начинающим, так и опытным программистам. ВведениеДанная статья посвящена критике языка PERL и является выражением субъективных взглядов автора на данный язык, сформировавшихся в результате его практического применения в сфере разработки Internet-приложений для e-commerce. Предметом данной статьи является, прежде всего, сам язык PERL, то есть, главным образом, его конструкции ("языковые средства"), а также особенности их использования. При этом встроенные переменные и функции рассматриваются лишь отчасти и только в контексте тех или иных конструкций. Приемы или стиль программирования на языке PERL, равно как и возможности его использования для решения тех или иных конкретных классов задач не рассматриваются вовсе и не являются предметом настоящей статьи. Статья построена по схеме "от простого к сложному", то есть последовательно рассматриваются все основные элементы языка, начиная от лексем и заканчивая средствами поддержки ООП. Web-программированием сегодня не занимается разве что ленивый, так что, предвидя нападки со стороны "корифеев" от Apache+CGI типа "а зачем PERL ведь есть php!", автор желает заявить следующее. Здесь язык PERL намеренно рассматривается в отрыве от тех областей, в которых он получил наиболее широкое применение (автоматизация административных задач в системах типа Unix и Web-программирование). Это обусловлено сильным развитием как самого языка, так и его "окружения" (обилие разного рода библиотек, модулей, классов, а также вспомогательных инструментов), что позволяет рассматривать PERL как, фактически, универсальную среду программирования, практически пригодную для решения очень разных задач, включая не только серверные приложения, но и приложения, ориентированные на конечного пользователя (средства поддержки столь модных нынче графических интерфейсов также имеются). И еще. Автор вовсе не считает себя "гуру" от программирования, однако вполне определенный опыт, полученный при выполнении конкретных проектов, а также разносторонние познания в этой области (заметим лишь, что количество языков программирования, которые освоены автором на практике, переваливает за десяток) дают автору основание и моральное право высказывать свои критические замечания, которые отнюдь не являются следствием эмоций или поверхностных представлений, а суть обоснованные суждения, в чем читателю и предлагается убедиться. Везде, где это уместно, автор проводит аналогии с некоторыми из известных ему языков программирования. Автору хотелось бы сделать еще одно замечание касательно терминологии. По сути, все известные автору реализации языка PERL, включая стандартную, поставляемую в составе Unix-систем, являются интерпретаторами. Однако очевидно, что PERL не является строго построчным интерпретатором, поскольку он выполняет синтаксический анализ всей программы, включая внешние модули, загруженные с помощью предложения Соответственно, при обсуждении вопросов, относящихся к моменту собственно выполнения программы, автор будет употреблять термин "этап выполнения". Если необходимо особо подчеркнуть, что те или иные результаты могут быть получены только на этапе выполнения, и причем, возможно, для получения этих результатов может потребоваться провести специально спланированные испытания, то автор будет употреблять словосочетание "на этапе выполнения при отладке" (некоторые почему-то считают, что отладка это приведение программы к "компилируемому состоянию", что является величайшим заблуждением вообще, а в случае PERL, как будет показано в дальнейшем, особенно). В условиях отсутствия формального определения языка PERL, при написании статьи автор использовал, помимо собственного опыта, документацию, которая входит в "комплект поставки" PERL (главным образом man-страницы), как наиболее авторитетный источник. Кроме того, полезной оказалась книга [1], написанная Larry Wall (автором данного языка), под названием "Programming PERL", вышедшая в издательстве O'Reilly & Associates, которая позволила познакомиться со взглядами автора языка PERL на жизнь вообще и на программирование в частности. При изложении тех или иных фактов, почерпнутых из документации, автор старался давать ссылки на конкретные man-страницы. Все цитаты, приведенные в данной статье, снабжены ссылками на соответствующие источники, список которых приведен к конце статьи. Цитаты из англоязычных источников снабжены переводами. Все, о чем говорится в данной статье, относится к языку PERL версии 5.005. Все упомянутые в статье факты, относящиеся к языку PERL, и все приведенные в тексте примеры проверены как на платформе Unix (Red Hat Linux 6.02), так и на платформе Win32 (Microsoft Windows 95 OSR 2). Все примеры снабжены комментариями, поясняющими ту или иную мысль, которую эти примеры призваны проиллюстрировать. Факты, относящиеся к другим языкам программирования, также проверены автором лично. ЛексемыАнализ языка PERL начнем с исследования его лексем. В данном разделе рассмотрены не все классы лексем языка PERL, а только те, чьи особенности реализации представляют интерес с точки зрения критики. КомментарииВ PERL предусмотрено два типа комментариев: однострочный и документирующий. Многострочный комментарий отсутствует, а ведь "комментарии в общем будут лучшими, когда они помещаются в многострочных блоках, которые чередуются с блоками кода программы. Для этого комментарий должен на высоком уровне описывать, что делают несколько последующих строк кода. Если комментарии попадаются через строчку, то это похоже на чтение двух книг одновременно, причем по строке из каждой поочередно" [7]. Не случайно, наверное, в языках Pascal и C многострочный комментарий является единственным поддерживаемым на уровне языка средством документирования программ. В качестве многострочного комментария в PERL предлагается использовать документирующий, однако это не всегда удобно, поскольку любой таким образом оформленный многострочный комментарий будет включен в выходной файл, генерируемый утилитой perldoc, как часть документации модуля. Кроме того, pod-директивы, используемые при оформлении документирующего комментария, должны начинаться в первой позиции строки (символом "="), что также не особенно удобно при обычном многострочном комментировании. Однако можно применить один трюк, который позволяет включать многострочные комментарии так, чтобы они не попали в выходную документацию. Он состоит в том, чтобы вместо стандартных pod-директив использовать незнакомую perldoc директиву, в результате данная директива не будет интерпретироваться PERL, поскольку в первой позиции строки стоит символ "=", но и perldoc она будет молча пропущена. Кстати, этот трюк описан в man-страницах (именно perlsyn), поставляемых с PERL. Кстати, поддержка документирующего комментария как отдельного синтаксического средства представляется избыточным при должной реализации соответствующей утилиты выделения документации из исходного модуля (в случае PERL это perldoc) можно было бы обойтись многострочным комментарием с включением в него специальных тегов, распознаваемых этой утилитой. У любого здорового программиста в связи с этим возникает один вопрос: а что мешало сделать так, как сделано, скажем, в Java? Это вопрос чисто риторический трюкачество без меры есть стиль программирования, навязываемый языком PERL. ИдентификаторыПравила построения идентификаторов в PERL, мягко говоря, причудливы. Кроме стандартного набора букв, цифр и символа "_" в пользовательских идентификаторах допускается использование символа "'", причем в качестве первого тоже. Зачем и кому это понадобилось непонятно, ведь данная особенность приводит к появлению еще одного (среди бесчисленного множества других) потенциального источника ошибок, которые, правда, можно обнаружить на этапе компиляции. Так, следующий код компилируется исправно: $'Foo = "john"; print($'Foo); А этот приведет к ошибке компиляции: $Foo' = "john"; print($Foo'); На это можно возразить, что, мол, если не нужно не используйте. Однако здесь есть еще одна тонкость, которая связана с модульностью. Дело в том, что в ранних версиях PERL в качестве разделителя элементов квалифицированного идентификатора использовался именно символ "'", причем в целях обратной совместимости это поддерживается до сих пор. А это может привести к ошибке, которую будет очень трудно найти. Например, следующий код будет компилироваться и выполняться без ошибок, но результат, скорее всего, будет отличным от ожидаемого:
$Owner = "John";
print("That's $Owner's house.");
Если только у вас на самом деле нет модуля Owner, который экспортирует переменную s, то на терминале будет напечатано: That's house. Эта тонкость (чтобы не назвать глупостью), к счастью, описана в man-страницах (именно perlmod), откуда и позаимствован приведенный пример. Теперь, кстати, в качестве символа-разделителя в квалифицированных идентификаторах используется составной символ "::". Наверное, ребята ну очень хотели сделать все как в C++ (о чем прямо заявлено в той же perlmod). Но что же предлагается использовать для решения этой проблемы? А вот что:
print("That's ${Owner}'s house."); # блок или элемент хеша?
Ничего, кроме горькой усмешки, это вызывать не может, тем более что с точки зрения здорового программирования гораздо надежнее вместо автоматических подстановок использовать обычную операцию конкатенации. ЧислаИ здесь не обошлось без сюрпризов. Кроме стандартных форм записи целых и вещественных чисел вроде 123 # целое 123.123 # вещественное .5 # при записи десятичных дробей ноль в целой части можно опускать .5E-10 # экспоненциальная форма 0xABCD # шестнадцатеричные числа записываются как в C 0377 # если первая цифра - ноль, то это восьмеричное число PERL поддерживает и такую: 9_123_456 # это, видимо, для "удобства чтения" и даже, как показывает практика, такую: 9_1__2_3___456_____ # что сие означает, надо спросить у Larry Wall Но только будьте осторожны! Ни в коем случае не ставьте символ "_" в начале числа будут сюрпризы, например: $f = _1; print($f + 2); # будет напечатано 2 вместо ожидаемых 3 print(_1 + 2); # а так вообще ничего не будет напечатано, здорово, правда? PERL скорее сделает невозможное, чем выдаст сообщение об ошибке. Ваша программа заработает сразу же и будет работать всегда, не вызывая сообщений об ошибках, только насколько правильно это, как говорится, "тонкий философский вопрос". СтрокиСтроки обычно считаются обычными лексемами, но в PERL это операции. Дело в том, что существует несколько способов обозначения строк, в зависимости от используемого способа PERL выполняет те или иные преобразования над их содержимым на этапе выполнения. Причем для каждого способа существует две формы: сокращенная и полная. При использовании полной формы программисту предоставлена возможность самостоятельно выбрать символ(ы), которые будут использованы как символы-ограничители строки, причем если в качестве символов-ограничителей выбраны скобки (круглые, прямоугольные, угловые или фигурные), то они обязательно должны быть парными, в остальных случаях в качестве начального и концевого ограничителя должен использоваться один и тот же символ. В приведенной ниже таблице (она позаимствована из man-страницы perlop) указаны обе формы для каждого из способов обозначения строк и их назначение, при описании полной формы для определенности в качестве символов-ограничителей использованы фигурные скобки.
Если использованный способ обозначения строки допускает выполнение подстановки, то перед преобразованием PERL, рассматривая подстроки, следующие после символов "$" и "@", встречающихся в строке, как идентификаторы соответствующих скаляров и массивов, вставляет их значения, которые автоматически преобразуются к строковому типу, в строку, как если бы они были указаны непосредственно, причем значения элементов массива сливаются в одну сплошную подстроку. Кроме того, распознаются около 10 esc-последовательностей, начинающихся с символа "\" ("\n", "\t" и проч.). Автоматические подстановки значений переменных в строки, а также разнообразные возможности сопоставления, выделения и замены подстрок по шаблону традиционно считаются сильной стороной языка PERL и одним из главных аргументов, используемых апологетами данного языка. Тем не менее, при ближайшем рассмотрении оказывается, что наличие данных возможностей скорее служит признаком слабости, нежели силы. Необходимость в автоматических подстановках, которые были взяты на вооружение, по всей видимости, у Bourne Shell, более чем сомнительна и, по сути, является опасной, учитывая трудности с идентификаторами, обсуждавшимися ранее. Гораздо надежнее и проще с точки зрения чтения программ использовать операцию конкатенации строк, а не пытаться искать приключения. Шаблоны (регулярные выражения) заслуживают отдельного обсуждения. Однако наличие встроенной непосредственно в язык поддержки такого рода возможностей, как сопоставление с шаблоном, является признаком проблемной ориентированности языка, что неприемлемо для языков общего назначения. В этом наиболее сильно проявляется тот факт, что изначально область применения языка PERL была вполне определенной и далеко не такой широкой, какой она является сейчас. Да, шаблоны полезны и позволяют решать достаточно широкий класс задач, связанных с анализом и обработкой строк, но включение их поддержки непосредственно в язык заметно усложняет его, для языка общего назначения логично было бы вынести поддержку шаблонов во внешние библиотеки. Однако в случае PERL это было бы неприемлемо с точки зрения производительности, так как, если исключить шаблоны, в нем не останется средств эффективной реализации операций обработки строк, поскольку PERL, являясь языком, реализующим концепцию автоматической сборки мусора, не поддерживает указателей и адресной арифметики в явном виде, как это делает, скажем, язык C. Кроме того, для решения подавляющего большинства рядовых задач обработки строк, возникающих при разработке приложений, не связанных непосредственно с синтаксическим анализом, вполне достаточно простых функций, наподобие тех, что реализованы в стандартной библиотеке string языка C, тем более что их использование в подобных случаях гораздо более эффективно. Однако многочисленные руководства типа "секреты и советы" по программированию на PERL, несмотря на поддержку достаточного набора простых функций обработки строк в нем, пропагандируют использование выражений с шаблонами везде, где только возможно, даже там, где их применение ничем не оправдано и только затрудняет понимание текста программы. Так, например, для выделения TLD (top level domain) из полного доменного имени вполне можно использовать простые функции обработки строк
$TLD = "";
$p = rindex($DomainName, ".");
# Если символ "." не найден, то доменное имя не верно,
# и TLD не может быть выделен, в противном случае выделяем TLD функцией substr
if ($p != -1) {
$TLD = substr($DomainName, $p + 1);
}
Однако в мире программистов на PERL такой простой, ясный и эффективный подход, по-видимому, не приветствуется, поскольку нужно слишком много писать и слишком много думать. Еще бы! Ведь если верить словам автора PERL, то " the three great virtues of a programmer: laziness, impatience, and hubris"1 [1]. С их точки зрения, гораздо удобнее написать все, по возможности, в одну строку, например так:
($TLD) = $DomainName =~ m/.*\.(.*)/; # $TLD необходимо заключить в круглые
# скобки, указав тем списковый контекст
Однако несмотря на то, что в первом варианте в пять раз больше строк исходного кода, чем во втором, последний менее эффективен, что легко установить, выполнив приведенные примеры в цикле с контролем времени выполнения при помощи функции Наконец, многочисленные выражения с шаблонами могут весьма затруднить понимание программы, что становится особенно критичным при отладке или доказательстве правильности программ. В связи с этим уместно привести слова Н. Вирта: "Богатый по своим возможностям язык программирования может приветствоваться профессиональным "программистом-писателем", получающим удовольствие от процесса освоения всех его сложных особенностей. Однако интересы "программиста-читателя" требуют от языка программирования разумной скромности в предоставляемых им возможностях. Надо сказать, что к категории "программистов-читателей" могут быть отнесены не только люди (в том числе и сам автор программы), но и трансляторы с различных языков программирования и средства автоматического доказательства правильности программ" [5]. Типы данных, переменные и константыЯзык PERL является нетипизированным языком. В нем начисто отсутствуют базовые типы и механизм статической типизации. Это означает, что в PERL не существует таких понятий, как числовой тип, символьный тип, логический тип, строковый тип, как не существует и возможности обеспечить контроль типов при вычислении выражений и присваиваниях ни на этапе компиляции, ни на этапе выполнения. Значения любых, по сути разных, типов при необходимости преобразуются PERL автоматически на этапе выполнения, так что PERL, с точки зрения программиста, совершенно не в состоянии, например, отличить числовое значение от строкового или указателя, а строковое значение от логического. Автоматические преобразования типов PERL выполняет исходя из контекста, в котором используется та или иная переменная. Контекст определяется, во-первых, операцией, которая должна быть выполнена со значением переменной, во-вторых, оператором, содержащим выражение, в вычислении которого участвует значение переменной, а в-третьих, также специальным символом, указанным в качестве первого символа идентификатора переменной (будем называть его символом контекста, хотя одного этого символа недостаточно для определения результата преобразования типов). Символ контекста является, фактически, неотъемлемой частью идентификатора и указывает тип структуры переменной. Существует 3 типа структур и соответствующих им символов контекста: скаляр (символ "$"), массив (символ "@") и хеш (символ "%"). Поскольку символ контекста является неотъемлемой частью идентификатора переменной, то переменные, скажем, $i = 1; $s = "Number ".i; print($s); # печать строки "Number i" вместо ожидаемой "Number 1" Поиск и исправление такой ситуации может потребовать очень и очень больших усилий, так что за контекстом приходится постоянно следить самому. Правда, при попытке выполнить присваивание переменной без символа контекста будет выдана ошибка компиляции, например:
$i = 1;
print(i++); # ошибка компиляции,
# операция инкремента неявно изменяет значение переменной
И вообще, в виду отсутствия статических типов в языке PERL, у программиста нет возможности явно описывать типы и четко отделять описания типов от описаний переменных, а описания переменных от операций над переменными. А ведь "средства описания типов способствуют не только улучшению прозрачности и повышению надежности программ, но и генерации более эффективного объектного кода" [5]. СкалярыВвиду того, что PERL является нетипизированным языком, скаляр может принимать значения различных типов. Фактически, в качестве значения скаляра может выступать число (целое либо вещественное) либо строка, либо ссылка (адрес) на другую переменную либо функцию. Важным их свойством является то, что скаляры атомарны, то есть содержат одно и только одно значение и не имеют внутренней структуры. В качестве значения скаляра может выступать и специальное значение В PERL отсутствуют символьный и логический типы. Если с отсутствием первого еще можно смириться, то отсутствие второго не вызывает ничего, кроме раздражения. Исходя из руководств, в качестве логического значения Но и это еще куда ни шло по сравнению с тем обстоятельством, что в PERL вообще невозможно различать строки и числа. Дело в том, что PERL всегда представляет числа как строки, а неявные преобразования происходят по мере необходимости при вычислениях выражений и присваиваниях. Возможно, что с точки зрения автора данного языка это "круто", да вот с точки зрения практического программирования это совсем не круто, поскольку чревато такими трудноуловимыми "глюками" (иначе и не назовешь), о каких Larry Wall, наверное, и не догадывался. Вот реальный пример из собственной практики автора. Итак, есть модуль, реализующий набор функций для обработки дат. Функции этого модуля, манипулирующие с датами, принимают значения дат в виде упорядоченных троек чисел, обозначающих, соответственно, год, месяц и день. Кроме того, модуль реализует пару функций, одна из которых для переданного строкового представления даты в виде "YYYY-MM-DD" (а если более точно, то в виде
0: sub ToString {
1: my ($Year, $Month, $Day) = @_;
2:
3: return
4: (
5: $Year < 10 ? "000" : (
6: $Year < 100 ? "00" : ($Year < 1000 ? "0" : "")
7: )
8: ).$Year."-".
9: ($Month < 10 ? "0" : "").$Month."-".
10: ($Day < 10 ? "0" : "").$Day;
11: }
12:
13: sub ToNumberList {
14: my $Date = $_[0];
15: my ($Year, $Month, $Day) =
16: $Date =~ /^(\d{1, 4})-(\d{1, 2})-(\d{1, 2})$/;
17:
18: if (!(defined($Year) && defined($Month) && defined($Day))) {
19: ($Year, $Month, $Day) = (0, 0, 0);
20: }
21:
22: return ($Year, $Month, $Day);
23: }
Проблема была в том, что при попытке выполнить вот такой вызов с совершенно правильным аргументом
ToString(ToNumberList("0800-01-01"));
возвращалась неправильная строка "00800-001-001", которая потом приводила к ошибке там, где она использовалась далее. Причина на самом деле в том, что PERL хранит числа в виде строк, а при вычислениях автоматически преобразует строки в числа и наоборот. Поэтому для того, чтобы решить данную проблему и сделать модуль "пуленепробиваемым", пришлось строку 1 заменить на следующие: my $Year = $_[0] + 0; my $Month = $_[1] + 0; my $Day = $_[2] + 0; а строку 22 на вот это: return ($Year + 0, $Month + 0, $Day + 0); что хоть и выглядит очень глупо, зато работает справно. МассивыМассивы в PERL рассматриваются как упорядоченные линейные множества скаляров. Таким образом, во-первых, на уровне языка поддерживаются только одномерные массивы, а во-вторых элементы массива могут быть, фактически, произвольного типа. Несмотря на то, что в PERL существует возможность блочного конструирования массивов с использованием уже существующих массивов, например так:
@Array = (1, 2, 3);
@NewArray = (@Array, 4, @Array); # это эквивалентно такому определению:
# @NewArray = (1, 2, 3, 4, 1, 2, 3);
в сконструированном таким образом массиве исходные массивы неразличимы, как если бы их элементы были непосредственно перечислены при описании массива. Для работы с многомерными структурами данных приходится вручную описывать массивы ссылок на массивы нужных значений, например так, как это описано в man-странице perllol: @LoL = ( ["fred", "barney"], ["george", "jane", "elroy"], ["homer", "marge", "bart"] ); и при этом иметь в виду контекст при получении доступа к элементам такой структуры:
print(${LoL[1]}->[1]); # печать строки "jane"
print($LoL[1][1]); # как это ни странно, так тоже работает
При увеличении размерности, скажем, до трех, общий подход сохраняется, и придется определить массив ссылок на массивы ссылок на массивы значений. Разыменовывать ссылки при этом не обязательно, так же как и в двумерном случае. Массивы индексируются числами (или строками, представляющими числа), начиная с нуля, то есть первый элемент имеет индекс 0, второй 1 и т. д. Очень интересным является то, что индексы могут быть, во-первых, отрицательными, а во-вторых даже вещественными (это же PERL!). В случае отрицательных индексов отсчет ведется с конца массива, то есть последний элемент имеет индекс -1, предпоследний -2 и т. д. При индексировании вещественными числами (как положительными, так и отрицательными) в качестве индекса используется целая часть числа. Кстати, по какой-то непонятной причине возникает ошибка времени выполнения при индексации массивов вещественными числами, представленными в экспоненциальной форме (с чего бы это, ведь это же так естественно!). Но и здесь можно исхитриться, заключив записанное в экпоненциальной форме число в кавычки, вот так:
@Array = ("x", "y", "z");
print($Array["0E0"]); # печать первого элемента массива @Array
Чудеса, да и только. В PERL отсутствуют статические массивы все массивы являются динамически переопределяемыми. Это, с одной стороны, опасно, а с другой неэффективно с точки зрения времени выполнения. Опасность заключается в том, что PERL не в состоянии надежно контролировать выход индекса за пределы диапазона даже на этапе выполнения, поскольку этот диапазон как таковой отсутствует и может неявно изменяться в ходе выполнения программы (например, при присваиваниях в списковом контексте). Исключение всего одно, оно заключается в том, что при индексации отрицательными числами нельзя выполнять присваивания элементам, лежищм "левее" первого элемента (уж и не знаю, как по-другому это выразить). Таким образом, массивы не могут расти "влево", по мере уменьшения индексов, а только "вправо", по мере увеличения индексов. Все остальное разрешено, например, считывание из несуществующих элементов массива (в том числе и тех, которые "левее"), запись в сколь угодно удаленные "вправо" элементы и т. д. При необходимости, размеры массива массива увеличлежащимя, неинициализированные и несуществующие элементы при обращении к ним возвращают значение
@Array = ("x", "y", "z");
$Array[5] = "a"; # теперь @Array содержит следующее:
# ("x", "y", "z", undef, undef, "a")
Такая динамичность массивов исключает возможность статического распределения памяти, откуда следует, что при считывании значений элементов массива и, особенно, при записи в них, мягко говоря, расходуется время. Кроме того, переопределение массивов (особенно при присваиваниях в списковом контексте) время от времени требует копирования всего содержимого массива в новый, предварительно выделенный блок памяти требуемого размера. Очень интересным является также тот факт, что массиву можно присвоить скаляр, и, наоборот, скаляру можно присвоить массив. В первом случае массив будет состоять из одного элемента, значение которого будет равно значению скаляра. Второй случай более изощрен. Если массив указан как переменная, то значение скаляра будет равно количеству элементов массива. Но если массив указан непосредственно, то значение скаляра будет равно последнему элементу массива. Ниже приведен фрагмент исходного кода, который иллюстрирует данную ситуацию: @Array = (2, 3, 4); $s1 = @Array; # значение $s1 равно 3 $s2 = (2, 3, 4); # значение $s2 равно 4 Таким образом, программист практически лишен автоматического контроля типов, причем не только при присваиваниях, но и при вычислении выражений, и при передаче параметров в функции. А что мешало определить стандартную функцию, которая бы возвращала текущий размер или границы диапазонов индексов массива, как это сделано, например, в языках Java Script (функция
@Array = (2, 3, 4);
$s = $#Array; # значение $s равно 2, первый символ "#" - это не комментарий!
$#Array = 4; # увеличиваем размер массива до 5 элементов,
# неинициализированные элементы получают значение undef
# При выполнении следующего цикла foreach будет напечатано:
# 2
# 3
# 4
# undefined
# undefined
foreach $Element (@Array) {
print((defined($Element) ? $Element : "undefined")."\n");
}
$#Array = 1; # уменьшаем размер массива до 2 элементов,
# "лишние" элементы безвозвратно уничтожаются
print(@Array); # печать строки "23"
ХешиХеши в PERL это динамические структуры данных, элементы которых адресуются произвольными строками символов. Хеши были введены в язык с целью обеспечения поддержки структурного типа, недостаток которого, как было замечено в одном из руководств, так остро ощущался в ранних версиях PERL. Однако можно заметить, что хеши вовсе не являются адекватной заменой структуре (или записи, если говорить в терминах Pascal) сразу по нескольким причинам. Во-первых, хеши имеют динамическую природу, в отличие от статических структур. Это означает, что новые ключи (и связанные с ними значения) могут быть добавлены или удалены в любое время, при этом, естественно, выполняются медленные операции с динамической памятью. Кроме того, любое обращение к элементу хеша приводит, фактически, к необходимости поиска (пусть и очень быстрого) соответствующего ключа, которые, кстати, тоже необходимо хранить, на что расходуется дополнительная память. При работе со статическими структурами вся работа по распределению памяти выполняется еще на этапе компиляции, при этом динамическая память не используется вовсе, а все обращения к элементам структуры выполняются по статическим адресам, вычисленным на этапе компиляции, что полностью исключает потери производительности и какие бы то ни было накладные расходы по памяти. Хранить же имена полей структуры вообще нет никакой необходимости. Согласно документации (man-страница perltoot), хеши еще менее эффективны, чем массивы, в среднем на 10-15%. Во-вторых, вынужденное использование хешей (ввиду отсутствия структурного типа) создает еще один источник потенциальных ошибок, которые могут быть обнаружены только на этапе выполнения при отладке. Дело в том, что обращение к несуществующему ключу не вызовет ошибки или предупреждения ни на этапе компиляции, поскольку это невозможно в принципе, ни на этапе выполнения в результате будет просто возвращено значение Как и в случае с массивами, скаляры можно присваивать хешам, а хеши скалярам. В первом случае будет создан хеш с одним ключом, имя которого равно значению скаляра, и с этим ключом будет связано значение
$s = "foo";
%h = $s;
foreach $Key (keys(%h)) {
print($Key." => ".$h{$Key}."\n"); # будет напечатано "foo => "
}
$s = %h;
if ($s && !defined($h{"foo"})) { # условие сработает,
print($s); # и будет напечатано "1/8"
}
Кстати, присваивания хешам разрешены и для массивов. В этом случае элементы массива рассматриваются как последовательные упорядоченные пары имени ключа и соответствующего ему значения. В случае, если количество элементов массива нечетно, то с последним по порядку ключом будет связано значение
@a1 = ("foo1", 1, "foo2", 2);
%h = @a1;
# При выполнении следующего цикла foreach будет напечатано:
# foo1 => 1
# foo2 => 2
foreach $Key (keys(%h)) {
print($Key." => ".$h{$Key}."\n");
}
@a2 = %h;
# При выполнении следующего цикла foreach будет напечатано:
# foo1
# 1
# foo2
# 2
foreach $Element (@a2) {
print($Element."\n");
}
Таким образом, достаточно неправильно указать символ контекста, и программа начнет работать неправильно, не вызывая при этом никаких сообщений об ошибках или предупреждений ни на этапе компиляции, ни на этапе выполнения. Переменные и константыОбъявление переменных может быть сделано в любом месте, где может стоять оператор, то есть практически везде, даже в выражении инициализации цикла for, в точности такая же ситуация наблюдается в C/C++ и Java. Но если в указанных языках обращение к или присваивание необъявленной переменной вызывает ошибку компиляции, то в PERL ни то, ни другое не вызывает никаких ошибок ни на этапе компиляции, ни на этапе выполнения, что, как и в случае с хешами, является источником потенциальных ошибок, обнаруживаемых только на этапе выполнения при отладке. Чтение несуществующей переменной возвращает значение В PERL, так же как и в большинстве современных языков программирования, например C++, Visual Basic, Java, отсутствуют нормальные средства определения констант. Под нормальными средствами автор понимает такие, которые, помимо очевидного связывания некоторого значения или, возможно, выражения, вычислимого на этапе компиляции, с символическим именем, обеспечивают следующие возможности:
Из всех известных автору языков программирования только язык Pascal, созданный в конце далеких 60-x, а также "выросшие" из него языки Modula и, позднее, Oberon в полной мере обеспечивают нормальные средства определения констант, отсутствие же оных в широко разрекламированных современных "промышленных" языках программирования (это камешек в огород под названием "Java") ничего, кроме раздражения, не вызывает. Например, языки C и C++ предлагают аж три различных средства определения констант (посредством директивы препроцессора Итак, пример определения константы, которая трактуется как неизменяемый скаляр, в языке PERL выглядит так: *e = \2.718282; Вот пример синтаксиса, который ясным ну никак не назовешь! Если константа это скаляр, как это сказано в man-странице perlmod, то зачем здесь символ "*"? Тем более что последующие обращения к константе в данном случае следует записывать как Константы, вследствие того, что это, фактически, скалярные переменные, могут быть любого типа (числовые, строковые, ссылочные), но, опять же вследствие того, что это скаляры, никакого контроля типов не выполняется, при необходимости значения констант преобразуются к нужному типу автоматически исходя из контекста со всеми вытекающими отсюда плачевными последствиями. Использование констант в режиме отладки возможно, ввиду того, что это, фактически, обычные переменные, хранимые в памяти, но, опять же ввиду того, что их значения хранятся в памяти в течение всего времени выполнения программы, использование констант приводит к ничем не оправданным расходам памяти. Чтобы облегчить сопровождение, программист просто вынужден смириться с этими накладными расходами. Как говорится, из двух зол выбирают меньшее. Справедливости ради нужно отметить, что созданные таким образом "константообразные" переменные действительно неизменяемы при попытке присвоить значение такой переменной произойдет ошибка этапа выполнения. Однако на этапе компиляции они не вызывают никаких сообщений. А компилятор Pascal (просто для сравнения) о такого рода ошибках выдает сообщение еще на этапе компиляции. В заключение этого раздела уместно процитировать слова Н. Вирта: "Абстракция, о которой мы не устаем говорить, это важнейшее понятие типа данных, и мы указываем, что его ценность основана на том, что компилятор будет проверять, соблюдаются ли правила, управляющие типами, и именно компилятор будет гарантировать целостность абстракции. Если система не в состоянии это обеспечить, если она допускает выполнение логической операции над числами или не способна идентифицировать доступ к массиву с некорректным индексом просто привожу два примера для иллюстрации то она вряд ли может претендовать на титул "система с языком высокого уровня"" [11]. Операции и выраженияДанный раздел автору хотелось бы начать с цитаты: "В интересах достижения простоты и обеспечения возможности эффективной трансляции в языке Pascal существует очень маленькое число уровней приоритетов операций. В сравнении с языком Pascal язык Algol 60, в котором имеется девять уровней приоритетов операций, кажется слишком причудливым" [5]. А если более точно, то в языке Pascal определено всего четыре уровня приоритетов. В языках Java и C++ их, соответственно, 14 и 16, так что они могут показаться даже еще более причудливыми, чем Algol 60. Ну а в языке PERL имеется 24 уровня приоритетов операций. В этом смысле язык PERL просто расчудесен, достижение простоты языка явно не было приоритетной задачей при его разработке. В приведенной ниже таблице указаны все операции PERL вместе с соответствующими им уровнями приоритета.
Такое изобилие операций и уровней приоритетов есть прямое следствие нетипизированной природы языка PERL, когда в одном выражении могут быть использованы объекты самых разных типов и, соответственно, самые разнообразные операции. А теперь некоторые из операций рассмотрим более подробно. Арифметические операцииНачнем с того, что операции инкремента ( 1: $f = "46e3(*&^"; # первые 4 символа образуют вещественное число 2: print(++$f); # будет напечатано "46001" 3: $f = "Zz9"; # только прописные, строчные буквы и цифры 4: print(++$f); # будет напечатано "AAa0", перенос учитывается! 5: $f = "Z 8 0"; # строка содержит спецсимвол (пробел) 6: print(++$f); # будет напечатано "1" Нужно отметить, что во втором варианте (строки 3 и 4) операции инкремента и декремента НЕ ЭКВИВАЛЕНТНЫ суммированию или вычитанию единицы, поскольку в случае суммирования или вычитания такого рода строки считаются равными числу 0 (как и в третьем варианте, строки 5 и 6). Возможность оперирования с подобными строками является, видимо, уникальной особенностью операций инкремента и декремента. Вообще, применимость арифметических операций к строкам и возникающие из-за этого трудноуловимые ошибки программирования является следствием нетипизированной природы PERL. Реализация процедур инкремента и декремента в виде операций фактически приводит к совмещению понятий "операция" и "оператор" (в данном случае оператор присваивания, более подробно это обсуждается в следующем разделе). Это, вообще говоря, плохо, поскольку операции и операторы выполняют в языке совершенно различные функции: операции, совместно с константами, переменными и функциями, используются для конструирования выражений с целью вычисления того или иного значения, а операторы используются для управления вычислением выражений. Такого рода совмещение (именно операции суммирования или вычитания с оператором присваивания) делает весьма затруднительным (если не невозможным) процесс доказательства правильности программ: "Отметим предположение, что изменяется значение только той переменной, имя которой появляется слева от знака присваивания. Не допускаются так называемые "побочные эффекты", в результате которых изменяются значения других переменных. Если данное допущение нарушается, то в общем случае не будут верными ни аксиома оператора присваивания, ни те правила вывода, которые из нее следуют" [2]. Возможно, именно по этой причине в языке Pascal процедуры инкремента и декремента реализованы именно как стандартные процедуры ( Язык PERL поддерживает операцию возведения в степень, для нее одной предоставлен свой собственный уровень приоритета. Отметим лишь, что наличие встроенной непосредственно в язык операции возведения в степень совершенно излишне ее реализация должна быть вынесена во внешнюю библиотеку, поскольку необходимость в ее использовании возникает достаточно редко и только при программировании приложений, ориентированных на сравнительно узкую предметную область (разного рода прикладные библиотеки численного характера). Это замечание становится тем более верным, что в языке PERL уже присутствуют встроенные функции
sub Power {
my ($Base, $Power) = @_;
return exp($Power*log($Base));
}
Кстати, встроенная функция Операция вычисления остатка от деления
-x div y <> -(x div y) -x mod y <> -(x mod y) Однако, в нарушение данного определения, в PERL (как и во всех остальных упоминавшихся языках) разрешено использование отрицательного делителя при вычислении остатка от целочисленного деления, результаты такого рода вычислений, строго говоря, лишены всякого математического смысла: print((-7) % (-5)); # будет напечатано "-2" print(( 7) % (-5)); # будет напечатано "-3" Здесь -7 div -5 = 1 7 div -5 = -2 Здесь Тем не менее, существуют обобщения операций Справедливости ради нужно отметить, что для положительного делителя операция вычисления остатка от целочисленного деления реализована в PERL (а также в FoxPRO и MatLab) совершенно правильно, в отличие от других упоминавшихся языков, так print((-7) % ( 5)); # будет напечатано "3" print(( 7) % ( 5)); # будет напечатано "2" А вот в языках Pascal, Visual Basic, C и Java операция вычисления остатка от деления реализована неправильно даже для положительного делителя, ниже приведен пример для C.
printf("%i", (-7) % ( 5)); /* будет напечатано "-2", неправильно */
printf("%i", ( 7) % ( 5)); /* будет напечатано "2", правильно */
В первом из указанных случаев результат операции Кстати, согласно man-странице perlop в PERL есть возможность вычислять остатки от целочисленного деления в точности так же, как в C, для этого достаточно указать директиву Ввиду отсутствия механизма типизации и, как следствие, наличия возможности применения одной и той же операции к практически любым типам данных, для конкатенации строк потребовалось ввести отдельную операцию Операции сравненияОперации сравнения в PERL разделены на две группы: операции сравнения на неравенство и операции сравнения на равенство, каждой группе выделен свой уровень приоритета. Такое разделение весьма искусственно и в высшей степени бессмысленно, в связи с введением дополнительного уровня приоритета оно только затрудняет синтаксический анализ. В тех же поистине редчайших случаях, когда необходимо совместить в одном выражении несколько операций сравнения, вполне можно использовать скобки для указания нужной последовательности выполнения операций. Ввиду нетипизированной природы PERL и вытекающей из этого невозможности различать строки и числа в PERL предусмотрено два варианта операций сравнения: один вариант предназначен для сравнения скаляров в так называемом числовом контексте, когда все строки преобразуются в соответствующие им числа с учетом особенностей, обсуждавшихся в предыдущем пункте, а второй вариант предназначен для сравнения скаляров в так называемом строковом контексте, когда все числа преобразуются в соответствующие им строки, поэтому программист вынужден постоянно и очень внимательно следить за контекстом, используя в зависимости от решаемой задачи тот или иной вариант, причем ситуации неправильного использования операций сравнения PERL, естественно, отследить не в состоянии, поскольку все "необходимые" преобразования типов выполняются автоматически за спиной программиста, что чревато трудноуловимыми смысловыми ошибками. Приведенный ниже фрагмент исходного кода иллюстрирует изложенное.
if (1 > 2 != 2 > 1) { # обе операции > выполнятся до операции !=
print("OK"); # и строка "OK" будет напечатана
}
if ("abcdefgh" > 1) { # в числовом контексте "abcdefgh" == 0
print("Эта строка никогда не будет напечатана");
}
if ("abcdefgh" gt 1) { # в строковом контексте 1 eq "1"
print("Эта строка обязательно будет напечатана");
}
Сверх традиционных операций сравнения вроде Прочие операции и особенности выраженийВ этом пункте кратко рассмотрим остальные интересные операции и начнем с операций привязки ( Согласно man-странице perlref примечательной особенностью операции разыменования ссылки Операция повторения Количество логических операций явно избыточно возможности заимствованных из языка C операций Операции диапазона (а их целых две) также лишние вместо них обеих гораздо лучше было бы реализовать такой полезный тип данных, как множество, избавившись таким образом от еще одного уровня приоритета. Лишней является и заимствованная из языка C триарная условная операция Заимствованная из языка C операция При анализе выражений, порой встречающихся в программах на языке PERL, не искушенными в этом языке людьми может возникнуть недоумение в связи с использованием вызова функции в левой части оператора присваивания. Это чистая правда: PERL позволяет использовать две из встроенных функций в качестве леводопустимых выражений, это функции
$s = "abc";
substr($s, 1, 1) = "po"; # а ведь можно не извращаться
# и использовать такую форму: substr($s, 1, 1, "po");
print($s); # печать строки "apoc"
Такое использование функции Функция
%h = ("boo" => 1);
print(scalar(%h)); # будет напечатано "1/8"
keys(%h) = 150;
print(scalar(%h)); # будет напечатано "1/256" - новый размер округлен до 2**8
$h{"baa" => 2);
print(scalar(%h)); # будет напечатано "2/256" -
# для нового элемента используется уже выделенная ячейка
delete($h{"baa"});
print(scalar(%h)); # будет напечатано "1/256" -
# информация удалена, но память не освободилась
keys(%h) = 0; # этот способ не работает
print(scalar(%h)); # снова "1/256"
undef(%h); # нужно так
print(scalar(%h)); # будет напечатано "0"
Опять же непонятно, какая необходимость была в том, чтобы реализовать возможность управления размером хеш-таблицы именно таким изощренным способом? Почему нельзя было сделать это в виде обычной встроенной функции? ОператорыЯзык PERL обладает явно избыточным и, порой, запутанным синтаксисом. Продемонстрировать это главная цель настоящего раздела. А во введении хотелось бы обратить внимание на некоторые общие особенности операторов в PERL. В PERL, как и в C, понятия оператора и выражения почти неразличимы, поскольку выражение может быть в любом месте, где может быть оператор (обратное верно только для операторов присваивания), а любой оператор возвращает какое либо значение. Так, результатом выполнения оператора присваивания будет присвоенное значение (в скалярном контексте) или количество удачно скопированных элементов массива (в списковом контексте), а результатом блока операторов (например, в операторах if, for, while, при выполнении функций) будет результат последнего оператора, выполненного в блоке. Таким образом, следующие строки вполне законны и не вызовут ошибок ни на этапе компиляции, ни на этапе выполнения: "Hallo ".", "."Larry Wall!"; 2 + 2; 1; # так почти всегда завершаются модули PERL - # указывается код, возвращаемый в ОС или в импортирующий модуль И вообще, можно, написав любое слово, что придет в голову, поставить символ ";" в конце строки, и это будет считаться правильным выражением/оператором, например так: I_hate_PERL_-_what_a_tricky_language; Секрет прост слова, не поддающиеся никакой интерпретации, PERL считает строками, заключенными в кавычки, в результате чего получается выражение, а это уже допустимый оператор, о как! Таким образом можно, например, вызвать несуществующую процедуру, просто опечатавшись при наборе ее имени, и потратить много-много часов на отладку неработающего фрагмента ведь вы не дождетесь от PERL сообщения об ошибке или хотя бы предупреждения ни на этапе компиляции, ни на этапе выполнения, так что ошибку придется искать самостоятельно. Операторы присваиванияВ PERL определено аж 16 видов операторов присваивания. Вот они: = += -= *= /= %= **= &= |= ^= <<= >>= &&= ||= .= x= Если сюда еще прибавить операции инкремента и декремента, выполняющие "скрытую" модификацию значения переменной, то получится 18, немыслимое число. Разработчики PERL, видимо, постарались смешать оператор присваивания со всеми операциями, какими только возможно. Ничего, кроме путаницы, в программу это не вносит, тем более что такое смешение, позаимствованное у языка C, изначально было придумано с тем, чтобы при его использовании компилятор мог построить более эффективный код (непонятно, правда, за счет чего). При использовании же языка PERL, который является интерпретируемым, об эффективности не может быть и речи вне зависимости от того, сколько там способов осуществить присваивание и насколько они эффективны, ведь PERL не в состоянии даже предварительно откомпилировать исходный текст в псевдокод, так что "компиляция" происходит при каждом запуске программы. Операторы ветвленияВ PERL существует целых 6 способов осуществить ветвление. Начнем с самых примитивных, взятых на вооружение у Bourne Shell: # Вызов SomeFunction2 произойдет, только если # вызов SomeFunction1 возвращает true # (если только то, что считается в PERL true, можно назвать true): SomeFunction1() && SomeFunction2(); # Вызов SomeFunction2 произойдет, только если # вызов SomeFunction1 возвращает false: SomeFunction1() || SomeFunction2(); Такой способ ветвления возможен именно благодаря тому, что любое выражение в PERL может выступать в качестве оператора. А вот еще один, позаимствованный из C: $Result = (IsSomeCondition() ? SomeFunction1() : SomeFunction2()); И еще пара, чисто в стиле PERL: # Присваивание будет выполнено, только если # вызов IsSomeCondition возвращает true: $a = SomeFunction() if IsSomeCondition(); # Значение $b будет напечатано, только если # вызов IsSomeCondition возвращает false: print($b) unless IsSomeCondition(); В данном случае
if (IsSomeCondition1()) { # здесь блок - не пижонство,
SomeFunction1(); # а жизненная необходимость
} elsif(IsSomeCondition2()) { # секция elsif необязательна,
SomeFunction2(); # их может быть много
} else { # секция else также необязательна
DefaultAction();
}
Следует отметить, что при всем многообразии способов ветвления оператор Операторы циклаОператоров цикла еще больше целых 9. Начнем с наиболее традиционных
for ($i = 10; $i > 0; $i--) { # стиль C "три в одном":
# инициализация, условие продолжения, инкремент
SomeFunction();
}
Второй несколько оригинальней:
$i = 10; # инициализация
while ($i > 0) { # условие продолжения
SomeFunction();
} continue { # инкремент, этот блок необязателен
$i--;
}
Приведенный здесь в качестве примера для цикла А вот еще одна форма цикла
foreach $Element (@Array) { # элементы массива @Array последовательно
# присваиваются переменной $Element,
# размер массива может быть неизвестен
Process($Element); # обработка очередного элемента массива
}
Указание переменной, которой присваивается значение очередного элемента массива, необязательно, в случае ее отсутствия присваивание идет встроенной переменной
for ($i = $#Array; $i >= 0; $i--) {
Process($Array[$i]);
}
Существует, кроме того, пара single modifiers, которые, в зависимости от способа использования, моделируют либо цикл с пред-, либо цикл с постусловием. Рассмотрим первый вариант:
$i = 10; # инициализация
print(" ".$i--) while $i > 0; # печать строки " 10 9 8 7 6 5 4 3 2 1"
print(++$i." ") until $i > 9; # печать строки "1 2 3 4 5 6 7 8 9 10 "
В данном случае условие single modifier вычисляется всегда до вычисления выражения, после которого он следует, даже для single modifier
$i = 10; # инициализация
do {
print(" ".$i--);
} while $i > 10; # перед выполнением цикла данное условие ложно,
# тем не менее, будет выполнена печать строки "10"
do {
print(++$i." ");
} until $i < 10; # перед выполнением цикла данное условие истинно,
# однако цикл начнет свое выполнение и никогда не остановится
Несмотря на то, что Кроме
print("$_\n") foreach @Array; # печать элементов массива @Array в столбик
И, наконец, последний способ организации цикла:
$i = 0; # инициализация
Loop: {
print($i++); # печать с инкрементом ("два в одном")
redo Loop; # оператор управления циклом
} # а теперь нажмите Ctrl + C
А вот этот код не работает так, как ожидается, хотя согласно man-странице (именно perlsyn), описывающей синтаксис операторов цикла, блок
$i = 0; # инициализация
Loop: {
print($i);
next Loop; # переход не работает
} continue { # а вот инкремент выполняется - только один раз
$i++;
}
В данном случае возможность описания блока Автор сих строк считает, что в языке программирования должен быть один и только один оператор цикла оператор Операторы безусловного переходаНа тему оператора безусловного перехода было написано очень многими, очень много и проблемы, им порождаемые, были известны очень давно. Зря старались. Все это, как видно, прошло мимо разработчиков PERL. Поэтому в данном языке существует 4 разных оператора безусловного перехода, причем самый опасный из них реализован аж в трех "ипостасях". Однако, обо всех по порядку. Начнем с операторов управления циклом, их три: Оператор
Loop: while (IsSomeCondition1()) {
DoSomething1();
next Loop if IsSomeCondition2();
DoSomething3(); # не будет выполнено,
# если вызов IsSomeCondition2 возвращает true
} continue { # этот блок будет выполняться при каждой итерации
DoSomething2();
}
Если целью перехода оператора Оператор
Loop: while (IsSomeCondition1()) {
last Loop if IsSomeCondition2();
DoSomething1();
} continue { # этот блок не будет выполняться при выходе из цикла через last
DoSomething2();
}
# last передает управление сюда
Оператор
$i = 0; # инициализация
Loop: {
print($i++); # печать с инкрементом ("два в одном")
redo Loop;
} # а теперь нажмите Ctrl + C
Наконец, рассмотрим оператор
goto SomeLabel; goto ($SomeLabel1, $SomeLabel2, $SomeLabel3) [$i]; goto &SomeFunction(); Наличие первой из указанных форм оператора В заключение этого раздела автору очень хотелось бы процитировать замечательные слова статьи О. Лекарма и П. Дежардена "Дополнительные замечания по поводу языка программирования Pascal": " составление бесконечного списка конструкций не тот путь, по которому стоит идти при разработке новых (более хороших, чем старые) языков программирования. Самой неудачной попыткой движения в этом направлении является создание языка PL/1, хотя неисправимые поклонники и энтузиасты-адепты языка PL/1 требуют включения в него все новых и новых конструкций" [9]. Но, несмотря на это, "программистский мир все время жаждет более мощных языков, а вовсе не ограниченных подмножеств" [11]. В связи с изложенным в данном разделе заявление "PERL is in many ways a simple language"3 [1] может быть воспринято как откровенное издевательство. ПодпрограммыВсе подпрограммы в PERL являются подпрограммами-функциями, возвращающими какое-либо значение, даже если это не определено явно, поэтому далее подпрограммы называются просто функциями. Синтаксис определения и вызова подпрограммСинтаксис определения функций прост до убожества, так что не только программисту, но и компилятору трудно понять, какие параметры должны передаваться и что должно быть возвращено. Вот типичный пример:
sub SomeFunction {
# тело функции
}
С точки зрения программиста, о функции необходимо знать всего три вещи:
Первая и вторая необходимы для правильного вызова функции, а третья для правильного использования результата, возвращаемого функцией. В языках высокого уровня традиционно присутствуют средства описания этой информации средствами самого языка, поскольку это, с одной стороны, позволяет повысить надежность кода за счет автоматических проверок, выполняемых на этапе компиляции, а с другой упростить документирование, сделав код самодокументированным. Ясно, что правильно выбранные идентификаторы функции и параметров косвенно описывают назначение функции и способ ее вызова. Однако эти средства отсутствуют в PERL, таким образом, описания функций на языке высокого уровня PERL не более информативны, чем, скажем, описания подпрограмм на языке ассемблера. О проверках же на этапе компиляции и вовсе говорить не приходится, о чем подробнее рассказано далее. Функции вызываются по имени, после которого следует список параметров, разделяемых запятыми. Здесь интересно то, что, во-первых, список параметров можно не заключать в круглые скобки, а во-вторых, перед именем функции можно указывать символ "&", указывающий так называемый контекст, обозначающий, что это именно вызов функции, а не обращение к переменной или что-либо еще: $a = &SomeFunction 1, "blah-blah-blah", \%MyHash; В ранних версиях PERL символ "&" нужно было указывать при каждом вызове функции, однако теперь это, наконец, стало необязательным, однако при вызове без этого символа PERL, возможно, будет выполнять кое-какие дополнительные проверки на этапе компиляции, которые, впрочем, способны только сбить с толку и привести к появлению дополнительных сюрпризов в программе, о чем будет подробнее сказано далее. В любом случае, наличие нескольких возможностей для реализации даже такой простой вещи, как вызов функции, совершенно ничем не оправдано и приводит к путанице. Наиболее опасно с точки зрения надежности отсутствие необходимости заключать список параметров в скобки, поскольку это может привести к трудноуловимым смысловым ошибкам, которые могут быть устранены только на этапе выполнения при отладке. Вот несколько примеров: print 1 + 2 + 3; # печать "6" print (1 + 2) + 3; # печать "3", а не "6" print(1 + 2 + 3); # печать "6" Поэтому такую возможность использовать не рекомендуется, а программировать следует по возможности так, как это делается в здоровых языках типа Pascal, то есть список параметров заключать в круглые скобки, хотя, естественно, за этим необходимо следить самостоятельно, PERL не будет это контролировать. Но самым интересным является тот факт, что PERL не в состоянии обнаружить отсутствия вызываемой функции на этапе компиляции. Автор сам пал жертвой такой подлости (иначе и не назовешь), опечатавшись однажды и написав Передача параметров и возврат значенийВсе функции в PERL являются функциями с переменным числом параметров (обязательных параметров не существует), что в сочетании с отсутствием механизма типизации исключает не только возможность выполнить проверку правильности вызова функции на этапе компиляции, но даже возможность описать средствами самого языка то, как следует ее вызывать. Это приводит, с одной стороны, к большим трудностям при отладке, поскольку подавляющее большинство ошибок при вызове функций может быть обнаружено только на этапе выполнения, а с другой к необходимости четко документировать каждую, даже самую простую функцию, так как в противном случае разобраться в исходном коде через некоторое время становится практически невозможно. Характерным является то, что на этапе компиляции невозможно проконтролировать число и типы переданных параметров, поскольку, с одной стороны, у программиста, как уже говорилось, нет средств указать компилятору эти сведения при описании функции, а с другой в языке отсутствует механизм типизации, позволяющий отличать строки от чисел, а числа от указателей или массивов. Все параметры, являющиеся глобальными (по отношению к функции) переменными, в функции передаются по ссылке, а константы, естественно, по значению как элементы встроенного массива
sub SomeFunction {
my ($Parameter1, $Parameter2) = @_;
# теперь вместо $_[0] и $_[1] можно использовать
# $Parameter1 и $Parameter2 соответственно,
# при этом гарантируется, что, если переданные параметры являются
# глобальными переменными, то они не изменятся при присваиваниях
# $Parameter1 и $Parameter2 внутри функции
}
Следует обратить внимание на то, что при копировании в локальные переменные, фактически, значений из массива Присваивание элементу массива
sub SomeFunction {
$_[0] = 123; # о типе переданного параметра должен заботиться разработчик
}
Начиная с версии 5.002 язык PERL позволяет при описании функции указывать так называемый "прототип", то есть шаблон, которому должны соответствовать переданные параметры. Шаблон состоит из последовательности символов "$" (обозначает скаляр), "@" (обозначает массив), "%" (обозначает хеш), перед любым из них может стоять символ "\", обозначающий ссылку на соответствующую структуру данных. Можно также использовать символ "*", который обозначает элемент таблицы символов. Обязательные параметры должны быть сгруппированы вначале и отделяются от необязательных символом ";". Вот примеры, приведенные в man-странице perlsub:
Объявление Пример вызова
sub mylink ($$) mylink $old, $new
sub myvec ($$$) myvec $var, $offset, 1
sub myindex ($$;$) myindex &getstring, "substr"
sub mysyswrite ($$$;$) mysyswrite $buf, 0, length($buf) - $off, $off
sub myreverse (@) myreverse $a, $b, $c
sub myjoin ($@) myjoin ":", $a, $b, $c
sub mypop (\@) mypop @array
sub mysplice (\@$$@) mysplice @array, @array, 0, @pushme
sub mykeys (\%) mykeys %{$hashref}
sub myopen (*;$) myopen HANDLE, $name
sub mypipe (**) mypipe READHANDLE, WRITEHANDLE
sub mygrep (&@) mygrep { /foo/ } $a, $b, $c
sub myrand ($) myrand 42
sub mytime () mytime
Проверки на предмет соответствия вызова функции объявленным прототипам выполняются компилятором весьма наивным образом, а при несовпадении контекста он автоматически производит преобразования по правилам, обсуждавшимся ранее в разделе "Типы данных, переменные и константы", так что реально можно проконтролировать только количество переданных параметров, да и то не всегда: если в прототипе указан символ "@" или "%", то все остальные символы прототипа, следующие за ним, во внимание не принимаются. Более того, все эти приносящие мало пользы и еще более запутывающие при сопровождении программ проверки будут выполнены, только если при вызове функции не указан символ "&", если же он указан, то все работает так, будто прототипа и вовсе нет. Результатом выполнения функции, который возвращается в вызывающую программу, считается результат выполнения последнего оператора. Как уже говорилось ранее, каждый оператор в PERL, подобно выражению, возвращает некоторое значение результат выполнения. Для принудительного выхода из функции можно использовать оператор return, заимствованный из C, необязательный параметр которого указывает возвращаемое значение. Поскольку, как уже говорилось, у программиста нет возможности при описании функции указать тип возвращаемого значения, то, фактически, функция может возвращать значения совершенно разных типов. Преобразования типов при использовании результата вызова функции в выражении происходят автоматически, исходя из контекста, так что нужно быть очень внимательным при использовании функций, особенно встроенных, так результат работы большинства из них зависит от контекста, в котором этот результат используется. Так, например, встроенная функция Непонятно, зачем это Larry Wall так сильно старался изобрести велосипед, когда тот не только давно изобретен, но и уже вытеснен более совершенными видами транспорта? Язык Pascal, обеспечивающий здоровые средства описания подпрограмм, их вызова и автоматического контроля соответствия вызовов подпрограмм их описаниям, причем на этапе компиляции, был создан еще в конце 60-х годов. Как видно, автор языка PERL мало знаком с тем, что было достигнуто виднейшими учеными и ведущими специалистами в области языков и технологий программирования за последние 40 лет. Ему, наверное, и не снилось то, что грамотные программисты всего мира освоили уже очень давно. Впрочем, это неудивительно, ведь язык PERL, насколько это известно, разрабатывался средствами языка C, весьма и весьма далекого от совершенства в плане обеспечения надежности и дисциплины при программировании. А результат плачевен: вместо того, чтобы раз и навсегда реализовать ясные и четко определенные средства описания и вызова подпрограмм, язык PERL по мере своего "развития" обогащается все новыми и новыми средствами делать разные трюки. Область видимости идентификаторовДля объявления внутри функции локальных переменных существует 2 способа. Первый (устаревший) заключается в использовании встроенной функции
sub f1 {
local $i = 20;
&f2;
}
sub f2 {
print($i); # печать строки "20" -
# local-переменные доступны в вызываемых функциях
print($main::i); # опять "20" -
# local-переменная в этом блоке ведет себя как глобальная
}
$i = 10;
&f1;
print($i); # печать строки "10" -
# исходное значение глобальной переменной сохранилось
А вот пример для функции
sub f1 {
print($i);
}
$i = 20;
{
my $i = 10;
{
print($i); # печать строки "10" -
# my-переменные доступны из вложенных блоков
print($main::i); # печать строки "20" -
# глобальные переменные тоже доступны
}
&f1; # печать строки "20" - my-переменные недоступны из вызываемых функций
}
print($i); # печать строки "20" - значение глобальной переменной не изменилось
Наличие своей области видимости для каждого блока излишество, вполне достаточно двух областей видимости: область видимости модуля и область видимости подпрограммы. Наличие же Модульное программированиеВ данном разделе покажем, насколько слабо развиты средства модульного программирования в языке PERL, как они примитивны и какими сюрпризами они чреваты. Синтаксис определения модуляХотя в языке PERL и существует понятие модуль (package), синтаксис соответствующей конструкции и, вообще, само понятие определить четко (в виде РБНФ, например) в общем случае довольно сложно. Дело в том, что, в отличие, скажем, от языков с традиционно развитыми средствами модульного программирования, вроде Modula и Oberon, в которых модуль это статическая конструкция, где каждый элемент имеет свое, четко определенное назначение и место, в PERL модуль это, скорее, даже не законченная синтаксическая конструкция, а некий конгломерат объявлений, функций, операторов, выражений, директив импорта и тэгов, причем все это беспрепятственно может быть перемешано в такую кашу (а на практике это не редкость), разобраться в которой будет очень непросто. Примеры такого стиля программирования интересующиеся легко могут обнаружить в стандартной библиотеке PERL, они свидетельствуют лишь о том, что PERL нисколько не способствует повышению дисциплины при программировании, а даже наоборот, поощряет, стимулирует, буквально провоцирует написание программ, не поддающихся никакому сопровождению. Все это усугубляется наличием дополнительных модулей, "облегчающих", "автоматизирующих", да и просто (чего уж там скрывать) "поддерживающих" дополнительные возможности модульного программирования. Не следует думать, что модуль это обязательная конструкция для оформления программ на PERL. Если, скажем, о языке Oberon можно сказать, что ЛЮБАЯ программная система, написанная на нем, есть совокупность модулей, то в отношении языка PERL такого сказать нельзя: программная система на нем в общем случае состоит из модулей, классов (рассматриваемых как некая разновидность модуля о классах чуть позже) и скриптов (файлов, содержащих тело модуля без заголовка). Вот номинальный синтаксис оформления модуля: package SomeModule; # Все, что следует после заголовка модуля, считается телом модуля, # которое оканчивается заголовком другого модуля или концом файла # Можно, хотя это необязательно, в конце модуля поставить тэг __END__ # или тэг __DATA__ (выбирайте сами), после которых и до конца файла # любой текст будет проигнорирован компилятором (правда, в этом случае # вам не удастся описать еще один модуль в этом же файле) # Разработчики PERL так и не сумели определить путный терминатор # модуля, вроде ключевого слова end Недурно, правда? В модулях PERL синтаксисом не предусмотрены, как это сделано в Modula и Oberon, специальные разделы для описания списков импорта и экспорта, типов, констант, переменных, функций, инициализации. Блоки инициализации и завершения, директивы импорта, операторы и выражения, описания функций могут следовать в любом порядке практически без ограничений, а большая часть ошибок, неизбежно возникающих при таком "программировании", может быть обнаружена только на этапе выполнения. Переменные могут использоваться до их объявления и инициализации, а функции до их описания, и такие ситуации не контролируются компилятором все они обрабатываются на этапе выполнения. Все убожество языка PERL, вся его нелепость и глупость очень ярко проявляются хотя бы в том факте, что PERL допускает множественное определение функции с одним и тем же именем в области видимости одного модуля. При вызове функции по этому имени управление будет передано той, что была описана последней, а сообщение об ошибке не появится ни на этапе компиляции, ни на этапе выполнения. Данная "особенность", совместно с неспособностью PERL проконтролировать наличие вызываемой функции на этапе компиляции, способна, мягко говоря, несколько осложнить отладку. Автор однажды сам попался на эту удочку, когда, после внесения в один и тот же модуль изменений разными разработчиками независимо друг от друга и последующего слияния, в итоговом модуле оказались две версии одной и той же функции, причем ошибочная версия, естественно, оказалась последней. Зато мы провели тогда удивительный вечер. Кстати, данную ситуацию не следует путать с механизмом перегрузки функций в стиле C++ существование данного механизма, основанного на различиях в описании принимаемых перегруженными функциями параметров, просто исключено в PERL, поскольку, как уже указывалось в разделе "Подпрограммы", средства описания функций PERL слишком примитивны, чтобы позволить программисту определить принимаемые функцией параметры средствами самого языка это удел документации (если она вообще есть). В PERL существует несколько имен, которые имеют предопределенное назначение и используются интерпретатором во время выполнения для тех или иных целей. Здесь рассмотрим три, предназначенных для описания функций: Блоки
BEGIN {
# вызван первым
}
END {
# вызван последним
}
BEGIN {
# вызван вторым
}
END {
# вызван предпоследним
}
...
Наличие блока инициализации модуля понятно, но почему, спрашивается, для него нет четко определенного синтаксисом места? Почему язык допускает, чтобы код инициализации был разбросан по всему модулю в виде множества блоков? Разве может быть в этом хоть какая-то объективная необходимость? Следующий код, например, вполне законен, но может запросто ввести в заблуждение:
if (0) {
BEGIN {
print("Ку-ку");
}
}
Наличие же "деструкторов" и вовсе бессмысленно при завершении работы интерпретатора все ресурсы будут освобождены автоматически либо самим интерпретатором, либо операционной системой. Блоки Блок Данный механизм, по сути, является избыточным, поскольку в PERL уже есть средство динамической компиляции и загрузки модуля это директива Экспорт и импортСинтаксисом модуля языка PERL не предусмотрена возможность описания списка экспортируемых объектов. Модуль в PERL может экспортировать лишь переменные и функции, поскольку возможность не то что экспорта, но даже описания констант и типов отсутствует. По умолчанию все переменные и все функции являются экспортируемыми, то есть доступными из внешних модулей, импортирующих данный модуль. Для объявления частных переменных модуля можно использовать встроенную функцию
my $PrivateFunction = sub {
# тело функции
};
&$PrivateFunction("boo-boo-boo", 24);
Для импорта модулей предназначены две директивы:
use SomeModule ("SomeVariable", "SomeFunction");
Если список указан, и он не пуст, либо если список не указан вовсе, то модуль будет откомпилирован, загружен и будет вызвана функция Чтобы "облегчить" жизнь разработчику модулей, в стандартную библиотеку PERL включен модуль Надо отметить, что под словом "импорт" в PERL понимается нечто отличное от того, что принято понимать под этим словом в других языках. "Импорт" означает, что при обращении к импортированным объектам можно не использовать квалифицированных идентификаторов, а работать с ними так, будто они описаны в данном модуле. При этом сохраняется возможность обращения к прочим доступным объектам (не my-переменным и всем функциям) с помощью квалифицированных идентификаторов, например так: Наконец, отметим, что все имена, определенные в модуле, хранятся в хеше с именем, совпадающим с именем модуля, с "присоединенным" к нему символом "::". Причем этот хеш и его элементы доступны как для чтения, так и для записи. Возможностью "пополнения" таблицы символов модуля активно пользуются такие стандартные модули PERL, как Объектно-ориентированное программированиеХотя в среде PERL-программистов и принято считать, что в языке PERL присутствуют средства объектно-ориентированного программирования (далее ООП), эти средства являются, по сути, надстройкой над средствами модульного программирования, то есть они реализованы на основе уже имеющихся, слабо развитых, как было показано ранее, средств в виде довольно странного их расширения. Так что при использовании PERL программист для реализации объектно-ориентированных проектов вынужден, фактически, обходиться процедурно-ориентированными средствами, поскольку язык PERL не предлагает ровным счетом НИКАКИХ синтаксических конструкций для поддержки ООП. Еще бы, ведь с точки зрения разработчиков этого языка, "an object is simply a referenced thingy that happens to know which class it belongs to. A class is simply a package that happens to provide methods to deal with objects. A method is simply a subroutine that expects an object reference (or a package name, for class methods) as its first argument"4 [1]. Таким образом, говорить о наличии средств ООП в языке PERL не приходится, можно говорить только о поддержке тех или иных вспомогательных (не синтаксических) средств, которые при должном их использовании позволяют лишь в той или иной степени реализовать основные концепции, лежащие в основе ООП. Из приведенной цитаты видно также, что разработчики PERL мало знакомы с природой ООП и лежащей в его основе объектной моделью. Дело в том, что объектная модель "имеет четыре главных элемента: абстрагирование, инкапсуляция, модульность, иерархия. Эти элементы являются главными в том смысле, что без любого из них модель не будет объектно-ориентированной" [3]. Далее, рассмотрев средства, предлагаемые языком PERL для использования при реализации объектно-ориентированных проектов, станет ясно, что средства эти, равно как и результаты, получающиеся при их использовании, далеки от соответствия тем главным элементам объектной модели, о которых было сказано. Кроме того, "понятие класса служит для того, чтобы дать программисту инструмент для построения новых типов В идеале использование определенного пользователем типа не должно отличаться от использования встроенных типов. Различия возможны только в способе построения" [12]. Как было показано ранее в разделе "Типы данных, переменные и константы", в языке PERL отсутствует механизм типизации, что делает фактически бессмысленными всякие попытки обеспечить поддержку ООП в нем. Здесь же сделаем замечание о терминологии. По словам Г. Буча, "унаследовав от многих предшественников, объектный подход, к сожалению, перенял и запутанную терминологию" [3]. С учетом терминологии, использованной в фундаментальных трудах [3] и [12], здесь для краткости и определенности автор будет использовать следующие термины:
Иногда, где это необходимо, для прочих терминов автор в скобках будет указывать эквивалентные им термины языка C++, выделяя их шрифтом. Синтаксис определения класса, область видимости методов и свойствВвиду отсутствия языковых средств поддержки ООП, в PERL отсутствуют и средства, специально предназначенные для определения класса. Класс, как уже было замечено в цитате из [1], есть модуль, не больше и не меньше. То есть содержимое модуля определяет члены соответствующего класса. Это означает, что:
Функции модуля, используемые как методы соответствующего класса, могут быть запрограммированы так, чтобы их можно было использовать как статические (static) методы (такие методы в PERL называются class methods) либо как виртуальные (virtual) методы (такие методы в PERL называются object methods), либо как и те, и другие. В любом случае для обеспечения возможности использования функций модуля в качестве методов соответствующего класса эти функции должны быть запрограммированы исходя из того, что первым параметром (причем, в отличие, скажем, от C++, доступным внутри метода именно как переданный параметр) в них будет передаваться имя класса, экземпляром которого является данный объект, в виде строки (для class methods) либо ссылка на данные конкретного объекта, с которыми этот метод может оперировать (для object methods). Функции, которые предполагается использовать то как class methods, то как object methods в качестве первого параметра будут принимать неизвестно что (то строку, то ссылку), для преодоления этой трудности в PERL предусмотрена встроенная функция
Связывание структуры данных, соответствующей конкретному объекту, с именем класса, экземпляром которого этот объект является, должно быть выполнено программистом вручную при создании объекта. О создании и удалении объектов поговорим чуть позже. Поскольку, как уже говорилось в разделе "Модульное программирование", все функции модуля, фактически, являются экспортируемыми, то при использовании соответствующего модуля как класса эти функции становятся публичными (public) методами. При необходимости создать частный (private) метод приходится использовать тот же трюк, который применяется при создании частной (не подлежащей экспорту) функции модуля, то есть объявлять частную переменную-ссылку на анонимную функцию. При этом возможность создания защищенных (protected) методов полностью отсутствует. Последние два обстоятельства могут оказаться весьма неприятными: первое потому, что использование ссылок на анонимные функции неудобно (и медленно) по сравнению с обычными функциями, а второе потому, что на практике, как правило, вместо частных методов класса, доступ к которым возможен только для членов этого класса, требуются защищенные методы, что дает возможность предоставить доступ классам-потомкам к методам классов-предков, скрыв их при этом от внешнего мира. Впрочем, последней неприятности можно избежать, реализовав отсутствующую в языке возможность самостоятельно, что чуть позже и будет продемонстрировано. Самым интересным аспектом реализации концепций ООП в языке PERL является то, что представление свойств, то есть собственно данных, лежащих в основе класса, полностью отдано на откуп программисту. Сам по себе язык не предусматривает для этого никаких специальных средств. Как было заявлено в [1], "an object is simply a referenced thingy that happens to know which class it belongs to"5. Таким образом, главное в реализации свойств это обеспечение динамического размещения данных при создании объектов и передачи ссылки на эти данные при вызове методов. Более конкретно об этом чуть позже, а сейчас хотелось бы затронуть такой важный вопрос, как инкапсуляция. Термин инкапсуляция означает "процесс отделения друг от друга элементов объекта, определяющих его устройство и поведение; инкапсуляция служит для того, чтобы изолировать контрактные обязательства абстракции от их реализации" [3]. При этом "чаще всего инкапсуляция выполняется посредством скрытия информации, то есть маскировкой всех внутренних деталей, не влияющих на внешнее поведение" [3]. Причем, естественно, "важным преимуществом ограничения доступа является возможность внесения изменений в объект без изменения других объектов" [3]. Однако при реализации классов на PERL невозможно обеспечить ограничение доступа к их свойствам, поскольку, как уже заявил Tom Christiansen в [1], объект класса это структура данных, к которой можно получить доступ по ссылке. Это означает, что АБСОЛЮТНО все свойства объекта, которые представляют основу его внутренней архитектуры, доступны пользователю этого объекта. Следовательно, забота об обеспечении инкапсуляции (и, таким образом, отчасти сопровождаемости класса) в PERL фактически перекладывается с разработчика класса на разработчиков приложений, использующих соответствующий класс: "while you might know the particular implementation of an object, generally you should treat the object as a black box. All access to the object should be obtained through the published interface via the provided methods. This allows the implementation to be revised, as long as the interface remains frozen (or at least, upward compatible). By published interface, we mean the written documentation describing how to use a particular class. (Perl does not have an explicit interface facility apart from this. You are expected to exercise common sense and common decency.)"6 [1]. Справедливости ради необходимо отметить, что в руководствах по языку PERL (например, в man-странице perltoot) постоянно подчеркивается мысль о том, что "it relies on you to read the documentation of each class"7. Однако есть один весьма замысловатый способ, с помощью которого можно самостоятельно реализовать отсутствующий в языке PERL механизм ограничения доступа к свойствам объекта. Он основан на нерасторопности (в оригинале: lazy) системы автоматической сборки мусора и представлении объекта не как некоторой структуры данных, а как анонимной функции, которая и предоставляет доступ к свойствам объекта (реализованных, в конечном итоге, в виде структуры данных). Для того, чтобы исключить вызов этой функции вне методов класса (ведь это возможно, поскольку значением объектной переменной класса будет указатель на эту функцию) предлагается использовать встроенную функцию Во-первых, данный способ просто неудобен: вместо прямого доступа к свойствам объекта внутри методов класса приходится вызывать функцию. Во-вторых, использование данного способа ввиду его замысловатости приводит к тому, что классы, разработанные на его основе, с трудом воспринимаются, а значит и хуже сопровождаются. В-третьих, чтобы получить доступ к свойствам объекта внутри метода класса необходимо сделать дополнительный вызов функции, да еще по ссылке (не учитывая вызова А как все это соотносится с объектной моделью, лежащей в основе ООП? Как было только что показано, налицо отсутствие инкапсуляции в том смысле, в каком она понимается в [3] и [12]. Также с трудом можно говорить и об абстрагировании в PERL, поскольку в языках программирования вообще, а в объектно-ориентированных языках в особенности основой для представления абстракций является понятие типа данных вместе с механизмом строгой статической типизации: "без контроля типов само понятие абстракции в языках программирования становится пустым и имеющим чисто академический интерес. Абстракция может работать только в языках, постулирующих строгий статический типовой контроль для каждой переменной и функции чтобы об объектно-ориентированных языках можно было говорить всерьез, в них должна быть реализована строгая статическая типизация, которую нельзя было бы нарушить; это дало бы возможность программисту полагаться на компилятор в деле идентификации разного рода несогласованностей" [4]. В языке PERL же, как было показано в разделе "Типы данных, переменные и константы", понятие типа, равно как и механизм статической типизации отсутствуют вовсе. Класс же, с точки зрения автора языка, и вовсе не является типом данных это лишь модуль. Кроме того, в языке PERL отсутствует такой важный элемент объектной модели, как модульность. В [3] дано представление о модульности как элементе объектной модели: "Модульность это свойство системы, которая была разложена на внутренне связные, но слабо связанные между собой модули Модули выполняют роль физических контейнеров, в которые помещаются определения классов и объектов при логическом проектировании системы Объект логически определяет границы определенной абстракции, а инкапсуляция и модульность делают их физически незыблемыми вычленение классов и объектов в проекте и организация модульной структуры независимые действия Процесс вычленения классов и объектов составляет часть процесса логического проектирования системы, а деление на модули этап физического проектирования". Однако нельзя говорить о поддержке языком PERL принципа модульности как самостоятельной концепции в рамках ООП понятия "класс" и "модуль" здесь смешаны довольно странным образом: "A module is just a reusable package that is defined in a library file whose name is the same as the name of the package (with a .pm on the end). A module may provide a mechanism for exporting some of its symbols into the symbol table of any other package using it. Or it may function as a class definition and make its operations available implicitly through method calls on the class and its objects, without explicitly exporting any symbols. Or it can do a little of both"8 [1]. Таким образом, модуль, с точки зрения объектной модели, это контейнер для абстракций, определенных как классы (типы данных), а в языке PERL модуль это контейнер для переменных и функций, которые, в зависимости от способа кодирования, от соглашений, принятых разработчиком модуля и, возможно, оговоренных в документации, и от прочих не имеющих отношения к собственно языку программирования внешних факторов, могут использоваться как модуль программной системы либо как класс, либо как и то, и другое одновременно. В этом проявляется несоответствие понятия модульности в языке PERL и понятия модульности в объектной модели. Кроме того, это несоответствие, в купе с оговоренными выше, приводит к тому, что, вынужденно поддаваясь тлетворному влиянию "PERL-культуры", программист не только начинает с трудом различать основные концепции программирования, но и вовсе перестает правильно понимать их смысл, что может привести к плохому проектированию как процедурно-ориентированных, так и объектно-ориентированных проектов и, как следствие, к огромной массе несопровождаемого кода. На эти замечания можно возразить, что, мол, есть возможность описать несколько классов в одном файле. В этом случае модуль (package) выполняет роль класса, а файл роль модуля, физически содержащего определения классов. Однако такого рода возражения не изменяют положения вещей в принципе сразу по двум причинам:
# Файл SomeModule.pm
package Boo1;
sub New {
print("Hallo! That's Boo1's constructor!\n");
}
package Boo2;
sub New {
print("Hallo! That's Boo2's constructor!\n");
}
1; # это определенно необходимо, иначе "use" не сработает; чудно, правда?
# Файл Test.pl
use SomeModule; # импортируется файл, а не модуль:
# package с именем SomeModule не существует
use ... # далее импортируется еще около 10-15 файлов
SomeModule::Boo1->New(); # can't locate object method "New"
# via package "SomeModule::Boo1" at Test.pl at line 6
Boo2->New(); # Hallo! That's Boo2's constructor! А в каком ты модуле, родной?
О еще одном основном элементе объектной модели о наследовании, а также об особенностях его реализации в языке PERL поговорим чуть позже. Вызов методовКак уже говорилось, методы классов в PERL бывают двух видов: class methods (аналог статических функций-членов в C++) и object methods (аналог виртуальных функций-членов в C++). Принципиальное отличие между ними заключается в том, что первые можно вызывать, не создавая при этом объектов класса. Вторые можно вызывать, лишь предварительно создав объект. В виде class methods оформляются, например, методы-конструкторы, которые необходимо вызывать еще до создания объектов класса. В виде object methods оформляются обычные методы, оперирующие с данными объектов. Для вызова как class methods, так и object methods следует использовать операцию обращения к элементу данных по ссылке use SomeClass; $a = SomeClass->SomeMethod(); Несмотря на то, что здесь в вызове функции Если же слева от операции use SomeClass; $SomeObject = SomeClass->CreateObject(); $SomeObject->SomeMethod(); Несмотря на то, что здесь в вызове функции Наряду с операцией
use SomeClass;
# Вызов class method,
# он эквивалентен такому вызову:
# $SomeObject = SomeClass->CreateObject("foo");
$SomeObject = CreateObject SomeClass "foo";
# Вызов object method,
# он эквивалентен такому вызову:
# $a = $SomeObject->SomeMethod("boo");
$a = SomeMethod $SomeObject "boo";
Поддержка множества различных способов сделать одно и то же, каждый из которых по-своему уникален теми сюрпризами, которые связаны с его использованием, есть одно из основных свойств языка PERL, ведь общепризнанным девизом программирования на PERL является фраза "there's more than one way to do it"9 [1]. Так, вторая форма вызова методов может быть использована как списковый оператор, когда следующий за ним список параметров можно не заключать в круглые скобки, а первая нет, поскольку в этом случае список фактических параметров обязательно должен быть заключен в круглые скобки:
# Класс SomeClass
package SomeClass;
sub SomeMethod {
shift; # удаляем из массива @_ первый элемент имя класса
foreach (@_) {
print($_, " ");
}
print("\n");
}
1;
# Программа-клиент
use SomeClass;
SomeClass->SomeMethod "a", "b", "c", "d"; # ошибка времени выполнения
SomeClass->SomeMethod "a", ("b", "c"), "d"; # ошибка времени выполнения
SomeClass->SomeMethod ("a", "b"), "c", "d"; # будет напечатано: "a b "
SomeMethod SomeClass "a", "b", "c", "d"; # будет напечатано: "a b c d "
SomeMethod SomeClass "a", ("b", "c"), "d"; # будет напечатано: "a b c d "
SomeMethod SomeClass ("a", "b"), "c", "d"; # будет напечатано: "a b "
Однако в остальном обе эти формы эквивалентны, так что в дальнейшем мы будем использовать только первую, имея в виду тот факт, что все, сказанное ниже, в равной степени относится и ко второй. Чтобы интерпретатор имел возможность найти тот класс (модуль), в котором определен вызываемый метод, необходимо предварительно связать объект (структуру данных, на которую указывает ссылка) с именем класса (модуля), экземпляром которого он является. Эта задача обычно решается при создании объекта. Создание и удаление объектовСоздание объектов полностью отдано на откуп программисту, в языке PERL отсутствует понятие конструктора, который бы вызывался автоматически при создании объекта. Таким образом, задача выделения памяти для создаваемого объекта и инициализации возлагается на один или несколько обычных методов класса, которые с точки зрения языка ничем не отличаются от остальных методов. Способ создания объектов класса должен оговариваться в документации класса. Обычно метод, который выполняет роль конструктора, называют В языке PERL отсутствуют как средства объявления переменных объектного типа, так и средства динамического создания таких переменных, вроде операции Ввиду того, что в PERL отсутствуют средства объявления переменных объектного типа, то есть средства создания статических объектов, все объекты являются динамическими. Именно поэтому "an object is just a referenced thingy"10 [1]. Следовательно, основной задачей метода-конструктора является выделение памяти для создаваемого объекта, то есть его свойств, и возврате ссылки на выделенную и, возможно, инициализированную память. Эта ссылка и будет значением переменной "объектного типа". При этом в качестве основы можно использовать любую структуру данных: скаляр, массив, хеш или произвольную их комбинацию. Вся ответственность за способ внутреннего представления данных объекта и способ его использования полностью возлагается на программиста. Но, естественно, чаще всего для представления данных объекта используется хеш как некий аналог структурного типа, хотя это совершенно необязательно: в man-странице perltoot приведен пример использования для этих целей массива. Вот пример простейшего конструктора:
sub CreateObject {
return {
"SomeProperty" => undef,
"AnotherProperty" => undef,
"ListOfSomething" => [
"blah-blah-blah",
0,
"boo-boo-boo"
]
}
}
Если данную функцию поместить в класс (модуль) под названием, скажем, use SomeClass; $SomeObject = SomeClass->CreateObject(); или даже так: use SomeClass; $SomeObject = SomeClass::CreateObject(); В данном конкретном случае оба способа эквивалентны, что еще раз подчеркивает неразрывную связь средств поддержки ООП в PERL с средствами модульного программирования. У созданного таким образом объекта и, соответственно, у конструктора класса есть два больших недостатка. Во-первых, мы в дальнейшем не сможем использовать операцию $SomeProperty = $SomeObject->GetSomeProperty(); Дело в том, что интерпретатор не сможет узнать, в каком классе (модуле) определен вызываемый object method, и выдаст сообщение об ошибке времени выполнения: "Can't call method "GetSomeProperty" on unblessed reference at <имя файла> line <номер строки>". Для того, чтобы можно было вызывать object methods с помощью операции Во-вторых, создав объект, мы сами потом не сможем узнать, какому классу он принадлежит, то есть узнать "тип" объекта. Ведь в PERL нет операций Для этих целей используется встроенная функция
sub CreateObject {
my $Object = {
# определяем свойства объекта
...
}
bless($Object);
return $Object;
}
Однако и в таком виде конструктор далек от совершенства, поскольку он не приспособлен для наследования его классами-потомками. Допустим, что этот конструктор определен в классе Для преодоления этой трудности необходимо выполнить три условия:
Обычно методы-конструкторы реализуются и используются как class methods, поэтому рассмотрим применение указанных условий для этого случая. Использование операции
sub CreateObject {
my $Class = $_[0];
my $Object = {
# Определяем свойства объекта
...
}
bless($Object, $Class);
return $Object;
}
В таком виде конструктор может безболезненно наследоваться, поскольку он будет связывать создаваемый объект с именем именно того класса, экземпляром которого он является, что обеспечит доступ к методам подклассов и правильную идентификацию типа объектов. Более подробно о наследовании поговорим чуть позже. При желании конструировать новые объекты на основе уже имеющихся метод-конструктор будет использоваться как object method, и в этом случае нужно внутри него выделять из переданной в качестве первого параметра ссылки на объект имя класса, экземпляром которого он является, с помощью встроенной функции
sub DuplicateObject {
my $SourceObject = $_[0];
my $Class = ref($SourceObject);
my %ObjectCopy = %$SourceObject;
my $DuplicateObject = \%ObjectCopy;
bless($DuplicateObject, $Class);
return $DuplicateObject;
}
Следующий фрагмент исходного кода иллюстрирует использование обоих конструкторов
use SomeClass;
# Создаем новый объект
$SomeObject = SomeClass->CreateObject();
# PERL не обеспечивает никакой инкапсуляции...
$SomeObject->{"SomeProperty"} = "coo-coo";
# Конструируем новый объект копированием имеющегося
$AnotherObject = $SomeObject->DuplicateObject();
# Изменяем значение скопированного свойства
$AnotherObject->{"SomeProperty"} = "kaa-kaa";
# Значение этого свойства у первого объекта не изменилось
print($SomeObject->{"SomeProperty"}); # будет напечатано "coo-coo"
Здесь необходимо отметить, что функция
$a = {};
$b = $a;
bless $a, BLAH; # слово BLAH будет воспринято как строка в кавычках
print "\$b is a ", ref($b), "\n"; # будет напечатано "BLAH", несмотря на то,
# что вызова bless($b, BLAH) не было
Однако, несмотря на это, при копировании объекта в новую область памяти, что и происходит в конструкторе use SomeClass; # Создаем новый объект $SomeObject = SomeClass->CreateObject(); # Конструируем новый объект копированием имеющегося... $AnotherObject = $SomeObject->DuplicateObject(); # И получаем "безродный" объект print(ref($AnotherObject); # будет напечатано "HASH" Вообще, функцию Если параметр, переданный функции Какой в итоге можно сделать вывод? Средства доступа к методам и свойствам "объектов", также как и средства описания классов и создания объектов, есть просто надстройка над средствами поддержки модульного программирования. В принципе то, что было описано выше, может быть с равным успехом достигнуто и без тех "объектно-ориентированных средств", что предлагает язык PERL, а это, напомню еще раз, всего пара встроенных функций (
# Модуль поддержки "средств" ООП
package OOPSupport;
# Аналог встроенной функции ref
sub TypeOf {
$Object = $_[0];
return $Object->{"CLASS"};
}
1;
# "Класс", реализованный без встроенных средств ООП
package SomeClass;
# "Конструктор", реализованный как "class method"
sub CreateObject {
my $Class = $_[0];
my $Object = {
# Делаем "bless"
"CLASS" => $Class;
# Определяем свойства "объекта"
"SomePoperty" => "blah-blah-blah"
}
return $Object;
}
# "Интерфейсный метод", реализованный как "object method"
sub GetSomeProperty {
$Object = $_[0];
return $Object->{"SomeProperty"};
}
1;
# Программа-клиент
use OOPSupport;
use SomeClass;
# Создаем новый "объект", вызываем "class method"
$SomeObject = SomeClass::CreateObject("SomeClass");
# Получаем доступ к "частным" данным, вызываем "class method"
$SomeProperty = SomeClass::GetSomeProperty($SomeObject);
# А это - "анализ типов времени выполнения"
if (OOPSupport::TypeOf($SomeObject) eq "AnotherClass" {
DoSomething();
} else {
DoNothing();
}
Приведенный выше фрагмент исходного кода есть наглядный пример объектно-ориентированного программирования на не объектно-ориентированном языке. Однако как мало отличается по своей сути этот код от того, что пишут с использованием "средств поддержки ООП" в PERL, в том числе от приведенных ранее примеров! Особенно это видно по реализации класса, она практически не изменилась! С таким же успехом можно заниматься объектно-ориентированным программированием на языке ассемблера. Единственное, что не учтено в этом примере, так это объектная "ориентированность", то есть возможность наследования. Минутку терпения, скоро мы разберемся и с этим. Несмотря на то, что в языке PERL реализован механизм автоматической сборки мусора, он поддерживает деструкторы. Они бывают, как и конструкторы, двух видов: object destructors и class destructors. Object destructor это метод класса с предопределенным именем Деструктор можно вызывать и явно, что особенно актуально в связи с тем, что PERL не поддерживает автоматический вызов деструкторов объектов суперклассов при уничтожении объектов подклассов, если в подклассах определены собственные деструкторы, в отличие от языка C++. Это особенно забавно в свете того, что в PERL, в отличие от C++, как конструкторы, так и деструкторы могут наследоваться, поскольку это обычные методы, причем если деструктор не был переопределен, то деструктор суперкласса будет вызван при уничтожении объекта подкласса, в противном случае он должен быть вызван явно, что сильно обесценивает факт поддержки деструкторов, особенно при разработке сложных иерархий классов, тем более здесь нет тех трудностей, что возникают при попытке реализовать возможность автоматического вызова конструкторов суперклассов, которые в общем случае могут принимать параметры. В любом случае поддержка деструкторов при наличии системы автоматической сборки мусора представляется излишеством, особенно учитывая нерасторопность (в оригинале: lazy), свойственную реализации этой системы в языке PERL, и необходимость явного вызова деструкторов в случае наследования: проще явно вызывать соответствующий обычный метод. Наследование и полиморфизмНачнем с цитаты: "Объектно-ориентированое программирование вышло из принципов и понятий традиционного процедурного программирования. Скажу больше: в ООП не добавлено ни одного действительно нового понятия; просто по сравнению с процедурным оно делает значительно более сильный акцент на двух понятиях. Первое это привязка процедуры к составной переменной Второе понятие это конструирование нового типа данных путем расширения заданного типа" [11]. Реализация первого понятия в языке PERL была рассмотрена ранее, теперь рассмотрим "реализацию" второго. Для управления наследованием, также как и для описания классов, язык PERL не предлагает никаких синтаксических конструкций. Вместо этого для указания классов, от которых наследует данный класс, следует при описании данного класса объявить глобальный массив с предопределенным именем При одиночном наследовании необходимо разрешить три вопроса:
Все три вопроса связаны с вызовом методов и все они разрешаются использованием операции
Из первого пункта этой последовательности видно, что все object methods в языке PERL являются виртуальными. Это и понятно, ведь в PERL нет возможности указать тип объектной переменной при ее объявлении. Однако для управления поиском можно при вызове метода указывать класс, с которого необходимо начать процедуру поиска, например: # Квалифицированные имена методов можно использовать в операции -> $DerivedClassObject->BaseClass::OverridenMethod(@ParameterList); # Indirect object syntax также допускает квалифицированные имена BaseClass::OverridenMethod $DerivedClassObject @ParameterList; Эта возможность используется как внутри методов класса, так и в клиентских программах как средство вызова метода суперкласса при переопределении его в данном классе: переопределение имеет место при описании в подклассе метода с именем, совпадающим с именем какого-либо метода, описанного в одном из суперклассов. Для обращения к методам ближайшего суперкласса в подклассе вместо явного имени можно использовать предопределенное имя Переопределение методов есть единственное средство поддержки полиморфизма в PERL, такая возможность, как перегрузка методов, доступная, скажем, в языке C++, отсутствует, что, вообще говоря, и к лучшему. Однако это достоинство не есть следствие разумного подхода, использованного при проектировании языка, а, скорее, случайный побочный эффект, вызванный тем обстоятельством, что синтаксис определения методов (функций) не позволяет описать средствами самого языка количество, порядок и типы принимаемых ими параметров, так что методы (функции) различаются только по имени, в отличие, скажем, от языка C++, где кроме имени учитывается еще и список параметров. Несмотря на то, что перегрузка методов не реализована в языке PERL, перегрузка операций возможна, причем она реализована с помощью специального модуля Вообще говоря, полезность механизма перегрузки операций весьма сомнительна, не даром от него, как от опасного излишества, отказались при разработке языка Java: "Java не поддерживает перегрузки операций. Данный механизм иногда является источником неоднозначности в программе на C++, и группа разработчиков Java чувствовала, что она вызывает больше неприятностей, чем выгод" [10]. Трудности, связанные с перегрузкой операций, носят творческий характер и кратко могут быть обозначены так: "Операция это не произвольный значок, который позволяет делать все, что угодно!.. Вы можете вполне разумно доказывать, что при конкатенации "прибавляете" одну строку к концу другой, поэтому перегрузка Кроме того, область применения механизма перегрузки операций невелика: "Перегрузка операций была реализована в языке (речь идет о C++ прим. автора) прежде всего для того, чтобы вы могли интегрировать разработанный вами арифметический тип в существующую арифметическую систему языка Этот механизм никогда не предназначался в качестве средства расширения этой системы. Следовательно, перегрузку операций лучше всего применять только в классах, реализующих арифметический тип" [7]. Свойство автогенерации, присущее механизму перегрузки операций в языке PERL, по сути, является опасным, поскольку легко может привести к различным побочным эффектам, которые будет трудно обнаружить: если вы решили перегрузить операцию, то лучше сделать это явно. К счастью, автогенерацию можно легко отключить. В любом случае, перегрузка операций может заметно усложнить сопровождение, поскольку необходимо внимательно следить за тем, какие операции перегружены, а какие нет, какие из операций используются и отключена ли автогенерация. Кроме того, затрудняется отладка, поскольку все ошибки, связанные с перегрузкой операций, могут быть обнаружены только на этапе выполнения. При этом возможны ситуации, когда работает неправильно, не вызывая при этом сообщений об ошибках ни на этапе компиляции, ни на этапе выполнения. Следующие примеры иллюстрируют указанные трудности.
package SomeClass;
use overload
# Следующшие две строки нужны для выполнения первого примера
# "+" => \&AddOperator,
# "=" => \&DuplicateObject;
# Следующая строка нужна для выполнения второго примера
# "++" => \&IncOperator;
sub CreateObject {
my $Class = $_[0];
my $Object = {
"Counter" => 0
};
bless($Object, $Class);
return $Object;
}
sub DuplicateObject {
my $SourceObject = $_[0];
my $Class = ref($SourceObject);
my %ObjectCopy = %$SourceObject;
my $DuplicateObject = \%ObjectCopy;
bless($DuplicateObject, $Class);
return $DuplicateObject;
}
sub AddOperator {
my $Object = $_[0];
my $Addend = $_[1];
# Функция AddOperator обладает побочным эффектом:
# она модифицирует объект, участвующий в вычислении выражения
$Object->{"Counter"} += $Addend;
return $Object;
}
sub IncOperator {
my $Object = $_[0];
my $Addend = $_[1];
$Object->{"Counter"}++;
return $Object;
}
1;
# Первый пример
use SomeClass;
$a = SomeClass->CreateObject();
$b = $a; # конструктор копии не сработает...
$b = $b + 1; # и будут модифицированы оба объекта $a и $b
print($a->{"Counter"}."\n");
print($b->{"Counter"}."\n");
$b = $a;
$b += 4; # автогенерация по умолчанию включена
# и будет вызвана функция SomeClass::AddOperator,
# а конструктор копии опять не сработал
print($a->{"Counter"}."\n");
print($b->{"Counter"}."\n");
# Второй пример
use SomeClass;
$a = SomeClass->CreateObject();
$a++; # конструктор копии не определен, но операция ++ сработает
print($a->{"Counter"}."\n");
$b = $a;
$b++; # ошибка времени выполнения, конструктор копии не определен
Единственным способом решения всех этих трудностей является отказ от автогенерации, явная перегрузка всех необходимых операций, программирование без побочных эффектов. В частности, реализация функции AddOperator должна быть такой:
sub AddOperator {
my $SourceObject = $_[0];
my $Addend = $_[1];
my $DuplicateObject = $SourceObject->DuplicateObject();
$DuplicateObject->{"Counter"} += $Addend;
return $DuplicateObject;
}
В языке PERL все классы являются ПРЯМЫМИ потомками встроенного класса Особенностью класса В принципе, наличие в языке класса-предка по умолчанию (вроде Кроме того, "аргументы (функций прим. автора) класса Интересна реализация множественного наследования в PERL: "The way it works is actually pretty simple: just put more than one package name in your Первая проблема связана с вызовом методов: "When it comes time for PERL to go finding methods for your object, it looks at each of these packages (имена которых включены в массиве Другая проблема связана с предопределенным именем Но самая большая проблема при множественном наследовании связана с конструированием объектов подкласса. Если только базовые классы не являются лишь "объектно-ориентированными" оболочками модулей, реализующими только методы и не имеющими свойств, то при конструировании объекта подкласса необходимо САМОСТОЯТЕЛЬНО обеспечить:
Данные проблемы не могут быть решены на уровне языка PERL, поскольку в нем просто-напросто отсутствуют подходящие для этого средства. В результате все эти проблемы становятся головной болью программиста, что сводит на нет факт "поддержки" множественного наследования в PERL: "Why don't people use MI (multiple inheritance прим. автора) for object methods much? One reason is that it can have complicated side-effects"15 [perltoot]. Вообще, множественное наследование "вещь нехитрая, но оно осложняет реализацию языков программирования" [3]. Кроме того, "множественным наследованием часто злоупотребляют плохо сформированные структуры множественного наследования могут быть сведены к единственному суперклассу плюс агрегация других классов подклассом" [3]. Так что "in practice, few class modules have been seen that actually make use of MI (multiple inheritance прим. автора). One nearly always chooses simple containership of one class within another over MI"16 [perltoot]. Таким образом, поддержка множественного наследования вряд ли может быть оправданной, поскольку его использование приносит больше проблем, чем выгод: "Множественное наследование несет с собой несколько частных случаев, которые должны быть обработаны. Оно добавляет накладные расходы как компилятору, так и исполнительной системе, обеспечивая только минимальную выгоду для программиста" [10]. В заключение данного раздела приведем пример объектно-ориентированной программы, написанной без использования средств поддержки ООП в языке PERL. Основное назначение данного примера продемонстрировать возможность создания такого рода программ и показать, что они ничуть не уступают аналогичным программам, созданным с использованием средств поддержки ООП, предлагаемых PERL, ни с точки зрения производительности, ни с точки зрения надежности, ни с точки зрения автоматического контроля целостности типов. Возможность разработки такого рода примеров доказывает ущербность реализации концепций ООП в языке PERL, которая не предоставляет программисту никаких реальных преимуществ, кроме, разве что, несколько более краткой записи.
# Модуль управления внешними ресурсами
package ExternalResourceManager;
# Функция доступа к внешнему ресурсу
sub GetExternalResource {
print(" Getting external resource... ");
# Нужный код вставьте здесь
print("done\n");
return 1;
}
# Функция освобождения выделенного внешнего ресурса
sub FreeExternalResource {
print(" Destroying external resource... ");
# Нужный код вставьте здесь
print("done\n");
return 1;
}
1;
# Модуль "поддержки" средств ООП
package OOPSupport;
# Эмуляция встроенной функции ref языка PERL
sub TypeOf {
my $Object = $_[0];
return $Object->{"CLASS"};
}
# Эмуляция метода UNIVERSAL::isa языка PERL
sub Is {
my $Object = $_[0];
my $TargetClass = $_[1];
my $CurrentClass = $Object->{"CLASS"};
my $Result = 0;
print("Analysing ".$CurrentClass."...\n");
while (defined($CurrentClass) && !$Result) {
if ($CurrentClass eq $TargetClass) {
$Result = 1;
} else {
$CurrentClass = ${${$CurrentClass."::"}{"SUPER"}};
print(" Analysing ".$CurrentClass."...\n");
}
}
print("done\n");
return $Result;
}
# Эмуляция операции as языка ObjectPascal
sub As {
my $Object = $_[0];
my $Class = $_[1];
my %Object = %$Object; # копируем объект в новую область памяти
my $Result = undef;
if (Is($Object, $Class)) {
$Result = \%Object;
$Result->{"CLASS"} = $Class; # преобразуем тип
$Result->{"VMT"} = \%{${$Class."::"}{"VMT"}};
print(
"Object of class ".$Object->{"CLASS"}.
" converted to an object of class ".$Class."\n"
);
} else {
print(
"Runtime error: ".$Object->{"CLASS"}.
" objects are not type-compatible with ".$Class." objects\n"
);
}
return $Result;
}
# Эмуляция операции -> языка PERL
# Клиенты должны использовать эту и только эту функцию для вызова методов
sub CallObjectMethod {
my $Object = shift(@_);
my $Method = shift(@_);
my $Class = $Object->{"CLASS"};
my $VMT = $Object->{"VMT"};
my $Result = undef;
if (exists($VMT->{$Method})) {
print("Calling object method ".$Method."...\n");
$Result = &{$VMT->{$Method}}($Object, @_);
print("done\n");
} else {
print(
"Runtime error: method ".$Method.
" doesn't exist in class ".$Class."\n"
);
}
return $Result;
}
# Функция для вызова методов суперкласса
# Эту функцию следует использовать только внутри методов класса
# для вызова методов суперкласса, переопределенных в данном подклассе
# Ее также можно использовать для вызова любых унаследованных методов,
# чтобы избежать такого кода: &{$VMT{"InheritedMethod"}}($Object, ...)
sub CallSuperclassMethod {
my $Class = shift(@_);
my $Method = shift(@_);
my $Object = shift(@_);
my %VMT = %{${$Class."::"}{"VMT"}};
my $Result = undef;
if (Is($Object, $Class)) {
if (exists($VMT{$Method})) {
print("Calling superclass method ".$Method."...\n");
$Result = &{$VMT{$Method}}($Object, @_);
print("done\n");
} else {
print(
"Runtime error: method ".$Method.
" doesn't exist in class ".$Class."\n"
);
}
} else {
print(
"Runtime error: ".$Object->{"CLASS"}.
" objects are not type-compatible with ".$Class." objects\n"
);
}
return $Result;
}
1;
# Базовый класс
package BaseClass;
use ExternalResourceManager;
# Имя ближайшего суперкласса
$SUPER = undef; # ну прямо как в Java
# Таблица виртуальных методов
%VMT = (
"GetSomeProperty" => \&GetSomeProperty,
"SetSomeProperty" => \&SetSomeProperty
);
# Для реализации private- и protected-методов можно расширить
# описание %VMT так:
# %VMT = (
# "SomePublicMethod" => [\&SomePublicMethod, $IsPublic],
# "SomePrivateMethod" => [\&SomePrivateMethod, $IsPrivate],
# "SomeProtectedMethod" => [\&SomeProtectedMethod, $IsProtected],
# ...
# );
# Далее необходимо соответствующим образом модифицировать функции
# CallObjectMethod и CallSuperclassMethod модуля OOPSupport, при этом
# проверку на предмет защищенности вызываемого метода следует включить
# лишь в функцию CallObjectMethod - именно она будет использоваться
# клиентом для вызова методов
# Конструктор
sub CreateObject {
print("Running BaseClass constructor...\n");
my $Object = {
"SomeProperty" => "SomeValue",
"SomeExternalResource" => ExternalResourceManager::GetExternalResource(),
"CLASS" => "BaseClass", # это для поддержки "механизма типизации"
"VMT" => \%VMT # VMT не дублируется в каждом объекте, она одна на класс
};
print("done\n");
return $Object;
}
# Деструктор
sub DestroyObject {
my $Object = $_[0];
print("Running BaseClass destructor...\n");
ExternalResourceManager::FreeExternalResource(
$Object->{"SomeExternalResource"}
);
undef($Object);
print("done\n");
return 1;
}
# Интерфейсный метод для чтения
sub GetSomeProperty {
my $Object = $_[0];
print(" Running BaseClass::GetSomeProperty\n");
return $Object->{"SomeProperty"};
}
# Интерфейсный метод для записи
sub SetSomeProperty {
my $Object = $_[0];
my $NewValue = $_[1];
my $PreviousValue = $Object->{"SomeProperty"};
print(" Running BaseClass::SetSomeProperty\n");
$Object->{"SomeProperty"} = $NewValue;
return $PreviousValue;
}
1;
# Производный класс
package DerivedClass;
use OOPSupport;
use BaseClass;
use ExternalResourceManager;
$SUPER = "BaseClass";
%VMT = %BaseClass::VMT; # копируем VMT базового класса
# Переопределяем метод базового класса
$VMT{"SetSomeProperty"} = \&SetSomeProperty;
# "Расширяем" базовый класс
$VMT{"GetOtherProperty"} = \&GetOtherProperty;
$VMT{"SetOtherProperty"} = \&SetOtherProperty;
sub CreateObject {
print("Running DerivedClass constructor...\n");
# Вызываем конструктор базового класса
# Условимся в каждом классе явно определять конструкторы и деструкторы,
# а также явно вызывать конструктор и деструктор ближайшего суперкласса
# Такой подход одновременно прост (конструкторы и деструкторы будут
# вызваны по цепочке для всех суперклассов иерархии) и эффективен (не
# нужно использовать наследование через VMT и медленный вызов
# OOPSupport::CallSuperclassMethod)
my $Object = BaseClass::CreateObject();
# "Расширяем" базовый класс
$Object->{"OtherProperty"} = 0;
$Object->{"OtherExternalResource"} =
ExternalResourceManager::GetExternalResource();
$Object->{"CLASS"} = "DerivedClass"; # не будем забывать о
# "механизме типизации"...
$Object->{"VMT"} = \%VMT; # и собственной VMT, конечно
print("done\n");
return $Object;
}
sub DestroyObject {
my $Object = $_[0];
print("Running DerivedClass destructor...\n");
# Деструкторы, как и положено, вызываются в обратном порядке
ExternalResourceManager::FreeExternalResource(
$Object->{"OtherExternalResource"}
);
BaseClass::DestroyObject($Object);
print("done\n");
return 1;
}
# Переопределенный метод
sub SetSomeProperty {
my $Object = $_[0];
my $NewValue = $_[1];
print(" Running DerivedClass::SetSomeProperty\n");
# Вызов метода суперкласса
# Здесь можно было бы использовать явный вызов:
# BaseClass::SetSomeProperty(@_), но этот подход недостаточно гибок,
# поскольку он не работает в случае, если данный метод реализован не
# в непосредственном суперклассе, а в более раннем предке
my $PreviousValue = OOPSupport::CallSuperclassMethod(
"BaseClass", "SetSomeProperty", @_
);
$Object->{"SomeProperty"} .= " (c) Fictivity Inc.";
return $PreviousValue;
}
sub GetOtherProperty {
my $Object = $_[0];
print(" Running DerivedClass::GetOtherProperty\n");
return $Object->{"OtherProperty"};
}
sub SetOtherProperty {
my $Object = $_[0];
my $NewValue = $_[1];
my $PreviousValue = $Object->{"OtherProperty"};
print(" Running DerivedClass::SetOtherProperty\n");
$Object->{"OtherProperty"} = $NewValue;
return $PreviousValue;
}
1;
# Еще один производный класс
package OtherDerivedClass;
use DerivedClass;
$SUPER = "DerivedClass";
%VMT = %DerivedClass::VMT;
sub CreateObject {
print("Running OtherDerivedClass constructor...\n");
my $Object = DerivedClass::CreateObject();
$Object->{"CLASS"} = "OtherDerivedClass";
$Object->{"VMT"} = \%VMT;
print("done\n");
return $Object;
}
sub DestroyObject {
my $Object = $_[0];
print("Running OtherDerivedClass destructor...\n");
DerivedClass::DestroyObject($Object);
print("done\n");
return 1;
}
1;
# Программа-клиент
use OOPSupport;
use OtherDerivedClass;
$OtherDerivedClassObject = OtherDerivedClass::CreateObject();
print(
"OtherDerivedClassObject SomeProperty is: ".
OOPSupport::CallObjectMethod(
$OtherDerivedClassObject, "GetSomeProperty"
)."\n"
);
print(
"OtherDerivedClassObject OtherProperty is: ".
OOPSupport::CallObjectMethod(
$OtherDerivedClassObject, "GetOtherProperty"
)."\n"
);
OOPSupport::CallObjectMethod(
$OtherDerivedClassObject, "SetSomeProperty", "SomeOtherValue"
);
OOPSupport::CallObjectMethod(
$OtherDerivedClassObject, "SetOtherProperty", 1
);
print(
"OtherDerivedClassObject SomeProperty is: ".
OOPSupport::CallObjectMethod(
$OtherDerivedClassObject, "GetSomeProperty"
)."\n"
);
print(
"OtherDerivedClassObject OtherProperty is: ".
OOPSupport::CallObjectMethod(
$OtherDerivedClassObject, "GetOtherProperty"
)."\n"
);
$ClassName = "BaseClass";
if (OOPSupport::TypeOf($OtherDerivedClassObject) eq $ClassName) {
print("OtherDerivedClass objects are of type ".$ClassName."\n");
} else {
print("OtherDerivedClass objects are not of type ".$ClassName."\n");
}
if (OOPSupport::Is($OtherDerivedClassObject, $ClassName)) {
print(
"OtherDerivedClass objects are type-compatible ".
"with ".$ClassName." objects\n"
);
} else {
print(
"OtherDerivedClass objects are not type-compatible ".
"with ".$ClassName." objects\n"
);
}
$BaseClassObject = OOPSupport::As($OtherDerivedClassObject, "BaseClass");
OOPSupport::CallObjectMethod(
$OtherDerivedClassObject, "SetOtherProperty", 2
); # ok
OOPSupport::CallObjectMethod(
$BaseClassObject, "SetOtherProperty", 2
); # oops!
OtherDerivedClass::DestroyObject($OtherDerivedClassObject);
OtherDerivedClass::DestroyObject($BaseClassObject);
# Ну чем вам не C++?
Вывод этой программы представлен ниже. Running OtherDerivedClass constructor... Running DerivedClass constructor... Running BaseClass constructor... Getting external resource... done done Getting external resource... done done done Calling object method GetSomeProperty... Running BaseClass::GetSomeProperty done OtherDerivedClassObject SomeProperty is: SomeValue Calling object method GetOtherProperty... Running DerivedClass::GetOtherProperty done OtherDerivedClassObject OtherProperty is: 0 Calling object method SetSomeProperty... Running DerivedClass::SetSomeProperty Analysing OtherDerivedClass... Analysing DerivedClass... Analysing BaseClass... done Calling superclass method SetSomeProperty... Running BaseClass::SetSomeProperty done done Calling object method SetOtherProperty... Running DerivedClass::SetOtherProperty done Calling object method GetSomeProperty... Running BaseClass::GetSomeProperty done OtherDerivedClassObject SomeProperty is: SomeOtherValue (c) Fictivity Inc. Calling object method GetOtherProperty... Running DerivedClass::GetOtherProperty done OtherDerivedClassObject OtherProperty is: 1 OtherDerivedClass objects are not of type BaseClass Analysing OtherDerivedClass... Analysing DerivedClass... Analysing BaseClass... done OtherDerivedClass objects are type-compatible with BaseClass objects Analysing OtherDerivedClass... Analysing DerivedClass... Analysing BaseClass... done Object of class OtherDerivedClass converted to an object of class BaseClass Calling object method SetOtherProperty... Running DerivedClass::SetOtherProperty done Runtime error: method SetOtherProperty doesn't exist in class BaseClass Running OtherDerivedClass destructor... Running DerivedClass destructor... Destroying external resource... done Running BaseClass destructor... Destroying external resource... done done done done Running OtherDerivedClass destructor... Running DerivedClass destructor... Destroying external resource... done Running BaseClass destructor... Destroying external resource... done done done done ЗаключениеА что же можно сказать в заключение? Апологеты PERL, наверное, скрежещут зубами за ту горсть горьких пилюль, что пришлось только что проглотить. Они-то знают, что многих мелких неприятностей их тех, что обсуждались выше, при программировании на PERL можно избежать, указывая директивы Автор хотел бы извиниться за порою весьма резкий тон и легкий сарказм, однако уникальным свойством языка PERL является то, что критиковать его можно практически бесконечно, это один из тех языков, в котором все сделано не так, как нужно. В данной статье автор постарался сконцентрироваться на самых основных элементах языка, имеющих принципиальное значение, и рассмотреть их как можно тщательнее. Автор надеется, что ему это удалось. К сожалению, за рамками статьи остались такие важные вопросы, как ввод-вывод и концепция файлов, синтаксис регулярных выражений и средства форматирования текстовых данных, стандартная библиотека PERL, а также некоторые другие. Анализ каждого из них, возможно, потребовал бы написания отдельных статей. Тем не менее, представленный здесь материал обширен достаточно для того, чтобы можно было сделать вполне обоснованные выводы. После всего сказанного можно отметить, что язык PERL является крайне плохо структурированным, в принципе нетипизированным и, как результат, весьма ненадежным языком программирования с избыточным, неясно определенным синтаксисом. Вынужденное использование неэффективных как с точки зрения объемов памяти, так и с точки зрения производительности динамических структур данных, навязываемых данным языком, а также отсутствие механизма статической типизации приводит к тому, что программы, написанные на PERL, крайне неэффективны, что еще более усугубляется необходимостью компиляции программы целиком, включая все используемые ей модули, при каждом запуске. Кроме того, большая часть работ по отладке программ, написанных на PERL, может быть выполнена в принципе только на этапе выполнения, что страшно затрудняет и резко замедляет не только собственно процесс отладки, но также и многократно усложняет процесс сопровождения. Средства, предлагаемые языком PERL для решения даже рядовых задач программирования, зачастую уродливы и опасны сюрпризами и побочными эффектами, они разрушают четкое, структурированное мышление, никак не способствуют повышению дисциплины при программировании, а также стимулируют небрежное проектирование. Регулярное практическое применение данного языка формирует неверное понимание основных концепций, которые выкристаллизовались в ходе развития технологии программирования. В связи с этим можно только сожалеть о том, что язык PERL получил такое широкое распространение, в том числе как одно из основных средств разработки Internet-проектов. Причиной тому, очевидно, является фактическое отсутствие альтернативы, которая была бы такой же распространенной, переносимой и, в то же время, бесплатной. Язык Java, несмотря все его достоинства именно как гораздо более надежного, концептуально выдержанного языка программирования, здесь явно проигрывает, поскольку является, в отличие от доступного в исходных текстах PERL, фирменной и, наверное, недешевой разработкой Sun Microsystems, Inc. Что ж, в данной ситуации остается только надеяться, что, возможно, со временем здесь все же произойдут положительные изменения прогресс не стоит на месте, особенно в такой динамичной области, как программирование. В заключение стоит процитировать слова Н. Вирта: "Да, я убежден, что есть нужда в высококачественном ПО. И придет время, когда будет признано, что стоит вкладывать усилия в его разработку и в использование точного и структурированного подхода на основе безопасных, структурированных языков" [11]. Литература
Владимир, октябрь-декабрь 2001 г.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| © 2000-05 Станислав.ру | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||