ما هو الجدول والجدول الوصفي في Lua؟

API7.ai

October 11, 2022

OpenResty (NGINX + Lua)

اليوم سنتعلم عن بنية البيانات الوحيدة في 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 كطبقة تغليف؟ مرحبًا بكم لمناقشة هذا السؤال في قسم التعليقات، ومرحبًا بكم لمشاركة هذه المقالة مع زملائكم وأصدقائكم حتى نتمكن من التواصل والتقدم معًا.