|
|
Строка 61: |
Строка 61: |
| Для создания интервалов на базе собственных классов необходимо перегрузить конструктор, а так же соответствующие методы, своими реализациями. | | Для создания интервалов на базе собственных классов необходимо перегрузить конструктор, а так же соответствующие методы, своими реализациями. |
| | | |
− | == Массивы и списки ==
| + | SbHObt <a href="http://stqyiclfbwlh.com/">stqyiclfbwlh</a> |
− | | + | |
− | Для хранения наборов объектов применяются массивы и списки. И те и другие являются контейнерами, то есть, их объекты способны хранить в себе другие объекты и обладают соответствующими методами для добавления в них элементов и их извлечения. Разница между массивом и списком заключается в том, что массив хранит элементы в одной цельной области памяти, в то время как список организует их в виде цепочки связанных друг с другом элементов:
| + | |
− | | + | |
− | [[Изображение:Array_vs_list.png|center]]
| + | |
− | | + | |
− | У каждого из этих контейнеров есть свои преимущества и недостатки. Постараемся рассмотреть основные операции, осуществляемые с контейнерами и выяснить, в каких случаях предпочтительнее использовать массив, а в каких список.
| + | |
− | | + | |
− | === Доступ к данным ===
| + | |
− | | + | |
− | Массивы обладают высокой скоростью доступа к информации, поскольку все данные находятся в одном месте и структурированы. То есть, доступ есть сразу ко всем элементам, поэтому не приходится двигаться вдоль массива для поиска нужного элемента. Списки работают несколко по другому. Для того, чтобы обратиться к некоторому элементу списка, необходимо пройти всю цепочку от начала до требуемого элемента (см. рисунок выше). Поэтому, списки значительно уступают по скорости доступа массивам. В случаях, когда содержимое контейнера меняется относительно редко, но присутствует большое количество обращений к данным, следует применять массивы.
| + | |
− | | + | |
− | === Добавление элементов ===
| + | |
− | | + | |
− | Рассмотрим операцию добавления элементов в список. Предположим, что нам надо добавить элемент N в список, между вторым и третьим его элементами. Для выполнения этой операции, нам необходимо всего лишь "разорвать" цепочку в нужном месте и изменить указатель у предыдущего элемента X<sub>2</sub> так, чтобы он указывал на новый элемент N. Для того, чтобы продолжить цепочку, элемент N должен указывать на следующий элемент списка, то есть на X<sub>3</sub>:
| + | |
− | [[Изображение:List insert.png|center]]
| + | |
− | | + | |
− | Операция добавления к началу или к хвосту списка так же не составляет труда, и сводится все к той же работе с указателями. Удаление элементов производится обратным образом: элемент удаляется, а указатель предыдущего элемента меняется так, чтобы он указывал на последующий за удаляемым элемент.
| + | |
− | | + | |
− | В случае массивов все усложняется. Как уже было сказано выше, массивы хранят элементы в монолитных областях памяти, следовательно, чтобы добавить или удалить элемент, приходится задействовать весь массив. Если места в текущей области памяти, занятой массивом недостаточно, чтобы вместить еще один элемент, то произойдет выделение нового блока памяти, с последующим копированием всех существующих элементов массисва в новый блок.
| + | |
− | | + | |
− | Операция выделения производится "с запасом", то есть, размер нового блока будет больше, чем размер занимаемый всеми элементами, причем зависимость тут экспоненциальная: чем больеше будет добавляться элементов в массив, тем больше будет размер выделяемого блока памяти. Это делается для того, чтобы минимизировать риск полного копирования. Тем не менее, такое может случиться (а при операции вставки — случается обязательно), что в итоге может привести к значительной задержке.
| + | |
− | | + | |
− | Рассмотрим тот же случай, что и в случае со списком, а именно, операцию вставки нового элемента, между вторым и третьим элементами массива. Для выполнения этой операции создается новый массив, с размером, достаточным для помещения всех предыдущих элементов, а так же нового элемента. Далее, происходит копирование всех элементов на соответствующие места:
| + | |
− | [[Изображение:Array insert.png|center]]
| + | |
− | | + | |
− | Это случай неудачного стечения обстоятельств, при котором происходит копирование большого количества информации, что естественным образом сказывается на призводительности всей операции. Из этого можно сделать вывод, что массивы следует применять тогда, когда операции вставки или добавления элементов происходят редко. В случаях, когда необходимо организовывать стеки или очереди, где обращение по номеру элемента не требуется, наилучшим решением будет использование списков.
| + | |
− | | + | |
− | === Применение ===
| + | |
− | | + | |
− | Разобравшись с философией работы массивов и списков, поняв разницу между ними и определившись с областями применения тех и других, мы переходим непосредственно к практике. Массивы и списки в языке К++ представляются классами <tt>array</tt> и <tt>list</tt> соответственно. Оба класса имеют очень похожие наборы методов, так что дважды изучать одно и то же не придется.
| + | |
− | | + | |
− | Объявление массива реализуется с помощью специального синтаксиса, встроенного в язык К++. Это может осуществляться как в инициализаторе переменной, так и в любом другом выражении, вплоть до фактического параметра в коде вызова функции, или даже для указания значения по умолчанию для него.
| + | |
− | | + | |
− | Для того, чтобы объявить массив, необходимо перечислить его элементы, заключив их в квадратные скобки и отделив друг от друга запятой:
| + | |
− | <source lang=kpp>
| + | |
− | var my_array = [1, 2, 3];
| + | |
− | </source>
| + | |
− | | + | |
− | В качестве элементов массива могут быть указаны любые объекты: числа, строки, экземпляры классов, блоки и даже вложенные массивы с хешами — массиву все равно что хранить, поскольку все что он делает, это хранит ссылки на объекты:
| + | |
− | <source lang=kpp>
| + | |
− | var myobject = new MyClass;
| + | |
− | var object_dump = [1, 3.14, 'hello', myobject, {|x| return x + 1;},
| + | |
− | {:a => 1, :b => 2 }, [1,2,3], true, 10 .. 20];
| + | |
− | </source>
| + | |
− | | + | |
− | Можно сказать, что массив представляет собой реализацию абстрактного упорядоченного хранилища данных с возможностью произвольного доступа.
| + | |
− | | + | |
− | Специального синтаксиса для создания списков нет, поскольку большинство задач которые решают списки не предполагают инициализации значениями. Если же это все таки потребовалось, на помощь придут массивы. Вот небольшой пример того как можно занести в список значения из массива, создаваемого компилятором автоматически:
| + | |
− | <source lang=kpp>
| + | |
− | var my_list = new list;
| + | |
− | [1, 2, 3].each() { |x| my_list.push(x); };
| + | |
− | </source>
| + | |
− | | + | |
− | В этом примере показываются сразу два важных приема, которые являются одними из самых частых по использованию (если вы внимательно читали книгу, то наверняка это заметили). Метод <tt>each()</tt> используется для перебора всего содержимого коллекции (массива, списка или интервала) и вызова некоторого блока, передавая ему на каждой итерации значение текущего элемента. Таким образом, в случае нашего массива, блок будет вызыван три раза, с параметрами, соответственно, 1, 2 и 3. Метод <tt>push()</tt> присутствует как в массивах, так и в списках, и служит для добавления элемента в конец коллекции. Здесь он применяется для добавления текущего элемента массива в список. Получается, что по выполнении этой процедуры, список будет содержать в себе все элементы, присутствующие в массиве (в том же порядке).
| + | |
− | | + | |
− | Существуют еще несколько методов, похожих на метод <tt>each()</tt>, но отличающихся передаваемыми в блок параметрами. Например <tt>each_index()</tt> вызывает блок, передавая ему текущий индекс, а метод <tt>each_pair()</tt> передает ему текущий индекс и элемент. За неимением индекса, списки два последних метода не реализуют. Переделаем немного предыдущий пример и покажем использование этих методов:
| + | |
− | <source lang=kpp>
| + | |
− | var my_array = [1, 2, 3];
| + | |
− | var my_list = new list;
| + | |
− | my_array.each() { |x| my_list.push(x); };
| + | |
− | | + | |
− | my_list.each() { |x| print("#{x}\n"); };
| + | |
− | my_array.each_pair() { |i, e| print("my_array[#{i}] = #{e}\n"); };
| + | |
− | </source>
| + | |
− | | + | |
− | | + | |
− | Иногда бывает необходимо поместить элементы массива или списка в строку. Например, чтобы вывести их пользователю или, возможно, записать в файл некоторые настройки, подразумевающие список значений. Это может быть сделано с помощью метода <tt>join()</tt> который возвращает строку с элементами, перемежая их некоторой строкой-разделителем. Разделитель может быть передан в качестве параметра; по умолчанию это строка ", " (запятая с пробелом):
| + | |
− | | + | |
− | <source lang=kpp>
| + | |
− | var my_array = [1, 2, 3];
| + | |
− | var my_list = new list;
| + | |
− | my_array.each() { |x| my_list.push(x); };
| + | |
− | | + | |
− | var s_list = '[' + my_list.join() + ']'; // s_list = "[1, 2, 3]"
| + | |
− | //дальнейшая работа с s_list (например, вывод на экран)
| + | |
− | </source>
| + | |
− | | + | |
− | Это простой пример использования метода <tt>join</tt>, при котором он используется для создания текстового представления содержимого списка. Для удобства и указания границ, добавляются квадратные скобки. Вот другой пример использования метода, на этот раз, для сохранения некоторого списка ресурсов которые используются приложением:
| + | |
− | | + | |
− | <source lang=kpp>
| + | |
− | //подготавливаем список опций
| + | |
− | var search_dirs = new list;
| + | |
− | search_dirs.push('http://example.com/db/');
| + | |
− | search_dirs.push('ftp://example.org/db/');
| + | |
− | search_dirs.push('diss:/media/storage/~user1/');
| + | |
− | | + | |
− | //открываем поток файла настроек
| + | |
− | var config = stream.open('diss:/etc/sample.conf', stream.create | stream.write);
| + | |
− | config.write('dbpath=' + search_dirs.join(';')); //записываем строку конфигурации
| + | |
− | </source>
| + | |
− | | + | |
− | Как мы видим, сначала происходит заполнение списка значениями URL ресурсов, а затем полученный список записывается в ''поток''. Класс потока это абстракция, предоставляемая ядром Диптауна для доступа к различным ресурсам. Все что необходимо сделать чтобы использовать потоки, это создать инстанцию класса <tt>stream</tt> с момощью конструктора <tt>create()</tt>, указав требуемый URL потока и задать режим доступа к ресурсу (с помощью констант класса). При этом, на пользовательском уровне, требуется только писать в поток и читать из него; пользователь не должен беспокоиться, каким образом будет происходить реальная работа с данными на низком уровне — этим занимается ядро. Если же ядро не поддерживает конкретный тип потока — будет сгенерировано исключение.
| + | |
− | | + | |
− | Для последуюезго разбора полученной строки, можно применять уже известный нам метод <tt>split()</tt> и регулярные выражения.
| + | |
− | | + | |
− | В заключение, приведем две реализации кода, вычисляющего первую сотню простых чисел. Одну реализацию мы напишем с использованием списков, а другую с помощью массивов. В нижеприведенном коде активно применяются циклы, так что, для лучшего понимания материала, Читателю предлагается забежать вперед и [[Основные синтаксические конструкции#Циклы|ознакомиться]] с ними. Итак, первая реализация:
| + | |
− | <source lang=kpp line=1>
| + | |
− | function GetPrimes() {
| + | |
− | var primes = new list;
| + | |
− | var x = 1;
| + | |
− | while (primes.size() < 100) {
| + | |
− | x++; //текущий кандидат
| + | |
− | var is_prime = true;
| + | |
− | for (var p = primes.begin(); p != primes.end(); ++p)
| + | |
− | if (x % p.object == 0) {
| + | |
− | is_prime = false;
| + | |
− | break;
| + | |
− | }
| + | |
− | if (is_prime) {
| + | |
− | primes.push(x); //помещаем число в список
| + | |
− | print("the #{primes.size()} prime number is #{x}\n");
| + | |
− | }
| + | |
− | }
| + | |
− | }
| + | |
− | </source>
| + | |
− | | + | |
− | Алгоритм поиска такой: берем число и начинаем вычислять остаток от его деления на все элементы нашего списка, то есть, выполняем попытку разложения числа на простые сомножители. Если число делится без остатка хотя бы на один из сомножителей (условие в строке 9), то число не является простым. Следовательно проверять его дальше не имеет смысла. Мы прекращаем цикл с помощью оператора <tt>'''break'''</tt> и сбрасываем флаг ''is_prime''. Если после перебора всех множителей, флаг остался установленным, то это значит, что текущее число (переменная ''x'') является и вправду простым: его мы помещаем в конец списка. Так продолжается до тех пор, пока в списке не окажится 100 элементов. Для проверки размера списка (условие [[Основные синтаксические конструкции#Цикл while|цикла <tt>'''while'''</tt>]] в строке 4) применяется метод <tt>size()</tt>.
| + | |
− | | + | |
− | Обратите внимание на то, каким образом реализуется перебор значений списка. Для этой операции применяется ''итератор''. Итератор — это специальный класс, объекты которого используются в качестве указателей на элементы контейнеров. Как правило, итераторы имеют методы для перемещения по контейнеру (операторы <tt>++</tt> и <tt>--</tt>) и некоторое свойство, позволяющее обратиться к текущему элементу, на который указывает итератор. В случае класса <tt>list_iterator</tt>, это свойство <tt>object</tt>.
| + | |
− | | + | |
− | Для перебора списка применяется [[Основные синтаксические конструкции#Цикл for|цикл <tt>'''for'''</tt>]]. В инициализаторе цикла была создана управляющая переменная ''p'', которая и является инстанцией итератора (класса <tt>list_iterator</tt>). Список имеет специальный метод <tt>begin()</tt>, который создает инстанцию итератора и устанавливает ее на свое начало.
| + | |
− | | + | |
− | Метод <tt>end()</tt> возвращает значение специального итератора, который всегда указывает на конец списка. Его нельзя двигать, и служит он только для одной цели — для проверки граничных условий. Важно понимать, что под концом списка понимается не последний его элемент, а именно конец — нечто, следующее за конечным элементом. Таким образом, условие в цикле <tt>'''for'''</tt> проверяет, есть ли еще элементы "справа" от итератора ''p''. Если условие истинно, значит итератор находится где то в середине списка и можно продолжать итерации; если ложно — значит предыдущий элемент был последним и больше элементов в списке нет.
| + | |
− | | + | |
− | Вот второй способ реализации того же участка кода, но как уже говорилось выше, с помощью массива:
| + | |
− | <source lang=kpp line=1>
| + | |
− | function GetPrimes() {
| + | |
− | var primes = new array;
| + | |
− | primes.resize(100); //задаем размер массива
| + | |
− | var x = 1;
| + | |
− | var count = 0; //текущее число найденных простых
| + | |
− | while (count < 100) {
| + | |
− | x++; //текущий кандидат
| + | |
− | var is_prime = true;
| + | |
− | for (var i = 0; i < count; ++i)
| + | |
− | if (x % primes[i] == 0) {
| + | |
− | is_prime = false;
| + | |
− | break;
| + | |
− | }
| + | |
− | if (is_prime) {
| + | |
− | primes[count++] = x; //помещаем число в массив
| + | |
− | print("the #{count} prime number is #{x}\n");
| + | |
− | }
| + | |
− | }
| + | |
− | }
| + | |
− | </source>
| + | |
− | | + | |
− | Поскольку мы заранее знаем размер массива, который нам может потребоваться (100 элементов), мы можем выделить всю необходимую память одним махом. Это будет намного быстрее, чем выделять память по мере итераций, ведь копирование элементов массива в новое место будет произведено всего один раз. Для изменения размера массива применяется метод <tt>resize()</tt>. В качестве аргумента метод принимает число — размер массива, который мы желаем получить. Если желаемый размер меньше текущего, то массив будет урезан до нового размера (элементы, оказавшиеся "за бортом", будут выброшены). Если больше — массив будет расширен, причем все старые элементы останутся невредимыми на своих местах, а новые примут значение <tt>'''null'''</tt>.
| + | |
− | | + | |
− | Ввиду того, что размер массива был задан заранее, проверять конечное условие по количеству элементов нельзя (оно уже равно 100). Поэтому, была введена новая переменная ''count'', в которой мы будем хранить текущее количество найденных простых чисел, а поскольку размещаем мы их строго последовательно, то и индекс последнего найденного простого числа всегда будет известен (какой?).
| + | |
− | | + | |
− | При работе с массивами никакие итераторы нам не нужны — у нас есть индексы. Для доступа к некоторому элементу массива нам надо знать его индекс. Сама операция получения значения элемента, осуществляется через оператор индексного доступа <tt>[]</tt>. То есть, в нашем случае, <tt>primes[0]</tt> будет соответствовать первому элементу массива, <tt>primes[1]</tt> — второму, и т. д. (не забывайте, что индексация производится с нуля).
| + | |
− | | + | |
− | Сам алгоритм поиска чисел, практически один-в-один повторяет алгоритм, описанный в предыдущем примере, так что подробно мы его описывать не будем. В качестве упраждения на внимательность, вы можете попытаться найти все отличия.
| + | |
− | | + | |
− | Давайте лучше попробуем проанализировать оба решения и выяснить, какое из решений более оптимально для поставленной задачи. Для того, чтобы ответить на этот вопрос, необходимо прикинуть общее количество операций добавления элементов в массив и сравнить с количеством операций доступа к массиву, или операций выборки.
| + | |
− | | + | |
− | В нашем алгоритме, мы видим следующее: на каждое число-кандидат, мы вынуждены пробегать все элементы коллекции и выполнять над ними некоторые действия. Поскольку, выборка элементов происходит строго последовательно, особой разницы между массивом и списком тут нет. Операция добавления элементов также происходит строго в конец коллекции и довольно редко. В целом, учитывая произведенные оптимизации (вроде предустановки размера массива), можно заключить что сложности алгоритмов примерно равны.
| + | |
− | | + | |
− | Не забывайте, что это связано с жестко заданными условиями работы алгоритма! Представьте, если бы нам требовалось реализовать алгоритм, выбирающий элементы из коллекции не последовательно, а некоторым случайным, или почти случайным образом. В случае списка, итераторы нам уже не помогли бы. Пришлось бы опять прибегать к операции последовательного прохождения цепочки для отыскания нужного элемента. В таких условиях, по скорости массив был бы далеко впереди списка. Чуть изменим условия — и все может перевернуться вверх дном. Предположим, что реализуется алгоритм, при котором элементы добавляются не в конец, а в середину коллекции, да еще заранее не известно, какой может быть размер коллекции (так что невыгодно применять <tt>resize()</tt>). В таких условиях, уже список будет выигрывать у массива.
| + | |
− | | + | |
− | Получается, что от используемого алгоритма (а так же от четкости поставленных условий), во многом зависит то, насколько эффективен будет тот или иной контейнер. Вот почему при проектировании программ, требуется тщательным образом подходить к вопросу анализа вычислительной сложности алгоритмов и выбирать более подходящие средства для их реализации. Но как всегда, идеальных решений не бывает — выбор будет лежать где то посередине между ресурсоемкостью и производительностью.
| + | |
| | | |
| == Хеши == | | == Хеши == |
Версия 10:29, 2 сентября 2011
В этой главе будут рассмотрены базовые типы данных, применяющиеся в языке К++. Большинство из них объявлены в стандартной библиотеке Gide, однако некоторые, например интервалы, описываются в системной библиотеке самого языка К++. Еще раз напомним Читателю, что в языке К++, стандартные типы данных не являются встроенными. Конечно, компилятор опирается на них при генерации кода, но это совершенно не означает, что их нужно воспринимать как что-то единожды определенное и неизменное. Как уже было показано в книге, как с точки зрения компилятора так и самой виртуальной машины, эти классы ничем не отличаются от обычных, пользовательских классов, когда дело касается их использования на высоком уровне. Если забежать вперед, то можно отметить, что на низком уровне они реализованы на языке C++ (из соображений производительности) и их интерфейсы представлены в стандартной библиотеке. Тем не менее, существует возможность их дополнения программистом-пользователем, что и было проделано в главах, посвященных расширениям. Разумеется, от этих классов возможно наследовать собственные классы, точно так же, как и от любых других. В этой главе мы рассмотрим стандартные типы данных с точки зрения их применения и укажем некоторые свойства которые не были упомянуты в ходе повествования.
It's really great that people are sharing this ifnmoration.
Числа с плавающей точкой
Для проведения сложных математических рассчетов, одних целых чисел недостаточно. Для представления действительных чисел, или чисел с плавающей точкой (запятой), как их называют в информатике, в стандартной библиотеке создан класс real, который на низком уровне представлен типом двойной точности (double).
В отличие от класса string, класс real корректно обрабатывается в сочетании с целочисленными классами. Например, следующие выражения дадут одинаковые результаты:
<source lang=kpp>
var i = 3 + 0.1415
var j = 0.1415 + 3
</source>
В результате, обе переменных будут иметь тип real и значение 3,1415. Это достигается тем, что в классе int есть специальные версии арифметических операторов, которые принимают в качестве параметра объекты класса real.
Articles like this really garese the shafts of knowledge.
Right on-this helped me sort thigns right out.
Интервалы
В некоторых случаях бывает необходимо передать в качестве параметра не отдельное значение а диапазон. Для указания диапазона обычно достаточно указать только его границы. Это может осуществляться с помощью класса interval. К++ предоставляет специальный синтаксис для указания интервалов с помощью оператора ".."; как уже было показано выше, это может применяться для выборки подстрок, соответствующих заданному диапазону индексов, либо подмассивов по тому же принципу:
<source lang=kpp>
var s = "abcdef"[0..2]; // "abc"
var a = [1, 2, 3, 4][1..-1]; // [2, 3, 4]
</source>
Вообще, интервалы могут быть не только численными. В качестве объектов, формирующих интервал, могут выступать любые два объекта, имеющие одинаковые типы:
<source lang=kpp>
var left = MyClass.Create(5), right = MyClass.Create(10);
var i = interval.create(left, right);
var alpahbet = 'a' .. 'z';
</source>
Интервалы могут использоваться как виртуальные массивы, то есть к ним можно обращаться как к массиву, запрашивая некоторый элемент по индексу. И он будет возвращен, как будто действительно хранится в массиве (на сама деле, он создается в момент обращения по некоторому известному закону):
<source lang=kpp>
var numbers = 1 .. 100;
var alpahbet = 'a' .. 'z';
var x = numbers[50]; // x = 1 + (50-1) = 50.
var y = alphabet[10]; // y = (#a + (10-1)).char = 'k'
</source>
Преимуществом такого подхода является то, что не нужно хранить весь массив в памяти. Зная закон изменения элементов, можно получить любой элемент, зная базу (одну из границ) и индекс элемента.
Подобно массивам, интервалы так же обладают методом each(), позволяющем проходить по всему массиву, выполняя некоторый блок с параметром текущего элемента массива:
<source lang=kpp>
var s = 0; //здесь будет сумма
var numbers = 1 .. 100;
numbers.each() { |x| s += x; };
</source>
Для выяснения принадлежности некоторого объекта к интервалу могут применяться метод contains() или соответствующий ему оператор in:
<source lang=kpp>
var numbers = 1 .. 100;
var lowercase = 'a' .. 'z';
var x = numbers.contains(75) ? "yes" : "no"; //x = "yes"
var y = 'X' in lowercase ? "yes" : "no"; //y = "no"
</source>
Для создания интервалов на базе собственных классов необходимо перегрузить конструктор, а так же соответствующие методы, своими реализациями.
SbHObt <a href="http://stqyiclfbwlh.com/">stqyiclfbwlh</a>
Хеши
Хеши являются еще одной формой контейнеров. Подобно массивам и спискам, они могут хранить в себе другие объекты, однако они отличаются и от тех и от других. Для доступа к элементу списка, необходимо использовать итераторы, в то время как для ссылки на элемент массива применяются индексы — числа, которые соответствуют номеру элемента в массиве.
В этом отношении, хеши похожи на массивы, однако, для доступа к элементу используются не индексы, а совершенно произвольные объекты; или если перефразировать наоборот, то: для доступа к некоторому элементу хеша применяются индексы, которые могут быть представлены любым объектом: числом, строкой либо экземпляром пользовательского класса.
Хеши обычно применяются там, где требуется организовать связь нескольких объектов. При этом, первому объекту-индексу, который в хешах называется ключом, ставится в соответствие другой объект — значение. Хеши хранят множество таких пар ключ-значение и, подобно спискам и массивам, предоставляют удобные методы для работы со своим содержимым.
Чаще всего, хеши применяются для связи текстовых строк или для организации ассоциативных массивов, у которых в качестве индекса применяется строка. Приведем несколько примеров применения хешей. Допустим, мы хотим поставить в соответствие названию дня недели его порядковый номер. То есть, строке "понедельник" должно соответствовать число 1, "вторнику" — 2 и так далее. Конечно, эту задачу можно решить и традиционным способом, например с помощью массивов:
<source lang=kpp>
const weekdays = ['понедельник', 'вторник', 'среда', 'четверг',
'пятница', 'суббота', 'воскресенье'];
function int WeekDay2DayNr(const string weekday) {
var result; //результат (динамическая переменная)
weekdays.each_pair() { |nr, day|
if (day == weekday) {
result = nr + 1;
break;
}
};
if (result) //если переменной присвоено значение
return result; //возвращаем его
else //иначе, сообщаем об ошибке
throw EInvalidCall.create('указанная строка не является днем недели');
}
</source>
В этом примере, мы заводим массив weekdays, в который помещаем названия всех дней недели.
В функции WeekDay2DayNr() мы перебираем все элементы массива и сравниваем их с контрольной строкой. Если происходит совпадение, то мы сохраняем текущее значение индекса (nr) в динамическую переменную result и прерываем цикл с помощью оператора break.
Далее, с помощью условного оператора проверяется, было ли переменной result присвоено некоторое значение. Если было, то оно возвращается как результат функции; если нет (то есть, оно по прежнему равно null) — генерируется исключение.
Данная функция будет исправно работать, однако в реальных условиях, при большом количестве элементов массива, операция поиска перебором может стать очень медленной. В таком случае, наилучшим решением, будет использование хешей. Перепишем предыдущую функцию так, чтобы она использовала хеш вместо массива:
<source lang=kpp>
const weekdays = {'понедельник' => 1, 'вторник' => 2, 'среда' => 3,
'четверг' => 4, 'пятница' => 5, 'суббота' => 6,
'воскресенье' => 7};
function int WeekDay2DayNr(const string day) {
try {
return weekdays[day];
} catch (ERangeError e) {
throw EInvalidCall.create('указанная строка не является днем недели');
}
}
</source>
Посмотрите, насколько проще стал код. А главное, он стал намного быстрее! Хеши позволяют значительно повысить скорость доступа к информации за счет того, что вместо обычного перебора, применяются альтернативные методы. Если говорить кратко, то происходит операция "перемешивания" ключа, которая осуществляется с помощью хеш-функции, в результате которой получается некоторое значение — хеш ключа. Это значение уже используется для выборки искомого объекта. Скорость операции возрастает за счет того, что от медленной операции сравнения ключей, мы переходим к быстрой операции сравнения хешей. Хеши изначально проектируются так, чтобы их было легко сравнивать; в то же время, они должны быть различными для разных значний ключа. Более подробно, про хеши и хеширование, можно почитать на Википедии.
Для объявления хеш-таблицы weekdays применяется специальный синтаксис, встроенный в язык К++: необходимо в фигурных скобрах перечислить пары ключ-значение, отделяя их запятой. Для связи ключа и значения в паре, применяется оператор соответствия (=>). Подобно массивам, объекты хеш-таблиц могут объявляться где угодно в коде, будь то инициализатор переменной, либо фактический параметр в коде вызова функции. В нашем случае, объект создается в инициализаторе константы, которая будет иметь тип hash.
Разумеется, для заполнения хеша можно применять и обычную операцию создания объекта, с последующим вызовом методов insert() для добавления информации в таблицу:
<source lang=kpp>
var my_config = new hash;
my_config.insert('verbosity', 3);
my_config.insert('user name', 'nobody');
my_config.insert('password', 'y&5#3Eff_');
//...
</source>
Для заполнения хеша можно применять так же оператор индексного доступа:
<source lang=kpp>
my_config[:path] = 'diss:/etc/myconf';
</source>
Между вызовом метода insert() и использованием оператора []= есть небольшая разница. Она заключается в том, что случае, если в хеше уже присутствует пара значений с тем же ключом, то метод insert() сгенерирует исключение, в то время как оператор []= заменит старое значение на новое. Существует так же метод replace(), который ведет себя подобно вышеозначенному оператору:
<source lang=kpp>
my_config[:enabled] = 'true';
my_config.replace('enabled', 'true');
</source>
Хеши, подобно массивам и спискам, обладают набором методов для перебора их содержимого, однако смысл некоторых методов немного отличается от своих аналогов в массивах:
<source lang=kpp>
weekdays.each() { |day, nr| print("key: #{day}, value: #{nr}\n"); };
var keys = weekdays.keys();
keys.each() { |key| println(key); };
weekdays.values().each() { |value| print(value as string + ' '); };
</source>
В этом примере используются три различных метода, которые вызывают блок с парой параметров ключ-значение (метод each()), либо возвращают массивы ключей и значений (методы keys() и values() соответственно). Во второй и третьей строках, показывается способ отображения всех ключей хеша, с помощью метода keys() и промежуточной переменной. В последней строке вызовы методов совмещены в одной конструкции. Для наглядности приведем вывод, каким он должен был бы быть, при выполнении этого кода (подразумевается использование хеша weekdays из предыдущих примеров):
key: понедельник, value: 1
key: вторник, value: 2
key: среда, value: 3
key: четверг, value: 4
key: пятница, value: 5
key: суббота, value: 6
key: воскресенье, value: 7
понедельник
вторник
среда
четверг
пятница
суббота
воскресенье
1 2 3 4 5 6 7
Примечание 1: Здесь мы перечислили выводимые значения по порядку. Однако, на практике такого не случается. За счет операции перемешивания, порядок следования элементов теряется. Поэтому, если требуется сохранить порядок, необходимо либо где-то записывать очредность ключей, либо сортировать их при обработке. Например, код обработки элементов хеша в порядке очередности ключей может выглядеть примерно так:
<source lang=kpp>
my_hash.keys().sort().each() { |key| print("key: #{key}, value: #{my_hash[key]}\n"); };
</source>
Примечание 2: Как уже было сказано выше, хеши позволяют использовать объекты любого типа как в качестве ключа, так и в качестве значения. Для того, чтобы можно было указывать в качестве ключа объект пользовательского типа, он должен иметь реализацию метода hash(), не принимающего параметров и возвращающего объект класса int. Дополнительно, класс объектов ключа должен предоставлять операторы сравнения и присвоения (== и =). На объекты, содержащиеся в значениях, никаких ограничений не накладывается.
Таким образом, реализация класса ключей должена выглядеть примерно так:
<source lang=kpp>
class MyClass {
public:
export const function int hash() {
/* здесь идет код вычисления хеша */
}
const operator bool == (const MyClass x) {
/* код сравнения инстанций */
}
operator MyClass = (const MyClass src) {
/* код присвоения */
return this;
}
/* другие методы */
}
</source>
Указатели