Функции
Ни один современный язык программирования не был бы возможен без функций. Функции это "кирпичики", из которых складывается программа. Для работы функциям, подобно обычным программам могут потребоваться входные данные — их называют аргументами, или параметрами функции. Функции так же могут возвращать результат. С этой точки зрения, функцию можно сравнить с "черным ящиком", имеющим несколько входов и один выход. Однако, в отличие классического "черного ящика", функция так же может изменять сами входные значения. Но в целом, смысл функций тот же, что и в математике.
С точки зрения языка К++, каждая функция представляет собой подпрограмму, то есть некоторый участок кода, который функионирует автономно. Сами функции а так же области и примеры их применения были неоднократно рассмотрены в предыдущих главах книги. В этой главе внимание будет уделено синтаксису функций, способам их объявления и некоторым моментам, связанным с их применением.
Содержание |
Объявление
Объявление любой функции начинается с указания ключевого слова 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 записан без выражения.
Локальные переменные
Объявление
Область видимости
Экспортирование функций
Изначально, платформа и виртуальная машина Gide была разработана для свободного взаимодействия любого количества gide-совместимых языков программирования, которые могли бы свободно использоваться при написании программ. Причем, модуль написанный на одном языке программирования может импортировать другие модули, написанные на других языках. Одним из способов обеспечения совместимости языков является введение стандартного языка описания интерфейсов модулей, который должны понимать языки, подразумевающие совместную работу в рамках системы.
Написание совместимых программ накладывает определенные ограничения на именование идентификаторов классов и методов, поскольку в языках программирования имена методов как правило декорируются для обеспечения возможности перегрузки операторов и методов (см. ниже). Правила декорации имен в различных языках могут быть различными, соответственно пришлось бы их стандартизировать.
В языке К++ введено специальное ключевое слово export, которое позволяет отменить декорацию имени для отдельно взятой функции. Зная имя функции, ее можно будет вызывать из внешнего кода (имеется в виду внешний по отношению к gide код, например код некоторого плагина).
При отмене декорирования функции, накладываются определенные ограничения на перегрузку функций: попытка объявления двух экспортируемых методов с одинаковыми именами в рамках одного класса приведет к ошибке времени компиляции. Если экспортируется только один из этих методов, то это не является ошибкой.
Экспортирование функций применяется при объявлении точек входа в программу. Для консольных программ, такой точкой является функция main(): <source lang="kpp"> export function void main() {
/* тело функции */
} </source>
После загрзки программы в память, происходит поиск функции с именем "main" которой передается управление.