Учебник по LuaRu

Материал из ТХАБ.РФ
Версия от 16:25, 3 апреля 2018; Сандро (обсуждение | вклад) (Типы данных: "так вот/так вот" - тавтология... (убрал))

Перейти к: навигация, поиск

Исправленная версия статьи: Lua за 60 минут Исправленная версия (В процессе перевода на LuaRu) Учебник по Lua для программистов

Исправлены

  • сообщения переведены на русский
  • исправлены ошибки в примерах

Для LuaRu

  • Переводятся (В ПРОЦЕССЕ!!!) на LuaRu операторы

Lua? Что это?

Lua — простой встраиваемый язык (его можно интегрировать с вашими программами, написанными на других языках), легкий и понятный, с одним типом данных, с однообразным синтаксисом. Идеальный язык для изучения.

Зачем?

Lua может вам пригодится:

  • если вы геймер (плагины для World of Warcraft и множества других игр)
  • если вы пишете игры (очень часто в играх движок пишут на C/C++, а AI — на Lua)
  • если вы системный программист (на Lua можно писать плагины для nmap, wireshark, nginx и других утилит)
  • если вы embedded-разработчик (Lua очень быстрый, компактный и требует очень мало ресурсов)

Что надо для того, чтобы читать дальше?

1. Научитесь программировать. Хотя бы немного. Не важно на каком языке. 2. Установите Lua. Для этого либо скачайте здесь версию 5.3 (http://www.lua.org/download.html), либо ищите ее в репозиториях для linux.

Все примеры из статьи запускайте в терминале командой наподобие «lua file.lua».

Первые впечатления

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

-- моя первая программа на luaRu : приветМир.lua
печать=print
print "Привет мир!";
печать("Пока мир.")

Что уже можно сказать о языке:

  • однострочные комментарии начинаются с двух дефисов "--"
  • скобки и точки-с-запятыми можно не писать

Операторы языка

Набор условных операторов и циклов довольно типичен:

-- условные операторы (ветки иначе может не быть)
если ж == 0 то
  печать("ж это ноль")
иначе
  печать("ж это не ноль")
всё

-- сокращенная форма если/то/иначе/иначе_если/всё (вместо switch/case)
если a == 0 то
  печать("ноль")
иначе_если a == 1 то
  печать("один")
иначе_если a == 2 то
  печать("два")
иначе
  печать("другое")
всё

-- цикл со счетчиком
для ж = 1, 10 делать
  печать(ж)
всё

-- цикл с предусловием
ж = 5
пока ж > 0 делать
  ж = ж - 1
всё

-- цикл с постусловием
повторять
  ж = ж + 1
пока_не ж >= 5

ПОДУМАЙТЕ: что может означать цикл "для ж = 1, 10, 2 делать ... конец"?

В выражениях можно использовать такие вот операторы над переменными:

  • присваивание: x = 0
  • арифметические: +, -, *, /, % (остаток от деления), ^ (возведение в степень)
  • логические: and, or, not
  • сравнение: >, <, ==, <=, >=, ~= (не-равно, да-да, вместо привычного «!=»)
  • конкатенация строк (оператор «..»), напр.: s1=»hello»; s2=»world»; s3=s1..s2
  • длина/размер (оператор #): s=»hello»; a = #s (‘a’ будет равно 5).
  • получение элемента по индексу, напр.: s[2]

Битовых операций в языке долгое время не было, но в версии 5.2 появилась библиотека bit32, которая их реализует (как функции, не как операторы).

Типы данных

Я вам соврал, когда сказал, что у языка один тип данных. Их у него много (как и у каждого серьезного языка):

  • пусто, nil (ровным счётом ничего)
  • логические значения (да/нет, правда/ложь) (true/false)
  • числа (numbers) — без деления на целые/вещественные. Просто числа.
  • строки — кстати, они очень похожи на строки в паскале
  • функции — да, переменная может быть типа «функция»
  • поток (thread)
  • произвольные данные (userdata)
  • таблица (table)

Если с первыми типами все понятно, то что же такое userdata? Вспомним о том, что Lua — язык встраиваемый, и обычно тесно работает с компонентами программ, написанными на других языках. Так вот, эти «чужие» компоненты могут создавать данные под свои нужды и хранить эти данные вместе с lua-объектами. Отсюда userdata — и есть "подводная часть айсберга", которая с точки зрения языка lua не нужна, хотя не обращать внимания на нее мы не можем.

А теперь самое важное в языке — таблицы.

Таблицы

Я вам снова соврал, когда сказал, что у языка 8 типов данных. Можете считать что он один: всё — это таблицы (это, кстати, тоже неправда). Таблица — это очень изящная структура данных, она сочетает в себе свойства массива, хэш-таблицы («ключ»-«значение»), структуры, объекта.


-- Итак, вот пример таблицы как массива:
ж = {1, 2, 3} -- массив из 3-х элементов
печать(ж[2]) -- выведет "2", потому что индексы считаются с единицы
-- А таблица в виде разреженного массива (у которого есть не все элементы)
ж = {} -- пустая таблица
ж[1] = 1
ж[3] = 5

ПОДУМАЙТЕ: чему равно ж[2] в случае разреженного массива?

В примере выше таблица ведет себя как массив, но на самом деле — у нас ведь есть ключи (индексы) и значения (элементы массива). И при этом ключами могут быть какие угодно типы, не только числа:

ж = {}
ж["привет"] = правда
ж["мир"] = ложь
ж[правда] = 1
-- или так:
ж = {
  привет = 123,
  мир = 456
}
печать(ж["привет"))
печать(ж.привет) -- то же самое, что и ж["привет"], хотя выглядит как структура с полями

Кстати, раз уж у таблицы есть ключи и значения, то можно в цикле перебрать все ключи и соответствующие им значения:

таблица = {
  а = 3,
  ж = 4
}
для ключ, значение в pairs(таблица) делать
  печать(ключ, значение) -- выведет "а 3", потом "ж 4"
конец

А как же объекты? О них мы узнаем чуть позже, вначале — о функциях.

Функции

Вот пример обычной функции.

функция сложить(ф, ж)
  вернуть ф + ж   -- вариант результат ф + ж
конец

печать(сложить(5, 3)) -- напечатает "8"

Функции языка позволяют принимать несколько аргументов, и возвращать несколько аргументов. Так аргументы, значения которых не указаны явно, считаются равными пусто (nil).

ПОДУМАЙТЕ: зачем может понадобиться возвращать несколько аргументов?

функция поменять_местами(ф, ж)
  результат ж, ф
конец

x, y = поменять_местами(x, y)
-- кстати, это можно сделать и без функции:
x, y = y, x
-- и если уж функция возвращает несколько аргументов, 
-- а они вам не нужны - игнорируйте их с помощью 
-- специальной переменной-подчеркивания "_"
a, _, _, d = какаято_функция()

Функции могут принимать переменное количество аргументов:

-- в прототипе переменное число аргументов записывается как троеточие
функция сумма(...) 
  ж = 0
  для _, ф в pairs(arg) делать -- в функции обращаются к ним, как к таблице "arg"
     ж = ж + ф
  конец
  результат ж
конец

печатьprint (сумма(1, 2, 3)) -- вернет 10
печать (сумма(1, 2, 3, 4)) -- вернет 14

Поскольку функции — это полноценный тип данных, то можно создавать переменные-функции, а можно передавать функции как аргументы других функций

ф = функция (ж) -- функция, умножающая Ж на 2
        результат ж * 2
    конец 
-- краткая форма записи
ф = функция(ж) результат ж + 1 конец -- функция, увеличивающая Ж на 1

функция таб_функ(таблица, функ)
  табл = {} -- создание пустой таблицы
  для ключ, значение в pairs(таблица) делать
    табл[ключ] = функ(значение) -- заменяем элемент на какую-то функцию от этого элемента
    печать (табл[ключ]) -- печать результата применения функции функ к элементу массива t
  конец 
конец 

-- ПОДУМАЙТЕ: что вернут вызовы
таб = {1, 3, 5}
печать(таб_функ(таб, а))
печать(таб_функ(таб, ж))

Объекты = таблицы + функции

Раз мы можем сохранять функции в переменных, то и в полях таблиц тоже сможем. А это уже получаются как-бы-методы. Для тех, кто не знаком с ООП скажу, что основная его польза (по крайней мере в Lua) в том, что функции и данные, с которыми они работают находятся рядом — в пределах одного объекта. Для тех, кто знаком с ООП скажу, что классов здесь нет, а наследование прототипное.

Перейдем к примерам. Есть у нас объект, скажем, лампочка. Она умеет гореть и не гореть. Ну а действия с ней можно сделать два — включить и выключить:

-- таблица ключ=значение
лампа = {
  вкл = нет
}

функция включить(л)
  л.вкл = да
конец
-- или в кратком виде
функция отключить(л) л.вкл = нет конец

-- это просто функции для работы со структурой
включить(лампа)
печать(лампа.вкл) -- вывод состояния переменной лампа.on  
отключить(лампа)
печать(лампа.вкл) -- вывод состояния переменной лампа.on  

А если лампочку сделать объектом, и функции отключить и включить сделать полями объекта, то получится:

лампа = {
  вкл = нет,
  включить = функция(л)
                  л.вкл = да
            конец,
  отключить = функция(л) л.вкл = нет конец 
}
лампа.включить(лампа)
печать (лампа.вкл) -- вывод состояния переменной лампа.вкл  
лампа.отключить(лампа)
печать (лампа.вкл) -- вывод состояния переменной лампа.вкл  

Мы вынуждены передавать сам объект лампочки в качестве первого аргумента, потому что иначе наша функция не узнает с какой именно лампочкой надо работать, чтобы сменить состояние on/off. Но чтобы не быть многословными, в Lua есть сокращенная запись, которую обычно и используют — lamp:turn_on(). Итого, мы уже знаем несколько таких упрощений синтаксиса:

лампа:включить() -- самая общепринятая запись
лампа.включить(лампа) -- то с точки зрения синтаксиса это тоже правильно
лампа["включить"](лампа) -- и это

Продолжая говорить о сокращениях, функции можно описывать не только явно, как поля структуры, но и в более удобной форме:

лампа = {
 вкл = нет
}
-- через точку, тогда аргумент надо указывать
функция лампа.включить(l) l.вкл = да конец
-- через двоеточие, тогда аргумент неявно задается сам, как переменная "self"
-- "self" - и есть та лампочка, для которой вызвали метод
функция лампа:отключить() self.вкл = нет конец

Интересно?

Специальные функции

Раскрыть тему Lua.Специальные функции

Некоторые имена функций таблиц (методов) зарезервированы, и они несут особый смысл:

  • __add(a, b), __sub(a, b), __div(a, b), __mul(a, b), __mod(a, b), __pow(a, b) — вызываются, когда выполняются арифметические операции над таблицей
  • __unm(a) — унарная операция «минус» (когда пишут что-то типа «x = -x»)
  • __lt(a, b), __le(a, b), __eq(a, b) — вычисляют результат сравнения (<, <=, ==)
  • __len(a) — вызывается, когда делается "#a"
  • __concat(a, b) — вызывается при "a..b"
  • __call(a, …) — вызывается при "a()". Переменные аргументы — это аргументы при вызове
  • __index(a, i) — обращение к a[i], при условии, что такого элемента не существует
  • __newindex(a, i, v) — создание "a[i] = v"
  • __gc(a) — когда объект удаляется при сборке мусора

Ниже список всех доступных событий и их описание:

  • __index( self, key ) чтение по ключу
  • __newindex( self, key, value ) запись по ключу
  • __call( self, ... ) вызов как функции
  • __add( self, arg ) сложение
  • __sub( self, arg ) вычитание
  • __mul( self, arg ) умножение
  • __div( self, arg ) деление
  • __mod( self, arg ) остаток от деления
  • __pow( self, arg ) возведение в степень
  • __unm( self ) унарный минус
  • __concat( self, arg ) конкатенация строк
  • __len( self ) длина
  • __eq( self, arg ) оператор "равно"
  • __lt( self, arg ) оператор "меньше"
  • __le( self, arg ) оператор "меньше или равно"
  • __tostring( self ) Вызывается при попытке перевести объект в строковое представление (например с помощью функции tostring)
  • __gc Вызывается для userdata-объектов при сборке мусора
  • __metatable Если задать это поле в метатаблице, то getmetatable будет просто возвращать его значение, а setmetatable вернет ошибку.

Подменяя эти методы, можно перегружать операторы и использовать синтаксис языка для своих целей. Главное не переусердствовать.

Наследование

Для тех, кто не знает ООП, наследование позволяет расширить функциональность уже существующего класса. Например, просто лампочка умеет включаться-выключаться, а супер-лампочка будет еще и яркость менять. Зачем нам переписывать методы turn_on/turn_off, если можно их повторно использовать?

В Lua для этого есть понятие мета-таблицы, т.е. таблицы-предка. У каждой таблицы есть одна таблица-предок, и дочерняя таблица умеет делать все, что умеет предок.

  • Метатаблица - это обычная таблица в которой описываются определённые события (например сложение, вычитание, сравнивание и т.д.)
  • В качестве названия события выступает индекс таблицы, а её значение - обработчик события (метод)

setmetatable связывает таблицы новую и таблицу-предок, с помощью метода __index. Этот метод вызывается всякий раз, когда запрашиваемый атрибут таблицы не найдет (в нашем случае — superlamp:turn_on). И поскольку superlamp.__index == nil и lamp.__index == nil, то вызова метода не происходит.

Допустим, что объект-таблицу lamp мы уже создали. Тогда супер-лампочка будет выглядеть так:

lamp = {
on = false;
turn_on = function(l) l.on = true end;
turn_off = function(l) l.on = false end;
}
-- говорим искать в таблице lamp все неизвестные атрибуты
lamp.__index = lamp
superlamp = {
brightness = 100
}
-- указываем родительскую таблицу
setmetatable(superlamp, lamp)
-- и ее методы теперь доступны (ищутся в таблице, superlamp, потом в superlamp.__index, потом в ее метатаблице lamp.__index
superlamp:turn_on()
print (superlamp.on)
superlamp:turn_off()
print (superlamp.on)

Расширение функциональности

Работает но непонятно 

Родительские таблицы есть у многих типов (ну у строк и таблиц точно, у чисел и булевых чисел, и у nil их нет). Допустим, мы хотим складывать все строки с помощью оператора "+", а не "..". Для этого надо подменить функцию «+» (__add) для родительской таблицы всех строк:

s = getmetatable("") -- получили родительскую таблицу строки
s.__add = function(s1, s2) return s1..s2 end -- подменили метод

-- проверяем
a = "Привет"
b = "Мир!"
print(a + b) -- напишет "ПриветМир!"

Собственно, мы еще можем заменить функцию print с помощью «print = myfunction», да и много других хакерских дел можно сделать.

Области видимости

Не понятно. Надо примеры 

Переменные бывают глобальные и локальные. При создании все переменные в Lua являются глобальными.

ПОДУМАЙТЕ: почему?

Для указания локальной области видимости пишут ключевое слово local:

local x
local var1, var2 = 5, 3

Не забывайте об этом слове.

Обработка ошибок

Часто, если возникают ошибки, надо прекратить выполнение определенной функции. Можно, конечно, сделать множество проверок и вызывать «return», если что-то пошло не так. Но это увеличит объем кода. В Lua используется что-то наподобие исключений (exceptions).

Ошибки порождаются с помощью функции error(x). В качестве аргумента можно передать все, что угодно (то, что имеет отношение к ошибке — строковое описание, числовой код, объект, с которым произошла ошибка и т.д.)

Обычно после этой функции вся программа аварийно завершается. А это надо далеко не всегда. Если вы вызываете функцию, которая может создать ошибку (или ее дочерние функции могут создать ошибку), то вызывайте ее безопасно, с помощью pcall():

function f(x, y)
...
  if ... then
    error("failed to do somthing")
  end
...
end

status, err = pcall(f, x, y) -- f:функция, x-y: ее аргументы
if not status then 
  -- обработать ошибку err. В нашем случае в err находится текст ошибки
end

Стандартные библиотеки

Стандартных библиотек мало, зато это позволяет запускать Lua где угодно. Подробнее можно получить их список здесь — http://www.lua.org/manual/5.2/manual.html

Нестандартных библиотек много, их можно найти на LuaForge, LuaRocks и в других репозиториях.

Между Lua и не-Lua

ВНИМАНИЕ: эту часть рекомендуется читать людям со знанием языка C.

А если нам недостаточно функциональности стандартных библиотек? Если у нас есть наша программа на C, а мы хотим вызывать ее функции из Lua? Для этого есть очень простой механизм.

Допустим, мы хотим создать свою функцию, которая возвращает случайное число (в Lua есть math.random(), но мы хотим поучиться). Нам придется написать вот такой код на C:

#include <lua.h>
#include <lauxlib.h>
#include <time.h>

/* собственно, что делать при вызове `rand(from, to)` */
static int librand_rand(lua_State *L) {
   int from, to;
   int x;

   from = lua_tonumber(L, 1); /* первый параметр функции */
   to = lua_tonumber(L, 2); /* второй параметр функции */

   x = rand() % (to - from + 1) + from;

   lua_pushnumber(L, x); /* возвращаемое значение */
   return 1; /* возвращаем только один аргумент */
}

/* в Lua "rand" соответствует нашей функции librand_rand() */
static const luaL_reg R[] = {
   {"rand", librand_rand}, 
   {NULL, NULL} /* конец списка экспортируемых функций */
};

/* вызывается при загрузке библиотеку */
LUALIB_API int luaopen_librand(lua_State *L) {
   luaL_openlib(L, "librand", R, 0);
   srand(time(NULL));
   return 1; /* завершаемся успешно */
}

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

random = require("librand") -- загружаем библиотеку

print(random.rand(1, 100))
print(random.rand(0, 1))

А если мы хотим вызывать код, написанный на Lua из наших программ? Тогда наши программы должны создавать виртуальную машину Lua, в которой и будут выполняться Lua-скрипты. Это намного проще:

#include "lua.h"
#include "lauxlib.h"

int main() {
   lua_State *L = lua_open(); // создаем виртуальную машину Lua
   luaL_openlibs(L); // загружаем стандартные библиотеку
   luaL_dofile(L, "rand.lua"); // выполняем скрипт
   lua_close(L); // закрываем Lua
   return 0;
}

Все.

Вы теперь можете писать на Lua. Если вы узнаете интересные моменты про Lua, которые можно было бы отразить в статье — пишите!

см. также

Ссылки