cv.cm

Lập trình · Ngôn ngữ lập trình

Hướng dẫn học lập trình Lua cơ bản cho người mới

8 phút đọc

Trả lời nhanh

Lua là ngôn ngữ kịch bản nhẹ, có thể nhúng, được dùng trong game, Roblox (qua phương ngữ Luau), Neovim, Redis và các thiết bị nhúng. Cú pháp của nó nhỏ gọn và chỉ có một cấu trúc dữ liệu là table, đóng vai trò mảng, từ điển và đối tượng. Trang này bao gồm cài Lua, biến và kiểu dữ liệu, điều khiển luồng, hàm, table, metatable, xử lý lỗi với pcall và một ví dụ hoàn chỉnh.

Lua là gì và dùng để làm gì

Lua là ngôn ngữ kịch bản nhỏ, nhanh, kiểu động, được tạo ra năm 1993 tại PUC-Rio, một trường đại học ở Brazil. Toàn bộ trình thông dịch là một thư viện gọn viết bằng C, nên rất dễ nhúng vào các chương trình lớn hơn. Lua chỉ có một cấu trúc dữ liệu là table và rất ít khái niệm, nên học rất nhanh.

Lua chủ yếu được dùng làm ngôn ngữ kịch bản nhúng: công cụ làm game và các tựa game (Roblox dùng phương ngữ Lua gọi là Luau; LÖVE và Defold dùng Lua), trình soạn thảo Neovim, cơ sở dữ liệu Redis, các công cụ mạng như Nginx với OpenResty, và nhiều thiết bị nhúng. LuaJIT là trình biên dịch just-in-time phổ biến cho mã Lua 5.1.

Cài Lua và chạy Hello, world

Cài Lua bằng trình quản lý gói (ví dụ brew install lua hoặc apt install lua5.4) hoặc biên dịch từ mã nguồn tại lua.org. Chạy riêng lua để mở dấu nhắc tương tác.

-- hello.lua
print("Hello, world!")
lua hello.lua
# Hello, world!

Chú thích bắt đầu bằng --. Chú thích nhiều dòng viết là --[[ ... ]]. Câu lệnh không cần dấu chấm phẩy.

Biến và các kiểu dữ liệu cơ bản

Biến mặc định là toàn cục. Hãy luôn khai báo bằng local trừ khi bạn thực sự muốn biến toàn cục. Lua có tám kiểu cơ bản; những kiểu bạn dùng hằng ngày nằm ở bảng dưới.

local name = "Lua"
local year = 1993              -- integer
local pi = 3.14159             -- float
local ok = true
local nothing = nil

print(type(name), type(year), type(nothing))  -- string  number  nil
print(math.type(year), math.type(pi))         -- integer  float
print(name .. " " .. year)                    -- Lua 1993 (.. joins strings)
print(#name)                                  -- 3 (length)
print(7 / 2, 7 // 2, 7 % 2, 2 ^ 10)           -- 3.5  3  1  1024.0
print(string.format("%.2f", pi))              -- 3.14
print(tonumber("42") + 1, tostring(10))       -- 43  10
KiểuVí dụGhi chú
nilnilkhông có giá trị; biến chưa gán là nil
booleantrue, falsechỉ false và nil được coi là sai; 0 và "" là đúng
number42, 3.14có kiểu con số nguyên và số thực từ Lua 5.3
string"hi", 'hi', [[long]]chuỗi byte bất biến
table{1, 2, x = 3}cấu trúc dữ liệu duy nhất
functionfunction() endgiá trị hạng nhất (first-class)

Toán tử khác là ~=, không phải !=. Các toán tử logic là những từ and, or và not.

Điều khiển luồng

Các khối kết thúc bằng từ khóa end. Lua có if/elseif/else, while, repeat ... until, vòng for số học và vòng for tổng quát làm việc với iterator.

local score = 72
if score >= 90 then
  print("A")
elseif score >= 70 then
  print("B")                   -- B
else
  print("C")
end

for i = 1, 3 do                -- 1 to 3, inclusive
  io.write(i, " ")             -- 1 2 3
end
print()

for i = 10, 1, -4 do io.write(i, " ") end   -- 10 6 2
print()

local n = 3
while n > 0 do n = n - 1 end

repeat
  n = n + 1
until n >= 2
print(n)                       -- 2

Không có câu lệnh continue; hãy dùng goto continue với nhãn ::continue::, hoặc cấu trúc lại vòng lặp bằng if.

Hàm

Hàm là giá trị. Hàm có thể nhận số lượng đối số thay đổi với ... và trả về nhiều giá trị. Closure bắt giữ các biến cục bộ của phạm vi bao ngoài.

local function add(a, b)
  return a + b
end

local function minmax(t)
  local lo, hi = math.huge, -math.huge
  for _, v in ipairs(t) do
    lo, hi = math.min(lo, v), math.max(hi, v)
  end
  return lo, hi                -- two return values
end

local function counter()
  local count = 0
  return function()            -- a closure
    count = count + 1
    return count
  end
end

print(add(2, 3))               -- 5
local lo, hi = minmax({4, 9, 1})
print(lo, hi)                  -- 1  9
local next_id = counter()
print(next_id(), next_id())    -- 1  2
local function sum(...)
  local total = 0
  for _, v in ipairs({...}) do total = total + v end
  return total
end
print(sum(1, 2, 3))            -- 6

Table: mảng, từ điển và đối tượng

Table là mảng kết hợp có thể được lập chỉ mục bằng bất kỳ giá trị nào trừ nil. Cùng một cấu trúc đóng vai trò mảng, từ điển, tập hợp, module hoặc đối tượng. Theo quy ước, mảng bắt đầu từ chỉ số 1.

local fruits = {"apple", "banana"}
table.insert(fruits, "cherry")
print(#fruits, fruits[1])            -- 3  apple

for i, f in ipairs(fruits) do        -- array part, in order
  print(i, f)
end

local ages = {ann = 31, bo = 25}
ages["cy"] = 40
ages.bo = nil                        -- remove a key
for k, v in pairs(ages) do           -- all keys, order not defined
  print(k, v)
end

table.sort(fruits, function(a, b) return a > b end)
print(table.concat(fruits, ", "))    -- cherry, banana, apple

Metatable và Lua hướng đối tượng

Metatable là tính năng đặc trưng của Lua. Metatable gắn vào một table sẽ thay đổi cách table đó hoạt động: __index cung cấp các khóa còn thiếu (dùng cho kế thừa), và các trường như __add, __eq và __tostring nạp chồng toán tử. Cú pháp dấu hai chấm obj:method() truyền đối tượng làm tham số đầu tiên ẩn self.

local Vector = {}
Vector.__index = Vector

function Vector.new(x, y)
  return setmetatable({x = x, y = y}, Vector)
end

function Vector:length()
  return math.sqrt(self.x ^ 2 + self.y ^ 2)
end

Vector.__add = function(a, b) return Vector.new(a.x + b.x, a.y + b.y) end
Vector.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")" end

local v = Vector.new(3, 4) + Vector.new(0, 0)
print(tostring(v))       -- (3, 4)
print(v:length())        -- 5.0

Lua còn có coroutine (coroutine.create, coroutine.resume, coroutine.yield), cho phép một hàm tạm dừng và tiếp tục sau đó. Script game thường dùng chúng cho hành vi từng bước kéo dài qua nhiều khung hình.

Xử lý lỗi

Phát sinh lỗi bằng error(message). Bắt lỗi bằng pcall (protected call), hàm trả về false cùng lỗi thay vì dừng chương trình. Nhiều hàm thư viện trả về nil và một thông báo lỗi thay vì phát sinh lỗi, như io.open.

local function divide(a, b)
  if b == 0 then
    error("division by zero")
  end
  return a / b
end

local ok, result = pcall(divide, 10, 2)
print(ok, result)                  -- true  5.0

ok, result = pcall(divide, 1, 0)
print(ok, result)                  -- false  script.lua:3: division by zero

local f, err = io.open("missing.txt", "r")
if not f then print("open failed: " .. err) end

Tên tệp và số dòng trong thông báo lỗi phụ thuộc vào nơi mã chạy. assert(value, message) phát sinh lỗi khi value là false hoặc nil, và là cách ngắn gọn để kiểm tra kết quả.

Một chương trình hoàn chỉnh nhỏ: đếm từ

Chương trình này đếm từ trong một văn bản bằng khớp mẫu, sắp xếp theo tần suất và in ba từ đứng đầu. Nó dùng mẫu chuỗi, table, sắp xếp với hàm so sánh tùy chỉnh và định dạng chuỗi.

local text = [[
The cat and the dog. The dog sat,
and the cat ran.
]]

local counts = {}
for word in text:lower():gmatch("%a+") do
  counts[word] = (counts[word] or 0) + 1
end

local list = {}
for word, n in pairs(counts) do
  list[#list + 1] = {word = word, n = n}
end
table.sort(list, function(a, b)
  if a.n ~= b.n then return a.n > b.n end
  return a.word < b.word
end)

for i = 1, 3 do
  print(string.format("%-4s %d", list[i].word, list[i].n))
end
-- the  4
-- and  2
-- cat  2

Học tiếp ở đâu

  • Lua Reference Manual tại lua.org, ngắn gọn mà đầy đủ.
  • Programming in Lua của Roberto Ierusalimschy, một trong những người tạo ra Lua; ấn bản đầu tiên có miễn phí trên mạng.
  • Tài liệu của môi trường chủ của bạn, như hướng dẫn Lua của Neovim, LÖVE hoặc tài liệu Luau của Roblox.
  • LuaRocks, trình quản lý gói cho các module Lua.

Câu hỏi thường gặp

Vì sao mảng Lua bắt đầu từ 1?

Các nhà thiết kế Lua chọn cách đánh chỉ số từ 1 để phù hợp với quy ước toán học thông thường và người không làm lập trình. Các hàm thư viện chuẩn như ipairs, # và table.insert đều giả định dãy bắt đầu từ 1.

Lua có nhanh hơn Python không?

Trình thông dịch Lua chuẩn nhỏ gọn và nhìn chung nhanh so với một ngôn ngữ kiểu động, còn LuaJIT nhanh hơn nhiều với nhiều tác vụ. Tốc độ thực tế phụ thuộc vào bài toán, nên hãy tự đo với trường hợp của bạn.

pairs và ipairs khác nhau thế nào?

ipairs duyệt phần mảng theo thứ tự từ chỉ số 1 cho đến nil đầu tiên. pairs duyệt mọi khóa trong table theo thứ tự không xác định.

Luau là gì?

Luau là ngôn ngữ phát triển từ Lua 5.1 do Roblox xây dựng. Nó bổ sung kiểu dần (gradual typing) và các cải tiến hiệu năng, và được dùng cho các game Roblox.

Lua có phù hợp cho người mới không?

Có. Lua có cú pháp nhỏ, nhất quán và ít khái niệm, nên phổ biến trong việc mod game và giảng dạy. Thư viện chuẩn tối giản nghĩa là bạn phải dựa vào chương trình chủ hoặc các module để có thêm tính năng.

Tất cả hướng dẫn lập trình