Абстрактные типы данныхВ предыдущей части было сделано утверждение относительно роли абстракций типов данных в программировании. Для того, чтобы в этом убедиться, следует вернуться к понятию “переменная”, но теперь уже – в контексте программирования. Кто-то может сказать, что переменная – это имя в программе. Что будет не совсем точно, потому что “имя”, обладает единственным свойством – его уникальностью. Очевидно, что переменные обладают и другими свойствами. Кто-то скажет, что это “хранилище”, которое принимает различные значения. Это опять не совсем точно в отношении переменных в программах, так как часто переменная не хранит значение, которое ей присваивается, при этом значение не может существовать в программе само по себе, ведь оно тоже абстрактно. Оно будет иметь некоторое конкретное представление. Например, число является значением, оно будет представлено в памяти компьютера в виде двоичного: ;
а в программе – еще и в виде десятичного: ;
или шестнадцатеричного: набора данных. На основании этих уточнений можно выделить три основных момента, присущих любой переменной. Переменные обладают способом представления в конкретном языке (целое число, дробное число, текст и т.д.). Переменные обладают порядком хранения в памяти компьютера (занимаемые байты, а также назначение каждого бита в соответствующих байтах). Наконец, переменные обладают непосредственно значением, которое определяет их полезность в программе. Таким образом мы столкнулись с тремя уровнями абстракции, совершая переход от математических концепций к их воплощению в памяти компьютера посредством какого-то языка программирования1. Так как математические абстракции принято классифицировать по каким-либо признакам для удобства (например, переменные разделяются на такие, которые представляют целые или дробные числа), такой подход применяется и в языках программирования. Классификация производится по свойствам, перечисленным выше, а именно: какое множество значений принимают данные, какую форму принимают значения из этого множества, какие операции возможны для значений из этого множества [Wirth]. То есть, тип данных – это совокупность представления какого-то значения в программе и в памяти компьютера, которая позволяет работать с конкретным значением в программе с помощью соответствующих операций. Внутри самой программы также есть “конкретные” сущности и абстрактные сущности. Первые ассоциируются с объектами, вторые – со значениями [McJones]. В общем смысле, объект – это некоторое отношение между типом данных и самими данными в памяти компьютера. Свойства объекта определяются его типом. Второе значение слова “абстрактный” – удаленный, то есть, рассматриваемый в отрыве от своего источника. Так разделение подходов в программировании на парадигмы (программирование может быть структурное, объектно-ориентированное, функциональное и т.д.) является абстракцией, потому что с точки зрения целевого устройства все эти парадигмы производят один и тот же машинный код, который исполняется независимо от того, какую идею вкладывал в него программист. Предназначение абстракций в программировании заключается в том, чтобы проектировать программы, понимать их и проверять их логику на правильность на основании законов, которые управляют абстракциями (например, на основании законов математики), не отвлекаясь на подробности реализации этих абстракций в конкретном компьютере. Рассмотрим несложный пример. Предположим, что перед вами поставлена задача разработать тип данных в языке С, который будет использоваться для представления рациональных2 чисел в различных программах. Например, такое число можно было бы хранить в виде структуры:
Тогда атрибут – это числитель, а – знаменатель. В этом случае создание рационального числа в программе могло бы выглядеть так:
Все три способа являются правильными с точки зрения компилятора (в данном примере используется gcc3). Но внимательный читатель может заметить, что тут есть проблемы. Во-первых, в качестве числителя и знаменателя можно передавать любые значения, в том числе . Это, конечно, противоречит правилам работы с рациональными числами, так как в числителе должен обнулять все число, а в знаменателе недопустим. Во-вторых, и числитель, и знаменатель можно свободно менять уже после создания переменной, что чревато всеми проблемами, перечисленными выше. В-третьих, оба атрибута можно оставить без значений, что делает такую переменную бесполезной, а поведение программы, которая ее использует, непредсказуемым. Общепринятый способ решения похожих проблем заключается в создании специальных функций, которые устанавливают значения согласно определенному набору требований (в нашем случае – определенные значения числителя и знаменателя). Для рациональных чисел эта функция могла бы выглядеть так:
Функция проверяет: числитель на нулевое значение (тогда вся дробь обнуляется), знаменатель на нулевое значение (тогда даже нет смысла продолжать, и работа программы прерывается c помощью макроса
Конечно, те, кто более-менее разбирается в нюансах создания переменных в С, понимают, что эта функция никак не решает проблемы изменения значений числителя и знаменателя в дальнейшем. В целом, ограничения, которые эта функция накладывает, условны. Никак не запрещается создавать переменные такого типа без использования этой функции. Другими словами, она рассчитана на то, что разработчики будут руководствоваться здравым смыслом при работе с такими рациональными числами. Тем не менее, компилятор позволяет установить более жесткие ограничения, которые полностью контролируют создание переменных и изменение атрибутов каждого создаваемого объекта типа Тут интересно другое. Когда было принято решение создать функцию, задача которой – заботиться о правильности инициирования рациональных чисел, вы абстрагировались от деталей реализации рациональных чисел в программе. Использование рациональных чисел в своих программах является основным критерием для пользователя. Аналогичные функции можно написать для всех операций, в которых будут участвовать такие объекты. Результатом становится абстрактный тип данных. Такой тип данных предоставляет не только механизм для создания объектов с данными, но и механизмы для полноценной работы с этими объектами – стандартные операции над ними. УпражненияАбстрактные типы данных будут составлять основу большинства концепций, обсуждаемых в этом тексте, поэтому с ними нужно освоиться как можно лучше. Для этого читателю предлагается дополнить тип данных, описанный выше, несколькими операциями. Во-первых, было бы неплохо поддерживать сокращенную форму всех создаваемых в программе рациональных чисел (при заданных числителе и знаменателе, которые кратны одному и тому же числу, делить их на это число по умолчанию: превращать в , – в ). Во-вторых, было бы удобно отображать такие числа в текстовом виде как дроби, а не пару целых чисел. Пример этих дополнений будет ниже, но настоятельно рекомендуется сделать их самостоятельно, и только в случае полной неудачи посмотреть реализацию. Пример решенияИдея, стоящая за функцией сокращения дробей довольно проста. Во-первых, если числитель равен нулю или знаменатель – единице, делать сокращение нет необходимости. Во-вторых, если сокращение делать все таки надо, для этого достаточно определить значение наибольшего общего делителя для дроби и поделить на эту величину числитель и знаменатель.
Обратите внимание на то, что тип данных для переменной
Здесь используется модифицированный алгоритм Евклида [Степанов]. Данный алгоритм легко переписывается с помощью рекурсии для тех языков, которые не имеют встроенных циклов. У многих реализаций этого алгоритма есть некоторые недостатки. Алгоритм зависит от операции деления, которая является недостаточно эффективной. Алгоритм также зависит от типов данных его аргументов, что привязано к архитектуре процессора и используемому компилятору, поэтому неудачная реализация может нарушать важные математические свойства алгоритма [Cormen]. На практике часто можно встретить использование расширенной версии алгоритма Евклида или двоичного алгоритма, разработанного Д. Штайном [Knuth]. Поэтому лучшим выбором для среднестатистического разработчика будет использование встроенного в стандартную библиотеку алгоритма, реализация которого остается на совести авторов компилятора. Например, в С++:
Стандартная функция использует шаблоны типов для того, чтобы в качестве аргументов можно было передавать любые целые числа. С этого момента будем предполагать, что все упоминания функции В случае с выводом строковой формы дроби реализация будет чуть сложнее. Для этого необходимо решить две задачи. Первая задача заключается в определении всех цифр, которые составляют числа в числителе и знаменателе. Вторая задача состоит в составлении строки, которая содержит в себе символы, отвечающие полученным числовым значениям. Достичь этого можно несколькими способами, и ниже представлен лишь один из них.
Тонкий момент в коде выше связан с включением в функцию другой функции. Это сделано исходя из того факта, что внутренняя функция никогда не будет использоваться отдельно от родительской функции, соответственно нет причины захламлять пространство имен, связанное с этим файлом, именем еще одной функции. Сама внутренняя функция
Символы поглощаются справа налево, следовательно запись в массив происходит по мере их получения, то есть, в порядке обратном тому, который нужен для правильного отображения всей дроби. Поэтому часть массива, которая только что была заполнена, должна быть перевернута (это делается во втором цикле внутренней функции Наконец, важно отметить несколько неочевидных моментов. Во-первых, функция не знает заранее, какой величины должна быть результирующая строка, и для таких случаев строка либо поставляется в функцию вместе с дробью (то есть, размер уже определен до вызова функции), либо строка создается динамически в самой функции, что подразумевает несколько дополнительных операций (подсчет общего количества символов и выделение нужного участка памяти в соответствии с результатом подсчета). В примере выше используется первый способ. Но если вы чувствуете уверенность в себе, автор рекомендует попробовать реализовать второй способ самостоятельно. Во-вторых, функция возвращает строку, которая передается в нее изначально, что может показаться бесполезным. Причина состоит в том, что строки в С и С++ являются указателями, что подразумевает отсутствие процесса “конструирования” объекта внутри функции. Это значит, что данная функция не создает новых объектов, а только лишь дублирует адрес строки, в которой были произведены необходимые манипуляции с данными. Другими словами, можно считать, что у функции нет возвращаемого значения:
Тем не менее, согласно первоначальной идее того, как эта функция должна работать, она возвращает строковое представление:
То есть, результат работы этой функции можно передавать как аргумент в другие функции, ожидающие указатель на строку в качестве аргумента. Из этого следует, что в идеале наша функция должна возвращать не адрес той строки, которую мы в нее передали, а непосредственно символы, составляющие такую строку, которые можно скопировать в новую переменную типа
В одной из следующих глав будет показано как это можно сделать. Но многие читатели, наверное, догадались, что это будет сделано с помощью абстрактного типа данных. Сноски1Программирование можно считать одной из областей математики, языки программирования можно называть абстрактными компьютерами [Strachey]. 2Действительное число, которое можно представить как пару целых чисел, записанных одно над другим через прочерк, в виде дроби. 3GCC, the GNU Compiler Collection, gcc.gnu.org.
Copyright © 2023 Брынзан, Л. В. |