Битовая строка TBitList
Предоставляет методы и свойства для работы со строкой бит, которую можно представить как упакованный массив элементов типа Boolean. Изначально, при создании экземпляра класса TBitList, все биты строки устанавливаются в 0, т.е. в значение False. Метод Load используется для загрузки битовой строки из бинарного потока типа TBinaryReader, произвольной области памяти или другого экземпляра класса TBitList. Кроме того, данные могут быть загружены из ключа реестра Windows. Для сохранения данных в бинарном потоке типа TBinaryWriter или в реестре Windows предназначен метод Save. К отдельным элементам битовой строки можно обратиться с помощью свойства Items. Общее число элементов возвращается и устанавливается свойством Count. Методы IndexOf, LastIndexOf используются для поиска, соответственно, вперед и назад первого установленного или сброшенного бита, начиная с указанного индекса. Функция CountOf подсчитывает общее количество установленных или сброшенных бит в массиве. Метод SetAll устанавливает все биты в значение 0 или 1. Если два экземпляра класса TBitList содержат одинаковое число элементов, к ним можно применить различные логические операции. Например, метод AndSet выполняет операцию логического умножения элементов двух списков. В результате, установленными окажутся только биты, которые были установлены в обоих списках, а все остальные биты будут установлены в 0. Аналогичные методы предусмотрены для выполнения операций: OR, XOR, NOT, AND NOT. Функция Equals используется для проверки равенства содержимого двух экземпляров класса TBitList. Функция Clone возвращает копию экземпляра TBitList.
Форматирование даты и времени
В начале модуля AcedCommon объявлены константные массивы, содержащие названия месяцев и дней недели, записанных по-русски и по-английски. Для форматирования даты используется функция G_FormatDate. Она возвращает дату представленную краткой числовой формой или полной формой, содержащей название месяца и слово "года". Строка-результат может включать также полное или краткое название дня недели или, вообще, состоять только из дня недели без даты. Функция G_SplitDate позволяет выделить из даты день месяца как числовое значение, а сам месяц и год представить в виде строки. Для форматирования времени используется функция G_FormatTime, которая преобразует время в строку по правилам русского языка с возможным указанием числа секунд и миллисекунд.
Функции для построения отчета
В модуле AcedExcelReport определены глобальные функции, часто используемые при построении отчетов. Для выполнения других действий можно обращаться к объектам Microsoft Excel через интерфейсы, объявленные в модуле Excel97, сгенерированном из библиотеки типов. В AcedExcelReport есть несколько функций, возвращающих ссылку на диапазон ячеек рабочего листа, т.е. объект типа Excel97.ExcelRange. В частности, функция GetExcelCell возвращает ячейку рабочего листа, находящуюся на пересечении заданной строки и столбца; функция GetExcelRange возвращает прямоугольный диапазон ячеек. Функция GetNamedExcelRange возвращает именованный диапазон ячеек. Функции GetExcelRows и GetExcelColumns возвращают диапазоны, состоящие из всех ячеек определенных строк или столбцов. Чтобы установить значение ячейки или поставить вариантный массив в соответствие ячейкам прямоугольного диапазона, нужно получить ссылку на диапазон функциями GetExcelCell или GetExcelRange, а затем присвоить соответствующее значение свойству Value этого диапазона. То же самое касается, например, изменения шрифта текста с помощью свойства Font диапазона ячеек, задания режима горизонтального/вертикального выравнивания текста с помощью свойств: HorizontalAlignment, VerticalAlignment, задания режима переноса текста свойством WrapText, а также объединения ячеек рабочего листа вызовом метода Merge диапазона ячеек.
Функция InsertExcelRows предназначена для вставки на рабочем листе одной или нескольких строк перед указанной строкой. Функция InserExceltColumns вставляет один или несколько столбцов перед указанным столбцом. Процедуры AssignAbsoluteFormula и AssignRelativeFormula используются для назначения ячейкам рабочего листа формул, реализующих групповые операции, такие как суммирование данных, нахождение максимального значения в диапазоне, подсчет числа непустых ячеек и т.д. При записи формулы адрес обрабатываемого диапазона ячеек указывается в виде абсолютной ссылки (процедурой AssignAbsoluteFormula) или в виде относительной ссылки (процедурой AssignRelativeFormula).
Строку, содержащую абсолютную ссылку на прямоугольный диапазон ячеек, можно получить вызовом функции GetAbsoluteAddress. Аналогичную строку, содержащую относительную ссылку на диапазон ячеек, возвращает функция GetRelativeAddress. Для форматирования границ ячеек вызывается процедура DrawExcelBorders. В нее передается диапазон ячеек в виде объекта Excel97.ExcelRange. Второй параметр (CellBorders) выбирает, какие именно границы ячеек должны отображаться. Здесь передается одна из констант xlcb… или комбинация таких констант. Остальные параметры процедуры DrawExcelBorders задают толщину, стиль и цвет линий. Чтобы, наоборот, удалить прорисовку границ используется процедура ClearExcelBorders. Для применения заливки к диапазону ячеек рабочего листа вызывается процедура FillExcelInterior. Кроме ссылки на диапазон ячеек в нее передается цвет заливки (одна из констант xlColor…), шаблон, накладываемый поверх заливки (одна из констант xlPattern, объявленных в модуле Excel97) и цвет линий шаблона. После того, как содержимое отчета полностью подготовлено, можно воспользоваться процедурами FreezeExcelRows и FreezeExcelColumns, чтобы облегчить просмотр данных. Эти процедуры задают, соответственно, строки и столбцы, которые не должны перемещаться при прокрутке окна рабочей книги. Иногда бывает удобно выделить шапку таблицы, чтобы она всегда была на экране независимо от длины таблицы. Если предполагается, что пользователь не будет изменять готовый отчет, можно вызвать процедуру ProtectExcelWorksheet для защиты рабочего листа от случайных изменений. Если просматривать отчет удобнее в режиме панорамирования, можно воспользоваться процедурой G_ToggleKey из модуля AcedCommon для включения режима Scroll Lock клавиатуры. После выполнения всех подготовительных действий вызывается процедура ShowExcelWorkbook, в которую передается ссылка на рабочую книгу, полученная ранее при вызове функции CreateExcelWorkbook. Процедура ShowExcelWorkbook отображает рабочую книгу на экране и устанавливает ее свойство Saved в значение True, чтобы при закрытии книги не спрашивалось, нужно ли сохранять изменения.
Функции для замера временных интервалов
Когда в программе нужно отмерить временные интервалы, можно воспользоваться стандартной функцией GetTickCount из модуля Windows, которая возвращает число миллисекунд, прошедшее с момента загрузки операционной системы. Поскольку это значение выражается числом типа LongWord, со временем оно может переполниться и пойти на следующий "круг". Функция G_TickCountSince из модуля AcedCommon учитывает эту особенность и возвращает число миллисекунд, прошедшее с того момента, как функция GetTickCount вернула значение Tick, передаваемое в качестве параметра в G_TickCountSince. Хотя GetTickCount возвращает число миллисекунд, точность этой функции не превышает 0.1 секунды. Когда нужно отмерить интервал с точностью до одной миллисекунды можно воспользоваться таймером высокого разрешения. Получить текущее значение таймера можно вызовом функции G_QueryPC, которая возвращает значение, полученное вызовом системной функции QueryPerformanceCounter. По разности значений таймера можно определить длительность временного интервала с помощью функции G_GetTimeInterval. Кроме того, с помощью функции G_GetPC_Delta можно, наоборот, вычислить разность значений таймера, соответствующую временному интервалу определенной длительности.
К замеру временных интервалов с помощью таймера высокого разрешения имеет смысл прибегать только когда требуется высокая точность. В остальных случаях лучше воспользоваться функциями GetTickCount и G_TickCountSince, т.к. они используют меньше ресурсов системы. В модуле AcedCommon имеется также функция G_RDTSC, считывающая значение 64-разрядного счетчика, который увеличивается на каждом такте процессора. В некоторых случаях эта функция может заменить собой таймер высокого разрешения. Кроме того, она может быть полезна при инициализации генератора случайных чисел.
Индексы для коллекции
В коллекции TSerializableCollection элементы упорядочены по возрастанию значения уникального идентификатора элемента. Такой порядок не всегда удобен. Например, конечному пользователю обычно нужен список, отсортированный по именам или по датам и т.п. С этой целью при создании коллекции в конструктор класса TSerializableCollection передается массив, содержащий так называемые индексы – объекты, задающие альтернативный порядок сортировки элементов коллекции. Индексы должны быть созданы заранее, до вызова конструктора коллекции. Все индексы представляются экземплярами классов, производных от TDataIndex. Конкретный класс выбирается в зависимости от типа признака, по которому сортируются данные. Если этот признак – строка (например, наименование объекта), создается индекс типа TStringIndex; если признак – дата, используется класс TDateTimeIndex и т.д. Есть еще специальный класс TCompoundIndex, предназначенный для сортировки элементов коллекции сразу по нескольким признакам.
Индексы предназначены не только для задания порядка элементов коллекции. Основной причиной использования индексов является необходимость быстрого поиска элементов по значению индексируемого признака. Индексы позволяют не только быстро находить отдельные элементы, но также выделять группы элементов, для которых значение признака лежит в определенном интервале. Кроме того, индексы могут быть уникальными, т.е. не допускающими дублирования значения признака. При добавлении и изменении элементов коллекция опрашивает все свои уникальные индексы на предмет того, не вызовет ли каждое конкретное изменение нарушения уникальности какого-либо индекса. Если такая ситуация имеет место, предполагаемые изменения отклоняются.
Изначально все индексы находятся в неактивном состоянии, т.е. не занимают память и не обновляются при каждом изменении коллекции. При попытке воспользоваться индексом, например, для поиска элемента, индекс автоматически активизируется. Его состоянием можно управлять с помощью свойства Active, объявленного в классе TDataIndex.
Следует обратить внимание на то, что в часто изменяемых коллекциях лучше постоянно держать все уникальные индексы в активном состоянии. В противном случае, при любом изменении данных выполняется медленный последовательный перебор элементов коллекции индексом для выяснения того, что значение индексируемого признака измененного элемента уникально в пределах набора данных. Каждый активный индекс содержит полный набор элементов коллекции, отсортированный по значению соответствующего признака. Свойство Descending индекса по умолчанию равно False. Это означает, что элементы сортируются по возрастанию значения признака. Если свойство Descending равно True, элементы сортируются по убыванию значения признака. Для класса TStringIndex, если свойство CaseSensitive равно False, при сортировке и поиске элементов с помощью индекса регистр символов не принимается во внимание. Если это свойство равно True, регистр символов учитывается. К элементам сортированного списка можно обратиться через свойство ItemList индекса, которое возвращает массив указателей на элементы коллекции – экземпляры класса TSerializableObject. Число элементов в этом массиве определяется свойством Count основной коллекции, к которой можно обратиться через свойство Owner индекса. Свойство Unique определяет, является ли данный индекс уникальным. Значение этого свойства передается в конструктор класса индекса и в дальнейшем не может быть изменено. Кроме того, при создании индекса в его конструктор обычно передается адрес функции, которая возвращает значение индексируемого признака для элемента коллекции. В случае класса TCompoundIndex вместо этого передается адрес функции, которая сравнивает между собой два элемента коллекции. В каждом индексе есть методы для поиска элементов. Метод ScanPointer выполняет линейный поиск указателя во внутреннем массиве индекса. Этим методом стоит пользоваться только если никакие другие не подходят. Метод Contains возвращает True, если в коллекции присутствует элемент с указанным значением признака.
Функция IndexOf находит во внутреннем массиве первый элемент с определенным значением признака и возвращает его индекс в качестве результата, а функция SearchObject в аналогичной ситуации возвращает сам элемент. Во всех индексах, кроме TCompoundIndex, есть функции SelectRange для нахождения диапазона во внутреннем массиве, в котором значение признака больше или равно значению Key1 и меньше значения Key2. При сортировке элементов по убыванию эти функции выделяют диапазон, в котором значение признака меньше или равно значению Key1 и больше значения Key2. Имеется также вариант этой функции, которая возвращает индекс первого элемента массива, для которого индексируемый признак больше или равен (меньше или равен в случае, когда Descending равно True) указанному значению. В классе TStringIndex есть еще метод StartsWith для выделения диапазона во внутреннем массиве, где значение признака для всех элементов начинается с указанной подстроки с учетом или без учета регистра символов в зависимости от значения свойства CaseSensitive.
Класс TSerializableCollection
Является центральным элементом концепции объектного хранилища данных. Коллекция содержит набор объектов типа, производного от TSerializableObject, который определяется параметром ItemClassType при вызове конструктора класса TSerializableCollection. Таким образом, все элементы коллекции имеют один и тот же тип. Полиморфизм здесь не используется, т.к. возможность присутствия в коллекции объектов различного типа приводит к неоправданному усложнению механизмов работы с данными. Внутренний массив элементов, к которому можно обратиться через свойство ItemList, упорядочен по возрастанию значений первичного ключа, т.е. ID. Количество элементов в коллекции возвращается свойством Count. Для ускорения поиска элементы коллекции могут хешироваться по первичному ключу. Чтобы включить хеширование, надо установить в True значение свойства MaintainHash коллекции. По умолчанию для экономии памяти хеширование отключено. Найти элемент коллекции по значению первичного ключа или просто убедиться в том, что элемент с таким ключом присутствует в коллекции, можно с помощью метода SearchObject. Если нужен не сам объект, а его индекс во внутреннем массиве элементов, следует воспользоваться методом IndexOf. Если в коллекции мало элементов и нужно определить индекс одного из них во внутреннем массиве, можно вызвать метод ScanPointer для линейного поиска указателя. Если элементов много, функция IndexOf быстрее найдет нужный элемент методом бинарного поиска.
Коллекция может быть загружена из бинарного потока типа TBinaryReader методом Load и сохранена в бинарном потоке типа TBinaryWriter методом Save. Метод Equals поэлементно сравнивает данную коллекцию с другой коллекцией и возвращает True, если все соответствующие элементы обеих коллекций равны. Функция Clone возвращает копию коллекции, содержащую копии всех элементов и каждого из индексов. Если предполагается добавить в коллекцию большое число элементов, можно вызвать метод EnsureCapacity для резервирования места во внутренних массивах коллекции.
Для полной очистки коллекции используется метод Clear. При вызове этого метода свойство Changed устанавливается в False, т.к. эта операция не предполагает фактического удаления данных на диске. Например, метод Clear вызывается перед загрузкой с диска обновленных данных коллекции. Чтобы по-настоящему удалить из коллекции все элементы, нужно вызвать метод Delete или DeleteDirect для каждого элемента коллекции. Однако, при монопольном доступе к данным и отсутствии необходимости вызова события OnItemDeleted для каждого удаляемого элемента коллекции, можно очистить ее методом Clear, а затем вручную установить свойство Changed в значение True. Это самый быстрый способ удаления всех данных. Новые элементы коллекции создаются вызовом функции NewItem класса TSerializableCollection. При этом элемент сразу не добавляется в коллекцию. Сначала он должен быть заполнен данными. Затем, для подтверждения изменений вызывается метод EndEdit, который помещает информацию о добавлении новой записи в кэш изменений. Если новая запись не нужна, например, если пользователь нажал кнопку "Отмена" в окне добавления записи, нужно вызвать метод CancelEdit для освобождения памяти, занятой новым элементом. При необходимости корректировки данных нельзя вносить изменения непосредственно в элементы списка ItemList коллекции. Вместо этого, должен быть вызван метод BeginEdit, в который передан идентификатор изменяемого элемента коллекции. Метод BeginEdit возвращает ссылку на копию элемента, в которую можно вносить изменения. Затем эта копия передается в методы EndEdit для подтверждения изменений или CancelEdit для отмены изменений. Удалить элемент коллекции можно вызовом метода Delete с передачей в него идентификатора удаляемого элемента. Все описанные выше манипуляции с объектами не затрагивают основной список элементов ItemList, т.е. фактический набор данных коллекции. Изменения кэшируются во внутренних массивах: InsertedItemList и DeletedItemList с числом элементов, соответственно, InsertedCount и DeletedCount.
В первом из этих массивов находятся элементы, добавленные в коллекцию, а также исправленные, т.е. новые, версии измененных объектов. Во втором массиве находятся удаленные объекты и исходные, т.е. старые, версии измененных объектов. Эти массивы отсортированы в порядке убывания идентификаторов элементов. Проверить наличие кэшированных изменений вообще или изменений для элемента с конкретным идентификатором можно вызовом функции HasChanges. Чтобы применить кэшированные изменения к основному набору элементов используется метод ApplyChanges. Обычно перед вызовом этого метода проверяется наличие обновленных данных на файловом сервере. Если версия файла данных на сервере изменилась, коллекция перечитывается с диска и изменения применяются к новым данным. Управление версиями файлов будет рассмотрено далее в этом разделе. В классе TSerializableCollection предусмотрены специальные события: OnItemInserted, OnItemChanged, OnItemDeleted, которые инициируются, соответственно, при добавлении, изменении и удалении элемента основного набора данных коллекции. Метод ApplyChanges может вернуть одно из следующих значений: appChangesOk (изменения применены успешно), appChangesOriginalObjectChanged (произошла ошибка, связанная с тем, что изменяемый элемент коллекции был одновременно изменен другим пользователем), appChangesUniqueIndexViolation (ошибка, возникающая при попытке вставить значение, нарушающее уникальность одного из индексов коллекции, который не допускает дублирования значений). Чтобы очистить кэш изменений без фактической модификации данных используется метод RejectChanges. Поясним работу с первичными ключами элементов коллекции. При создании нового элемента методом NewItem свойству ID этого элемента присваивается временное отрицательное значение, которое последовательно уменьшается для каждого следующего создаваемого элемента. При вызове ApplyChanges для добавляемого элемента временное значение ID заменяется настоящим идентификатором, который создается функцией GenerateID коллекции.
При изменении идентификатора объекта в коллекции инициируется событие OnItemIDChanged, чтобы можно было обновить внешние ключи в элементах других коллекций, ссылающихся на добавленный элемент. В экземпляре класса TSerializableCollection предполагается, что элементы сохраняют значение свойства ID в виде числа типа Integer. Если заранее известно, что число элементов коллекции не превысит 65535, можно хранить уникальные идентификаторы как 2-байтные значения типа Word. Тогда вместо класса TSerializableCollection надо использовать производный от него класс TWordPrimaryKeyCollection, в котором перекрывается метод GenerateID, чтобы значения первичного ключа не превышали 65535. Если в коллекции может быть не более 255 элементов, имеет смысл хранить ID как один байт. Тогда в качестве коллекции нужно использовать класс TBytePrimaryKeyCollection. Возможна также ситуация, когда вообще нет смысла сохранять уникальный идентификатор вместе с данными объекта. Значение ID может динамически назначаться в момент загрузки коллекции из бинарного потока. Для реализации такой возможности предусмотрен класс TFakePrimaryKeyCollection. При отказе от хранения идентификатора возникает проблема удаления элементов в режиме многопользовательского доступа, т.к. после удаления элемента и повторной загрузки коллекции динамические идентификаторы следующих за ним элементов изменятся. Таким образом, при работе с TFakePrimaryKeyCollection в режиме многопользовательского доступа удаление отдельных элементов коллекции должно быть запрещено. В классе TSerializableCollection есть еще пара методов для работы с данными в обход кэша изменений. Метод EndEditDirect подтверждает изменение или добавление нового элемента аналогично методу EndEdit, а затем сразу применяет это изменение к основному набору данных, что эквивалентно вызову метода ApplyChanges с идентификатором только что измененного или добавленного объекта. Метод DeleteDirect удаляет элемент с указанным идентификатором из основного набора данных. Методы EndEditDirect и DeleteDirect подходят для работы с данными в режиме монопольного доступа, когда другие пользователи не имеют возможности изменить файл данных одновременно с текущими изменениями. Каждая коллекция, не являющаяся частью какого-либо объекта, хранится на диске в виде отдельного файла.
Для загрузки коллекции из файла предназначен метод LoadFile класса TSerializableCollection, для ее сохранения в файле – метод SaveFileDirect. Второй метод не предназначен для работы с данными в режиме многопользовательского доступа, т.к. он перезаписывает любые изменения, сделанные другими пользователями. При одновременной работе нескольких пользователей для сохранения изменений необходимо открыть файл данных методом OpenFile, затем применить кэшированные изменения к данным методом ApplyChanges. Если изменения применены успешно, можно сохранить коллекцию на диске вызовом метода SaveIfChanged. Если во время применения изменений к данным произошла ошибка, необходимо восстановить исходное состояние набора данных в памяти. Для этого используется метод UndoIfChanged коллекции при открытом файле данных. В конце операции файл данных должен быть закрыт методом CloseFile. Данные коллекции могут сохраняться на диске в упакованном виде. Для этого в методы OpenFile и SaveFileDirect передается параметр CompressionMode, который, если он отличен от dcmNoCompression, задает режим сжатия записываемого файла данных (одна из констант, перечисленных в описании модуля AcedCompression). Не следует, однако, применять сжатие к файлам, которые содержат часто изменяемые данные, так как это может значительно понизить общую производительность системы, особенно в режиме многопользовательского доступа к данным. В упакованном виде лучше хранить справочники, коды и прочие данные, которые меняются редко. В методы LoadFile, OpenFile и SaveFileDirect передается также параметр EncryptionKey. Он позволяет указать ключ для шифрования файла данных методом RC6. При использовании шифрования, данные, кроме всего прочего, защищаются цифровой сигнатурой SHA-256. Так же как и сжатие, шифрование не стоит использовать для часто изменяемых данных без крайней на то необходимости. Несколько пользователей могут одновременно читать данные из одного файла методом LoadFile. Однако, если кто-либо открыл файл методом OpenFile, другие пользователи не могут открыть этот файл для чтения или для записи, пока он не будет закрыт методом CloseFile.
При отказе в открытии файла на экране появляется сообщение для пользователя с предложением подождать или повторить попытку открытия файла. Кроме того, пользователь может отменить текущую операцию, в результате чего методы LoadFile, OpenFile и SaveFileDirect вернут значение False. При успешном открытии файла эти методы возвращают True. В первых четырех байтах файла данных хранится его версия – число, которое изменяется при каждом сохранении данных на диске. Если основной набор данных коллекции не менялся с момента ее загрузки в память или с момента предыдущего сохранения на диске (свойство Changed коллекции равно False), и при вызове метода LoadFile оказывается, что версия файла данных не изменилась за это время, то фактического считывания данных не происходит, так как в этом нет необходимости. То же самое происходит при открытии файла методом OpenFile. Это позволяет уменьшить нагрузку на файловый сервер, сократить объем информации, передаваемой по сети, и в целом повысить производительность приложения.
Класс TSerializableObject
Абстрактный базовый класс TSerializableObject используется для создания на его основе класса, описывающего записи таблицы данных. Конструктор этого класса является виртуальным. Он может перекрываться в классах-потомках. Каждый экземпляр TSerializableObject содержит идентификатор типа Integer (свойство ID), значение которого уникально в пределах данной коллекции. При загрузке данных объекта из бинарного потока вызывается метод Load, принимающий два параметра: ссылку на экземпляр класса TBinaryReader, из которого нужно прочитать данные, и число, соответствующее сохраненной версии данных. Понятие версии данных введено для того, чтобы была возможность изменять хранимый формат данных в процессе доработки приложения без необходимости создания специального конвертера данных под новый формат. Каждый объект должен уметь загружать в методе Load любую из своих версий до самой последней, включительно. Число, соответствующее последней, т.е. сохраняемой, версии объекта передается в качестве параметра в конструктор коллекции TSerializableCollection. В методе Load при попытке загрузить данные для версии, которая не поддерживается объектом, следует вызвать процедуру RaiseVersionNotSupported из модуля AcedConsts, в которую передаются параметры: Self и числовое значение версии, которая не может быть загружена. Для сохранения данных объекта в бинарном потоке вызывается метод Save класса TSerializableObject, в который передается ссылка на экземпляр класса TBinaryWriter. В методе Save объект должен записать свои данные в бинарный поток. В каждом классе, унаследованном от TSerializableObject, кроме методов Load и Save должны перекрываться методы Equals для проверки равенства двух экземпляров класса и Clone для создания точной копии данного экземпляра класса.
Объекты типа TSerializableObject обычно не создаются сами по себе конcтруктором Create и свойство ID в них не назначается произвольным образом. Все это делается методами коллекции TSerializableCollection, которая полностью управляет временем жизни объектов, загрузкой и сохранением их в бинарном потоке, назначением уникальных идентификаторов, согласованием изменений и прочими аспектами работы с объектами данных.
Класс TStringBuilder
Этот класс предназначен для динамического создания строки из отдельных фрагментов и изменения строки произвольной длины. В принципе, можно изменять и дополнять обычную строку типа AnsiString. Однако при каждом изменении длины такой строки происходит выделение памяти под новую строку и копирование в нее данных. Перераспределение памяти – это довольно-таки медленная операция, независимо от используемого менеджера памяти. В экземпляре TStringBuilder строка, по возможности, изменяется на месте, без лишней перетасовки памяти. Например, если необходимо сформировать строку из элементов некоторого списка, то вместо того, чтобы по очереди добавлять строки к переменной типа String, лучше создать экземпляр класса TStringBuilder, добавить каждую строку из списка методом Append, а затем преобразовать результат к типу String вызовом метода ToString класса TStringBuilder.
В классе TStringBuilder реализованы основные методы для внесения изменений и дополнения строки. В частности, метод Append добавляет к данной строке другую строку, фрагмент строки, один или несколько повторяющихся символов, десятичное или шестнадцатеричное представление числа; метод Insert вставляет подстроку в указанную позицию; метод Delete удаляет фрагмент строки; метод Reverse обращает порядок символов во всей строке или в отдельном фрагменте; метод Clear очищает строку. Еще есть методы для создания копии экземпляра класса TStringBuilder (метод Clone), преобразования строки или ее фрагмента к типу AnsiString (метод ToString), заполнения фрагмента строки указанным символом (метод Fill), добавления в конец строки символов перевода строки (метод AppendNewLine) и другие. Свойство Chars класса TStringBuilder позволяет работать со строкой как с обычным массивом символов.
Классы ассоциированных списков
Экземпляры классов: TIntegerAssociatedList, TIntegerHashTable, TStringAssociatedList, TStringHashTable представляют собой набор ключей типа Integer или String, с каждым из которых связано значение типа TObject. Они предназначены для быстрого поиска значений в списке по ключу. Классы типа AssociatedList хранят ключи в виде сортированного списка. Классы типа HashTable реализуют ассоциативный массив (словарь) с использованием хеширования ключей. Вероятно, сортированный список лучше подходит для небольших наборов данных в пределах одной-двух сотен элементов, когда накладные расходы на хеширование не оправданны. Хеш-таблицы можно применять для больших наборов данных, т.к. при использовании хеш-функции, дающей хорошее распределение, скоростные характеристики хеш-таблицы близки к O(1) (константны).
Во всех классах реализованы операции для добавления и удаления элементов, извлечения значения по ключу. Свойство OwnValues управляет принадлежностью значений спискам. Например, если это свойство равно True, то при очистке списка методом Clear, для каждого значения типа TObject будет вызван метод Free. По умолчанию это свойство равно False. При вызове конструктора классов TStringAssociatedList, TStringHashedList можно указать, нужно ли различать регистр символов в значениях ключей типа String. Чтобы заранее распределить память под известное число элементов, добавляемых в ассоциированный список, можно передать соответствующее значение в конструктор класса или воспользоваться методом EnsureCapacity.
Классы TExcelRange, TExcelInterval
Эти классы используются для группирования ячеек рабочего листа. Экземпляр класса TExcelInterval представляет собой прямоугольный диапазон ячеек, экземпляр TExcelRange – коллекцию прямоугольных диапазонов. Когда экземпляр класса TExcelRange проецируется на рабочий лист, вся коллекция диапазонов может быть представлена одним объектом типа Excel97.ExcelRange. Таким образом можно применять различные операции, например, заливку фона ячеек или прорисовку границ, ко всему диапазону сразу, даже если он содержит несмежные ячейки. В модуле AcedExcelReport коллекция интервалов TExcelRange используется, кроме того, для получения текстовой ссылки на диапазон ячеек, которая передается в качестве аргумента для функций рабочего листа, типа "СУММ" или "СЧЁТ".
При создании интервала, т.е. экземпляра класса TExcelInterval, в его конструктор передается номер столбца, строки и, возможно, число строк и столбцов в интервале. Если число строк и столбцов не указано, создается интервал, состоящий из одной ячейки. Кроме того, можно создать интервал, который представляет все ячейки одной или нескольких строк или все ячейки одного или нескольких столбцов рабочего листа. Чтобы спроецировать интервал на рабочий лист, т.е. получить объект типа Excel97.ExcelRange, нужно вызвать функцию GetRange. Для того, чтобы проверить равенство двух интервалов, используется функция Equals. Метод Clone возвращает экземпляр класса, представляющий копию интервала. В классе TExcelInterval имеются свойства, возвращающие номер первой и последней строк интервала, а также номера первого и последнего столбцов.
Экземпляр класса TExcelRange создается в виде пустой коллекции. Но в его конструктор можно передать данные первого интервала, который сразу будет добавлен в коллекцию. Кроме того, коллекция интервалов может быть создана на основе другого экземпляра TExcelRange. В этом случае в нее добавляются копии всех интервалов исходной коллекции. Новые интервалы добавляются в коллекцию методами Add, AddRows, AddColumns.
Чтобы полностью очистить коллекцию и освободить память, занимаемую каждым интервалом, используется метод Clear. К отдельным интервалам в составе коллекции можно обратиться через свойство Intervals. Число интервалов возвращается свойством IntervalCount. Метод Equals сравнивает коллекцию с другой коллекцией интервалов. Если две коллекции содержат одни и те же (равные) интервалы, метод возвращает True, если есть какие-либо различия, возвращается False. Метод Clone возвращает копию коллекции, содержащую копии каждого интервала. Для удобства использования интервалы могут быть отсортированы методом EnsureSorted коллекции. Сортировка выполняется по строкам, а затем по столбцам левой верхней ячейки интервала. Чтобы получить объект типа Excel97.ExcelRange представляющий коллекцию интервалов TExcelRange, надо вызвать функцию GetRange этого класса. Методы GetAbsoluteAddress и GetRelativeAddress класса TExcelRange возвращают текстовую строку, содержащую ссылку на все ячейки коллекции интервалов. Первый метод отличается от второго тем, что он возвращает абсолютную ссылку, т.е., например, строку "R1C1:R2C5;R6:R8". Второй метод возвращает строку, содержащую ссылку относительно указанной ячейки. Например, для той же коллекции интервалов ссылка относительно ячейки R2C5 представляется строкой: "R[-1]C[-4]:RC;R[4]:R[6]".
Классы TIntegerList, TWordList
Эти классы предназначены для работы со списком значений типа Integer или Word, соответственно. Классы полностью аналогичны друг другу, поэтому рассмотрим только первый из них – класс TIntegerList. Список значений может быть сортированным или несортированным. Это зависит от свойства MaintainSorted. Если оно равно True, при выполнении операций добавления и удаления элементы списка гарантированно располагаются в порядке возрастания значений. Если свойство MaintainSorted равно False, новые элементы добавляются в конец списка, независимо от их значений. В этом случае допустимо использовать метод Insert для вставки элемента в произвольное место списка. Кроме того, можно использовать методы UnorderedRemove и UnorderedRemoveAt для быстрого удаления элементов списка. При вызове данного метода удаляемый элемент замещается последним элементом списка, после чего свойство Count уменьшается на 1. Таким образом, удаление элемента списка происходит без смещения следующих за ним элементов. Когда MaintainSorted равно True, при вызове методов Insert и UnorderedRemove/UnorderedRemoveAt возникает исключение, т.к. произвольная вставка и unordered-удаление могут нарушить порядок сортировки элементов.
Для поиска значений в списке применяется метод IndexOf, который возвращает индекс найденного элемента или -1, если искомый элемент отсутствует. Когда элементы списка отсортированы, используется быстрый алгоритм бинарного поиска, в противном случае – линейный поиск с помощью функции G_Scan_Integer из модуля AcedBinary. Иногда бывает удобнее не поддерживать список все время в отсортированном виде, так как это замедляет процесс вставки и удаления элементов. Можно просто перед началом поиска вызвать метод EnsureSorted. Тогда, если список не был отсортирован, он сортируется, а затем выполняется быстрый бинарный поиск. Естественно, это имеет смысл делать, только если поиск выполняется многократно, т.к. сама сортировка занимает больше времени, чем линейный поиск.
Аналогично классу TBitList, в TIntegerList есть методы для загрузки списка из бинарного потока типа TBinaryReader, из произвольной области памяти, из реестра Windows или из другого экземпляра класса TIntegerList.
Список может быть сохранен в бинарном потоке TBinaryWriter или в реестре Windows. Кроме того, список может быть загружен из строки, содержащей данные в кодировке Base64, и может быть сохранен в виде строки в кодировке Base64. Новые элементы обычно добавляются в список методами Add или AddIfNotExists, когда необходимо избежать дублирования значений в списке. Чтобы удалить элемент из списка по индексу, вызывается метод RemoveAt, а чтобы удалить элемент с определенным значением – метод Remove. При удалении значений методами Remove и RemoveAt порядок остающихся в списке элементов не меняется. Список полностью очищается при вызове метода Clear. Функция Equals позволяет поэлементно сравнить два списка, а функция Clone возвращает копию данного экземпляра класса TIntegerList. Когда свойство MaintainSorted равно False, экземпляр TIntegerList можно использовать в качестве стека. Значения помещаются в стек (в конец списка) методом Add, а извлекаются из стека (с конца списка) методом Pop в соответствии с правилом: "первым вошел – последним вышел". Получить последнее значение, помещенное в стек, без его удаления можно методом Peek. При вызове методов Peek и Pop количество элементов в списке не проверяется. Ответственность за то, что список не является пустым, возлагается на пользователя класса. Обратиться к отдельным элементам списка можно через свойство ItemList, которое возвращает указатель на массив элементов типа Integer. Число используемых элементов в этом массиве определяется свойством Count. Количество элементов, под которое распределена память во внутреннем массиве, считывается и устанавливается свойством Capacity. Начальное значение этого свойства передается как параметр в конструктор класса TIntegerList. При описании модуля AcedBinary были рассмотрены функции, позволяющие работать с массивами чисел типа Integer как с множествами, в частности, применять к ним различные логические операции. Эти операции можно производить и с экземплярами класса TIntegerList. Например, пусть имеется два отсортированных списка значений типа Integer List1 и List2, из которых необходимо получить третий список List3, содержащий значения, присутствующие в первом списке, но отсутствующие во втором.Это соответствует операции вычитания множеств. Можно создать новый экземпляр класса TIntegerList для хранения списка-результата List3, передав в его конструктор число элементов первого списка, т.к. это число соответствует максимальной длине выходного массива для случая, когда два исходных множества не имеют пересечения. Операция вычитания множеств и заполнения списка List3 выполняется одной строкой кода: List3.Count := G_AndNotSet_Integer(List1.ItemList, List1.Count, List2.ItemList, List2.Count, List3.ItemList).
Модуль AcedBinary
В этом модуле собрано большое число функций для работы с блоками памяти, массивами байт, слов, двойных слов, битовыми строками. Подробное их описание можно найти в исходном коде модуля. Следует обратить внимание на то, что функции из модулей AcedBinary и AcedCrypto используют инструкции MMX для достижения большей производительности. Поэтому на компьютере пользователя должен быть установлен, как минимум, процессор Pentium MMX или Pentium II, поддерживающий данный набор инструкций. Рассмотрим основные группы функций в AcedBinary.
К первой группе относятся функции, работающие с массивами одно-, двух- или четырехбайтных элементов. Например, распределить блок памяти и сразу обнулить его можно вызовом функции G_AllocMem. Когда необходимо заполнить нулями уже распределенный массив, пригодится функция G_ZeroMem. Если нужно скопировать содержимое одного блока памяти в другой и заранее известно, что эти блоки памяти не перекрываются, вместо стандартной процедуры Move лучше воспользоваться функцией G_CopyMem из AcedBinary, которая выполнит то же действие значительно быстрее. При работе с массивами двойных слов можно использовать функцию G_FillLongs для заполнения массива определенным значением, функцию G_CopyLongs для копирования такого массива или его фрагмента в другой массив. Когда требуется найти число типа Word или Integer в несортированном массиве, вместо организации цикла с перебором элементов массива лучше вызвать функцию G_Scan_Word или G_Scan_Integer. Эти функции используют специальные машинные инструкции для быстрого поиска значения в массиве двух- или четырехбайтных элементов.
В AcedBinary есть группа функций для работы с отсортированными массивами элементов типа Integer, Word, LongWord, которая включает функции для сортировки массивов по возрастанию (G_Sort…), выполнения бинарного поиска (G_Search…), обращения порядка элементов (G_Reverse…). Кроме того, такие массивы можно рассматривать как множества и применять к ним соответствующие операции. Например, отсортированный массив уникальных элементов типа Integer – это одно из возможных представлений множества: [-2147483648..2147483647].
Функция G_AndSet_Integer формирует из двух таких массивов новый массив, состоящий только из тех элементов, которые присутствуют в обоих массивах; G_OrSet_Integer возвращает массив, состоящий из элементов, которые присутствуют хотя бы в одном из исходных массивов; G_XorSet_Integer формирует массив из элементов, которые присутствуют только в одном из исходных массивов; G_AndNotSet_Integer возвращает массив элементов, которые присутствуют в первом, но отсутствуют во втором массиве. Есть еще две информационные функции для работы со множествами: G_SetIntersectsWith_Integer возвращает True, если в двух массивах есть какие-либо общие элементы, т.е. пересечение двух множеств не является пустым множеством; G_SetContainedIn_Integer возвращает True, если все элементы первого массива присутствуют во втором массиве, т.е. первое множество является подмножеством второго. Аналогичные функции имеются для массивов элементов типа Word и LongWord. Эти функции могут использоваться совместно с классами AcedIntegerList и AcedWordList, определенными в модуле AcedStreams. Следующая группа функций в AcedBinary предназначена для работы с битами в составе двойного слова, т.е. значения типа LongWord. Эта группа включает функции: G_CeilPowerOfTwo, G_FloorPowerOfTwo (округляют двойное слово до ближайшей степени числа 2 вверх или вниз, соответственно); G_ReverseBits (обращает порядок следования битов); G_BitTest32, G_BitSet32, G_BitReset32, G_BitToggle32 (используются для проверки, установки, сброса или инвертирования одного бита в составе двойного слова). Есть также функции для подсчета числа установленных и сброшенных битов, поиска первого или последнего бита с заданным состоянием и т.п. Последняя группа функций аналогична только что рассмотренным, но эти функции работают с битами в составе битовой строки. Например, функция G_BitSet устанавливает в единицу бит с заданным индексом в битовой строке, адресуемой первым параметром этой функции; функция G_ToggleBits инвертирует все биты в заданном диапазоне битовой строки; G_CountOfFreeBits подсчитывает число нулевых битов в битовой строке; G_SetBitScanForward возвращает индекс первого установленного, т.е.единичного, бита в составе битовой строки. Эти и другие функции используются классом TBitList, определенным в модуле AcedLists, для эффективной работы с данными, представленными в виде битовой строки.
Модуль AcedCommon
В AcedCommon собраны разнообразные функции, в том числе, для форматирования даты и времени, преобразования числа в строку и строки в число, записи числовых и денежных сумм прописью, замены символов строки шестнадцатеричными кодами этих символов и, наоборот, замены кодов символами, кодирования байтовых массивов и строк в Base64 и обратного их преобразования. Кроме того, здесь находятся функции для вычисления контрольной суммы Адлера и CRC32, работы со счетчиками, таймером высокого разрешении, а также несколько вспомогательных функций.
Модуль AcedCompression
В модуле AcedCompression находятся функции, предназначенные для сжатия бинарных данных методом Зива-Лемпела с последующим преобразованием в код Хаффмана. Упаковка данных осуществляется функцией G_Deflate, которая принимает в качестве параметра Mode одну из констант, выбирающих режим сжатия. Возможны следующие режимы: dcmNoCompression – данные не сжимаются, просто копируются в выходной массив. Функция G_Deflate переключается в этот режим автоматически, если сжимаемые данные не являются избыточными, например, если они зашифрованы или упакованы другим методом. Размер исходного массива увеличивается при этом на 4 байта, в которых сохраняется длина исходного массива. Таким образом, 4 байта – это максимальная величина, на которую увеличивается длина исходного массива в случае невозможности его сжатия.
dcmFastest – используется, когда надо сжать данные максимально быстро. При этом качество сжатия во многом зависит от характера самих данных. Если они содержат большое число повторяющихся фрагментов, использование этого режима нежелательно, т.к. в нем используется "облегченный" вариант словарного метода сжатия (Зива-Лемпела). Сжатие Хаффмана, наоборот, реализовано в полной мере. Оно основано на разности частот появления отдельных кодов символов. Если к исходным данным применим такой способ упаковки и они содержат мало повторяющихся последовательностей, режим dcmFastest может оказаться даже более эффективным, чем режим dcmMaximumCompression, за счет большей дисперсии частот отдельных кодов. Это справедливо, например, при сжатии wav-файлов. Максимальное расстояние между повторяющимися последовательностями байт в этом режиме принимается равным 8191 байту.
dcmFast – обычная степень сжатия, достигаемая за минимальное время. Вероятно, данный режим является оптимальным при сжатии бинарных данных. В этом случае использование режимов dcmNormal и dcmMaximumCompression приводит к уменьшению размера выходного массива на 1-2%, но время сжатия возрастает в несколько раз. В режиме dcmFast максимальное расстояние между повторяющимися фрагментами данных равно 65535 байтам.
dcmNormal – обычно соответствует хорошей степени сжатия.
Его можно использовать для данных с большим числом повторяющихся фрагментов, например, текстов или doc-файлов. При этом сжатие происходит в 1.5-2 раза быстрее, чем в режиме dcmMaximumCompression, но примерно во столько же раз медленнее, чем в режиме dcmFast. Максимальное расстояние между повторяющимися группами байт принимается равным 131071 байту.
dcmMaximumCompression – в этом режиме достигается максимальная степень упаковки, доступная для реализованного в AcedCompression метода сжатия. Может применяться для сильно избыточных данных или для данных, которые редко сжимаются, а затем многократно используются. Максимальное расстояние между повторяющимися фрагментами данных принимается равным 262143 байтам.
В модуле AcedCompression есть два варианта функции G_Deflate. В первом варианте функция сама распределяет память под массив, достаточный для хранения упакованных данных. Во втором варианте массив должен быть создан заранее, а функция G_Deflate только заполняет его данными. Имеются также функции для распаковки сжатого массива данных. Размер исходного массива можно получить вызовом функции G_GetInflatedLength, а сама распаковка данных выполняется функцией G_Inflate. Имеется два варианта этой функции. В первом варианте память под массив, содержащий выходные данные, распределяется функцией G_Inflate. Во втором варианте, массив для хранения распакованных данных должен быть создан заранее размером не менее, чем длина, которую вернул вызов функции G_GetInflatedLength для упакованного массива данных. Следует отметить, что используемый режим сжатия не влияет на скорость распаковки данных (естественно, кроме режима dcmNoCompression, при котором данные итак находятся в распакованном виде). Продолжение
Модуль AcedCrypto
Модуль объединяет функции для шифрования и верификации данных, а также для генерации псевдослучайных чисел.
Функции G_RC4… предназначены для шифрования данных методом RC4. Это широко распространенный поточный шифр. Возможно, по стойкости он уступает блочным шифрам, типа Rijndael или CAST, но, зато, значительно превосходит их по производительности. Не так давно в этом шифре было обнаружено несколько уязвимостей, которые касаются алгоритма заполнения внутреннего массива ключей (key scheduling algorithm). Чтобы избежать связанной с этим гипотетической опасности взлома зашифрованных данных, компания RCA, разработчик данного шифра, рекомендует пропускать первые 256 байт выходной псевдослучайной последовательности. Функции из модуля AcedCrypto написаны с учетом этого замечания. При правильном использовании метод RC4 является вполне надежным. Надо только помнить основную особенность поточных шифров – один и тот же ключ не должен применяться для шифрования различных данных. Например, ключ может вычисляться с помощью побитовой операции xor двух чисел, одно из которых является постоянным и рассчитывается как цифровая сигнатура пароля, вводимого пользователем с клавиатуры, а второе число является изменяемым и создается генератором псевдослучайных чисел перед каждым сеансом шифрования данных. Изменяемое число может сохраняться в первых байтах зашифрованного потока данных.
Следующий набор функций из AcedCrypto G_RC6… предназначен для шифрования данных методом RC6. Это блочный шифр, разработанный компанией RCA. Нормальная длина ключа при шифровании методом RC6 составляет 256 бит (32 байта). В AcedCrypto имеются функции для инициализации процесса шифрования, задания начального вектора, шифрования и дешифрования 128-битного блока данных в режиме ECB (электронная кодовая книга), шифрования и дешифрования произвольного массива байт в режимах CFB (обратная загрузка шифротекста) и OFB (обратная загрузка выходных данных) с размером блока 128 бит. В режимах CFB и OFB размер шифруемых и дешифруемых фрагментов данных должен совпадать или размер этих фрагментов должен быть кратен 16 байт.
При использовании в качестве ключа шифра пароля, вводимого пользователем с клавиатуры, нужно передавать в функцию G_RC6Init в качестве ключа не саму строку, содержащую введенный пароль, а цифровую сигнатуру SHA-256, рассчитанную для строки, введенной пользователем. Далее в модуле AcedCrypto находятся функции для расчета значения односторонней хеш-функции SHA-256 массива байт. Цифровая сигнатура SHA-256 представляет собой массив из 32 байт, который вычисляется по данным исходного массива байт таким образом, чтобы, во-первых, невозможно было подобрать другой массив байт, для которого значение сигнатуры совпало бы с данной сигнатурой, и, во-вторых, чтобы по значению цифровой сигнатуры невозможно было восстановить какие-либо значения из исходного массива байт. Алгоритм расчета SHA-256 реализован в соответствии с документом FIPS180-2. Имеются функции для расчета цифровой сигнатуры массива байт и строки символов. Кроме того, можно рассчитать сигнатуру для массива байт, представленного в виде нескольких фрагментов. Это удобно, например, при работе с данными, организованными в виде потока типа TStream. Функции G_Random… реализуют собой генератор псевдослучайных чисел Mersenne Twister, период которого можно считать равным бесконечности. Функция G_RandomNext возвращает число типа LongWord, равномерно распределенное в интервале от 0 до $FFFFFFFF; функция G_RandomUniform генерирует псевдослучайную величину, равномерно распределенную в интервале [0.0, 1.0]; G_RandomGauss используется для моделирования непрерывной случайной величины с нормальным законом распределения. Функция G_RandomFill позволяет заполнить область памяти псевдослучайной последовательностью байт. Функция G_RandomEncryptVector шифрует внутренний вектор, используемый при генерации псевдослучайных чисел, методом RC6. Это соответствует переводу генератора на новую числовую последовательность. Следует отметить, что сама по себе выходная числовая последовательность данного генератора не является криптографически стойкой.Чтобы превратить ее в криптографически стойкую, можно воспользоваться, например, функцией G_SHA256Transform.
Модуль AcedExcelReport
Предназначен для генерации отчетов с помощью пакета Microsoft Excel. Отчеты создаются на основе XLT-шаблонов или как новые рабочие книги. При построении отчетов, кроме AcedExcelReport, используются модули: Variants, ActiveX, ComObj и, главное, Excel97, содержащий данные библиотеки типов Microsoft Excel. Версии этой библиотеки для Office 2000 и Office XP мало чем отличаются от Excel 97 с точки зрения возможностей, используемых при генерации отчетов. Однако, при подключении к программе модулей Excel2000 и ExcelXP нарушается совместимость со старой версией Microsoft Excel 97. При построении отчетов в Excel через COM-интерфейсы особое внимание следует уделить сокращению числа обращений к серверу, особенно через интерфейс IDispatch. Например, вместо того, чтобы последовательно заполнять данными отдельные ячейки прямоугольной области рабочего листа, гораздо эффективнее создать в памяти вариантный массив, заполнить его, и передать в Excel сразу все значения ячеек прямоугольной области.
Построение отчета обычно начинается с подготовки данных, которые нужно представить в виде таблицы. При этом могут создаваться временные списки типа TArrayList для группирования, сортировки данных и т.п. Затем, когда уже известно количество элементов в каждой области отчета, создаются массивы типа Variant вызовом функции VarArrayCreate из модуля Variants. В эту функцию передаются диапазоны индексов создаваемого массива и тип элементов массива, который может быть равен varVariant, если в массив помещаются данные различного типа. При создании вариантного массива для последующего проецирования его на рабочий лист Microsoft Excel надо учитывать, что первый индекс в этом массиве задает строку, а второй – столбец рабочего листа. После создания массива он заполняется данными, которые нужно показать в отчете. Все значения типа Currency должны быть преобразованы к типу Double функцией G_CurrencyToDouble из модуля AcedCommon перед помещением их в вариантный массив или в ячейку рабочего листа. Для подсчета итоговых сумм лучше использовать функции рабочего листа, а не вычислять эти суммы в процессе группирования данных и не помещать их в отчет в виде готовых чисел.
Следующим этапом после подготовки данных и заполнения вариантных массивов является запуск приложения Microsoft Excel и создание рабочей книги.
Для запуска Microsoft Excel вызывается функция StartExcel из модуля AcedExcelReport. Если на компьютере пользователя Microsoft Excel не установлен, на экране появляется соответствующее сообщение для пользователя и функция возвращает False. В случае успеха она возвращает True. Если Excel уже запущен, функция StartExcel ничего не делает, просто возвращает True. Для создания новой рабочей книги вызывается функция CreateExcelWorkbook. В нее как var-параметр передается переменная типа Excel97._Workbook, в которой затем сохраняется ссылка на созданный экземпляр рабочей книги. Один из вариантов функции CreateExcelWorkbook создает пустую рабочую книгу с заданным числом листов. Другой вариант этой функции создает рабочую книгу на основе указанного XLT-шаблона. В случае успеха эта функция возвращает True, а если в процессе создания рабочей книги произошло исключение, оно перехватывается и функция возвращает False. После этого настраивается внешний вид рабочей книги. Это делается вызовом процедуры InitializeExcelWorkbook. При этом можно определить заголовок окна, в котором показывается отчет, а также указать на необходимость или отсутствие необходимости отображения ярлычков листов рабочей книги, нулевых значений на листе, заголовков строк и столбцов (A, B, C…1, 2, 3…), линий сетки, горизонтальной и вертикальной полосы прокрутки рабочего листа. Получить ссылку на первый лист рабочей книги можно следующим образом: (WB.Worksheets[1] as _Worksheet), где WB – ссылка на рабочую книгу. Диапазон всех ячеек рабочего листа возвращается функцией Get_Cells, вызванной для экземпляра класса _Worksheet. Ссылку на этот диапазон лучше сразу сохранить в отдельной переменной, чтобы впоследствии передавать ее в качестве параметра в различные функций из модуля AcedExcelReport. Следует обратить внимание на то, что по умолчанию нумерация строк и столбцов на рабочем листе начинается с единицы. Если предпочтительно использовать нумерацию с нуля, можно установить в единицу глобальные переменные ExcelRowOffset и ExcelColumnOffset, объявленные в модуле AcedExcelReport.Значения этих переменных прибавляются, соответственно, к номеру строки и номеру столбца при вызове любой функции из AcedExcelReport, в которую передается номер строки и/или номер столбца. Переменные ExcelRowOffset и ExcelColumnOffset позволяют произвольным образом задать начало отсчета на рабочем листе. Например, если таблица данных начинается с ячейки B5, можно присвоить ExcelRowOffset значение 5, а ExcelColumnOffset значение 2 и при обращении к ячейкам таблицы данных использовать индексацию с нуля. Эти переменные обнуляются при вызове процедуры InitializeExcelWorkbook. Обратиться к запущенному экземпляру приложения Microsoft Excel можно через глобальную переменную ExcelApp, объявленную в модуле AcedExcelReport. Для завершения работы приложения Microsoft Excel и выгрузки его из памяти предназначена процедура ShutdownExcel. Обычно ее не нужно вызывать из прикладной программы, т.к. ShutdownExcel вызывается из раздела finalization модуля AcedExcelReport.
Модуль AcedLists
Содержит классы для работы со списками. Кроме перечисленных ниже классов, в AcedLists определены функции G_SortAscending и G_SortDescending для потокобезопасной сортировки, соответственно, по возрастанию и убыванию массива указателей. Элементы сортируемого массива сравниваются с помощью функции, адрес которой передается в качестве параметра в функции G_SortAscending и G_SortDescending. Вместо этих двух функций можно было бы оставить одну, а порядок сортировки изменять путем задания различных функций сравнения элементов массива. Однако, при изменении структуры данных пришлось бы вносить согласованные изменения во все функции сравнения. Этот путь легко может привести к ошибкам. Гораздо удобнее и надежнее иметь одну функцию сравнения элементов массива и, в зависимости от желаемого порядка сортировки, вызывать либо G_SortAscending, либо G_SortDescending.
Модуль AcedMemory
Этот модуль предназначен для замены стандартного менеджера памяти Borland Delphi альтернативным механизмом распределения памяти. Целью разработки нового менеджера памяти было стремление оптимизировать работу с небольшими блоками памяти, которые возникают при организации хранения данных в виде объектной базы, когда каждая запись представляется экземпляром соответствующего класса. Под оптимизацией подразумевается, во-первых, повышение скорости при выделении/освобождении блоков памяти, во-вторых, решение проблемы фрагментации свободных блоков. Рассмотрим подробнее, за счет чего это достигается при использовании AcedMemory.
Память запрашивается у операционной системы вызовом VirtualAlloc блоками размером 2МБ, которые в данном контексте называются регионами. Каждый регион состоит из 32 блоков размером 64кБ. Каждый такой блок предназначен для фрагментов памяти определенного размера. Например, один блок может содержать 4 фрагмента по 16кБ, другой – 1024 фрагмента по 64 байта и т.д. Когда из прикладной программы вызывается функция GetMem, размер выделяемых фрагментов памяти округляется вверх до ближайшего числа, равного 2 в степени N, где N может быть от 3 до 16. Когда все фрагменты памяти нужного размера в составе блока заняты, в регионе выделяется следующий блок под фрагменты памяти нужного размера. Если все 32 блока в составе региона заняты, создается новый регион размером 2МБ. Адрес каждого фрагмента памяти (до 64кБ) кратен его размеру, что положительно влияет на производительность. В отличие от стандартного менеджера памяти, размер выделяемых фрагментов не хранится в памяти с отрицательным смещением, а вычисляется, при необходимости, по адресу фрагмента. Когда требуется выделить фрагмент памяти размером более 64кБ, этот размер округляется вверх до числа, кратного 4096 байтам, а затем вызывается функция VirtualAlloc для запроса соответствующего блока памяти у операционной системы.
Для подключения AcedMemory к проекту, ссылку на этот модуль необходимо добавить в раздел uses файла проекта, причем AcedMemory должен стоять первым в этом разделе.
Стандартные модули Forms, Classes и прочие должны находиться в списке uses, после AcedMemory. Если этого не сделать, при завершении программы возникнет ошибка. Это происходит из-за того, что в разделе finalization модуля Classes косвенно вызывается функция FreeMem для освобождения некоторых блоков памяти, выделенных в процессе работы программы. Модули финализируются в порядке, обратном их перечислению в разделе uses файла проекта. Чтобы менеджер памяти AcedMemory был еще активен на момент вызова раздела finalization модуля Classes, ссылка на AcedMemory должна находиться в списке uses файла проекта перед модулем Classes и любыми другими модулями, использующим Classes, как, например, модуль Forms. Кроме, собственно, механизма для выделения/освобождения памяти, в модуле AcedMemory имеется несколько вспомогательных функций: G_GetMemSize возвращает размер блока памяти, адрес которого передан как параметр; функция G_GetTotalMemAllocated возвращает общий объем памяти, выделенный прикладной программе. Вызов этой функции не приводит к длительному пересчету размеров всех выделенных блоков. Вместо этого просто возвращается значение внутреннего счетчика памяти. Функция G_GetTotalMemCommitted возвращает общий размер физической памяти, выделенной программе операционной системой. Это число не учитывает память, выделенную в "куче" для служебных структур модуля AcedMemory, а также память, распределенную средствами, отличными от менеджера памяти.
Модуль AcedStorage
Содержит типы и классы, предназначенные для организации объектного хранилища данных на локальном компьютере или на файловом сервере с возможностью многопользовательского доступа. Хранение данных на файловом сервере широко использовалось до появления серверов баз данных. В настоящее время для хранения информации почти всегда используется какой-либо сервер БД, контролирующий соблюдение ограничений и целостность данных. Несмотря на очевидные достоинства такого подхода, в некоторых случаях имеет смысл пожертвовать этими возможностями в обмен на компактный формат хранения данных, высокую производительность при выборке и изменении данных, отсутствие необходимости установки серверной части приложения, объектно-ориентированное представление данных. Возложение ответственности за целостность данных на клиентское приложение приводит к некоторому усложнению кода. Тем не менее, наличие готового механизма разрешения коллизий на файловом сервере в значительной мере облегчает эту задачу.
При небольшом объеме хранящихся данных довольно эффективным решением может быть загрузка в память приложения целых таблиц с данным и полная перезапись файлов с данными при их модификации. Недостаток такого способа хранения информации, в отличие от реляционных баз данных, заключается в том, что при изменении даже одного элемента коллекции файл данных приходится переписывать целиком. Чтобы уменьшить значимость этой проблемы можно разбивать большие наборы данных на группы элементов, объединенных по какому-либо признаку, и хранить каждую группу в отдельном файле данных. В памяти следует создать ассоциативный массив, связывающий значение признака с соответствующей коллекцией, представляющей часть общего набора данных. При внесении изменений в одну из таких коллекций нет необходимости перезаписывать другие коллекции того же набора данных.
В реляционных базах данных язык SQL можно рассматривать в качестве посредника между потребителем данных (клиентом) и СУБД (сервером). При подготовке и выполнении сложных запросов возникают две проблемы.
Во-первых, клиент должен сгенерировать текст запроса в виде предложения на языке SQL. Если структура базы данных включает сложные связи между таблицами, конструирование SQL-запросов представляет собой нетривиальную задачу. Во-вторых, на стороне сервера должен быть произведен разбор полученного SQL-предложения, а затем, с учетом метаданных, оптимизация запроса и выполнение кода для фактической манипуляции данными. Эффективное выполнение SQL-запросов представляет собой научную проблему. С другой стороны, можно изначально отказаться от посредника в виде языка SQL при взаимодействии потребителя с хранилищем данных. Безусловно, такой подход может быть применен только ограниченно, в рамках одного приложения или группы проектов. Однако, при правильной организации структуры данных прямое обращение клиентов к данным хранилища может не только повысить производительность, но и, в конечном счете, уменьшить сложность кода, сократить затраты на разработку и сопровождение приложения, чему способствует, в частности, наличие механизма контроля версий данных. При использовании модуля AcedStorage все данные представляются в виде объектов: таблицы – в виде экземпляров класса TSerializableCollection, записи в таблицах – экземплярами класса TSerializableObject, точнее производных от него классов. Для сортировки и поиска записей применяются индексы – объекты типа TDataIndex. К библиотеке AcedUtils прилагается демонстрационный проект – приложение, использующее для хранения данных и одновременного доступа к данным нескольких пользователей классы из модуля AcedStorage. Пример кратко описывается далее в этой статье. Рассмотрим все эти классы подробнее.
Модуль AcedStreams
Содержит классы, предназначенные для работы с бинарным потоком данных. Бинарный поток в данном контексте – это просто массив байт в памяти, размер которого динамически увеличивается по мере добавления новых данных. После того, как все данные помещены в поток, содержимое бинарного потока может быть преобразовано в массив, строку в кодировке Base64, сохранено в файле или в другом потоке типа TStream. При этом может быть произведена упаковка данных методом LZ+Huffman, шифрование методом RC6 и подпись цифровой сигнатурой SHA-256. Для проверки целостности данных вместе с ними в бинарный поток помещается значение контрольной суммы Адлера. Бинарный поток создается с помощью экземпляра класса TBinaryWriter. Cтандартные классы в составе VCL ориентированы на работу с потоком типа TStream. Чтобы решить эту проблему в модуле AcedStreams предусмотрен класс TStreamWriter, который является потомком стандартного класса TStream и, одновременно, представляет собой оболочку над TBinaryWriter. Таким образом, можно, например, сохранить изображение типа TGraphic в потоке TBinaryWriter. Для этого следует создать экземпляр класса TStreamWriter, передав в его конструктор ссылку на TBinaryWriter. Затем этот экземпляр TStreamWriter можно передать как параметр в метод TGraphic.SaveToStream. Кроме классов TBinaryWriter, TStreamWriter, в модуле AcedStreams имеются соответствующие им классы TBinaryReader, TStreamReader, предназначенные для загрузки данных из бинарного потока.
Когда нужно считать данные из потока, после создания экземпляра класса TBinaryReader конструктором Create, чтобы загрузить массив с данными в память, необходимо вызвать один из следующих методов: LoadFromArray (загружает данные, представленные в виде массива байт, ссылка на который передается в качестве параметра), LoadFromBase64 (загружает данные из строки в кодировке Base64), LoadFromFile (загружает данные из файла, который был предварительно открыт и установлен в позицию, соответствующую началу блока данных), LoadFromStream (загружает данные из потока типа TStream).
Все методы LoadFrom… принимают в качестве параметра ссылку на 256-битный ключ EncryptionKey. Если в этом параметре передан указатель на ключ, отличный от nil, при загрузке данных выполняется их дешифрование с указанным ключом и проверка целостности данных по сохраненному в потоке значению цифровой сигнатуры SHA-256. В классе TBinaryReader имеются методы для чтения из потока значений различного типа, включая String, Integer, TDateTime и т.д., а также метод Read, копирующий заданное число байт из бинарного потока в указанную область памяти. Если данные бинарного потока должны быть считаны некоторым стандартным классом, работающим только с потоками типа TStream, таким как класс TBitmap, нужно создать экземпляр класса TStreamReader, передав в его конструктор ссылку на соответствующий экземпляр TBinaryReader. После этого данные могут быть загружены методом TBitmap.LoadFromStream с передачей в него ссылки на созданный экземпляр TStreamReader. Класс TBinaryWriter используется для создания и заполнения динамического массива, представляющего собой бинарного поток. Данные записываются в поток методами WriteString, WriteInteger, WriteDateTime и т.п. Кроме того, метод Write позволяет скопировать в поток содержимое произвольной области памяти. Когда все необходимые данные помещены в бинарный поток, можно вызвать один из следующих методов: SaveToArray (представляет данные потока, возможно, сжатые и зашифрованные, в виде массива байт), SaveToBase64 (представляет данные в виде строки в кодировке Base64), SaveToFile (сохраняет данные в файле, дескриптор которого передан в качестве параметра), SaveToStream (сохраняет данные в потоке типа TStream). Все методы SaveTo… принимают параметр CompressionMode, который устанавливает режим сжатия бинарных данных перед помещением их в выходной массив (см. перечень констант, выбирающих режим сжатия, при описании модуля AcedCompression). Методы SaveTo… принимают также параметр EncryptionKey. Если он не равен значению nil, то должен содержать ссылку на 256-битный ключ, т.е.массив из 32 байт. В этом случае данные бинарного потока шифруются методом RC6 с указанным ключом. Кроме того, для них вычисляется значение цифровой сигнатуры SHA-256, которая также записывается в выходной массив и используется в дальнейшем при расшифровке данных для проверки их целостности. Ключ шифра передается параметром типа PSHA256Digest. Это является своего рода подсказкой того, что в качестве ключа при шифровании методом RC6 удобно использовать значение односторонней функции SHA-256, рассчитанное для некоторого массива байт или строки символов, вводимой пользователем в качестве пароля.
Модуль AcedStrings
AcedStrings содержит набор функций, предназначенных для работы со строками типа AnsiString. Кроме того, в этом модуле находится класс TStringBuilder, являющийся аналогом классов StringBuilder из .NET или Java. Рассмотрим подробнее, что полезного есть в этом модуле.
В AcedStrings находятся функции, реализующие быстрое сравнение строк с учетом или без учета регистра символов. Сравнение выполняется на больше-меньше или на равно-не равно. При сравнении могут учитываться все символы или только определенное число начальных символов строки. Следующая группа объединяет функции, используемые для преобразования строк и отдельных символов к верхнему или нижнему регистру. Далее следуют функции, которые облегчают работу со строками, сохраненными в кодировке MS-DOS. Они позволяют быстро менять кодировку строк с DOS на Windows и обратно. В процессе преобразования используются массивы перекодировки ToOemChars и ToAnsiChars, определенные в конце интерфейсного раздела модуля. Эти массивы по умолчанию соответствуют кодировке "Кириллица Win1251". Однако, используя небольшую программу, текст которой приведен в виде комментария в модуле AcedStrings, можно сформировать соответствующие таблицы для любой другой кодировки символов. Есть также возможность динамического формирования таблиц перекодировки в момент запуска программы на компьютере конечного пользователя в зависимости от текущих региональных настроек. Этот режим включается при определении символа USE_DYNAMIC_TABLES в коде модуля AcedStrings.
Следующая группа содержит функции поиска, замены, удаления подстрок и отдельных символов. Здесь находятся функции быстрого поиска подстрок с учетом и без учета регистра символов. Поиск выполняется с начала (функции G_PosStr/G_PosText) или с конца строки (функции G_LastPosStr/G_LastPosText). Для поиска отдельных символов служат функции G_CharPos и G_LastCharPos. Функция G_Paste заменяет фрагмент строки определенной длины, начиная с указанного символа, другим фрагментом. Функции: G_ReplaceStr, G_ReplaceText, G_ReplaceChar, G_ReplaceChars, G_ReplaceCharsWithOneChar используются для замены всех вхождений подстроки или некоторого символа в строке другой подстрокой или символом.
Функции: G_Delete, G_CutLeft, G_CutRight, G_DeleteStr, G_DeleteText, G_DeleteChar, G_DeleteChars, G_KeepChars используются для удаления из строки фрагментов, подстрок или отдельных символов. Функции: G_Compact, G_Trim, G_TrimLeft, G_TrimRight удаляют из строки пробелы и управляющие символы. Далее следуют функции для работы с маской. Под маской здесь понимается шаблон, подобный тому, который используется при поиске файлов на диске, как, например, "rpt??w?.xls" или "*.tmp". Функция G_ApplyMask форматирует строку символов с помощью маски, а G_ExtractWithMask, наоборот, извлекает данные из строки по маске; G_ValidateMask используется для проверки того, что строка удовлетворяет заданной маске; функции G_ValidateWildStr и G_ValidateWildText выполняют проверку соответствия строки заданному шаблону, который может содержать специальные символы, предполагающие замену их любым количеством произвольных символов, т.е. аналогичные символу '*' в шаблоне для поиска файлов. Кроме перечисленных, в AcedStrings входят функции: G_CountOfChar, G_CountOfChars для подсчета числа вхождений одного или нескольких символов в строку; G_IsEmpty для проверки того, что строка является пустой или содержит только пробелы и управляющие символы; G_PadLeft, G_PadRight, G_Center для дополнения строки, соответственно, слева, справа или с обеих сторон указанным символом до требуемой длины; функция G_Duplicate формирует строку, состоящую из нескольких копий другой строки; G_StrReverse обращает порядок символов в строке. Если для управления памятью в проекте используется менеджер памяти AcedMemory, и в тексте модуля AcedStrings определен символ USE_ACED_MEMORY, становятся доступны функции G_NewStr, G_Append, G_Insert, G_AppendNewLine. Они могут использоваться как альтернатива классу TStringBuilder для того, чтобы эффективно изменять и дополнять строки типа AnsiString на месте, без лишнего перераспределения памяти. Например, формирование длинной строки в цикле с помощью функции G_Append выполняется значительно быстрее простой конкатенации строк.Однако, этот способ все же менее эффективен, чем использование класса TStringBuilder, т.к. при каждом изменении строки тратится дополнительное время на определение размера области памяти, выделенной под строку. Функции типа G_Append, G_Insert лучше применять для внесения небольшого числа изменений в длинную строку, когда создание специального экземпляра класса TStringBuilder неоправданно.
Операции с числами типа Double и Currency
Следующая группа объединяет функции для работы со значениями типа Double и Currency. В частности, процедуры G_Inc, G_Dec, G_Mult, G_Div принимают в качестве первого параметра ссылку на переменную типа Double или Currency и, соответственно, складывают, вычитают, умножают или делят ее значение на число, переданное вторым параметром. Эти процедуры призваны компенсировать собой отсутствие в языке Delphi операций "+=", "/=" и т.п. из C-подобных языков. Функцию G_CurrencyToDouble необходимо вызывать для приведения значения типа Currency к типу Double, т.к. обычный способ приведения: "Double(Currency)" не срабатывает из-за ошибки в компиляторе. Функция G_ThousandsOfCurrencyToDouble преобразует значение типа Currency к типу Double, одновременно делит его на 1000 и округляет до 3-х знаков после запятой.
Описание демонстрационного проекта
Кроме исходного кода модулей библиотека AcedUtils включает пример, реализующий полноценное приложение для работы с данными на основе коллекций из модуля AcedStorage. Пример включает несколько отчетов, которые строятся с помощью функций из модуля AcedExcelReport. Рассмотрим подробнее задачу, решаемую этим приложением, и некоторые особенности реализации хранилища данных на основе файлового сервера.
Программа предназначена для учета товаров, поступающих от различных поставщиков. Товары классифицируются по категориям. Основные типы и наборы данных, используемые приложением, описываются в модуле DataTypes, являющемся аналогом модуля данных. Структура базы данных состоит из трех таблиц: поставщики товаров (коллекция Suppliers), категории товаров (коллекция Categories) и сами товары (коллекция Products). Каждая из этих сущностей описывается классом, производным от класса TSerializableObject из модуля AcedStorage. Атрибуты сущностей, т.е. поля таблиц базы данных, представляются свойствами этих классов. Обычно каждому такому свойству соответствует private-поле в классе объекта, используемое для хранения значения этого свойства. Кроме того, в модуле DataTypes имеется класс TPictureObject, предназначенный для сохранения в бинарном потоке и чтения из потока данных объекта типа TPicture. Этот класс используется для хранения изображения, ассоциированного с категорией товаров, которая представляется классом TCategoryObject.
Класс TSupplierObject описывает поставщика товара. Он включает свойства: ID – уникальный идентификатор записи (наследуется от класса TSerializableObject); CompanyName – наименование поставщика; Country – страна поставщика; CityRegion – строка с названием города и, возможно, области; Address – адрес поставщика; PostalCode – почтовый индекс; PhoneFax – номера телефонов и факсов; HttpEmail – адреса электронной почты и web-сайта поставщика; ContactPerson – представитель поставщика, с которым поддерживается контакт; Comments – дополнительная информация о поставщике.
Как и во всяком классе, производном от TSerializableObject, в классе TSupplierObject перекрываются методы: Load, Save, Equals, Clone базового класса. В частности, в методе Load выполняется чтение состояния объекта из бинарного потока. Значения всех полей, в том числе FID, унаследованного из TSerializableObject, считываются методами класса TBinaryReader. Метод Save помещает соответствующие данные в бинарный поток, представляемый классом TBinaryWriter. На примере класса TSupplierObject демонстрируется один из приемов оптимального использования пространства в бинарном потоке. Вместо того, чтобы сохранять все подряд значения полей, в методе Save создается переменная Flags, содержащая битовую карту используемых полей, т.е. полей, значения которых отличны от значений по умолчанию. Эта битовая карта помещается в бинарный поток в виде однобайтного значения. Затем в потоке сохраняются значения только тех полей, которые помечены как используемые. В методе Load сначала считывается битовая карта, а затем значения полей, которые помечены в ней единичными битами. В методе Equals класса TSupplierObject выполняется сравнение значений каждого из полей объекта с соответствующими полями другого экземпляра того же класса. Если все поля обоих экземпляров равны, функция возвращает True, в противном случае – False. Метод Clone создает новый экземпляр класса TSupplierObject и копирует в него значения всех полей данного объекта, включая поле FID, унаследованное от класса-предка. Класс TCategoryObject предназначен для описания категории товара. Он содержит наименование категории (свойство CategoryName), комментарий (свойство Comments) и рисунок, иллюстрирующий данную категорию (свойство Picture). Кроме того, класс TCategoryName включает свойство ID, унаследованное от класса-предка TSerializableObject. Аналогично TSupplierObject, в данном классе перекрываются методы Load, Save, Equals и Clone базового класса. Кроме того, перекрывается виртуальный конструктор Create для создания объекта типа TPictureObject, представляющего рисунок, и метод Destroy для освобождения этого объекта.
Следует обратить внимание на то, как в этом классе сохраняется и считывается значение уникального идентификатора записи – поля FID базового класса TSerializableObject. Предполагается, что число возможных категорий товара не превышает 255, поэтому идентификатор записи хранится в виде байта, а не значения типа Integer. При этом сама коллекция категорий товаров представляется экземпляром класса TBytePrimaryKeyCollection. Класс TProductObject содержит свойства, используемые при описания товара: ID – уникальный идентификатор записи; ProductName – наименование товара; SupplierID – внешний ключ, содержащий ссылку на элемент коллекции Suppliers, представляющий поставщика товара; CategoryID – внешний ключ, содержащий ссылку на элемент коллекции Categories, представляющий категорию товара; QuantityPerUnit – строка, описывающая единицу измерения товара; UnitPrice – цена единицы товара; UnitsInStock – количество товара на складе; UnitsOnOrder – ожидаемое количество товара; Discontinued – признак того, что поставки товара прекращены; Little – признак того, что на складе осталось мало соответствующего товара. На примере класса TProductObject демонстрируется работа с версиями объектов. Предполагается, что в первоначальной версии данного класса отсутствовало свойство Little, оно было добавлено во второй версии. Сохраняемая версия элементов коллекции определяется параметром Version при вызове конструктора класса данной коллекции. Так, при вызове конструктора для коллекции Products во втором параметре передается значение 2. В методе Load класса TProductObject проверяется значение параметра Version. Если версия равна 1, данные сохранены в исходном формате, т.е. без поля FLittle. Если сохраненная версия данных равна 2, в бинарном потоке присутствует значение поля FLittle, которое должно быть прочитано методом ReadBoolean класса TBinaryReader. Класс TPictureObject, используемый для хранения рисунка, иллюстрирующего категорию товара, может сам по себе найти применение во многих приложениях.
Он позволяет сохранять в бинарном потоке растровый рисунок (TBitmap), метафайл (TMetafile) или иконку (TIcon), которые представляются классом TPicture из модуля Graphics. Причем, данные изображения постоянно находятся в упакованном виде и распаковываются только, когда нужно загрузить изображение в объект TPicture. Метод Load класса TPictureObject считывает изображение из объекта TPicture, бинарного потока, представленного классом TBinaryReader, или другого экземпляра класса TPictureObject. Метод Save сохраняет изображение в потоке. Метод Assign загружает рисунок в объект TPicture. Функция Equals возвращает True, если данный экземпляр класса TPictureObject, содержит то же самое изображение, что и экземпляр, переданный в функцию как параметр. Свойства IsBitmap, IsMetafile, IsIcon класса TPictureObject позволяют проверить тип изображения, представленного объектом. Свойство Bytes возвращает указатель на массив байт, содержащий упакованное изображение. Свойство Length – длину этого массива в байтах. Коллекции Suppliers, Categories, Products создаются процедурой CreateCollections в модуле DataTypes, которая вызывается из раздела инициализации этого модуля. Перед созданием каждой из коллекций создаются индексы, предназначенные для упорядочивания элементов коллекций по какому-либо признаку. Так, поставщики сортируются по значению свойства CompanyName, т.е. по наименованию поставщика, список категорий – по наименованию категории, товары – по наименованию товара, идентификатору поставщика и идентификатору категории. Индексы по наименованиям являются уникальными, что обеспечивает отсутствие в коллекциях элементов с дублирующимися наименованиями. Индексы по внешним ключам в коллекции товаров используются для проверки наличия подчиненных записей в таблице товаров при попытке удаления записи из главной таблицы – коллекции поставщиков или коллекции категорий товаров. Так как количество поставщиков может быть большим, для коллекции Suppliers включается режим хеширования элементов по значению уникального идентификатора (свойство MaintainHash устанавливается в True).
Уничтожаются коллекции процедурой FreeCollections, вызываемой из раздела finalization модуля DataTypes. Для удобства загрузки и сохранения коллекций в модуле DataTypes определены глобальные функции, такие как LoadSuppliers, LoadProducts, SaveSuppliers и т.п., предназначенные для работы с данными в режиме многопользовательского доступа. Функция LoadSuppliers и аналогичные ей функции для загрузки коллекций Categories и Products просто вызывают метод LoadFile соответствующей коллекции и передают в него имя файла данных и, в случае коллекции поставщиков, пароль для дешифрования файла данных. Функции для сохранения изменений в файле реализуют более сложный алгоритм. Рассмотрим его на примере функции SaveSuppliers, используемой для сохранения на диске коллекции поставщиков. В начале функции с помощью свойства HasChanges проверяется наличие в памяти каких-либо кэшированных изменений для коллекции Suppliers. Если изменений нет, то отсутствует необходимость перезаписи файла данных. Если изменения есть, соответствующий файл данных открывается методом OpenFile коллекции. При этом указывается пароль, используемый для шифрования и дешифрования данных коллекции, а также указывается константа dcmFast, включающая режим быстрого сжатия данных для экономии места на диске и сокращения объема информации, пересылаемой по сети. В случае невозможности открытия файла данных (например, если этот файл открыт для записи другим пользователем и его приложение почему-то зависло), кэш изменений очищает и на экран выводится сообщение об ошибке. После того, как файл данных успешно открыт и, при наличии в нем изменений, перечитан с диска, для коллекции Suppliers вызывается метод ApplyChanges. Этот метод применяет кэшированные изменения к набору данных коллекции. Если изменения применены успешно, коллекция сохраняется на диске вызовом метода SaveIfChanged. Если в процессе применения изменений возникли какие-либо проблемы, данные коллекции перечитываются с диска методом UndoIfChanged, а затем на экран выводится сообщение об ошибке.
Метод UndoIfChanged перечитывает данные с диска только в случае, если коллекция содержит изменения, т.е. ее свойство Changed равно True. В конце работы файл данных должен быть обязательно закрыт методом CloseFile коллекции. Для большей надежности вызов метода CloseFile помещается в раздел finally блока try-finally структурированной обработки исключений. Рассмотрим, как выполняется добавление, изменение, удаление записей на примере коллекции поставщиков. Корректировка списка поставщиков производится с помощью формы TSuppliersForm, определенной в модуле SuppliersUnit. При этом используется глобальная переменная SupplierObject типа TSupplierObject, объявленная в модуле DataTypes. Когда пользователь хочет добавить в список нового поставщика и нажимает соответствующую кнопку, вызывается функция NewItem коллекции Suppliers, создающая новый экземпляр класса TSupplierObject, который затем присваивается глобальной переменной SupplierObject. Для ввода данных о новом поставщике на экране отображается форма TSupplierForm из модуля SupplierUnit. Эта форма позволяет пользователю скорректировать значения свойств экземпляра класса TSupplierObject, назначенного переменной SupplierObject. Когда пользователь заполнит все необходимые поля и нажмет кнопку для сохранения изменений, выполняется предварительная проверка введенных данных. Например, проверяется, что наименование поставщика не является пустой строкой и что это наименование не дублируется в списке. Позже, при сохранении данных, это привело бы к ошибке, вызванной нарушением уникальности индекса, и откату всех изменений. Такие ошибки удобнее выявлять на этапе предварительной проверки, чтобы позволить пользователю исправить некорректно введенные данные. Если предварительная проверка прошла успешно, форма ввода данных закрывается, для коллекции Suppliers вызывается метод EndEdit, сохраняющий информацию о добавлении записи в кэше изменений. Затем вызывается рассмотренная выше глобальная функция SaveSuppliers из модуля DataTypes для применения кэшированных изменений к набору данных и сохранения коллекции на диске.
Если пользователь отказался от сохранения изменений закрытием по Escape формы ввода данных, вызывается метод CancelEdit коллекции Suppliers для уничтожения объекта, присвоенного переменной SupplierObject. Корректировка записи о поставщике отличается от добавления нового поставщика только тем, что в начале вместо NewItem вызывается функция BeginEdit коллекции поставщиков Suppliers, в которую передается идентификатор редактируемого элемента коллекции. Когда пользователь удаляет поставщика из коллекции Suppliers, на экране сначала появляется окно для подтверждения удаления записи. Затем открывается файл данных для коллекции товаров Products вызовом метода OpenFile. Используя индекс по внешнему ключу SupplierID коллекции Products, проверяется наличие в списке товаров элемента, ссылающегося на удаляемого поставщика. Если такой товар присутствует в списке, поставщик не может быть удален. Если на этого поставщика никто не ссылается, для коллекции Suppliers вызывается метод Delete, в который передается идентификатор удаляемого элемента. Затем вызывается функция SaveSuppliers для фактического удаления записи из набора данных и сохранения измененной коллекции на диске. Файл данных коллекции Products закрывается только после сохранения всех изменений в коллекции Suppliers. Это нужно, чтобы другой пользователь не мог в момент удаления поставщика добавить товар, который на него ссылается. С этой же целью в момент сохранения коллекции товаров Products вызовом глобальной функции SaveProducts для каждого добавленного или измененного товара проверяется наличие в коллекции Suppliers поставщика, идентификатор которого назначен свойству SupplierID данного товара.
и классов, составляющих библиотеку AcedUtils,
При разработке функций и классов, составляющих библиотеку AcedUtils, упор делался на оптимизацию по быстродействию. Но разработчик прикладной программы должен понимать, что секрет достижения максимальной производительности заключается не столько в оптимизации машинного кода, ассемблерных вставках и т.п., сколько в использовании правильных алгоритмов. Например, если стоит задача поиска некоторого значения в массиве элементов, можно пойти несколькими путями. Самый простой путь – последовательный перебор элементов и сравнение их с искомым значением. Для массива, содержащего всего несколько элементов, этот метод может оказаться самым эффективным. Если массив содержит много элементов, лучше заранее отсортировать его, а затем использовать бинарный поиск, который несравнимо быстрее последовательного перебора элементов. Если число элементов массива измеряется десятками тысяч и поиск в нем выполняется часто, имеет смысл пожертвовать лишней памятью для организации хешированного списка. Время поиска в хеше не зависит от размера массива. Но и хешированный список имеет свои недостатки, как, например, невозможность последовательной индексации элементов. Кратко рассмотрим назначение модулей в составе AcedUtils. AcedMemory – реализует быстродействующий менеджер памяти, оптимизированный для работы с большим числом мелких фрагментов данных. Особенностью является ведение пула блоков памяти одного размера. Таким образом решается проблема дефрагментации пространства памяти, занятого блоками размером до 64кБ. Кроме того, данный менеджер памяти отличается высоким быстродействием и оптимальным, с точки зрения кэширования памяти, выравниванием распределяемых блоков. AcedConsts – содержит константы и методы, используемые другими модулями в составе AcedUtils. Кроме того, содержит определение типов ссылок на массивы, таких как PIntegerItemList, PObjectItemList, PPointerItemList и других. AcedBinary – объединяет функции для работы с блоками памяти, массивами байт, чисел типа Integer, Word, LongWord и битовыми строками.
Включает функции для эффективной работы с упорядоченными массивами одинарных и двойных слов, выполнения логических операций над множествами, представленными в виде таких массивов. AcedStreams – содержит классы TBinaryReader и TBinaryWriter, позволяющие считывать данные из бинарного потока и помещать данные в бинарный поток. Исходные и выходные данные бинарного потока могут представляться в виде массива байт, строки в кодировке Base64, файла на диске или потока типа TStream. Данные, помещенные в бинарный поток, защищаются контрольной суммой Адлера и, при необходимости, сжимаются методом LZ+Huffman. Кроме того, данные в потоке могут быть зашифрованы методом RC6 и защищены цифровой сигнатурой SHA-256. В модуле AcedStreams также находятся классы TReaderStream и TWriterStream, которые позволяют работать с экземплярами классов TBinaryReader и TBinaryWriter, как будто эти классы являются потомками стандартного класса TStream. AcedStrings – содержит разнообразные функции для работы со строками, в частности, для сравнения строк с учетом и без учета регистра символов, перевода символов и строк в верхний/нижний регистр, перекодировки строк из DOS в Windows и обратно, поиска, замены, удаления подстрок и т.д. Кроме того, в AcedStrings находится класс TStringBuilder, предназначенный для построения длинных строк из отдельных фрагментов, а также изменения таких строк "на месте". При использовании этого класса память не распределяется заново каждый раз при добавлении к строке следующего фрагмента. Построение длинной строки с помощью экземпляра класса TStringBuilder выполняется значительно быстрее простой конкатенации строк в цикле. AcedCommon – объединяет функции, не выделенные в отдельные категории, в частности, функции форматирования даты и времени, преобразования числа в строку и наоборот, функции для записи денежной суммы прописью, перевода строки или байтового массива в кодировку Base64 и восстановления из кодировки Base64, а также для расчета контрольной суммы Адлера и CRC32, для работы со счетчиками и таймерами при измерении временных интервалов.
Кроме того, здесь находятся некоторые вспомогательные функции, в том числе относящиеся к пользовательскому интерфейсу. AcedCompression – содержит функции, реализующие сжатие бинарных данных методом LZ+Huffman, и последующую распаковку данных, сжатых таким способом. Используемый алгоритм упаковки представляет собой немного измененный вариант алгоритма, который применяется в архиваторе PKZIP. За счет этих изменений достигается большая степень сжатия в режимах, отличных от скоростного (dcmFastest). AcedCrypto – объединяет функции для шифрования данных методами RC4 и RC6 (в режимах ECB, CFB и OFB), для расчета значения односторонней хеш-функции SHA-256, используемой при верификации данных. Цифровая сигнатура SHA-256 может вычисляться как для целой строки или массива байт, так и для данных, представленных в виде нескольких фрагментов. В этом же модуле находятся функции, реализующие генератор псевдослучайных чисел Mersenne Twister с периодом 219937-1. AcedLists – содержит классы для работы со списками: TBitList – битовая строка, т.е. упакованный набор элементов типа Boolean; классы TIntegerList, TWordList представляют собой упорядоченные наборы значений типа Integer и Word, соответственно; классы TIntegerAssociatedList, TStringAssociatedList – это сортированные списки ключей типа Integer и String, соответственно, с каждым из которых связан объект типа TObject; классы TIntegerHashTable, TStringHashTable аналогичны двум предыдущим, но ключи в них хранятся в виде хешированного списка; TLinkedList – связанный список, в узлах которого помещаются значения типа TObject; классы TArrayList, TArrayReadOnlyList – это коллекции указателей на объекты или записи с возможностью сортировки, поиска и группирования элементов. AcedStorage – объединяет классы для организации объектного хранилища данных, которое представляет собой эффективный способ хранения данных на локальном компьютере или файловом сервере с возможностью многопользовательского доступа. При реализации объектного хранилища используется хеширование записей по первичным ключам, индексация с возможностью выделения диапазонов значений, уведомления об изменении данных, поддерживаются ограничения уникальности.
Для обеспечения возможности одновременного изменения данных одного набора, т.е. коллекции, несколькими пользователями применяется механизм транзакций. Каждая таблица данных представляется коллекцией типа TSerializableCollection, в которой содержатся записи – объекты, производные от класса TSerializableObject. AcedNetWait – форма, которая используется модулем AcedStorage при возникновении коллизий на файловом сервере. На экран выводится сообщение о том, что в данный момент открываемый файл данных заблокирован, т.к. он записывается другим пользователем или другим приложением. В этом случае пользователь должен подождать, повторить попытку открытия файла или отменить операцию. Следует отметить, что файл данных блокируется только на время автоматического согласования изменений и непосредственного сохранения данных. Блокировка не выполняется в процессе интерактивной корректировки данных одной и той же таблицы несколькими пользователями. AcedExcelReport – содержит функции и классы, облегчающие подготовку и печать отчетов с помощью Microsoft Excel. Обычно при работе с Microsoft Excel из Delphi приходится обращаться к различным интерфейсам из модуля Excel97, сгенерированного на основе библиотеки типов Microsoft Excel. В модуле AcedExcelReport собраны функции, наиболее часто используемые при подготовке отчетов. Это позволяет сократить размер и уменьшить сложность кода для генерации отчетов, а также сократить число обращений к Microsoft Excel через COM-интерфейсы, что существенно повышает производительность. AcedGridFrame, AcedCheckFrame, AcedViewFrame – фреймы для отображения на экране данных в виде таблицы с возможностью навигации. Представляют собой аналог компонента TStringGrid с расширенными возможностями. AcedGridFrame отображает таблицу и строку поиска. Для столбцов задается режим выравнивания и заголовок. Каждая ячейка может отображаться своим цветом. Поддерживается многострочный текст в ячейках и режим отложенного выделения записей. AcedCheckFrame, кроме того, показывает окно пометки (Checkbox) рядом с каждой записью и предоставляет коллекцию помеченных записей и коллекцию недоступных записей, для которых нельзя изменить пометку.AcedViewFrame отображает информацию в виде таблицы без строки поиска и выделения цветом текущей записи. Этот фрейм игнорирует события от клавиатуры, но предоставляет методы для скроллинга таблицы из кода. Дополнительную информацию о grid-фреймах можно почерпнуть из комментариев в исходном коде соответствующих модулей. AcedGrids – содержит описание общих типов и событий, используемых фреймами: AcedGridFrame, AcedCheckFrame и AcedViewFrame. Теперь подробнее рассмотрим функциональность основных модулей AcedUtils.
Преобразование числа в строку и строки в число
В AcedCommon есть несколько функций для преобразования целого числа в строку, содержащую его десятичное или шестнадцатеричное представление (функции G_IntToStr, G_UIntToHex и др.). G_HexToUInt возвращает число типа LongWord, шестнадцатеричная запись которого передана параметром типа String. Функции G_Between… используются для проверки того, что число, записанное в виде строки, лежит в нужном диапазоне. Функции G_StrTo… выполняют преобразование строки в число, типа Integer, LongWord, Int64, Extended или Currency. В случае ошибки эти функции не генерируют исключение, а возвращают значение False. Еще есть функция G_ModDiv10, которая делит значение var-параметра на 10 и возвращает остаток от деления как результат функции. Она может быть полезна при преобразовании в строку длинных чисел.
Функции G_NumToStr и G_NumToRub используются для записи числовых и денежных сумм прописью по-русски. Различные особенности записи чисел прописью настраиваются соответствующими параметрами форматирования. Например, вызов G_NumToStr(54321, S, nfFemale) вернет в переменной S строку: "пятьдесят четыре тысячи триста двадцать одна", а вызов G_NumToRub(3.2, ruFull, ruNumShort) вернет строку: "Три рубля 20 коп.".
Прочие функции
Функция G_SwitchToApplication переводит на передний план запущенный экземпляр приложения, к которому принадлежит окно с указанным именем класса. Обычно эта функция применяется для предотвращения повторного запуска приложения, чтобы вместо запуска нового экземпляра вывести на передний план запущенный ранее экземпляр приложения. Кроме того, ее можно использовать, например, для переключения на другую задачу при организации приложения в виде нескольких независимых процессов. Функция G_SelectMenu используется для имитации выбора пользователем заданного пункта или подпункта меню текущего окна. При этом в функцию окна посылаются события, имитирующие нажатие пользователем клавиши вызова меню, клавиш со стрелками вправо и вниз, клавиши Enter, в последовательности, необходимой для выбора нужного пункта меню. При этом глубина раскрываемого подпункта может быть любой. Функции G_ToggleKey и G_IsToggled управляют состоянием клавиш: Num Lock, Caps Lock, Scroll Lock. Первая функция позволяет изменить состояние любой из этих клавиш, вторая – считывает текущее состояние соответствующей клавиши. Последняя функция G_ODS помещает сообщение в Event Log (журнал, вызываемый из отладчика в среде Borland Delphi по Ctrl+Alt+V).
Работа с кодировкой Base64 и шестнадцатеричными кодами
Функция G_StrToCodes возвращает строку, в которой каждый символ исходной строки, переданной параметром, представлен его шестнадцатеричным кодом. Например, вызов G_StrToCodes('ABC') вернет строку: "414243". Функция G_CodesToStr используется для обратного преобразования: G_CodesToStr('414242') вернет строку "ABC". Здесь же находятся функции для работы с кодировкой Base64, позволяющей представить строку символов или массив байт в виде строки, не содержащей пробелов, управляющих символов и символов в национальной кодировке. Функция G_Base64Encode выполняет кодирование строки символов или массива байт в Base64. Результат возвращается в виде строки типа AnsiString. Обратное преобразование выполняется функцией G_Base64Decode, которая восстанавливает исходный массив байт или строку символов из строки в кодировке Base64.
Список указателей TArrayList
TArrayList является аналогом класса ArrayList из .NET Framework. Он предназначен для хранения набора указателей. В конструктор класса передается предполагаемое число элементов, которое будет добавлено в список. Элементы можно загрузить методом Load из другого экземпляра класса TArrayList или из указанной области памяти. Отдельные элементы добавляются методом Add в конец списка или методом Insert в произвольное место списка. Для удаления элемента по индексу используется метод RemoveAt. Если элемент содержит ссылку на объект, производный от TObject, вызов метода RemoveAndFreeItemAt не только удалит элемент из списка, но и освободит занимаемую им память вызовом TObject.Free. Методы UnorderedRemoveAt и UnorderedRemoveAndFreeItemAt, аналогичны двум предыдущим. Они позволяют быстро удалить элемент из списка, заменив его последним элементом списка и уменьшив на 1 значение свойства Count. Эти методы могут применяться, когда порядок элементов не имеет значения. Для полной очистки списка предназначен метод Clear; для очистки списка с вызовом метода Free для каждого элемента используется метод ClearAndFreeItems. Аналогично классам TIntegerList, TWordList, в классе TArrayList имеются методы для работы со списком как со стеком. Метод Pop возвращает последний элемент списка и одновременно удаляет его, уменьшая на 1 значение свойства Count. Метод Peek возвращает последний элемент списка, не удаляя его. Поместить элемент в стек можно с помощью метода Add.
Обращение к отдельным элементам списка осуществляется через свойство ItemList, которое возвращает ссылку на массив указателей типа Pointer. Длина списка возвращается свойством Count. Общее число элементов, под которое распределена память во внутреннем массиве, определяется свойством Capacity. Свойство Count может произвольным образом изменяться. Никаких проверок при этом не выполняется. Capacity обычно превышает Count, что позволяет добавлять новые элементы в список без немедленного выделения нового блока памяти. Чтобы предельно уменьшить объем памяти, занимаемый списком, можно вызвать метод TrimToSize, который распределяет под внутренний массив область памяти, достаточную для хранения Count элементов, но не более того.
После вызова этого метода свойство Capacity становится равно свойству Count. Метод Clone возвращает копию экземпляра класса TArrayList. Метод Equals поэлементно сравнивает текущий список с заданным списком и возвращает True, если все элементы, т.е. указатели, обоих списков равны, или False, если между списками есть какие-либо различия. Чтобы найти элемент в списке TArrayList можно воспользоваться функцией IndexOf, которая сканирует внутренний массив в поисках нужного указателя. Если надо найти не просто указатель, а элемент с определенным значением признака, придется перебрать в цикле все элементы массива ItemList. Для более эффективного поиска, а также просто для упорядочивания элементов списка, можно отсортировать список методом Sort. Данный метод принимает в качестве параметра адрес функции, сравнивающей два элемента списка. Элементы сортируются по возрастанию. Когда список отсортирован, для нахождения элемента методом бинарного поиска можно воспользоваться функцией Search, которая принимает два параметра: указатель на искомое значение признака и функцию для сопоставления элемента списка с этим значением. Имеется также модификация этого метода, которая принимает два указателя на искомые значения, например, для поиска улицы в определенном городе, если эти признаки находятся в разных полях записи, описывающей адрес. В классе TArrayList предусмотрена возможность группирования элементов. При этом элементы с одинаковым значением признака объединяются в одну группу. Группирование выполняется методом EnumerateGroups, который принимает в качестве параметра адрес функции, сравнивающей два элемента списка. При выполнении этой операции список сортируется в порядке возрастания значения признака. Функция EnumerateGroups возвращает коллекцию групп – экземпляр класса TGroupEnumerator. Число групп в этой коллекции определяется свойством GroupCount. Указатель на массив, содержащий группы, возвращается свойством GroupList класса TGroupEnumerator. Каждая группа представляется списком TArrayReadOnlyList – аналог класса TArrayList, который не позволяет добавлять и удалять элементы.Свойство ItemList этого класса возвращает указатель на массив, содержащий элементы группы. Количество элементов можно определить с помощью свойства Count. В классе TArrayReadOnlyList предусмотрены методы для линейного поиска указателя (IndexOf), для сортировки элементов группы (Sort), для бинарного поиска (Search), для выделения подгрупп (EnumerateGroups). Следует обратить внимание на то, что в экземпляре класса TArrayReadOnlyList нет собственного внутреннего массива для хранения элементов списка. Вместо этого он пользуется внутренним массивом экземпляра класса TArrayList, для которого был вызван метод EnumerateGroups. Поэтому в процессе работы с группами нельзя вносить изменения в основной экземпляр TArrayList.
Связанный список TLinkedList
Список TLinkedList представляет собой упорядоченный набор элементов, каждый из которых содержит ссылку на предыдущий и последующий элементы. Связанный список удобен для последовательного перебора элементов вперед или назад, а также для добавления и удаления элементов в произвольном месте списка. Этот класс может использоваться, например, для организации очередей, работающих по принципу: "первым пришел – первым вышел", с возможностью добавления элементов в конец очереди и выборки их с начала очереди. Особенностью списка на основе класса TLinkedList является отсутствие возможности индексированного доступа к элементам. При необходимости обращения к элементам списка по индексу лучше воспользоваться другими классами, такими как TArrayList.
Свойство HeadNode экземпляра класса TLinkedList возвращает указатель на первый элемент, т.е. узел, списка. В противоположность этому свойству, TailNode возвращает указатель на последний элемент списка. С помощью этих свойств, а также полей NextNode и PrevNode узла списка TLinkedListNode, представляющих, соответственно, указатель на следующий и предыдущий элементы списка, можно перебрать все элементы списка. Значение типа TObject узла списка сохраняется в поле Value записи TLinkedListNode. Если свойство OwnValues данного экземпляра TLinkedList равно True, при очистке списка методом Clear или удалении отдельных элементов методами RemoveHead, RemoveTail или Remove для значения Value каждого удаляемого узла списка вызывается метод Free. По умолчанию свойство OwnValues равно False, и метод Free для значений не вызывается. Добавить значение в начало или в конец связанного списка можно, соответственно, функциями AddHead и AddTail, которые возвращают ссылку на добавленный узел. Чтобы вставить значение в произвольное место списка, необходимо воспользоваться функциями InsertBefore или InsertAfter, предназначенными для добавления элемента перед или после определенного узла списка. Функция IsEmpty возвращает True, если список пустой. Функции PopHeadValue и PopTailValue позволяют извлечь значение, соответственно, из первого и последнего узлов, а сами узлы удалить из связанного списка.
Следующая группа функций предназначена для
Следующая группа функций предназначена для вычисления контрольных сумм, которые используются при проверке целостности данных. Контрольная сумма представляет собой число типа LongWord, которое зависит от всего исходного массива данных. При изменении хотя бы одного бита данных, значение контрольной суммы меняется. Это значение может, например, передаваться вместе с данными по сети, а затем пересчитываться на приемном конце и сравниваться с исходным значением контрольной суммы. Если оба значения равны, то с большой вероятностью можно утверждать, что данные переданы без ошибок. Здесь имеются в виду случайные ошибки при передаче, а не предумышленное искажение данных. Для надежной защиты данных от любого вида вмешательства вместо контрольной суммы нужно использовать цифровую сигнатуру, такую как SHA-256 (см. описание модуля AcedCrypto). Правда, значение односторонней функции SHA-256 рассчитывается намного медленнее, чем контрольная сумма типа CRC32. Функция G_Adler32 вычисляет контрольную сумму Адлера массива байт в соответствии с RFC 1950. Эта контрольная сумма используется библиотекой zlib. Она получается быстрее, чем CRC32, но, по всей видимости, является чуть менее надежной. Функции G_CRC32 и G_NextCRC32 возвращают значение контрольной суммы CRC32 указанной области памяти. Вторая функция отличается от первой тем, что она используется для поэтапного расчета контрольной суммы массива, представленного в виде нескольких фрагментов. Например, если длинный массив передается по сети в виде нескольких пакетов, функция G_NextCRC32, вызываемая последовательно для каждого пакета данных, вычислит в результате контрольную сумму целого массива. Пара функций G_CRC32_Str и G_CRC32_Text может использоваться при хешировании строк. Первая функция возвращает значение типа LongWord, которое зависит от всех символов строки с учетом их регистра. Вторая функция возвращает значение, которое зависит от всех символов строки, но не зависит от их регистра, т.е. это значение будет одинаковым для строк, которые отличаются только регистром символов.
В документе описана библиотека функций
В документе описана библиотека функций и классов AcedUtils, призванная помочь разработчику на Borland Delphi в создании быстрых и экономичных с точки зрения использования ресурсов памяти и дискового пространства приложений, работающих на платформе Win32. Начало
Класс AcedBinary
В AcedBinary собраны функции для работы с бинарными данными, которые используются другими классами в составе AcedUtils.NET. Однако, они могут вызываться и из прикладной программы. Например, функция SwapBytes() обращает порядок следования байт в значении типа System.UInt32, функция ReverseBits() выполняет аналогичное действие с битами в составе двойного слова. Точнее, размер исходного значения может варьироваться от 1 до 32 бит. Функция Adler32() вычисляет значение контрольной суммы Адлера в соответствии с RFC 1950 для массива байт или его фрагмента. Данный алгоритм расчета контрольной суммы отличается от CRC32 большей производительностью. В этом классе есть еще несколько unsafe-методов, предназначенных для копирования массива байт, быстрого заполнения массива чисел типа System.Int32 и копирования одного такого массива в другой.
с RFC 2144. Это незапатентованный
В AcedCast5 реализован алгоритм CAST-128 (CAST5) в соответствии с RFC 2144. Это незапатентованный алгоритм шифрования с ключом размером 128 бит, отличающийся высоким быстродействием и стойкостью к различным видам криптоанализа. При применении шифра к данным используется режим обратной загрузки шифротекста (CFB) с размером блока входных данных 64 бита. Класс AcedCast5 используется при шифровании и дешифровании бинарного потока данных, представленного классами Aced(…)Writer/Aced(…)Reader. Кроме того, он может применяться самостоятельно для шифрования произвольных данных. Два основных метода класса AcedCast5, методы Encrypt() и Decrypt(), предназначены, соответственно, для шифрования и дешифрования массива байт или его фрагмента с ключом, который задается параметром keyBytes в виде 16-байтного массива. Если в программе ключ представляется значением типа System.Guid, то соответствующий ему массив байт можно получить вызовом функции Guid.ToByteArray(). Одновременно с шифрованием в классе AcedCast5 вычисляется значение односторонней хеш-функции RipeMD-160 для шифруемых данных. Функция Encrypt() возвращает массив из 5 значений типа System.Int32, представляющих собой цифровую сигнатуру фрагмента данных, рассчитанную до того, как данные были зашифрованы. Функция Decrypt() возвращает аналогичный массив, представляющий цифровую сигнатуру фрагмента данных, рассчитанную после того, как данные были расшифрованы. Если при шифровании и дешифровании использован один и тот же ключ и данные в массиве не были повреждены, функции Encrypt() и Decrypt() должны вернуть одно и тоже значение хеш-функции RipeMD-160. Имеются также unsafe-варианты этих функций, в которые передается указатель на массив шифруемых байт. Кроме того, функции Encrypt() и Decrypt() могут принимать параметр iv, задающий начальный вектор для шифрования или дешифрования данных. В классе AcedCast5 есть функций для шифрования данных, представленных несколькими фрагментами, т.е. поточного шифрования. В частности, функция ScheduleKey() на основе ключа шифра keyBytes создает или заполняет специальный массив key, содержащий ключевую информацию, который передается затем в качестве ключа в остальные функции, относящиеся к данному разделу.
Таким образом, ключевой массив создается только однажды, а не перед каждым шифрованием следующего фрагмента данных. Функция GetOrdinaryIV() возвращает значение, которое может использоваться в качестве начального вектора. Это значение получается шифрованием нулевого вектора с помощью текущего ключа шифра. Функции Encrypt() и Decrypt(), которые принимают параметр key, используются непосредственного для шифрования и дешифрования данных в поточном режиме. Каждая из этих функций, кроме ключа и ссылки на шифруемые или дешифруемые данные, принимает параметр iv, в котором передается значение начального вектора. Новое значение вектора, которое может использоваться при следующем вызове функций Encrypt()/Decrypt(), возвращается как результат функции. Когда все данные зашифрованы или расшифрованы, нужно вызвать метод ClearKey() для очистки массива key, содержащего ключевую информацию. А можно вместо или после этого передать массив key в метод ScheduleKey() для заполнения его информацией о новом ключе для шифрования других данных с другим ключом без пересоздания массива key.
Класс AcedMemoryReader
Предназначен для чтения данных из массива байт, созданного экземпляром класса AcedMemoryWriter. В конструктор класса AcedMemoryReader передается ссылка на массив байт с указанием фрагмента, содержащего данные бинарного потока. Если данные зашифрованы, в последнем параметре конструктора необходимо передать значение типа System.Guid, соответствующее ключу шифра, который использовался при вызове метода ToArray() класса AcedMemoryWriter. Отдельные значения могут быть прочитаны из потока методами, названия которых состоят из префикса "Read" и наименования типа читаемого значения. Фрагменты массивов, состоящих из элементов стандартных value-типов, считываются методом Read(). Для возвращения текущей позиции на начало потока, чтобы заново прочитать данные, используется метод Reset(). Чтобы пропустить некоторое количество байт во входном потоке вызывается метод Skip(). При попытке чтения данных за пределами потока возникает исключение типа AcedReadBeyondTheEndException.
Свойство Position класса AcedMemoryReader возвращает индекс следующего считываемого байта во внутреннем массиве, ссылка на который возвращается функцией GetBuffer(). Размер внутреннего массива определяется свойством Size. Смещение во внутреннем массиве, с которого начинаются данные потока – свойством Offset. Если исходный массив байт, переданный в конструктор класса, является упакованным, в памяти создается новый массив для распакованных данных. Тогда функция GetBuffer() возвращает ссылку на этот временный массив, а свойство Offset всегда равно нулю. Если же исходный массив не является упакованным, функция GetBuffer() возвращает ссылку на массив, переданный параметром bytes в конструктор класса AcedMemoryReader. Если данные потока зашифрованы, массив байт, передаваемый в конструктор этого класса, расшифровывается на месте. Это означает, что один и тот же зашифрованный массив байт нельзя использовать для инициализации нескольких экземпляров класса AcedMemoryReader.
Если при создании экземпляра класса AcedMemoryReader в конструктор передан массив недостаточной длины или рассчитанная для него контрольная сумма Адлера не совпадает с сохраненным в потоке значением контрольной суммы, возникает исключение AcedDataCorruptedException.
Если после дешифрования данных оказывается, что рассчитанное значение цифровой сигнатуры RipeMD-160 для данных потока не совпадает со значением сигнатуры, сохраненным в начале массива данных, возникает исключение AcedWrongDecryptionKeyException, которое является потомком от класса AcedDataCorruptedException. Пример использования класса AcedMemoryReader: private void GetData(byte[] dataBytes, out byte[] bytes, out short n, out string s, out int[] otherValues) { AcedMemoryReader r = new AcedMemoryReader(dataBytes,
0, dataBytes.Length); /* AcedMemoryReader r = new AcedMemoryReader(dataBytes,
0, dataBytes.Length, new Guid("CA761232-ED42-11CE-BACD-00AA0057B223")); */
bytes = r.ReadByteArray(); n = r.ReadInt16(); otherValues = new int[120]; r.Read(otherValues, 10, 100); s = r.ReadString(); }
Предполагается, что массив байт, передаваемый параметром dataBytes в функцию GetData(), получен как результат функции PutData(), код которой приведен выше в разделе, описывающем класс AcedMemoryWriter. Используемый здесь конструктор класса AcedMemoryReader предполагает, что данные в бинарном потоке не зашифрованы. Закомментированный фрагмент кода содержит вызов конструктора с передачей в него ключа шифра, соответствующего варианту 3 функции PutData().
Класс AcedMemoryWriter
Позволяет сохранять разнотипные данные в массиве байт, длина которого динамически увеличивается по мере добавления в него данных. Затем эти данные представляются в виде массива типа System.Byte[], который, кроме самих данных, содержит их контрольную сумму и, возможно, значение цифровой сигнатуры. Возвращаемый массив может быть упакован для экономии места и зашифрован для ограничения доступа к информации.
При создании экземпляра класса AcedMemoryWriter можно указать предполагаемое число байт, которое будет помещено в бинарный поток. Таким образом удается избежать лишнего перераспределения памяти под внутренний массив. В AcedMemoryWriter есть методы, названия которых начинаются с "Write", предназначенные для помещения в поток значений следующих типов: Boolean, Byte, Byte[], Char, DateTime, Decimal, Single, Double, Guid, Int16, Int32, Int64, SByte, String, TimeSpan, UInt16, UInt32, UInt64. Кроме того, можно добавлять сразу фрагменты массивов с элементами стандартных value-типов с помощью перегруженных методов Write(). При этом указывается индекс первого сохраняемого элемента массива и число записываемых элементов. Общее число байт, помещенное в бинарный поток, возвращается свойством Length класса AcedWriter. Метод Reset() обнуляет длину потока, позволяя заполнить его новыми данными без пересоздания экземпляра класса AcedMemoryWriter.
Текущая длина внутреннего массива возвращается и устанавливается свойством Capacity. Ссылку на внутренний массив можно получить вызовом функции GetBuffer(). Правда, эта ссылка изменяется при каждом перераспределении памяти, т.е. при каждом изменении свойства Capacity. В некоторых случаях, например, при чтении данных из файла с помощью FileStream.Read(), удобнее передать ссылку на внутренний массив непосредственно в метод FileStream.Read(), вместо того, чтобы считывать данные в промежуточный массив, а затем переписывать их в поток методом Write(). Чтобы сделать это быстрее, нужно сохранить во временной переменной текущую длину потока, т.е.
значение свойства Length, затем вызвать метод Skip(), передавая в него число байт, которое будет прочитано из файла. При этом длина потока увеличится на указанное число байт без фактического заполнения их данными. Теперь можно получить ссылку на внутренний массив из функции GetBuffer(), а затем вызвать метод FileStream.Read(), передавая в него полученную ссылку на массив и значение, сохраненное во временной переменной, в качестве смещения в массиве. Когда все необходимые данные записаны в бинарный поток, вызывается функция ToArray(), возвращающая результирующий массив данных. Имеется несколько вариантов этой функции, которые отличаются набором принимаемых параметров. Наиболее функциональным является вариант, принимающий два параметра: compressionMode типа AcedCompressionMode и keyGuid типа System.Guid. Вызов функции ToArray() с одним параметром эквивалентен передаче значения Guid.Empty в параметре keyGuid. Вызов этой функции без параметров эквивалентен передаче значения NoCompression в параметре compressionMode и значения Guid.Empty в параметре keyGuid. Рассмотрим подробнее, чем управляют эти параметры и как они влияют на сохраняемый формат данных. Параметр типа AcedCompressionMode выбирает режим сжатия данных. Его значение соответствует одной из констант, рассмотренных выше при описании класса AcedDeflator. Если этот параметр равен значению NoCompression, данные бинарного потока не сжимаются. Параметр keyGuid задает ключ шифрования для выходного массива байт. Если этот параметр равен Guid.Empty, шифрование не выполняется. Значение типа System.Guid используется в качестве ключа шифра по нескольким причинам. Во-первых, легко сгенерировать новое уникальное значение ключа вызовом функции Guid.NewGuid(). Во-вторых, значения такого типа имеют общепринятое строковое представление. В-третьих, Guid легко получить из значения односторонней хеш-функции RipeMD-160. Если ключ шифра вводится пользователем с клавиатуры в виде строки символов, необходимо преобразовать эту строку в цифровую сигнатуру вызовом AcedRipeMD.Compute(), а затем в значение типа System.Guid вызовом метода ToGuid() класса AcedRipeMD.
Шифрование данных выполняется методами классом AcedCast5. Но прежде, чем шифровать данные, для них вычисляется значение 20-байтной сигнатуры RipeMD-160, которое помещается в выходной массив вместе с данными и используется при последующем чтении из потока для проверки того, что данные в потоке расшифрованы с правильным ключом и что они не были повреждены. Последовательность действий при вызове метода ToArray() класса AcedMemoryWriter следующая. Сначала выполняется упаковка данных классом AcedDeflator. Затем для полученного массива рассчитывается значение односторонней хеш-функции RipeMD-160 методами класса AcedRipeMD. Это значение помещается в выходной массив перед данными. Потом данные шифруются методами класса AcedCast5. Значение цифровой сигнатуры не шифруется. На заключительном этапе для всего содержимого выходного массива рассчитывается контрольная сумма Адлера вызовом метода AcedBinary.Adler32(), которая размещается в первых 4-х байтах выходного массива. Заполненный таким образом массив возвращается как результат функции ToArray(). В зависимости от параметров, могут опускаться этапы упаковки и/или расчета цифровой сигнатуры и шифрования данных. Пример использования класса AcedMemoryWriter: private byte[] PutData() { AcedMemoryWriter w = new AcedMemoryWriter(); w.WriteByteArray(new byte[] {5, 6, 7, 8, 9}); w.WriteInt16(10000); int[] otherValues = new int[120]; for (int i = 0; i < 120; i += 3) { otherValues[i] = 1; otherValues[i + 1] = 2; otherValues[i + 2] = 3; } w.Write(otherValues, 10, 100); w.WriteString("Hello world!");
////////////////////////////////////////////////////// // Вариант 1: данные возвращаются как есть с // добавлением контрольной суммы Адлера. //////////////////////////////////////////////////////
return w.ToArray();
/* ////////////////////////////////////////////////////// // Вариант 2: данные сжимаются и защищаются // контрольной суммой Адлера. //////////////////////////////////////////////////////
return w.ToArray(AcedCompressionMode.Fast); */
/* ////////////////////////////////////////////////////// // Вариант 3: данные сжимаются, шифруются и защищаются // цифровой сигнатурой RipeMD-160. //////////////////////////////////////////////////////
return w.ToArray(AcedCompressionMode.Fast, new Guid("CA761232-ED42-11CE-BACD-00AA0057B223"));
*/ }
В данном примере функция PutData() помещает в бинарный поток массив байт как целый объект, потом значение типа Int16, затем фрагмент массива элементов типа Int32, а в конце – строку символов. Результатом функции может быть просто массив байт, содержащий данные, записанные в поток, защищенные контрольной суммой Адлера. Размер этого массива составляет 443 байта. Если передать в функцию AcedMemoryWriter.ToArray() параметр compressionMode со значением AcedCompression.Fast, данные бинарного потока будут упакованы и размер полученного массива составит 51 байт. Если, кроме того, передать некоторое непустое значение типа Guid в параметре keyGuid, сжатые данные будут защищены цифровой сигнатурой RipeMD-160 и зашифрованы методом CAST-128. За счет добавления сигнатуры размер выходного массива увеличится при этом на 20 байт и составит 71 байт.
Класс AcedRegistry
AcedRegistry восполняет собой отсутствие в .NET Framework класса, подобного классу TRegistry в Borland Delphi. Его особенностью по сравнению со стандартным классом Microsoft.Win32.Registry является наличие специальных методов для помещения в реестр значений различного типа и для чтения соответствующих значений из реестра. Класс AcedRegistry включает методы для работы с данными, которые представлены значениями следующих типов: String, Byte[], Int32, Boolean, DateTime, Decimal, Double, Guid, Int64.
Работа с классом AcedRegistry начинается с вызова конструктора, который принимает три параметра: первый (registryBaseKey) – выбирает ветвь реестра, такую как HKEY_CURRENT_USER или HKEY_LOCAL_MACHINE; второй параметр (registryKey) указывает наименование ключа реестра, с которым предполагается работать; третий параметр задает режим работы: только чтение или чтение/запись. Если указанный ключ не существует, то при открытии его в режиме "только для чтения" ошибка не возникает. Тогда каждое обращение к функциям Get() для чтения значений ключа вернет False, а при вызове GetDef() будет возвращаться значение по умолчанию. При открытии ключа в режиме, допускающем запись, если он отсутствует, соответствующий ключ немедленно создается в реестре. Обратиться к объекту типа Microsoft.Win32.RegistryKey, представляющему открытый ключ реестра, можно через свойство RegistryKey класса AcedRegistry.
Перегруженный метод Put() предназначен для записи в реестр значений различного типа. Функция Get() считывает значение с указанным именем и сохраняет его в переменной, передаваемой как ref-параметр. Если запрашиваемое значение присутствует в реестре, функция возвращает True. Функция GetDef() отличается от Get() тем, что она возвращает прочитанное значение как результат функции. Если соответствующее значение отсутствует в реестре, GetDef() возвращает значение по умолчанию, которое задается вторым параметром при вызове этой функции.
В конце работы с экземпляром класса AcedRegistry для него обязательно надо вызвать метод Dispose().
При использовании языка C# удобно поместить создание класса AcedRegistry в блок using для гарантированного освобождения unmanaged-ресурсов. Пример использования класса AcedRegistry: private const string
DemoRegistryKey = "Software\\AcedUtils.NET\\Demo", cfgStreamFileName = "StreamFileName", cfgCompressionMode = "CompressionMode";
private static string _streamFileName = String.Empty; private static AcedCompressionMode _compressionMode;
private static void LoadConfig() { using (AcedRegistry config = new AcedRegistry
(AcedBaseKey.CurrentUser, DemoRegistryKey, false)) { config.Get(cfgStreamFileName, ref
_streamFileName); _compressionMode = (AcedCompressionMode)
config.GetDef(cfgCompressionMode, 0); } }
private static void SaveConfig() { using (AcedRegistry config = new AcedRegistry
(AcedBaseKey.CurrentUser, DemoRegistryKey, true)) { config.Put(cfgStreamFileName, _streamFileName); config.Put(cfgCompressionMode, (int)
_compressionMode); }} }Данный пример взят из демонстрационного проекта, прилагаемого к статье. Значения статических полей _streamFileName и _compressionMode сохраняются в реестре методом SaveConfig() и считываются из реестра методом LoadConfig(). Тип AcedCompressionMode представляет собой перечисление, которое нужно привести к типу System.Int32, чтобы поместить его в реестр. После чтения из реестра с помощью GetDef() значение должно быть преобразовано обратно к типу AcedCompressionMode.
Класс AcedRipeMD
Смысл односторонней хеш-функции заключается в том, что практически невозможно подобрать другой набор байт, для которого значение цифровой сигнатуры совпадало бы с исходным значением. Кроме того, невозможно восстановить состояние исходных данных по известному значению цифровой сигнатуры. Класс AcedRipeMD реализует алгоритм расчета односторонней хеш-функции RipeMD-160 в полном соответствии с документом: "RIPEMD-160: A Strengthened Version of RIPEMD" (Hans Dobbertin, Antoon Bosselaers, Bart Preneel), April 18, 1996. Длина получаемой сигнатуры составляет 20 байт (160 бит). Цифровую сигнатуру удобно представить в виде массива из 5 элементов типа System.Int32.
Чтобы получить значение односторонней хеш-функции для массива байт или строки символов, можно воспользоваться функцией Compute() класса AcedRipeMD. При передаче в нее массива байт указывается смещение и длина обрабатываемого фрагмента массива. Имеется также unsafe-вариант этой функции, принимающий в качестве параметра указатель на массив байт. Иногда, например, при работе с потоком данных, требуется рассчитать цифровую сигнатуру для массива байт, представленного в виде нескольких фрагментов. В этом случае можно применить функции для поточного расчета сигнатуры RipeMD-160. Для этого сначала вызывается функция Initialize, которая возвращает или заполняет служебный массив hashData. Затем нужно последовательно вызвать метод Update для каждого фрагмента данных. В этот метод передается массив hashData, а также ссылка на первый или следующий фрагмент данных в виде массива байт или строки символов, для которого вычисляется значение сигнатуры. После того, как функция Update была вызвана для каждого фрагмента, можно получить само значение цифровой сигнатуры вызовом метода Finalize().
Алгоритм шифрования CAST-128, используемый при работе с бинарным потоком классами Aced(…)Writer/Aced(…)Reader, предполагает, что длина ключа шифра составляет 128 бит. Цифровая сигнатура RipeMD-160 как нельзя лучше подходит для использования ее в качестве ключа при шифровании данных. Однако, она представляется числом размером 160 бит, а не 128. Для решения этой проблемы в класс AcedRipeMD добавлена функция ToGuid(). Она принимает значение 20-байтной цифровой сигнатуры и возвращает соответствующее ему значение типа System.Guid, размер которого составляет 128 бит.
В классе AcedRipeMD есть еще несколько вспомогательных методов, облегчающих работу с цифровой сигнатурой, представленной в виде массива из 5 значений типа System.Int32. Например, функция Copy() позволяет быстро скопировать значение хеш-функции в массив байт или, наоборот, считать его из массива байт. Функция Equals() используется для проверки равенства двух значений цифровой сигнатуры, одно из которых может быть представлено массивом байт. Функция Clear() обнуляет 5 элементов массива типа System.Int32[], предназначенного для хранения сигнатуры RipeMD-160.
Классы AcedDeflator и AcedInflator
Эти классы предназначены для сжатия и распаковки данных, представленных массивом байт или его фрагментом. Применяемый алгоритм сжатия аналогичен описанному в RFC 1951 и реализованному в библиотеке zlib, но имеет ряд отличий, в частности, использует другой формат блока данных. Формат описан в исходном коде библиотеки в начале файла Compressor.cs.
Упаковка данных производится методом Compress() класса AcedDeflator, распаковка – методом Decompress() класса AcedInflator. Особенность работы с этими классами заключается в том, что их экземпляры не следует создавать напрямую вызовом конструктора. Лучше вместо этого использовать ссылку на единственный экземпляр каждого класса, которую можно получить, обратившись к статическому свойству Instance. Это ограничение связано с тем, что при создании экземпляров этих классов, особенно класса AcedDeflator, выделяется значительный объем памяти под внутренние массивы. Обычно не требуется использовать параллельно несколько экземпляров архиватора. Кроме того, частое перераспределение памяти ведет к снижению производительности. При первом обращении к свойству Instance создается один экземпляр соответствующего класса. Ссылка на него сохраняется в статическом поле класса и возвращается при каждом следующем обращении к свойству Instance. Когда возникает необходимость освобождения памяти, занятой внутренними массивами архиватора, можно вызвать статический метод Release для обнуления внутренней ссылки на экземпляр соответствующего класса. Тогда, если нет других ссылок на этот экземпляр, при следующей "сборке мусора" память будет возвращена операционной системе.
Для сжатия данных функцией AcedDeflator.Compress() в нее передается ссылка на массив байт с указанием смещения и длины сжимаемого фрагмента данных. Есть два варианта этой функции. В первом случае память под массив для сохранения упакованных данных распределяется самой функцией Compress(). Параметры beforeGap и afterGap этой функции задают отступ, соответственно, в начале и в конце выходного массива на случай, если кроме упакованных данных в него должна быть помещена еще какая-то информация.
Во втором случае в функцию Compress() передается ссылка на уже существующий массив, в который должны быть записаны упакованные данные, а также смещение в этом массиве, с которого начинается запись. Максимальный размер упакованного фрагмента в случае, если данные несжимаемы, равен длине исходного фрагмента плюс 4 байта. Таким образом, длина приемного массива должна быть достаточна для хранения исходного фрагмента данных плюс 4 байта и плюс смещение в этом массиве. Функция Compress() возвращает размер сжатого фрагмента, т.е. число байт, сохраненное в выходном массиве. Параметр типа AcedCompressionMode, передаваемый в функции Compress(), выбирает режим сжатия данных. Он принимает одно из следующих значений: NoCompression – данные не сжимаются, а просто копируются в входной массив с добавлением 4-байтной длины фрагмента для последующей его распаковки; Fastest – самый быстрый режим сжатия, который, тем не менее, может быть эффективен для некоторых типов данных; Fast – используется режим быстрого сжатия, когда максимальное расстояние между повторяющимися последовательностями во входном потоке принимается равным 65535 байтам; Normal – обычное сжатие, когда максимальное расстояние между последовательностями составляет 131071 байт; MaximumCompression – максимальное сжатие, доступное данному архиватору, предполагающее, что максимальное расстояние между повторяющимися последовательностями составляет 262143 байта. Сжатые данные распаковываются методом AcedInflator.Decompress(). Прежде чем вызывать этот метод необходимо подготовить область памяти, достаточную для хранения результата. Узнать первоначальный размер сжатых данных можно вызовом статической функции GetDecompressedLength() класса AcedInflator. В нее передается ссылка на массив байт и смещение в этом массиве, с которого начинаются упакованные данные. Функция возвращает длину фрагмента данных после его распаковки. Затем можно создать массив байт достаточного размера и передать его в функцию Decompress() для заполнения распакованными данными.Эта функция принимает ссылку на исходный массив, содержащий сжатые данные, смещение в этом массиве, а также ссылку на приемный массив, в который выполняется распаковка, и смещение в приемном массиве. Функция возвращает число байт сохраненное в выходном массиве. Есть еще другой вариант функции Decompress(), в котором память под выходной массив распределяется самой функцией. Эта функция принимает параметры beforeGap и afterGap, которые задают число байт, которое надо зарезервировать, соответственно, в начале и в конце выходного массива.
Классы AcedStreamWriter, AcedStreamReader
Эти классы аналогичны описанным выше классам AcedMemoryWriter, AcedMemoryReader. При их использовании, однако, данные помещаются не в массив байт, а в поток типа System.IO.Stream, ассоциированный с экземпляром класса AcedStreamWriter, и читаются не из массива байт, а из потока типа System.IO.Stream, ассоциированного с классом AcedStreamReader.
При работе с классом AcedStreamWriter в памяти создается буфер размером 2МБ, который постепенно заполняется данными. При достижении конца буфера, вызове методов Flush() или Close() класса AcedStreamWriter содержимое буфера упаковывается методом Compress() класса AcedDeflator. Сжатые данные сохраняются в другом буфере, размер которого также составляет 2МБ. Для упакованных данных вычисляется цифровая сигнатура RipeMD-160, после чего данные шифруются методом CAST-128. Длина фрагмента данных, контрольная сумма Адлера, цифровая сигнатура RipeMD-160 и сами сжатые и зашифрованные данные записываются в выходной поток типа System.IO.Stream. После этого содержимое буфера очищается и в него можно записывать следующие данные. При вызове метода Close() класса AcedStreamWriter, если ассоциированный с ним поток поддерживает операцию Seek, поток позиционируется на начало записанных данных и в потоке сохраняется общая длина (в байтах) данных, помещенных в поток классом AcedStreamWriter. Этот размер представляется значением типа System.Int64. Если операция Seek не поддерживается потоком типа System.IO.Stream, длина остается равной значению -1, записанному в поток при его ассоциации с классом AcedStreamWriter. Метод AssignStream класса AcedStreamWriter используется, чтобы связать данный экземпляр класса с потоком System.IO.Stream. Кроме ссылки на поток в этот метод передается константа, выбирающая режим сжатия данных, а также значение типа System.Guid, которое, если оно отлично от Guid.Empty, задает ключ для шифрования данных. Таким образом, в зависимости от параметров, переданных в метод AssignStream, этапы сжатия данных, расчета цифровой сигнатуры и шифрования данных могут опускаться.
Чтобы прочитать данные, сохраненные в потоке System.IO.Stream классом AcedStreamWriter, нужно воспользоваться классом AcedStreamReader.
Экземпляр этого класса может быть ассоциирован с потоком типа System.IO.Stream с помощью метода AssignStream. Если данные, помещенные в поток, зашифрованы, при вызове метода AssignStream следует указать ключ шифра в виде значения типа System.Guid. В методе AssignStream сразу считывается длина фрагмента данных, помещенного в поток классом AcedStreamWriter. Это значение возвращается свойством Length класса AcedStreamReader. Длина может быть равна значению -1, если не было возможности сохранить в потоке настоящее значение длины. В экземпляре класса AcedStreamReader также имеется два буфера, каждый размером по 2МБ. Первый предназначен для данных, считанных из потока System.IO.Stream, второй – для распакованных данных. Когда вызывается один из методов Read… класса AcedStreamReader, сначала предпринимается попытка считать значение из буфера распакованных данных. Если достигнут конец буфера, из потока System.IO.Stream считывается следующий фрагмент данных. Для этого фрагмента проверяется значение контрольной суммы Адлера. Затем, если данные зашифрованы, выполняется их дешифрование и проверка цифровой сигнатуры RipeMD-160. Потом, если данные упакованы, производится их распаковка во второй буфер. Теперь значение может быть прочитано и возвращено функцией Read.… При чтении из потока длинных массивов перегруженным методом Read() класса AcedStreamReader возможна ситуация, когда для считывания всего массива приходится несколько раз заполнять внутренний буфер данными из потока System.IO.Stream. Так как экземпляры классов AcedStreamWriter и AcedStreamReader занимают собой значительный объем памяти (каждый свыше 4МБ), создавать их при каждом чтении из потока нерационально. Сборщик мусора в .NET Framework автоматически относит блоки памяти свыше 85000 байт ко второму поколению (об этом см. в книге Джеффри Рихтера "Программирование на платформе .NET Framework" – M.: Издательско-торговый дом "Русская Редакция", 2003). Такие блоки лучше использовать для ресурсов с длительным временем существования.
В противном случае, частое пересоздание больших блоков памяти отрицательно сказывается на общей производительности приложения. Для решения этой проблемы в классах AcedStreamWriter и AcedStreamReader имеется статическое свойство Instance, которое при первом обращении к нему создает экземпляр соответствующего класса, а при следующих обращениях просто возвращает ссылку на существующий экземпляр. Тогда, вместо того, чтобы создавать новые экземпляры классов вызовом соответствующих конструкторов, лучше воспользоваться единственным экземпляром, возвращаемым свойством Instance. Этот подход аналогичен тому, который применяется в классах AcedDeflator и AcedInflator. Чтобы освободить занимаемую память можно вызвать статический метод Release(), освобождающий ссылку на экземпляр соответствующего класса. После помещения всех данных в поток AcedStreamWriter, а также после чтения необходимых данных из потока AcedStreamReader, нужно вызвать метод Close() для выполнения завершающих действий. Если в параметре closeStream метода Close() передано значение True, поток типа System.IO.Stream, ассоциированный с данным экземпляром класса AcedStreamWriter или AcedStreamReader, закрывается вызовом метода Close() потока.
Классы AcedWriterStream, AcedReaderStream
Эти классы представляют собой оболочку над другими классами, предназначенными для работы с бинарным потоком. Они используются, когда надо представить экземпляры других классов в виде объектов, производных от класса System.IO.Stream.
Класс AcedWriterStream является потомком класса System.IO.Stream и предназначен для записи данных в потоки типа AcedMemoryWriter и AcedStreamWriter. В его конструктор передается ссылка на интерфейс IAcedWriter, который поддерживается классами AcedMemoryWriter и AcedStreamWriter. Класс AcedWriterStream используется только для записи данных в поток, поэтому его свойства CanRead и CanSeek возвращают значение False, а свойство CanWrite – значение True. Вызов методов Write(), WriteByte(), Flush(), Close() перенаправляется соответствующим методам объекта, реализующего интерфейс IAcedWriter. При чтении свойств Length и Position возвращается число байт, помещенное в выходной бинарный поток. Однако, присвоение значения свойству Position или вызов методов Read(), ReadByte(), Seek(), SetLength() приводит к возникновению исключения типа System.NotSupportedException. Свойство Writer класса AcedWriterStream возвращает ссылку на объект, реализующий интерфейс IAcedWriter, которая была передана в конструктор класса при его создании.
Аналогичным образом применяется класс AcedReaderStream, который также является потомком класса System.IO.Stream. Этот класс предназначен для чтения данных из потоков типа AcedMemoryReader и AcedStreamReader, реализующих интерфейс IAcedReader. Класс AcedReaderStream предназначен исключительно для чтения данных, поэтому его свойство CanRead возвращает значение True, а свойства CanWrite и CanSeek возвращают значение False. Вызов методов Read(), ReadByte(), Close() перенаправляется соответствующим методам объекта, реализующего интерфейс IAcedReader. При чтении свойства Position возвращается текущая позиция в потоке относительно начала данных. Свойство Length возвращает общее число байт, которое может быть прочитано из потока. В некоторых случаях количество байт, помещенное в поток, неизвестно. Тогда свойство Length возвращает значение -1. Попытка присвоения значения свойству Position или вызова одного из следующих методов: Seek(), SetLength(), Write(), WriteByte() заканчивается возникновением исключения типа System.NotSupportedException. Свойство Reader класса AcedReaderStream возвращает интерфейс IAcedReader, переданный в конструктор класса. Подробную информацию о свойствах и методах интерфейсов IAcedWriter, IAcedReader можно найти в файле Interfaces.cs исходного кода.
Чтобы проиллюстрировать различные способы работы
Чтобы проиллюстрировать различные способы работы с бинарными данными с помощью рассмотренных выше классов, разработано небольшое приложение, которое представляет собой примитивный аналог архиватора файлов. Верхняя часть главной формы используется для помещения в бинарный поток данных произвольных файлов. При нажатии на кнопку "Добавить файл" пользователю предлагается выбрать на диске файл, который будет добавлен в поток. После помещения в поток одного или нескольких файлов можно сохранить весь поток на диске в виде одного файла. Причем данные при этом могут быть упакованы и зашифрованы. Чтобы проверить механизм контроля целостности данных, можно немного повредить данные в выходном потоке при сохранении его на диске. Для этого нужно пометить опцию "Инвертировать третий байт". Нижняя часть формы позволяет загрузить данные потока из файла на диске. Если поток зашифрован, перед чтением с диска надо установить опцию "Расшифровывать с паролем" и указать соответствующий пароль в поле ввода. Кроме данных, в бинарном потоке сохраняются имена и размеры файлов, которые отображаются в соответствующем списке после чтения потока с диска. Отдельные файлы из этого списка можно пометить и выгрузить нажатием кнопки "Сохранить отмеченный файл". Информация об имени файла потока, а также об использованном режиме сжатия сохраняется в реестре с помощью класса AcedRegistry и восстанавливается при следующем запуске программы. При добавлении очередного файла в бинарный поток сначала записывается его имя методом AcedMemoryWriter.WriteString(), потом длина файла в байтах методом AcedMemoryWriter.WriteInt32(), затем в потоке резервируется место для самих данных вызовом AcedMemoryWriter.Skip(). Фактическое заполнение данными происходит при вызове метода FileStream.Read(), в который передается ссылка на внутренний массив экземпляра класса AcedMemoryWriter, возвращаемый функцией GetBuffer(), и смещение, соответствующее длине потока до вызова метода Skip().
При сохранении потока на диске ключ шифрования получается как значение односторонней хеш-функции RipeMD-160 для строки символов, введенной пользователем в качестве пароля. Это значение преобразуется к типу System.Guid вызовом метода AcedRipeMD.ToGuid() и передается в метод ToArray() класса AcedMemoryWriter. В момент загрузки потока с диска проверяется его контрольная сумма. Затем данные потока расшифровываются и для них проверяется сигнатура RipeMD-160, после чего данные распаковываются. Из потока читаются имена и размеры файлов методами AcedMemoryReader.ReadString() и AcedMemoryReader.ReadInt32(). Для каждого файла вычисляется значение сигнатуры RipeMD-160. Эта информация помещается в список типа ListView. Сами данные пропускаются вызовом метода AcedMemoryReader.Skip(). В свойстве Tag каждого элемента типа ListViewItem списка сохраняется соответствующее ему смещение данных относительно начала потока. Когда пользователь выбрал элемент списка и нажал кнопку "Сохранить отмеченный файл", поток позиционируется на начало методом AcedMemoryReader.Reset(), а затем текущая позиция смещается на число байт, соответствующее значению свойства Tag элемента списка. После создания соответствующего файла на диске, данные выгружаются в файл напрямую из бинарного потока, минуя промежуточные массивы. Для этого в метод FileStream.Write() передается ссылка на внутренний массив экземпляра класса AcedMemoryReader с указанием смещения, равного текущей позиции в потоке. В этом приложении демонстрируются также способы работы с потоками на базе классов AcedStreamWriter и AcedStreamReader. При нажатии кнопок в нижней части главной формы приложения вызывается функция TestStreams(). Если параметр useAcedStreams этой функции равен False, в качестве основного хранилища данных используется стандартный класс System.IO.MemoryStream. Функция TestStreams() подготавливает некоторые разнотипные данные, а затем передает их в метод PutData(), который должен поместить их в поток типа System.IO.Stream (в данном случае MemoryStream).
Метод PutData() ассоциирует экземпляр класса AcedStreamWriter с переданным в нее потоком типа Stream. При этом указывается, что сохраняемые данные должны сжиматься в режиме Fast и шифроваться с ключом, который передается как последний параметр в функцию AssignStream(). Затем данные помещаются в поток с помощью методов класса AcedStreamWriter. Последним вызываемым методом является Close(), которые помещает в поток все буферизованные изменения. После выхода из PutData() заполненный бинарный поток типа MemoryStream превращается в массив байт, а его размер в байтах выводится в окне сообщения. Следующим этапом работы функции TestStreams() является загрузка данных из потока MemoryStream, созданного на основе полученного массива байт. Чтение данные выполняется методом GetData() с помощью экземпляра класса AcedStreamReader, ассоциированного с потоком типа System.IO.Stream (в данном случае MemoryStream). Если в параметре useAcedStreams функции TestStreams() передано значение True, в качестве хранилища данных вместо MemoryStream используется экземпляр класса AcedMemoryWriter. Так как метод PutData() работает только с потоками типа System.IO.Stream, необходимо создать класс-оболочку AcedWriterStream, который является потомком класса System.IO.Stream и в то же время инкапсулирует экземпляр класса AcedMemoryWriter. Ссылка на класс-оболочку передается в метод PutData(), и через него данные записываются в поток AcedMemoryWriter. В завершении, данные из AcedMemoryWriter превращаются в массив байт вызовом функции ToArray() аналогично предыдущему случаю. На этапе чтения данных в качестве хранилища выступает экземпляр класса AcedMemoryReader, который создается на основе полученного массива байт. Так как метод GetData() загружает данные только из потоков типа System.IO.Stream, создается класс-оболочка AcedReaderStream на основе экземпляра AcedMemoryReader. Ссылка на AcedReaderStream передается в метод GetData(). Таким образом, данные считываются из потока типа AcedMemoryReader посредством класса-оболочки AcedReaderStream, являющегося потомком класса System.IO.Stream.
NET было стремление создать классы
Основной целью разработки AcedUtils. NET было стремление создать классы для эффективного выполнения основных операций с данными, включая сжатие, шифрование, работу с бинарным потоком. Весь код библиотеки написан на языке C# и максимально оптимизирован по быстродействию. Библиотека AcedUtils.NET содержит следующие классы, принадлежащие пространству имен AcedUtils: AcedBinary – содержит статические методы и функции для работы с бинарными данными, в том числе для вычисления контрольной суммы Адлера, для копирования массивов байт и работы с массивами чисел типа Int32. AcedRipeMD – используется для вычисления значения односторонней хеш-функции RipeMD-160 массива байт или строки символов. Включает методы для копирования и сравнения цифровой сигнатуры, преобразования ее в значение типа Guid, очистки массива, содержащего цифровую сигнатуру. AcedCast5 – предназначен для шифрования и дешифрования массива байт методом CAST-128 в режиме CFB (64 бита). Блочный алгоритм шифрования реализован в соответствии с RFC 2144. Алгоритм отличается высоким быстродействием и надежностью. AcedDeflator, AcedInflator – используются для сжатия и распаковки массива байт с помощью алгоритма LZ+Huffman. AcedMemoryReader, AcedMemoryWriter – предназначены для помещения данных в бинарный поток и чтения из потока. При использовании этих классов бинарный поток представляется массивом типа byte[], размер которого динамически увеличивается по мере добавления новых данных. При этом сами данные могут быть упакованы с применением оригинального алгоритма сжатия, зашифрованы методом CAST-128 и защищены значением цифровой сигнатуры RipeMD-160. AcedStreamReader, AcedStreamWriter – предназначены для помещения данных в бинарный поток и чтения данных из потока. Здесь, в отличие от классов AcedMemoryReader и AcedMemoryWriter, размер бинарного потока не ограничивается объемом оперативной памяти. Данные помещаются в поток и читаются из потока типа System.IO.Stream, который ассоциируется, соответственно, с экземпляром класса AcedStreamWriter или AcedStreamReader. AcedReaderStream, AcedWriterStream – классы-оболочки, позволяющие работать с перечисленными выше классами бинарных потоков так, как будто они являются потомками класса System.IO.Stream. AcedRegistry – объединяет методы для сохранения в реестре Windows значений различного типа, в том числе, строк, массивов байт, значений типа Boolean, DateTime, Decimal, Double и т.д. Кроме того, в AcedRegistry находятся методы для чтения соответствующих значений из реестра Windows. Рассмотрим подробнее каждый из перечисленных классов.
В статье описываются классы, которые
В статье описываются классы, которые могут быть полезны разработчику на платформе .NET. Некоторые из них, такие как AcedRipeMD, AcedDeflator, являются аналогами классов, добавленных в .NET 2.0. Однако, реализация этих алгоритмов в библиотеке AcedUtils.NET представляется все же более эффективной.