Функции
Ни один современный язык программирования не был бы возможен без функций. Функции это "кирпичики", из которых складывается программа. Для работы функциям, подобно обычным программам могут потребоваться входные данные — их называют аргументами, или параметрами функции. Функции так же могут возвращать результат. С этой точки зрения, функцию можно сравнить с "черным ящиком", имеющим несколько входов и один выход. Однако, в отличие классического "черного ящика", функция так же может изменять сами входные значения. Но в целом, смысл функций тот же, что и в математике.
С точки зрения языка К++, каждая функция представляет собой подпрограмму, то есть некоторый участок кода, который функионирует автономно. Сами функции а так же области и примеры их применения были неоднократно рассмотрены в предыдущих главах книги. В этой главе внимание будет уделено синтаксису функций, способам их объявления и некоторым моментам, связанным с их применением.
Содержание |
Объявление
Объявление любой функции начинается с указания ключевого слова function следом за которым указывается тип возвращаемого значения, после которого идет идентификатор имени функции. Указание типа можно опустить, тогда будет подразумеваться, что функция возвращает переменную динамического типа. После указания имени функции идет блок описания параметров, или аргументов функции. Блок заключается в круглые скобки; сами параметры, в ходе описания отделяются друг от друга запятой. Завершает конструкцию тело функции, которое записывается в фигурных скобках.
Вот примеры объявления функций: <source lang="kpp"> function MyFunction() { return 5; } function Compare(x, y) { return x < y; } </source>
Первая функция не принимает параметров, но возвращает числовую константу 5. Согласитесь, не очень полезный код. Вторая функция немного "поумнее": она принимает два объекта и пытается их сравнить, используя оператор отношения "меньше". Результатом выполнения такой функции будет логическое значение "истина", если x и вправду меньше чем y, либо ложь в противном случае. Для правильной работы этой функции требуется, чтобы передаваемые параметры допускали возможность сравнения (то есть, были бы определены соответствующие операторы). Если этого нет, — будет сгенерировано исключение, то есть ошибка времени исполнения. Поскольку типы параметров никак не указаны, компилятор не имеет возможности контролировать фактические типы передаваемых параметров, а следовательно не может предупредить программиста если по его мнению что-то не так. Тем не менее, это обеспечивает программиста возможностью написания гибких программ и функций, которые не зависят от конкретных типов передаваемых данных.
Несмотря на простоту последней функции, подобный код может с успехом применяться в реальных программах. Пример такого кода будет приведен при описании блоков, чуть дальше по ходу книги.
Аргументы
Аргументы функции — это та информация, которую программист хочет передать в функцию ее для последующей обработки. Как уже было показано ранее, в качестве аргументов функций могут передаваться любые объекты, любых типов. При этом, будет генерироваться динамический код, не привязанный к конкретным типам данных. Такие функции могут применяться в случаях, когда они должны принимать в качестве параметров целый набор объектов различных типов. Однако, это негативно сказывается на производительности кода (нет возможности прямого вызова методов и проверки типов). Чтобы повысить эффективность кода, следует применять типизацию аргументов (рекомендуется).
Типизация аргументов
Если функция подразумевает передачу параметров строго определенного типа, то применяется расширенная форма записи аргументов. При этом, идентификатор имени параметра предворяется именем типа, который следует принимать. Например, вышеописанную функцию Compare() мы можем переписать так, чтобы она принимала в качестве параметров только объекты, представляющие собой целые числа. При этом, имена переменных x и y мы дополняем сведениями о типе:
<source lang="kpp"> function Compare(int x, int y) { return x < y; } </source>
Практически не изменившись, функция из динамической превратилась в статическую. Таким образом, компилятор обладает достаточными сведениями для генерации эффективного, статического кода. Так же, в целях уменьшения вероятности ошибок, при компиляции вызовов такой функции, комилятор будет проверять соответствие типов фактически переданных параметров и типов, указанных в объявлении функции. Если типы различны, то компилятор попытается выполнить операцию приведения типов, если же типы неприводимы — будет сгенерирована ошибка времени компиляции.
В общем случае, в описании функции можно указывать параметры любых типов, и даже смешивать типированные и нетипированные параметры:
<source lang="kpp"> function MyFunction(int p1, string p2, block p3) { /* тело функции */ } function OtherFunction(MyClass p1, p2) { /* тело функции */ } </source>
Как видно из кода, функция MyFunction() имеет три параметра: целочисленный p1, строковый p2 и блок p3.
Функция OtherFunction(), в качестве параметров может принимать экземпляры класса MyClass (параметр p1), и объекты любого типа в качестве параметра p2. Обратите внимание, что может показаться что тип MyClass указан для обоих аргументов, но на самом деле это не так. Тип привязывается только к переменной, указанной сразу после него. Более подробно, эта проблема рассмотрена в главе Объявление переменных и констант.
Приведем несколько примеров вызова функции с различными наборами параметров: <source lang="kpp" line="1"> var myblock = { |x| x += 2; } MyFunction(10, "hello", myblock); //верно, типы фактических параметров совпадают MyFunction("20", 10, myblock); //частично верно, выполняется операция приведения MyFunction([1,2,3], myblock, 5); //неверно. переданы неприводимые типы
var o1 = new MyClass; var o2 = new OtherClass; OtherFunction(o1, o2); OtherFunction(o2, o1); OtherFunction(o1, o1); OtherFunction(o2, o2); </source>
- 1
- Мы создаем экземпляр переменной-блока, который будет передаватсья в качестве параметра функциям. Здесь он не имеет особого значения, так что на него можно практически не обращать внимания (важен только его тип).
- 2-4
- Производятся вызовы функции MyFunction() с различными наборами параметров. В первом вызове типы фактических параметров точно совпадают с типами в описании функции, следовательно никакого приведения не происходит и все работает как есть. Во втором вызове происходит приведение строковой константы "20" к числу, а числа 10 к строке. "Частично" верна эта конструкция потому, что на момент компиляции невозможно определить, сработает ли первая операция приведения (строки к числу) или нет. Если строка содержит нецифровые символы, то в результате операции приведения будет сгенерировано исключение. Разумеется в данном случае, все будет хорошо. Второе приведение, а именно числа 20 к строке так же выполнится успешно, потому что абсолютно любое число можно представить в виде строки символов, соответствующих цифрам числа.
- Третий вызов функции MyFunction() является совершенно неверным, поскольку все фактические параметры переданные в функцию, являются неприводимыми к соответствующим типам формальных параметров.
- 6-11
- Здесь создаются две инстанции классов MyClass и некоторого класса OtherClass. Далее выполняется вызов функции OtherFunction(), которой в разных комбинациях передаются вышесозданные объекты. В качестве упражнения, Читателю предлагается самому решить, какие из вызовов верные а какие нет.
- Постарайтесь ответить на следующие вопросы:
- Какие из вызовов приведут к ошибке времени компиляции?
- Какие из вызовов приведут к исключению (ошибке времени исполнения)?
- Что изменится, если реализовать оператор приведения типа MyClass к OtherClass?
- Что изменится, если реализовать оператор приведения типа OtherClass к MyClass?
- Что изменится, если типы будут взаимноприводимыми?
Инициализаторы аргументов (значения по умолчанию)
В некоторых случаях может возникнуть необходимость задания параметров функции по умолчанию, либо возможность указания только части параметров. Например, функция может иметь обширный список аргументов, большая часть которых обычно не используется. То есть, они влияют на некоторые очень специфичные свойства объекта, которые редко применяются. Если такую функцию описывать традиционным образом, то получится что программист, использующий ее в своей программе, должен будет каждый раз писать что-то вроде: <source lang="kpp"> var x = SomeWeirdFunction(1, "a", 0, 0, 0, 0, 0, 0); ... var y = SomeWeirdFunction(2, "b", 0, 0, 0, 0, 0, 0); ... </source>
Такие вызовы портят внешний вид кода и усложняют его написание (особенно если параметры по умолчанию далеко не такие простые, как в этом примере). Желательно было бы сделать так, чтобы функция сама "знала", какие значения параметров нужно указать, если в коде вызова они были опущены. Для этой цели применяются инициализаторы. Инициализаторы аргументов (их еще называют "значениями по умолчанию"), подобно инициализаторам обычных переменных устанавливают начальное значение аргумента, но только если оно не было установлено явным образом при вызове данной функции.
Приведем более приближенный к реальности пример. Допустим, программист решил написать свою реализацию некоторого класса контейнера, служащего для содержания других объектов. Предположим так же, что данный класс подобно массиву позволяет обратиться к своим элементам через индекс и имеет некоторое свойство, показывающее текущий размер коллекции. Программист желает написать функцию, возвращающую некоторое подмножество данной коллекции, расположенное между верхним и нижним индексами. Логично предположить, что может возникнуть желание указать только один из индексов, в то время как другой должен подставиться как граница коллекции с соответствующей стороны. То есть, в случае нижнего индекса, граничное значение будет равно нулю, в то время как граничное значение для верхнего индекса заранее не определено. Фактически, оно равно количеству размещенных в коллекции элементов минус один (если нумерация ведется с нуля). Разумеется, это число меняется по мере добавления и удаления элементов. Если бы инициализаторов не существовало, программисту приходилось бы каждый раз явно указывать верхнюю границу выборки на основании его представления о текущем количестве элементов в коллекции (еще одно потенциальное место для ошибки). Это по меньшей мере неудобно. По большей — невозможно, ведь не всегда заранее известно, какое количество элементов содержится в коллекции в данный момент. В общем, с учетом существования инициализаторов, интерфейс класса коллекции мог бы выглядеть примерно так:
<source lang="kpp"> class MyCollection {
public function int Add(...); //добавление элемента private function GetLength(); // размер коллекции property length read GetLength; const function MyCollection SubCollection(int from = 0, int to = this.length - 1);
} </source>
В первой строке объявляется метод добавления элемента в массив. Для удобства применения класса, он оформлен в виде функции с переменным списком аргументов (см. ниже). Если говорить коротко, то это функция, которая может принимать различное (сколь угодно большое) количество параметров в зависимости от вызова. При этом, они не описываются в заголовке функции, а вместо них ставится оператор-многоточие (...). Это необходимо, чтобы отличить такую функцию от обычной функции. При вызове, фактические параметры автоматически добавляются в массив, который можно использовать внутри функции.
Далее следуют аксессор, возвращающий текущее количество элементов в коллекции и свойство, связанное с ним.
Последняя строка как раз представляет для нас интерес. Как видно из описания, метод SubCollection() возвращает некоторе подмножество класса, которое само является экземпляром данного класса; границы выборки задаются двумя параметрами: нижним индексом from и верхним to. Аргументы, соответственно, инициализированы нулем и выражением доступа к свойству length, которое будет возвращать значение верхнего индекса. Поскольку в выражении есть ссылка на текущий экземпляр (this), то значение выражения будет зависеть от того, метод какого экземпляра вызывается в каждом конкретном случае.
Вот пример некоторого участка программы, использующей вышеописанный класс: <source lang="kpp"> var c = new MyCollection; MyCollection.Add(1, "a", [2], { |x| x + 1; }, { 1 => 'a', 2 => 'b'}); var sub1 = c.SubCollection(); var sub2 = c.SubCollection(3); var sub3 = c.SubCollection(1, 4); </source>
Модификаторы и копирование
Довольно часто создаются функции, принимающие в качестве параметров не отдельные переменные, вроде чисел строк или блоков, а целые массивы. Например, программист может захотеть написать функцию, рассчитывающую математическое ожидание некоторой величины. При этом, в качестве параметров функция будет принимать массивы значений и соответствущие значениям вероятности; вычисленное значение математического ожидания возвращается в качестве значения функции.
Математическое ожидание, это величина соответствующая наиболее вероятному из значений исходной величины. Для набора дискретных величин (а именно такой случай мы и рассматриваем) она рассчитывается, как сумма произведений значения величины на ее вероятность:
Приведем пример функции, реализующей эту формулу: <source lang="kpp" line="1"> function real AOD(array values, array probs) {
if (values.size != probs.size) //проверка исходных данных throw "массивы должны иметь одинаковое количество элементов";
var int i, real M; //счетчик и накопитель для величины математического ожидания for(i = 0; i < values.size; ++i) M += values[i] * probs[i]; return M;
} </source>
- 1-2
- Для того чтобы значение можно было рассчитать, размеры исходных массивов должны совпадать. Если они не совпадают, то генерируется исключение с соответствующим сообщением об ошибке.
- 4
- Здесь мы объявляем переменную счетчик, которая будет использоваться в цикле и переменную, накапливающую сумму, то есть конечную величину математического ожидания.
- 5-8
- Для рассчета значения применяется цикл for, который перебирает переменную-счетчик i начиная с нуля и заканчивая последним индексом массивов. Тело цикла представляет собой вышеописанную формулу. Результат возвращается с помощью оператора return.
Теперь, если мы создадим два массива и заполним их некоторыми значениями, то мы сможем вычислить для них значение математического ожидания:
<source lang="kpp">
var v = [-3, -2, -1, 0, 1, 2, 3 ]; //значения
var p = [0.01, 0.05, 0.2, 0.3, 0.35, 0.05, 0.04]; //вероятности
var avg = AOD(v, p);
</source>
С точки зрения математики вышеприведенный пример верен. Однако, существует одна проблема, которая может существенно сказаться на производительности работы системы и на объемах используемой памяти. Дело заключается в том, что при передаче параметров происходит копирование объекта параметра с передачей в функцию уже его копии. Это сделано для того, чтобы функция могла свободно распоряжаться параметрами, как обычными локальными переменными, без опасности случайно изменить значение внешней переменной.
Таким образом, в вышеприведенном примере при вызове функции AOD(), будет произведено копирование двух переданных массивов, а значит и всех их элементов. Естественно, чем больше размеры массивов, тем больший объем данных приходится копировать. На практике, статистические массивы могут иметь тысячи и даже десятки тысяч значений!
Чтобы решить эту проблему, мы должны указать компилятору, что объекты параметров копировать не нужно. Мы, в свою очередь "обещаем" не изменять их значения, так что вызывающий код будет "спокоен", что с переданными объектами ничего не случится. Это осуществляется с помощью ключевого слова-спецификатора const, которое указывается в блоке описания параметров непосредственно перед самим параметром. Оно указывает, что данный параметр не будет изменяться в функции, что позволит компилятору произвести соответствующую оптимизацию. При этом, внутрь функции будет передан сам объект, точнее ссылка на него. Разумеется, компилятор так же следит за тем, чтобы в коде функции не было попыток изменить значение такого параметра, а если это все же случится, — будет сгенерирована ошибка времени компиляции.
Перепишем вышеприведенный пример с использованием спецификаторов const: <source lang="kpp"> function real AOD(const array values, const array probs) {
if (values.size != probs.size) //проверка исходных данных throw "массивы должны иметь одинаковое количество элементов";
var int i, real M; //счетчик и накопитель для величины математического ожидания for(i = 0; i < values.size; ++i) M += values[i] * probs[i]; return M;
} </source>
В вышеописанной функции, массивы используются только для вычисления значения математического ожидания и их изменения не происходит, так что применение спецификатора никак не скажется на реализации самой функции (тело функции фактически осталось неизмененным), но теперь код сможет работать намного быстрее (при большом количестве элементов). Это как раз тот случай, когда внешне незначительные изменения программы существенно влияют на ее производительность. Поэтому, хороший программист всегда должен держать в уме особенности конкретного языка программирования и учитывать их при написании программ.
Но что если нам необходимо решить прямо противоположную задачу? А именно, передать в функцию некоторый объект, для его изменения внутри функции. Если просто написать код вида:
<source lang="kpp">
function mutate(int x) {
println("x = #{x}"); // вывод: x = 5 x = 10; println("x = #{x}"); // вывод: x = 10
} ... var v = 5; mutate(v); println("v = #{v}"); // вывод: v = 5 </source> ...то ничего не получится. Да, переменная v будет передана внутрь функции mutate(). Но при передаче параметра произойдет его копирование. Таким образом, внутри функции будет изменено значение копии, которая будет уничтожена при выходе из функции. Изначальная переменная как была установлена, так и осталась равной пяти.
Чтобы указать компилятору, что в функцию следует передавать сам объект, а не его копию — следует указать ключевое слово-спецификатор var (по аналогии с const). При этом, копироваться аргумент не будет но будет возможно его изменение в теле функции:
<source lang="kpp"> function mutate(var int x) {
println("x = #{x}"); // вывод: x = 5 x = 10; println("x = #{x}"); // вывод: x = 10
} ... var v = 5; mutate(v); println("v = #{v}"); // вывод: v = 10 </source>
Функции с переменным списком аргументов
Бывают случаи, когда заранее не известно, какое количество параметров должна принимать функция. В качестве примера можно привести задачу форматируемого вывода. Форматируемый вывод является удобным средством формирования строки, содержащей как текст так и значения некоторых переменных, связанных с этим текстом. Предположим, что на основании статистических данных, нам надо вывести строку следующего содержания:
Пользователь vpupkin (501) обращался к файлу '~/docs/index.txt' 122 раз(а).
Данная строка содержит как чисто текстовую информацию, так и значения, которые мы должны "внедрить" в текст. "Лобовое" решение проблемы может выглядеть примерно так:
<source lang="kpp"> var myfile = '~/docs/index.txt'; //имя файла var fstat = File.stat(myfile); //получение информации о файле var uname = 'vpupkin'; //имя пользователя var userid = 501; //системный идентификатор пользователя print("Пользователь " + uname + "(" + userid +
") обращался к файлу '" + myfile + "' " + fstat.times_accessed(userid) + " раз(а).\n");
</source>
Согласитесь, функция вывода выглядит не очень привлекательно. Она изобилует операторами конкатенации строк и прочей информацией, которая разбросана по всему вызову. Попробуем ее переписать с помощью оператора-вставки (см. главу Стандартные типы данных):
<source lang="kpp"> print("Пользователь #{uname} (#{userid}) обращался к файлу #{myfile} " +
"#{fstat.times_accessed(userid)} раз(а).\n");
</source>
В такой реализации, код стал намного более читаемым, однако есть еще одна проблема. Что если впоследствии потребуется написать версию программы, поддерживающую другой язык интерфейса? Пришлось бы либо писать свою копию программы для каждой языковой версии, либо каким-то образом отделить строковые данные от вставок и вынести их в отдельный файл ресурсов. Тогда, для того чтобы перевести интерфейс программы на другой язык, достаточно будет всего лишь отредактировать файл ресурсов, без необходимости изменения текстов программы.
Хорошим решением для этого является форматируемый вывод. При этом, операция вывода сводится к указанию строки-формата, а так же набора значений переменных, которые необходимо внедрить в текст. Для вышеприведенного примера, строки формата в зависимости от языка будут выглядеть так:
Пользователь % (%) обращался к файлу '%' % раз(а).\n User % (%) accessed file '%' % times.\n
Каждый символ % соответствует одному значению переменной из последующего списка. При разборе (форматировании) такой строки, каждый символ % будет замещен на значение соответствующей переменной. Для вывода самого символа % применяется escape последовательность из двух подряд идущих символов %%.
Таким образом, в функцию форматирования необходимо передать как минимуму один параметр — формат. Дальнейшие параметры и их количество будут зависеть уже от самой строки формата. Но как это описать в терминах языка программирования? Одним из решений может быть передача в функцию массива с параметрами. Тогда, код описания функции форматируемого вывода и ее вызова может выглядеть так:
<source lang="kpp"> function printf(const string format, const array args); //... printf("Пользователь % (%) обращался к файлу '%' % раз(а).\n",
[uname, userid, myfile, fstat.times_accessed(userid)]);
</source>
Для вывода результирующей строки, мы должны искать вхождения символа % в строке формата и подставлять значение текущего элемента массива. После подстановки, текущим элементом становится следующий элемент массива.
Более красивое решение заключается в использовании оператора-многоточие (...) в объявлении функции, который указывет, что функция должна принимать переменное количество аргументов:
<source lang="kpp"> function printf(const string format, ...); //... printf("Пользователь % (%) обращался к файлу '%' % раз(а).\n",
uname, userid, myfile, fstat.times_accessed(userid));
</source>
Теперь, код вызова такой функции ничем не отличается от вызова любой другой функции или метода. Программисту не нужно помнить об особенностях функции — нужно просто записывать параметры один за одним. Обладая всей необходимой информацией, компилятор языка сам сформирует массив передаваемых параметров, который будет передан в функцию. Для разбора аргументов внутри функции используется специальный метод arg_array(), возвращающий массив аргументов, либо arg_list(), возвращающий их список.
Примечание: на самом деле, приведенная в примерах функция printf(), реализована как расширение класса string стандартной билбиотеки языка К++. Желающие реализовать форматируемый вывод могут ее использовать. Здесь же она была приведена, как наиболее подходящий пример функции с переменным списком аргументов.
Возврат значения
Для выхода из функции применяется оператор return, после которого указывается выражение, значение которого необходимо вернуть в качестве значения функции. Если функция ничего не возвращает, то есть тип результата определен как void, то выражение результата не указывается. Если тип возвоащаемого значения не указан, то подразумевается что функция возвращает нетипированную переменную; если тип указан явно, то при возврате значения, тип результата выражения будет приведен к типу результата. Если типы неприводимы, то будет сгенерирована ошибка времени компиляции.
Приведем примеры объявления функций с различными типами возвращаемых значений и прокомментируем каждый из случаев: <source lang="kpp"> function F1(int x) {
if (x > 5) return "hello world"; else return x;
} function string F2() { return 10; } function void F3() { return; } </source>
Первая функция возвращает переменную динамического типа, которая может быть либо строкой, либо числом в зависимости от значения аргумента. Вторая функция возвращает строку, но в качестве выражения в операторе return указана константа типа int, поэтому будет выполнена операция приведения. Третья функция не возвращает результата, поэтому оператор return записан без выражения.
Локальные переменные
Существуют несколько видов переменных, каждый из которых объявляется в своей области и действует в ее пределах. Глобальные переменные объявляются в контексте пакета (файла) и действуют в его пределах. Такие переменные могут быть доступны из любой функции или метода, объявленных в том же пакете. Переменные, объявленные в теле класса называются полями. Их область применения ограничена методами этого класса и его потомков (более точно поведение задается с помощью спецификаторов доступа); за пределами класса такие переменные не видны.
Самой узкой областью действия обладают локальные переменные. Они объявляются и действуют в пределах отдельной функции, или даже ее части. Локальные переменные служат для хранения промежуточных результатов рассчетов, хранения состояния локальных объектов и другой временной информации. При контекста управления из области действия переменой (например при выходе из функции) она уничножается.
Объявление
Объявление локальных переменных ничем не отличается от объявления переменной в любой другой области. Переменные могут объявляться в любом месте функции или метода. Единственное, что нужно иметь в виду: переменная должна быть объявлена до места ее использования. Хорошей практикой являетсяобъявление переменных как можно ближе к месту их применения. Одними из существенных недостатков языка программирования Паскаль можно назвать отсутствие возможности объявления переменных прямо по ходу программы. Видимо, авторы Паскаля считали, что это дезорганизует программиста и делает его код менее читаемым. Однако, практика показывает, что при программировании достаточно больших процедур и функций это требование только мешает. Программисту приходится "бегать" вперед-назад от текущего кода к блоку описания переменных и наоборот. При этом, вместо собственно решения проблемы, он занимается оформлением кода и разбором получившейся кучи локальных переменных, сваленных в одном месте. Это сильно отвлекает и в конечном счете ведет к появлению ошибок. Практика объявления переменных по мере использования помогает программисту сосредоточиться на текущем месте программы, не отвлекаясь от него и имея весь код "на виду".
Область видимости
Как уже было сказано выше, каждая переменная действует в пределах своей области видимости. Для локальных переменых, областью видимости является тот блок программы, в пределах которого она была объявлена. Она будет доступна в пределах всего блока, а так же во всех вложенных блоках. Если во вложенном блоке объявляется переменная с тем же именем что уже была объявлена ранее, то этому имени в коде программы будет сопостовляться внутренняя переменная. При изменении внутренней переменной значение внешней не меняется. За пределами блока переменная считается необъявленной, то есть ее использование в выражениях приведет к ошибке времени компиляции.
Ниже приведен пример некоторой функции, в которой рассмотрены типовые ситуации, возникающие при работе с локальными переменными. В качестве упражнения, постарайтесь ответить на вопрос, какими будут значения переменных на момент вызова функции print() в каждом из случаев. Записывайте ваши ответы по ходу разбора листинга, а затем сравните их с контрольными ответами.
<source lang="kpp" line="1"> function f(const int p) {
var x = 1, k = 2; print("x = #{x}, k = #{k}\n"); //(1) { x = 5; var k = 3; print("x = #{x}, k = #{k}\n"); //(2) } print("x = #{x}, k = #{k}\n"); //(3)
if (p > 5) { print("x = #{x}\n"); //(4) } else { var x = 10; var y = "hello"; print("x = #{x}, #{y}\n"); //(5) x = 6; } print("x = #{x}\n"); //(6) print("#{y}"); //(7)
} </source>
Примечание: В вышеприведенном коде, в строках 5-9 применен inline блок, который применяется для изоляции локальных переменных в некоторой области. Это может быть удобно для того чтобы быть уверенным что используемые переменные будут своевременно освобождены. Примером такой ситуации может послужить работа с файлами. Предположим, что мы должны прочитать некоторый файл, после чего выполнить долгую и вычислительно сложную операцию по его обработке. Для обеспечения целостности существует требование, чтобы работа с файлом происходила в эксклюзивном режиме, то есть в один момент времени с файлом может работать только одно приложение, в то время как доступ других будет заблокирован.
Если мы реализуем код вида: <source lang="kpp"> function ProcessFile(const string filename = '/var/db/mydb') {
var dbfile = OpenDB(filename); var db = ReadDB(myfile); ProcessDB(db);
} </source> ...то произойдет следующее: сперва будет выполнена операция открытия файла и чтения его содержимого в функции ReadDB(). После этого, будет выполнена операция ProcessDB() в течение которой файл попрежнему будет открыт в эксклюзивном режиме и другие программы не смогут его использовать. На самом же деле, нашему приложению файл уже не нужен, так что его можно было бы и освободить. Для этого, мы должны применить дополнительный блок для изоляции переменной файла в пределах логического блока работы с файлом (который ограничен первыми двумя строками):
<source lang="kpp"> function ProcessFile(const string filename = '/var/db/mydb') {
var db; { var dbfile = OpenDB(filename); var db = ReadDB(dbfile); } ProcessDB(db);
} </source>
Теперь, переменная db будет существовать только в пределах блока, соответственно, при выходе из него, переменная будет удалена и доступ к файлу будет открыт.
Экспортирование функций
Изначально, платформа и виртуальная машина Gide была разработана для свободного взаимодействия любого количества gide-совместимых языков программирования, которые могли бы свободно использоваться при написании программ. Причем, модуль написанный на одном языке программирования может импортировать другие модули, написанные на других языках. Одним из способов обеспечения совместимости языков является введение стандартного языка описания интерфейсов модулей, который должны понимать языки, подразумевающие совместную работу в рамках системы.
Написание совместимых программ накладывает определенные ограничения на именование идентификаторов классов и методов, поскольку в языках программирования имена методов как правило декорируются для обеспечения возможности перегрузки операторов и методов (см. ниже). Правила декорации имен в различных языках могут быть различными, соответственно пришлось бы их стандартизировать.
В языке К++ введено специальное ключевое слово export, которое позволяет отменить декорацию имени для отдельно взятой функции. Зная имя функции, ее можно будет вызывать из внешнего кода (имеется в виду внешний по отношению к gide код, например код некоторого плагина).
При отмене декорирования функции, накладываются определенные ограничения на перегрузку функций: попытка объявления двух экспортируемых методов с одинаковыми именами в рамках одного класса приведет к ошибке времени компиляции. Если экспортируется только один из этих методов, то это не является ошибкой.
Экспортирование функций применяется при объявлении точек входа в программу. Для консольных программ, такой точкой является функция main(): <source lang="kpp"> export function void main() {
/* тело функции */
} </source>
После загрзки программы в память, происходит поиск функции с именем "main" которой передается управление.
Перегрузка функций и операторов
При разработаке интерфейсов классов, часто бывает необходимо предоставить возможность выполнения одних и тех же операций с различными типами исходных данных. Например, класс пользовательского хранилища данных должен обладать методами добавления (импортирования) элементов из стандартных хранилищ, вроде массивов и списков. Точно так же, он должен предоставлять интерфейс экспортирования данных в стандартные типы. Если интерфейс будет реализован с учетом только одного из типов, то теоретически программа все же сможет работать (поскольку, типы array и list являются взаимоприводимыми). На практике, эта операция может привести к значительным потерям производительности, связанным в основном с многократным копированием содержимого при операции приведения.
Решение может заключаться в реализации нескольких методов для импортирования и экспортирования данных в определенных форматах: <source lang="kpp"> class MyCollection { public:
function Add(const x); //добавление одиночного элемента function AddArray(const array a); //добавление массива function AddList(const list l); //добавление списка function AddCSV(const string s, //добавление строки с разделителем const string separator = ","); function Insert(const x, const int pos = 0); //вставка одиночного элемента function InsertArray(const array a, const int pos = 0); //вставка массива function InsertList(const list l, const int pos = 0); //вставка списка function InsertCSV(const string s, //вставка строки с разделителем const int pos = 0, const string separator = ","); const function array ExportArray(); //экспортирование в массив const function list ExportList(); //экспортирование в список const function string ExportCSV(); //экспортирование в строку
} </source>
Теперь, при использовании такого класса, программисту придется для каждой операции применять функцию, рассчитанную на данный тип параметров: <source lang="kpp"> var col = new MyCollection; col.Add("hello world"); col.AddArray([1, 2, 3]); var l = new list; foreach (1 .. 10) { |x| l.push(x); }; col.AddList(l); col.AddCSV("волк,коза,капуста"); //(код вставки и экспортирования пишется по аналогии) </source>
С одной стороны, вроде бы код довольно читаем и красив, но с другой, — есть одна маленькая неприятность. Допустим, программист написал некоторую функцию или даже целый класс, которые часто работают с инстанциями контейнера MyCollection и со списками, которые импортируются в контейнер. Предположим, что однажды было решено, для повышения быстродействия использовать массивы вместо списков. В коде интерфейсов это изменение может затронуть всего пару строк, в то время как код функции придется переписывать, заменяя все вызовы AddList() и InsertList() на соответствующие им вызовы: AddArray() и InsertArray(). Если в коде функции существует только одна сущность представленная списком, то достаточно сделать замену в тексте одной подстроки на другую. Но что если их много? Придется проходить код строка за строкой и проверять каждый вызов — благодатная почва для ошибок. Конечно, кому-то может показаться, что приведенный пример "притянут за уши", однако такая проблема на самом деле имеет место, просто довольно сложно придумать навскидку подходящую ситуацию.
Как вы уже могли догадаться, наиболее рациональное решение заключается в использовании перегруженных функций. Перегруженными, называются функции или методы, которые имеют одинаковые имена, но различные списки параметров. Вот примеры перегруженных функций:
<source lang="kpp">
function Hello(int x); //1
function Hello(int x, real y); //2
function Hello(string x = "the value"); //3
function Hello(array a, list l, hash h); //4
</source>
Все вышеприведенные функции могут быть с успехом объявлены в одной области: либо как глобальные функции, либо в пределах класса — как методы. Как видите, имена всех функций одинаковы, а отличаются они лишь списком параметров. Отличия могут быть как в количестве параметров, так и в их типах.
При вызове такого метода, первым делом проверяется количество фактически передаваемых параметров и их типы. На основании этой информации компилятор решает, какой же из методов необходимо использовать в каждом конкретном случае. Если, к примеру, пользователь вызвал функцию как Hello(1), то очевидно что имелся в виду метод номер 1. Вызов Hello(1, 2) будет соответствовать методу номер 2. Несмотря на то что тип второго фактического параметра это int, компилятор все равно разберется, поскольку это единственный из методов, принимающий два параметра. Конечно, при генерации вызова будет выполнена операция приведения типа для второго параметра к типу real. Наконец, вызов метода Hello() без параметров будет соответствовать методу номер 3, поскольку в описании присутствует инициализатор.
Аналогичным образом перегружаются и операторы.
Особенности применения
При использовании перегрузки функций существует несколько ограничений, которые нужно иметь в виду при написании интерфейсов и программ. Перегружать функции и методы на основании типа их возвращаемого значения нельзя. Это невозможно по нескольким причинам. Во-первых, подобная практика привела бы к написанию плохого кода, который очень трудно отлаживать при возникновении каких либо проблем. Во-вторых, и это самое главное, существуют ситуации, когда компилятор не может решить какую из функций необходимо выбрать, например такое может случиться при использовании динамических переменных: <source lang="kpp"> function int f() { return 1; } function string f() { return "hello"; }
function caller() {
var x; //динамическая переменная x = f(); //какую из функций вызвать?!
} </source> В приведенном примере выражение x = f(); не может быть корректно разобрано, поскольку компилятору не на что равняться при выборе конкретной реализации функции. Даже если бы мы использовали явное приведение типов в выражении (x = f() as int), то компиляция все равно была бы неуспешной, поскольку с точки зрения компилятора эта операция должна выполняться уже после самого вызова функции.
Та же проблема возникает и в случае передачи динамической переменной в перегруженную функцию: <source lang="kpp"> function f(int x) { return x; } function f(string s) { return x; }
function caller() {
var x; //динамическая переменная f(x); //какую из функций вызвать?!
} </source> Написание подобного кода ведет к ошибке времени компиляции. Однако, такая проблема возникает только при одинаковом количестве неинициализированных параметров в перегруженных функциях.
В случае с операторами, в неоднозначной ситуации будет генерироваться динамический код. Поскольку операторы имеют строго определенные списки параметров и как правило принимают всего один или два параметра, то это оправдано. В случае с функциями, пришлось бы генерировать динамический код, даже если 9 из 10 параметров статические, а 1 динамический (который все и портит). Ограничение на функции было введено намеренно, чтобы избежать большого количества динамического кода и как следствие — падения производительности.
Разрешение вышеописанной ситуации для функций сводится к принудительной типизации переменной: <source lang="kpp"> function caller() {
var x; f(x as int);
} </source>
Применение перегрузки в расширениях
Еще одним аргументом в пользу перегруженных методов, является возможность их применения в расширениях. Например, программист может расширить функциональность некотрого системного класса, добавляя возможность передачи своего класса в качестве аргумента методам. При этом, он добавляет метод, имеющий то же имя (но другой набор параметров), что и метод, существующий в расширяемом классе.
При расширении сторонних классов следует быть особенно внимательным и не забывать, что правила декорирования имен методов в других языках могут быть различными, либо декорирование может не применяться вовсе. К примеру, в случае стандартной библиотеки, применять расширения можно смело, но нельзя их помечать как export.