الذاكرة


الذاكرة (RAM) مقسمة إلى وحدات ذاكرية نسميها البايت (Byte) وهي تعادل 8 بتات (bit) وهذه الوحدة هي أصغر وحدة يمكنك حجزها لتخزين المتغيرات. حيث يتم ترقيم كل بايت في الذاكرة برقم متسلسل يبدأ ب 0 وندعو هذا الرقم عنوان الذاكرة (memory address).
إذاً، بفرض كانت سعة الذاكرة لديك هي 1KB (في الواقع تكون أكبر بكثير مثلاً 4 غيغا أو 8 أو 16 إلخ..) فهذا يعني أن الذاكرة مكونة من 1024 بايت لأن 1KB تعادل 1024 بايت، وبالتالي سيكون لدينا 1024 عنوان ذاكرة متاح، بدءاً من العنوان 0 (أول وحدة ذاكرية -بايت-) و وصولاً للعنوان 1023 (آخر بايت).
كما يجب التنويه إلى أن معمارية الحواسيب هي من يحدد عدد العناوين الذاكرية التي يمكن الوصول إليها، فلابد وأنك قد سمعت بمصطلح "هذا المعالج أو هذا الحاسوب 32 بت أو 64 بت" هذا الرقم متعلق بمعمارية المعالج. ويمثل عدد العناوين التي يمكن الوصول لها أو عنونتها كما ذكرت، فالمعمارية ب 32 بت تستطيع عنونة 2 للأس 32 أي 4294967296 عنوان وهذا يعادل ذاكرة RAM بحجم 4 غيغابايت.وهذه ال 4294967296 عنوان يمكن تخزينها في 32 بت (4 بايت) وهو حجم المتغير من النوع مؤشر (Pointer) في هذه المعمارية (معمارية 32-bit).

المؤشرات


تمتاز لغة الأسس بقدرتها على التعامل مع الذاكرة بشكل مباشر ما يتيح إنشاء برمجيات ذات كفاءة عالية في إستخدام الذاكرة، ويرجع الفضل بشكل كبير إلى المؤشرات، حيث يمكن أن يتم التعامل مع أي متغير أي كان حجمه بمجرد معرفة عنوانه. وتستخدم المؤشرات بشكل كبير للتعامل مع المصفوفات، حيث تعتبر المصفوفة نوع من أنواع المؤشرات وبالتالي يتم إستخدام العمليات الحسابية على المؤشرات (pointer arithmetic) للتنقل بين عناصر المصفوفة كما سنتعرف عليه لاحقا. ويتم إستخدام المؤشرات لتمرير المتغيرات ذات الحجم الكبير بين الدوال، فعند تمرير أي متغير لأي دالة عادة ما يتم إعادة نسخ المتغير في المساحة المخصصة من الذاكرة للدالة (بإستثناء المصفوفات) وتسبب هذه العملية إهدار للوقت المستغرق في عملية النسخ وإهدار للذاكرة المتاحة حيث يتم نسخ المتغير والإحتفاظ بالمتغير الأصلي أي أن المتغير يوجد في الذاكرة مرتين، وتعرف عملية تمرير المؤشرات للدوال بالتمرير عن طريق المرجع أو العنوان (pass by reference). تعتبر المؤشرات موضوع حيوي و أساسي عند التحدث عن هياكل البيانات (data structures)، حيث تستخدم المؤشرات للربط بين وحدات البيانات أو كما تعرف بـ Nodes. و تستخدم المؤشرات لإنشاء متغيرات ديناميكية أو كما تعرف بـ dynamic-variables والتي تمكن المبرمج من إنشاء متغيرات أثناء تشغيل البرنامج عند الحاجة إليها، أو إنشاء مصفوفات بالحجم الذي يحدده مستخدم البرنامج، فكما تعلمنا أن المصفوفات في لغة البرمجة C يجب أن يتم تحديد عدد عناصرها بشكل ثابت أثناء إنشائها، ولكن بإستخدام المؤشرات يمكنك إنشاء مصفوفات بأي حجم يحدده المستخدم أثناء التشغيل وهو ما يعرف بالمصفوفات المرنة أو الديناميكية (dynamic arrays).


- ماهو المؤشر (pointer)؟
المؤشر عبارة عن متغير يستخدم للإشارة إلى موقع في الذاكرة، وبمكننا من خلاله التحكم بمحتويات ذلك الموقع. أو يمكن القول بأنه عبارة عن متغير تحتوي قيمته على عنوان متغير آخر في الذاكرة، ومن الاسم فهو يشير إلى عنوان متغير آخر.

بدايةً دعونا نتفق على أن كل متغير يتم إنشاؤه في البرنامج يخصص له موقع في الذاكرة وعنوان ذاكري. ويمكننا معرفة هذا العنوان من خلال ذكر اسم المتغير متبوعاً بالمعامل (أو المؤثر) `~مؤشر` (~ptr)، كما في المثال التالي:


  اشمل "مـتم/طـرفية"؛
  استخدم مـتم.طـرفية؛
  عرف س: صحيح؛
  س = 51؛
  اطبع("قيمة س: %d\ج"، س)؛ // قيمة س: 51
  اطبع("عنوان س: %p\ج"، س~مؤشر)؛ // عنوان س: 0x55a3b28493f0
import "Srl/Console.alusus";
use Srl.Console;
def x: int;
x = 51;
print("value of x: %d\n", x); // value of x: 51
print("x Address is : %p\n", x~ptr); // x Address is : 0x55a3b28493f0

كما يجب أن تدرك أن عنوان المتغير سوف يتغير في كل مرة تقوم فيها بتشغيل الكود، حيث أنه في كل مرة يتم تخصيص عنوان جديد للمتغيرات ضمن الكود.

- لتعريف متغير من الصنف `مؤشر` (ptr) في لغة الأسس نستخدم الكلمة المفتاحية `عرف` (def) يتبعها اسم المتغير يتبعها نقطتان : يتبعها اسم الصنف، أي `مؤشر` (ptr)، يتبعها صنف البيانات ضمن قوسين معقوفين لأنه عند الإعلان عن متغير من النوع `مؤشر` يجب تحديد نوع البيانات الخاص بالمتغير الآخر الذي سوف يشير إليه وذلك حتى يتمكن مترجم اللغة من تحديد حجم المتغير الآخر:

عرف اسم_المؤشر: مؤشر[صـنف_البيانات]؛
def pointer_name: ptr[Data_type];

المثال التالي يعرض لنا كيفية إنشاء متغير من الصنف `مؤشر`:

اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
عرف م: مؤشر[صحيح]؛
اطبع("قيمة المؤشر هي: %pج"، م)؛ // قيمة المؤشر هي: (nil)
// لاحظ أن قيمة المؤشر هي nil (يكافئ Null) أي لاشيء، أي أنك قمت بتعريف
// متغير من النمط ptr لكن قيمته لاتشير إلى أي موقع ذاكري.
عرف س: صحيح؛
س = 5؛
م = س~مؤشر؛ // الآن أصبح م يشير إلى عنوان الموقع الذاكرية للمتغير س
اطبع("عنوان س هو: %p\ج"، س~مؤشر): // عنوان س هو: 0x55cdf422e950
اطبع("قيمة المؤشر هي: %p\ج"، م)؛ // قيمة المؤشر هي: 0x55cdf422e950
// وللحصول على المحتوى الذي يشير إليه المتغير م نستخدم المؤثر ~محتوى
اطبع("المحتوى المشار إليه هو: %dج"، م~محتوى)؛ // المحتوى المشار إليه هو: 5
// وهذا مانسميه بالتتبع (dereferencing)، أي تحديد قيمة المتغير المستخدم مع المؤشر.
// ولتغيير قيمة المتغير عن طريق إستخدام المؤشر، فيمكننا القيام بذلك كالتالي:
م~محتوى = 9؛ // هنا قمنا بتغيير قيمة المتغير من خلال المؤشر
اطبع("قيمة س هي: %d\ج"، س)؛ // قيمة س هي: 9
import "Srl/Console.alusus";
use Srl.Console;
def p: ptr[Int];
print("Pointer Address is : %p\n", p); // Pointer Address is : (nil)
// لاحظ أن قيمة المؤشر هي nil (يكافئ Null) أي لاشيء، أي أنك قمت بتعريف
// متغير من النمط ptr لكن قيمته لاتشير إلى أي موقع ذاكري.
def x: Int;
x = 5;
p = x~ptr; // x يشير إلى عنوان الموقع الذاكري للمتغير p الآن أصبح المؤشر
print("x Address is : %p\n", x~ptr); // x Address is : 0x55cdf422e950
print("Pointer Address is : %p\n", p); // Pointer Address is : 0x55cdf422e950
// وللحصول على المحتوى الذي يشير إليه المتغير ptr نستخدم المؤثر ~cnt
print("Pointer Content is : %d\n", p~cnt); // Pointer Content is: 5
// وهذا مانسميه بالتتبع (dereferencing)، أي تحديد قيمة المتغير المستخدم مع المؤشر.
// ولتغيير قيمة المتغير عن طريق إستخدام المؤشر، فيمكننا القيام بذلك كالتالي:
p~cnt = 9; // هنا قمنا بتغيير قيمة المتغير من خلال المؤشر
print("x Value is : %d\n", x); // x Value is : 9

- لابد من تحديد نوع البيانات الخاص بالمتغير الآخر الذي سوف يشير إليه وذلك حتى يتمكن مترجم اللغة من تحديد حجم المتغير الآخر.

- يمكن تطبيق عمليات الجمع والطرح على المؤشرات، وفي هذه الحالة فإن مقدار الزيادة لقيمة المؤشر ستكون من مضاعفات حجم الصنف الذي يشير إليه المؤشر. على سبيل المثال، إذا أضفت 1 لمؤشر على عدد صحيح فإن قيم المؤشر ستزداد بمقدار حجم العدد الصحيح، أي بمقدار 4 بايتات في حالة العدد الصحيح بصيغة 32 بت. (سترى إحدى فوائد هذه العمليات في درس الدوال عندما نتحدث عن الدوال المرنة).

السندات


السندات يمكن اعتبارها طريقة أبسط للتعامل مع مفهوم المؤشرات، حيث لا تتطلب منك سوى تعريف المتغير على أنه سند ومن ثم التعامل معه بنفس طريقة التعامل مع متغيرات اعتيادية، اي الوصول إلى المحتوى دون الحاجة لاستخدام المؤثر `~محتوى`. ويمكن تعريف السند بالشكل التالي:

عرف اسم_السند: سند[صـنف_البيانات]
def ref_name: ref[Data_type]

حيث أن:
اسم_السند (ref_name): هو اسم المتغير.
صـنف_البيانات (Data_type): نوع البيانات الخاص بالمتغير الآخر الذي سوف يكون سنداً له.

مثال:

اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
عرف س: صحيح = 9؛
عرف سس: سند[صحيح]؛ // تعريف السند
اطبع("قيمة س هي: %dج"، س)؛
اطبع("عنوان س هو: %p\ج"، س~مؤشر)؛
سس~مؤشر = س~مؤشر؛ // الآن أصبح المتغير سس عبارة عن سند لـ س
س = 51؛
اطبع("قيمة سس: \dج"، سس)؛ // طباعة قيمة السند
اطبع("عنوان سس: %p\ج"، سس~مؤشر)؛ // طباعة عنوانه
/*
قيمة س هي: 9
عنوان س هو: 0x563b63224bf0
قيمة سس: 51
عنوان سس: 0x563b63224bf0
*/
import "Srl/Console.alusus";
use Srl.Console;
def x: int = 9;
def re: ref[int]; // تعريف سند
print("value of x: %d\n", x);
print("x Address is : %p\n", x~ptr);
re~ptr = x~ptr; // x عبارة عن سند ل re الآن أصبح المتغير
x = 51;
print("value of re: %d\n", re); // طباعة قيمة السند
print("re Address is : %p\n", re~ptr); // طباعة عنوانه
/*
value of x: 9
x Address is : 0x563b63224bf0
value of re: 51
re Address is : 0x563b63224bf0
*/

لاحظ أن أي تعديل يطال المتغير أو السند سينعكس على الآخر بعد أصبح لديهم نفس العنوان الذاكري.

- يمكنك أيضاً استخدام المؤثر `~عطل_التتبع` (no_deref~) كطريقة ثانية لتغيير عنوان السند، أي في المثال السابق بدلاً من كتابة:

سس~مؤشر = س~مؤشر؛
re~ptr = x~ptr;

يمكننا كتابة:

سس~عطل_التتبع = س؛
re~no_deref = x;

أي أن المؤثر يخبر المترجم أنك تنوي تغيير قيمة السند وليس قيمة الكائن الذي يؤشر إليه السند. هذا المؤثر مفيد في بعض الحالات مثل بناء القوالب التي نريدها أن تعمل كما هو متوقع في حالة السندات (مثلا الصنف Array يحتاج هذا المؤثر لتمكين المصفوفة من تعيين قيم السندات بدل قيم الكائنات).
ميزة هذا المؤثر أنه لا يعطيك خطأ لو استخدمته على متغير ليس سنداً، وهذا ما يجعله مفيداً داخل القوالب (فيعمل القالب كما هو متوقع في حال كان الصنف المحدد سنداً أو لا).

- يمكنك أيضاً تعريف سند لسند، مثلاً في المثال السابق سنضيف سند آخر على السند سس بالشكل التالي:

اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
عرف س: صحيح = 9؛
عرف سس: سند[صحيح]؛
عرف سسس: سند[سند[صحيح]]؛ // تعريف سند على سند من الصنف الصحيح
سس~مؤشر = س~مؤشر؛ // الآ، أصبح سس سندًا لـ س
سسس~مؤشر~مؤشر = سس~مؤشر~مؤشر؛ // الآن السند سسس يشير إلى السند سس
// الآن لو قمنا بطباعة قيمة المتغيرات، ستلاحظ أن جميعها أصبح يملك نفس القيمة
اطبع("%d\ج"، س) // 9
اطبع("%d\ج"، سس) // 9
اطبع("%d\ج"، سسس) // 9
// الآن لو قمنا بإسناد قيمة للسند الجديد، سنلاحظ أن جميعها أصبح يملك نفس القيمة
سسس = 100؛
اطبع("%d\ج"، س) // 100
اطبع("%d\ج"، سس) // 100
اطبع("%d\ج"، سسس) // 100
import "Srl/Console.alusus";
use Srl.Console;
def x: int = 9;
def re: ref[int];
def rere: ref[ref[int]]; // تعريف سند على سند من الصنف الصحيح
re~ptr = x~ptr; // x عبارة عن سند ل re الآن أصبح المتغير
rere~ptr~ptr = re~ptr~ptr; // re يشير إلى السند rere الآن بنفس الأسلوب سنجعل السند
// الآن لو قمنا بطباعة قيمة المتغيرات، ستلاحظ أن جميعها أصبح يملك نفس القيمة
print("%d\n",x) // 9
print("%d\n",re) // 9
print("%d\n",rere) // 9
// الآن لو قمنا بإسناد قيمة للسند الجديد، سنلاحظ أن جميعها أصبح يملك نفس القيمة
rere=100
print("%d\n",x) // 100
print("%d\n",re) // 100
print("%d\n",rere) // 100

نلاحظ من المثال أعلاه أن:

  1. العمليات التي نطبقها على السند دائماً تطبق على المحتوى بغض النظر عن عمق السند (يمكن ملاحظة ذلك عندما أسندنا القيمة 100 ل `سسس` (rere)).
  2. في حالة سند لسند (المتغير سسس أو rere) فإن استخدام `~مؤشر~ (~ptr) في هذه الحالة يعطيك مؤشر الكائن وليس مؤشر السند، بينما ~ptr~ptr يعطيك مؤشرا على السند الداخلي، وهكذا.

- بالإمكان أيضا استخدام المؤثر `~مؤشر` (~ptr) لجعل السند يشير إلى حجز ديناميكي للذاكرة (سنرى ذلك في نهاية الدرس).

- استخدام السندات يكون مفيداً جداً في بعض الحالات كما سنرى لاحقاً في درس الدوال و الأصناف. على سبيل المثال بالنسبة للدوال (سنتحدث عن الدوال في الدروس القادمة) عندما تريد تعريف دالة تقوم ببعض العمليات الإجرائية على بعض البيانات وتريد أن يتم تطبيق هذه التغييرات على البيانات الأصلية (أي ليس على نسخة من البيانات). لاحظ المثال التالي:

اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
عرف س: صحيح = 3؛
// الآن سنقوم بتعريف دالة تقوم بالزيادة بمقدار واحد.
دالة أضف_واحد (م: سند[صحيح]) {
  م += 1؛
}
أضف_واحد(س)؛ // الآن قمنا باستدعاء الدالة لكي تزيد لنا قيمة المتغير س بمقدار 1
اطبع("%d"، س)؛ // 4
import "Srl/Console.alusus"
use Srl.Console;
def x: int = 3;
// الآن سنقوم بتعريف دالة تقوم بالزيادة بمقدار واحد.
function inc (a: ref[int]) {
  a += 1;
}
inc(x) // بمقدار 1 x الآن قمنا باستدعاء الدالة لكي  تزيد لنا قيمة المتغير
print("%d", x); // 4

وهنا يجب أن ننوه لملاحظتين:

  1. عندما يكون السند معطى لدالة (كما في المثال أعلاه) فكل ما تحتاج لفعله هو تمرير متغير من صنف محتوى السند إلى الدالة.
  2. لو لم نقم باستخدام السند كمعطى لدالة في المثال السابق، فالتغيرات التي سيتم تطبيقها على المتغير ستكون محصورة ضمن الدالة (سيتم التعديل على نسخة من المُعطى وليس على النسخة الأصلية). لاحظ في المثال التالي كيف أن التغيرات تبقى محصورة ضمن مجال الدالة وبالتالي لم تتأثر قيمة س بالتعديلات:
اشمل "مـتم/طـرفية"؛
استخدم مـتم.طـرفية؛
عرف س: صحيح = 3؛
دالة أضف_واحد (م: صحيح) {
  م += 1؛
  اطبع("%d\ج"، م)؛ // 4
}
أضف_واحد(س)؛
اطبع("%d"، س)؛ // 3
import "Srl/Console.alusus"
use Srl.Console;
def x: int = 3;
function inc (a: int) {
  a += 1;
  print("%d\n", a); // 4
}
inc(x);
print("%d", x); // 3

متى نستخدم المؤشر ومتى نستخدم السند؟

ننصح باستخدم السند ما لم تملك سبباً يجبرك على استخدام المؤشر. إن لم تعرف أنك تحتاج لاستخدام المؤشر فهذا قد يكون دليلاً على أنك لا تحتاج المؤشر ويمكنك الاكتفاء بالسند.
لمزيد من التفاصيل عن السندات والمؤشرات يمكنك الانتقال إلى مرجع الاستخدام من هنا

السندات الذكية


بالنسبة لأولئك الذين يريدون أو يتوقعون أنهم بحاجة لاستخدام الكثير من المؤشرات في مشاريعهم الأفضل الاعتماد على السندات الذكية بدل السندات البسيطة، لأن هذا يمكن أن يخفف العديد من المشكلات المتعلقة بالمؤشرات والسندات الأولية، مثل نسيان حذف الكائن وتسريب الذاكرة.
- يمكنك الاكتفاء بالسندات البسيطة فقط في حالة تمرير المعطيات للدالات عندما نريد للدالة تغيير قيمة المعطى.
- السندات الذكية بشكل عام تتميز بقدرتها على إدارة أمور الذاكرة بنفسها كما سنرى، وتهدف إلى تقليل الأخطاء الناتجة عن إساءة استخدام المؤشرات أو السندات العادية، و الحفاظ على الكفاءة وتوفير استهلاك الذاكرة.
- يمكنك تخيل السند الذكي على أنه صنف يحوي بداخله سنداً عادياً، ومتغيرات أخرى ربما، كما يحوي بداخله العديد من الدالات التي تدير التعامل مع السند الداخلي.
- السندات الذكية تتبع سياسة فيما يتعلق بوقت إتلاف الكائن. لا يزال يتعين عليك إنشاء الكائن، لكن لم يعد عليك القلق بشأن حذفه وتحرير ذاكرته. وهناك عدة أنواع للسندات الذكية سنتعرف عليها في هذا الدرس.
- قبل التعامل مع أي نوع من أنواع السندات الذكية لابد من تضمين الوحدة سندات (refs) الموجودة ضمن مكتبة التنفيذ المعيارية مـتم (Srl)، وذلك لاستيراد جميع أصناف السندات الذكية:

اشمل "مـتم/سندات"؛
import "Srl/refs.alusus";

سـندنا (SrdRef)

هذا الصنف يتولى تلقائياً تحرير الكائن عندما تنتهي الحاجة له، كما ويحتفظ هذا السند بعداد لعدد السندات المشتركة التي تشير لنفس الكائن. كلما يُتلف أحد السندات يُنقص العداد وعندما يصل العداد لصفر يُتلف الكائن وتحرر ذاكرته تلقائياً.
باستخدام سـندنا، يمكن أن يشير أكثر من سند واحد إلى نفس الكائن وسيحتفظ بعداد مرجعي لإحصاء عددهم. وبالتالي إذا كان هناك كائن تتم الإشارة إليه من قبل سندين فإن العداد ستكون قيمته 2. أي يمكن القول أن الصنف `سـندنا` يُعرّف سنداً مُشتركًا قادراً على مشاركة ملكيّة كائن ما مع سندات مشتركة أخرى. ويُنفَّذ سلوك المشاركة عبر تقنية تُعرف بعدِّ المراجع (reference counting). ويُدمَّر هذا الكائن تلقائياً عندما يصل هذا العدد إلى الصفر.

- لتعريف سند مشترك يشير إلى كائن من صنف محدد نتبع الصيغة التالية (تعريف سند يشير إلى صنف بيانات ما):

  عرف اسم_السند: مـتم.سـندنا[صـنف_البيانات]؛
def ref_name: Srl.SrdRef[Data_type];

حيث أن:
اسم_السند: اسم السند المشترك.
صـنف_البيانات: اسم صنف البيانات المُراد الإشارة إليها من خلال السند.

- لحجز ذاكرة للكائن الذي سيشير السند له وتهيئته أيضاً (أي لإنشائه) نستخدم الدالة `أنشئ` (construct):

اسم_السند.أنشئ()؛
ref_name.construct();

- لتحرير السند المشترك نستخدم الدالة `حرر` (release):

اسم_السند.حرر()؛
ref_name.release();

وهنا يتم تحرير السند المشترك لكن لايتم تدمير الكائن إلا إذا كان هذا السند هو آخر سند يشير للكائن.

- للوصول للكائن الذي يشير له السند نستخدم الواصفة `كائن` (obj):

اسم_السند.كائن
ref_name.obj

- لمعرفة عدد السندات المشتركة في ملكية الكائن، نستخدم `refCounter.count`.

ref_name.refCounter.count

كما يمكنك الاطلاع على باقي الدوال المُعرّفة ضمن هذا الصنف من هنا.

مثال:
سنقوم الآن بتعريف سند مشترك يشير إلى كائن من الصنف الصحيح int وسنقوم بإنشاء الكائن من خلال دالة `أنشئ` (construct) كما يلي:

عرف س: سـندنا[صحيح]؛
س.أنشئ()؛
def x: SrdRef[int];
x.construct();

- الآن لإسناد قيمة للكائن الذي يشير له السند المشترك x نستخدم الواصفة `كائن` (obj) بالشكل التالي:

س.كائن = 9؛
x.obj = 9;

- لإنشاء عدّة سندات ذكية تشترك في نفس الكائن، نحتاج إلى إنشاء سند مشترك آخر ثم نساويه بالسند الأول المشترك:

  عرف ص: سـندنا[صحيح] = س؛ // الآن كلا السندين يشيران لنفس الكائن وعددهم 2
def y: SrdRef[MyType] = x; // الآن كلا السندين يشيران لنفس الكائن وعددهم 2

- الآن لطباعة قيمة الكائن الذي تتم الإشارة إليه من قبل السندين س (x) و ص (y):

اطبع(س.كائن)؛ // 9
اطبع(ص.كائن)؛ // 9
print(x.obj); // 9
print(y.obj); // 9

- لمعرفة عدد السندات المشتركة:

print(x.refCounter.count); // 2

- لتحرير السند المشترك الأول نكتب:

س.حرر()؛
x.release();

بالتالي تؤدي إلى تحرير السند المشترك الأول وإنقاص العداد بمقدار واحد، أي أن قيمته 1، وهذا يعني أنه مازال هناك سند آخر يؤشر على الكائن وبالتالي الكائن الذي تتم الإشارة إليه لايتم تحريره.
- الآن إذا قمنا بتحرير السند المشترك الثاني:

ص.حرر()؛
y.release();

سيؤدي ذلك إلى تحرير السند المشترك ص (y) وإنقاص العداد بمقدار واحد، أي أن قيمته أصبحت 0، وبالتالي لاداعي لوجود الكائن لإنه لايوجد أي مستخدم له وبالتالي يتم إتلافه وتحرير ذاكرته.

ملاحظة: في العادة لن تحتاج لاستدعاء دالة `حرر` يدويًا فهي تُستدعى تلقائيًا عند إتلاف السند المشترك (مثلا عند خروج التنفيذ من المجال الذي فيه عرف السند المشترك) أو عند إسناد كائن جديد للسند المشترك.

الكود كاملاً:

اشمل "مـتم/طـرفية"؛
اشمل "مـتم/سندات"
استخدم مـتم؛
عرف س: سـندنا[صحيح]؛
س.أنشئ()؛
س.كائن = 9؛
عرف ص: سـندنا[صحيح] = س؛
طـرفية.اطبع(س.كائن)؛ // 9
طـرفية.اطبع(ص.كائن)؛ // 9
طـرفية.اطبع(س.عداد_السندات.count)؛ // 2
س.حرر()؛
ص.حرر()؛
import "Srl/Console.alusus";
import "Srl/refs.alusus";
use Srl;
def x: SrdRef[int];
x.construct();
x.obj = 9;
def y: SrdRef[int] = x;
Console.print(x.obj); // 9
Console.print(y.obj); // 9
Console.print(x.refCounter.count); // 2
x.release();
y.release();

ملاحظة: لاحقاً عند التعامل مع الأصناف و عند التعامل مع عناصر الكائن فلن تحتاج لكتابة `كائن` (obj). مثلاً للولوج إلى العنصر `س` (x) من الكائن فيمكنك كتابة `سندي.س` (myRef.x) ولست مضطراً لكتابة `سندي.كائن.س` (myRef.obj.x).

لاحظ المثال التالي:

اشمل "مـتم/طـرفية"؛
اشمل "مـتم.سندات"؛
استخدم مـتم؛
صنف صـنفي {
  عرف القيمة: صحيح؛
}
عرف ا: سـندنا[صـنفي]؛ // تعريف سند مشترك لهذا الصنف
ا.أنشئ()؛
ا.القيمة = 200؛ // إسناد قيمة للمتغير الموجود ضمن هذا الكائن
طـرفية.اطبع("%d"، ا.القيمة)؛ // 200
ا.حرر()؛
import "Srl/Console.alusus";
import "Srl/refs.alusus";
use Srl;
class A {
  def val: int;
}
def a: SrdRef[A]; // تعريف سند مشترك لهذا الصنف
a.construct();
a.val = 200; // إسناد قيمة للمتغير الموجود ضمن هذا الكائن
Console.print("%d", a.val); // 200
a.release();


سـندهم (WkRef)

نوع آخر من السندات الذكية. يتم إنشاء `سـندهم` (WkRef) كنسخة عن `سـندنا` (SrdRef)، حيث يوفر الوصول إلى كائن مملوك من قبل واحد أو أكثر من نسخ SrdRef ولكن لا يشارك بعدِّ المراجع (reference counting). كما أن وجود أو إتلاف WkRef ليس له أي تأثير على SrdRef أو نسخه الأخرى. و في حالة تم تحرير جميع نسخ SrdRef فإن الكائن يُتلف حتى لو كان هناك WkRef. يفيد هذا النوع من السندات في تجنب حلقات السندات المغلقة أو ما يسمى ب التبعية الدورية (cyclic dependency) التي تؤدي إلى تسرب في الذاكرة.
على سبيل المثال لنفكر في سيناريو لدينا فيه صنفان ا و ب، وكلاهما له مؤشر للصنف الآخر. لذلك، دائماً ما يكون الأمر كما لو أن ا يشير إلى ب ويشير ب إلى ا. ومن ثم، فإن عداد السندات لن يصل أبداً إلى الصفر ولن يتم حذفهما أبداً. هذا هو سبب استخدامنا لـ `سـندهم` فهي ليست مرجعية محسوبة، أي أن الملكية غير مشتركة، و لكن يمكنها الوصول إلى الكائن. لذلك، يمكن منع حالات التبعية الدورية باستخدام `سـندهم` لكسرها في نقطة معينة. مثلاً في المثال الذي ذكرناه يمكن استخدام `سـندنا` في ا يشير إلى ب واستخدام `سـندهم` في ب يشير إلى ا، وعندها ما أن تُزال السندات الخارجية لـ ا سيُحرر وسيُحرر معه ب. للمزيد من التفاصيل عن هذا الصنف من هنا


UnqRef

هو صنف يُدير دورة حياة الكائنات المخزّنة ديناميكياً، وعلى خلاف SrdRef، فإنّ كل كائن يكون مملوكاً لسند حصري واحد في أيّ لحظة. كما أنه لا يستخدم عداداً للسندات. عند إتلاف هذا السند فإنه سيحرر الكائن الذي يملكه مباشرة.

مثال:

اشمل "مـتم/طـرفية"؛
اشمل "مـتم/سندات"؛
اشمل "مـتم/نـص"؛
استخدم مـتم؛
عرف نص: سـندي[نـص]؛
نص.أنشئ()؛
نص.كائن = "أهلا..."؛
طـرفية.اطبع(نص.كائن)؛ // أهلا...
نص.حرر()؛ // يتم تحرير الكائن
import "Srl/Console.alusus";
import "Srl/refs.alusus";
import "Srl/String.alusus";
use Srl;
def str: UnqRef[String];
str.construct();
str.obj = "Hi..";
Console.print(str.obj); // Hi..
str.release(); // يتم تحرير الكائن

حجز الذاكرة الديناميكي


يمكن تعريف تخصيص الذاكرة الديناميكي في الأسس كإجراء يتم فيه تغيير حجم بنية البيانات أثناء وقت التشغيل، ففي بعض الحالات قد يضطر المبرمج إلى تخصيص ذاكرة لمتغير أو كائن ما بحجم محدد.
في الأسس قمنا بتعريف الوحدة `ذاكـرة` (Memory) ضمن الوحدة `مـتم` (Srl) لتسهيل كل هذه العمليات، حيث تحتوي العديد من الدوال التي تكفي معظم احتياجاتك. يمكنك الاطلاع على تفاصيلها هنا
تحتوي الوحدة Memory على الدالة `احجز` (alloc) التي لها الشكل التالي:

@تصدير[malloc] دالة احجز (حجم: صـحيح_متكيف): مؤشر;
@expname[malloc] func alloc (size: ArchInt): ptr;

هذه الدالة وكما يشير اسمها يمكنك استخدامها لحجز كتلة واحدة كبيرة من الذاكرة بشكل ديناميكي بالحجم الذي تريده، و كما هو موضح في تعريفها فإنها تُرجع مؤشراً ptr لكن لم يحدد على أي صنف، لذا يجب عليك تحويله (عملية التمثيل أو casting) إلى مؤشر من صنف البيانات الذي تريده.
على سبيل المثال المتغير من الصنف صحيح له حجم 4 بايت ونريد حجز ذاكرة له بحجم 8 بايت، نقوم باستخدام الدالة `احجز` (alloc) ثم نقوم بعملية التمثيل إلى الصنف المطلوب وفي مثالنا هو مؤشر[صحيح]، كما في المثال التالي:

  اشمل "مـتم/ذاكـرة"؛
  استخدم مـتم؛
  عرف س: سند[صحيح]؛
// حجز ذاكرة بالحجم المطلوب ثم تطبيق عملية التمثيل
  س~مؤشر = ذاكـرة.احجز(صحيح~حجم * 2)~مثل[مؤشر[صحيح]]؛
import "Srl/Memory.alusus";
use Srl;
def p: ref[int];
// حجز ذاكرة بالحجم المطلوب ثم تطبيق عملية التمثيل
p~ptr = Memory.alloc(int~size * 2)~cast[ptr[int]];