Функции
Ни один современный язык программирования не был бы возможен без функций. Функции это "кирпичики", из которых складывается программа. Для работы функциям, подобно обычным программам могут потребоваться входные данные — их называют аргументами, или параметрами функции. Функции так же могут возвращать результат. С этой точки зрения, функцию можно сравнить с "черным ящиком", имеющим несколько входов и один выход. Однако, в отличие классического "черного ящика", функция так же может изменять сами входные значения. Но в целом, смысл функций тот же, что и в математике.
С точки зрения языка К++, каждая функция представляет собой подпрограмму, то есть некоторый участок кода, который функионирует автономно. Сами функции а так же области и примеры их применения были неоднократно рассмотрены в предыдущих главах книги. В этой главе внимание будет уделено синтаксису функций, способам их объявления и некоторым моментам, связанным с их применением.
Содержание |
Объявление
Объявление любой функции начинается с указания ключевого слова 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>