Menu
flag Ar iconالعربيةdown icon

تضمين البنى في Golang: التضمين والتوابع المرقّاة

يضع التضمين نوعًا داخل آخر دون اسم حقل، فتُرقّى حقوله وتوابعه إلى النوع الخارجي. تعلّم كيف تعمل الترقية، وتضمين الواجهات، وتعارض الأسماء، ولماذا ليس التضمين وراثة.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

التضمين في مثال واحد

الحقل الذي له نوع وليس له اسم حقل مضمّن (embedded). تصبح حقول النوع المضمّن وتوابعه قابلة للوصول مباشرة على النوع الخارجي.

المخرجات:

Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}

اسم الحقل المضمّن هو اسم نوعه، User. وبه تشير إليه في الحرفيات (User: User{...}) وعندما تحتاج القيمة الداخلية نفسها (a.User). لا يمكن ضبط حقل مرقّى باسمه المختصر في حرفية: تفشل Admin{Name: "Ana"} برسالة unknown field Name in struct literal of type Admin.

التوابع المرقّاة تحقّق الواجهات

تضيف الترقية توابع النوع المضمّن إلى مجموعة توابع النوع الخارجي. فيحقّق النوع الخارجي كل واجهة يحقّقها النوع الداخلي.

تتبع قواعد مجموعات التوابع النوع المضمّن. تضمين T يرقّي توابع مستقبِل القيمة في T إلى القيمة الخارجية والمؤشر الخارجي كليهما، لكنه يرقّي توابع مستقبِل المؤشر في *T إلى المؤشر الخارجي وحده. أما تضمين *T فيرقّي المجموعتين إلى الاثنين. عند الشك ضمّن المؤشر، أو استخدم النوع الخارجي عبر مؤشر.

لتضمين المؤشر كلفة: قد يكون المؤشر nil. تُترجم Service{} دون Logger، ثم تسبّب svc.Log(...) حالة panic.

التضمين ليس وراثة

يشبه التضمين الاشتقاق من صنف، لكن ثلاثة أشياء تتصرّف بشكل مختلف عن Java أو Python.

النوع الخارجي ليس النوع الداخلي. Admin ليس User. الدالة التي تأخذ User لن تقبل Admin؛ مرّر a.User.

لا يوجد إرسال افتراضي (virtual dispatch). يعمل التابع المرقّى على القيمة المضمّنة، ولا يعرف شيئًا عن النوع الخارجي. إذا استدعى تابعًا آخر فهو يستدعي نسخة نوعه، حتى لو عرّف النوع الخارجي تابعًا بالاسم نفسه.

المخرجات:

woof
The animal says ...

في لغة قائمة على الأصناف كانت Speak ستطبع "woof". أما في Go فإن d.Speak() اختصار لـ d.Animal.Speak()، ومستقبِل ذلك الاستدعاء هو Animal. عندما تريد سلوكًا يختلف حسب النوع استخدم واجهة: مرّر Sounder إلى الشيفرة التي تحتاجه.

النوع الداخلي لا يعرف أنه مضمّن. لا يوجد super. لتوسيع تابع مرقّى، عرّف تابعًا بالاسم نفسه على النوع الخارجي واستدعِ الداخلي صراحة:

func (a Admin) Greet() string {
	return a.User.Greet() + " (admin)"
}

تعارض الأسماء والتظليل

الحقل أو التابع على النوع الخارجي يظلّل المرقّى الذي له الاسم نفسه، كما فعل Dog.Sound سابقًا. الاسم الأقل عمقًا يفوز.

عندما يرقّي نوعان مضمّنان في العمق نفسه الاسم نفسه، يصبح ذلك الاسم ملتبسًا. ويبقى البرنامج قابلًا للترجمة ما دام لا شيء يستخدم الاسم الملتبس:

حلّ التعارض باستخدام المسار الكامل، أو بتعريف ID على النوع الخارجي.

تضمين الواجهات

يمكن للواجهات تضمين واجهات أخرى. تبني المكتبة القياسية الواجهات الأكبر بهذه الطريقة:

type ReadWriter interface {
	Reader
	Writer
}

ويمكن للبنية أيضًا تضمين واجهة. تحقّق البنية عندها تلك الواجهة عبر أي قيمة تخزّنها في الحقل، وتعيد تعريف التوابع التي تهمّك فقط. هذا هو نمط المزخرف (decorator) بشيفرة شبه معدومة:

الاستدعاء الصريح c.Reader.Read(p) ضروري. كتابة c.Read(p) داخل Read ستستدعي نفسها إلى الأبد.

تضمين واجهة في بديل وهمي للاختبار اختصار شائع: ضمّن الواجهة الكبيرة، وطبّق التابع الوحيد الذي يحتاجه الاختبار، واترك البقية. أي تابع غير مطبّق يسبّب panic بسبب إلغاء مرجعية مؤشر nil إذا استُدعي، وهذا غالبًا ما تريده في اختبار.

استخدام حقيقي شائع: تضمين sync.Mutex

type Stats struct {
	sync.Mutex
	hits map[string]int
}

func (s *Stats) Hit(page string) {
	s.Lock()
	defer s.Unlock()
	s.hits[page]++
}

هذا يُقرأ جيدًا، لكنه يصدّر أيضًا Lock وUnlock كجزء من الواجهة البرمجية لـ Stats، فيستطيع أي مستدعٍ قفل بنيتك. للأنواع المستخدمة خارج الحزمة، يُبقي حقل مسمّى (mu sync.Mutex) القفل خاصًا. وهذه المقايضة تنطبق على كل تضمين: كل ما يصدّره النوع المضمّن يصبح جزءًا من السطح العام لنوعك.

أخطاء شائعة

  • توقّع الإرسال الافتراضي. التوابع المرقّاة لا تستدعي أبدًا إعادات تعريف النوع الخارجي.
  • ضبط الحقول المرقّاة في حرفية. استخدم اسم النوع المضمّن: Admin{User: User{Name: "Ana"}}.
  • المؤشرات المضمّنة nil. يجب ضبط *T أو الواجهة المضمّنة قبل استدعاء توابعها.
  • سطح واجهة برمجية غير مقصود. التضمين يصدّر كل التوابع المُصدَّرة للنوع الداخلي على نوعك.

الأسئلة الشائعة

ما هو تضمين البنى في Go؟

تعريف حقل بنوع فقط دون اسم: type Admin struct { User; Level int }. تُرقّى حقول User المضمّن وتوابعه، فتعمل a.Name وa.Greet() على Admin مباشرة. وتبقى القيمة المضمّنة حقلًا عاديًا، يمكن الوصول إليه بـ a.User.

هل تملك Go الوراثة؟

لا. لا تملك Go أصنافًا ولا وراثة أنواع فرعية. يعطي التضمين إعادة استخدام الشيفرة عبر التركيب: يحصل النوع الخارجي على توابع النوع الداخلي، لكن Admin ليس User. لا يمكنك تمرير Admin حيث يُتوقَّع User، ولا تستطيع التوابع المرقّاة استدعاء توابع النوع الخارجي. يأتي تعدّد الأشكال في Go من الواجهات.

كيف تهيّئ بنية مضمّنة في Go؟

في الحرفية المركّبة، سمِّ الحقل المضمّن باسم نوعه: Admin{User: User{Name: "Ana"}, Level: 2}. لا يمكنك ضبط الحقول المرقّاة مباشرة في الحرفية: تفشل Admin{Name: "Ana"} برسالة unknown field Name in struct literal of type Admin.

هل يمكن تضمين واجهة في بنية في Go؟

نعم. تحقّق البنية عندها الواجهة عبر القيمة المضمّنة، ويمكنك إعادة تعريف توابع بعينها. هذا شائع في المزخرفات والبدائل الوهمية للاختبار. إذا كان حقل الواجهة المضمّنة nil، فإن استدعاء تابع لم تُعِد تعريفه يسبّب panic بسبب إلغاء مرجعية مؤشر nil.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن