أفضل النصائح: تحديد المفاهيم الفريدة والمزالق في Lua
API7.ai
October 12, 2022
في المقال السابق، تعلمنا عن دوال المكتبة المتعلقة بالجداول في LuaJIT. بالإضافة إلى هذه الدوال الشائعة، سأقدم لكم اليوم بعض المفاهيم الفريدة أو الأقل شيوعًا في Lua والمزالق الشائعة في OpenResty.
الجدول الضعيف (Weak Table)
أولاً، هناك الجدول الضعيف، وهو مفهوم فريد في Lua، ويرتبط بجمع القمامة. مثل اللغات عالية المستوى الأخرى، Lua تقوم بجمع القمامة تلقائيًا، ولا داعي للقلق بشأن التنفيذ، ولا داعي لاستدعاء GC بشكل صريح. سيقوم جامع القمامة بجمع المساحة التي لا يتم الرجوع إليها تلقائيًا.
ولكن العد البسيط للرجوع ليس كافيًا تمامًا، وأحيانًا نحتاج إلى آلية أكثر مرونة. على سبيل المثال، إذا أدخلنا كائن Lua Foo
(جدول أو دالة) في الجدول tb
، فإن هذا ينشئ رجوعًا إلى هذا الكائن Foo
. حتى إذا لم يكن هناك أي رجوع آخر إلى Foo
، فإن الرجوع إليه في tb
سيظل موجودًا دائمًا، وبالتالي لا توجد طريقة لـ GC لاستعادة الذاكرة التي يشغلها Foo
. في هذه الحالة، لدينا خياران فقط.
- الأول هو تحرير
Foo
يدويًا. - الثاني هو جعله مقيمًا في الذاكرة.
على سبيل المثال، الكود التالي.
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
print(#tb) -- 2
collectgarbage()
print(#tb) -- 2
table.remove(tb, 1)
print(#tb) -- 1
ومع ذلك، أعتقد أنك لا تريد أن تظل الذاكرة مشغولة بكائنات لا تستخدمها، خاصة وأن LuaJIT لديها حد ذاكرة 2 جيجابايت. توقيت التحرير اليدوي ليس سهلًا ويضيف تعقيدًا إلى الكود الخاص بك.
ثم يأتي دور الجدول الضعيف. انظر إلى اسمه، الجدول الضعيف. أولاً، هو جدول، ثم كل العناصر في هذا الجدول هي روابط ضعيفة. المفهوم دائمًا مجرد، لذا لنبدأ بالنظر إلى قطعة كود معدلة قليلاً.
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "v"})
print(#tb) -- 2
collectgarbage()
print(#tb) -- 0
'
كما ترى، يتم تحرير الكائنات التي لا يتم استخدامها. أهم ما في ذلك هو السطر التالي من الكود.
setmetatable(tb, {__mode = "v"})
هل تشعر بأنك رأيت هذا من قبل؟ أليست هذه عملية الجدول الواصف (metatable)؟ نعم، الجدول يصبح جدولًا ضعيفًا عندما يحتوي على حقل __mode
في جدوله الواصف.
- إذا كانت قيمة
__mode
هيk
، فإن مفتاح الجدول هو رجوع ضعيف. - إذا كانت قيمة
__mode
هيv
، فإن قيمة الجدول هي رجوع ضعيف. - بالطبع، يمكنك أيضًا تعيينها إلى
kv
، مما يشير إلى أن كل من المفاتيح والقيم في هذا الجدول هي روابط ضعيفة.
أي من هذه الجداول الضعيفة الثلاثة سيتم استعادة كائن المفتاح-القيمة بالكامل بمجرد استعادة مفتاحه أو قيمته.
في مثال الكود أعلاه، قيمة __mode
هي v
، tb
هو مصفوفة، وقيمة المصفوفة هي الجدول وكائن الدالة بحيث يمكن إعادة تدويرها تلقائيًا. ومع ذلك، إذا قمت بتغيير قيمة __mode
إلى k
، فلن يتم تحريرها، على سبيل المثال، إذا نظرت إلى الكود التالي.
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "k"})
print(#tb) -- 2
collectgarbage()
print(#tb) -- 2
'
نحن فقط نوضح الجداول الضعيفة حيث القيمة هي رجوع ضعيف، أي الجداول الضعيفة من نوع المصفوفة. بشكل طبيعي، يمكنك أيضًا بناء جدول ضعيف من نوع جدول التجزئة باستخدام كائن كمفتاح، على سبيل المثال، كما يلي.
$ resty -e 'local tb = {}
tb[{color = red}] = "red"
local fc = function() print("func") end
tb[fc] = "func"
fc = nil
setmetatable(tb, {__mode = "k"})
for k,v in pairs(tb) do
print(v)
end
collectgarbage()
print("----------")
for k,v in pairs(tb) do
print(v)
end
'
بعد استدعاء collectgarbage()
يدويًا لإجبار GC، سيتم تحرير جميع العناصر في الجدول tb
. بالطبع، في الكود الفعلي، لا نحتاج إلى استدعاء collectgarbage()
يدويًا، حيث سيتم تشغيله تلقائيًا في الخلفية، ولا داعي للقلق بشأنه.
ومع ذلك، بما أننا ذكرنا دالة collectgarbage()
، سأقول بضع كلمات عنها. يمكن تمرير عدة خيارات مختلفة لهذه الدالة وتكون القيمة الافتراضية collect
، وهي GC كامل. خيار آخر مفيد هو count
، والذي يعيد مقدار مساحة الذاكرة التي يشغلها Lua. هذه الإحصائية مفيدة لرؤية ما إذا كان هناك تسرب للذاكرة وتذكرنا بعدم الاقتراب من الحد الأعلى 2 جيجابايت.
الكود المتعلق بالجداول الضعيفة أكثر تعقيدًا في الكتابة عمليًا، وأقل سهولة في الفهم، وبالتالي، يحتوي على أخطاء خفية أكثر. لا داعي للعجلة. لاحقًا، سأقدم مشروعًا مفتوح المصدر، يستخدم الجداول الضعيفة التي تسبب مشكلة تسرب الذاكرة.
الإغلاق (Closure) و upvalue
بالانتقال إلى الإغلاق و upvalue، كما أكدت سابقًا، جميع القيم هي مواطنون من الدرجة الأولى في Lua، وكذلك الدوال المضمنة. هذا يعني أن الدوال يمكن تخزينها في متغيرات، تمريرها كمعاملات، وإعادتها كقيم لدالة أخرى. على سبيل المثال، يظهر هذا الكود النموذجي في الجدول الضعيف أعلاه.
tb[2] = function() print("func") end
إنها دالة مجهولة يتم تخزينها كقيمة في جدول.
في Lua، تعريف الدالتين في الكود التالي متكافئ. ومع ذلك، لاحظ أن الأخير يعين دالة لمتغير، وهي طريقة نستخدمها غالبًا.
local function foo() print("foo") end
local foo = fuction() print("foo") end
بالإضافة إلى ذلك، Lua تدعم كتابة دالة داخل دالة أخرى، أي الدوال المتداخلة، مثل الكود النموذجي التالي.
$ resty -e '
local function foo()
local i = 1
local function bar()
i = i + 1
print(i)
end
return bar
end
local fn = foo()
print(fn()) -- 2
'
يمكنك أن ترى أن دالة bar
يمكنها قراءة المتغير المحلي i
داخل دالة foo
وتعديل قيمته، حتى إذا لم يتم تعريف المتغير داخل bar
. هذه الميزة تسمى النطاق المعجمي (lexical scoping).
هذه الميزات في Lua هي الأساس للإغلاق. الإغلاق هو ببساطة دالة تصل إلى متغير في النطاق المعجمي لدالة أخرى.
بحكم التعريف، جميع الدوال في Lua هي في الواقع إغلاقات، حتى إذا لم تتداخل. هذا لأن مترجم Lua يأخذ خارج نص Lua ويغلقه بطبقة أخرى من الدالة الرئيسية. على سبيل المثال، الأسطر البسيطة التالية من الكود.
local foo, bar
local function fn()
foo = 1
bar = 2
end
بعد التجميع، سيبدو هكذا.
function main(...)
local foo, bar
local function fn()
foo = 1
bar = 2
end
end
ودالة fn
تلتقط متغيرين محليين من الدالة الرئيسية، لذا فهي أيضًا إغلاق.
بالطبع، نعلم أن مفهوم الإغلاق موجود في العديد من اللغات، وليس فريدًا في Lua، لذا يمكنك المقارنة والتباين لفهم أفضل. فقط عندما تفهم الإغلاق يمكنك فهم ما سنقوله عن upvalue
.
upvalue
هو مفهوم فريد في Lua، وهو المتغير خارج النطاق المعجمي الذي يتم التقاطه في الإغلاق. لنستمر مع الكود أعلاه.
local foo, bar
local function fn()
foo = 1
bar = 2
end
يمكنك أن ترى أن دالة fn
تلتقط متغيرين محليين، foo
و bar
، اللذين ليسا في نطاقهما المعجمي الخاص، وأن هذين المتغيرين هما في الواقع upvalue
للدالة fn
.
المزالق الشائعة
بعد تقديم بعض المفاهيم في Lua، سأتحدث عن المزالق المتعلقة بـ Lua التي واجهتها في تطوير OpenResty.
في القسم السابق، ذكرنا بعض الاختلافات بين Lua ولغات التطوير الأخرى، مثل أن الفهرس يبدأ من 1
، المتغيرات العامة الافتراضية، إلخ. في تطوير الكود الفعلي لـ OpenResty، سنواجه المزيد من المشاكل المتعلقة بـ Lua و LuaJIT، وسأتحدث عن بعض الأكثر شيوعًا أدناه.
هنا تذكير بأنه حتى إذا كنت تعرف جميع المزالق، فستضطر حتمًا إلى المرور بها بنفسك لتكون متأثرًا. الفرق، بالطبع، هو أنك ستكون قادرًا على الخروج من الحفرة والعثور على جوهر المشكلة بطريقة أفضل بكثير.
هل الفهرس يبدأ من 0 أم 1؟
المزلق الأول هو أن فهرس Lua يبدأ من 1
، كما ذكرنا مرارًا وتكرارًا من قبل.
ولكن يجب أن أقول أن هذا ليس الحقيقة الكاملة. لأنه في LuaJIT، المصفوفات التي تم إنشاؤها باستخدام ffi.new
تبدأ فهرسها من 0
مرة أخرى:
local buf = ffi_new("char[?]", 128)
لذا، إذا كنت تريد الوصول إلى buf
cdata
في الكود أعلاه، تذكر أن الفهرس يبدأ من 0
، وليس 1
. تأكد من الانتباه بشكل خاص إلى هذا المكان عند استخدام FFI للتفاعل مع C.
مطابقة النمط العادي (Regular Pattern Match)
المزلق الثاني هو مشكلة مطابقة النمط العادي، وهناك مجموعتان من طرق مطابقة السلاسل في OpenResty: مكتبة string في Lua وواجهة برمجة التطبيقات ngx.re.*
في OpenResty.
مطابقة النمط العادي في Lua هي تنسيقها الفريد وتكتب بشكل مختلف عن PCRE. هنا مثال بسيط.
resty -e 'print(string.match("foo 123 bar", "%d%d%d"))' — 123
هذا الكود يستخرج الجزء الرقمي من السلسلة، وستلاحظ أنه مختلف تمامًا عن التعبيرات العادية التي نعرفها. مكتبة مطابقة النمط العادي في Lua مكلفة في الصيانة ومنخفضة الأداء - JIT لا يمكنها تحسينها، والأنماط التي تم تجميعها مرة واحدة لا يتم تخزينها مؤقتًا.
لذا، عند استخدام مكتبة string المضمنة في Lua لـ find
، match
، إلخ، لا تتردد في استخدام ngx.re
من OpenResty بدلاً من ذلك إذا كنت بحاجة إلى شيء مثل التعبير العادي. عند البحث عن سلسلة ثابتة، ننظر في استخدام الوضع العادي لاستدعاء مكتبة string.
هنا اقتراح: في OpenResty، نعطي الأولوية دائمًا لواجهة برمجة التطبيقات في OpenResty، ثم واجهة برمجة التطبيقات في LuaJIT، ونستخدم مكتبات Lua بحذر.
ترميز JSON لا يميز بين المصفوفة والقاموس
المزلق الثالث هو أن ترميز JSON لا يميز بين المصفوفة والقاموس؛ نظرًا لأن Lua لديها بنية بيانات واحدة فقط، وهي table، عند ترميز جدول فارغ في JSON، لا توجد طريقة لتحديد ما إذا كان مصفوفة أو قاموسًا.
resty -e 'local cjson = require "cjson"
local t = {}
print(cjson.encode(t))
'
على سبيل المثال، الكود أعلاه يخرج {}
، مما يظهر أن مكتبة cjson
في OpenResty تقوم بترميز الجدول الفارغ كقاموس افتراضيًا. بالطبع، يمكننا تغيير هذا الافتراضي العالمي باستخدام دالة encode_empty_table_as_object
.
resty -e 'local cjson = require "cjson"
cjson.encode_empty_table_as_object(false)
local t = {}
print(cjson.encode(t))
'
هذه المرة، يتم ترميز الجدول الفارغ كمصفوفة []
.
ومع ذلك، هذا الإعداد العالمي له تأثير كبير، لذا هل يمكننا تحديد قواعد الترميز لجدول معين؟ الجواب طبعًا نعم، وهناك طريقتان للقيام بذلك.
الطريقة الأولى هي تعيين userdata
cjson.empty_array
إلى الجدول المحدد بحيث يتم التعامل معه كمصفوفة فارغة عند الترميز في JSON.
$ resty -e 'local cjson = require "cjson"
local t = cjson.empty_array
print(cjson.encode(t))
'
ومع ذلك، أحيانًا لا نكون متأكدين مما إذا كان الجدول المحدد فارغًا دائمًا. نريد ترميزه كمصفوفة عندما يكون فارغًا، لذا نستخدم دالة cjson.empty_array_mt
، وهي طريقتنا الثانية.
ستقوم بتمييز الجدول المحدد وترميزه كمصفوفة عندما يكون الجدول فارغًا. كما يمكنك أن ترى من اسم cjson.empty_array_mt
، يتم تعيينه باستخدام metatable
، كما في عملية الكود التالية.
$ resty -e 'local cjson = require "cjson"
local t = {}
setmetatable(t, cjson.empty_array_mt)
print(cjson.encode(t))
t = {123}
print(cjson.encode(t))
'
الحد على عدد المتغيرات
لننظر إلى المزلق الرابع، الحد على عدد المتغيرات. Lua لديها حد أعلى لعدد المتغيرات المحلية وعدد upvalue
في دالة، كما يمكنك أن ترى من مصدر Lua.
/*
@@ LUAI_MAXVARS هو الحد الأقصى لعدد المتغيرات المحلية لكل دالة
@* (يجب أن يكون أصغر من 250).
*/
#define LUAI_MAXVARS 200
/*
@@ LUAI_MAXUPVALUES هو الحد الأقصى لعدد upvalues لكل دالة
@* (يجب أن يكون أصغر من 250).
*/
#define LUAI_MAXUPVALUES 60
هذه العتبات محددة بشكل ثابت بـ 200
و 60
على التوالي، وعلى الرغم من أنه يمكنك تعديل مصدر الكود يدويًا لضبط هذه القيم، إلا أنه يمكن تعيينها فقط إلى حد أقصى 250
.
بشكل عام، لا نتجاوز هذا الحد. ومع ذلك، عند كتابة كود OpenResty، يجب أن تكون حريصًا على عدم الإفراط في استخدام المتغيرات المحلية و upvalue
، ولكن استخدام do ... end
قدر الإمكان لتقليل عدد المتغيرات المحلية و upvalue
.
على سبيل المثال، لننظر إلى الكود الزائف التالي.
local re_find = ngx.re.find
function foo() ... end
function bar() ... end
function fn() ... end
إذا كانت الدالة foo
فقط تستخدم re_find
، فيمكننا تعديلها كما يلي:
do
local re_find = ngx.re.find
function foo() ... end
end
function bar() ... end
function fn() ... end
الخلاصة
من وجهة نظر "طرح المزيد من الأسئلة"، من أين يأتي حد 250
في Lua؟ هذا هو سؤالنا التفكيري لليوم. نرحب بترك تعليقاتك ومشاركة هذا المقال مع زملائك وأصدقائك. سنتواصل ونحسن معًا.