Шаг 8
Этот шаг логически никак не связан с предыдущими шагами, но тема, разобранная в нем, очень важна.
Сегодня немного поговорим о сериализации. Часто в программе нужно
сохранить результаты её жизнедеятельности, а затем их прочитать
обратно. В книге Иерусалимского "Programming in Lua" есть код
сериализации таблиц (взято здесь):
function basicSerialize (o)
if type(o) == "number" then
return tostring(o)
else -- assume it is a string
return string.format("%q", o)
end
end
function save (name, value, saved)
saved = saved or {} -- initial value
io.write(name, " = ")
if type(value) == "number" or type(value) == "string" then
io.write(basicSerialize(value), "\n")
elseif type(value) == "table" then
if saved[value] then -- value already saved?
io.write(saved[value], "\n") -- use its previous name
else
saved[value] = name -- save name for next time
io.write("{}\n") -- create a new table
for k,v in pairs(value) do -- save its fields
local fieldname = string.format("%s[%s]", name,
basicSerialize(k))
save(fieldname, v, saved)
end
end
else
error("cannot save a " .. type(value))
end
end
Он позволяет сохранять таблицы с циклическими ссылками, вида:
a = {x=1, y=2; {3,4,5}}
a[2] = a -- cycle
a.z = a[1] -- shared sub-table
Функция save сохраняет таблицу в виде текста в текущий поток вывода io.
Если он не настроен, то, по умолчанию, вывод производится на экран. У
этой функции есть небольшой недостаток - она не сохраняет булевские
переменные. Этот недостаток легко устранить, добавив пару проверок. Так
же есть смысл оформить сериализатор как отдельный модуль Serializer.lua:
local base = _G
module('Serializer')
-- модуль для сохранения таблицы в файл
-- могут быть сохранены переменные типа число, строка или булевский тип
-- таблица сохраняется в текущий поток вывода
-- если вы хотите, чтобы сохранение производилось в файл,
-- то поток вывода Lua должен быть предварительно настроен, например так:
-- открываем файл для записи данных
-- local f = io.open("data.lua", "w")
-- сохраняем предыдущий поток вывода
-- local prevOutput = io.output()
-- устанавливаем вывод стандартного потока в наш файл
-- io.output(f)
-- сериализация...
-- закрываем файл
-- f:close()
-- восстанавливаем предыдущий поток вывода
-- io.output(prevOutput)
-- ...
-- проверка, что переменная может быть сохранена как строка
-- т.е это число, строка или булевский тип)
local function isValidType(valueType)
return "number" == valueType or
"boolean" == valueType or
"string" == valueType
end
-- конвертация переменной в строку
local function valueToString (value)
local valueType = base.type(value)
if "number" == valueType or "boolean" == valueType then
result = base.tostring(value)
else -- assume it is a string
-- обратите внимание на флаг "%q"!
-- этот флаг правильно обрабатывает строки,
-- содержащие в себе кавычки и другие управляющие символы
result = base.string.format("%q", value)
end
return result
end
function save (name, value, saved)
saved = saved or {} -- initial value
base.io.write(name, " = ")
local valueType = base.type(value)
if isValidType(valueType) then
base.io.write(valueToString(value), "\n")
elseif "table" == valueType then
if saved[value] then -- value already saved?
base.io.write(saved[value], "\n") -- use its previous name
else
saved[value] = name -- save name for next time
base.io.write("{}\n") -- create a new table
for k,v in base.pairs(value) do -- save its fields
-- добавляем проверку ключа таблицы
local keyType = base.type(k)
if isValidType(keyType) then
local fieldname = base.string.format("%s[%s]", name, valueToString(k))
save(fieldname, v, saved)
else
base.error("cannot save a " .. keyType)
end
end
end
else
base.error("cannot save a " .. valueType)
end
end
То, что добавил я, прокомментировано по русски. Пишем тест, для проверки сериализатора serializerTest.lua:
local serializer = require("Serializer")
local prev = io.output()
local f = io.open("data.lua", "w")
io.output(f)
a = {x=1, y=2; {3,4,5}, true, false, "zzz"}
a.self = a -- cycle
a.z = a[1] -- shared sub-table
serializer.save("a", a)
f:close()
io.output(prev)
На выходе получаем такой файл data.lua:
a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
a[2] = true
a[3] = false
a[4] = "zzz"
a["y"] = 2
a["x"] = 1
a["self"] = a
a["z"] = a[1]
Вполне похоже на правду. Теперь проверим, как поведёт себя
сериализатор, если таблица будет содержать недопустимые типы данных. В
таблицу а добавим поле типа функция:
a["function"] = function () end
В ответ получили ожидаемую ошибку (скопировано из консоли):
..\bin\lua.exe: .\Serializer.lua:67: cannot save a function
stack traceback:
[C]: in function 'error'
.\Serializer.lua:67: in function 'save'
.\Serializer.lua:60: in function 'save'
serializerTest.lua:11: in main chunk
[C]: ?
Теперь наоборот, добавим текстовое поле с ключом типа функция:
a[function () end] = "function"
Отлично, Lua ругнулась теперь уже на другую строчку (скопировано из консоли):
..\bin\lua.exe: .\Serializer.lua:62: cannot save a function
stack traceback:
[C]: in function 'error'
.\Serializer.lua:62: in function 'save'
serializerTest.lua:12: in main chunk
[C]: ?
На этом первую часть шага можно считать законченной. Мы научились сохранять таблицу в файл.
Теперь нам нужно её оттуда прочитать (более подробно о функциях загрузки можно прочитать здесь).
Нет ничего проще:
dofile("data.lua")
Чудесно! В глобальном пространстве имён у нас появилась некая таблица a,
которая, возможно, затёрла ранее существующую таблицу а, в которой
у нас хранились необычайно важные данные.
Вариантов решения проблемы два.
Первый способ загрузки файла требует некоторой предварительной подготовки при сохранении файла.
Нужно дописать в начале файла перед именем таблицы "local " (без кавычек), а
в конце файла дописать "return имя_таблицы". Этим способом пользуюсь я сам, как наиболее очевидным. Код сохранения таблицы примет следующий вид:
local f = io.open("data.lua", "w")
-- сохраняем предыдущий поток вывода
local prevOutput = io.output()
-- устанавливаем вывод стандартного потока в наш файл
io.output(f)
io.write('local ') -- добавлено
-- сериализация...
io.write('\n') -- добавлено
io.write('return a') -- добавлено
io.write('\n') -- добавлено
-- закрываем файл
f:close()
-- восстанавливаем предыдущий поток вывода
io.output(prevOutput)
Сам файл с данными станет выглядеть так:
local a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
a[2] = true
a[3] = false
a[4] = "zzz"
a["y"] = 2
a["x"] = 1
a["self"] = a
a["z"] = a[1]
return a
Загрузка файла с данными осуществляется очень просто:
local result = dofile('data.lua')
В этом случае нам совершенно нет нужды знать имя таблицы, с которым она хранится в файле.
При использовании dofile
следует помнить, что если файл отсутствует, то Lua упадёт с сообщением
об ошибке. В таком случае следует использовать функцию loadfile, которая ошибок не генерирует. Функция loadfile возвращает функцию, при помощи которой можно выполнить загруженный чанк (как это будет по-рюсски?), или nil и сообщение об ошибке, если загрузить чанк не удалось.
local result
local func, errorMsg = loadfile(fileName)
if func then
result = func()
else
print(errorMsg)
end
На самом деле, в реальной жизни все обычно пользуются функцией dofile.
Все-таки программист обычно уверен в том, что уж его-то скрипт всегда
будет лежать на нужном месте :) Чтобы защититься от падений при вызове dofile, можно воспользоваться функцией pcall:
pcall (f, arg1, ···)
Функция pcall вызывает функцию f с аргументами, переданными в параметрах и возвращает результат успешности вызова функции. Если функция f была вызвана успешно, то остальные возвращаемые значения pcall являются результатом вызова функции f, иначе второе возвращаемое значение является сообщением об ошибке. Например:
local callResult, result = pcall(dofile, fileName)
if callResult then
-- все в порядке, result это то, что вернула функция dofile
else
-- result это сообщение об ошибке
print(result)
end
Второй способ загрузки файла использует переключение окружения и уже знакомую функцию loadfile:
-- загружаем чанк из файла
local result
local func, errorMsg = loadfile('data2.lua')
if func then
-- создаем таблицу для окружения,
-- в котором будет выполняться функция func
local P = {}
-- устанавливаем окружение для функции func
setfenv(func, P)
-- вызываем функцию func
func()
-- все "глобальные" переменные,
-- объявленные внутри чанка, попадут в таблицу P
result = P.a
else
print(errorMsg)
end
Пишем тест для проверки первого способа readData1.lua:
-- убедимся, что нет глобальной переменной с именем a
print('before a = ', a)
local zzz = dofile('data1.lua')
-- убедимся, что глобальная переменная с именем a не появилась
print('after a = ', a)
-- распечатаем загруженную таблицу:
for k, v in pairs(zzz) do
print(k, v)
end
Результат (скопировано из консоли):
..\bin\lua.exe readData1.lua
before a = nil
after a = nil
1 table: 003295C0
2 true
3 false
4 zzz
y 2
x 1
self table: 00329468
z table: 003295C0
Пишем тест для проверки второго способа readData2.lua:
-- убедимся, что нет глобальной переменной с именем a
print('before a = ', a)
-- загружаем чанк из файла
local result
local func, errorMsg = loadfile('data2.lua')
if func then
-- создаем таблицу для окружения,
-- в котором будет выполняться функция func
local P = {}
-- устанавливаем окружение для функции func
setfenv(func, P)
-- вызываем функцию func
func()
-- все "глобальные" переменные,
-- объявленные внутри чанка, попадут в таблицу P
result = P.a
else
print(errorMsg)
end
-- убедимся, что глобальная переменная с именем a не появилась
print('after a = ', a)
-- распечатаем загруженную таблицу:
for k, v in pairs(result) do
print(k, v)
end
Результат (скопировано из консоли):
..\bin\lua.exe readData2.lua
before a = nil
after a = nil
1 table: 0032C0E0
2 true
3 false
4 zzz
y 2
x 1
self table: 0032C0B8
z table: 0032C0E0
На этом, как мне кажется, сегодня можно остановиться. Файлы урока можно взять здесь.
Удачи!
Назад