Руководство программиста
Глава 2. Создание моделей блоков
§2.8. Сохранение и загрузка параметров блока
§2.8.4. Поиск ключевых слов с помощью объекта RDS
Рассматривается вспомогательный объект RDS, облегчающий поиск ключевых слов в текстовом формате хранения личных данных блока. Модель из предыдущего примера переписывается с использованием этого объекта.
Рассмотренный пример имеет небольшой недостаток – при анализе загруженного текста опознание ключевого слова производится последовательными сравнением в конструкции if … else if … else … Чем дальше в этой конструкции находится оператор сравнения с конкретным ключевым словом, тем больше проверок будет выполнено перед тем, как он сработает. Например, если считано слово «impulse», сначала оно будет сравниваться со словами «type» и «period», и только после этого будет опознано. Конечно, при трех ключевых словах это не вызовет сильного замедления загрузки. Тем не менее, в сложных блоках может быть несколько десятков параметров, и каждому из них может соответствовать ключевое слово в текстовом формате. В этом случае сравнение последовательным перебором может занять заметное время, особенно если таких блоков в схеме будет очень и очень много. Здесь логичнее использовать какой-либо другой, более быстрый, способ поиска ключевого слова. Например, можно отсортировать все ключевые слова по алфавиту и искать среди них считанное из текста слово методом деления пополам. В RDS есть механизм, позволяющий организовать такой поиск при помощи вспомогательного объекта. В §2.7.2 рассматривался вспомогательный объект RDS, открывающий модальные окна. Теперь воспользуемся другим объектом, позволяющим производить быстрый поиск слов в переданном ему массиве.
Попутно внесем в модель блока еще одно усовершенствование. В предыдущем примере каждое ключевое слово фигурировало два раза: первый раз – в форматной строке функции sprintf при сохранении параметров, второй раз – в параметре функции strcmp при загрузке. Необходимость указывать каждое ключевое слово два раза в разных местах программы повышает вероятность ошибки при ее написании. Конечно, можно было бы сделать ключевые слова define-константами, и использовать при загрузке и записи параметров их символические имена – это решило бы проблему с возможными опечатками. Однако, есть более удачное решение – создать глобальный массив ключевых слов. Такой массив все равно понадобится нам для вспомогательного объекта, разбирающего текст. При сохранении параметров мы будем обращаться к элементам этого массива по индексам, таким образом, один и тот же массив слов будет использоваться и при сохранении параметров, и при загрузке.
С глобальным массивом ключевых слов и указанными изменениями функция SaveText примет следующий вид:
// Глобальный массив ключевых слов // Индексы 0 1 2 const char *TestGen_Keywords[]={"type","period","impulse",NULL}; // define-константы для индексов #define TESTGEN_KW_TYPE 0 #define TESTGEN_KW_PERIOD 1 #define TESTGEN_KW_IMPULSE 2 //========================================= // Функция сохранения параметров void TTestGenData::SaveText(void) { // Запись "type" и целого значения rdsWriteWordValueText(TestGen_Keywords[TESTGEN_KW_TYPE],Type); // Запись "period" и вещественного значения rdsWriteWordDoubleText(TestGen_Keywords[TESTGEN_KW_PERIOD], Period); // Запись "impulse" и вещественного значения if(Type==2) rdsWriteWordDoubleText(TestGen_Keywords[TESTGEN_KW_IMPULSE], Impulse); } //=========================================
Перед функцией описывается массив ключевых слов TestGen_Keywords. Технически, его можно было бы сделать статическим членом класса TTestGenData, но, для простоты примера, он описан как глобальный. Каждое ключевое слово – это не изменяемая программой строка (const char*), поэтому массив описан как const char *имя_массива[]. Он состоит из четырех элементов – трех ключевых слов и нулевого указателя (NULL), который будет использоваться в качестве маркера конца массива. После массива описываются define-константы для индексов каждого из трех ключевых слов. Это сделано для лучшей читаемости текста: запись TestGen_Keywords[TESTGEN_KW_PERIOD] выглядит гораздо более информативно, чем TestGen_Keywords[1] – сразу становится понятно, что этот элемент имеет отношение к периоду.
В новой версии функции SaveText больше не используется сервисная функция rdsWriteBlockDataText. Ее заменили вызовы rdsWriteWordValueText и rdsWriteWordDoubleText, специально предназначенные для передачи в RDS целого и вещественного значений для записи вместе с ключевыми словами. Целое значение параметра Type записывается при помощи функции rdsWriteWordValueText, которая добавляет к уже сформированному на данный момент тексту параметров блока ключевое слово, переданное в первом параметре (TestGen_Keywords[TESTGEN_KW_TYPE], то есть «type»), и целое число Type, переданное во втором. При этом перед ключевым словом и после него автоматически добавляется один пробел. Преобразование целого числа в символьную форму производится внутри сервисной функции, поэтому в новой версии SaveText теперь не нужен массив символов для формирования текста. Вещественные значения Period и Impulse сохраняются функцией rdsWriteWordDoubleText, которая отличается от rdsWriteWordValueText только тем, что второй параметр у нее вещественный, а не целый. В результате трех вызовов этих функций в RDS будет передан точно такой же текст с параметрами блока, как и в первом варианте SaveText, в котором использовалась функция sprintf. Новая версия функции стала короче и выглядит теперь понятнее – на каждый сохраняемый параметр приходится только один вызов сервисной функции.
Теперь перепишем функцию LoadText, использовав в ней объект для разбора текста:
// Функция загрузки параметров void TTestGenData::LoadText(char *text) { RDS_HOBJECT Parser; // Вспомогательный объект BOOL work=TRUE; // Флаг цикла // Создание объекта для разбора текста Parser=rdsSTRCreateTextReader(TRUE); // Передача объекту массива ключевых слов rdsSTRAddKeywordsArray(Parser,TestGen_Keywords,-1,0); // Передача объекту разбираемого текста rdsSetObjectStr(Parser,RDS_HSTR_SETTEXT,0,text); // Цикл до тех пор, пока в тексте не кончатся слова while(work) { int id; // Уникальный идентификатор слова // Извлечь из текста и опознать слово id=rdsSTRGetWord(Parser,NULL,NULL,NULL,TRUE); // Действия в зависимости от слова switch(id) { // Нет слова – конец текста case RDS_HSTR_DEFENDOFTEXT: work=FALSE; // Выйти из цикла break; // Перевод строки – пропускаем case RDS_HSTR_DEFENDOFLINE: break; // Слово "type" case TESTGEN_KW_TYPE: // Извлекаем следующее слово и переводим в целое Type=rdsGetObjectInt(Parser,RDS_HSTR_READINT,TRUE); break; // Слово "period" case TESTGEN_KW_PERIOD: // Извлекаем следующее слово и переводим в double Period=rdsGetObjectDouble(Parser,RDS_HSTR_READDOUBLE,TRUE); break; // Слово "impulse" case TESTGEN_KW_IMPULSE: // Извлекаем следующее слово и переводим в double Impulse=rdsGetObjectDouble(Parser,RDS_HSTR_READDOUBLE,TRUE); break; default: // Слово не опознано – ошибка work=FALSE; // Выйти из цикла } // Конец switch(...) } // Конец while(work) // Удаление вспомогательного объекта rdsDeleteObject(Parser); } //=========================================
В начале функции описываются две вспомогательных переменных: Parser (типа RDS_HOBJECT), для хранения идентификатора объекта, разбирающего текст, и логическая переменная work, которая будет использоваться как условие цикла while. Чтобы разбирать текст при помощи вспомогательного объекта RDS, нужно сначала создать этот объект и передать ему массив ключевых слов и текст, после чего можно будет вызывать его в цикле и запрашивать идентификаторы считанных из текста слов.
Для создания объекта используется сервисная функция RDS rdsSTRCreateTextReader, принимающая единственный логический параметр: TRUE, если слова текста нужно сравнивать с ключевыми без учета регистра, и FALSE в противном случае. В данном случае регистр слов нас не интересует, поэтому в функцию передается значение TRUE. Возвращенный идентификатор созданного функцией объекта записывается в переменную Parser.
После того, как объект создан, необходимо передать ему массив ключевых слов, описанный ранее перед функцией SaveText, и сам текст, который этот объект будет разбирать на слова. Массив передается объекту при помощи функции rdsSTRAddKeywordsArray:
BOOL RDSCALL rdsSTRAddKeywordsArrayA( RDS_HOBJECT Parser, // Объект RDSCSTR *pWords, // Массив слов (UTF8) int WordCount, // Число слов в массиве или -1 int StartId); // Начальный идентификатор BOOL RDSCALL rdsSTRAddKeywordsArrayW( RDS_HOBJECT Parser, // Объект RDSWCSTR *pWords, // Массив слов (UTF16) int WordCount, // Число слов в массиве или -1 int StartId); // Начальный идентификатор // Функция-псевдоним BOOL RDSCALL rdsSTRAddKeywordsArray( RDS_HOBJECT Parser, // Объект RDSXCSTR *pWords, // Массив слов (кодировка по умолчанию) int WordCount, // Число слов в массиве или -1 int StartId); // Начальный идентификатор
В первом параметре функции передается идентификатор объекта, разбирающего текст – в нашем случае он хранится в переменной Parser. Во втором параметре передается указатель на начало массива слов TestGen_Keywords, который должен иметь тип «указатель на строку», причем тип строки зависит от варианта (суффикса имени) используемой функции. В тексте программы записан вызов функция-псевдонима rdsSTRAddKeywordsArray без суффикса «A» или «W». Поскольку в начале исходного текста модели нет макроопределений RDS_UTF16DEFAULT или RDS_NO_UTFALIASES, этот псевдоним будет ссылаться на вариант функции для кодировки UTF-8 (rdsSTRAddKeywordsArrayA). Значит, второй параметр функции должен иметь тип RDSCSTR*. Тип RDSCSTR в описаниях RDS соответствует стандартному типу «const char*» языка C, поэтому RDSCSTR* будет соответствовать «const char**», то есть типу «указатель на указатель на char», что соответствует описанию массива (char *TestGen_Keywords[]). В параметре WordCount передается число ключевых слов, которое нужно считать из массива, или −1, если нужно считать все слова до значения NULL. В данном случае массив ключевых слов завершается нулевым значением, и из него нужно считать все слова, поэтому этот параметр в примере равен −1. Наконец, в последнем параметре StartId передается идентификатор, который получит первое ключевое слово массива. Следующее за ним слово получит идентификатор StartId+1, следующее – StartId+2 и т.д. Эти идентификаторы будут возвращаться объектом при совпадении слова, считанного из текста, с одним из слов массива. В данном примере в StartId передается 0, поэтому идентификаторы ключевых слов будут совпадать с их индексами в массиве и, таким образом, с define-константами, описанными для этих индексов сразу после массива.
Далее нужно указать созданному объекту на текст, который он будет разбирать. Для этого используется универсальная функция передачи строки объекту rdsSetObjectStr. Эта функция уже использовалась при создании окна настройки параметров этого же блока. Как и парная ей функция получения строки rdsGetObjectStr, а также другие универсальные функции получения и установки параметров объекта (rdsGetObject… и rdsSetObject…), эта функция может использоваться для взаимодействия с любым вспомогательным объектом RDS. Результат ее действия зависит от типа объекта, идентификатор которого передан в первом параметре, и целых констант, переданных в втором и третьем. При вызове этой функции для объекта, созданного при помощи rdsSTRCreateTextReader, с константой RDS_HSTR_SETTEXT (вторая целая константа при этом не используется, поэтому в третьем параметре функции передается 0), в объект передается указатель на разбираемый текст из четвертого параметра функции, то есть значение переменной text.
Теперь, когда объект подготовлен к работе, можно начинать разбор текста. В цикле while из текста будет извлекаться слово за словом до тех пор, пока текст не закончится. Чтением слова из текста с одновременным поиском соответствующего ему идентификатора в массиве ключевых слов занимается функция rdsSTRGetWord:
int RDSCALL rdsSTRGetWordA( RDS_HOBJECT Parser, // Объект, разбирающий текст RDSCSTR *pWord, // Возвращаемый указатель на слово (UTF8) RDSCSTR *pNext, // Указатель на следующее слово (UTF8) char *pSym, // Тип слова или первый символ BOOL Analyse); // Сравнивать ли с ключевыми словами int RDSCALL rdsSTRGetWordW( RDS_HOBJECT Parser, // Объект, разбирающий текст RDSWCSTR *pWord, // Возвращаемый указатель на слово (UTF16) RDSWCSTR *pNext, // Указатель на следующее слово (UTF16) char *pSym, // Тип слова или первый символ BOOL Analyse); // Сравнивать ли с ключевыми словами // Функция-псевдоним int RDSCALL rdsSTRGetWord( RDS_HOBJECT Parser, // Объект, разбирающий текст RDSXCSTR *pWord, // Возвращаемый указатель на слово (кодировка по умолчанию) RDSXCSTR *pNext, // Указатель на следующее слово (кодировка по умолчанию) char *pSym, // Тип слова или первый символ BOOL Analyse); // Сравнивать ли с ключевыми словами
Функция rdsSTRGetWord очень похожа на уже рассмотренную ранее rdsGetTextWord, но, в отличие от последней, эта функция возвращает не указатель на считанное во внутренний буфер слово (теперь он возвращается через параметр pWord), а целый идентификатор опознанного ключевого слова, если параметр Analyse равен TRUE. Если считанное слово не совпадает ни с одним из ключевых, или параметр Analyse равен FALSE, возвращается специальный идентификатор неопознанного слова. По умолчанию это константа RDS_HSTR_DEFUNKNOWNWORD, равная −1, но, при необходимости, для неопознанного слова можно установить другое значение, чтобы оно не пересекалось с используемыми для ключевых слов. В нашем примере ключевые слова нумеруются начиная с 0, поэтому можно оставить стандартное значение. При обнаружении конца строки и конца текста также возвращаются специальные идентификаторы, по умолчанию – RDS_HSTR_DEFENDOFLINE (−2) и RDS_HSTR_DEFENDOFTEXT (−3) соответственно.
Идентификатор считанного из текста слова присваивается вспомогательной переменной id. Параметры pWord, pNext и pSym в вызове rdsSTRGetWord имеют значение NULL, поскольку в данном случае нас не интересует ни само считанное слово, ни его тип, ни указатель на следующее – нам нужен только идентификатор. Поскольку идентификатор – целое число, больше не нужна конструкция if … else if …, как в прошлом примере, вместо нее используется более наглядный оператор switch(id).
Мы не меняли у объекта значения стандартных идентификаторов, поэтому при обнаружении конца текста в id будет записана константа RDS_HSTR_DEFENDOFTEXT. В этом случае необходимо выйти из цикла разбора текста, для этого условию цикла (переменной work) присваивается значение FALSE. При обнаружении конца строки (id равно RDS_HSTR_DEFENDOFLINE) необходимо просто пропустить его и продолжить разбор текста – после соответствующего оператора case внутри switch записан оператор break без каких-либо действий. Далее следуют три оператора case для каждого из трех ключевых слов, которые используются в этом блоке.
Если из текста было считано слово «type», функция rdsSTRGetWord вернет число 0, поскольку «type» – первое слово, то есть нулевой индекс, в переданном объекту массиве ключевых слов TestGen_Keywords, и в качестве начального идентификатора для слов из этого массива было указано нулевое значение: 0 + 0 = 0. Сразу после описания массива TestGen_Keywords перед функцией SaveText для нулевого идентификатора, соответствующего этому слову, была определена define-константа TESTGEN_KW_TYPE, поэтому действия, выполняемые при обнаружение слова «type», записаны внутри switch(id) после оператора case TESTGEN_KW_TYPE.
В принципе, по аналогии с предыдущей версией функции LoadText, для чтения типа формируемого сигнала можно было бы ввести вспомогательную переменную word типа char* и записать такую конструкцию:
rdsSTRGetWord(Parser,&word,NULL,NULL,FALSE); Type=atoi(word);
В первой строке из текста считывается очередное слово, и указатель на него записывается в переменную word (распознавание ключевых слов при этом не производится – параметр Analyse функции rdsSTRGetWord равен FALSE), во второй – это слово преобразуется в целое число и присваивается параметру блока Type. Однако, есть более простой способ – можно сразу запросить у объекта Parser преобразование следующего слова в целое число при помощи стандартной функции получения данных объекта rdsGetObjectInt с параметром RDS_HSTR_READINT. Третий параметр функции (TRUE) в данном случае указывает на необходимость пропускать переводы строк, если они встретятся перед следующим словом текста. Таким образом, для чтения очередного слова текста и преобразования его в целое число с пропуском всех «лишних» переводов строк теперь требуется всего один вызов сервисной функции.
Аналогичным образом, при обнаружении в тексте слов «period» и «impulse» (оператор case с константами TESTGEN_KW_PERIOD и TESTGEN_KW_IMPULSE соответственно), следующее за ними слово преобразуется в вещественное число при помощи вызова rdsGetObjectDouble с параметром RDS_HSTR_READDOUBLE и присваивается соответствующему параметру блока. Если же считанное из текста ключевое слово не было опознано, будет выполнен оператор, следующий за default внутри switch(id), и переменной work будет присвоено значение FALSE, что приведет к выходу из цикла while(work).
Перед завершением функции загрузки параметров необходимо уничтожить вспомогательный объект, созданный функцией rdsSTRCreateTextReader. Объект уничтожается при помощи универсальной сервисной функции rdsDeleteObject, которая уже использовалась в других примерах для уничтожения объектов-окон. Эта функция может корректно удалить любой вспомогательный объект RDS.
Текст новой функции LoadText получился несколько длиннее старого варианта, но зато приобрел более четкую структуру. При этом новый вариант функции будет выполняться несколько быстрее. Какой из способов разбора текста выбрать – решать программисту.