تُمكّنك الدوال Functions من تنظيم منطق مُحدد ضمن إجراءات procedures قابلة للتكرار يمكنها استخدام وسطاء مختلفة في كل مرة تُنفّذ فيها. تعمل عدة دوال غالبًا على نفس الجزء من البيانات في كل مرة. يُمكن لغة جو التعرّف على هذه الأنماط، وتسمح لك بتعريف دوال خاصة تُسمى "التوابع Methods"، وتجري عمليات على نوع بيانات مُحدد يُشار إليه باسم "المُستقبل Receiver".
تعريف تابع
قواعد كتابة التوابع مُشابهة لقواعد كتابة الدوال، والاختلاف الوحيد هو إضافة معامل يُوضع بعد الكلمة المفتاحية func
لتحديد مُستقبل التابع. المُستقبل هو تصريح لنوع البيانات الذي تُريد تعريف التابع من أجله. نُصرّح في المثال التالي عن تابع يُطبّق على نوع بنية struct:
package main import "fmt" type Creature struct { Name string Greeting string } func (c Creature) Greet() { fmt.Printf("%s says %s", c.Name, c.Greeting) } func main() { sammy := Creature{ Name: "Sammy", Greeting: "Hello!", } Creature.Greet(sammy) }
نحصل عند تشغيل الشيفرة على ما يلي:
Sammy says Hello!
أنشأنا بنيةً أسميناها Creature
تمتلك حقلين من النوع string
باسم Name
و Greeting
. لدى Creature
تابع وحيد مُعرّف يُسمى Greet
. أسندنا أثناء التصريح عن المستقبل نسخةً من Creature
إلى المتغير c
، بحيث يُمكننا من خلالها الوصول إلى حقول Creature
وطباعة محتوياته باستخدام دالة fmt.Printf
.
يُشار عادةً للمُستقبل في لغات البرمجة الأخرى بكلمات رئيسية مثل self
كما في لغة بايثون أو this
كما في لغة جافا. يُعد المستقبل في لغة جو مُتغيرًا كما باقي المتغيرات، وبالتالي يُمكنك تسميته كما تشاء. يُفضّل عمومًا تسميته بأول حرف من اسم نوع البيانات الذي تُريد أن يتعامل معه التابع، فقد سُمّي المعامل في المثال السابق بالاسم c
لأن نوع بيانات المستقبل كان Creature
.
أنشأنا داخل الدالة main
نسخةً من البنية Creature
وأعطينا حقولها Name
و Greeting
القيم "Sammy" و "!Hello" على التوالي. استدعينا التابع Greet
والذي أصبح مُعرّفًا على نوع البيانات Creature
من خلال وضع نقطة Creature.Greet
، ومرّرنا له النسخة التي أنشأناه للتو مثل وسيط. قد يبدو هذا الأسلوب في الاستدعاء مُربكًا قليلًا، سنعرض فيما يلي أسلوبًا آخر لاستدعاء التوابع:
package main import "fmt" type Creature struct { Name string Greeting string } func (c Creature) Greet() { fmt.Printf("%s says %s", c.Name, c.Greeting) } func main() { sammy := Creature{ Name: "Sammy", Greeting: "Hello!", } sammy.Greet() }
ستحصل عند تشغيل الشيفرة على نفس الخرج السابق:
Sammy says Hello!
لا يختلف هذا المثال عن المثال السابق إلا في استدعاء التابع Greet
، إذ استخدمنا هنا الأسلوب المختصر، بحيث نضع اسم المتغير الذي يُمثّل البنية Creature
وهو sammy
متبوعًا بنقطة ثم اسم التابع بين قوسين ()sammy.Greet
. يُحبّذ دومًا استخدام هذا الأسلوب الذي يُسمّى التدوين النقطي dot notation، فمن النادر أن يستخدم المطورون الأسلوب السابق الذي يُسمّى أسلوب الاستدعاء الوظيفي functional invocation style. يوضح المثال التالي أحد الأسباب التي تجعل هذا الأسلوب أكثر انتشارًا:
package main import "fmt" type Creature struct { Name string Greeting string } func (c Creature) Greet() Creature { fmt.Printf("%s says %s!\n", c.Name, c.Greeting) return c } func (c Creature) SayGoodbye(name string) { fmt.Println("Farewell", name, "!") } func main() { sammy := Creature{ Name: "Sammy", Greeting: "Hello!", } sammy.Greet().SayGoodbye("gophers") Creature.SayGoodbye(Creature.Greet(sammy), "gophers") }
عند تشغيل الشيفرة السابقة سيكون الخرج على النحو التالي:
Sammy says Hello!! Farewell gophers ! Sammy says Hello!! Farewell gophers !
عدّلنا الأمثلة السابقة لكي نُقدّم تابعًا جديدًا يُسمى SayGoodbye
، وعدّلنا أيضًا تعريف التابع Greet
بحيث يُعيد أيضًا المُعامل c
الذي يُمثل Creature
، وبالتالي سيكون لدينا إمكانية استخدام القيمة المُعادة من هذا التابع والمُتمثّلة بنسخة Creature
. نستدعي في الدالة main
التابعين SayGoodbye
و Greet
على المتغير sammy
باستخدام أسلوبي التدوين النقطي والاستدعاء الوظيفي.
يعطي الأسلوبان نفس النتائج في الخرج، لكن التدوين النُقطي أسهل للقراءة، كما أنه يعرض لنا عملية استدعاء التوابع مثل تسلسل، إذ نستدعي التابع Greet
من خلال المتغير sammy
الذي يُمثّل Creature
، ثم نستدعي التابع SayGoodbye
من خلال القيمة التي يُعيدها هذا التابع والتي تمثّل Creature
أيضًا.لا يعكس أسلوب الاستدعاء الوظيفي هذا الترتيب بسبب إضافة معامل إلى استدعاء SayGoodbye
يؤدي إلى حجب ترتيب الاستدعاءات. وضوح التدوين النقطي هو السبب في أنه النمط المفضل لاستدعاء التوابع في لغة جو، سواءً في المكتبة القياسية أو في الحزم الخارجية.
تعريف التوابع على نوع بيانات مُحدد هو أمر مُختلف عن تعريف الدوال التي تعمل بناءً على قيمة ما، وهذا له أهمية خاصة في لغة جو لأنه مفهوم أساسي في الواجهات interfaces.
الواجهات interfaces
عندما تُعرّف أي تابع على نوع بيانات في لغة جو، يضُاف هذا التابع إلى مجموعة التوابع الخاصة بهذا النوع، وهي مجموعة من الدوال المرتبطة بهذا النوع ويستخدمها مُصرّف لغة جو لتحديد إذا كان يمكن إسناد نوع ما لمتغير من نوع "واجهة interface"؛ وهذا النوع هو نهج يعتمده المُصرّف لضمان أن مُتغيّرًا من نوع البيانات المطلوب يُحقق التوابع التي تتضمنها الواجهة. يُعد أي نوع يمتلك توابع لها نفس الاسم ونفس المعلمات ونفس القيم المُعادة؛ مثل تلك الموجودة في تعريف الواجهة منفِّذًا لتلك الواجهة ويُسمح بإسناده إلى متغيرات من تلك الواجهة. نعرض فيما يلي تعريفًا لواجهة fmt.Stringer
من المكتبة القياسية:
type Stringer interface { String() string }
لاحظ هنا استخدام الكلمة المفتاحية type
لتعريف نوع بيانات جديد يُمثّل "واجهة". لكي ينفّذ أي نوع واجهة fmt.Stringer
، يجب أن يوفر تابعًا اسمه ()String
يُعيد سلسلة نصية. سيُمكّنك تنفيذ هذه الواجهة من طباعة "نوعك" تمامًا كما تريد ويُسمى ذلك "طباعة مُرتّبة pretty-printed"، وذلك عند تمرير نسخة من النوع الخاص بك إلى دوال محددة في حزمة fmt
. يُعرّف المثال التالي نوعًا ينفّذ هذه الواجهة:
package main import ( "fmt" "strings" ) type Ocean struct { Creatures []string } func (o Ocean) String() string { return strings.Join(o.Creatures, ", ") } func log(header string, s fmt.Stringer) { fmt.Println(header, ":", s) } func main() { o := Ocean{ Creatures: []string{ "sea urchin", "lobster", "shark", }, } log("ocean contains", o) }
ويكون الخرج على النحو التالي:
ocean contains : sea urchin, lobster, shark
صرّحنا في هذا المثال عن نوع بيانات جديد يُمثّل بنيةً اسمها Ocean
. يُمكننا القول أن هذه البنية تنفّذ الواجهة fmt.Stringer
لأنها تعرِّف تابعًا اسمه String
لا يأخذ أي وسطاء ويعيد سلسلةً نصية، أي تمامًا كما في الواجهة. عرّفنا داخل الدالة main
متغيرًا o
يُمثّل بنيةً Ocean
ومرّرناه إلى الدالة log
التي تطبع سلسة نصية تُمرّر لها، متبوعةً بأي شيء تُنفّذه وليكن fmt.Stringer
.
سيسمح لنا مُصرّف جو هنا أن نُمرّر البنية o
لأنه يُنفّذ كل التوابع التي تطلبها الدالة fmt.Stringer
(هنا يوجد تابع وحيد String
). نستخدم داخل الدالة log
دالة الطباعة fmt.Println
التي تستدعي التابع String
من البنية Ocean
لأننا مررنا لها المعامل s
من النوع fmt.Stringer
في ترويستها (أي بمثابة أحد معاملاتها).
إذا لم تنفّذ البنية Ocean
التابع String
سيُعطينا جو خطأً في التصريف، لأن الدالة log
تتطلب تمرير وسيط من النوع fmt.Stringer
، وسيكون الخطأ كما يلي:
src/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (missing String method)
سيتحقق مُصرّف لغة جو من مطابقة التابع ()String
للتابع المُستدعى من قِبل الواجهة fmt.Stringer
، وإذا لم يكن مُطابقًا، سيعطي الخطأ:
src/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log: Ocean does not implement fmt.Stringer (wrong type for String method) have String() want String() string
استخدمت التوابع المُعرّفة في الأمثلة السابقة "مُستقبل قيمة"، أي إذا استخدمنا أسلوب الاستدعاء الوظيفي للتوابع، سيكون المعامل الأول الذي يشير إلى النوع الذي عُرّف التابع عليه قيمةً من هذا النوع وليس مؤشرًا؛ وهذا يعني أن أي تعديل نُجريه على هذا النوع المُمثّل بالمعامل المُحدد (مثلًا في المثال السابق كان هذا المعامل هو o
) سيكون محليًّا وسيُنفّذ على نسخة من البيانات ولن يؤثر على النسخة الأصلية. سنرى فيما يلي أمثلة تستخدم مُستقبلات مرجعية "مؤشر".
مستقبلات مثل مؤشرات
يُشبه استخدام المؤشرات مثل مستقبلات في التوابع إلى حد كبير استخدام "مستقبل القيمة"، والفرق الوحيد هو وضع علامة *
قبل اسم النوع. يوضح المثال التالي تعريف تابع على مستقبل مؤشر إلى نوع:
package main import "fmt" type Boat struct { Name string occupants []string } func (b *Boat) AddOccupant(name string) *Boat { b.occupants = append(b.occupants, name) return b } func (b Boat) Manifest() { fmt.Println("The", b.Name, "has the following occupants:") for _, n := range b.occupants { fmt.Println("\t", n) } } func main() { b := &Boat{ Name: "S.S. DigitalOcean", } b.AddOccupant("Sammy the Shark") b.AddOccupant("Larry the Lobster") b.Manifest() }
سيكون الخرج على النحو التالي:
The S.S. DigitalOcean has the following occupants: Sammy the Shark Larry the Lobster
يُعرّف هذا المثال نوعًا يُسمّى Boat
يمتلك حقلين هما Name
و occupants
. نريد حماية الحقل occupants
بحيث لا تتمكن الشيفرات الأخرى من خارج الحزمة أن تُعدّل عليه إلا من خلال التابع AddOccupant
، لهذا السبب جعلناه حقلًا غير مُصدّر، وذلك بجعل أول حرف صغيرًا. نريد أيضًا جعل التعديلات التي يُجريها التابع AddOccupant
على نفس المتغير وبالتالي نحتاج إلى تمريره بالمرجع؛ أي يجب أن نُعرّف مُستقبل مؤشر (b *Boat)
وليس مُستقبل قيمة، إذ سيعمل مُستقبل المؤشر على إجراء التعديلات على نفس بيانات المتغير الأصلي الموجودة في الذاكرة من خلال تمرير عنوانه. تعمل المؤشرات مثل مراجع إلى متغير من نوع محدد بدلاً من نسخة من هذا النوع، لذلك سيضمن تمرير عنوان متغير من النوع Boat
إلى التابع AddOccupant
تنفيذ التعديلات على المتغير نفسه وليس نسخةً منه.
نُعرّف داخل الدالة main
متغيرًا b
يحمل عنوان بنية من النوع Boat
، وذلك من خلال وضع &
قبل تعريف البنية (Boat*)
كما في الشيفرة أعلاه. استدعينا التابع AddOccupant
مرتين لإضافة عُنصرين.
يُعرّف التابع Manifest
على النوع Boat
ويستخدم مُستقبل قيمة (b Boat)
. ما زلنا قادرين على استدعاء Manifest
داخل الدالة main
لأن لغة جو قادرة على تحصيل dereference قيمة المؤشر تلقائيًا من Boat
، إذ تكافئ ()b.Manifest
هنا ()b).Manifest*)
.
بغض النظر عن نوع المستقبل الذي تُعرّفه لتابع ما؛ مستقبل مؤشر أو مستقبل القيمة، فإن له آثارًا مهمة عند محاولة إسناد قيم للمتغيرات من نوع واجهة.
المستقبلات مثل مؤشرات والواجهات
عندما تحاول إسناد قيمة متغير من نوع محدد إلى متغير نوعه واجهة، يتأكد مُصرّف جو ما إذا كان ذلك النوع يُنفّذ كل التوابع التي تتطلبها الواجهة. تختلف مجموعة التوابع لمستقبل المؤشر عن مستقبل القيمة لأن التوابع التي تتلقى مؤشرًا يمكنها التعديل على المُستقبل الخاص بها، بينما لا يمكن لتلك التي تتلقى قيمة فعل ذلك.
package main import "fmt" type Submersible interface { Dive() } type Shark struct { Name string isUnderwater bool } func (s Shark) String() string { if s.isUnderwater { return fmt.Sprintf("%s is underwater", s.Name) } return fmt.Sprintf("%s is on the surface", s.Name) } func (s *Shark) Dive() { s.isUnderwater = true } func submerge(s Submersible) { s.Dive() } func main() { s := &Shark{ Name: "Sammy", } fmt.Println(s) submerge(s) fmt.Println(s) }
سيكون الخرج على النحو التالي:
Sammy is on the surface Sammy is underwater
عرّفنا واجهة تُسمى Submersible
، تقبل أنواعًا تُنفّذ التابع ()Dive
الخاص بها. عرّفنا أيضًا النوع Shark
مع الحقل Name
والتابع isUnderwater
لتتبع حالة متغيرات هذا النوع. عرّفنا أيضًا التابع ()Dive
مع مُستقبل مؤشر من النوع Shark
، إذ يُعدّل هذا التابع قيمة التابع isUnderwater
لتصبح true
. عرّفنا أيضًا التابع ()String
مع مُستقبل قيمة من النوع Shark
لطباعة حالة Shark
بأسلوب مُرتب باستخدام fmt.Println
ومن خلال الواجهة fmt.Stringer
التي تعرّفنا عليها مؤخرًا. عرّفنا أيضًا الدالة submerge
التي تأخذ معاملًا من النوع Submersible
.
يسمح لنا استخدام الواجهة Submersible
بدلًا من Shark*
في الدالة submerge
جعل هذه الدالة تعتمد فقط على السلوك الذي يوفره النوع، وبالتالي جعلها أكثر قابلية لإعادة الاستخدام، فلن تضطر إلى كتابة دوال submerge
جديدة لحالات خاصة أخرى مثل Submarine
أو Whale
أو أي كائنات مائية أخرى في وقت لاحق، فطالما أنها تُعرّف التابع ()Dive
يمكنها أن تعمل مع الدالة submerge
.
نُعرّف داخل الدالة main
المتغير s
الذي يمثّل مؤشرًا إلى Shark
ونطبعه مباشرةً باستخدام الدالة fmt.Println
مما يؤدي إلى طباعة Sammy is on the surface
على شاشة الخرج. نمرر بعدها المتغير s
إلى الدالة submerge
ثم نستدعي الدالة fmt.Println
مرةً أخرى على المتغير s
مما يؤدي لطباعة Sammy is underwater
.
إذا جعلنا s
من النوع Shark
بدلًا من Shark*
، سيعطي المُصرّف الخطأ التالي:
cannot use s (type Shark) as type Submersible in argument to submerge: Shark does not implement Submersible (Dive method has pointer receiver)
يخبرنا مُصرّف جو أن Shark
تمتلك التابع Dive
وأن ذلك معرّفٌ في مستقبل المؤشر. عندما ترى رسالة الخطأ هذه، فإن الحل هو تمرير مؤشر إلى نوع الواجهة باستخدام العامل &
قبل اسم المتغير الذي ترغب بإسناده لمتغير آخر.
خاتمة
لا يختلف التصريح عن توابع في لغة جو كثيرًا عن تعريف الدوال التي تستقبل أنواعًا مختلفة من المتغيرات، إذ تنطبق نفس قواعد العمل مع المؤشرات. توفر لغة جو الراحة في تعريف الدوال ويسمح بجمعها في مجموعات من التوابع التي يمكن تفسيرها من خلال أنواع الواجهة. سيسمح لك استخدام التوابع بطريقة فعّالة بالعمل مع الواجهات في شيفراتك البرمجية وتحسين إمكانية الاختبار، كما يجعل الشيفرة أكثر تنظيمًا وسهلة القراءة للمطورين الآخرين.
ترجمة -وبتصرف- للمقال Defining Methods in Go لصاحبه Gopher Guides.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.