ما هو الجدول والجدول الوصفي في Lua؟
API7.ai
October 11, 2022
اليوم سنتعلم عن بنية البيانات الوحيدة في LuaJIT: table
.
على عكس لغات البرمجة النصية الأخرى التي تحتوي على هياكل بيانات غنية، فإن LuaJIT لديها بنية بيانات واحدة فقط، وهي table
، والتي لا يتم تمييزها عن المصفوفات، الهاشات، المجموعات، وما إلى ذلك، ولكنها نوعًا ما مختلطة. دعونا نراجع أحد الأمثلة التي ذكرناها سابقًا.
local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil
في هذا المثال، يحتوي الجدول color
على مصفوفة وهاش ويمكن الوصول إليهما دون أن يتداخلا مع بعضهما البعض. على سبيل المثال، يمكنك استخدام الدالة ipairs
للتكرار فقط خلال الجزء المصفوفي من الجدول.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
for k, v in ipairs(color) do
print(k)
end
'
عمليات table
مهمة جدًا لدرجة أن LuaJIT قامت بتوسيع مكتبة الجداول القياسية في Lua 5.1، كما قام OpenResty بتوسيع مكتبة الجداول في LuaJIT بشكل أكبر. دعونا نلقي نظرة على كل من هذه الدوال المكتبية.
دوال مكتبة الجداول
لنبدأ بدوال مكتبة الجداول القياسية. Lua 5.1 لا تحتوي على العديد من دوال مكتبة الجداول، لذا يمكننا التصفح بسرعة.
table.getn
الحصول على عدد العناصر
كما ذكرنا في فصل Lua القياسية وLuaJIT، فإن الحصول على العدد الصحيح لجميع عناصر الجدول يمثل مشكلة كبيرة في LuaJIT.
بالنسبة للتسلسلات، يمكنك استخدام table.getn
أو العامل الأحادي #
لإرجاع العدد الصحيح للعناصر. المثال التالي يعيد العدد 3 الذي نتوقعه.
$ resty -e 'local t = { 1, 2, 3 }
print(table.getn(t))
لا يمكن إرجاع القيمة الصحيحة للجداول التي ليست متسلسلة. في المثال الثاني، القيمة التي يتم إرجاعها هي 1.
$ resty -e 'local t = { 1, a = 2 }
print(#t) '
لحسن الحظ، تم استبدال هذه الدوال الصعبة الفهم بواسطة توسيعات LuaJIT، والتي سنذكرها لاحقًا. لذا في سياق OpenResty، لا تستخدم الدالة table.getn
والعامل الأحادي #
إلا إذا كنت تعرف بشكل صريح أنك تحصل على طول التسلسل.
أيضًا، table.getn
والعامل الأحادي #
ليسا من نوع O(1) بل من نوع O(n)، وهو سبب آخر لتجنبهما إذا أمكن.
table.remove
إزالة العنصر المحدد
الثانية هي دالة table.remove
، والتي تقوم بإزالة العناصر في الجدول بناءً على الفهارس، أي يمكن إزالة العناصر الموجودة فقط في الجزء المصفوفي من الجدول. دعونا نلقي نظرة على مثال color
مرة أخرى.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.remove(color, 1)
for k, v in pairs(color) do
print(v)
end'
سيقوم هذا الكود بإزالة العنصر blue
ذو الفهرس 1. قد تسأل، كيف يمكنني حذف الجزء الهاشي من الجدول؟ الأمر بسيط، قم بتعيين القيمة المقابلة للمفتاح إلى nil
. وبالتالي، في مثال color
، يتم حذف العنصر green
المقابل لـ third
.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
color.third = nil
for k, v in pairs(color) do
print(v)
end'
table.concat
دالة دمج العناصر
الثالثة هي دالة دمج العناصر table.concat
. تقوم بدمج عناصر الجدول معًا بناءً على الفهارس. نظرًا لأن هذا يعتمد مرة أخرى على الفهارس، فإنه يظل للجزء المصفوفي من الجدول. مرة أخرى مع مثال color
.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
print(table.concat(color, ", "))'
بعد استخدام دالة table.concat
، يتم إخراج blue, yellow
ويتم تخطي الجزء الهاشي.
بالإضافة إلى ذلك، يمكن لهذه الدالة أيضًا تحديد موضع بدء الفهرس لإجراء الدمج؛ على سبيل المثال، يتم كتابتها كما يلي
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow", "orange"}
print(table.concat(color, ", ", 2, 3))'
هذه المرة يتم إخراج yellow, orange
، مع تخطي blue
.
من فضلك لا تقلل من شأن هذه الدالة التي تبدو غير مفيدة، ولكنها يمكن أن يكون لها تأثيرات غير متوقعة عند تحسين الأداء وهي واحدة من الشخصيات الرئيسية في فصول تحسين الأداء لاحقًا.
table.insert
إدراج عنصر
أخيرًا، دعونا نلقي نظرة على دالة table.insert
. تقوم بإدراج عنصر جديد في الفهرس المحدد، مما يؤثر على الجزء المصفوفي من الجدول. للتوضيح، مرة أخرى، باستخدام مثال color
.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.insert(color, 1, "orange")
print(color[1])
'
يمكنك أن ترى أن العنصر الأول من color
أصبح orange
، ولكن بالطبع، يمكنك ترك الفهرس غير محدد بحيث يتم إدراجه في نهاية الطابور بشكل افتراضي.
يجب أن أشير إلى أن table.insert
هي عملية شائعة، ولكن الأداء ليس جيدًا. إذا كنت لا تقوم بإدراج العناصر بناءً على الفهرس المحدد، فستحتاج إلى استدعاء lj_tab_len
من LuaJIT في كل مرة للحصول على طول المصفوفة لإدراجها في نهاية الطابور. كما هو الحال مع table.getn
، فإن تعقيد الوقت للحصول على طول الجدول هو O(n).
لذلك، بالنسبة لعملية table.insert
؛ يجب أن نحاول تجنب استخدامها في الأكواد الساخنة. على سبيل المثال:
local t = {}
for i = 1, 10000 do
table.insert(t, i)
end
دوال توسيع الجداول في LuaJIT
بعد ذلك، دعونا نلقي نظرة على دوال توسيع الجداول في LuaJIT. قامت LuaJIT بتوسيع Lua القياسية بدوالتين مفيدتين للجداول لإنشاء وإفراغ جدول، والتي سأصفها أدناه.
table.new(narray, nhash)
إنشاء جدول جديد
الأولى هي دالة table.new(narray, nhash)
. بدلاً من أن تنمو بنفسها عند إدراج العناصر، ستقوم هذه الدالة بتخصيص حجم المساحة المحدد للصفيف والهاش مسبقًا، وهو ما تعنيه المعلمتان narray
وnhash
. النمو الذاتي هو عملية مكلفة تتضمن تخصيص المساحة، resize
وrehash
، ويجب تجنبها بأي ثمن.
لاحظ هنا أن وثائق table.new
ليست على موقع LuaJIT ولكنها موجودة في عمق وثائق التوسيع لمشروع GitHub، لذا يصعب العثور عليها حتى إذا قمت بالبحث في Google، لذا فإن عددًا قليلاً من المهندسين يعرفون عنها.
إليك مثال بسيط، وسأريكم كيف تعمل. أولاً، هذه الدالة هي دالة موسعة، لذا قبل أن تتمكن من استخدامها، تحتاج إلى require
لها.
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end
كما ترون، يقوم هذا الكود بإنشاء جدول جديد يحتوي على 100 عنصر صفيف و0 عنصر هاش. بالطبع، يمكنك إنشاء جدول جديد يحتوي على 100 عنصر صفيف و50 عنصر هاش حسب الحاجة، وهذا قانوني.
local t = new_tab(100, 50)
بدلاً من ذلك، إذا تجاوزت حجم المساحة المحدد مسبقًا، فلا يزال يمكنك استخدامها بشكل طبيعي، ولكن الأداء سيتدهور، وستفقد نقطة استخدام table.new
.
في المثال التالي، لدينا حجم مسبق التحديد هو 100، ولكننا نستخدم 200.
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 200 do
t[i] = i
end
تحتاج إلى تحديد حجم مساحة الصفيف والهاش في table.new
وفقًا للسيناريو الفعلي حتى تتمكن من العثور على توازن بين الأداء واستخدام الذاكرة.
table.clear()
إفراغ الجدول
الثانية هي دالة الإفراغ table.clear()
. تقوم بإفراغ جميع البيانات في الجدول ولكنها لا تحرر الذاكرة التي تشغلها أجزاء الصفيف والهاش. لذلك، فهي مفيدة عند إعادة تدوير الجداول في Lua لتجنب تكاليف إنشاء وتدمير الجداول بشكل متكرر.
$ resty -e 'local clear_tab =require "table.clear"
local color = {first = "red", "blue", third = "green", "yellow"}
clear_tab(color)
for k, v in pairs(color) do
print(k)
end'
ومع ذلك، لا توجد العديد من السيناريوهات التي يمكن استخدام هذه الدالة فيها، وفي معظم الحالات، يجب أن نترك هذه المهمة لـ GC في LuaJIT.
دوال توسيع الجداول في OpenResty
كما ذكرت في البداية، يحتفظ OpenResty بفرع خاص به من LuaJIT، والذي قام أيضًا بتوسيع الجداول، بعدة واجهات برمجية جديدة: table.isempty,
table. isarray
, table.nkeys
وtable.clone
.
قبل استخدام هذه الواجهات البرمجية الجديدة، يرجى التحقق من إصدار OpenResty، حيث يمكن استخدام معظم هذه الواجهات البرمجية فقط في إصدارات OpenResty بعد 1.15.8.1. وذلك لأن OpenResty لم يكن لديه إصدار جديد لمدة عام تقريبًا قبل الإصدار 1.15.8.1، وتمت إضافة هذه الواجهات البرمجية في تلك الفترة.
لقد قمت بتضمين رابط المقالة، لذا سأستخدم table.nkeys
كمثال. الواجهات البرمجية الثلاث الأخرى واضحة من حيث التسمية، لذا قم بالاطلاع على وثائق GitHub، وستفهمها. يجب أن أقول إن وثائق OpenResty عالية الجودة، بما في ذلك أمثلة التعليمات البرمجية، سواء كانت يمكن أن تكون JIT، وما يجب الانتباه إليه، وما إلى ذلك. عدة أوامر من حيث الحجم أفضل من وثائق Lua وLuaJIT.
حسنًا، عد إلى دالة table.nkeys
. قد تربكك تسميتها، ولكنها دالة تحصل على طول الجدول وتعيد عدد عناصر الجدول، بما في ذلك عناصر الصفيف والجزء الهاشي. لذلك، يمكننا استخدامها بدلاً من table.getn
، على سبيل المثال، كما يلي.
local nkeys = require "table.nkeys"
print(nkeys({})) -- 0
print(nkeys({ "a", nil, "b" })) -- 2
print(nkeys({ dog = 3, cat = 4, bird = nil })) -- 2
print(nkeys({ "a", dog = 3, cat = 4 })) -- 3
الجداول الوصفية
بعد الحديث عن دالة الجداول، دعونا نلقي نظرة على metatable
المشتقة من table
. الجداول الوصفية هي مفهوم فريد في Lua، وتستخدم على نطاق واسع في المشاريع الواقعية. ليس من المبالغة القول إنك ستجدها في أي مكتبة lua-resty-*
تقريبًا.
تتصرف الجداول الوصفية مثل التحميل الزائد للعوامل؛ على سبيل المثال، يمكننا التحميل الزائد لـ __add
لحساب دمج مصفوفتين في Lua أو __tostring
لتحديد الدوال التي تحول إلى سلاسل.
من ناحية أخرى، توفر Lua دالتين للتعامل مع الجداول الوصفية.
- الأولى هي
setmetatable(table, metatable)
، والتي تقوم بإعداد جدول وصفي لجدول. - الثانية هي
getmetatable(table)
، والتي تحصل على الجدول الوصفي للجدول.
بعد كل هذا، قد تكون أكثر اهتمامًا بما تفعله، لذا دعونا نلقي نظرة على ما يتم استخدام الجداول الوصفية بشكل محدد. إليك جزء من الكود من مشروع فعلي.
$ resty -e ' local version = {
major = 1,
minor = 1,
patch = 1
}
version = setmetatable(version, {
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(tostring(version))
'
نقوم أولاً بتعريف جدول باسم version
، وكما ترون، فإن الغرض من هذا الكود هو طباعة رقم الإصدار في version
. ومع ذلك، لا يمكننا طباعة version
مباشرة. يمكنك تجربة ذلك وسترى أن الطباعة المباشرة ستخرج فقط عنوان الجدول.
print(tostring(version))
لذلك، نحتاج إلى تخصيص دالة تحويل السلسلة لهذا الجدول، وهي __tostring
، وهنا يأتي دور الجدول الوصفي. نستخدم setmetatable
لإعادة تعيين طريقة __tostring
للجدول version
لطباعة رقم الإصدار: 1.1.1.
بالإضافة إلى __tostring
، فإننا غالبًا ما نقوم بتجاوز الطريقتين التاليتين في الجدول الوصفي في المشاريع الواقعية.
إحداهما هي __index. عندما نبحث عن عنصر في جدول، فإننا نبحث عنه أولاً مباشرة من الجدول، وإذا لم نجده، ننتقل إلى __index
في الجدول الوصفي.
نقوم بإزالة patch
من الجدول version
في المثال التالي.
$ resty -e ' local version = {
major = 1,
minor = 1
}
version = setmetatable(version, {
__index = function(t, key)
if key == "patch" then
return 2
end
end,
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(tostring(version))
'
في هذه الحالة، لا يحصل t.patch
على القيمة، لذا ينتقل إلى دالة __index
، والتي تطبع 1.1.2.
__index
يمكن أن تكون ليست فقط دالة ولكن أيضًا جدولًا، وإذا حاولت تشغيل الكود التالي، فسترى أنها تحقق نفس النتيجة.
$ resty -e ' local version = {
major = 1,
minor = 1
}
version = setmetatable(version, {
__index = {patch = 2},
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(tostring(version))
'
طريقة وصفية أخرى هي __call. إنها تشبه الدالة التي تسمح باستدعاء جدول.
دعونا نبني على الكود أعلاه الذي يطبع رقم الإصدار ونرى كيف يمكن استدعاء جدول.
$ resty -e '
local version = {
major = 1,
minor = 1,
patch = 1
}
local function print_version(t)
print(string.format("%d.%d.%d", t.major, t.minor, t.patch))
end
version = setmetatable(version,
{__call = print_version})
version()
'
في هذا الكود، نستخدم setmetatable
لإضافة جدول وصفي للجدول version
، وتشير الطريقة الوصفية __call
بداخله إلى الدالة print_version
. لذا، إذا حاولنا استدعاء version
كدالة، فسيتم تنفيذ الدالة print_version
هنا.
و getmetatable
هي العملية المقترنة مع setmetatable
للحصول على الجدول الوصفي الذي تم تعيينه، مثل الكود التالي.
$ resty -e ' local version = {
major = 1,
minor = 1
}
version = setmetatable(version, {
__index = {patch = 2},
__tostring = function(t)
return string.format("%d.%d.%d", t.major, t.minor, t.patch)
end
})
print(getmetatable(version).__index.patch)
'
بالإضافة إلى هذه الطرق الوصفية الثلاث التي تحدثنا عنها اليوم، هناك بعض الطرق الوصفية الأخرى التي لا تُستخدم كثيرًا والتي يمكنك الرجوع إلى الوثائق لمعرفة المزيد عنها عندما تواجهها.
البرمجة الكائنية
أخيرًا، دعونا نتحدث عن البرمجة الكائنية. كما تعلمون، Lua ليست لغة برمجة كائنية، ولكن يمكننا استخدام الجداول الوصفية لتنفيذ OO.
دعونا نلقي نظرة على مثال عملي. lua-resty-mysql هو العميل الرسمي لـ MySQL في OpenResty، ويستخدم الجداول الوصفية لمحاكاة الفئات وطرق الفئات، والتي تُستخدم بالطريقة التالية.
$ resty -e 'local mysql = require "resty.mysql" -- أولاً قم بالإشارة إلى مكتبة lua-resty
local db, err = mysql:new() -- إنشاء مثيل جديد من الفئة
db:set_timeout(1000) -- استدعاء طرق الفئة
يمكنك تنفيذ الكود أعلاه مباشرة باستخدام سطر الأوامر resty
. هذه الأسطر من الكود سهلة الفهم؛ الشيء الوحيد الذي قد يسبب لك مشكلة هو.
عند استدعاء طريقة الفئة، لماذا يتم استخدام النقطتين بدلاً من النقطة؟
في الواقع، كل من النقطتين والنقطة جيدان هنا، وdb:set_timeout(1000)
وdb.set_timeout(db, 1000)
متكافئان تمامًا. النقطتان هي سكر نحوي في Lua تسمح بحذف الوسيطة الأولى self
للدالة.
كما نعلم جميعًا، لا توجد أسرار أمام الكود المصدري، لذا دعونا نلقي نظرة على التنفيذ المحدد المقابل للأسطر السابقة من الكود حتى تتمكن من فهم أفضل لكيفية محاكاة البرمجة الكائنية باستخدام الجداول الوصفية.
local _M = { _VERSION = '0.21' } -- استخدام الجدول لمحاكاة الفئة
local mt = { __index = _M } -- mt هي اختصار لـ metatable، __index تشير إلى الفئة نفسها
-- مُنشئ الفئة
function _M.new(self)
local sock, err = tcp()
if not sock then
return nil, err
end
return setmetatable({ sock = sock }, mt) -- مثال على محاكاة الفئات باستخدام الجدول والجدول الوصفي
end
-- وظائف الأعضاء في الفئة
function _M.set_timeout(self, timeout) -- استخدام الوسيطة self للحصول على مثيل الفئة الذي تريد التعامل معه
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:settimeout(timeout)
end
الجدول _M
يحاكي فئة يتم تهيئتها بمتغير عضو واحد _VERSION
ويتم تعريف وظائف الأعضاء مثل _M.set_timeout
لاحقًا. في المُنشئ _M.new(self)
، نعيد جدولًا يكون جدوله الوصفي هو mt
، وتشير الطريقة الوصفية __index
لـ mt
إلى _M
بحيث يعيد الجدول محاكاة مثيل الفئة _M
.
الخلاصة
حسنًا، هذا يختتم المحتوى الرئيسي لليوم. يتم استخدام الجداول والجداول الوصفية بشكل كبير في مكتبات lua-resty-*
في OpenResty والمشاريع المفتوحة المصدر القائمة على OpenResty. آمل أن يجعل هذا الدرس من السهل عليك قراءة وفهم الكود المصدري.
هناك وظائف قياسية أخرى في Lua بالإضافة إلى الجداول، والتي سنتعلمها معًا في الدرس القادم.
أخيرًا، أود أن أترككم مع سؤال مثير للتفكير. لماذا تقوم مكتبة lua-resty-mysql
بمحاكاة OO كطبقة تغليف؟ مرحبًا بكم لمناقشة هذا السؤال في قسم التعليقات، ومرحبًا بكم لمشاركة هذه المقالة مع زملائكم وأصدقائكم حتى نتمكن من التواصل والتقدم معًا.