dlang-book/05-данные-и-функции-функцио...
Alexander Zhirov 075f071136 5.1 2023-02-25 17:37:36 +03:00
..
README.md 5.1 2023-02-25 17:37:36 +03:00

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.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 и needle1 передаются в нее по значению. Это вовсе не означает, что если вы передадите в функцию массив из миллиона элементов, то он будет полностью скопирован; как отмечалось в главе 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.

В начало ⮍ Наверх ⮍


  1. Функция find ищет «иголку» (needle) в «стоге сена» (haystack). Прим. науч. ред. ↩︎