Bảng Metatables cho phép các bảng trở nên mạnh mẽ hơn trước. Chúng được gắn vào dữ liệu và chứa các giá trị gọi là metamethods. Các metamethods được kích hoạt khi một hành động nhất định được sử dụng với dữ liệu mà nó được gắn vào.
Thao tác với bảng Metatables
Hai hàm chính để thêm và tìm bảng metatable của một bảng là setmetatable() và getmetatable().
local x = {}
local metaTable = {} -- Bảng Metatables cũng là bảng!
setmetatable(x, metaTable) -- Gán cho x một bảng metatable gọi là metaTable!
print(getmetatable(x)) --> table: [địa chỉ bộ nhớ thập lục phân]Hàm setmetatable() cũng trả về bảng mà bạn đang thiết lập metatable, vì vậy hai đoạn mã sau thực hiện cùng một việc:
local x = {}
setmetatable(x, {})local x = setmetatable({}, {})Metamethods
Metamethods là các hàm được lưu trữ bên trong một bảng metatable. Chúng có thể thực hiện từ việc gọi một bảng, đến việc cộng một bảng, thậm chí là chia các bảng. Dưới đây là danh sách các metamethods có sẵn:
| Phương thức | Mô tả |
|---|---|
| __index(table, index) | Được kích hoạt khi table[index] được chỉ mục, nếu table[index] là nil. Cũng có thể được thiết lập thành một bảng, trong trường hợp đó bảng đó sẽ được chỉ mục. |
| __newindex(table, index, value) | Được kích hoạt khi table[index] cố gắng được thiết lập (table[index] = value), nếu table[index] là nil. Cũng có thể được thiết lập thành một bảng, trong trường hợp đó bảng đó sẽ được chỉ mục. |
| __call(table, ...) | Được kích hoạt khi bảng được gọi như một hàm, ... là các tham số đã được truyền vào. |
| __concat(table, value) | Được kích hoạt khi toán tử nối chuỗi .. được sử dụng trên bảng. |
| __unm(table) | Được kích hoạt khi toán tử đơn – được sử dụng trên bảng. |
| __add(table, value) | Toán tử cộng +. |
| __sub(table, value) | Toán tử trừ –. |
| __mul(table, value) | Toán tử nhân *. |
| __div(table, value) | Toán tử chia /. |
| __idiv(table, value) | Toán tử chia lấy phần nguyên //. |
| __mod(table, value) | Toán tử modulo %. |
| __pow(table, value) | Toán tử lũy thừa ^. |
| __tostring(table) | Được kích hoạt khi tostring được gọi trên bảng. |
| __metatable | Nếu có, khóa bảng metatable để getmetatable() sẽ trả về cái này thay vì bảng metatable và setmetatable() sẽ báo lỗi. Giá trị không phải hàm. |
| __eq(table, value) | Toán tử bằng ==¹ |
| __lt(table, value) | Toán tử nhỏ hơn <¹ |
| __le(table, value) | Toán tử nhỏ hơn hoặc bằng <=¹ |
| __mode | Được sử dụng trong các bảng yếu, khai báo xem các khóa và/hoặc giá trị của một bảng có yếu hay không. Xem Sử dụng bảng yếu. |
| __len(table) | Được kích hoạt khi toán tử độ dài # được sử dụng trên đối tượng. |
| __iter(table) | Được sử dụng để chỉ định một bộ lặp tùy chỉnh khi sử dụng lặp tổng quát. |
Cần lưu ý rằng khi viết các hàm cho cả metamethods số học hoặc quan hệ, hai tham số hàm có thể hoán đổi cho nhau giữa bảng đã kích hoạt metamethod và giá trị khác. Ví dụ, khi thực hiện các phép toán vector với các số vô hướng, phép chia không giao hoán. Do đó, nếu bạn đang viết các metamethods cho lớp vector2 của riêng bạn, bạn sẽ muốn cẩn thận để tính đến cả hai kịch bản.
local vector2 = {__type = "vector2"}
local mt = {__index = vector2}
function mt.__div(a, b)
if type(a) == "number" then
-- a là một số vô hướng, b là một vector
local scalar, vector = a, b
return vector2.new(scalar / vector.x, scalar / vector.y)
elseif type(b) == "number" then
-- a là một vector, b là một số vô hướng
local vector, scalar = a, b
return vector2.new(vector.x / scalar, vector.y / scalar)
elseif (a.__type and a.__type == "vector2" and b.__type and b.__type == "vector2") then
-- cả a và b đều là vector
return vector2.new(a.x / b.x, a.y / b.y)
end
end
function mt.__tostring(t)
return t.x .. ", " .. t.y
end
function vector2.new(x, y)
local self = setmetatable({}, mt)
self.x = x or 0
self.y = y or 0
return self
end
local a = vector2.new(10, 5)
local b = vector2.new(-3, 4)
print(a / b) -- -3.3333333333333, 1.25
print(b / a) -- -0.3, 0.8
print(2 / a) -- 0.2, 0.4
print(a / 2) -- 5, 2.5Cách sử dụng
Có nhiều cách để sử dụng bảng Metatables.
Áp dụng toán tử âm một ngôi cho các bảng
Metamethod __unm xử lý toán tử âm một ngôi. Sử dụng nó để phủ định mọi giá trị trong một bảng:
local metatable = {
__unm = function(t) -- __unm là cho toán tử - một ngôi
local negated = {}
for key, value in t do
negated[key] = -value -- phủ định tất cả các giá trị trong bảng này
end
return negated -- trả về bảng
end
}
local table1 = setmetatable({10, 11, 12}, metatable)
print(table.concat(-table1, "; ")) --> -10; -11; -12Chỉ mục các giá trị thiếu từ một bảng khác
Dưới đây là một cách thú vị để khai báo các thứ bằng cách sử dụng __index:
local metatable = {
__index = {x = 1}
}
local t = setmetatable({}, metatable)
print(t.x) --> 1__index đã được kích hoạt khi x được chỉ mục trong bảng và không tìm thấy. Luau sau đó đã tìm kiếm qua bảng __index để tìm một chỉ mục có tên là x, và, khi tìm thấy, đã trả về nó.
Gọi một bảng như một hàm
Bảng Metatables có thể làm nhiều hơn là cung cấp một giá trị thiếu. Lấy ví dụ này:
local t = {10, 20, 30}
print(t(5))Thông thường bạn không thể gọi một bảng, nhưng với bảng Metatables bạn có thể:
local metatable = {
__call = function(t, param)
local sum = {}
for i, value in ipairs(t) do
sum[i] = value + param -- Thêm tham số (5) vào giá trị, sau đó đặt nó vào bảng mới (t).
end
return unpack(sum) -- Trả về các giá trị bảng riêng lẻ
end
}
local t = setmetatable({10, 20, 30}, metatable)
print(t(5)) --> 15 25 35Áp dụng toán tử cộng cho các bảng
Bạn cũng có thể làm nhiều hơn nữa, chẳng hạn như cộng các bảng:
local table1 = {10, 11, 12}
local table2 = {13, 14, 15}
for k, v in table1 + table2 do
print(k, v)
endĐiều này sẽ báo lỗi nói rằng bạn đang cố gắng thực hiện phép toán trên một bảng, nhưng nó hoạt động khi thử với một bảng Metatables:
local metatable = {
__add = function(t1, t2)
local sum = {}
for key, value in t1 do
sum[key] = value
end
for key, value in t2 do
if sum[key] then
sum[key] += value
else
sum[key] = value
end
end
return sum
end
}
local table1 = setmetatable({10, 11, 12}, metatable)
local table2 = setmetatable({13, 14, 15}, metatable)
for k, v in table1 + table2 do
print(k, v)
endThiết lập giá trị mà không kích hoạt metamethods
Khi làm việc với bảng Metatables, bạn có thể gặp một số vấn đề. Nếu bạn cần sử dụng metamethod __index để tạo các giá trị mới trong một bảng, nhưng bảng đó cũng có một metamethod __newindex, bạn sẽ muốn sử dụng hàm tích hợp sẵn của Luau rawset() để thiết lập giá trị mà không kích hoạt bất kỳ metamethod nào. Lấy đoạn mã sau làm ví dụ về những gì xảy ra nếu bạn không sử dụng các hàm này.
local t = setmetatable({}, {
__index = function(self, i)
self[i] = i * 10
return self[i]
end,
__newindex = function(self, i, v)
-- Không thiết lập giá trị cho bảng theo cách bình thường
end
})
print(t[1]) -- Gây ra tràn ngăn xếpTràn ngăn xếp xảy ra khi bạn cố gắng gọi một hàm từ chính nó quá nhiều lần. Trong hàm __index ở trên, self[i] được thiết lập thành một giá trị, vì vậy khi đến dòng tiếp theo, self[i] nên tồn tại và có lẽ sẽ không gọi metamethod __index. Vấn đề là __newindex không cho phép bạn thiết lập giá trị. Sự hiện diện của nó ngăn các giá trị được thêm vào bảng bằng phương pháp tiêu chuẩn t[i] = v. Để vượt qua điều này, hãy sử dụng hàm rawset():
local t = setmetatable({}, {
__index = function(self, i)
rawset(self, i, i * 10)
return self[i]
end,
__newindex = function(self, i, v)
-- Không thiết lập giá trị cho bảng theo cách bình thường
end
})
print(t[1]) --> 10Sử dụng bảng yếu
Các biến giữ bảng, hàm, luồng, và userdata, bao gồm Instances, lưu trữ các tham chiếu đến các đối tượng đó thay vì các bản sao của chúng. Gán một đối tượng cho một biến khác tạo ra một tham chiếu khác đến cùng một đối tượng:
local map = {key = 1}
local mapReference = map
print(map == mapReference) --> true
print(map == {key = 1}) --> false
local function func() end
local funcReference = func
print(func == funcReference) --> true
local part = Instance.new("Part")
local partReference = part
print(part == partReference) --> trueCác đối tượng không còn có thể truy cập thông qua các tham chiếu mạnh trở thành đủ điều kiện để thu gom rác. Thiết lập một biến thành nil không làm cho một đối tượng đủ điều kiện để thu gom rác nếu một tham chiếu mạnh khác vẫn trỏ đến đối tượng đó:
local child = {}
local parent = {Child = child}
child = nil
print(parent.Child) --> table: [địa chỉ bộ nhớ thập lục phân]Mặc dù child không còn trỏ đến bảng con, nhưng parent.Child vẫn làm vậy. Chừng nào bảng cha vẫn có thể truy cập được, tham chiếu của nó giữ cho bảng con cũng có thể truy cập được.
Một bảng yếu giữ các khóa, giá trị, hoặc cả hai mà không ngăn cản bộ thu gom rác thu hồi các đối tượng không thể truy cập khác. Thiết lập trường __mode của bảng metatable để kiểm soát các tham chiếu nào là yếu:
- "k" làm cho các khóa yếu.
- "v" làm cho các giá trị yếu.
- "kv" hoặc "vk" làm cho cả khóa và giá trị đều yếu.
Đoạn mã sau tạo một bảng với các giá trị yếu:
local children = setmetatable({}, {
__mode = "v",
})
do
local child = {}
children.example = child
endSau khi khối do kết thúc, giá trị được lưu trữ trong children.example không có tham chiếu mạnh nào và trở thành đủ điều kiện để thu gom rác. Khi bộ thu gom rác thu hồi bảng con, Luau sẽ xóa mục của nó khỏi children. Việc thu gom rác là không xác định, vì vậy đừng phụ thuộc vào thời điểm mục biến mất.
Bất kỳ chế độ nào bạn bỏ qua sẽ vẫn mạnh. Các khóa trong bảng __mode = "v" vẫn giữ các tham chiếu mạnh, vì vậy một đối tượng được lưu trữ dưới dạng khóa vẫn có thể truy cập và mục của nó vẫn ở trong bảng.
Đối với Instances, việc thu gom rác áp dụng cho tham chiếu Luau thay vì cho chính đối tượng. Engine lưu trữ dữ liệu instance bên ngoài VM Luau, vì vậy việc thu gom một tham chiếu Luau không xóa instance, và một instance vẫn ở trong mô hình dữ liệu không giữ tham chiếu Luau của nó sống. Để giải phóng một instance, gọi Instance:Destroy() và xóa mọi tham chiếu còn lại đến nó.
Sử dụng kiểu dữ liệu tập hợp
Một tập hợp là một tập hợp các mục không có thứ tự và không có phần tử trùng lặp. Một mục có hoặc không có trong một tập hợp. Sử dụng bảng Metatables, bạn có thể xây dựng và thao tác các tập hợp trong các kịch bản Luau.
Các phương thức cơ bản
Đoạn mã sau bao gồm các chức năng cơ bản của tập hợp, cho phép bạn xây dựng các tập hợp mới, thêm và xóa một mục, kiểm tra xem một tập hợp có chứa một mục hay không, và xuất nội dung của một tập hợp.
local Set = {}
Set.__index = Set
-- Hàm để xây dựng một tập hợp từ danh sách các mục tùy chọn
function Set.new(items)
local newSet = {}
for key, value in items or {} do
newSet[value] = true
end
return setmetatable(newSet, Set)
end
-- Hàm để thêm một mục vào một tập hợp
function Set:add(item)
self[item] = true
end
-- Hàm để xóa một mục khỏi một tập hợp
function Set:remove(item)
self[item] = nil
end
-- Hàm để kiểm tra xem một tập hợp có chứa một mục hay không
function Set:contains(item)
return self[item] == true
end
-- Hàm để xuất tập hợp dưới dạng danh sách phân cách bằng dấu phẩy để gỡ lỗi
function Set:output()
local elems = {}
for key, value in self do
table.insert(elems, tostring(key))
end
print(table.concat(elems, ", "))
endTạo tập hợp
Một tập hợp mới có thể được xây dựng bằng cách gọi Set.new() với một mảng tùy chọn các mục để thêm.
local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})Lưu ý rằng theo định nghĩa, một tập hợp không có khái niệm về thứ tự.
Thêm mục
Thêm một mục vào một tập hợp hiện có có thể được thực hiện thông qua phương thức Set:add().
local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
fruits:add("Mango")Xóa mục
Để xóa một mục khỏi một tập hợp, gọi Set:remove() với tên mục.
local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
fruits:remove("Orange")Kiểm tra mục
Để kiểm tra xem một tập hợp có chứa một mục cụ thể hay không, sử dụng Set:contains().
local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
local result1 = fruits:contains("Cherry")
print(result1) -- true
local result2 = fruits:contains("Watermelon")
print(result2) -- falseCác phương thức bổ sung
Các phép toán hữu ích khác có thể được triển khai cho các tập hợp, cho phép bạn so sánh các mục giữa các tập hợp, kết hợp các tập hợp, hoặc trừ một tập hợp khỏi một tập hợp khác.
Giao nhau
Khi xem xét các tập hợp như các biểu đồ Venn, bạn có thể lấy giao nhau của hai tập hợp như sau, có nghĩa là các mục xuất hiện trong cả hai tập hợp.
local function getIntersection(set1, set2)
local result = Set.new()
for key, value in set1 do
if set2:contains(key) then
result:add(key)
end
end
return result
end
local freshFruits = Set.new({"Mango", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
local frozenFruits = Set.new({"Mango", "Peach", "Pineapple"})
local commonFruits = getIntersection(freshFruits, frozenFruits)
commonFruits:output() -- Mango, PeachHợp nhất
Bạn có thể lấy hợp nhất của hai tập hợp với hàm sau, có nghĩa là một tập hợp các mục trong cả hai tập hợp mà không có phần tử trùng lặp. Lưu ý rằng hàm này sử dụng phương thức metatable __add để cung cấp một phím tắt cộng set1 + set2.
function Set:__add(otherSet)
local result = Set.new()
for entry in self do
result[entry] = true
end
for entry in otherSet do
result[entry] = true
end
return result
end
local sweetFruits = Set.new({"Apple", "Mango", "Cherry", "Peach"})
local sourFruits = Set.new({"Lemon", "Lime"})
local allFruits = sweetFruits + sourFruits
allFruits:output() -- Peach, Lime, Apple, Cherry, Lemon, MangoTrừ
Bạn có thể xóa tất cả các mục trong một tập hợp khỏi các mục trong một tập hợp khác thông qua hàm sau. Tương tự như hàm trên, hàm này sử dụng phương thức metatable __sub để cung cấp một phím tắt trừ set1 - set2.
function Set:__sub(otherSet)
local result = Set.new()
for entry in self do
result[entry] = true
end
for entry in otherSet do
result[entry] = nil
end
return result
end
local allFruits = Set.new({"Apple", "Lemon", "Mango", "Cherry", "Lime", "Peach"})
local sourFruits = Set.new({"Lemon", "Lime"})
local sweetFruits = allFruits - sourFruits
sweetFruits:output() -- Mango, Apple, Cherry, Peach