11.5. Классы QString и QVariant.

Строки используются практически во всех программах ничуть не реже других типов.

Язык C++ предоставляет два типа строк: традиционные строки языка C -- массивы символов, завершающиеся символом '\0' и класс string. Qt предоставляет гораздо более мощный класс QString. Он предназначен для хранения строк с 16-ти битными символами Unicode. Unicode содержит наборы символов ASCII и Latin-1 с их обычными числовыми значениями. Но поскольку каждый символ в QString представлен 16-ю битами, он может содержать тысячи других символов. Дополнительную информацию об Unicode вы найдете в Главе 15.

Конкатенация двух строк QString может выполняться двухместным оператором "+" или оператором "+=". Ниже приводится пример использования обоих операторов:

QString str = "User: "; str += userName + "\n"; Кроме того, имеется функция QString::append(), которая идентична по своему действию оператору "+=": str = "User: "; str.append(userName); str.append("\n"); И совершенно иной подход к объединению строк состоит в использовании функции QString::sprintf(): str.sprintf("%s %.1f%%", "perfect competition", 100.0); Она поддерживает тот же набор спецификаторов формата, что и библиотечная функция sprintf(). В примере выше, в строку str будет записана строка "perfect competition 100.0%".

Еще один способ "сборки" строки из других строк и чисел -- использовать arg():

str = QString("%1 %2 (%3s-%4s)") .arg("permissive").arg("society").arg(1950).arg(1970); В этом примере "%1" будет заменено словом "permissive", "%2" -- "society", "%3" -- "1950" и "%4" -- "1970". В результате получится строка "permissive society (1950s-1970s)". Класс имеет несколько перегруженных функций arg() для обработки различных типов данных. Некоторые из них имеют дополнительные параметры, управляющие длиной выходной строки, базой системы счисления и точностью представления чисел с плавющей точкой. В большинстве случаев arg() представляет лучшее решение, чем sprintf(), потому что она более безопасна, полностью поддерживает Unicode и позволяет переводчикам изменять порядок следования параметров "%n".

QString позволяет преобразовывать числа в их строковое представление, с помощью статической функции QString::number():

str = QString::number(59.6); или с помощью QString::setNum(): str.setNum(59.6); Обратное преобразование может быть выполнено функциями toInt(), toLongLong(), toDouble() и т.д., например: bool ok; double d = str.toDouble(&ok); Эти функции могут принимать необязательный аргумент типа bool, в котором возвращается признак успеха преобразования. Если преобразование не может быть выполнено, они всегда возвращают 0.

Зачастую возникает ситуация, когда необходимо извлечь часть строки. Функция mid() возвращает подстроку заданной длины, начиная с заданной позиции в исходной строке. Например, следующий код выводит строку "pays":

QString str = "polluter pays principle"; cerr << str.mid(9, 4).ascii() << endl; Если опустить второй аргумент (или передать в качестве второго аргумента число -1), функция вернет подстроку, начиная с заданной позиции и до конца исходной строки. Например, следующий код выведет строку "pays principle": QString str = "polluter pays principle"; cerr << str.mid(9).ascii() << endl; Дополнительно имеются функции left() и right(). Они обе принимают количество символов n и возвращают первые или последние n символов исходной строки, соответственно. Например, следующий код выведет строку "polluter principle": QString str = "polluter pays principle"; cerr << str.left(8).ascii() << " " << str.right(9).ascii() << endl; Если нужно выполнить проверку -- начинается ли или заканчивается ли строка определенной комбинацией символов, для этих целей существуют функции startsWith() и endsWith(): if (uri.startsWith("http:") && uri.endsWith(".png")) ... Это гораздо быстрее и проще, чем: if (uri.left(5) == "http:" && uri.right(4) == ".png") ... Оператор сравнения строк "==" чувствителен к регистру символов. Для выполнения регистронезависимого сравнения, можно воспользоваться функциями upper() или lower(), например: if (fileName.lower() == "readme.txt") ... Для замены одной подстроки в строке другой подстрокой, используйте функцию replace(): QString str = "a sunny day"; str.replace(2, 5, "cloudy"); в результате получится строка "a cloudy day". То же самое действие может выполнено с помощью функций remove() и insert(): str.remove(2, 5); str.insert(2, "cloudy"); В первой строке удаляется пять символов, начиная со 2-й позиции, в результате получается строка "a day" (с двумя пробелами), затем, во второую позицию вставляется слово "cloudy".

Существуют перегруженные версии функции replace(), которые заменяют все вхождения первого аргумента на второй. Например, чтобы заменить все символы '&' в строке на "&amp;":

str.replace("&", "&amp;"); Очень часто возникает необходимость выбросить из начала и конца строки все лишние пробельные символы (такие как: пробелы, символы табуляции, символы перевода строки). Для этой цели существует функция stripWhiteSpace(): QString str = " BOB \t THE \nDOG \n"; cerr << str.stripWhiteSpace().ascii() << endl; Строка str может быть изображена как:

А результат, возвращаемый функцией stripWhiteSpace(), как:

Для удаления лишних пробельных символов, как на концах строки, так и внутри, предназначена функция simplifyWhiteSpace(): QString str = " BOB \t THE \nDOG \n"; cerr << str.simplifyWhiteSpace().ascii() << endl; Результат работы функции будет выглядеть так:

Строки могут быть разбиты на подстроки с помощью функции QStringList::split(): QString str = "polluter pays principle"; QStringList words = QStringList::split(" ", str); В этом примере, строка "polluter pays principle" разбивается на три подстроки; "polluter", "pays" и "principle". Функция split() может принимать третий необязательный параметр типа bool, который определяет -- должны ли игнорироваться пустые подстроки (по-умолчанию) или нет.

Элементы списка QStringList могут быть объединены в одну строку, с помощью функции join(). В качестве аргумента ей передается строка, которая должна быть вставлена между объединяемыми строками. Например, следующий код демонстрирует, как можно объединить все строки в списке, отсортированном по алфавиту, в единую строку, причем подстроки отделяются друг от друга символом перевода строки:

words.sort(); str = words.join("\n"); Еще одна немаловажная операция над строками -- определение длины строки. Для этого предназначена функция length() и, как вариант, isEmpty(), которая возвращает true, если длина строки равна 0.

QString различает пустые строки и несуществующие (NULL) строки. Эти различия корнями уходят в язык программирования C. Чтобы проверить -- существует ли строка, можно вызывать функцию isNull(). Для большинства приложений очень важно знать -- содержит ли строка хотя бы один символ. Функция isEmpty() вернет true, если строка не содержит ни одного символа (пустая или несуществующая строка).

Преобразования, между const char * и QString, в большинстве случаев выполняются автоматически:

str += " (1870)"; Этот код добавляет строку типа const char * к строке типа QString.

В некоторых ситуациях возникает необходимость явно выполнять преобразование между const char * и QString. Чтобы преобразовать строку QString в const char *, используйте функцию ascii() или latin1(). Обратное преобразование может быть выполнено за счет операции приведения типа.

Когда вызываются функции ascii() или latin1(), или когда выполняется автоматическое преобразование к типу const char *, возвращаемая строка принадлежит объекту QString. Это означает, что нас не должна беспокоить проблема утечки памяти -- Qt самостоятельно утилизирует память, по мере необходимости. С другой стороны, необходимо проявлять большую осторожность при работе с указателями. Например, если оригинальная версия строки QString будет изменена, то ранее полученный указатель на const char * может оказаться недопустимым. Если же необходимо сохранить предыдущий вариант строки, то для этих целей можно воспользоваться услугами класса QByteArray или QCString. Они хранят полную копию данных.

Класс QString поддерживает implicit sharing (неявное совместное использование данных). Это означает, что на копирование строки уходит времени не больше, чем необходимо для копирования указателя на строку. Собственно копирование производится только тогда, когда выполняется попытка изменить одну из копий. Все это делается автоматически и незаметно для нас.

Вся прелесть неявного совместного использования данных состоит в том, что таким образом оптимизируется скорость выполнения операций и при этом нам не нужно постоянно помнить об этом -- это просто работает!

Qt использует это метод оптимизации и для других классов, включая: QBrush, QFont, QPen, QPixmap, QMap<K, T>, QValueList<T> и QValueVector<T>. Что повышает эффективность передачи экземпляров классов по значению, как в виде аргументов функций, так и в виде возвращаемых значений.

C++ -- это строго типизированный язык, однако, иногда возникает необходимость сохранять данные в более общем виде. Самый простой способ -- использовать строки. Например, строки могут хранить текстовые или числовые данные. Qt предоставляет более простой способ работы с переменными -- класс QVariant.

Класс QVariant может хранить значения многих типов Qt: QBrush, QColor, QCursor, QDateTime, QFont, QKeySequence, QPalette, QPen, QPixmap, QPoint, QRect, QRegion, QSize и QString.. Он так же может хранить контейнеры: QMap<QString, QVariant>, QStringList и QValueList<QVariant>.. Мы уже использовали QVariant, когда разрабатывали приложение Spreadsheet, в Главе 4, для хранения содержимого ячейки.

Одно из обычных применений класса QVariant -- создание словарей (map), в которых в качестве ключа используются строки, а в качестве значений -- экземпляры класса QVariant. Как правило, информация о конфигурации приложения сохраняется и загружается с помощью QSettings, но иногда приложения могут обслуживать настройки напрямую, например, сохраняя их в базе данных. QMap<QString, QVariant> идеально подходит в таких ситуациях:

QMap<QString, QVariant> config; config["Width"] = 890; config["Height"] = 645; config["ForegroundColor"] = black; config["BackgroundColor"] = lightGray; config["SavedDate"] = QDateTime::currentDateTime(); QStringList files; files << "2003-05.dat" << "2003-06.dat" << "2003-07.dat"; config["RecentFiles"] = files;

Принцип Действия Неявного Совместного Использования Данных

Механизм неявного совместного использования данных работает полностью автоматически и незаметно для нас. Таким образом, когда мы используем классы, поддерживающие этот механизм, мы не должны писать дополнительный код, который поддерживал бы его работу. Но знать принцип его действия вам все таки необходимо, поэтому рассмотрим простенький пример и исследуем -- что же скрыто от наших глаз.

QString str1 = "Humpty"; QString str2 = str1; Здесь в переменную str1 записывается строка "Humpty" и затем выполняется присваивание переменной str2. С этого момента обе переменные указывают на одну и ту же структуру данных в памяти (типа QStringData). Вместе с символами строки она хранит счетчик ссылок, который содержит количество объектов, ссылающихся на нее. Поскольку и str1, и str2 ссылаются на одни и те же данные, то счетчик ссылок равен 2. str2[0] = 'D'; Когда выполняется изменение содержимого переменной str2, то прежде всего создается полная копия данных, таким образом, теперь str1 и str2 ссылаются на различные структуры и все изменения будут производиться над их собственными копиями данных. Счетчик ссылок переменной str1 ("Humpty") теперь стал равен 1 и счетчик ссылок переменной str2 ("Dumpty") так же стал равен 1. Когда счетчик ссылок равен 1, это означает, что данные используются только одним объектом. str2.truncate(4); Если теперь выполнить модификацию переменной str2, то никакого копирования производиться уже не будет, потому что счетчик ссылок равен 1. Функция truncate() будет оперировать с данными, принадлежащими переменной str2, и счетчик ссылок останется равным 1. str1 = str2; После такого присваивания, счетчик ссылок переменной str1 станет равным 0, это означает, что строка "Humpty" больше не нужна. В этом случае память, ранее занимаемая переменной str1, будет освобождена. Теперь обе переменные будут ссылаться на строку "Dump", а счетчик ссылок станет равным 2.

Создание классов, использующих оптимизацию неявного совместного использования данных, выполняется довольно просто. В ежеквартальнике Qt Quarterly, в статье "Data Sharing with Class" ( http://doc.trolltech.com/qq/qq02-data-sharing-with-class.html) описывается -- как это сделать.

Обход в цикле элементов словаря, который хранит значения в вариантном виде, может оказаться не такой простой задачей, если некоторые из значений являются контейнерами. Чтобы проверить тип вариантного значения вам придется воспользоваться функцией type():

QMap<QString, QVariant>::const_iterator it = config.begin(); while (it != config.end()) { QString str; if (it.data().type() == QVariant::StringList) str = it.data().toStringList().join(", "); else str = it.data().toString(); cerr << it.key().ascii() << ": " << str.ascii() << endl; ++it; } С помощью QVariant можно создавать довольно сложные структуры данных, хранящие значения контейнерного типа: QMap<QString, QVariant> price; price["Orange"] = 2.10; price["Pear"].asMap()["Standard"] = 1.95; price["Pear"].asMap()["Organic"] = 2.25; price["Pineapple"] = 3.85; В этом примере был создан словарь со строковыми ключами (название продукта) и значениями типа double (цена) или типа QMap. Словарь верхнего уровня содержит три ключа: "Orange", "Pear" и "Pineapple". Значение, связанные с ключом "Pear" -- это словарь с двумя ключами ("Standard" и "Organic").

Возможность создания структур данных, подобных этой, может показаться очень соблазнительной, так как можно структурировать данные по своему усмотрению. Но удобство QVariant -- вещь дорогостоящая. Ради удобочитаемости придется пожертвовать быстродействием.