kstep 26.02.2011 14:56

CodingВведение в Lua для начинающих программистов — часть 2.1

В предыдущей части я рассказал о самых простых типах данных в Lua, а здесь я хочу рассказать о довольно странном универсальном типе данных-контейнере — таблицах.



Массивы+хеши=...

В Lua нет списков. Нет в нём и массивов, равно как и словарей/хеш-таблиц, структур, перечислений (enum-ов), множеств, кортежей и объектов. Да всё это и не нужно, потому что в нём есть таблицы, в точнее тип данных table. Что это такое? Пхпшникам это наверное понять будет проще всего, потому что таблицы в Lua очень похожи на массивы в PHP. Таблица объединяет в себе свойста массива и словаря. (Говоря по секрету, при создании таблицы как раз резервируется две отдельные структуры: массив для данных, индексируемых по индексу, и хеш-таблица, индексируемая по произвольному текстовому ключу.) Соответственно пока возможно Lua хранит данные в таблице как в массиве (т.к. доступ к данным в массиве очень быстр, порядка O(1)), и только по необходимости переключается на хранение в хеш-таблице (то есть при использовании нечисловых ключей, или числовых ключей, идущих не подряд один за другим, так что в массиве может получиться «дырка»).

Индексация данных в таблице, используемой как массив, начинается (сюрприз-сюрприз!) с единицы, той самой, которая 1.

Теперь определимся с синтаксисом:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
my_first_table = {}
my_first_table.field = "value"
print(my_first_table<'field'>) -- распечатает "value"

my_other_table = {"one", "two", "three"}
print(my_other_table<1>) -- распечатает "one"

my_hash_table = {key1 = "value1", key2 = "value2", <"key three"> = "value3"}
print(my_hash_table<"key three">) -- распечатает "value3"
my_hash_table<"key three"> = "value5"
print(my_hash_table<"key three">, my_hash_table.key2) -- распечатает "value5" и "value2"

my_mixed_table = {key1 = "value1", "value2", <"other key"> = "value3", key2 = "value4", <5> = "value5", "value6"}
-- таблица состоит из пар: "key1"=>"value1", 1=>"value2", "other key"=>"value3", "key2"=>"value4", 5=>"value5", 2=>"value6"



Итак конструктор таблиц — фигурные скобки { и }. Обращаться к элементам массива можно либо через квадратные скобки (my_other_table<2>, my_mixed_table<"other key">), или через точку (my_hash_table.key2). Обратите внимание, что через точку можно обращаться только к элементам таблицы, ключи для которых являются корректными идентификаторами, то есть не начинаются на число и состоят только из латинских букв, цифр и знака подчёркивания, во всех остальных случаях надо использовать квадратные скобки (включая индексацию по числам в стиле массива).

Инициировать таблицу можно прямо в конструкторе, при этом через запятую передаются либо пары ключ=значение, либо просто значения. Обратите внимание на единство синтаксиса: здесь точно так же можно задавать ключи без кавычек только если они являются корректными идентификаторами, иначе их надо писать как нормальные литералы и заключать в квадратные скобки.

Ещё надо заметить то, как Lua выдаёт последовательные индексы элементам таблицы, для которых не задан ключ. Нумерация начинаются с единицы (нулевой элемент можно задать явно через <0>="value") и увеличивается с каждым новым значением без ключа (см. таблицу my_mixed_table в примере). Тут явно видна двойственность таблиц в Lua: объявление элементов таблицы в конструкторе без ключа добавляет их в «массивную» часть внутренней структуры table, а если задан ключ, то значение с этим ключём кладётся в хеш-часть таблицы. При этом счётчик элементов в массиве действует независимо от явно заданных ключей и имеет над ними приоритет. Например вот такая запись может сбить с толку:

1
2
3
4
-- Обе эти таблицы будут содержать массив
-- {10, 20, 30, 40}
table1 = {10, 20, 30, <4>=41, 40}
table2 = {10, 20, 30, 40, <4>=41}



Проитерировть перебрать все элементы массива можно с помощью специальной разновидности цикла for, который мы видели в прошлой статье. Для этого я познакомлю вас с итераторами. Подробно про итераторы я расскажу в следующей статье, здесь я только покажу их использование на практике. Для итерации по элементам таблицы в Lua есть две функции: pairs() и ipairs(). Первая итерирует по обеим частям таблицы: сначала по массиву, потом по хеш-таблице, вторая — только по «массивной» части. Поскольку лучше один раз увидеть, я сразу перейду к примеру:

1
2
3
4
my_mixed_table = {key1 = "value1", "value2", <"other key"> = "value3", key2 = "value4", <5> = "value5", "value6"}
for k, v in pairs(my_mixed_table) do
print(k, v)
end



Результат:

1 value2
2 value6
key1 value1
5 value5
other key value3
key2 value4


1
2
3
4
my_mixed_table = {key1 = "value1", "value2", <"other key"> = "value3", key2 = "value4", <5> = "value5", "value6"}
for k, v in ipairs(my_mixed_table) do
print(k, v)
end



Результат:

1 value2
2 value6


Здесь мы видим, что pairs() и ipairs() выдают в цикле пары ключ-значение. (На самом деле это специальные функции-итераторы, но про них я расскажу немного позже, а тем, кому интересно, могут почитать про них дополнительно.) Здесь же мы столкнулись с новой специальной ипостасью forfor vars in func() do ... end. Сейчас можете просто принять эту форму как есть и использовать её с функциями pairs()/ipairs().

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

Получить число элементов в «массивной» части таблицы можно с помощью операции "#":

1
2
mytable = {10, 20, 30}
print(#mytable) -- будет 3



Так что цикл for k, v in ipairs(table) можно заменить на такой цикл:
1
2
3
4
for k in 1, #mytable do
print(k, mytable<k>)
end
</k>



Ещё один немаловажный момент напоследок: хранить в таблице значения nil нужно с большой опаской, т.к. с точки зрения pairs(), ipairs() и # такие элементы не существуют и обращаться к ним получится только напрямую по ключам, если специально знать, что они по таким ключам есть. Это так потому, что значение nil будут иметь и те элементы таблицы, которые вы никогда не инициализировали. Например:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ lua
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
&gt; table1 = {10, 20, nil, 40, 50}
&gt; print(#table1)
5
&gt; for k, v in ipairs(table1) do print(k, v) end
1 10
2 20
&gt; for k, v in pairs(table1) do print(k, v) end
1 10
2 20
4 40
5 50
&gt; ------------------------------------------------------
&gt; table2 = {10, 20, nil, 40, key1=50, key2=nil, key3=60}
&gt; print(#table2)
4
&gt; for k, v in ipairs(table2) do print(k, v) end
1 10
2 20
&gt; for k, v in pairs(table2) do print(k, v) end
1 10
2 20
4 40
key1 50
key3 60
&gt; ------------------------------------------------------
&gt; table3 = {10, 20, nil, 40, 50, nil}
&gt; print(#table3)
2
&gt; for k, v in ipairs(table3) do print(k, v) end
1 10
2 20
&gt; for k, v in pairs(table3) do print(k, v) end
1 10
2 20
4 40
5 50



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

ipairs() всегда итерирует по «массивной» части таблицы до первого nil, pairs() просто пропускает все элементы, которые имеют значение nil. А вот оператор длины на самом деле выдаёт такое число, что ни один из следующих по порядку элементов из «массивной» части таблицы не равен nil. Такая вот особая лунная магия (лунная потому, что Lua по португальски означает «луна»).

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

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
&gt; table4 = {1,nil,3,key1=nil,key2=10}
&gt; print(table4<1>)
1
&gt; print(table4<2>)
nil
&gt; print(table4<4>)
nil
&gt; print(table4<10>)
nil
&gt; print(table4.key2)
10
&gt; print(table4<"key1">)
nil
&gt; print(table4.key10)
nil



Теперь, надеюсь, ясно, почему такое странное поведение с элементами, равными nil: функции Lua не видят разницы, между неопределёнными элементами таблицы и элеметами, явно инициализированными значением nil. Кстати, так можно удалять любые переменные и элементы таблицы: просто присвойте им значение nil, и они вернутся в небытиё, из которого вышли (а за их призраками, которые остались в прошлом, прийдёт злобный лангольер гарбадж-коллектор).

Итог:

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

В следующей серии нашего сериала статье я расскажу много интересного о тайной жизни функций в Lua.


Тэги: lua luaintro программирование
+ 15 -
Похожие Поделиться

Shtsh 27.02.2011 23:00 #
Честно говоря, не понял отличие таблиц от ассоциативных массивов или хешей.
Но интересно :)
kstep 27.02.2011 23:33 #
А его практически нет. По сути это микс двух концепций, то есть это одновременно и массив и хеш, как в PHP, только реализация использует более эффективный по памяти механизм хранения.
Shtsh 27.02.2011 23:34 #
Ясно, спасибо
GalS 02.03.2011 10:43 #
1. Не совсем понятно, как работает "оператор длины".
>print(#{10, 20, nil, 40, 50})
5
>print(#{10, 20, nil, 40, 50, nil})
2???

2. Существует ли возможность перечислить элементы хэш-таблицы?

PS
оператор длинны
kstep 02.03.2011 20:07 #
Проитерировать перебрать все элементы массива можно с помощью специальной разновидности цикла for, который мы видели в прошлой статье. Для этого я познакомлю вас с итераторами. Подробно про итераторы я расскажу в следующей статье, здесь я только покажу их использование на практике. Для итерации по элементам таблицы в Lua есть две функции: pairs() и ipairs(). Первая итерирует по обеим частям таблицы: сначала по массиву, потом по хеш-таблице, вторая — только по «массивной» части. Поскольку лучше один раз увидеть, я сразу перейду к примеру:


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

kstep 02.03.2011 20:12 #
http://www.lua.org/manual/5.1/manual.html
The length of a table t is defined to be any integer index n such that t is not nil and t is nil; moreover, if t<1> is nil, n can be zero. For a regular array, with non-nil values from 1 to a given n, its length is exactly that n, the index of its last value. If the array has "holes" (that is, nil values between other non-nil values), then #t can be any of the indices that directly precedes a nil value (that is, it may consider any such nil value as the end of the array).
kstep 02.03.2011 20:16 #
Вольный перевод:

Длина таблицы определяется как индекс n такой, что t не является nil, а вот t уже nil.
Если t<1> == nil, то #t может быть нулём. Обычно если массив содержит не-nil значения первого по n-й элементы, то длина массива равна этому n, то есть индексу последнего не-nil элемента. Если же в массиве есть «дырки» (nil-ы между не nil-ами), то #t может быть любым индексом элемента, который стоит перед элементом со значением nil. Другими словами оператор длины может принять любой nil за конец массива.
GalS 03.03.2011 08:33 #
ага, спасибо
supermax 14.05.2013 09:00 #
Правильно ли я понимаю, что ipairs у нас для перебора в таблице, по сути одномерной:
test1 = {10, 20, 30, 40}
когда перебор идёт по порядку, т.к. присвоены индексы 1, 2, 3, 4 соответственно. А pairs некий микст.
Но вот у меня тут стоИт глобальная задача к примеру на такого рода таблицах:
test1 = {[0] = 20, [3] = 17, [5] = 12}
Делать последовательный перебор по возрастанию по ключам: [key]=value. ipairs-ом неправильно делать (пародия двумерного массива у меня), а pairs-ом перебирает не в том порядке котором я хочу ключ по возрастанию, а по тем, по ктороым он построил хэш. Неужели в lua не реально сделать перебор по ключу с возрастанием, а не в хаотичном порядке, согласно хэш-таблице :(
kstep 15.05.2013 01:28 #
Нативно такого нет. Ты можешь с помощью pairs собрать ключи массива в другой массив, отсортировать его с помощью table.sort() и итерировать по нему с помощью ipairs, получая ключи по порядку.
kstep 15.05.2013 14:07 #
Если хочется большей эффективности, то я бы предложил лучше продумать используемые структуры данных. Например вот ты говоришь о «пародии двухмерного массива». Может рассмотреть структуру вроде массива массивов?
supermax 15.05.2013 14:17 #
Да, спасибо за такой вариант. Я сделал немного по-другому.
test1 = {[0] = 20, [3] = 17, [5] = 12}
Сделал разбивку на 2 таблицы каждой такой мега таблицы.
test1_a = {0, 3, 5}
test1_b = {20, 17, 12}
И т.к. одномерный делаю последовательный перебор ipairs-ом по 1-й (мне как раз упорядоченные key-и нужны были), и потом когда нужный выбрал, беру из test1_b, с таким же ключом, как и в первой таблице. Да, немного жирней структура становится, зато ту коллизию решил-)
P.S. Со структурами не знаком, пишут скрипты для логики игровых дополнений, так далеко в Lua не погружался, да и думаю не понадобится, но спасибо за подсказку :)