Go الفعّالة › المُعرِّف الفارغ 12 / 16 السابق التالي

المُعرِّف الفارغ

لقد ذكرنا المُعرِّف الفارغ مرّات قليلة الآن، في سياق حلقات for range والخرائط. ويمكن إسناد المُعرِّف الفارغ أو تصريحه بأي قيمة من أي نوع، فتُهمَل القيمة بلا ضرر. وهو أشبه قليلًا بالكتابة إلى ملف /dev/null في يونكس: فهو يمثّل قيمة للكتابة فقط، تُستعمل كعنصر نائب في مكان نحتاج فيه متغيّرًا بينما القيمة الفعلية لا تهمّ. وله استعمالات تتجاوز ما رأيناه حتى الآن.

المُعرِّف الفارغ في الإسناد المتعدّد

استعمال المُعرِّف الفارغ في حلقة for range حالة خاصة من موقف عام: الإسناد المتعدّد.

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

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}

وستجد أحيانًا شيفرة تتخلّص من قيمة الخطأ بغرض تجاهل الخطأ؛ وهذه ممارسة سيّئة حقًّا. تحقّق دائمًا من قيم الأخطاء؛ فهي مقدَّمة لسبب ما:

// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
    fmt.Printf("%s is a directory\n", path)
}

الاستيرادات والمتغيّرات غير المستعملة

استيراد حزمة أو تصريح متغيّر من دون استعماله يُعدّ خطأ. فالاستيرادات غير المستعملة تنفخ حجم البرنامج وتُبطئ التصريف، أمّا المتغيّر المُهيّأ غير المستعمل فهو على الأقل حساب مهدور، وقد يكون إشارة إلى خلل أكبر.

غير أنه أثناء التطوير النشِط تنشأ فيها استيرادات ومتغيّرات غير مستعملة كثيرًا، وقد يكون حذفها لمجرّد أن يتم التصريف أمرًا مزعجًا، لا مجرّد أن تعود الحاجة إليها لاحقًا. ويوفّر المُعرِّف الفارغ حلًّا احتياطيًا.

فهذا البرنامج نصف المكتوب فيه استيرادان غير مستعملَين — fmt وio — ومتغيّر غير مستعمل هو fd، ولذلك لن يُصرَّف. لكن من الجيد أن نرى ما إذا كانت الشيفرة حتى الآن صحيحة:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
}

ولإسكات الشكاوى بشأن الاستيرادات غير المستعملة، استعمل مُعرِّفًا فارغًا للإشارة إلى رمز من الحزمة المستوردة. وبالمثل، فإن إسناد المتغيّر غير المستعمل fd إلى المُعرِّف الفارغ يُسكت خطأ المتغيّر غير المستعمل. وهذه النسخة من البرنامج تُصرَّف فعلًا:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader    // For debugging; delete when done.

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
    _ = fd
}

وبالعرف، ينبغي أن تأتي التصاريحات العامة المُستعملة لإسكات أخطاء الاستيراد مباشرةً بعد جملة الاستيراد، وأن تكون معلَّقة بتعليق، وذلك ليسهل العثور عليها، وتكون تذكيرًا بتنظيف الأمور لاحقًا.

الاستيراد من أجل الأثر الجانبي

الاستيراد غير المستعمل مثل fmt أو io في المثال السابق ينبغي أن يُستعمل أو يُحذف في النهاية: فالإسنادات الفارغة تُشير إلى أن الشيفرة على أنها عمل جارٍ. لكن أحيانًا يكون من المفيد استيراد حزمة من أجل آثارها الجانبية فحسب، من دون أي استعمال صريح. فمثلًا، أثناء دالتها init، تُسجّل حزمة net/http/pprof معالجات HTTP توفّر معلومات تنقيح. ولديها واجهة برمجية مصدَّرة، لكن معظم العملاء لا يحتاجون سوى تسجيل المعالجات والوصول إلى البيانات عبر صفحة ويب. ولاستيراد الحزمة من أجل آثارها الجانبية فحسب، أعِد تسمية الحزمة إلى المُعرِّف الفارغ:

import _ "net/http/pprof"

وهذه الصورة من الاستيراد توضّح أن الحزمة تُستورد من أجل آثارها الجانبية، إذ لا توجد أي استعمال آخر ممكن للحزمة: فليس لها اسم في هذا الملف. (ولو كان لها اسم ولم نستعمله لرفض المُصرِّف البرنامج.)

فحوص الواجهات

كما رأينا في الحديث عن الواجهات أعلاه، لا يلزم أن يُصرّح النوع عن إشبعه لواجهة. بل إن النوع يُشبع الواجهة بمجرّد تنفيذ دوال الواجهة. وعلى صعيد الممارسة، معظم تحويلات الواجهة تحويلات ساكنة، ولذلك تُفحص وقت التصريف. فمثلًا، تمرير *os.File إلى دالة تتوقّع io.Reader لن يُصرَّف ما لم يكن *os.File يُشبع الواجهة io.Reader.

غير أن بعض فحوص الواجهات تحدث وقت التشغيل فعلًا. ومن أمثلتها الحزمة encoding/json التي تعرّف واجهة Marshaler. فحين يتلقّى مُرمِّز JSON قيمة تُشبع تلك الواجهة، يستدعي دالة الترميز (marshaling) الخاصة بالقيمة ليحوّلها إلى JSON بدلًا من إجراء التحويل المعياري. ويفحص المُرمِّز هذه الخاصية وقت التشغيل بتأكيد نوع من هذا النوع:

m, ok := val.(json.Marshaler)

وإن كان المطلوب هو السؤال عمّا إذا كان نوع ما يُشبع واجهة ما، من دون استعمال الواجهة نفسها فعلًا — ربما كجزء من فحص خطأ — فاستعمل المُعرِّف الفارغ لتجاهل القيمة المؤكَّد عليها:

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

وكلفة هذا الموقف تظهر عندما يلزم ضمان، داخل الحزمة المنفِّذة للنوع، أن النوع يُشبع الواجهة فعلًا. فإذا كان نوع — مثل json.RawMessage — يحتاج تمثيل JSON مخصّصًا، فينبغي أن يُشبع json.Marshaler، لكن لا توجد تحويلات ساكنة تجعل المُصرِّف يتحقّق من ذلك تلقائيًا. ولو أخفق النوع من غير قصد في إشباع الواجهة، فإن مُرمِّز JSON سيعمل مع ذلك، لكنه لن يستعمل التنفيذ المخصّص. ولضمان صحّة التنفيذ، يمكن استعمال تصريح عام بالمُعرِّف الفارغ داخل الحزمة:

var _ json.Marshaler = (*RawMessage)(nil)

وفي هذا التصريح، يتطلّب الإسناد الذي ينطوي على تحويل *RawMessage إلى Marshaler أن يكون *RawMessage مُشبعًا لـ Marshaler، وستُفحص هذه الخاصية وقت التصريف. ولو تغيّرت واجهة json.Marshaler، فلن تُصرَّف هذه الحزمة بعد ذلك وسنُنبَّه إلى أنها بحاجة إلى تحديث.

ودلالة ظهور المُعرِّف الفارغ في هذا التركيب هي أن التصريح موجود لفحص الأنواع فحسب، لا لإنشاء متغيّر. لكن لا تفعل هذا مع كل نوع يُشبع واجهة ما. فبحسب العرف، لا تُستعمل هذه التصاريحات إلا حين لا توجد تحويلات ساكنة موجودة بالفعل في الشيفرة، وهو أمر نادر.