Коллекции
Предпосылки: Циклы (while), Функции (определение, вызов, параметры, возврат значения).
final_total(subtotal, discount, tax_rate) хорошо считает один заказ продавца. Но за день маркетплейс принимает тридцать заказов, и до сих пор каждый новый заказ значит ещё одну именованную переменную:
subtotal_1 = 2400
subtotal_2 = 1800
subtotal_3 = 3500
subtotal_4 = 900
subtotal_5 = 2100
# ... ещё 25 таких же строк ниже
total_1 = final_total(subtotal_1, 150, 0.10)
total_2 = final_total(subtotal_2, 0, 0.10)
total_3 = final_total(subtotal_3, 200, 0.10)
# ... ещё 27 вызовов с рукамиУродство видно сразу. Имена различаются только номером, и тридцать почти одинаковых строк хочется обойти циклом — но цикл не умеет перебирать subtotal_1, subtotal_2, subtotal_3. Для него это тридцать независимых имён, а не один набор. Нужна структура, в которой тридцать значений живут как одно целое.
Массив
Массив (array — «ряд», «набор») хранит упорядоченную последовательность значений, к каждому из которых можно обратиться по номеру. Номер называется индексом и начинается с нуля:
subtotals = [2400, 1800, 3500, 900, 2100] # и так далее до 30-го
puts subtotals[0] # 2400
puts subtotals[2] # 3500Тридцать значений теперь лежат в одной переменной. Индекс i — обычное целое число, а значит, по массиву можно пройти циклом:
i = 0
while i < subtotals.length # .length — количество элементов
puts final_total(subtotals[i], 150, 0.10)
i = i + 1
endВместо тридцати параллельных имён — одно имя и индекс. Как массив устроен в памяти и почему обращение по индексу стоит одной операции — в массиве.
В C массив объявляется с фиксированным размером:
int subtotals[5] = {2400, 1800, 3500, 900, 2100};Если заказов станет шесть, код нужно менять: размер зашит в объявление.
Динамический массив
В реальной программе количество заказов заранее неизвестно. Сегодня их три, завтра тридцать, на распродаже — триста. Фиксировать размер наперёд нельзя.
В Ruby массивы динамические: можно начать с пустого и добавлять элементы по мере поступления:
subtotals = []
subtotals << 2400 # << добавляет в конец
subtotals << 1800
subtotals << 3500Когда места внутри не хватает, массив сам расширяется. Как именно он растёт и почему добавление в конец в среднем стоит одной операции — в динамическом массиве.
Итерация
Итерация (от лат. iterare — «повторять») — один проход по элементам коллекции. Массив плюс цикл вместе закрывают задачу, с которой начиналась заметка: посчитать сумму по всем заказам одним правилом, независимо от их количества.
subtotals = [2400, 1800, 3500, 900, 2100]
sum = 0
i = 0
while i < subtotals.length
sum = sum + subtotals[i]
i = i + 1
end
puts sum # 10700Один цикл вместо тридцати почти одинаковых строк. И если завтра заказов станет триста — код останется тем же, поменяется только содержимое массива.
Хеш
Массив работает, пока значения различаются только порядком: первый заказ, второй, третий. Но у самого заказа внутри — разнородные поля: сумма, скидка, ставка налога, имя покупателя. Их естественно искать не по номеру, а по имени.
Если держать заказ как два параллельных массива, связь между ними приходится удерживать в голове:
fields = ["subtotal", "discount", "tax_rate"]
values = [2400, 150, 0.10]values[1] — это скидка или ставка? Ответ зависит от порядка, и при любом изменении структуры параллельные массивы начинают расходиться — ровно как subtotal_1, subtotal_2 в начале заметки, только в другой плоскости.
Хеш (hash — «нарезать, перемешать»: ключ превращается во внутренний адрес, где лежит значение) хранит пары ключ -> значение:
order = {
"subtotal" => 2400,
"discount" => 150,
"tax_rate" => 0.10
}
puts order["subtotal"] # 2400
puts order["discount"] # 150Теперь структура сама говорит, что хранится внутри: не нужно помнить, что скидка идёт второй. Как хеш устроен внутри и почему поиск по ключу в среднем не зависит от размера — в хеш-таблице.
Вложенные структуры
Коллекции комбинируются. Массив заказов, где каждый заказ — хеш с полями, закрывает ту самую задачу «тридцать заказов маркетплейса», с которой начиналась заметка:
def final_total(subtotal, discount, tax_rate)
taxable = subtotal - discount
taxable + (taxable * tax_rate)
end
orders = [
{ "customer" => "Alice", "subtotal" => 2400, "discount" => 150, "tax_rate" => 0.10 },
{ "customer" => "Bob", "subtotal" => 1800, "discount" => 0, "tax_rate" => 0.10 },
{ "customer" => "Charlie", "subtotal" => 3500, "discount" => 200, "tax_rate" => 0.10 }
]
i = 0
while i < orders.length
order = orders[i]
total = final_total(order["subtotal"], order["discount"], order["tax_rate"])
puts order["customer"]
puts total
i = i + 1
endФункция по-прежнему считает один заказ; коллекция хранит сразу много заказов; цикл прогоняет функцию по всем. Три инструмента, сложенные вместе, дают одну рабочую конструкцию — сколько бы заказов ни пришло.
Остаётся странность, которую заметка обошла. После copy = orders в обоих именах окажется один и тот же список заказов или две независимые копии? Если добавить новый заказ через copy, увидит ли его orders? Ответ не в самом массиве, а в том, как переменные и данные лежат в памяти.
Sources
- Thomas, D. et al., 2023, Programming Ruby 3.3. Pragmatic Bookshelf.
- Knuth, D., 1998, The Art of Computer Programming, Vol. 3: Sorting and Searching. Addison-Wesley.