أنواع الواجهات صنف خاص من الأنواع في Go. وتؤدي الواجهات عدة أدوار مهمة في Go. فمن حيث الأساس، تُمكّن أنواع الواجهات لغة Go من دعم تغليف القيم (value boxing). ونتيجة لذلك، يُدعم الانعكاس وتعدد الأشكال عبر تغليف القيم.
منذ الإصدار 1.18، أصبحت Go تدعم الأجناس العامة المخصّصة (custom generics). وفي الأجناس العامة المخصّصة، يمكن أيضاً استخدام نوع واجهة (دائماً) كقيد نوع. في الواقع، جميع قيود الأنواع هي في الحقيقة أنواع واجهات. قبل Go 1.18، كان يمكن استخدام جميع أنواع الواجهات كأنواع قيم. لكن منذ Go 1.18، قد تُستخدم بعض أنواع الواجهات كقيود أنواع فقط. وتُسمّى أنواع الواجهات التي يمكن استخدامها كأنواع قيم أنواع الواجهات الأساسية.
كُتب هذا المقال أساساً قبل أن تدعم Go الأجناس العامة المخصّصة، لذا يتناول أساساً الواجهات الأساسية. ولمعرفة أنواع الواجهات المخصّصة للقيود فقط، يرجى قراءة كتاب Go Generics 101 للحصول على التفاصيل.
أنواع الواجهات ومجموعات الأنواع
يعرّف نوع الواجهة بعض المتطلبات (النوعية). وتشكّل جميع الأنواع غير الواجهية التي تحقق هذه المتطلبات مجموعة أنواع تُسمّى مجموعة الأنواع (type set) لنوع الواجهة.
تُعبَّر المتطلبات المعرَّفة لنوع واجهة عن طريق تضمين بعض عناصر الواجهة في نوع الواجهة. حالياً (Go 1.25)، يوجد صنفان من عناصر الواجهة: عناصر الطرق وعناصر الأنواع.
- يظهر عنصر الطريقة كـمواصفة طريقة. ولا يجوز لمواصفة طريقة مضمنة في نوع واجهة أن تستخدم المعرّف الفارغ
_كاسم لها. - قد يكون عنصر النوع اسم نوع، أو تمثيلاً حرفياً لنوع، أو نوعاً تقريبياً، أو اتحاد أنواع. ولا يتناول هذا المقال الأخيرَين كثيراً، بل يتناول فقط أسماء الأنواع والتمثيلات الحرفية التي تدل على أنواع الواجهات.
مثلاً، نوع الواجهة error المُعلن مسبقاً، الذي يظهر تعريفه أدناه، يضمّن مواصفة الطريقة Error() string. وفي التعريف، يُسمّى interface{...} تمثيلاً حرفياً لنوع واجهة، وكلمة interface كلمة مفتاحية في Go.
type error interface {
Error() string
}
يمكننا أيضاً القول إن نوع الواجهة error يحدّد (مباشرةً) طريقة Error() string. وتتكوّن مجموعة أنواعه من جميع الأنواع غير الواجهية التي تملك طريقة بمواصفة Error() string. ومن الناحية النظرية، مجموعة الأنواع لا نهائية. وبالطبع، فهي منتهية بالنسبة إلى مشروع Go محدّد.
في ما يلي بعض تعريفات أنواع الواجهات الأخرى وتعريفات الأسماء المستعارة.
// هذا النوع يحدّد مباشرةً طريقتين
// ويضمّن نوعَي واجهة آخرين، أحدهما
// اسم نوع والآخر تمثيل حرفي لنوع.
type ReadWriteCloser = interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
error // اسم نوع
interface{ Close() error } // تمثيل حرفي لنوع
}
// هذا النوع يضمّن نوعاً تقريبياً. وتتضمّن
// مجموعة أنواعه جميع الأنواع التي نوعها الكامن []byte.
type AnyByteSlice = interface {
~[]byte
}
// هذا النوع يضمّن اتحاد أنواع. وتتضمّن
// مجموعة أنواعه 6 أنواع: uint وuint8 وuint16 وuint32 وuint64 وuintptr.
type Unsigned interface {
uint | uint8 | uint16 | uint32 | uint64 | uintptr
}
تضمين نوع واجهة (يُشار إليه إما باسم نوع أو بتمثيل حرفي) في نوع آخر يكافئ (توسيعاً تكراريّاً) عناصر الأول في الثاني. مثلاً، نوع الواجهة الذي يدل عليه الاسم المستعار ReadWriteCloser يكافئ نوع الواجهة الذي يدل عليه التمثيل الحرفي التالي، والذي يحدّد مباشرةً أربع طرق.
interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Error() string
Close() error
}
تتكوّن مجموعة الأنواع لنوع الواجهة أعلاه من جميع الأنواع غير الواجهية التي تملك على الأقل الطرق الأربع التي يحدّدها نوع الواجهة. ومجموعة الأنواع هذه لا نهائية أيضاً. وهي بالتأكيد مجموعة فرعية من مجموعة الأنواع لنوع الواجهة error.
يرجى ملاحظة أنه قبل Go 1.18، لم يكن يُسمح بتضمين في أنواع الواجهات إلا أسماء أنواع الواجهات.
أنواع الواجهات الظاهرة في الشيفرة التالية تُسمّى كلها أنواع واجهات فارغة، وهي لا تضمّن شيئاً ولا تحدّد أي طرق.
// نوع الواجهة الفارغ غير المسمّى.
interface{}
// Nothing نوع واجهة فارغ معرَّف.
type Nothing interface{}
في الواقع، قدّمت Go 1.18 اسماً مستعاراً مُعلناً مسبقاً هو any، والذي يدل على نوع الواجهة الفارغ interface{}.
تتكوّن مجموعة الأنواع لنوع واجهة فارغ من جميع الأنواع غير الواجهية.
مجموعات طرق الأنواع
لكل نوع مجموعة طرق مرتبطة به.
- بالنسبة إلى نوع غير واجهة، تتكوّن مجموعة طرقه من مواصفات جميع الطرق المعرَّفة له (سواء كانت صريحة أم ضمنية).
- بالنسبة إلى نوع واجهة، تتكوّن مجموعة طرقه من جميع مواصفات الطرق التي يحدّدها، سواء مباشرةً أو بشكل غير مباشر عبر تضمين أنواع أخرى.
في الأمثلة المعروضة في القسم السابق،
- تحتوي مجموعة طرق نوع الواجهة الذي يدل عليه
ReadWriteCloserعلى أربع طرق. - تحتوي مجموعة طرق نوع الواجهة المُعلن مسبقاً
errorعلى طريقة واحدة فقط. - مجموعة طرق نوع الواجهة الفارغ فارغة.
وللتسهيل، تُسمّى غالباً مجموعة طرق نوع ما أيضاً مجموعة طرق أي قيمة من ذلك النوع.
أنواع الواجهات الأساسية
أنواع الواجهات الأساسية هي أنواع الواجهات التي يمكن استخدامها كأنواع قيم. ويُسمّى نوع الواجهة غير الأساسي أيضاً نوع واجهة مخصّص للقيود فقط.
حالياً (Go 1.25)، يمكن تعريف كل نوع واجهة أساسي كلياً بمجموعة طرق (قد تكون فارغة). بعبارة أخرى، لا يحتاج نوع الواجهة الأساسي إلى عناصر أنواع ليُعرَّف.
في الأمثلة المعروضة في القسم قبل السابق، نوع الواجهة الذي يدل عليه الاسم المستعار ReadWriteCloser نوع أساسي، أما نوع الواجهة Unsigned والنوع الذي يدل عليه الاسم المستعار AnyByteSlice فليسا كذلك. والأخيران نوعا واجهة مخصّصان للقيود فقط.
أنواع الواجهات الفارغة ونوع الواجهة error المُعلن مسبقاً هي أيضاً جميعها أنواع واجهات أساسية.
يتطابق نوعا واجهة أساسيان غير مسمّيين إذا تطابقت مجموعتا طرقهما. يرجى ملاحظة أن الطرق غير المصدَّرة (التي أسماؤها معرّفات غير مصدَّرة) من حزم مختلفة تُعتبر دائماً طريقتين مختلفتين، حتى لو تطابقت أسماؤها.
التطبيق
إذا كان نوع غير واجهة محتوىً في مجموعة الأنواع لنوع واجهة، نقول إن النوع غير الواجهي يطبّق نوع الواجهة. وإذا كانت مجموعة الأنواع لنوع واجهة مجموعة فرعية من نوع واجهة آخر، نقول إن الأول يطبّق الثاني.
يطبّق نوع الواجهة نفسه دائماً، لأن مجموعة الأنواع دائماً مجموعة فرعية (أو شاملة) لنفسها. وبالمثل، يطبّق نوعا واجهة لهما مجموعة الطرق نفسها أحدهما الآخر. في الواقع، يتطابق نوعا واجهة غير مسمّيين إذا تطابقت مجموعتا أنواعهما.
إذا طبّق نوع T نوع واجهة X، فيجب أن تكون مجموعة طرق T مجموعة شاملة لمجموعة طرق X، سواء أكان T نوع واجهة أم نوعاً غير واجهة. مثلاً، في الأمثلة الواردة في قسم سابق، يطبّق نوع الواجهة الذي يدل عليه ReadWriteCloser نوع الواجهة error.
جميع عمليات التطبيق ضمنية في Go. فلا يشترط المصرِّف أن تُحدَّد علاقات التطبيق في الشيفرة صراحةً. وليست هناك كلمة مفتاحية implements في Go. وستتحقق مصرِّفات Go من علاقات التطبيق تلقائياً حسب الحاجة.
مثلاً، في المثال التالي، تحتوي مجموعات طرق نوع مؤشر البنية *Book، ونوع العدد الصحيح MyInt، ونوع المؤشر *MyInt جميعاً على مواصفة الطريقة About() string، لذا تطبّق كلها نوع الواجهة Aboutable المذكور أعلاه.
type Aboutable interface {
About() string
}
type Book struct {
name string
// حقول أخرى إضافية ...
}
func (book *Book) About() string {
return "Book: " + book.name
}
type MyInt int
func (MyInt) About() string {
return "I'm a custom integer value"
}
يتيح تصميم التطبيق الضمني جعل الأنواع المعرَّفة في حزم مكتبات أخرى، مثل الحزم القياسية، تطبّق سلبياً بعض أنواع الواجهات المعرَّفة في حزم المستخدم. مثلاً، إذا عرّفنا نوع واجهة كما يلي، فإن النوعين DB وTx المعرَّفين في الحزمة القياسية database/sql سيطبّقان نوع الواجهة تلقائياً، لأن كليهما يملك الطرق الثلاث المقابلة المحدّدة في الواجهة.
import "database/sql"
...
type DatabaseStorer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
}
لاحظ أنه بما أن مجموعة الأنواع لنوع الواجهة الفارغ تتكوّن من جميع الأنواع غير الواجهية، فإن جميع الأنواع تطبّق أي نوع واجهة فارغ. وهذه حقيقة مهمة في Go.
تغليف القيم
مرة أخرى، حالياً (Go 1.25)، يجب أن تكون أنواع قيم الواجهات أنواع واجهات أساسية. وفي ما تبقّى من هذا المقال، عندما يُذكر نوع قيمة، فقد يكون نوعاً غير واجهة أو نوع واجهة أساسي. وهو ليس أبداً نوع واجهة مخصّصاً للقيود فقط.
يمكننا النظر إلى كل قيمة واجهة كصندوق لتغليف قيمة غير واجهية. ولتغليف قيمة غير واجهية في قيمة واجهة، يجب أن يطبّق نوع القيمة غير الواجهية نوع قيمة الواجهة.
إذا طبّق نوع T نوع واجهة (أساسي) I، فيمكن تحويل أي قيمة من النوع T ضمنياً إلى النوع I. بعبارة أخرى، أي قيمة من النوع T قابلة للإسناد إلى قيم (قابلة للتغيير) من النوع I. وعندما تُحوَّل قيمة T (أو تُسند) إلى قيمة I،
- إذا كان النوع
Tنوعاً غير واجهة، فتُغلَّف نسخة من قيمةT(أو تُحاط) في قيمةIالناتجة (أو الوجهة). والتعقيد الزمني للنسخ هوO(n)، حيثnحجم قيمةTالمنسوخة. (يُجري المصرِّف القياسي لـ Go عدة تحسينات لتقليل تكاليف تغليف القيم التي تحقق شروطاً معينة. يرجى قراءة فصل "Interfaces" في كتاب Go Optimizations 101 للاطلاع على هذه التحسينات.) - إذا كان النوع
Tنوع واجهة أيضاً، فتُغلَّف نسخة من القيمة المُغلَّفة في قيمةT(أو تُحاط) في قيمةIالناتجة (أو الوجهة). ويُجري المصرِّف القياسي لـ Go تحسيناً هنا، لذا يكون التعقيد الزمني للنسخO(1)بدلاً منO(n).
وتُخزَّن أيضاً معلومات نوع القيمة المُغلَّفة في قيمة الواجهة الناتجة (أو الوجهة). (سيُشرح هذا أكثر أدناه.)
عندما تُغلَّف قيمة في قيمة واجهة، تُسمّى القيمة القيمة الديناميكية لقيمة الواجهة. ويُسمّى نوع القيمة الديناميكية النوع الديناميكي لقيمة الواجهة.
الجزء المباشر من القيمة الديناميكية لقيمة واجهة غير قابل للتغيير، مع أننا نستطيع استبدال القيمة الديناميكية لقيمة واجهة بقيمة ديناميكية أخرى.
في Go، تُمثَّل القيم الصفرية لأي نوع واجهة بالمعرّف nil المُعلن مسبقاً. ولا شيء مُغلَّف في قيمة واجهة nil. وإسناد nil غير المُنمَّط إلى قيمة واجهة يمسح القيمة الديناميكية المُغلَّفة في قيمة الواجهة.
(ملاحظة، تُمثَّل القيم الصفرية لكثير من الأنواع غير الواجهية في Go أيضاً بـ nil. ويمكن أيضاً تغليف قيم nil غير واجهية في قيم الواجهات. وقيمة الواجهة التي تُغلّف قيمة nil غير واجهية لا تزال تغلّف شيئاً، لذا فهي ليست قيمة واجهة nil.)
بما أن أي نوع يطبّق جميع أنواع الواجهات الفارغة، فإن أي قيمة غير واجهية يمكن تغليفها في قيمة واجهة فارغة (أو إسنادها إليها). ولهذا السبب، يمكن النظر إلى أنواع الواجهات الفارغة كنوع any في كثير من اللغات الأخرى.
عندما تُسند قيمة غير مُنمَّطة (باستثناء قيم nil غير المُنمَّطة) إلى قيمة واجهة فارغة، تُحوَّل القيمة غير المُنمَّطة أولاً إلى نوعها الافتراضي. (بعبارة أخرى، يمكننا اعتبار أن القيمة غير المُنمَّطة تُستنتج كقيمة من نوعها الافتراضي).
لنستعرض مثالاً يوضّح بعض الإسنادات التي تكون قيم الواجهات فيها وجهات.
package main
import "fmt"
type Aboutable interface {
About() string
}
// النوع *Book يطبّق Aboutable.
type Book struct {
name string
}
func (book *Book) About() string {
return "Book: " + book.name
}
func main() {
// تُغلَّف قيمة *Book في
// قيمة واجهة من النوع Aboutable.
var a Aboutable = &Book{"Go 101"}
fmt.Println(a) // &{Go 101}
// i قيمة واجهة فارغة.
var i interface{} = &Book{"Rust 101"}
fmt.Println(i) // &{Rust 101}
// Aboutable تطبّق interface{}.
i = a
fmt.Println(i) // &{Go 101}
}
يرجى ملاحظة أن نموذج الدالة fmt.Println المستخدم مرات كثيرة في المقالات السابقة هو
func Println(a ...interface{}) (n int, err error)
ولهذا السبب يمكن لاستدعاءات الدالة fmt.Println أن تأخذ معطيات من أي أنواع.
في ما يلي مثال آخر يوضّح كيف تُستخدم قيمة واجهة فارغة لتغليف قيم من أي نوع غير واجهة.
package main
import "fmt"
func main() {
var i interface{}
i = []int{1, 2, 3}
fmt.Println(i) // [1 2 3]
i = map[string]int{"Go": 2012}
fmt.Println(i) // map[Go:2012]
i = true
fmt.Println(i) // true
i = 1
fmt.Println(i) // 1
i = "abc"
fmt.Println(i) // abc
// امسح القيمة المُغلَّفة في قيمة الواجهة i.
i = nil
fmt.Println(i) // <nil>
}
ستبني مصرِّفات Go جدولاً عاماً يحتوي على معلومات كل نوع في وقت التصريف. وتتضمّن المعلومات صنف النوع، والطرق والحقول التي يملكها النوع، ونوع عنصر نوع الحاوية، وأحجام الأنواع، وما إلى ذلك. وسيُحمَّل الجدول العام في الذاكرة عند بدء البرنامج.
في وقت التشغيل، عند تغليف قيمة غير واجهية في قيمة واجهة، سيحلّل وقت تشغيل Go (على الأقل بالنسبة إلى وقت التشغيل القياسي لـ Go) معلومات التطبيق الخاصة بزوج النوعين للقيمتين ويبنيها، ثم يخزّن معلومات التطبيق في قيمة الواجهة. ولن تُبنى معلومات التطبيق لكل زوج من نوع غير واجهة ونوع واجهة إلا مرة واحدة وتُخزَّن في خريطة عامة مراعاةً لكفاءة التنفيذ. ولا يتناقص عدد مُدخلات الخريطة العامة أبداً. في الواقع، لا تستخدم قيمة الواجهة غير الصفرية إلا حقل مؤشر داخلي يشير إلى مُدخل معلومات تطبيق مخزَّن.
تتضمّن معلومات التطبيق لكل زوج (نوع واجهة، نوع ديناميكي) معلومتين:
- معلومات النوع الديناميكي (وهو نوع غير واجهة)
- وجدول طرق (شريحة) يخزّن جميع الطرق المقابلة التي يحدّدها نوع الواجهة والمعرَّفة للنوع غير الواجهي (النوع الديناميكي).
هاتان المعلومتان أساسيتان لتطبيق ميزتين مهمتين في Go:
- معلومات النوع الديناميكي هي مفتاح تطبيق الانعكاس في Go.
- ومعلومات جدول الطرق هي مفتاح تطبيق تعدد الأشكال (سيُشرح تعدد الأشكال في القسم التالي).
تعدد الأشكال
تعدد الأشكال وظيفة رئيسية توفّرها الواجهات، وهو ميزة مهمة في Go.
عندما تُغلَّف قيمة غير واجهية t من النوع T في قيمة واجهة i من النوع I، فإن استدعاء طريقة يحدّدها نوع الواجهة I على قيمة الواجهة i سيستدعي في الواقع الطريقة المقابلة المعرَّفة للنوع غير الواجهي T على القيمة غير الواجهية t. بعبارة أخرى، استدعاء طريقة قيمة واجهة سيستدعي في الواقع الطريقة المقابلة للقيمة الديناميكية لقيمة الواجهة. مثلاً، استدعاء الطريقة i.m سيستدعي في الواقع الطريقة t.m. ومع تغليف قيم ديناميكية مختلفة من أنواع ديناميكية مختلفة في قيمة الواجهة، تتصرف قيمة الواجهة بشكل مختلف. وهذا ما يُسمّى تعدد الأشكال.
عند استدعاء الطريقة i.m، سيُبحث في جدول الطرق ضمن معلومات التطبيق المخزّنة في i للعثور على الطريقة المقابلة t.m واستدعائها. وجدول الطرق شريحة، والبحث ليس إلا فهرسة عنصر في شريحة، لذا فهو سريع.
(ملاحظة، استدعاء الطرق على قيمة واجهة nil سيؤدي إلى panic في وقت التشغيل، لأنه لا توجد طرق معرَّفة متاحة للاستدعاء.)
مثال:
package main
import "fmt"
type Filter interface {
About() string
Process([]int) []int
}
// تُستخدم UniqueFilter لإزالة الأعداد المكرّرة.
type UniqueFilter struct{}
func (UniqueFilter) About() string {
return "remove duplicate numbers"
}
func (UniqueFilter) Process(inputs []int) []int {
outs := make([]int, 0, len(inputs))
pusheds := make(map[int]bool)
for _, n := range inputs {
if !pusheds[n] {
pusheds[n] = true
outs = append(outs, n)
}
}
return outs
}
// تُستخدم MultipleFilter للإبقاء فقط على
// الأعداد التي تكون من مضاعفات
// قيمة MultipleFilter كعدد صحيح.
type MultipleFilter int
func (mf MultipleFilter) About() string {
return fmt.Sprintf("keep multiples of %v", mf)
}
func (mf MultipleFilter) Process(inputs []int) []int {
var outs = make([]int, 0, len(inputs))
for _, n := range inputs {
if n % int(mf) == 0 {
outs = append(outs, n)
}
}
return outs
}
// بمساعدة تعدد الأشكال، لا نحتاج إلا إلى
// دالة "filterAndPrint" واحدة.
func filterAndPrint(fltr Filter, unfiltered []int) []int {
// استدعاء طرق "fltr" سيستدعي في الواقع
// طرق القيمة المُغلَّفة في "fltr".
filtered := fltr.Process(unfiltered)
fmt.Println(fltr.About() + ":\n\t", filtered)
return filtered
}
func main() {
numbers := []int{12, 7, 21, 12, 12, 26, 25, 21, 30}
fmt.Println("before filtering:\n\t", numbers)
// تُغلَّف ثلاث قيم غير واجهية في
// ثلاث قيم عناصر شريحة من واجهة Filter.
filters := []Filter{
UniqueFilter{},
MultipleFilter(2),
MultipleFilter(3),
}
// سيُسند كل عنصر شريحة إلى المتغير المحلي
// "fltr" (من نوع الواجهة Filter) واحداً تلو الآخر.
// وستُنسخ أيضاً القيمة المُغلَّفة في كل
// عنصر إلى "fltr".
for _, fltr := range filters {
numbers = filterAndPrint(fltr, numbers)
}
}
الناتج:
before filtering:
[12 7 21 12 12 26 25 21 30]
remove duplicate numbers:
[12 7 21 26 25 30]
keep multiples of 2:
[12 26 30]
keep multiples of 3:
[12 30]
في المثال أعلاه، يجعل تعدد الأشكال من غير الضروري كتابة دالة filterAndPrint واحدة لكل نوع مُرشِّح.
إلى جانب الفائدة أعلاه، يتيح تعدد الأشكال أيضاً لمطوّري حزمة شيفرة مكتبية تعريف نوع واجهة مصدَّر وتعريف دالة (أو طريقة) لها وسيط من نوع الواجهة، بحيث يستطيع مستخدم الحزمة تعريف نوع يطبّق نوع الواجهة في شيفرته وتمرير معطيات من نوعه إلى استدعاءات الدالة (أو الطريقة). ولا يحتاج مطوّرو حزمة الشيفرة إلى الاهتمام بكيفية تعريف نوع المستخدم، ما دام نوع المستخدم يحقق السلوكيات التي يحدّدها نوع الواجهة المعرَّف في حزمة الشيفرة المكتبية.
في الواقع، تعدد الأشكال ليس ميزة جوهرية لأي لغة. فهناك طرق بديلة لإنجاز المهمة نفسها، مثل دوال الاستدعاء المرتد. لكن طريقة تعدد الأشكال أنظف وأكثر أناقة.
الانعكاس
يمكن استخدام معلومات النوع الديناميكي المخزّنة في قيمة واجهة لفحص القيمة الديناميكية لقيمة الواجهة والتلاعب بالقيم التي تشير إليها القيمة الديناميكية. ويُسمّى هذا الانعكاس في البرمجة.
لن يشرح هذا المقال الوظائف التي توفّرها الحزمة القياسية reflect. يرجى قراءة الانعكاسات في Go لمعرفة كيفية استخدام تلك الحزمة. وفي ما يلي سنعرض فقط وظائف الانعكاس المدمجة في Go. في Go، تتحقق الانعكاسات المدمجة بتأكيدات النوع (type assertions) وكتل شيفرة التحكم في سير التنفيذ type-switch.
تأكيد النوع
هناك أربعة أصناف من حالات تحويل القيم التي تشمل قيم الواجهات في Go:
- تحويل قيمة غير واجهية إلى قيمة واجهة، حيث يجب أن يطبّق نوع القيمة غير الواجهية نوع قيمة الواجهة.
- تحويل قيمة واجهة إلى قيمة واجهة، حيث يجب أن يطبّق نوع قيمة الواجهة المصدر نوع قيمة الواجهة الوجهة.
- تحويل قيمة واجهة إلى قيمة غير واجهية، حيث يجب أن يطبّق نوع القيمة غير الواجهية نوع قيمة الواجهة.
- تحويل قيمة واجهة إلى قيمة واجهة، حيث لا يطبّق نوع قيمة الواجهة المصدر نوع الواجهة الوجهة، لكن قد يطبّق النوع الديناميكي لقيمة الواجهة المصدر نوع الواجهة الوجهة.
لقد شرحنا بالفعل الصنفين الأولين من الحالات. وكلاهما يشترط أن يطبّق نوع القيمة المصدر نوع الواجهة الوجهة. ويُتحقق من قابلية التحويل في الصنفين الأولين في وقت التصريف.
وسنشرح هنا الصنفين الأخيرين من الحالات. ويُتحقق من قابلية التحويل في الصنفين الأخيرين في وقت التشغيل، باستخدام صياغة تُسمّى تأكيد النوع. في الواقع، تنطبق هذه الصياغة أيضاً على الصنف الثاني من التحويل في القائمة أعلاه.
شكل تعبير تأكيد النوع هو i.(T)، حيث i قيمة واجهة وT اسم نوع أو تمثيل حرفي لنوع. ويجب أن يكون النوع T
- إما نوعاً غير واجهة اعتباطياً،
- أو نوع واجهة اعتباطياً.
في تأكيد النوع i.(T)، تُسمّى i القيمة المُؤكَّدة (asserted value) ويُسمّى T النوع المُؤكَّد (asserted type). وقد ينجح تأكيد النوع أو يفشل.
- في حالة كون
Tنوعاً غير واجهة، إذا وُجد النوع الديناميكي لـiوكان مطابقاً لـT، ينجح التأكيد، وإلا يفشل التأكيد. وعندما ينجح التأكيد، تكون نتيجة تقييمه نسخة من القيمة الديناميكية لـi. ويمكننا النظر إلى تأكيدات هذا الصنف كمحاولات لفك تغليف القيم. - في حالة كون
Tنوع واجهة، إذا وُجد النوع الديناميكي لـiوكان يطبّقT، ينجح التأكيد، وإلا يفشل التأكيد. وعندما ينجح التأكيد، تُغلَّف نسخة من القيمة الديناميكية لـiفي قيمةTوتُستخدم قيمةTكنتيجة تقييم للتأكيد.
عندما يفشل تأكيد نوع، تكون نتيجة تقييمه قيمة صفرية من النوع المُؤكَّد.
وفق القواعد الموصوفة أعلاه، إذا كانت القيمة المُؤكَّدة في تأكيد نوع قيمة واجهة nil، فسيفشل التأكيد دائماً.
في معظم الحالات، يُستخدم تأكيد النوع كتعبير ذي قيمة واحدة. لكن عندما يُستخدم تأكيد النوع كتعبير القيمة المصدر الوحيد في إسناد، يمكن أن ينتج قيمة منطقية ثانية اختيارية غير مُنمَّطة ويُنظر إليه كتعبير متعدد القيم. وتشير القيمة المنطقية الثانية الاختيارية غير المُنمَّطة إلى ما إذا كان تأكيد النوع قد نجح.
لاحظ أنه إذا فشل تأكيد نوع وكان تأكيد النوع مستخدماً كتعبير ذي قيمة واحدة (نتيجة bool الثانية الاختيارية غائبة)، فسيحدث panic.
مثال يوضّح كيفية استخدام تأكيدات النوع (الأنواع المُؤكَّدة أنواع غير واجهة):
package main
import "fmt"
func main() {
// سيستنتج المصرِّف نوع 123 كـ int.
var x interface{} = 123
// الحالة 1:
n, ok := x.(int)
fmt.Println(n, ok) // 123 true
n = x.(int)
fmt.Println(n) // 123
// الحالة 2:
a, ok := x.(float64)
fmt.Println(a, ok) // 0 false
// الحالة 3:
a = x.(float64) // سيؤدي إلى panic
}
مثال آخر يوضّح كيفية استخدام تأكيدات النوع (الأنواع المُؤكَّدة أنواع واجهات):
package main
import "fmt"
type Writer interface {
Write(buf []byte) (int, error)
}
type DummyWriter struct{}
func (DummyWriter) Write(buf []byte) (int, error) {
return len(buf), nil
}
func main() {
var x interface{} = DummyWriter{}
var y interface{} = "abc"
// الآن النوع الديناميكي لـ y هو "string".
var w Writer
var ok bool
// النوع DummyWriter يطبّق
// كلاً من Writer وinterface{}.
w, ok = x.(Writer)
fmt.Println(w, ok) // {} true
x, ok = w.(interface{})
fmt.Println(x, ok) // {} true
// النوع الديناميكي لـ y هو "string"،
// وهو لا يطبّق Writer.
w, ok = y.(Writer)
fmt.Println(w, ok) // <nil> false
w = y.(Writer) // سيؤدي إلى panic
}
في الواقع، بالنسبة إلى قيمة واجهة i نوعها الديناميكي T، فإن استدعاء الطريقة i.m(...) يكافئ استدعاء الطريقة i.(T).m(...).
كتلة التحكم في سير التنفيذ type-switch
قد تكون صياغة كتلة الشيفرة type-switch أغرب صياغة في Go. ويمكن النظر إليها كنسخة محسّنة من تأكيد النوع. وتشبه كتلة الشيفرة type-switch كتلة الشيفرة switch-case للتحكم في سير التنفيذ من بعض النواحي. وهي تبدو هكذا:
switch aSimpleStatement; v := x.(type) {
case TypeA:
...
case TypeB, TypeC:
...
case nil:
...
default:
...
}
جزء aSimpleStatement; اختياري في كتلة الشيفرة type-switch. ويجب أن يكون aSimpleStatement جملة بسيطة. ويجب أن تكون x قيمة واجهة وتُسمّى القيمة المُؤكَّدة. وتُسمّى v نتيجة التأكيد، ويجب أن ترد في صيغة تعريف متغير قصير.
يمكن أن تتبع كل كلمة مفتاحية case في كتلة type-switch معرّف nil المُعلن مسبقاً أو قائمة مفصولة بفواصل تتكوّن من اسم نوع واحد على الأقل وتمثيل حرفي لنوع. ولا يجوز تكرار أي من هذه العناصر (nil وأسماء الأنواع والتمثيلات الحرفية) في كتلة الشيفرة type-switch نفسها.
إذا لم يكن النوع الذي يدل عليه اسم نوع أو تمثيل حرفي يلي كلمة مفتاحية case في كتلة الشيفرة type-switch نوع واجهة، فيجب أن يطبّق نوع الواجهة للقيمة المُؤكَّدة.
إليك مثالاً تُستخدم فيه كتلة شيفرة type-switch للتحكم في سير التنفيذ.
package main
import "fmt"
func main() {
values := []interface{}{
456, "abc", true, 0.33, int32(789),
[]int{1, 2, 3}, map[int]bool{}, nil,
}
for _, x := range values {
// هنا، تُعرَّف v مرة واحدة، لكنها تدل
// على متغيرات مختلفة في فروع مختلفة.
switch v := x.(type) {
case []int: // تمثيل حرفي لنوع
// نوع v هو "[]int" في هذا الفرع.
fmt.Println("int slice:", v)
case string: // اسم نوع واحد
// نوع v هو "string" في هذا الفرع.
fmt.Println("string:", v)
case int, float64, int32: // عدة أسماء أنواع
// نوع v هو "interface{}"،
// مثل x في هذا الفرع.
fmt.Println("number:", v)
case nil:
// نوع v هو "interface{}"،
// مثل x في هذا الفرع.
fmt.Println(v)
default:
// نوع v هو "interface{}"،
// مثل x في هذا الفرع.
fmt.Println("others:", v)
}
// لاحظ أن كل متغير تدل عليه v في
// الفروع الثلاثة الأخيرة هو نسخة من x.
}
}
الناتج:
number: 456
string: abc
others: true
number: 0.33
number: 789
int slice: [1 2 3]
others: map[]
<nil>
المثال أعلاه يكافئ المثال التالي منطقياً:
package main
import "fmt"
func main() {
values := []interface{}{
456, "abc", true, 0.33, int32(789),
[]int{1, 2, 3}, map[int]bool{}, nil,
}
for _, x := range values {
if v, ok := x.([]int); ok {
fmt.Println("int slice:", v)
} else if v, ok := x.(string); ok {
fmt.Println("string:", v)
} else if x == nil {
v := x
fmt.Println(v)
} else {
_, isInt := x.(int)
_, isFloat64 := x.(float64)
_, isInt32 := x.(int32)
if isInt || isFloat64 || isInt32 {
v := x
fmt.Println("number:", v)
} else {
v := x
fmt.Println("others:", v)
}
}
}
}
تشبه كتل الشيفرة type-switch كتل الشيفرة switch-case في بعض الجوانب.
- مثل كتل
switch-case، لا يمكن أن يوجد في كتلة الشيفرةtype-switchأكثر من فرعdefaultواحد. - مثل كتل
switch-case، إذا وُجد فرعdefaultفي كتلة الشيفرةtype-switch، فيمكن أن يكون الفرع الأخير أو الأول أو فرعاً وسطياً. - مثل كتل
switch-case، قد لا تحتوي كتلة الشيفرةtype-switchعلى أي فروع، وستُنظر عندئذٍ كعملية لا فعل لها.
لكن، خلافاً لكتل الشيفرة switch-case، لا يمكن استخدام جمل fallthrough داخل كتل الفروع في كتلة الشيفرة type-switch.
المزيد عن الواجهات في Go
المقارنات التي تشمل قيم الواجهات
هناك حالتان من المقارنات التي تشمل قيم الواجهات:
- المقارنات بين قيمة غير واجهية وقيمة واجهة.
- المقارنات بين قيمتي واجهة.
في الحالة الأولى، يجب أن يطبّق نوع القيمة غير الواجهية نوع قيمة الواجهة (لنفترض أنه I)، لذا يمكن تحويل القيمة غير الواجهية إلى قيمة واجهة من I (أو تغليفها فيها). وهذا يعني أن مقارنة بين قيمة غير واجهية وقيمة واجهة يمكن ترجمتها إلى مقارنة بين قيمتي واجهة. لذا سيُشرح أدناه فقط المقارنات بين قيمتي واجهة.
مقارنة قيمتي واجهة هي في الواقع مقارنة نوعيهما الديناميكيين وقيمتيهما الديناميكيتين.
خطوات مقارنة قيمتي واجهة (بالمعامل ==):
- إذا كانت إحدى قيمتي الواجهة قيمة واجهة nil، فنتيجة المقارنة هي ما إذا كانت قيمة الواجهة الأخرى قيمة واجهة nil أيضاً.
- إذا كان النوعان الديناميكيان لقيمتي الواجهة نوعين مختلفين، فنتيجة المقارنة هي
false. - في حالة كون النوعين الديناميكيين لقيمتي الواجهة النوع نفسه، - إذا كان النوع الديناميكي نفسه نوعاً غير قابل للمقارنة، فسيحدث panic.
- وإلا، فنتيجة المقارنة هي نتيجة مقارنة القيمتين الديناميكيتين لقيمتي الواجهة.
باختصار، تتساوى قيمتا واجهة فقط إذا تحقق أحد الشرطين التاليين.
- أن تكونا معاً قيمتي واجهة nil.
- أن يكون نوعاهما الديناميكيان متطابقين وقابلين للمقارنة، وأن تتساوى قيمتاهما الديناميكيتان.
وفقاً لهذه القواعد، قد لا تتساوى قيمتا واجهة تكون قيمتاهما الديناميكيتان nil معاً. مثال:
package main
import "fmt"
func main() {
var a, b, c interface{} = "abc", 123, "a"+"b"+"c"
// حالة من الخطوة 2.
fmt.Println(a == b) // false
// حالة من الخطوة 3.
fmt.Println(a == c) // true
var x *int = nil
var y *bool = nil
var ix, iy interface{} = x, y
var i interface{} = nil
// حالة من الخطوة 2.
fmt.Println(ix == iy) // false
// حالة من الخطوة 1.
fmt.Println(ix == i) // false
// حالة من الخطوة 1.
fmt.Println(iy == i) // false
// []int نوع غير قابل للمقارنة
var s []int = nil
i = s
// حالة من الخطوة 1.
fmt.Println(i == nil) // false
// حالة من الخطوة 3.
fmt.Println(i == i) // سيؤدي إلى panic
}
البنية الداخلية لقيم الواجهات
بالنسبة إلى المصرِّف/وقت التشغيل الرسمي لـ Go، تُمثَّل قيم الواجهات الفارغة وقيم الواجهات غير الفارغة ببنييتين داخليتين مختلفتين. يرجى قراءة أجزاء القيمة للتفاصيل.
لا يمكن تحويل قيم []T مباشرةً إلى []I، حتى لو كان النوع T يطبّق نوع الواجهة I.
مثلاً، قد نحتاج أحياناً إلى تحويل قيمة []string إلى النوع []interface{}. وخلافاً لبعض اللغات الأخرى، لا توجد طريقة مباشرة لإجراء التحويل. ويجب إجراء التحويل يدوياً في حلقة:
package main
import "fmt"
func main() {
words := []string{
"Go", "is", "a", "high",
"efficient", "language.",
}
// نموذج دالة fmt.Println هو
// func Println(a ...interface{}) (n int, err error).
// لذا لا يمكن تمرير words... إليها كمعطى.
// fmt.Println(words...) // لا تُصرَّف
// حوّل قيمة []string إلى []interface{}.
iw := make([]interface{}, 0, len(words))
for _, w := range words {
iw = append(iw, w)
}
fmt.Println(iw...) // تُصرَّف بلا مشكلة
}
كل طريقة يحدّدها نوع واجهة تقابل دالة ضمنية
لكل طريقة اسمها m في مجموعة الطرق التي يحدّدها نوع واجهة I، ستعرّف المصرِّفات ضمنياً دالة اسمها I.m، لها وسيط إدخال واحد أكثر من الطريقة m، من النوع I. والوسيط الإضافي هو وسيط الإدخال الأول للدالة I.m. بافتراض أن i قيمة واجهة من I، فإن استدعاء الطريقة i.m(...) يكافئ استدعاء الدالة I.m(i, ...).
مثال:
package main
import "fmt"
type I interface {
m(int)bool
}
type T string
func (t T) m(n int) bool {
return len(t) > n
}
func main() {
var i I = T("gopher")
fmt.Println(i.m(5)) // true
fmt.Println(I.m(i, 5)) // true
fmt.Println(interface{m(int)bool}.m(i, 5)) // true
// الأسطر التالية تُصرَّف بلا مشكلة،
// لكنها ستؤدي إلى panic في وقت التشغيل.
I(nil).m(5)
I.m(nil, 5)
interface {m(int) bool}.m(nil, 5)
}