Шаг 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

На этом, как мне кажется, сегодня можно остановиться. Файлы урока можно взять здесь.
Удачи!

Назад

Hosted by uCoz