Ruby/Подробнее об ассоциативных массивах: различия между версиями

Материал из Викиучебника — открытых книг для открытого мира
Содержимое удалено Содержимое добавлено
м орфография, викификатор
Строка 1: Строка 1:
== Подробнее об ассоциативных массивах ==
== Подробнее об ассоциативных массивах ==

Различают два типа массивов: индексные, у которых в качестве индекса только целое число, и ассоциативные, где индексом может быть любой объект.
Различают два типа массивов: индексные, у которых в качестве индекса только целое число, и ассоциативные, где индексом может быть любой объект.


Строка 25: Строка 24:
'''Первый случай применимости хеша:''' если в массиве намечаются обширные незаполненные (то есть заполненные <code>nil</code>) области, то целесообразнее использовать хеш с целочисленным индексом.
'''Первый случай применимости хеша:''' если в массиве намечаются обширные незаполненные (то есть заполненные <code>nil</code>) области, то целесообразнее использовать хеш с целочисленным индексом.


Использовать хеш в данном случае лучше потому, что, формально, хеш для данного примера состоит из трёх значащих пар, а массив — из шести элементов, из которых лишь три элемента значащие. Исходя из этого, можно заключить, что массив будет хранить избыточную информацию, а хеш — только нужную.
Использовать хеш в данном случае лучше потому, что, формально, хеш для данного примера состоит из трёх значащих пар, а массив — из шести элементов, из которых лишь три элемента значащие. Исходя из этого, можно заключить, что массив будет хранить избыточную информацию, а хеш — только нужную.


Продолжим поиски случаев применимости хеша и на этот раз подсчитаем, сколько раз каждое число повторяется в данном целочисленном массиве. Решение массивом:
Продолжим поиски случаев применимости хеша и на этот раз подсчитаем, сколько раз каждое число повторяется в данном целочисленном массиве. Решение массивом:
Строка 66: Строка 65:
В данном примере было использовано два интересных приёма:
В данном примере было использовано два интересных приёма:


* Если в двумерном массиве заранее известное количество столбцов (в нашем случае — два), то каждому из столбцов (в рамках любого итератора) можно дать своё имя (в нашем случае: <code>key</code> и <code>value</code>). Если бы мы такого имени не давали, то вышеописанное решение выглядело бы так:
* Если в двумерном массиве заранее известное количество столбцов (в нашем случае — два), то каждому из столбцов (в рамках любого итератора) можно дать своё имя (в нашем случае: <code>key</code> и <code>value</code>). Если бы мы такого имени не давали, то вышеописанное решение выглядело бы так:


<source lang="ruby">array.find_all{ |array| array[0] == dns_name }[0][-1] #=> "192.168.0.3"</source>
<source lang="ruby">array.find_all{ |array| array[0] == dns_name }[0][-1] #=> "192.168.0.3"</source>


Без именования столбцов, внутри итератора вы будете работать с массивом (в двумерном массиве каждый элемент — массив, а любой итератор «пробегает» массив поэлементно). Это высказывание действительно, когда «пробежка» осуществляется по двумерному массиву.
Без именования столбцов, внутри итератора вы будете работать с массивом (в двумерном массиве каждый элемент — массив, а любой итератор «пробегает» массив поэлементно). Это высказывание действительно, когда «пробежка» осуществляется по двумерному массиву.


* Метод <code>.find_all</code> возвращает двумерный массив примерно следующего вида: <code><nowiki>[["comp1.mydomen.ru", "192.168.0.3"]]</nowiki></code>, чтобы получить строку <code>"192.168.0.3"</code> необходимо избавиться от двумерности. Делается это при помощи метода <code>[]</code>, который вызывается два раза (понижает размерность c двух до нуля). Метод <code>[0]</code> возвращает в результате — <code>["comp1.mydomen.ru", "192.168.0.3"]</code>, а метод <code>[-1]</code> — <code>"192.168.0.3"</code>. Как раз это нам и было нужно.
* Метод <code>.find_all</code> возвращает двумерный массив примерно следующего вида: <code><nowiki>[["comp1.mydomen.ru", "192.168.0.3"]]</nowiki></code>, чтобы получить строку <code>"192.168.0.3"</code> необходимо избавиться от двумерности. Делается это при помощи метода <code>[]</code>, который вызывается два раза (понижает размерность c двух до нуля). Метод <code>[0]</code> возвращает в результате — <code>["comp1.mydomen.ru", "192.168.0.3"]</code>, а метод <code>[-1]</code> — <code>"192.168.0.3"</code>. Как раз это нам и было нужно.


Теперь ту же самую задачу решим, используя хеш:
Теперь ту же самую задачу решим, используя хеш:
Строка 87: Строка 86:
Вполне естественно, что существуют и другие случаи применимости хеша, но вероятность столкнуться с ними в реальной работе намного меньше. Вышеописанных трёх случаев должно хватить надолго.
Вполне естественно, что существуют и другие случаи применимости хеша, но вероятность столкнуться с ними в реальной работе намного меньше. Вышеописанных трёх случаев должно хватить надолго.


В заключении, как и было обещанно, приводится решение задачи с использованием метода <code>.update</code>:
В заключении, как и было обещано, приводится решение задачи с использованием метода <code>.update</code>:


<source lang="ruby">array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
<source lang="ruby">array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
Строка 96: Строка 95:


=== Что используется в качестве ключей? ===
=== Что используется в качестве ключей? ===
В качестве ключей ассоциативного массива можно использовать любые типы. Например, другие ассоциативные массивы, строки, числа, символы или просто объекты любых классов.

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


Если состояние объектов-ключей изменилось, то хешу необходимо вызвать метод <code>.rehash</code>.
Если состояние объектов-ключей изменилось, то хешу необходимо вызвать метод <code>.rehash</code>.
Строка 112: Строка 110:
В данном примере ключами хеша (<code>hash</code>) являются два массива (<code>array1</code> и <code>array2</code>). Одному из них (<code>array1</code>) мы изменили нулевой элемент (с <code>"а"</code> на <code>"я"</code>). После этого доступ к значению был потерян. После выполнения метода <code>.rehash</code> всё встало на свои места.
В данном примере ключами хеша (<code>hash</code>) являются два массива (<code>array1</code> и <code>array2</code>). Одному из них (<code>array1</code>) мы изменили нулевой элемент (с <code>"а"</code> на <code>"я"</code>). После этого доступ к значению был потерян. После выполнения метода <code>.rehash</code> всё встало на свои места.


Как Ruby отслеживает изменение ключа в ассоциативном массиве? Очень просто: с помощью метода <code>.hash</code>, который генерирует «контрольную сумму» обьекта в виде целого числа. Например:
Как Ruby отслеживает изменение ключа в ассоциативном массиве? Очень просто: с помощью метода <code>.hash</code>, который генерирует «контрольную сумму» объекта в виде целого числа. Например:


<source lang="ruby">[1, 2, 3].hash #=> 25</source>
<source lang="ruby">[1, 2, 3].hash #=> 25</source>


=== Способы создания ассоциативного массива ===
=== Способы создания ассоциативного массива ===

При создании ассоциативного массива важно ответить на несколько вопросов:
При создании ассоциативного массива важно ответить на несколько вопросов:


* Какие данные имеются?
* Какие данные имеются?
* Какого типа эти данные?
* Какого типа эти данные?
* Что будет ключом, а что — значением?
* Что будет ключом, а что — значением?


Ответы определят способ создания хеша.
Ответы определят способ создания хеша.


==== Из одномерного массива ====
==== Из одномерного массива ====

Положим, что у нас в наличии индексный массив, где ключ и значение записаны последовательно. Тогда мы можем использовать связку методов <code>*</code> и <code>Hash[]</code>:
Положим, что у нас в наличии индексный массив, где ключ и значение записаны последовательно. Тогда мы можем использовать связку методов <code>*</code> и <code>Hash[]</code>:


Строка 136: Строка 132:


==== Из двумерного массива ====
==== Из двумерного массива ====

Если повезло меньше и нам достался двумерный массив с элементами вида <code>[["ключ_1", "значение_1"], ["ключ_2", "значение_2"], ["ключ_3", "значение_3"], …]</code>, то его надо сплющить (<code>.flatten</code>) и тем задача будет сведена к предыдущей:
Если повезло меньше и нам достался двумерный массив с элементами вида <code>[["ключ_1", "значение_1"], ["ключ_2", "значение_2"], ["ключ_3", "значение_3"], …]</code>, то его надо сплющить (<code>.flatten</code>) и тем задача будет сведена к предыдущей:


Строка 142: Строка 137:
Hash[*array.flatten] #=> {1=>4, 5=>3, 2=>2}</source>
Hash[*array.flatten] #=> {1=>4, 5=>3, 2=>2}</source>


Каждый нулевой элемент подмассива станет ключом, а каждый первый — значением.
Каждый нулевой элемент подмассива станет ключом, а каждый первый — значением.


Но может случиться так, что двумерный массив будет состоять из двух подмассивов: подмассива ключей и подмассива значений:
Но может случиться так, что двумерный массив будет состоять из двух подмассивов: подмассива ключей и подмассива значений:
Строка 154: Строка 149:


==== Нет данных ====
==== Нет данных ====

Если нет данных, то лучше записать хеш как пару фигурных скобок:
Если нет данных, то лучше записать хеш как пару фигурных скобок:


Строка 166: Строка 160:


==== Известен только тип значений ====
==== Известен только тип значений ====
Сведения о типе значений использовать следует так: создать хеш, в котором будет определён элемент по умолчанию. Элементом по умолчанию должен быть нулевой элемент соответствующего типа, то есть для строки это будет пустая строка (<code>""</code>), для массива — пустой массив (<code>[]</code>), а для числа — нуль (<code>0</code> или <code>0.0</code>). Это делается, чтобы к пустому элементу можно было что-то добавить и при этом не получить ошибку.

Сведения о типе значений использовать следует так: создать хеш, в котором будет определён элемент по умолчанию. Элементом по умолчанию должен быть нулевой элемент соответствующего типа, то есть для строки это будет пустая строка (<code>""</code>), для массива — пустой массив (<code>[]</code>), а для числа — нуль (<code>0</code> или <code>0.0</code>). Это делается, чтобы к пустому элементу можно было что-то добавить и при этом не получить ошибку.


<source lang="ruby">hash = Hash.new("")
<source lang="ruby">hash = Hash.new("")
Строка 184: Строка 177:


==== Всё известно и дано ====
==== Всё известно и дано ====

Если вам изначально известны все ключи и значения, то и записывайте их сразу в виде хеша, одним из способов:
Если вам изначально известны все ключи и значения, то и записывайте их сразу в виде хеша, одним из способов:


Строка 196: Строка 188:


=== Методы работы с ассоциативными массивами ===
=== Методы работы с ассоциативными массивами ===

Когда речь пойдёт о методах, которые присутствуют в ассоциативных массивах, то частенько будет возникать чувство [[w:Дежавю|дежавю]]. Во всяком случае, учить заново итераторы вам не придётся. Вполне естественно, что появятся новички, но их будет немного. Тем не менее, прилежный преподаватель первым делом представляет новичков группе. Поэтому и мы начнем с тех методов, которые будут необходимы нам при работе с ассоциативными массивами, но отсутствуют у индексных.
Когда речь пойдёт о методах, которые присутствуют в ассоциативных массивах, то частенько будет возникать чувство [[w:Дежавю|дежавю]]. Во всяком случае, учить заново итераторы вам не придётся. Вполне естественно, что появятся новички, но их будет немного. Тем не менее, прилежный преподаватель первым делом представляет новичков группе. Поэтому и мы начнем с тех методов, которые будут необходимы нам при работе с ассоциативными массивами, но отсутствуют у индексных.


==== Получение массива значений и массива ключей ====
==== Получение массива значений и массива ключей ====

Для получения отдельно массива ключей или значений существуют методы <code>.keys</code> и <code>.values</code>.
Для получения отдельно массива ключей или значений существуют методы <code>.keys</code> и <code>.values</code>.


Строка 209: Строка 199:


==== Замена ключей на значения ====
==== Замена ключей на значения ====

Чтобы поменять местами ключи и значения ассоциативного массива, следует применять метод <code>.invert</code>. Этот метод возвращает ассоциативный массив с ключами, заменёнными значениями, и значениями, заменёнными ключами.
Чтобы поменять местами ключи и значения ассоциативного массива, следует применять метод <code>.invert</code>. Этот метод возвращает ассоциативный массив с ключами, заменёнными значениями, и значениями, заменёнными ключами.


Строка 223: Строка 212:


==== Обновление пары ====
==== Обновление пары ====
Что вы делаете, если хотите обновить какую-то программу или игру? Правильно, устанавливаете апдейт. Вы не поверите, но для обновления значения в ассоциативном массиве используется метод <code>.update</code>. Странно, да? Пример использования этого метода в «боевых» условиях мы уже приводили в начале раздела. Если вы помните, то мы считали, ''сколько раз повторяется каждое число''. Наверняка, вы немного подзабыли его решение (у программистов есть привычка не помнить [[w:Константа|константы]]). Позволю себе его вам напомнить:

Что вы делаете, если хотите обновить какую-то программу или игру? Правильно, устанавливаете апдейт. Вы не поверите, но для обновления значения в ассоциативном массиве используется метод <code>.update</code>. Странно, да? Пример использования этого метода в "боевых" условиях мы уже приводили в начале раздела. Если вы помните, то мы считали, ''сколько раз повторяется каждое число''. Наверняка, вы немного подзабыли его решение (у программистов есть привычка не помнить [[w:Константа|константы]]). Позволю себе его вам напомнить:


<source lang="ruby">
<source lang="ruby">
Строка 235: Строка 223:
<source lang="ruby">result.update({i=>1}){ |key, old, new| old + new }</source>
<source lang="ruby">result.update({i=>1}){ |key, old, new| old + new }</source>


Сразу после названия метода (в нашем случае <code>.update</code>) идёт передача параметра. Страшная запись <code>{i=>1}</code> это не что иное, как ещё один хеш. Ключ его хранится в переменной <code>i</code> (счётчик итератора <code>.inject</code>), а в качестве значения выбрана единица. Зачем? Расскажу чуть позже.
Сразу после названия метода (в нашем случае <code>.update</code>) идёт передача параметра. Страшная запись <code>{i=>1}</code> — это не что иное, как ещё один хеш. Ключ его хранится в переменной <code>i</code> (счётчик итератора <code>.inject</code>), а в качестве значения выбрана единица. Зачем? Расскажу чуть позже.


Не обязательно писать именно <code>{i=>1}</code>. Можно «сократить» фигурные скобки и записать <code>i=>1</code>.
Не обязательно писать именно <code>{i=>1}</code>. Можно «сократить» фигурные скобки и записать <code>i=>1</code>.


'''Счётчик итератора''' — это переменная в которую итератор записывает текущий элемент последовательности.
'''Счётчик итератора''' — это переменная в которую итератор записывает текущий элемент последовательности.


Здесь вроде бы все понятно. Запись стала менее страшной, но всё равно вызывает дрожь. Будем это исправлять!
Здесь вроде бы все понятно. Запись стала менее страшной, но всё равно вызывает дрожь. Будем это исправлять!
Строка 249: Строка 237:
<source lang="ruby">… { … old + new } …</source>
<source lang="ruby">… { … old + new } …</source>


Всего лишь сложение <code>old</code> и <code>new</code>. Ничего не говорит? Тогда расскажу, что значат переменные <code>key</code>, <code>old</code> и <code>new</code>. В переменную <code>key</code> передаётся значение текущего ключа, в <code>old</code> — старое значение ключа ({{англ|old}} — старый), а в переменную <code>new</code> — добавляемое значение ключа ({{англ|new}} — новый).
Всего лишь сложение <code>old</code> и <code>new</code>. Ничего не говорит? Тогда расскажу, что значат переменные <code>key</code>, <code>old</code> и <code>new</code>. В переменную <code>key</code> передаётся значение текущего ключа, в <code>old</code> — старое значение ключа ({{англ|old}} — старый), а в переменную <code>new</code> — добавляемое значение ключа ({{англ|new}} — новый).


Теперь переведём запись <code>old + new</code> на русский: в случае обнаружения ключа в хеше, нам необходимо сложить старое значение с новым. Если помните, то новое значение равняется единице, то есть в случае когда ключ, хранимый в <code>i</code> уже есть в хеше <code>result</code>, то к старому значению просто добавляется единица. Вот и всё… а вы боялись.
Теперь переведём запись <code>old + new</code> на русский: в случае обнаружения ключа в хеше, нам необходимо сложить старое значение с новым. Если помните, то новое значение равняется единице, то есть в случае когда ключ, хранимый в <code>i</code> уже есть в хеше <code>result</code>, то к старому значению просто добавляется единица. Вот и всё… а вы боялись.
Строка 258: Строка 246:


==== Слияние двух массивов ====
==== Слияние двух массивов ====

Для слияния двух массивов используется метод <code>.merge</code> или <code>.merge!</code>:
Для слияния двух массивов используется метод <code>.merge</code> или <code>.merge!</code>:


Строка 269: Строка 256:


==== Размер ассоциативного массива ====
==== Размер ассоциативного массива ====

Ну вот, с новичками мы познакомились, теперь можно переходить к старым знакомым. Помните, как мы находили размер массива? Вот и с хешами точно также:
Ну вот, с новичками мы познакомились, теперь можно переходить к старым знакомым. Помните, как мы находили размер массива? Вот и с хешами точно также:


Строка 279: Строка 265:


==== Удаление пары по ключу ====
==== Удаление пары по ключу ====
О том, как добавлять элементы в массив мы знаем, а вот про удаление — нет. Необходимо это исправить. Чем мы сейчас и займёмся.

О том, как добавлять элементы в массив мы знаем, а вот про удаление — нет. Необходимо это исправить. Чем мы сейчас и займёмся.


<source lang="ruby">
<source lang="ruby">
Строка 290: Строка 275:
Как вы, наверно, уже догадались, удалением пары по ключу занимается метод <code>.delete</code>. Ему передаётся ключ от пары, которую следует удалить.
Как вы, наверно, уже догадались, удалением пары по ключу занимается метод <code>.delete</code>. Ему передаётся ключ от пары, которую следует удалить.


Метод <code>.delete</code> возвращает значение, которое соответствовало ключу в удаляемой паре. Если в хеше отсутствует пара с передаваемым ключом, то метод <code>.delete</code> возвращает <code>nil</code>. Напоминаем, что <code>nil</code> — это символ пустоты.
Метод <code>.delete</code> возвращает значение, которое соответствовало ключу в удаляемой паре. Если в хеше отсутствует пара с передаваемым ключом, то метод <code>.delete</code> возвращает <code>nil</code>. Напоминаем, что <code>nil</code> — это символ пустоты.


==== Удаление произвольной пары ====
==== Удаление произвольной пары ====

Многие программисты удивляются, когда узнаю́т, что ассоциативные массивы имеют метод <code>.shift</code>. Связано это удивление с тем, что у индексных массивов он удаляет первый элемент, возвращая его во время удаления. А вот как понять, какая пара является первой? И что такое первый в неупорядоченной последовательности пар?
Многие программисты удивляются, когда узнаю́т, что ассоциативные массивы имеют метод <code>.shift</code>. Связано это удивление с тем, что у индексных массивов он удаляет первый элемент, возвращая его во время удаления. А вот как понять, какая пара является первой? И что такое первый в неупорядоченной последовательности пар?


Строка 309: Строка 293:


==== Преобразовать в индексный массив ====
==== Преобразовать в индексный массив ====

Чуть ранее уже говорилось, что в большинстве случаев индексные массивы удобней ассоциативных.
Чуть ранее уже говорилось, что в большинстве случаев индексные массивы удобней ассоциативных.


Строка 334: Строка 317:


==== Упорядочение хеша ====
==== Упорядочение хеша ====

Да, множество пар в хеше неупорядоченно. Но это можно исправить, разве что результат потом будет не хешем, а двумерным массивом.
Да, множество пар в хеше неупорядоченно. Но это можно исправить, разве что результат потом будет не хешем, а двумерным массивом.


Строка 347: Строка 329:
Здесь мы упорядочили хеш по значению.
Здесь мы упорядочили хеш по значению.


Сначала хеш упорядочивается по ключам, а потом, в случаях равнозначных ключей при использовании <code>sort_by</code>, — по значениям.<!-- прошу прощения за множественные правки = ) -->
Сначала хеш упорядочивается по ключам, а потом, в случаях равнозначных ключей при использовании <code>sort_by</code>, — по значениям.<!-- прошу прощения за множественные правки =) -->


==== Поиск максимальной/минимальной пары ====
==== Поиск максимальной/минимальной пары ====

Максимальная пара в хеше ищется точно также, как и максимальный элемент в массиве
Максимальная пара в хеше ищется точно также, как и максимальный элемент в массиве


Строка 359: Строка 340:
но с небольшими особенностями:
но с небольшими особенностями:


* результат поиска — массив из двух элементов вида <code>[ключ, значение]</code>,
* результат поиска — массив из двух элементов вида <code>[ключ, значение]</code>,
* сначала поиск происходит по ключу, а в случае равноправных ключей при использовании <code>max_by</code> и <code>min_by</code> — по значению.
* сначала поиск происходит по ключу, а в случае равноправных ключей при использовании <code>max_by</code> и <code>min_by</code> — по значению.


Несколько больше возможностей приобрели методы <code>max_by</code> и <code>min_by</code>:
Несколько больше возможностей приобрели методы <code>max_by</code> и <code>min_by</code>:
Строка 372: Строка 353:


=== Логические методы ===
=== Логические методы ===

Работа логических методов похожа на допрос с пристрастием. Помните, как в детективах во время теста на детекторе лжи, главный герой восклицал: «Отвечать только „да“ или „нет“!» Если перевести это на язык Ruby, то это будет звучать примерно так: «Отвечать только <code>true</code> или <code>false</code>!»
Работа логических методов похожа на допрос с пристрастием. Помните, как в детективах во время теста на детекторе лжи, главный герой восклицал: «Отвечать только „да“ или „нет“!» Если перевести это на язык Ruby, то это будет звучать примерно так: «Отвечать только <code>true</code> или <code>false</code>!»


Строка 392: Строка 372:


==== Хеш пустой? ====
==== Хеш пустой? ====

Зададим вопрос «Хеш пустой?», но используя известный нам [[w:Лексикон|лексикон]]. Для начала спросим «Пустой хеш тебе не брат-близнец?»
Зададим вопрос «Хеш пустой?», но используя известный нам [[w:Лексикон|лексикон]]. Для начала спросим «Пустой хеш тебе не брат-близнец?»


Строка 424: Строка 403:


==== Есть такой ключ? ====
==== Есть такой ключ? ====

Если вам нужно узнать у хеша ответ на вопрос «есть у тебя такой ключ?», но вы не знаете как это правильно спросить, то скорее всего вы зададите вопрос в два этапа: «какие ключи у тебя есть?» и «есть среди них такой ключ?»
Если вам нужно узнать у хеша ответ на вопрос «есть у тебя такой ключ?», но вы не знаете как это правильно спросить, то скорее всего вы зададите вопрос в два этапа: «какие ключи у тебя есть?» и «есть среди них такой ключ?»


Строка 447: Строка 425:


==== Есть такое значение? ====
==== Есть такое значение? ====

Давайте подумаем, как задать вопрос «есть такое значение?» хешу. Скорее всего, мы опять зададим вопрос в два этапа: «какие значения есть?» и «есть ли среди них нужное нам?»
Давайте подумаем, как задать вопрос «есть такое значение?» хешу. Скорее всего, мы опять зададим вопрос в два этапа: «какие значения есть?» и «есть ли среди них нужное нам?»


Строка 462: Строка 439:


=== Итераторы ===
=== Итераторы ===

У ассоциативных массивов есть следующие итераторы:
У ассоциативных массивов есть следующие итераторы:


* <code>.find_all</code> — поиск всех элементов, которые удовлетворяют логическому условию;
* <code>.find_all</code> — поиск всех элементов, которые удовлетворяют логическому условию;
* <code>.map</code> — изменение всех элементов по некоторому алгоритму;
* <code>.map</code> — изменение всех элементов по некоторому алгоритму;
* <code>.inject</code> — сложение, перемножение и агрегация элементов массива.
* <code>.inject</code> — сложение, перемножение и агрегация элементов массива.


Набор итераторов точно такой же, как и у индексных массивов — сказывается их родство. Вот только ведут себя они несколько иначе:
Набор итераторов точно такой же, как и у индексных массивов — сказывается их родство. Вот только ведут себя они несколько иначе:


* Результатом является двумерный массив (как после метода <code>.to_a</code>).
* Результатом является двумерный массив (как после метода <code>.to_a</code>).
Строка 513: Строка 489:
Ассоциативный массив, как и индексный массив, имеет метод <code>.map</code>, который передаёт замыканию ключ и соответствующее ему значение. При этом в замыкание на самом деле передаётся массив с ключом и значением, но Ruby «разворачивает» их в две переменные при передаче замыканию.
Ассоциативный массив, как и индексный массив, имеет метод <code>.map</code>, который передаёт замыканию ключ и соответствующее ему значение. При этом в замыкание на самом деле передаётся массив с ключом и значением, но Ruby «разворачивает» их в две переменные при передаче замыканию.


Итератор <code>.map</code>, в свою очередь, возвращает индексный массив с результатами замыкания — по элементу массива на каждый ключ:
Итератор <code>.map</code>, в свою очередь, возвращает индексный массив с результатами замыкания — по элементу массива на каждый ключ:


<source lang="ruby">
<source lang="ruby">
Строка 523: Строка 499:


=== Хитрости ===
=== Хитрости ===

Одному программисту надоело писать <code>hash["key"]</code> и он захотел сделать так, чтобы можно было написать <code>hash.key</code>.
Одному программисту надоело писать <code>hash["key"]</code> и он захотел сделать так, чтобы можно было написать <code>hash.key</code>.


Строка 540: Строка 515:


=== Задачи ===
=== Задачи ===

# Дан массив слов. Необходимо подсчитать, сколько раз встречается каждое слово в массиве.
# Дан массив слов. Необходимо подсчитать, сколько раз встречается каждое слово в массиве.



Версия от 16:05, 28 декабря 2018

Подробнее об ассоциативных массивах

Различают два типа массивов: индексные, у которых в качестве индекса только целое число, и ассоциативные, где индексом может быть любой объект.

Информация

Индексные массивы чаще всего называют просто «массивами», а ассоциативные массивы — «хешами» или «словарями».

Хеши можно представить как массив пар: ключ=>значение. Но в отличие от массива, хеш неупорядочен: нельзя заранее сказать, какая пара будет первой, а какая последней. Правда, удобство использования массива это не умаляет. Более того, поскольку в Ruby переменные не типизированы и методам с похожей функциональностью дают похожие имена, то использование хеша чаще всего равносильно использованию массива.

Несмотря на мощь хеша, использовать его не всегда целесообразно. Бывают задачи, решаемые с хешами легко и удобно, но таких задач мало. Чаще всего хватает использования массива. Но представление о классе задач для хеша надо иметь.

Давайте создадим хеш, где в качестве ключа будем использовать целое число:

hash = {5=>3, 1=>6, 3=>2}
hash[5]                      #=> 3
hash[2]                      #=> nil  это значит, что объект отсутствует
hash[3]                      #=> 2

А вот так будет выглядеть та же самая программа, если мы будем использовать массив:

array = [nil, 6, nil, 2, nil, 3]
array[5]                            #=> 3
array[2]                            #=> nil
array[3]                            #=> 2

Первый случай применимости хеша: если в массиве намечаются обширные незаполненные (то есть заполненные nil) области, то целесообразнее использовать хеш с целочисленным индексом.

Использовать хеш в данном случае лучше потому, что, формально, хеш для данного примера состоит из трёх значащих пар, а массив — из шести элементов, из которых лишь три элемента значащие. Исходя из этого, можно заключить, что массив будет хранить избыточную информацию, а хеш — только нужную.

Продолжим поиски случаев применимости хеша и на этот раз подсчитаем, сколько раз каждое число повторяется в данном целочисленном массиве. Решение массивом:

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.uniq.map{ |i| [i, array.find_all{ |j| j == i }.size] }    #=> [[1, 3], [2, 4], [3, 1], [4, 1], [5, 1]]

Алгоритм получается ужасным. Не буду утомлять излишними терминами, а замечу, что по одному и тому же массиву итераторы (в количестве двух штук) пробегают много раз. А ведь достаточно одной «пробежки». Понятное дело, что такая программа не сделает вам чести. В качестве упражнения, предлагаю вам решить эту задачу другим, более оптимальным, способом.

Теперь рассмотрим решение этой же задачи, но с применением хеша:

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.inject(Hash.new{ 0 }){ |result, i|
    result[i] += 1
    result
}                                         #=> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}

Удалось избавиться от лишних методов и обойтись лишь одной «пробежкой» итератора по массиву.

Начальный хеш был создан хитроумной комбинацией Hash.new{0}, что в переводе на русский означает примерно следующее: «создадим пустой хеш, в котором любому несуществующему ключу будет соответствовать 0». Это нужно, чтобы суммирование (метод +) не выдавало ошибку вида: «не могу сложить nil и число типа Fixnum». В качестве упражнения, предлагаю вам заменить комбинацию Hash.new{ 0 } на {} и посмотреть, чем это чревато.

Зачем нужно дописывать result? Дело в том, что комбинация result[i] += 1 имеет в качестве результата целое число (учитывая, что массив целочисленный), а не хеш. Следовательно, параметру result автоматически будет присвоено целое число (см. описание итератора .inject). На следующей итерации мы будем обращаться к result, как к хешу, хотя там уже будет храниться число. Хорошо, если программа выдаст ошибку, а если нет? Проверьте это самостоятельно.

В качестве упражнения, предлагаю вам переписать программу без вышеописанных двух особенностей (используйте метод .update). Решение будет опубликовано ниже.

Второй случай применимости хеша: если требуется подсчитать число элементов массива, то целессобразнее применять хеш. Кстати, вместо подсчета количества, можно использовать конкатенацию массивов или строк. Но это уже более сложные задачи, которые будут рассмотрены позже.

Теперь представим, что мы работаем системными администраторами. У нас есть список DNS-имён и IP-адреса. Каждому DNS-имени соответствует только один IP-адрес. Как нам это соответствие записать в виде программы? Попробуем это сделать при помощи массива:

array = [["comp1.mydomen.ru", "192.168.0.3"], 
         ["comp2.mydomen.ru", "192.168.0.1"],
         ["comp3.mydomen.ru", "192.168.0.2"]]

Всё бы ничего, но чтобы найти IP-адрес по DNS имени, придётся перелопатить весь массив в поиске нужного DNS:

dns_name = "comp1.mydomen.ru"
array.find_all{ |key, value| key == dns_name }[0][-1]    #=> "192.168.0.3"

В данном примере было использовано два интересных приёма:

  • Если в двумерном массиве заранее известное количество столбцов (в нашем случае — два), то каждому из столбцов (в рамках любого итератора) можно дать своё имя (в нашем случае: key и value). Если бы мы такого имени не давали, то вышеописанное решение выглядело бы так:
array.find_all{ |array| array[0] == dns_name }[0][-1]    #=> "192.168.0.3"

Без именования столбцов, внутри итератора вы будете работать с массивом (в двумерном массиве каждый элемент — массив, а любой итератор «пробегает» массив поэлементно). Это высказывание действительно, когда «пробежка» осуществляется по двумерному массиву.

  • Метод .find_all возвращает двумерный массив примерно следующего вида: [["comp1.mydomen.ru", "192.168.0.3"]], чтобы получить строку "192.168.0.3" необходимо избавиться от двумерности. Делается это при помощи метода [], который вызывается два раза (понижает размерность c двух до нуля). Метод [0] возвращает в результате — ["comp1.mydomen.ru", "192.168.0.3"], а метод [-1] — "192.168.0.3". Как раз это нам и было нужно.

Теперь ту же самую задачу решим, используя хеш:

hash = {"comp1.mydomen.ru"=>"192.168.0.3", 
        "comp2.mydomen.ru"=>"192.168.0.1",
        "comp3.mydomen.ru"=>"192.168.0.2"}
hash["comp1.mydomen.ru"]                        #=> "192.168.0.3"

Нет ни одного итератора и, следовательно, не сделано ни одной «пробежки» по массиву.

Третий случай применимости хеша: когда требуется сопоставить один набор данных с другим, то целесообразнее использовать хеш.

Вполне естественно, что существуют и другие случаи применимости хеша, но вероятность столкнуться с ними в реальной работе намного меньше. Вышеописанных трёх случаев должно хватить надолго.

В заключении, как и было обещано, приводится решение задачи с использованием метода .update:

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.inject({}){ |result, i| result.update({ i=>1 }){ |key, old, new| old+new }}
    #=> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}

Описание метода .update будет дано ниже. На данном этапе попытайтесь угадать принцип работы метода .update.

Что используется в качестве ключей?

В качестве ключей ассоциативного массива можно использовать любые типы. Например, другие ассоциативные массивы, строки, числа, символы или просто объекты любых классов.

Если состояние объектов-ключей изменилось, то хешу необходимо вызвать метод .rehash.

array1 = ["а", "б"]
array2 = ["в", "г"]
hash = {array1=>100, array2=>300}
hash[array1]                         #=> 100
array1[0] = "я"
hash[array1]                         #=> nil
hash.rehash                          #=> {["я", "б"]=>100, ["в", "г"]=>300}
hash[array1]                         #=> 100

В данном примере ключами хеша (hash) являются два массива (array1 и array2). Одному из них (array1) мы изменили нулевой элемент (с "а" на "я"). После этого доступ к значению был потерян. После выполнения метода .rehash всё встало на свои места.

Как Ruby отслеживает изменение ключа в ассоциативном массиве? Очень просто: с помощью метода .hash, который генерирует «контрольную сумму» объекта в виде целого числа. Например:

[1, 2, 3].hash    #=> 25

Способы создания ассоциативного массива

При создании ассоциативного массива важно ответить на несколько вопросов:

  • Какие данные имеются?
  • Какого типа эти данные?
  • Что будет ключом, а что — значением?

Ответы определят способ создания хеша.

Из одномерного массива

Положим, что у нас в наличии индексный массив, где ключ и значение записаны последовательно. Тогда мы можем использовать связку методов * и Hash[]:

array = [1, 4, 5, 3, 2, 2]
Hash[*array]                  #=> {1=>4, 5=>3, 2=>2}

Элементы, стоящие на нечётной позиции (в данном случае: 1, 5 и 2) стали ключами, а элементы, стоящие на чётной позиции (то есть: 4, 3 и 2), стали значениями.

Из двумерного массива

Если повезло меньше и нам достался двумерный массив с элементами вида [["ключ_1", "значение_1"], ["ключ_2", "значение_2"], ["ключ_3", "значение_3"], …], то его надо сплющить (.flatten) и тем задача будет сведена к предыдущей:

array = [[1, 4], [5, 3], [2, 2]]
Hash[*array.flatten]                #=> {1=>4, 5=>3, 2=>2}

Каждый нулевой элемент подмассива станет ключом, а каждый первый — значением.

Но может случиться так, что двумерный массив будет состоять из двух подмассивов: подмассива ключей и подмассива значений:

[["ключ_1", "ключ_2", "ключ_3", ], ["значение_1", "значение_2", "значение_3", ]]

Вспоминаем методы работы с массивами. Там был метод .transpose (транспонирование массива), вызов которого сведёт задачу к предыдущей.

array = [[1, 5, 2], [4, 3, 2]]
Hash[*array.transpose.flatten]    #=> {1=>4, 5=>3, 2=>2}

Нет данных

Если нет данных, то лучше записать хеш как пару фигурных скобок:

hash = {}
hash[1] = 4
hash[5] = 3
hash[2] = 2
hash           #=> {1=>4, 5=>3, 2=>2}

И уже по ходу дела разобраться, что к чему.

Известен только тип значений

Сведения о типе значений использовать следует так: создать хеш, в котором будет определён элемент по умолчанию. Элементом по умолчанию должен быть нулевой элемент соответствующего типа, то есть для строки это будет пустая строка (""), для массива — пустой массив ([]), а для числа — нуль (0 или 0.0). Это делается, чтобы к пустому элементу можно было что-то добавить и при этом не получить ошибку.

hash = Hash.new("")
hash["песенка про зайцев"] += "В тёмно-синем лесу, "
hash["песенка про зайцев"] += "где трепещут осины"
hash    #=> {"песенка про зайцев"=>"В темно-синем лесу, где трепещут осины"}

Или ещё пример:

hash = Hash.new(0)
hash["зарплата"] += 60
hash["зарплата"] *= 21
hash      #=> {"зарплата"=>1260}

Но, как известно, из любого правила есть исключение: использовать нулевой элемент, когда значение будет записываться умножением, нежелательно. Потому как, даже не будучи пророком, можно предсказать результат. Он будет равен нулю.

Всё известно и дано

Если вам изначально известны все ключи и значения, то и записывайте их сразу в виде хеша, одним из способов:

{"март"=>400, "январь"=>350, "февраль"=>200}  #=> на выходе такой же текст
{fox: 1, wolf: 2, dragon: 3}                  #=> {:fox=>1, :wolf=>2, :dragon=>3} обратите внимание на знак ':', он говорит что fox - это не строка, 
                                              #   а чтото вроде перечисления (Enum), как в языке Си.

Не изобретайте велосипед и поступайте как можно проще.

Методы работы с ассоциативными массивами

Когда речь пойдёт о методах, которые присутствуют в ассоциативных массивах, то частенько будет возникать чувство дежавю. Во всяком случае, учить заново итераторы вам не придётся. Вполне естественно, что появятся новички, но их будет немного. Тем не менее, прилежный преподаватель первым делом представляет новичков группе. Поэтому и мы начнем с тех методов, которые будут необходимы нам при работе с ассоциативными массивами, но отсутствуют у индексных.

Получение массива значений и массива ключей

Для получения отдельно массива ключей или значений существуют методы .keys и .values.

{1=>4, 5=>3, 2=>2}.keys      #=> [1, 2, 5]
{1=>4, 5=>3, 2=>2}.values    #=> [4, 3, 2]

Ассоциативные массивы в Ruby неупорядоченны: массивы могут иметь любой порядок элементов.

Замена ключей на значения

Чтобы поменять местами ключи и значения ассоциативного массива, следует применять метод .invert. Этот метод возвращает ассоциативный массив с ключами, заменёнными значениями, и значениями, заменёнными ключами.

hash = {"первый ключ"=>4, "второй ключ"=>5}
hash.invert                                    #=> {4=>"первый ключ", 5=>"второй ключ"}

Поскольку ключи в ассоциативных массивах уникальны, то ключи с одинаковыми значениями будут отброшены:

hash = {"первый ключ"=>10, "второй ключ"=>10}
hash.invert                                      #=> {10=>"второй ключ"}

Небольшая хитрость: hash.invert.invert возвратит нам хеш с уникальными значениями.

Обновление пары

Что вы делаете, если хотите обновить какую-то программу или игру? Правильно, устанавливаете апдейт. Вы не поверите, но для обновления значения в ассоциативном массиве используется метод .update. Странно, да? Пример использования этого метода в «боевых» условиях мы уже приводили в начале раздела. Если вы помните, то мы считали, сколько раз повторяется каждое число. Наверняка, вы немного подзабыли его решение (у программистов есть привычка не помнить константы). Позволю себе его вам напомнить:

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5]
array.inject({}){ |result, i| result.update({i=>1}){ |key, old, new| old + new } }
    #=> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}

Страшноватая запись. Поэтому будем разбирать её по частям.

result.update({i=>1}){ |key, old, new| old + new }

Сразу после названия метода (в нашем случае .update) идёт передача параметра. Страшная запись {i=>1} — это не что иное, как ещё один хеш. Ключ его хранится в переменной i (счётчик итератора .inject), а в качестве значения выбрана единица. Зачем? Расскажу чуть позже.

Не обязательно писать именно {i=>1}. Можно «сократить» фигурные скобки и записать i=>1.

Счётчик итератора — это переменная в которую итератор записывает текущий элемент последовательности.

Здесь вроде бы все понятно. Запись стала менее страшной, но всё равно вызывает дрожь. Будем это исправлять!

 { |key, old, new|  } 

Раньше мы не встречались с такой записью. Но ничего страшного в ней нет. Это что-то вроде по́ля боя. Нам выдали вооружение и необходимо провести некий манёвр. В нашем случае, арсенал у нас внушительный: key, old и new. Бой начинается при некоторых условиях. Наш бой начнется, когда при добавлении очередной пары (переданной в предыдущей части страшной записи) обнаружится, что такой ключ уже есть в хеше. Нам предлагается описать наши действия именно в таком случае. Что же это за действия?

 {  old + new } 

Всего лишь сложение old и new. Ничего не говорит? Тогда расскажу, что значат переменные key, old и new. В переменную key передаётся значение текущего ключа, в old — старое значение ключа (англ. old — старый), а в переменную new — добавляемое значение ключа (англ. new — новый).

Теперь переведём запись old + new на русский: в случае обнаружения ключа в хеше, нам необходимо сложить старое значение с новым. Если помните, то новое значение равняется единице, то есть в случае когда ключ, хранимый в i уже есть в хеше result, то к старому значению просто добавляется единица. Вот и всё… а вы боялись.

Информация

Рекомендуется перечитать данную главу ещё раз, так как вы её немного не поняли.

Интересно, сколько читателей сможет прочитать эту строку и не зациклиться на предыдущей?

Слияние двух массивов

Для слияния двух массивов используется метод .merge или .merge!:

hash1 = {3 => "a", 4 => "c"}
hash2 = {5 => "r", 7 => "t"}
hash1.merge!(hash2)                   #=> {5=>"r", 7=>"t", 3=>"a", 4=>"c"}

Если во втором массиве ключ будет совпадать с каким-либо ключем из первого массива, значение будет заменено на значение из второго массива.

Размер ассоциативного массива

Ну вот, с новичками мы познакомились, теперь можно переходить к старым знакомым. Помните, как мы находили размер массива? Вот и с хешами точно также:

hash = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
hash.size                                #=> 5

Стоит уточнить, что если в индексных массивах под размером понимается количество элементов, то в ассоциативном массиве это количество пар вида ключ=>значение. В остальном же это наш старый добрый .size.

Удаление пары по ключу

О том, как добавлять элементы в массив мы знаем, а вот про удаление — нет. Необходимо это исправить. Чем мы сейчас и займёмся.

hash = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
hash.delete(5)                           #=> 1
hash                                     #=> {1=>3, 2=>4, 3=>1, 4=>1}
hash.delete(5)                           #=> nil

Как вы, наверно, уже догадались, удалением пары по ключу занимается метод .delete. Ему передаётся ключ от пары, которую следует удалить.

Метод .delete возвращает значение, которое соответствовало ключу в удаляемой паре. Если в хеше отсутствует пара с передаваемым ключом, то метод .delete возвращает nil. Напоминаем, что nil — это символ пустоты.

Удаление произвольной пары

Многие программисты удивляются, когда узнаю́т, что ассоциативные массивы имеют метод .shift. Связано это удивление с тем, что у индексных массивов он удаляет первый элемент, возвращая его во время удаления. А вот как понять, какая пара является первой? И что такое первый в неупорядоченной последовательности пар?

Ответ кроется в отсутствии метода-напарника .pop, так как если нельзя удалить последний элемент, то под .shift понимается удаление произвольной пары. Вот такое вот нехитрое доказательство.

Давайте посмотрим его в действии:

hash = {5=>3, 1=>6, 3=>2}
hash.shift                   #=> [5, 3]
hash                         #=> {1=>6, 3=>2}

Обратите внимание, что метод .shift возвращает удаляемую пару в виде индексного массива [ключ, значение].

Не стоит обольщаться по поводу того, что метод .shift возвращает первую пару. Помните, что ассоциативные массивы неупорядоченны.

Преобразовать в индексный массив

Чуть ранее уже говорилось, что в большинстве случаев индексные массивы удобней ассоциативных.

Информация

Некоторые программисты утверждают, что при больших объёмах данных лучше использовать двумерный индексный массив. Получается примерно то же, что и хеш (лишь поиск элемента по ключу осуществить сложнее), но обычно программа работает быстрей.

Мнение авторов таково, что у программиста на Ruby есть более благородные пути времяпровождения, чем заниматься такой вот псевдооптимизационной ерундой.

Чтобы преобразовать ассоциативный массив в индексный, надо использовать метод to_a. Его используют все, кто не может запомнить методов работы с хешами.

hash = {"гаечный ключ"=>10, "разводной ключ"=>22}
hash.to_a    #=> [["гаечный ключ", 10], ["разводной ключ", 22]]

Способ преобразования таков. Сперва пары (ключ=>значение) преобразуются в массив:

{["гаечный ключ"=>10], ["разводной ключ"=>22]}

Затем «стрелку» заменяем на запятую:

{["гаечный ключ", 10], ["разводной ключ", 22]}

и фигурные скобки выпрямляем, так что теперь их можно заправить в стэплер.

[["гаечный ключ", 10], ["разводной ключ", 22]]

Упорядочение хеша

Да, множество пар в хеше неупорядоченно. Но это можно исправить, разве что результат потом будет не хешем, а двумерным массивом.

hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.sort    #=> [["гаечный ключ", 4], ["разводной ключ", 10]]

В методе .sort_by передаются два значения:

hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.sort_by{ |key, value| value } #=> [["гаечный ключ", 4], ["разводной ключ", 10]]

Здесь мы упорядочили хеш по значению.

Сначала хеш упорядочивается по ключам, а потом, в случаях равнозначных ключей при использовании sort_by, — по значениям.

Поиск максимальной/минимальной пары

Максимальная пара в хеше ищется точно также, как и максимальный элемент в массиве

hash = {"гаечный ключ"=>10, "разводной ключ"=>22}
hash.max    #=> ["разводной ключ", 22]
hash.min    #=> ["гаечный ключ"  , 10]

но с небольшими особенностями:

  • результат поиска — массив из двух элементов вида [ключ, значение],
  • сначала поиск происходит по ключу, а в случае равноправных ключей при использовании max_by и min_by — по значению.

Несколько больше возможностей приобрели методы max_by и min_by:

hash = {"гаечный ключ"=>10, "разводной ключ"=>22}
hash.max_by{ |key, value| value }                    #=> ["разводной ключ", 22]
hash.min_by{ |array| array[0] }                      #=> ["гаечный ключ"  , 10]

Также, как и в методе sort_by есть возможность по разному получать текущую пару: в виде массива или двух переменных.

Логические методы

Работа логических методов похожа на допрос с пристрастием. Помните, как в детективах во время теста на детекторе лжи, главный герой восклицал: «Отвечать только „да“ или „нет“!» Если перевести это на язык Ruby, то это будет звучать примерно так: «Отвечать только true или false

В детективах набор вопросов стандартен:

  • Знали ли вы мистера X?
  • Вы были на месте преступления?
  • Убивали ли мистера X?

На Ruby примерно тоже самое:

  • Ты пустой?
  • Есть ли такой элемент?
  • Ты массив?
  • Уверен, что не строка?

Но давайте рассмотрим их подробней.

Хеш пустой?

Зададим вопрос «Хеш пустой?», но используя известный нам лексикон. Для начала спросим «Пустой хеш тебе не брат-близнец?»

empty_hash  = {}
filled_hash = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}

empty_hash  == {}    #=> true
filled_hash == {}    #=> false

Можно спросить по другому: «Размер у тебя не нулевой?»

empty_hash  = {}
filled_hash = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}

empty_hash .size.zero?    #=> true
filled_hash.size.zero?    #=> false

Но давайте будем задавать правильные вопросы

empty_hash  = {}
filled_hash = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}

empty_hash .empty?    #=> true
filled_hash.empty?    #=> false

а то ещё примут нас за приезжих…

Обратите внимание, что метод .empty? полностью повторяет такой же метод у индексных массивов.

Есть такой ключ?

Если вам нужно узнать у хеша ответ на вопрос «есть у тебя такой ключ?», но вы не знаете как это правильно спросить, то скорее всего вы зададите вопрос в два этапа: «какие ключи у тебя есть?» и «есть среди них такой ключ?»

pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.keys.include?("гаечный")    #=> true

В данном примере у нас в pocket нашёлся "гаечный" ключ.

Но лучше задавать вопрос напрямую, это покажет ваше прекрасное знание языка.

pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.key?("гаечный")    #=> true

или в стиле индексных массивов

pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.include?("гаечный")    #=> true

Это несколько сократит первоначальное предложение, но тогда можно перепутать хеш с массивом.

Этот же вопрос можно задать методами: .member? и .has_key?.

Есть такое значение?

Давайте подумаем, как задать вопрос «есть такое значение?» хешу. Скорее всего, мы опять зададим вопрос в два этапа: «какие значения есть?» и «есть ли среди них нужное нам?»

pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.values.include?("гаечный")       #=> false — ой, забыл сменить
pocket.values.include?("английский")    #=> true

Но аборигены говорят иначе и задают вопрос напрямую:

pocket = {"гаечный"=>20, "замочный"=>"английский", "разводной"=>34}
pocket.value?("английский")    #=> true

Задать вопрос «Есть такое значение?» можно не только при помощи метода .value?, но и при помощи более длинного .has_value?.

Итераторы

У ассоциативных массивов есть следующие итераторы:

  • .find_all — поиск всех элементов, которые удовлетворяют логическому условию;
  • .map — изменение всех элементов по некоторому алгоритму;
  • .inject — сложение, перемножение и агрегация элементов массива.

Набор итераторов точно такой же, как и у индексных массивов — сказывается их родство. Вот только ведут себя они несколько иначе:

  • Результатом является двумерный массив (как после метода .to_a).
  • В качестве счётчика (переменной в фотографии) передаётся массив вида [ключ, значение].
  • Можно развернуть массив вида [ключ, значение] в две переменные.
  • В итераторе .inject развернуть массив не получится.

Рассматривать заново работу каждого итератора в отдельности скучно. Поэтому мы будем рассматривать работу всех итераторов сразу.

hash = {"гаечный ключ"=>4, "разводной ключ"=>10}

hash.find_all{ |array| array[1] < 5 }
    #=> [["гаечный ключ", 4]]

hash.map { |array| "#{array[0]} на #{array[1]}" }
    #=> ["гаечный ключ на 4", "разводной ключ на 10"]

hash.inject(0){ |result, array| result + array[1] }
    #=> 14

Обратите внимание на то, что в качестве счётчика передаётся массив из двух элементов. В наших примерах счётчик итератора мы назвали array. В своих программах вы вольны называть его как угодно.

Есть подозрение, что перед работой любого из итераторов вызывается метод .to_a. Уж больно работа итераторов в хешах напоминает работу с двумерным массивом.

Теперь посмотрим, как можно развернуть array в две переменные. Делается это простой заменой array на key, value:

hash = {"гаечный ключ"=>4, "разводной ключ"=>10}

hash.find_all{ |key, value| value < 5 }
    #=> [["гаечный ключ", 4]]

hash.map{ |key, value| "#{key} на #{value}" }
    #=> ["гаечный ключ на 4", "разводной ключ на 10"]

hash.inject(0){ |result, key, value| result + value }
    #=> Ошибка в методе "+": невозможно сложить nil и число типа Fixnum

Обратите внимание, что развёртка массива прошла успешно только в первых двух итераторах. В третьем возникла ошибка. Давайте выясним, откуда там взялся nil. Дело в том, что развернуть массив не удалось, и теперь он стал называться не array, а key. Переменная value осталась «не у дел», и ей присвоилось значение nil. Чтобы это исправить, достаточно поставить круглые скобки:

hash.inject(0){ |result, (key, value)| result + value }
    #=> 14

Ассоциативный массив, как и индексный массив, имеет метод .map, который передаёт замыканию ключ и соответствующее ему значение. При этом в замыкание на самом деле передаётся массив с ключом и значением, но Ruby «разворачивает» их в две переменные при передаче замыканию.

Итератор .map, в свою очередь, возвращает индексный массив с результатами замыкания — по элементу массива на каждый ключ:

hash = {"гаечный ключ"=>4, "разводной ключ"=>10}
hash.map { | key, value | "#{key} на #{value}" }    #=> ["гаечный ключ на 4", "разводной ключ на 10"]
hash.map                                            #=> [["гаечный ключ", 4], ["разводной ключ", 10]]

Итератор .map, вызванный без аргументов, аналогичен методу .to_a: просто раскладывает хеш в двумерный массив.

Хитрости

Одному программисту надоело писать hash["key"] и он захотел сделать так, чтобы можно было написать hash.key.

class Hash
    def method_missing(id)
        self[id.id2name]
    end
end

hash = {"hello"=>"привет", "bye"=>"пока"}
hash.hello    #=> "привет"
hash.bye      #=> "пока"

Естественно, что ключи в таком хеше могут содержать только латиницу, символ подчёркивания и цифры (везде, кроме первого символа). Иначе говоря, удовлетворять всем требованиям, которые мы предъявляем к именам методов и именам переменных.

Задачи

  1. Дан массив слов. Необходимо подсчитать, сколько раз встречается каждое слово в массиве.