Alexander Zhirov 075f071136 | ||
---|---|---|
.. | ||
README.md |
README.md
5. Данные и функции. Функциональный стиль
- 5.1. Написание и модульное тестирование простой функции
- 5.2. Соглашения о передаче аргументов и классы памяти
- 5.2.1. Параметры и возвращаемые значения, переданные по ссылке (с ключевым словом ref)
- 5.2.2. Входные параметры (с ключевым словом in)
- 5.2.3. Выходные параметры (с ключевым словом out)
- 5.2.4. Ленивые аргументы (с ключевым словом lazy)
- 5.2.5. Статические данные (с ключевым словом static)
- 5.3. Параметры типов
- 5.4. Ограничения сигнатуры
- 5.5. Перегрузка
- 5.5.1. Отношение частичного порядка на множестве функций
- 5.5.2. Кроссмодульная перегрузка
- 5.6. Функции высокого порядка. Функциональные литералы
- 5.6.1. Функциональные литералы против литералов делегатов
- 5.7. Вложенные функции
- 5.8. Замыкания
- 5.8.1. Так, это работает... Стоп, не должно... Нет, все же работает!
- 5.9. Не только массивы. Диапазоны. Псевдочлены
- 5.9.1. Псевдочлены и атрибут @property
- 5.9.2. Свести – но не к абсурду
- 5.10. Функции с переменным числом аргументов
- 5.10.1. Гомогенные функции с переменным числом аргументов
- 5.10.2. Гетерогенные функции с переменным числом аргументов
- 5.10.2.1. Тип без имени
- 5.10.2.2. Тип данных Tuple и функция tuple
- 5.10.3. Гетерогенные функции с переменным числом аргументов. Альтернативный подход
- 5.10.3.1. Функции с переменным числом аргументов в стиле C
- 5.10.3.2. Функции с переменным числом аргументов в стиле D
- 5.11. Атрибуты функций
- 5.11.1. Чистые функции
- 5.11.1.1. «Чист тот, кто чисто поступает»
- 5.11.2. Атрибут nothrow
- 5.11.1. Чистые функции
- 5.12. Вычисления во время компиляции
Обсуждать данные и функции сегодня, когда даже разговоры об объектах устарели, – это как вернуться в 1970-е. Но, к сожалению, все еще за горами день, когда говоришь компьютеру, что нужно сделать, и он сам выясняет, как это сделать. А пока этот день не настал, функции – обязательный компонент всех основных направлений программирования. По большому счету, любая программа состоит из вычислений, гоняющих данные туда-сюда; возводимые нами замысловатые строительные леса – типы, объекты, модули, фреймворки, шаблоны проектирования – только придают вычислениям нужные нам свойства, такие как модульность, изоляция ошибок или легкость сопровождения. Правильный язык программирования позволяет своему пользователю держаться золотой середины между кодом «для действия» и кодом «для существования». Идеальное соотношение зависит от множества факторов, из которых самый очевидный – размер программы: основная задача короткого скрипта – действовать, тогда как большое приложение вынуждено заниматься поддержкой неисполняемых вещей вроде интерфейсов, протоколов и модульных ограничений.
Благодаря своим мощным средствам моделирования D позволяет создавать объемистые программы; при этом он старается сократить до разумных пределов код «для существования», позволяя сосредоточиться на том, что нужно «для действия». Хорошо написанные функции на D, как правило, соединяют в себе компактность и универсальность, достигая порой ошеломляющей удельной мощности. Так что пристегнитесь, будем жечь резину.
5.1. Написание и модульное тестирование простой функции
Можно с полным основанием утверждать: главное, чем занимаются компьютеры (помимо скучных дел вроде ожидания ввода данных), – это поиск. Серверы и клиенты баз данных – ищут. Программы искусственного интеллекта – ищут. (А надоедливый болтун – банковский автоответчик? Тоже ищет.) Интернет-поисковики… ну, с этими ясно. Да и собственный опыт наверняка говорит вам, что, по сути, многие программы, якобы не имеющие с поиском ничего общего, на самом деле тоже довольно-таки много ищут. Какую бы задачу ни требовалось решить, всегда задействуется поиск. В свою очередь, многие оригинальные решения зависят от интеллектуальности и удобства программного поиска. Как и следовало ожидать, в мире вычислений полно понятий, имеющих отношение к поиску: сопоставление c шаблоном, реляционная алгебра, бинарный поиск, хеш-таблицы, бинарные деревья, префиксные деревья, красно-черные деревья, списки с пропусками… все это нам здесь никак не охватить, так что сейчас поставим цель поскромнее – определим несколько простых функций поиска на D, начав с простейшей из них – функции линейного поиска. Итак, без лишних слов напишем функцию, которая сообщает, содержит ли срез значений типа int
определенное значение типа int
.
bool find(int[] haystack, int needle)
{
foreach (v; haystack)
{
if (v == needle) return true;
}
return false;
}
Отлично. Поскольку это первое наше определение функции на D, опишем во всех подробностях, что именно она делает. Встретив определение функции find
, компилятор приведет ее к более низкоуровневому представлению – скомпилирует в двоичный код. Во время исполнения программы при вызове функции find
параметры haystack
и needle
1 передаются в нее по значению. Это вовсе не означает, что если вы передадите в функцию массив из миллиона элементов, то он будет полностью скопирован; как отмечалось в главе 4, тип int[]
(срез массива элементов типа int
), который также называют толстым указателем (fat pointer), – это на самом деле пара «указатель + длина» или пара «указатель + указатель», которая хранит только границы указанного фрагмента массива. Из раздела 4.1.4 понятно, что передать в функцию find
срез миллионного массива на самом деле означает передать в нее информацию, достаточную для получения адреса начала и конца этого среза. (Язык D и его стандартная библиотека широко поддерживают работу с контейнером через его маленького, ограниченного представителя, который знает, как перемещаться по контейнеру. Обычно такой представитель называется диапазоном.) Так что в итоге в функцию find
из вызывающего ее кода передаются только три машинных слова. Как только управление передано функции find
, она делает свое дело и возвращает логическое значение (обычно в регистре процессора), которое вызвавший ее код уже готов получить. Что ж, как ободряюще говорят в конце телешоу «Большой ремонт», завершив какую-то неимоверно сложную работу: «Вот и все, что нужно сделать».
Если честно, в устройстве find
есть кое-какие недостатки. Возвращаемое значение имеет тип bool
, это очень неинформативно; также требуется информация о позиции найденного элемента, например, для продолжения поиска. Можно было бы возвращать целое число (и какое-нибудь особое значение, например -1
, для случаев «элемент не найден»). Но хотя целые числа отлично подходят для доступа к элементам массива, занимающего непрерывную область памяти, они ужасно неэффективны с большинством других контейнеров (таких как связные списки). Чтобы добраться до n
-го элемента связного списка после того, как функция find
вернула n
, понадобится пройти по списку элемент за элементом, начиная с его головы – то есть проделать почти ту же работу, что и сама операция поиска! Так что возвращать целое число в качестве результата – плохая идея в случае любой структуры данных, кроме массива.
Есть один способ, который сработает с разнообразными контейнерами – массивами, связными списками и даже с файлами и сокетами. Надо сделать так, чтобы функция find
просто отщипывала по одному элементу («соломинке»?) от «стога сена» (haystack
), пока не обнаружит искомое значение, и возвращала то, что останется от haystack
. (Соответственно, если значение не найдено, функция find
вернет опустошенный haystack
.) Вот простая и обобщенная спецификация: «функция find(haystack, needle)
сужает структуру данных haystack
слева до тех пор, пока значение needle
не станет началом, или до тех пор, пока не закончатся элементы в haystack
, и затем возвращает остаток haystack»
. Давайте реализуем эту идею для типа int[]
.
int[] find(int[] haystack, int needle)
{
while (haystack.length > 0 && haystack[0] != needle)
{
haystack = haystack[1 .. $];
}
return haystack;
}
Обратите внимание: функция find
обращается только к первому элементу массива haystack
и последовательно присваивает исходному массиву более узкое подмножество его самого. Эти примитивы потом легко можно заменить, скажем, специфичными для списков примитивами, но обобщением мы займемся чуть позже. А пока проверим, насколько хорошо работает полученная функция find
.
В последние годы большинство методологий разработки уделяют все больше внимания правильному тестированию программного обеспечения. Это верное направление, поскольку тщательное тестирование действительно помогает отслеживать гораздо больше ошибок. В духе времени напишем короткий тест модуля, проверяющий работу нашей функции find
. Просто вставьте следующий код после (как это сделал я) или до (как сделал бы фанат разработки через тестирование) определения функции find
:
unittest
{
int[] a = [];
assert(find(a, 5) == []);
a = [ 1, 2, 3 ];
assert(find(a, 0) == []);
assert(find(a, 1).length == 3);
assert(find(a, 2).length == 2);
assert(a[0 .. $ - find(a, 3).length] == [ 1, 2 ]);
}
Все, что нужно сделать, чтобы получить работающий модуль, – это поместить функцию и тест модуля в файл searching.d
, а затем ввести в командной строке:
$ rdmd --main -unittest searching.d
Если вы запустите компилятор с флагом -unittest
, тесты модулей будут скомпилированы и подготовлены к запуску перед исполнением основной программы. Иначе компилятор проигнорирует все блоки unittest
, что может быть полезно, если требуется запустить уже оттестированный код без задержек на начальном этапе. Флаг --main
предписывает rdmd
добавить ничего не делающую функцию main
. (Если вы забыли написать --main
, не волнуйтесь; компоновщик тут же витиевато напомнит вам об этом на своем родном языке – зашифрованном клингонском.) Заменитель функции main
нужен нам, так как мы хотим запустить только тест модуля, а не саму программу. Ведь наш маленький файл может заинтересовать массу программистов, и они станут использовать его в своих проектах, в каждом из которых определена своя функция main
.
-
Функция
find
ищет «иголку» (needle
) в «стоге сена» (haystack
). – Прим. науч. ред. ↩︎