Вывод данных в консоль в Lua: от print до io.write

Опубликовано:

Краткое содержание статьи:
  • В Lua для вывода в консоль чаще используется функция print.
  • Под капотом она вызывает tostring для всех типов данных.
  • Метаметод __tostring переопределяет поведение tostring. Пример кастомизации вывода таблицы.
  • Почему в Lua конкатенация строк выполняется через двойную точку (..).
  • Функция print при выводе разделяет аргументы табуляцией, а не пробелом.
  • Форматирование вывода осуществляется с помощью функции string.format.
  • Функция print всегда добавляет переход на новую строку.
  • Для тонкой настройки вывода на экран, его динамического изменения используется функция io.write.
  • В паре с io.write нередко вызывается io.flush для выталкивания символов из буфера памяти.
  • Особенности io.write: отсутствие приведения типов, аналог метода file:write для вывода в файл.

В Lua в большинстве случаев вывод на экран выполняется с помощью функции print.

local f = -0.258
local s = "Hello"
print(f)         -- -0.258
print(s)         -- Hello
print(102 + 39)  -- 141
print(false)     -- false
print(nil)       -- nil

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

local a = 10
local b = tostring(a)  -- Если надо сохранить данные в виде строки
print(a)  -- Преобразование было 
print(b)  -- Преобразования не было

Функция tostring возвращает таблицы в виде строк типа "table: 0x...", в которых указан адрес объекта в памяти. Чтобы вывод таблицы кастомизировать (настроить по своему), в ее метатаблицу добавляется метаметод __tostring. Он будет срабатывать при передаче таблицы в tostring.

local user = { name = "Luka", age = 35 }

print(user)  -- table: 0x5754284effd0

setmetatable(user, {
    __tostring = function(self)
        return self.name.." "..self.age
    end
})

print(user)  -- Luka 35

В Lua все метаметоды должны находиться именно в метатаблице-классе. Если же добавить __tostring в саму таблицу-объект, он не будет работать.

Из примера выше мы видим еще одну особенность Lua. Конкатенация строк здесь выполняется через две точки, а не знак плюса, как во многих других языках программирования. Такой способ выбран не просто так, он позволяет применять разную логику. Если в "объединяющем" выражении стоят число и строка, то в случае плюса строка будет преобразовываться к числу, в случае двойной точки — число станет строкой.

print("10" + 35)   -- 45 (строка "10" стала числом 10)
print("10" .. 35)  -- 1035 (число 35 стало строкой "35")
print(10 .. 35)    -- 1035
print("10" + "35") -- 45

Стандартная функция print в Lua может принимать несколько аргументов через запятую. При этом в выводе она разделяет их знаками табуляции (\t), а не пробелами, как это было бы в Python.

print('One', 1, 'I', 1.0)
print('One', '', 1, 'I', 1.0)
print('Seventeen', 17, 'XVII', 17.0)
user@home:~$ lua example.lua 
One     1       I       1.0
One             1       I       1.0
Seventeen       17      XVII    17.0

Поскольку терминал выравнивает табуляцию по фиксированным колонкам, пробельное расстояние между выведенными значениями может оказаться разным. Также расстояние зависит от настроек самого терминала (табуляция может быть в 4 или 8 пробелов).

Использование автоматического табулятора связано с тем, что функция print в Lua создавалась в первую очередь как инструмент для быстрой отладки и вывода простых отчетов. Табуляция позволяет выравнивать данные в ровные колонки (таблицы) при выводе в консоль, что удобно для чтения логов.

Вместо строковых шаблонов, которых в Lua нет, форматирование вывода и того, как выглядят данные (например, вывод числа с плавающей точкой до двух знаков после запятой), осуществляется аналогично printf в языке Си. Но в Lua для этого дополнительно используется функция string.format.

local pi = 3.14159
print(string.format("%s %s", "one", "two"))
print(string.format("Пи: %.2f", pi))
print(string.format("%d %5d %05d", 45, 86, 13))
print(string.format("%-d %-5d %-05d", 45, 86, 13))

local hello = string.format("Hello, %s!", "Tom")
print(hello)
one two
Пи: 3.14
45    86 00013
45 86    13   
Hello, Tom!

Функция string.format находится в модуле string. При этом в Lua из-за специфики работы со строками её можно вызывать двумя разными способами: как функцию модуля, когда строка шаблона передается первым аргументом, и как метод самой строки. В Lua у всех строк есть общая метатаблица, где поле __index указывает на модуль string. Это значит, что любая строка автоматически обладает всеми функциями этой библиотеки.

local h = ("Hello, %s!"):format("Tom")
print(h) -- Hello Tom!

Под капотом оба способа идентичны, так как во втором случае Lua сам подставит строку в качестве первого аргумента.

Функция print всегда добавляет перенос строки (\n). Простой возможности не делать этого, как в Python через параметр end, в Lua нет. Поэтому когда нужно вывести текст без такого перехода или динамически изменять символы в той же строке терминала используют более низкоуровневую функцию io.write. Например, в текстовых интерфейсах часто нужно запрашивать данные у пользователя в той строке, где написан вопрос:

io.write('First Name: ')
local f_name = io.read()

io.write('Last Name: ')
local l_name = io.read()

io.write(f_name, ' ', l_name, '\n')
user@home:~$ lua example.lua  
First Name: Robert
Last Name: Green
Robert Green
user@home:~$ 

Функции print и io.write работают с системным буфером. В операционных системах буферизация вывода, когда текст не выводится в консоль мгновенно по одной букве, позволяет экономить ресурсы. ОС (ее низкоуровневая библиотека ввода-вывода) копит символы в буфере памяти, пока не получит символ перехода на новую строку. Так как io.write не переносит строку, система может "подумать", что вы еще не закончили, и не покажет текст вовремя. Поэтому io.write часто используется в паре с функцией io.flush, которая принудительно выталкивает имеющееся содержимое буфера.

io.write("Загрузка: ")
for i = 1, 10 do
    io.flush() -- принудительно сразу выталкиваем текст на экран
    local start = os.clock()
    while os.clock() - start < 0.2 do end -- ожидание 0.2 секунды
    io.write(".")
end
print()  -- для перехода на новую строку

Результатом выполнения этого кода, независимо есть там команда io.flush() или нет, будет строка Загрузка: ........... Однако, если io.flush() есть, точки будут постепенно появляться через каждые 0.2 секунды, без io.flush() вы увидите всю строку целиком через 2 секунды.

В отличие от print, функция io.write автоматически не вызывает tostring для своих аргументов. Поэтому попытки с ее помощью вывести любые другие типы данных, кроме как строки и числа, будут приводить к ошибкам. Конвертация чисел в строки происходит на "низком" уровне — силами самого Си-движка Lua.

Для записи в файлы используется метод file:write, особенности работы которого аналогичны функции io.write. Это разные объекты (функция в модуле io и метод в метатаблице файла) на уровне Lua, но оба являются обертками к одной и той же функции на уровне Си-движка.