قد تكون عملية ملء الذاكرة دون تحريرها (العملية المعروفة بتسريب الذاكرة memory leak) صعبة الحدوث بمستوى أمان الذاكرة الذي تقدمه لغة رست Rust، إلا أن حدوث هذا الأمر غير مستحيل، إذ لا تضمن رست منع تسريب الذاكرة بصورةٍ كاملة، والمقصود هنا أن الذاكرة المُسرّبة آمنة في رست. نلاحظ أن رست تسمح بتسريب الذاكرة باستخدام Rc<T>
و RefCell<T>
، إذ أنه من الممكن إنشاء مراجع تشير إلى بعضها ضمن حلقة cycle، وسيسبب هذا تسريب ذاكرة لأن عدد المراجع لكل عنصر لن يصل إلى 0 أبدًا، وبهذا لن تُحرَّر أي قيمة.
إنشاء حلقة مرجع
لنلاحظ كيف من الممكن أن نشكّل حلقة مرجع لتفادي هذا الأمر، بدءًا بتعريف معدّد List
وتابع tail
في الشيفرة 25:
اسم الملف: src/main.rs
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() {}
الشيفرة 25: تعريف قائمة بنية تخزّن RefCell<T>
داخلها، بحيث نستطيع تعديل القيمة التي يشير إليها متغاير Cons
نستخدم هنا نوعًا مختلفًا من تعريف List
من الشيفرة 5، إذ يصبح العنصر الثاني من المتغاير Cons
أي variant مساويًا للنوع RefCell<Rc<List>>
، مما يعني أننا نحتاج تعديل قيمة List
المشار إليها في Cons
عوضًا عن حاجتنا لقابلية التعديل على قيمة النوع i32
كما فعلنا في الشيفرة 24 في المقال السابق، نضيف أيضًا التابع tail
لتسهيل الوصول إلى العنصر الثاني إذا وُجد متغاير Cons
.
أضفنا في الشيفرة 26 الدالة main
التي تستخدم التعريف الموجود في الشيفرة 25، إذ تُنشئ الشيفرة قائمةً في a
وقائمة في b
تشير إلى القائمة a
، وثم تعدِّل القائمة في a
لتشير إلى b
لتنشئ بذلك حلقة مرجع. كتبنا أيضًا تعليمات println!
لتظهر قيمة عدد المراجع في نقاط مختلفة ضمن هذه العملية.
اسم الملف: src/main.rs
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); println!("a initial rc count = {}", Rc::strong_count(&a)); println!("a next item = {:?}", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); println!("a rc count after b creation = {}", Rc::strong_count(&a)); println!("b initial rc count = {}", Rc::strong_count(&b)); println!("b next item = {:?}", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a)); // ألغِ تعليق السطر التالي لتلاحظ وجود حلقة المرجع، إذ ستتسبب الشيفرة البرمجية بطفحان المكدس // println!("a next item = {:?}", a.tail()); }
الشيفرة 26: إنشاء حلقة مرجع بحيث تشير قيمتي List
إلى بعضهما بعضًا
أنشأنا نسخةً من Rc<List>
تحتوي على القيمة List
في المتغير a
مع قائمة مبدئية تحتوي على القيم 5,Nil
، ثم أنشأنا نسخة Rc<List>
أخرى تحتوي قيمة List
أخرى في المتغير b
تحوي القيمة 10 وتشير إلى القائمة في a
.
عدّلنا a
لتشير إلى b
بدلًا من Nil
لنحصل بذلك على حلقة، وحقّقنا ذلك باستخدام التابع tail
للحصول على مرجع إلى RefCell<Rc<List>>
في a
، والذي وضعناه بعد ذلك في المتغير link
، ثم استخدمنا التابع borrow_mut
على القيمة RefCell<Rc<List>>
لتغيير القيمة داخل Rc<List>
التي تخزّن القيمة Nil
إلى القيمة Rc<List>
الموجودة في b
.
نحصل على الخرج التالي عندما ننفذ هذه الشيفرة البرمجية مع الإبقاء على تعليمة println!
الأخيرة معلّقة:
$ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.53s Running `target/debug/cons-list` a initial rc count = 1 a next item = Some(RefCell { value: Nil }) a rc count after b creation = 2 b initial rc count = 1 b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) }) b rc count after changing a = 2 a rc count after changing a = 2
يبلغ عدد مراجع نُسخ Rc<List>
في كلٍّ من a
و b
القيمة 2 وذلك بعد تغيير القائمة في a
لتشير إلى b
. تُسقِط أو تحذف لغة رست في نهاية الدالة main
المتغير b
مما يغيّر من عدد مراجع نسخة b
من Rc<List>
من 2 إلى 1، ولن تُحرّر الذاكرة التي تشغلها Rc<List>
على الكومة heap في هذه اللحظة لأن عدد المراجع هو 1 وليس 0، ومن ثم تُحرّر رست a
، مما يُنقص عداد المرجع لنسخة a
من Rc<List>
من 2 إلى 1 أيضًا. لا يُمكن تحرير ذاكرة النسخة هذه لأن نسخة Rc<List>
الأخرى لا تزال تشير إليها، وستبقى الذاكرة المحجوزة للقائمة شاغرة للأبد، ولتخيّل حلقة المرجع هذه بصريًا أنشأنا المخطط التالي في الشكل 4.
الشكل 4: حلقة مرجع خاصة بقائمتين a
و b
، تشيران إلى بعضهما بعضًا
إذا أزلت التعليق عن آخر تعليمة println!
ونفّذت البرنامج، فستحاول رست طباعة الحلقة مع إشارة القائمة a
إلى القائمة b
وإشارتها بدورها إلى القائمة a
وهكذا دواليك حتى يطفح المكدّس.
ليست عواقب إنشاء حلقة مرجعية هنا خطيرة مقارنةً بالبرامج الواقعية، إذ أن برنامجنا ينتهي بعد إنشائنا حلقة مرجع reference cycle، إلا أن البرنامج سيستخدم ذاكرة أكثر مما يحتاج إذا كان البرنامج أكثر تعقيدًا وشَغَل ذاكرة أكثر في الحلقة واحتفظ بها لفترة أطول، وربما سيتسبّب ذلك بتحميل النظام عبئًا كبيرًا مسببًا نفاد الذاكرة المتوفرة.
إنشاء حلقات مرجع ليس بالأمر السهل ولكنه ليس مستحيلًا، فإذا كانت لديك قيم RefCell<T>
تحتوي على قيم Rc<T>
أو تشكيلات متداخلة مشابهة لأنواع مع قابلية التغيير الداخلي وعدّ المراجع، فيجب عليك التأكد أنك لم تنشئ حلقات، إذ لا يجب عليك الاعتماد على رست لإيجادها. إنشاء حلقة مرجع يحصل نتيجة خطأ منطقي في برنامجك ولذلك يجب عليك استخدام الاختبارات التلقائية ومراجعة الشيفرة البرمجية ووسائل تطوير البرامج الأخرى لتقليل احتمالية حدوثها.
يمكن إعادة تنظيم هيكلية البيانات كحلٍ آخر لتفادي حلقات المرجع، بحيث تعبّر بعض المراجع عن الملكية بينما لا يعبّر بعضها الآخر، ونتيجةً لذلك سيكون لديك حلقات مكونة من بعض علاقات الملكية ownership وبعضها من علاقات لا ملكية non-ownership، بحيث تؤثر علاقات الملكية فقط إذا كانت القيمة ستُحرَّر.
نريد من المتغاير Cons
في الشيفرة 25 أن يملك قائمةً خاصةً به دائمًا، لذا فإن عملية إعادة تنظيم هيكلية البيانات ليست ممكنة. لنتابع مثالًا آخرًا باستخدام البيانات التخطيطية graphs بحيث تتكون من عُقد أب وعُقد أبناء لنرى كيف أن العلاقات اللا ملكية هي طريقة مناسبة لمنع حصول حلقات المرجع.
منع حلقات المرجع: بتحويل Rc إلى Weak
وضّحنا بحلول هذه النقطة أن استدعاء Rc::clone
يزيد من قيمة strong_count
الخاصة بالنسخة Rc<T>
وأن نسخة Rc<T>
تُحرَّر فقط عندما تكون قيمة strong_count
هي 0. بإمكاننا أيضًا إنشاء مرجع ضعيف weak reference للقيمة داخل نسخة Rc<T>
وذلك باستدعاء Rc::downgrade
وتمرير مرجع إلى Rc<T>
. المراجع القوية هي الطريقة التي يمكنك بها مشاركة الملكية لنسخة Rc<T>
، بينما لا تعبّر المراجع الضعيفة عن علاقة ملكية، ولا يتأثر عددها عندما تُحرَّر نسخة من Rc<T>
، ولا يسبب حلقة مرجعية، إذ ستُكسر الحلقات المتضمنة لمراجع ضعيفة عندما تصبح قيمة عدد المراجع القوية مساويةً إلى 0.
نحصل على مؤشر ذكي من النوع Weak<T>
عندما نستدعي Rc::downgrade
، وبدلًا من زيادة strong_count
في نسخة Rc<T>
بقيمة 1، سيزيد استدعاء Rc::downgrade
من قيمة weak_count
بمقدار 1. يستخدم النوع Rc<T>
القيمة weak_count
لمتابعة عدد مراجع Weak<T>
الموجودة بصورةٍ مشابهة للقيمة strong_count
، إلا أن الفرق هنا هو أن weak_count
لا يحتاج أن يكون 0 لكي تُنظف نسخة <Rc<T
.
يجب علينا التأكد أن القيمة موجودة فعلًا إذا أردت إجراء أي عمليات على القيمة التي يشير إليها Weak<T>
، وذلك لأن القيمة قد تُحرَّر، ويمكننا التحقق من ذلك باستدعاء تابع upgrade
على نسخة Weak<T>
التي تعيد قيمةً من النوع Option<Rc<T>>
، وسنحصل على نتيجة Some
إذا لم تُحرَّر القيمة Rc<T>
ونتيجة None
إذا حُرّرت القيمة، تضمن رست أن حالتي Some
و None
ستُعامل على النحو الصحيح ولن تصبح مؤشرًا غير صالح، وذلك لأن upgrade
تعيد <Option<Rc<T>
.
لنأخذ مثالًا، فبدلًا من استخدام قائمة تعرف عناصرها العنصر الذي يليها فقط، سننشئ شجرةً تعرف عناصرها كلٍ من أبنائها وآبائها.
إنشاء هيكل بيانات الشجرة يحتوي على عقدة مع عقد أبناء
أولًا، ننشئ شجرة مع عقد nodes تعرف عقد أبنائها، ولتحقيق ذلك ننشئ بنيةً ندعوها Node
تحتوي على قيم من النوع i32
إضافةً إلى مراجع تشير لقيم أبنائها Node
:
اسم الملف: src/main.rs
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); }
نريد من البنية Node
أن تمتلك أبنائها children
، كما نريد كذلك مشاركة الملكية مع المتغيرات لكي نتمكن من الوصول إلى كل Node
في الشجرة مباشرةً، ومن أجل تحقيق ذلك سوف نعرّف عناصر Vec<T>
بحيث تكون قيمًا من النوع Rc<Node>
؛ كما نريد أيضًا تعديل العقد الأبناء لعقدة أخرى، لذلك سيكون لدينا RefCell<T>
في الأبناء children
وحول Vec<Rc<Node>>
.
سنستخدم تعريف الهيكلية لإنشاء نسخة Node
واحدة بالاسم leaf
وقيمتها 3 دون أن تحتوي على أبناء، ونسخةً أخرى اسمها branch
قيمتها 5 تحتوي على leaf
بمثابة واحد من أبنائها كما هو موضح في الشيفرة 27:
اسم الملف: src/main.rs
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); }
الشيفرة 27: إنشاء عقدة leaf
دون أبناء وعقدة branch
مع leaf
بمثابة واحد من أبنائها
ننسخ القيمة Rc<Node>
الموجودة في leaf
ونخزّنها في branch
وبذلك تصبح Node
في leaf
مُمتلكةً من قبل مالكين، هما leaf
و branch
. يمكننا الانتقال من branch
إلى leaf
عبر branch.children
ولكن لا يوجد طريقة للوصول الى leaf
من branch
، وسبب ذلك هو أن leaf
لا تمتلك مرجع إلى branch
، وبالتالي لا تعرف بأنها مرتبطة مع branch
، إذًا نحن بحاجة لأن تعرف أن branch
هي العقدة الأب وهذا ما سنفعله.
إضافة مرجع يشير إلى عقدة ابن داخل عقدة أب
نحتاج لإضافة حقل parent
لجعل عقدة ابن تعرف بوجود آبائها وذلك ضمن تعريف الهيكل Node
. تكمن صعوبة الأمر في اختيار النوع الذي يجب استخدامه لتخزين القيمة parent
، إلا أننا نعلم أنه لا يمكن للقيمة أن تحتوي النوع Rc<T>
لأننا نحصل بذلك على حلقة مرجع تحتوي على القيمة leaf.parent
مشيرةً إلى branch
و branch.children
مشيرةً إلى leaf
مما يجعل قيم strong_count
غير مساوية إلى القيمة 0 في أي من الحالات.
لنفكّر بالعلاقات بطريقة أخرى؛ إذ يجب على العقدة الأب أن تمتلك أبنائها، إذا حُرِّرَت العقدة الأب فيجب على العُقد التابعة لها (الأبناء) أن تُسقط أيضًا، إلا أنه ليس من المفترض أن تمتلك عقدة ابن العقدة الأب، فإذا حرّرنا العقدة الابن يجب أن تبقى العقدة الأب موجودة، وهذه هي الحالة التي صُمّمت من أجلها المراجع الضعيفة.
لذا نجعل النوع parent
يستخدم Weak<T>
بدلًا من Rc<T>
وتحديدًا النوع RefCell<Weak<Node>>
، وسيصبح تعريف الهيكل Node
على النحو التالي:
اسم الملف: src/main.rs
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }
يمكن للعقدة أن تُشير إلى العقدة الأب ولكن لا يمكن أن تمتلكها. عدّلنا من الدالة main
في الشيفرة 28 بحيث تستخدم التعريف الجديد لكي تكون للعقدة leaf
طريقة للإشارة إلى العقدة الأب branch
:
اسم الملف: src/main.rs
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }
الشيفرة 28: عقدة leaf
مع مرجع ضعيف للعقدة الأب branch
يبدو إنشاء العقدة leaf
شبيهًا للشيفرة 27، باختلاف الحقل parent
، إذ تبدأ العقدة leaf
بدون أب وهذا يمكّننا من إنشاء نسخة لمرجع Weak<Node>
فارغ.
عندما نحاول الحصول على مرجع للعقدة الأب الخاصة بالعقدة leaf
وذلك باستخدام التابع upgrade
، نحصل على قيمة None
، ويمكن رؤية الخرج الناتج من أول تعليمة println!
:
leaf parent = None
نحصل على مرجع Weak<Node>
جديد عندما نُنشئ العقدة branch
وذلك في الحقل parent
لأن branch
لا تحتوي على عقدة أب. لا يزال لدينا leaf
وهي عقدة ابن للعقدة branch
، وحالما يوجد لدينا نسخة من Node
في branch
سيكون بإمكاننا تعديل leaf
بمنحها مرجعًا إلى العقدة الأب الخاصة بها من النوع Weak<Node>
. نستخدم التابع borrow_mut
على النوع RefCell<Weak<Node>>
في حقل parent
الخاص بالعقدة leaf
، ومن ثم نستخدم الدالة Rc::downgrade
لإنشاء مرجع من النوع Weak<Node>
يشير إلى العقدة branch
من النوع Rc<Node>
في branch
.
نحصل على متغاير Some
يحتوي على branch
عندما نطبع أب العقدة leaf
مجددًا، إذ يمكن للعقدة leaf
الآن الوصول إلى العقدة الأب. نستطيع أيضًا تفادي إنشاء الحلقة التي ستتسبب أخيرًا في طفحان المكدّس عند طباعة leaf
كما هو الحال في الشيفرة 26؛ إذ تُطبع مراجع Weak<Node>
مع الكلمة (Weak)
جانبها:
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
يشير عدم وجود خرج لانهائي إلى أن الشيفرة لم تُنشئ حلقة مرجع، ويمكنك التأكد من ذلك عن طريق ملاحظة القيم التي نحصل عليها باستدعاء كل من Rc::strong_count
و Rc::weak_count
.
مشاهدة التغييرات التي تحصل على strong_count و weak_count
دعنا نلاحظ كيف تتغير قيم نُسخ Rc<Node>
لكل من النسخة strong_count
و weak_count
، وذلك عن طريق إنشاء نطاق داخلي جديد ونقل عملية إنشاء branch
إلى هذا النطاق، إذ نستطيع بفعل ذلك مشاهدة ما الذي يحصل عند إنشاء العقدة branch
وتحريرها بعد أن تخرج من النطاق. التعديلات موضحة في الشيفرة 29:
اسم الملف: src/main.rs
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); { let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( "branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch), ); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); } println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); }
الشيفرة 29: إنشاء branch
في نطاق داخلي وفحص عدد المراجع القوية والضعيفة
يبلغ عدد المراجع القوية للنوع Rc<Node>
القيمة 1 بعد إنشاء leaf
، وعدد المراجع الضعيفة يساوي 0. نُنشئ في النطاق الداخلي العقدة branch
ونربطها مع leaf
وهذه هي النقطة التي نبدأ فيها بطباعة عدد المراجع، يبلغ عدد المراجع القوية الخاصة بالنوع Rc<Node>
في branch
القيمة 1 وعدد المراجع الضعيفة 1 (لأن leaf.parent
تشير إلى branch
باستخدام قيمة من النوع <Weak<Node
). نلاحظ تغيّر عداد المراجع القوية إلى 2 عندما نطبع عدد المراجع القوية والضعيفة في leaf
وذلك لأن branch
هي نسخةٌ من النوع Rc<Node>
من القيمة leaf
ومخزّنة في branch.children
، إلا أننا ما زلنا نحصل على عدد مراجع ضعيفة يساوي 0.
تخرج branch
عن النطاق عندما ينتهي النطاق الداخلي وينقص عدد المراجع القوي الخاص بالنوع Rc<Node>
إلى 0، لذا تُحرّر قيمة Node
الخاصة به. لا يوجد تأثير لعدد المراجع الضعيفة البالغ 1 ضمن leaf.parent
على تحرير القيمة Node
أو عدم تحريرها ولذلك لا نحصل على تسريب للذاكرة.
إذا أردنا الوصول إلى العقدة الأب الخاصة بالعقدة leaf
في نهاية النطاق، فسنحصل على None
مجددًا. عدد المراجع القوية الخاصة بالنوع Rc<Node>
في leaf
هو 1، وعدد المراجع الضعيفة هو 0 في نهاية البرنامج، وذلك لأن المتغير leaf
هو المرجع الوحيد للنوع Rc<Node>
مجددًا.
المنطق الذي يُدير عدّ وتحرير القيم مُطبَّق في كلٍّ من Rc<T>
و Weak<T>
، إضافةً إلى تنفيذ السمة Drop
. سيجعل تحديد أن علاقة العقدة الابن بالعقدة الأب يجب أن تكون مرجعًا ضعيفًا من النوع Weak<T>
في تعريف Node
، وجود عقدة أب تشير إلى عُقد ابن وبالعكس ممكنًا دون إنشاء حلقة مرجع والتسبُّب بتسريب ذاكرة.
ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language.
اقرأ أيضًا
- المقال التالي: استخدام الخيوط Threads لتنفيذ شيفرات رست بصورة متزامنة آنيًا
-
المقال السابق: المؤشر الذكي Refcell
ونمط قابلية التغيير الداخلي interior mutability في لغة رست Rust - المؤشرات الذكية Smart Pointers في رست Rust
- أنواع البيانات Data Types في لغة رست Rust
-
المؤشر Rc
الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust - مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة Rust
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.