Статьи

Руководство по коллекциям Ruby, часть I: массивы

collections_arrays

Программирование состоит в основном из сортировки и поиска. В более старом языке, таком как C, вы могли бы написать свои собственные структуры данных и алгоритмы для этих задач. Однако в Ruby эти конструкции были абстрагированы в пользу способности сосредоточиться на поставленной задаче.

Далее следует руководство к этим абстракциям. Она не является полностью исчерпывающей — целая книга может быть написана на коллекциях Ruby, — но я разыскиваю широкую сеть и рассказываю о том, что, как мне кажется, вы часто будете встречать как программист на Ruby. Он разбит на 4 части:

  1. Массивы и итерация
  2. Хэши, наборы и диапазоны
  3. Перечислимый и Перечислитель
  4. Хитрости и советы

Это очень пример руководства. Я думаю, что лучший способ изучить этот материал — это просто открыть оболочку irb и следовать по ней, создавая собственные умные вариации по пути.

Массивы

Массивы являются рабочими лошадками коллекций Ruby. В результате большинство методов, работающих с коллекциями, возвращают массив, даже если исходная коллекция не была таковой. По правде говоря, они на самом деле не являются массивами, а представляют собой единую структуру данных, подходящую для всех. Вы можете заставить их действовать как наборы, стеки или очереди. Они являются функциональным эквивалентом списков Python.

Творчество

Массивы Ruby создаются аналогично массивам других динамических языков.

>> numbers = [1, 0, 7]
>> numbers[2]
=> 7
>> numbers.size
=> 3

Массивы не обязательно должны состоять из одного и того же типа данных. Они могут быть разнородными.

 >> data = ["bob", 3, 0.931, true]

Поскольку Ruby полностью объектно-ориентирован, массивы представляются как объекты, а не просто как специальные правила интерпретатора. Это означает, что вы можете построить их как другие объекты.

 >> numbers = Array.new([1,2,3]) 
=> [1, 2, 3]

Конструктору массива может быть передан начальный размер, но он может работать не так, как вы ожидаете. Поскольку массивы Ruby являются динамическими, нет необходимости предварительно выделять для них место. Когда вы сами передаете число в Array # new, создается массив с таким количеством объектов nil.

 >> numbers = Array.new(3)
=> [nil, nil, nil]

Хотя nil является одноэлементным объектом, он занимает слот в коллекциях, как и любой другой объект. Поэтому, когда вы добавляете элемент в массив из nil объектов, он прикрепляется к концу.

 >> numbers << 3
=> [nil, nil, nil, 3]
>> numbers.size
=> 4

Если вы передаете Array # new второй аргумент, он становится значением заполнения вместо nil.

 >> Array.new(3, 0)
=> [0, 0, 0] 

>> Array.new(3, :DEFAULT)
=> [:DEFAULT, :DEFAULT, :DEFAULT]

В дополнение к стандартному литералу Ruby предоставляет некоторые другие ярлыки синтаксиса через нотацию% .

 >> ORD = "ord"
>> %W{This is an interpolated array of w#{ORD}s}
=> ["This", "is", "an", "interpolated", "array", "of", "words"] 

>> %w{This is a non-interpolated array of w#{ORD}s}
=> ["This", "is", "a", "non-interpolated", "array", "of", "w\#{ORD}s"]

Индексы массива

Большинство языков выдают исключение, если вы пытаетесь получить доступ к индексу массива, который еще не существует. Если вы попытаетесь прочитать несуществующий индекс, Ruby вернет ноль.

 >> spanish_days = []
>> spanish_days[3]
=> nil

Если вы пишете в несуществующий индекс, Ruby вставит nil в массив до этого индекса.

 >> spanish_days[3] = "jueves"  
=> [nil, nil, nil, "jueves"] 
>> spanish_days[6] = "domingo" 
=> [nil, nil, nil, "jueves", nil, nil, "domingo"]

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

 >> ["a","b","c"][-1]
=> "c"
>> ["a","b","c"][-3]
=> "a"

Если вы укажете несуществующий отрицательный индекс массива, результат будет таким же, как у несуществующего положительного индекса — ноль

 >> ["a","b","c"][-4]
=> nil

Диапазоны массивов

Еще одна полезная особенность массивов Ruby — возможность доступа к диапазонам элементов. Тем не менее, они могут быть хитрыми, учитывая, что существует множество различных способов указать диапазон элементов в Ruby.

 >> letters = %w{a b c d e f}
=> ["a", "b", "c", "d", "e", "f"]
>> letters[0..1]
=> ["a", "b"]
>> letters[0, 2]
=> ["a", "b"]
>> letters[0...2]
=> ["a", "b"]
>> letters[0..-5]
=> ["a", "b"]
>> letters[-6, 2]
=> ["a", "b"]

Вот причины этих примеров:

  1. буквы [0..1] — дайте мне элементы от 0 до 1
  2. буквы [0, 2] — начиная с индекса 0, дайте мне 2 элемента
  3. буквы [0… 2] — дайте мне элементы от 0 до 2
  4. буквы [0 ..- 5] — дайте мне элементы от 0 до -5
  5. буквы [-6, 2] — начиная с элемента -6, дайте мне 2 элемента

Если вы новичок в Ruby, вам может быть интересно, как это вообще возможно. Оказывается, доступ к массиву — это не что иное, как вызов метода # [].

 >> letters.[](0..1)
=> ["a", "b"]

Кроме того, 0..1 — это не что иное, как замаскированный объект Range. Вы можете проверить это с помощью метода #class.

 >> (0..1).class
=> Range

Так что на самом деле происходит, объект Range, представляющий целевой диапазон элементов, передается в Array # [].

 >> letters.[](Range.new(0,1))
=> ["a", "b"]

Эта объектно-ориентированная природа Ruby позволяет нам делать довольно сумасшедшие вещи, если нам это нравится. Что, если мы хотим использовать числа в форме английских слов?

Gem Numberrouno может использоваться для анализа английских чисел.

 $ gem install numerouno

>> require 'numerouno'
>> "one-hundred sixty three".as_number
=> 163

Теперь с Numberrouno вы можете создать класс массива, который принимает английские индексы.

 class EnglishArray < Array
  def [](idx)     
    if String === idx
      self.at(idx.as_number)  
    end
  end
end 

>> arr = EnglishArray.new(["a","b","c"])
>> arr["one"]
=> "b"

преобразование

Помните, как я сказал, что массив Ruby является универсальной структурой данных? Вот несколько примеров операций, которые вы можете выполнять с массивом.

Добавление элементов

 >> [1,2,3] << "a" 
=> [1,2,3,"a"]

>> [1,2,3].push("a")
=> [1,2,3,"a"]

>> [1,2,3].unshift("a")
=> ["a",1,2,3]

>> [1,2,3] << [4,5,6]
=> [1,2,3,[4,5,6]]

Удаление элементов

 >> arr = [1,2,3]
>> arr.pop
=> 3
>> arr
=> [1,2]

>> arr = ["a",1,2,3]
>> arr.shift
=> "a"
>> arr
=> [1,2,3]

>> arr = [:a, :b, :c]
>> arr.delete(:b)
=> :b
>> arr
=> [:a, :c]
>> arr.delete_at(1)
=> :c
>> arr
=> [:a]

Объединение массивов

 >> [1,2,3] + [4,5,6]
=> [1,2,3,4,5,6]

>> [1,2,3].concat([4,5,6])
=> [1,2,3,4,5,6]

>> ["a",1,2,3,"b"] - [2,"a","b"]
=> [1,3]

Булевы операции

 >> [1,2,3] & [2,3,4]
=> [2,3]

>> [1,2,3] | [2,3,4]
=> [1,2,3,4]

>> arr1 = [1,2,3]
>> arr2 = [2,3,4]
>> xor = arr1 + arr2 - (arr1 & arr2)
=> [1,4]

Движущиеся элементы

 >> [1,2,3].reverse
=> [3,2,1]

>> [1,2,3].rotate
=> [2,3,1]

>> [1,2,3].rotate(-1)
=> [3,1,2]

Обеспечение безопасности

 >> arr = [1,2,3]
>> arr.freeze
>> arr << 4  
=> RuntimeError: can't modify frozen Array

Объединение элементов в строку

 >> words = ["every","good","boy","does","fine"]
>> words.join
=> "everygoodboydoesfine"

>> words.join(" ")
=> "every good boy does fine"

Удаление вложенности

 >> [1,[2,3],[4,["a", nil]]].flatten
=> [1,2,3,4,"a",nil]

>> [1,[2,3],[4,["a", nil]]].flatten(1)
=> [1,2,3,4,["a", nil]]

Удаление Дубликатов

 >> [4,1,2,1,5,4].uniq
=> [4,1,2,5]

нарезка

 >> arr = [1,2,3,4,5]
>> arr.first(3)
  => [1,2,3]

>> arr.last(3)
=> [3,4,5]

Запросы

 >> ["a","b","c"].include? "d"
=> false

>> ["a", "a", "b"].count "a"
=> 2

>> ["a", "a", "b"].count "b"
=> 1

>> [1,2,[3,4]].size
=> 3

итерация

Итерация — это область, где Ruby действительно сияет. Во многих языках итерация выглядит неловко. Тем не менее, в Ruby вы никогда не должны чувствовать необходимость писать классический цикл for.

Центральной конструкцией в итерации Ruby является метод #each.

 >> ["first", "middle", "last"].each { |i| puts i.capitalize }
First
Middle
Last

Хотя #each является основным итератором в Ruby, есть много других. Например, вы можете перебирать коллекцию в обратном направлении, используя #reverse_each.

 >> ["first", "middle", "last"].reverse_each { |i| puts i.upcase }
LAST
MIDDLE
FIRST

Другой удобный метод — #each_with_index, который передает текущий индекс в качестве второго аргумента в блок.

 >> ["a", "b", "c"].each_with_index do |letter, index| 
>>   puts "#{letter.upcase}: #{index}"
>> end
A: 0
B: 1
C: 2

Как именно этот метод #each работает? Лучший способ понять это — создать свой собственный #each.

 class Colors
  def each
    yield "red"
    yield "green"
    yield "blue"
  end
end

>> c = Colors.new
>> c.each { |i| puts i }
red
green
blue

Метод #yield вызывает блок, который вы передаете в #each. Многим новичкам в рубинах может быть трудно обернуть голову вокруг. Думайте о #yield как о вызове анонимного тела кода, который вы предоставляете методу, в котором находится #yield. В предыдущем примере yield вызывается три раза, поэтому «{| i | ставит я} »работает три раза.

Частичная итерация

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

Если вам нужно перебрать только часть коллекции, есть по крайней мере несколько способов сделать это:

Нарезать коллекцию, а затем перебрать срез

 >> [1,2,3,4,5][0..2].each { |i| puts i }
1
2
3

Используйте диапазон для генерации индексов

 >> arr = [1,2,3,4,5]
>> (0..2).each { |i| puts arr[i] }
1
2
3

Хотя это немного уродливее, я готов поспорить, что вариант 2 более эффективен, поскольку размеры элементов массива увеличиваются и копирование занимает больше времени.

#each vs. # map / # collect

В дополнение к #each, вы, скорее всего, будете также часто встречать #map. #map похож на #each за исключением того, что он строит массив из результатов каждого вызова блока. Это полезно, потому что #each возвращает только вызывающего.

 >> [1,2,3].each { |i| i + 1 }
=> [1,2,3]

>> [1,2,3].map { |i| i + 1 }
=> [2,3,4]

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

 >> [1,2,3].map(&:to_f)
=> [1.0, 2.0, 3.0]

Который так же, как:

 >> [1,2,3].map { |i| i.to_f }
=> [1.0, 2.0, 3.0]

Обратите внимание, что #map не изменяет исходную коллекцию. Он просто возвращает массив на основе результата каждого вызова блока. Если вы хотите, чтобы оригинальная коллекция отражала изменения, используйте #map! вместо.

 >> numbers = [1,2,3]
>> numbers.map(&:to_f)
=> [1.0, 2.0, 3.0]
>> numbers
=> [1, 2, 3]
>> numbers.map!(&:to_f)
=> [1.0, 2.0, 3.0]
>> numbers
=> [1.0, 2.0, 3.0]

Возможно, вы также заметили #collect в коде Ruby. Он идентичен #map, поэтому
оба являются взаимозаменяемыми.

 >> letters = ["a", "b", "c"]
>> letters.map(&:capitalize)
=> ["A", "B", "C"]
>> letters.collect(&:capitalize)
=> ["A", "B", "C"]

Классическая Итерация

Ruby предоставляет классическую идиому «для». Хотя он выглядит чище и более знаком с новичками из других языков, он не идиоматичен, поскольку не является объектно-ориентированным и не принимает блок.

 >> animals = ["cat", "dog", "bird", "chuck testa"]
>> for animal in animals
>>   puts animal.upcase
>> end
CAT
DOG
BIRD
CHUCK TESTA

В случае, если «не-идиоматика» не является достаточным сдерживающим фактором для вас, вот неинтуитивный результат, продемонстрированный Nikals B. на stackoverflow :

 >> results = []
>> (1..3).each { |i| results << lambda { i } }
>> results.map(&:call)
=> [1, 2, 3]

>> results = []
>> for i in 1..3
>>   results << lambda { i }
>> end
>> result.map(&:call)
=> [3, 3, 3]

Кроме того, «временные» переменные, созданные в циклах for, вовсе не являются временными. Эта переменная животного, используемая в животных для цикла выше, теперь исчезла, верно?

Нет.

 >> animal
=> "chuck testa"

Заключение: Ruby for Loop — лучше всего используется для того, чтобы вызывать наглазники или выражения озабоченности со стороны аудитории конференции.