شعار ConstructiCat
CodeBust.
تصفح القسم ▾

الرقم السحري.

الرقم السحري قيمة حرفية بلا اسم في الشيفرة يضطر القارئ إلى تخمين معناها؛ وتسميتها كثابت تجعل الشيفرة أوضح وأكثر أماناً عند التعديل.

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

ويُستخدم المصطلح أيضاً بمعانٍ أخرى: ثابت يُخزَّن في بداية الملف لتحديد صيغته (ما يُعرف بـتوقيع الملف، مثل %PDF في بداية مستندات PDF)، وقيمة مميزة مثل 0xDEADBEEF تُكتب في الذاكرة لتسهيل التنقيح. هذه الاستخدامات ليست أنماطاً مضادة، وهي خارج نطاق هذه المقالة.

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

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

على سبيل المثال، في الشيفرة التالية التي تحسب السعر بعد الضريبة، يُعدّ 1.05 رقمًا سحريًا لأن القيمة تُرمّز معدّل ضريبة المبيعات، 5%، بطريقة أقل من واضحة.

price_after_tax = 1.05 * price

إن استخدام الأرقام السحرية في الشيفرة يحجب نيّة المطورين من اختيار ذلك الرقم، ويزيد من فرص حدوث أخطاء خفية، ويجعل تكييف البرنامج وتوسيعه مستقبلًا أكثر صعوبة. وكمثال، يصعب معرفة ما إذا كانت كل خانة في 3.14159265358979323846 قد كُتبت بشكل صحيح، أو ما إذا كان من الممكن اقتطاع هذا الثابت الخاص بـباي إلى 3.14159 دون التأثير في وظيفة البرنامج رغم دقته المنخفضة. إن استبدال كل الأرقام السحرية المهمّة بـثوابت مُسمّاة (تُسمّى أيضًا متغيرات توضيحية) يجعل البرامج أسهل في القراءة والفهم والصيانة.

يمكن تحسين المثال أعلاه بإضافة متغير ذي اسم وصفي:

TAX = 0.05
price_after_tax = (1.0 + TAX) * price

يمكن للاسم الجيد أن ينتج عنه شيفرة أسهل في الفهم على من يصونها وليس مؤلفها الأصلي، بل وحتى على المؤلف الأصلي بعد مرور فترة من الزمن. ومن أمثلة الثابت ذي الاسم غير المفيد int SIXTEEN = 16، في حين قد يكون int NUMBER_OF_BITS = 16 أكثر فائدة.

يمكن للبيانات غير العددية أن تحمل الخصائص السحرية ذاتها، وبالتالي المشكلات ذاتها التي تحملها الأرقام السحرية. لذا فإن التصريح بـconst string testUserName = "John" واستخدام testUserName قد يكون أفضل من استخدام القيمة الحرفية "John" مباشرة.

مثال: خلط أوراق اللعب

على سبيل المثال، إذا لزم خلط القيم عشوائيًا في مصفوفة تمثّل رزمة قياسية من أوراق اللعب، فإن هذه الشيفرة الزائفة تؤدي المهمة باستخدام خوارزمية خلط فيشر–ييتس:

for i from 1 to 52
    j := i + randomInt(53 - i) - 1
    a.swapEntries(i, j)

حيث a كائن مصفوفة، والدالة randomInt(x) تختار عددًا صحيحًا عشوائيًا بين 1 وx شاملًا الطرفين، وتُبدّل swapEntries(i, j) العنصرين i وj في المصفوفة. في المثال السابق، يُعدّ 52 و53 رقمين سحريين، وليس من الواضح أيضًا كيف يرتبطان ببعضهما. ويُعدّ من الأفضل أسلوبيًا في البرمجة كتابة ما يلي:

int deckSize:= 52
for i from 1 to deckSize
    j := i + randomInt(deckSize + 1 - i) - 1
    a.swapEntries(i, j)

وهذا أفضل لعدة أسباب:

  • قابلية قراءة أفضل. قد يتساءل مبرمج يقرأ المثال الأول: ماذا يعني الرقم 52 هنا؟ ولماذا 52؟ قد يستنتج المبرمج المعنى بعد قراءة الشيفرة بعناية، لكنه ليس بديهيًا. وتصبح الأرقام السحرية مربكة بوجه خاص عندما يُستخدم الرقم نفسه لأغراض مختلفة في قسم واحد من الشيفرة.
  • أسهل في الصيانة. من الأسهل تغيير قيمة الرقم لأنها غير مكررة. إن تغيير قيمة رقم سحري عُرضة للخطأ، لأن القيمة نفسها كثيرًا ما تُستخدم عدة مرات في أماكن مختلفة داخل البرنامج. كذلك، عندما يحمل متغيران أو رقمان متمايزان دلاليًا القيمة نفسها، فقد يُعدَّلان معًا عرضًا. ولتعديل المثال الأول كي يخلط رزمة تاروت، التي تضم 78 ورقة، قد يستبدل المبرمج بسذاجة كل ظهور للرقم 52 في البرنامج بالرقم 78. وهذا يسبب مشكلتين. أولاً، سيُغفِل القيمة 53 في السطر الثاني من المثال، مما يجعل الخوارزمية تفشل بطريقة خفية. ثانيًا، من المرجح أن يستبدل الحرفين «52» في كل مكان، بصرف النظر عمّا إذا كانا يشيران إلى حجم الرزمة أو إلى شيء آخر مختلف تمامًا، كعدد أسابيع السنة في التقويم الميلادي، أو على نحو أكثر خبثًا، أن يكونا جزءًا من رقم مثل «1523»، وكل ذلك من شأنه أن يُدخل أخطاءً برمجية. وعلى النقيض، فإن تغيير قيمة المتغير deckSize في المثال الثاني سيكون تغييرًا بسيطًا في سطر واحد.
  • يشجّع على التوثيق. يوفّر المكان الوحيد الذي يُصرَّح فيه عن المتغير المُسمّى موضعًا جيدًا لتوثيق ما تعنيه القيمة ولماذا حملت القيمة التي حملتها. أما وجود القيمة نفسها في عدد وفير من الأماكن فإما أن يؤدي إلى تعليقات مكررة (وما يصحبها من مشكلات عند تحديث بعضها وإغفال بعضها الآخر)، أو لا يترك أيّ مكانٍ واحد يكون فيه من الطبيعي للمؤلف أن يشرح القيمة ومن المرجح أن يبحث فيه القارئ عن شرح.
  • يجمّع المعلومات. يمكن وضع تصريحات متغيرات «الأرقام السحرية» معًا، عادةً في أعلى دالة أو ملف، مما يسهّل مراجعتها وتغييرها.
  • يكشف الأخطاء المطبعية. إن استخدام متغير (بدلًا من قيمة حرفية) يستفيد من فحص المترجم. فكتابة «62» عرضًا بدلًا من «52» ستمرّ دون كشف، في حين أن كتابة «dekSize» بدلًا من «deckSize» ستؤدي إلى تحذير المترجم بأن dekSize غير مُصرَّح به.
  • يقلّل من الكتابة. إذا كانت بيئة التطوير المتكاملة تدعم إكمال الشيفرة، فستملأ معظم اسم المتغير انطلاقًا من أحرفه القليلة الأولى.
  • يسهّل المعاملة الوسيطية. على سبيل المثال، لتعميم المثال أعلاه إلى إجراء يخلط رزمة بأي عدد من الأوراق، يكفي تحويل deckSize إلى وسيط لذلك الإجراء، في حين أن المثال الأول سيتطلب عدة تغييرات.
function shuffle (int deckSize)
   for i from 1 to deckSize
       j := i + randomInt(deckSize + 1 - i) - 1
       a.swapEntries(i, j)

أما العيوب فهي:

  • يكسر المحلّية. عندما لا يُعرَّف الثابت المُسمّى قرب موضع استخدامه، فإن ذلك يضرّ بمحلّية الشيفرة وبالتالي بقابليتها للفهم. إن وضع الرقم 52 في مكان قد يكون بعيدًا يعني أنه لفهم آلية عمل حلقة «for» فهمًا كاملًا (مثلًا لتقدير زمن تشغيل الحلقة)، يجب على المرء تتبّع التعريف والتحقق من أنه الرقم المتوقَّع. ومن السهل تجنّب ذلك (بإعادة موضع التصريح) عندما يُستخدم الثابت في جزء واحد فقط من الشيفرة. أما عندما يُستخدم الثابت المُسمّى في أجزاء متفرقة، فإن الموقع البعيد يكون دلالةً للقارئ على أن القيمة نفسها تظهر في أماكن أخرى من الشيفرة، وقد يستحق ذلك النظر فيه أيضًا.
  • يسبّب الإطناب. يضيف تصريح الثابت سطرًا. وعندما يكون اسم الثابت أطول من قيمته، خاصةً إذا ظهرت عدة ثوابت من هذا النوع في سطر واحد، فقد يستلزم ذلك تقسيم عبارة منطقية واحدة من الشيفرة على عدة أسطر. وقد تكون الزيادة في الإطناب مبرَّرة عندما يكون هناك احتمال للبس حول الثابت، أو عندما يكون هناك احتمال لحاجة الثابت إلى التغيير، مثل إعادة استخدام روتين خلط لألعاب ورق أخرى. وقد تكون مبرَّرة بالقدر نفسه باعتبارها زيادة في القدرة التعبيرية.
  • اعتبارات الأداء. قد تكون معالجة التعبير deckSize + 1 وقت التشغيل أبطأ من القيمة «53». ومع ذلك، فإن معظم المترجمات الحديثة تستخدم تقنيات مثل طيّ الثوابت وتحسين الحلقات لحلّ عملية الجمع أثناء الترجمة، لذا لا توجد عادةً عقوبة في السرعة أو تكون مهملة مقارنةً باستخدام الأرقام السحرية في الشيفرة. وعلى وجه الخصوص، يجب موازنة تكلفة التنقيح والوقت اللازم لمحاولة فهم شيفرة غير توضيحية في مقابل تكلفة الحساب الضئيلة.

الاستخدام المقبول

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

  • استخدام 0 و1 كقيمتين ابتدائيتين أو تزايديتين في حلقة for، مثل for (int i = 0; i < max; i += 1)
  • استخدام 2 للتحقق مما إذا كان رقم زوجيًا أم فرديًا، كما في isEven = (x % 2 == 0)، حيث % هو عامل باقي القسمة
  • استخدام القيم الحرفية البسيطة، مثلًا في تعابير من قبيل circumference = 2 * Math.PI * radius، أو لحساب مميِّز معادلة تربيعية على صورة d = b^2 − 4*a*c
  • استخدام قوى العدد 10 لتحويل القيم المترية (مثلًا بين الغرامات والكيلوغرامات) أو لحساب قيم النسبة المئوية والنسبة في الألف
  • الأُسُس في تعابير مثل (f(x) ** 2 + f(y) ** 2) ** 0.5 من أجل √(f(x)² + f(y)²)
  • تُستخدم القيمتان الحرفيتان 1 و0 أحيانًا لتمثيل القيمتين المنطقيتين true و false. ويمكن القول إن إسناد هاتين القيمتين إلى اسمين مثل TRUE وFALSE قد يكون أفضل.
  • في لغتي C وC++، كثيرًا ما يُستخدم 0 للدلالة على المؤشر الصفري رغم أن مكتبة C القياسية تعرّف ماكرو NULL وأن C++ الحديثة تتضمن الكلمة المفتاحية nullptr.

مثال

في الدالة أدناه، ترمّز القيم 1.2 و30 و0.015 قواعد عمل — ضريبة القيمة المضافة ومهلة السماح وغرامة التأخير — ويضطر القارئ إلى تخمين أيّها يعني ماذا. والأسوأ أن 30 تظهر مرتين بمعنيين مختلفين.

class Billing {
    static double total(double net, int daysLate) {
        double total = net * 1.2;
        if (daysLate > 30) {
            total += total * 0.015 * (daysLate / 30);
        }
        return total;
    }
}

صار لكل قيمة اسم، ويمكن تغيير مهلة السماح وفترة الفوترة كلٌّ على حدة (انظر استبدال الرقم السحري بثابت رمزي).

class Billing {
    static final double VAT_RATE = 0.20;
    static final int GRACE_PERIOD_DAYS = 30;
    static final int DAYS_PER_BILLING_PERIOD = 30;
    static final double LATE_FEE_RATE_PER_PERIOD = 0.015;

    static double total(double net, int daysLate) {
        double total = net * (1 + VAT_RATE);
        if (daysLate > GRACE_PERIOD_DAYS) {
            int overduePeriods = daysLate / DAYS_PER_BILLING_PERIOD;
            total += total * LATE_FEE_RATE_PER_PERIOD * overduePeriods;
        }
        return total;
    }
}

مقتبس من مقالة ويكيبيديا “Magic number (programming)” (المراجعة 1355218563)، والمتاحة بموجب رخصة CC BY-SA 4.0؛ تمت ترجمة النص وتحريره وإضافة أمثلة.