اذهب إلى المحتوى

تعريف التوابع Methods في لغة جو Go


هدى جبور

تُمكّنك الدوال 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...