Блоки
Содержание |
Основной идеей при написании как процедурных, так и объектно-ориентированных программ является локализация (заключение) функционала в некоторой области: в функции, либо в методе. Таким образом получается, что программа состоит из множества функционально независимых частей, каждая из которых решает конкретную, достаточно узкую задачу. Такой подход позволяет скрыть детали реализации алгоритмов внутри этих частей а "наружу" предоставить только интерфейс — совокупность способов взаимодействия данного функционального элемента с другими. В случае функции, под интерфейсом понимается совокупность ее параметров и возвращаемого значения; в случае классов: наборы методов и свойств.
Случается, что в работе некоторого алгоритма возникают четко определенные функциональные подсистемы, которые, однако, слишком зависимы от самого алгоритма, либо слишком просты для того, чтобы выделять их в отдельную функцию. В случае с C++ это все же приходится делать, даже если эта "минифункция" применяется всего в одном методе, но достаточное количество раз, чтобы программист не захотел копировать эти строки.
В других случаях, может возникнуть желание вынести некоторый функционал за пределы текущей функции, или даже предоставить программисту-пользователю возможность самому определить его.
И в том и в другом случае на помощь приходят замыкания, анонимные функции или лямбда функции, как их еще называют. Сущность такой функции в том, что она с одной стороны как бы является переменной, а с другой стороны функцией которую можно вызывать. В К++ такие функции носят название блоков. Сущность переменной блоки проявляют в том, что их можно создавать как локальные переменные, присваивать друг другу и даже передавать другим функциям в качестве параметра. Приняв такой параметр, функция может обратиться к нему, как к любой другой функции: вызывать с некоторыми параметрами и как использовать его возвращаемое значение: <source lang="kpp"> function Caller(x, y, block b) {
return b(x, y);
} </source>
Приведем простой пример объявления блоков и их передачи в вышеописанную функцию: <source lang="kpp"> var sum = { |x, y| return x + y; }; var mul = { |x, y| return x * y; }; var s = Caller(2, 3, sum); // s = 2 + 3 = 5 var m = Caller(2, 5, mul); // m = 2 * 5 = 10 </source>
Первый блок принимает два параметра и возвращает их сумму; второй блок так же принимает два параметра, но результатом его выполнения будет уже произведение.
Применение блоков
Наиболее подходящая задача, иллюстрирующая идеологию и способы использования блоков — это сортировка элементов массива. Предположим, что программисту требуется реализовать для класса array механизм сортировки элементов. Под сортировкой в данном случае понимается упорядочение набора элементов на основании некоторого критерия, по которому можно судить об их очередности в списке.
Проблема заключается в том, что на этапе написания реализации алгоритма мы не имеем представления о том, какие же элементы будут храниться в массиве. Как мы отсортируем массив который может состоять из чего угодно: строк, чисел, произвольных объектов и даже самих массивов? Пришлось бы либо писать обширную реализацию некоторого метода сравнения двух элементов, который бы учитывал все возможные варианты типов элементов, либо реализовывать свою функцию сортировки на каждый из типов данных элементов: <source lang="kpp"> extend array {
public function array SortAsNumbers(); public function array SortAsStrings(); public function array SortAsArrays(); public function array SortAsCollections();
} </source>
И тот и другой способ жестко ограничивают функциональность нашего класса только теми типами элементов, которые были изначально предусмотрены. А если программист-пользователь захочет использовать некоторый свой тип данных, или еще проще, захочет сортировать элементы не по возрастанию, а по убыванию? Вероятно, мало кто захочет переписывать все эти функции только для того чтобы изменить порядок сортировки.
Давайте взглянем на проблему шире. В сущности, все вышеописанные методы делают ровно одно и то же: некоторым из алгоритмов сортировки, они упорядочивают элементы в массиве. При этом, единственная различающаяся часть — это сама операция сравнения двух элементов. Решение напрашивается само собой — необходимо вынести эту часть из самой функции сортировки и предоставить пользователю самому обеспечить операцию сравнения. Таким образом мы получим ОДИН метод сортировки, который будет работать для ЛЮБЫХ типов элементов — программист-пользователь сам укажет необходимый критерий сортировки, который уже будет использован внутри метода.
Эту задачу можно красиво решить с помощью блоков: <source lang="kpp"> extend array {
public const function array sort(block cmp) { if (empty()) return [ ]; var array left, array right; for (var i = 1; i < size(); ++i) cmp(this[i], this[0]) ? left.push(this[i]) : right.push(this[i]); return left.sort(cmp) + [ this[0] ] + right.sort(cmp); }
} </source>
В качестве параметра, метод sort() принимает один аргумент — блок, который должен производить сравнение двух элементов и возвращать истину, если требуется их перестановка, то есть они расположены в неправильном порядке.
Примечание: Приведенный здесь код является реализацией алгоритма быстрой сортировки, однако он является не очень практичным, поскольку на каждом из этапов он оперирует с копиями соответствующих частей массива; в книге же был использован из соображений краткости кода.
Теперь, любой массив в программе использующей текущий модуль, может быть отсортирован на основании некоторого критерия. Результатом выполнения будет другой, уже отсортированный массив:
<source lang="kpp"> var array sorted, ary = [2, 7, 3, 0, 1, 5, 8, 9, 4, 6]; sorted = ary.sort() { |x, y| x < y; }; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] sorted = ary.sort() { |x, y| x > y; }; // [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
ary = ['q', 'e', 'b', 'z', 'a', 'x', 't']; sorted = ary.sort() { |x, y| x < y; }; // ['a', 'b', 'e', 'q', 't', 'x', 'z'] sorted = ary.sort() { |x, y| x > y; }; // ['z', 'x', 't', 'q', 'e', 'b', 'a'] </source>
Мы объявляем две переменных-массива ary и sorted. Переменная ary инициализируется массивом произвольных чисел, либо строк во втором случае. Затем выполняются вызовы метода sort(), результат выполнения которых сохраняется в переменной sorted.
Примечание: Для сокращения записи, в вышеописанном коде были применены т. н. встроенные блоки (inline). К++ поддерживает специальный синтаксис для передачи встроенных блоков в функции. Подробнее об этом будет написано чуть ниже.
Отличие от функций
В целом, блоки блоки очень похожи на обычные функции, однако существует несколько отличий, которые следует иметь в виду. Первое, наиболее очевидное отличие заключается в способе описания формальных параметров, которые, в отличие от функций, записываются в самом начале тела блока, между двумя вертикальными чертами. Друг от друга параметры отделяются запятой.
Главное отличие блоков заключается в том, что они в основном ориентированы на динамический код. Скажем, возвращаемое значение всегда является динамической переменной. В отдельных случаях допускается типизация аргументов блока, однако она происходит внутри самого блока и не влияет на фактически передаваемые параметры (см. ниже). Таким образом, с точки зрения вызывающего кода, блоки всегда представляют собой динамический код.
Блоки не могут быть экспортированы из текущего модуля и должны применяться только в нем самом. Разумеется, экспортировать функции, принимающие блоки в качестве параметров можно.
Типизация параметров
В некоторых случаях, для повышения эффективности кода блока можно типировать формальные параметры, с тем чтобы впоследствии компилятор, на основании этой информации, мог бы генерировать статический, более быстрый код. Типирование аргументов осуществляется точно так же, как и в обычных функциях: непосредственно перед идентификатором аргумента, указывается его тип. <source lang="kpp"> var myblock = { | int x, int y | /* код блока */ }; </source> Теперь, в теле блока, переменные x и y будут считаться статическими. Важно понимать, что этот подход является эффективным тогда, когда тело блока является довольно большим или содержит циклы, поскольку подобное типирование осуществляется путем вставки компилятором вызова оператора приведения в начало кода блока.
В предыдущих примерах этой главы, блоки представляли собой очень кородкие конструкции, сожержащие всего одно выражение, поэтому типирование параметров для них нецелесообразно; фактически, динамический вызов оператора "меньше" (<) для блока { |x, y| x < y; } будет выполняться быстрее, чем типированный код, при котором, сначала будут вызваны операторы приведения типов для переменных x и y, а затем статический вызов оператора "меньше".
Примечание: Поскольку на момент компиляции, у компилятора нет возможности проверить правильность типов передаваемых в блок параметров, ответственность за это ложится на самого программиста. Если в блок будет передан объект неприводимого типа, то это приведет к ошибке времени исполнения (исключению).
Встроенные блоки
Чаще всего, блоки применяются для записи небольших кусочков кода, которые, как в примерах выше, передаются в функции, для уточнения некоторых моментов. Для того, чтобы облегчить работу программиста и сделать код более читаемым, был введен специальный синтаксис для передачи блоков в функции с одновременным их объявлением "на месте" или "по факту использования". При этом, блоки записываются в сокращенной форме сразу за кодом вызова функции, после блока фактических параметров.
Покажем использование встроенных блоков на примере нахождения суммы чисел от 1 до 10, а так же получения списка четных чисел в этом же интервале. <source lang="kpp"> var s = 0; var numbers = []; (1..10).each() { |x| s += x; numbers.push(x); }; var evens = numbers.collect() { |x| x % 2 == 0 ? x : null }.compact(); </source>
Несмотря на свою простоту, данный пример показывает две особенности встроенных блоков, по сравнению с обычными блоками, объявляемыми как переменные. Первая особенность заключается в том, что встроенные блоки имеют доступ к локальным переменным в контексте вызова. Например, данный блок использует внешние по отношению к нему переменные: s для накопления суммы, а numbers для добавления текущего числа в массив.
Вторая особенность реализуется в коде второго встроенного блока. Она заключается в том, что встроенные блоки могут возвращать значения без явного указания оператора return. Для получения массива целых чисел используются два метода, объявленные в стандартной библиотеке языка K++: методы collect() и compact().
Метод collect() принимает в качестве аргумента блок. Далее, он проходит вдоль всего массива и вызывает блок с параметром текущего элемента. Результат выполнения блока помещается в другой массив, который возвращается вызывающему коду. Таким образом, этот метод может применяться для обработки элементов некоторого массива с получением результата. Метод compact() используется для "уплотнения" массива, путем удаленияи из него пустых элементов.
В вышеприведенном коде, мы используем оба этих метода для того чтобы сначала выбрать из исходного массива все четные элементы (вместо нечетных вставляется null), а затем уплотнить полученный массив.
Реализованы эти методы примерно так: <source lang="kpp"> extend array {
public const function array collect(block b) { var array result; for (var i = 0; i < this.size(); ++i) result.push(b(this[i])); return result; }
public const function array compact() { var array result; for (var i = 0; i < this.size(); ++i) if (this[i]) //проверяем на null result.push(this[i]); return result; }
} </source>