الأدلة التقنية

الأنظمة العددية للمبرمجين: من Binary إلى Floating Point

لماذا يظهر القرص 465 بدل 500، ويصبح العداد سالباً فجأة، ولا يساوي 0.1 + 0.2 القيمة 0.3؟ الإجابة في طريقة تخزين الأرقام.

الأنظمة العددية للمبرمجين: من Binary إلى Floating Point

كل يوم تقريباً يمر عليك رقم لا يشبه الأرقام التي تعلمتها في المدرسة، فالأمر chmod 750 يعطي صاحب الملف كل الصلاحيات ويمنع الآخرين، ولون الموقع مكتوب #E95420، والقرص الذي اشتريته بسعة 500 GB يظهر في النظام 465 فقط، وفي يوم ما سوف تكتب في JavaScript السطر 0.1 + 0.2 فتحصل على 0.30000000000000004، أو تجد عداداً في قاعدة البيانات أصبح فجأة رقماً سالباً. وكل هذه الأشياء لها سبب واحد، وهو أن الحاسب لا يعرف إلا البت Bit، أي صفراً أو واحداً، وكل رقم تراه في الكود أو في السيرفر هو طريقة معينة لكتابة هذه البتات أو تفسيرها. وإذا فهمت هذه الطرق مرة واحدة فسوف تقرأ هذه الأرقام كما تقرأ الأرقام العادية، وتعرف لماذا يحدث الخطأ قبل أن يصلك بلاغ من العميل.

وهذا المقال من مرحلة أساسيات علوم الحاسب Computer Science Fundamentals في خارطة عرب رووت، وهو الأساس الذي تقوم عليه بقية مقالاتها، فمقال الترميز Encoding يبدأ من البايت والـ Hex، ومقال الذاكرة للمبرمجين يتعامل مع العناوين وأحجام الأنواع. وسوف نناقش في هذا المقال ما يلي:

  • الأنظمة الموضعية Positional Numeral Systems: العشري والثنائي والثماني والست عشري، وكيف تحول بينها بيدك وفي الكود.
  • البت والبايت ووحدات الحجم، والفرق بين KB وKiB الذي يفسر القرص «الناقص».
  • الأرقام السالبة: لماذا فشلت الطريقة البديهية، وكيف حل المتمم الثنائي Two's Complement المشكلة.
  • الفيضان Overflow: ماذا يحدث عندما يتجاوز الرقم حجمه في C# وJava وPython وJavaScript، وحادثة حقيقية بسببه.
  • الأرقام العشرية Floating Point حسب IEEE 754، ولماذا لا يساوي 0.1 + 0.2 القيمة 0.3، وما الذي تستخدمه للمبالغ المالية.
  • ترتيب البايتات Endianness، والعمليات على البتات Bitwise Operations والأعلام Flags والصلاحيات.
  • تجربة عملية بالكود والمخرجات الحقيقية، ثم الأخطاء الشائعة.

قيمة الرقم تأتي من مكانه: الأنظمة الموضعية

عندما تكتب الرقم 2026 فأنت لا تكتب أربعة أرقام منفصلة، وإنما تقول إن لديك 2 من الألوف و0 من المئات و2 من العشرات و6 من الآحاد، فقيمة كل خانة تأتي من مكانها، وكل مكان أكبر من الذي على يمينه بعشر مرات، وهذا ما نسميه النظام الموضعي Positional Numeral System، والرقم 10 هنا هو الأساس Base. والفكرة نفسها تعمل مع أي أساس آخر، والفرق الوحيد هو عدد الرموز المتاحة لكل خانة وقيمة كل مكان:

  • العشري Decimal (الأساس 10): الرموز من 0 إلى 9، وقيم الخانات 1 و10 و100 و1000.
  • الثنائي Binary (الأساس 2): الرمزان 0 و1 فقط، وقيم الخانات 1 و2 و4 و8 و16، وهو النظام الذي يعمل به الحاسب فعلاً، والسبب أن الدائرة الكهربائية تميز بسهولة وموثوقية بين حالتين فقط: يوجد جهد أو لا يوجد.
  • الثماني Octal (الأساس 8): الرموز من 0 إلى 7، وكل خانة منه تساوي 3 بتات بالضبط، وأشهر مكان تراه فيه اليوم صلاحيات الملفات في Linux.
  • الست عشري Hexadecimal (الأساس 16): يحتاج 16 رمزاً لكل خانة، فبعد 9 نستخدم الحروف A=10 وB=11 حتى F=15، وكل خانة منه تساوي 4 بتات، أي أن البايت الواحد يكتب دائماً بخانتين، ولذلك تراه في الألوان وعناوين الذاكرة وعناوين MAC وIPv6 ومخرجات الهاش.

الصورة التالية تبين القاعدة نفسها في ثلاثة أنظمة، ولاحظ أن 7EA بالست عشري هي الرقم 2026 نفسه، كتبناه فقط بأساس مختلف:

ثلاثة صفوف: الرقم العشري 2026 وخاناته مضروبة في 1000 و100 و10 و1، والرقم الثنائي 1011 وخاناته مضروبة في 8 و4 و2 و1 فيساوي 11، والرقم الست عشري 7EA وخاناته مضروبة في 256 و16 و1 فيساوي 2026
كل خانة تضرب في الأساس مرفوعاً لرقم مكانها، ثم نجمع النواتج

وفي الكود تميز أغلب اللغات الأنظمة الثلاثة ببادئة Prefix قبل الرقم: 0b للثنائي و0x للست عشري، و0o للثماني في Python وJavaScript، بينما C# لا تدعم الثماني في الكود أصلاً، وفي C وJava يكفي صفر في البداية حتى يصبح الرقم ثمانياً، وهذا فخ سوف نعود إليه في الأخطاء الشائعة. ولاحظ أيضاً أن C# وJava وPython وJavaScript تسمح بالشرطة السفلية Underscore داخل الرقم لتسهيل القراءة، مثل 0b0111_1110_1010 أو 1_000_000، ولا تؤثر على قيمته.

كيف تحول بين الأنظمة؟

للتحويل من أي نظام إلى العشري استخدم الطريقة التي في الصورة: اضرب كل خانة في قيمة مكانها ثم اجمع. أما من العشري إلى الثنائي فالطريقة المعروفة هي القسمة المتكررة على 2، حيث تحتفظ بالباقي في كل مرة ثم تقرأ البواقي من الأسفل إلى الأعلى، فالرقم 11 مثلاً: 11 ÷ 2 = 5 والباقي 1، ثم 5 ÷ 2 = 2 والباقي 1، ثم 2 ÷ 2 = 1 والباقي 0، ثم 1 ÷ 2 = 0 والباقي 1، فيكون الناتج 1011. وهناك طريقة أسرع في الرأس للأرقام الصغيرة، وهي أن تبحث عن أكبر قوة للعدد 2 تدخل في الرقم ثم تطرحها وتكرر، فالرقم 11 فيه 8 ويبقى 3، وفي 3 يوجد 2 ويبقى 1، فالخانات 8 و2 و1 هي الخانات التي فيها 1.

والجميل في الثماني والست عشري أنك لا تحتاج أي حساب للتحويل بينهما وبين الثنائي، فلأن 16 = 24 فكل خانة Hex هي 4 بتات بالضبط، ولأن 8 = 23 فكل خانة ثمانية هي 3 بتات، وبالتالي يكفي أن تقسم البتات من اليمين إلى مجموعات. والصورة التالية تبين ذلك على الرقم 2026:

الرقم 2026 بالثنائي في 12 خانة 011111101010، فوقها ثلاث مجموعات من أربع بتات تعطي 7 وE وA، وتحتها أربع مجموعات من ثلاث بتات تعطي 3 و7 و5 و2، وعلى اليمين النتائج 0x7EA و2026 و0o3752
أربع بتات لكل خانة Hex، وثلاث بتات لكل خانة ثمانية

ولهذا السبب بالذات يستخدم المبرمجون الـ Hex بدلاً من الثنائي عند عرض البايتات، فالبايت 11111111 يصبح FF، والرقم 32 بت يصبح 8 خانات بدلاً من 32، ومع قليل من التعود سوف ترى البتات من خلال الـ Hex مباشرة. أما كيف تطبع الملفات والبايتات بالـ Hex في سطر الأوامر وما الفرق بينه وبين Base64 فقد شرحناه في مقال الترميز، فلا نكرره هنا.

البت والبايت ووحدات الحجم

البت Bit هو خانة ثنائية واحدة، وأربع بتات تسمى Nibble وهي خانة Hex واحدة، وثماني بتات تسمى بايت Byte وهي أصغر وحدة يمكن أن يكون لها عنوان في الذاكرة على الأجهزة التي تستخدمها اليوم. والقاعدة التي يجب أن تحفظها أن n بت تحمل 2n قيمة مختلفة، فالبايت يحمل 256 قيمة من 0 إلى 255، و16 بت تحمل 65,536 قيمة، و32 بت تحمل أكثر من 4 مليارات قيمة بقليل، و64 بت تحمل رقماً من 20 خانة عشرية. ومن هنا تأتي أحجام الأنواع التي تستخدمها كل يوم، فنوع int في C# وJava هو 4 بايتات، وlong هو 8 بايتات، وكذلك المفاتيح في التشفير، فمفتاح AES-256 هو 256 بت أي 32 بايتاً، وعدد المفاتيح الممكنة 2256، كما في مقال التشفير للمطورين.

وهنا يأتي خلط قديم في وحدات الحجم، فالبادئة كيلو Kilo في النظام الدولي SI تعني 1000 بالضبط، ولكن مطوري الحاسب استخدموها لعقود بمعنى 1024، لأن 1024 = 210 قريبة من 1000 وتناسب الذاكرة التي تبنى بقوى العدد 2. ولحل هذا الخلط اعتمدت اللجنة الكهروتقنية الدولية IEC في ديسمبر 1998 بادئات خاصة بالقوى الثنائية، كما يشرح المعهد الوطني الأمريكي للمعايير والتقنية NIST:

بالنظام الدولي SIالقيمةبالبادئات الثنائية IECالقيمة
كيلوبايت kB1000 بايتكيبيبايت KiB1024 بايت
ميجابايت MB1,000,000 بايتميبيبايت MiB1,048,576 بايت
جيجابايت GB109 بايتجيبيبايت GiB1,073,741,824 بايت
تيرابايت TB1012 بايتتيبيبايت TiB240 بايت

وهذا يفسر القرص «الناقص»، فإذا كانت سعة القرص 500 GB بالنظام الدولي أي 500 × 109 بايت، ثم عرضها النظام بوحدة 230 مع أنه يكتبها GB، فسوف ترى 500 × 109 ÷ 1,073,741,824 ≈ 465.66، والقرص لم ينقص منه أي بايت، وإنما اختلفت الوحدة. وكلما كبر الحجم زاد الفرق، فهو 2.4% في الكيلو ونحو 7.4% في الجيجا ونحو 10% في التيرا.

وأوضح مكان ترى فيه هذا الفرق في عمل السيرفرات هو حدود الذاكرة في Kubernetes، فالتوثيق الرسمي لإدارة موارد ال Containers يقبل اللاحقة M بمعنى ميجابايت وMi بمعنى ميبيبايت، ويعطي أمثلة متقاربة القيمة مثل 129M و123Mi، ويحذر من أن m الصغيرة تعني جزءاً من ألف من البايت، فمن يكتب 400m يطلب 0.4 بايت وهو يقصد غالباً 400Mi. فإذا كتبت حدود الذاكرة في أي ملف إعداد فاقرأ توثيق الأداة لتعرف أي الوحدتين تقصد.

الأرقام السالبة: من الطريقة البديهية إلى المتمم الثنائي

الأرقام بدون إشارة Unsigned سهلة، فالبتات الثماني تمثل الأرقام من 0 إلى 255 كما رأينا، ولكن كيف نمثل -5 والحاسب لا يملك إلا صفراً وواحداً؟ لنبدأ بالطريقة البديهية كما يفكر فيها أي شخص لأول مرة، ثم نرى لماذا تركها مصممو المعالجات.

الطريقة البديهية: بت للإشارة Sign-Magnitude. نخصص البت الأيسر للإشارة، صفر للموجب وواحد للسالب، والبتات الباقية للقيمة، فيكون 5 هو 0000 0101 و-5 هو 1000 0101. وتبدو الفكرة منطقية، ولكن فيها مشكلتان: الأولى أن لديك صفرين، 0000 0000 و1000 0000 أي +0 و-0، فيحتاج كل اختبار مساواة إلى حالة خاصة، والثانية أن الجمع العادي لا يعمل، فإذا جمعت 5 و-5 بالطريقة التي يجمع بها المعالج البتات فسوف تحصل على 1000 1010 أي -10 بدلاً من صفر، وبالتالي يحتاج المعالج دوائر مختلفة للجمع والطرح بحسب الإشارات. وهذا غير مناسب إطلاقاً لدائرة يجب أن تكون بسيطة وسريعة.

الطريقة التي تستخدمها كل المعالجات اليوم: المتمم الثنائي Two's Complement. الفكرة أن نعطي البت الأيسر قيمة سالبة بدلاً من أن نجعله علامة، ففي 8 بتات يكون وزن البت الأيسر -128 بدلاً من +128، وبقية البتات كما هي. وبالتالي يكون 1111 1111 هو -128 + 127 = -1، و1000 0000 هو -128 وحده، ويوجد صفر واحد فقط. وللحصول على سالب أي رقم هناك قاعدة سهلة: اقلب كل البتات ثم أضف 1. والأهم من ذلك أن الجمع العادي نفسه يعمل على الموجب والسالب دون أي حالة خاصة، فالمعالج لا يحتاج أن يعرف إن كان الرقم سالباً أصلاً. والصورة التالية تبين ذلك على أرقام من 4 بتات، حيث تظهر الأرقام الـ 16 كعجلة، وكل إضافة 1 تعني خطوة واحدة مع عقارب الساعة:

عجلة من 16 خانة لكل أنماط البتات الأربع من 0000 إلى 1111، النصف الأيمن بالأخضر للأرقام من 0 إلى 7 والنصف الأيسر بالبرتقالي للأرقام من -8 إلى -1، وسهم أحمر من 7 إلى -8 مكتوب عليه الفيضان، وعلى اليمين خطوات قلب البتات وإضافة 1 لتحويل 5 إلى -5، وجدول بمدى الأرقام في 8 و16 و32 و64 بت
في المتمم الثنائي يعمل الجمع العادي على كل الأرقام، وبعد أكبر رقم موجب يأتي أصغر رقم سالب مباشرة

في الصورة أعلاه لاحظ التالي:

  • المدى غير متماثل، فأربع بتات تحمل من -8 إلى 7، و32 بت تحمل من -2,147,483,648 إلى 2,147,483,647، والسبب أن الصفر أخذ مكاناً من نصف الأرقام الموجبة.
  • البت الأيسر يخبرك بالإشارة دائماً، فكل رقم يبدأ بـ 1 سالب، ولكنه ليس علامة منفصلة وإنما جزء من القيمة.
  • عندما تضيف 1 إلى 7 تنتقل في العجلة إلى -8، وهذا بالضبط ما يحدث في الكود عندما يتجاوز الرقم حجمه، وهو موضوع القسم التالي.

وقد يتساءل البعض: لماذا لا نستخدم الأنواع بدون إشارة ونرتاح من كل هذا؟ والإجابة أن الأنواع بدون إشارة مثل uint وulong في C# موجودة، ولكنها تنقل المشكلة إلى الطرف الآخر من العجلة، فطرح 1 من الصفر في uint يعطي 4,294,967,295، وهذا أسوأ غالباً من رقم سالب لأنه يبدو رقماً صحيحاً. وJava مثلاً لا تملك أنواعاً صحيحة بدون إشارة إلا char، كما في مواصفة اللغة. لذلك استخدم الأنواع بدون إشارة عندما يكون المعنى ثنائياً بطبيعته، مثل البايتات والأعلام والهاش، وليس لمجرد أن الرقم «لن يكون سالباً».

الفيضان Overflow: عندما يتجاوز الرقم حجمه

كل نوع صحيح له حجم ثابت، فماذا يحدث إذا كان ناتج العملية أكبر من هذا الحجم؟ الإجابة تختلف من لغة إلى أخرى، وهذا الاختلاف بالذات مصدر أخطاء كثيرة عندما ينتقل الكود أو البيانات بين نظامين:

  • C#: العمليات الصحيحة افتراضياً بدون فحص Unchecked، فإذا تجاوز الناتج الحجم تؤخذ البتات المنخفضة فقط ويلتف الرقم بصمت، كما في توثيق العمليات الحسابية. ويمكنك أن تطلب الفحص بالكلمة checked على تعبير أو كتلة، أو للمشروع كله بالخاصية CheckForOverflowUnderflow، فترمي العملية OverflowException.
  • Java: مواصفة اللغة تقول صراحة في القسم 15.18.2 إن الجمع الصحيح إذا فاض يعطي البتات المنخفضة من الناتج بالمتمم الثنائي، ولا ترمي أي استثناء، وإذا أردت أن ترمي استثناء فاستخدم الدوال Math.addExact وأخواتها في الكلاس Math.
  • Python: النوع int بدقة غير محدودة Unlimited Precision كما في التوثيق، فالرقم يكبر في الذاكرة كلما احتاج، ولا يوجد فيضان في الحساب العادي. ولكن المشكلة تظهر عندما تخرج القيمة من Python إلى مكان له حجم ثابت، مثل عمود integer في قاعدة البيانات أو ملف ثنائي تكتبه بالوحدة struct أو مصفوفة NumPy.
  • JavaScript: النوع Number ليس عدداً صحيحاً أصلاً، وإنما رقم عشري بحجم 64 بت حسب IEEE 754 كما في مواصفة ECMAScript، وبالتالي فالأرقام الصحيحة دقيقة فقط حتى Number.MAX_SAFE_INTEGER أي 253 - 1، وبعده لا يفيض الرقم وإنما يفقد دقته بصمت، فيصبح 2**53 + 1 مساوياً لـ 2**53. وللأرقام الأكبر يوجد النوع BigInt. ولاحظ أن العمليات على البتات في JavaScript تحول الرقم إلى 32 بت بإشارة قبل العملية، فيصبح 2147483648 | 0 مساوياً لـ -2147483648.

وفيما يلي مثال Java، ولم نشغله في هذه التجربة لأن الجهاز الذي نفذنا عليه بقية الأمثلة لا يحتوي على JDK، والقيم في التعليقات هي ما تنص عليه المواصفة والتوثيق المرتبطان أعلاه، أما أمثلة C# وPython وJavaScript فقد شغلناها فعلاً وتجد مخرجاتها في قسم التجربة:

public class Overflow {
    public static void main(String[] args) {
        int max = Integer.MAX_VALUE;               // 2147483647
        int wrapped = max + 1;                      // JLS 15.18.2: wraps to -2147483648, no exception
        long wide = (long) max + 1;                 // 2147483648: widen before adding
        System.out.println(wrapped + " " + wide);
        int safe = Math.addExact(max, 1);           // throws ArithmeticException (Math.addExact docs)
    }
}

ومشكلة الفيضان ليست نظرية، فأشهر مثال عليها مشكلة سنة 2038، حيث يحفظ الوقت كعدد ثوان في 32 بت بإشارة، فيلتف بعد 2,147,483,647 ثانية من 1970 إلى رقم سالب يعود إلى سنة 1901، وقد شرحناها في مقال التاريخ والوقت. ونفس الشيء يحدث مع المعرف المتسلسل في عمود integer بقاعدة PostgreSQL، فحده الأعلى 2147483647، وبعده يفشل كل إدخال جديد، ولهذا يفضل bigint لمفاتيح الجداول التي تكبر، كما ناقشنا في مقال المعرفات الفريدة.

✈️
في مايو 2015 أصدرت هيئة الطيران الفيدرالية الأمريكية FAA توجيهاً إلزامياً AD 2015-09-07 لكل طائرات Boeing 787، لأن الطائرة إذا بقيت تعمل بالكهرباء 248 يوماً متواصلة فقد تفقد كل مصادر التيار المتردد AC، والسبب كما في نص التوجيه «عداد برمجي داخل وحدات التحكم في المولدات GCU يفيض بعد 248 يوماً من التشغيل المتواصل»، فتدخل الوحدات كلها في وضع الأمان في الوقت نفسه. والحل المؤقت الذي فرضه التوجيه كان إطفاء الكهرباء في الطائرة دورياً حتى يعود العداد إلى الصفر. والتوجيه لا يذكر حجم العداد، ولكن لاحظ أن 231 جزءاً من مئة من الثانية تساوي 248.55 يوماً بالضبط، وهي القيمة نفسها التي يلتف عندها عداد صحيح من 32 بت بإشارة يزيد مئة مرة في الثانية. والدرس أن أي عداد يزيد مع الوقت يجب أن تحسب متى يمتلئ، قبل أن يحسبه لك الواقع.

الأرقام العشرية Floating Point: لماذا 0.1 + 0.2 لا تساوي 0.3؟

الأرقام الصحيحة سهلة نسبياً، ولكن كيف نخزن 0.1 أو 3.14 أو رقماً بحجم 10300 في 64 بت فقط؟ الحل الذي تستخدمه كل اللغات والمعالجات تقريباً هو الفاصلة العائمة Floating Point حسب المعيار IEEE 754، وفكرته تشبه الصيغة العلمية التي تكتب بها 6.02 × 1023، ولكن بالأساس 2. فالرقم يقسم إلى ثلاثة أجزاء:

  • الإشارة Sign: بت واحد، صفر للموجب وواحد للسالب.
  • الأس Exponent: 11 بت في double و8 بتات في float، وهو يحدد «مكان الفاصلة»، أي إلى أي قوة للعدد 2 نضرب الرقم، ويخزن بعد إضافة قيمة ثابتة Bias حتى لا يحتاج إشارة.
  • الكسر Fraction أو Mantissa: 52 بت في double و23 بتاً في float، وهو الأرقام المهمة نفسها، أي دقة الرقم.

والمشكلة أن 0.1 بالثنائي كسر لا ينتهي، تماماً كما أن الثلث بالعشري 0.3333 ولا ينتهي، فهو بالثنائي 0.000110011001100110011 والنمط 0011 يتكرر إلى ما لا نهاية، ولأن الكسر له 52 بتاً فقط فيقطع الرقم ويقرب إلى أقرب قيمة يمكن تمثيلها. والصورة التالية تبين البتات الحقيقية للرقم 0.1 كما طبعتها C# وNode.js وPython في التجربة:

64 خانة لبتات الرقم 0.1 من النوع double بالقيمة 0x3FB999999999999A مقسمة إلى بت الإشارة بالبنفسجي و11 بت للأس بالبرتقالي و52 بتاً للكسر بالأزرق، وتحتها 32 خانة للنوع float بالقيمة 0x3DCCCCCD، مع القيمة الحقيقية المخزنة لكل منهما
القيمة المخزنة للرقم 0.1 ليست 0.1 بالضبط، وإنما أقرب رقم إليها يمكن كتابته بهذه البتات

وبالتالي فـ 0.1 في الذاكرة هي في الحقيقة 0.1000000000000000055511151231257827، و0.2 أكبر قليلاً من 0.2 كذلك، وعندما تجمعهما يتراكم الخطأ ويقرب الناتج إلى رقم غير الرقم الذي يقرب إليه 0.3، فتطبع اللغة 0.30000000000000004. وهذا ليس خطأ في JavaScript ولا في Python، وإنما سلوك المعيار نفسه، والنتيجة نفسها ظهرت في C# وNode.js وPython في تجربتنا، ويشرحه دليل Python الرسمي بالتفصيل. ومن هذا ثلاث قواعد عملية:

  1. لا تقارن رقمين عشريين بـ ==، والسبب أن خطأ تقريب صغيراً يكفي ليفشل الاختبار، وإنما قارن الفرق بينهما بقيمة سماح Tolerance صغيرة تناسب مسألتك، مثل Math.Abs(a - b) < 1e-9، أو استخدم math.isclose في Python.
  2. الدقة محدودة بعدد الأرقام وليس بحجم الرقم، فـ double يعطي نحو 15 إلى 17 رقماً مهماً، وfloat نحو 6 إلى 9 فقط كما في جدول أنواع C#، ولذلك أعاد 16777217f في التجربة القيمة 16777216، لأن 224 + 1 يحتاج 25 بتاً من الدقة وfloat لا يملك إلا 24.
  3. الأرقام الصحيحة الكبيرة في double تفقد دقتها، وهذا سبب الحد 253 في JavaScript الذي رأيناه، فالكسر 52 بتاً مع بت مخفي يعطي 53 بتاً للأرقام الصحيحة. ولهذا ترسل الأنظمة التي تستخدم معرفات 64 بت، مثل Snowflake، المعرف كنص في JSON، كما شرحنا في مقال المعرفات الفريدة.

ولا يعني هذا أن الفاصلة العائمة سيئة، فهي الأنسب للقياسات والفيزياء والرسوميات والتعلم الآلي، حيث الأرقام تقريبية أصلاً والسرعة أهم من آخر خانة، ولهذا بنينا محرك التعلم في سلسلة نموذج اللغة بلغة C# على مصفوفات float، ورأينا هناك كيف تظهر حدود الدقة نفسها عندما تصبح الفروق صغيرة جداً. ولكن هناك مكاناً واحداً لا تصلح له إطلاقاً، وهو المال.

المبالغ المالية: لا تستخدم double

لنفرض أن لديك متجراً يبيع منتجاً بسعر 0.10 ريال، والعميل اشترى عشر قطع، فإذا جمعت السعر عشر مرات في double فسوف تحصل على 0.9999999999999999 كما في التجربة، وليس 1.0. وقد يبدو الفرق تافهاً، ولكن عندما تجمع ملايين العمليات، أو تقارن المجموع بالمبلغ المدفوع، أو تقرب الناتج إلى هللتين، فسوف يظهر فرق في التقارير لا يستطيع المحاسب تفسيره. والسبب أن المال يحسب بالنظام العشري، و0.10 رقم دقيق بالعشري ولكنه كسر لا ينتهي بالثنائي. والحلول الصحيحة هي:

  • نوع عشري Decimal: يخزن الرقم بالأساس 10، فيمثل 0.1 بالضبط. في C# النوع decimal بحجم 16 بايتاً ودقة 28 إلى 29 رقماً، وفي Java الكلاس BigDecimal، وفي Python الوحدة decimal، وفي قاعدة PostgreSQL النوع numeric الذي ينصح به التوثيق صراحة للمبالغ المالية. وهذه الأنواع أبطأ من double بكثير لأن المعالج لا يحسبها مباشرة، ولكن الدقة في المال أهم.
  • عدد صحيح بأصغر وحدة: تخزن المبلغ بالهللات بدلاً من الريالات، فيصبح 0.10 ريال هو الرقم الصحيح 10، والجمع والمقارنة دقيقة تماماً، وهذا هو الحل الأنسب في JavaScript التي لا تملك نوعاً عشرياً مدمجاً، ومع BigInt إذا كانت المبالغ ضخمة.
⚠️
انتبه إلى الطريقة التي تنشئ بها الرقم العشري، فـ Decimal(0.1) في Python يأخذ قيمة double الخاطئة أصلاً ويحفظها بكل خاناتها، فيعطي 0.1000000000000000055511151231257827021181583404541015625، بينما Decimal("0.1") من النص يعطي 0.1 بالضبط، ونفس الشيء مع new BigDecimal(0.1) مقابل new BigDecimal("0.1") في Java. وفي C# اكتب اللاحقة m، أي 0.1m، حتى يكون الرقم decimal من البداية.

والتقريب Rounding فيه فخ آخر، فـ round(2.675, 2) في Python يعطي 2.67 وليس 2.68، وMath.round(1.005 * 100) / 100 في JavaScript يعطي 1 وليس 1.01، والسبب في الحالتين أن 2.675 و1.005 مخزنتان كقيم أصغر قليلاً من الرقم المكتوب، فـ 1.005 * 100 هو في الحقيقة 100.49999999999999 كما طبعته التجربة. لذلك قرب المبالغ بنوع عشري، وحدد طريقة التقريب صراحة بحسب ما يطلبه النظام المحاسبي.

ترتيب البايتات Endianness

الرقم 0x12345678 من النوع int هو 4 بايتات: 12 و34 و56 و78، والذاكرة سلسلة من الخانات كل خانة بايت واحد ولها عنوان، فبأي ترتيب نضع البايتات الأربعة في أربعة عناوين متتالية؟ هناك طريقتان، والصورة التالية تبينهما:

الرقم 0x12345678 كأربعة بايتات ملونة، ثم صفان مع شريحة ذاكرة: في الأول Little-endian توضع البايتات في العناوين من 0x1000 إلى 0x1003 بالترتيب 78 و56 و34 و12، وفي الثاني Big-endian بالترتيب 12 و34 و56 و78
القيمة واحدة، والذي يختلف هو البايت الذي يوضع في العنوان الأصغر
  • Little-endian: البايت الأقل قيمة أولاً، وهذا ما تستخدمه معالجات x86-64، والجهاز الذي شغلنا عليه التجربة أعاد BitConverter.IsLittleEndian بالقيمة True، وطبع بايتات الرقم بالترتيب 78563412.
  • Big-endian: البايت الأكبر قيمة أولاً، كما نكتب الرقم بأيدينا، وهو ترتيب بروتوكولات الشبكة، فمواصفة IP في RFC 791 تنص على أن البايتات ترسل بترتيب قراءتها الطبيعي من اليسار، ولذلك يسمى هذا الترتيب Network Byte Order.

وفي الكود العادي لن تشعر بهذا الترتيب أبداً، لأن المعالج يقرأ الرقم ويكتبه بالترتيب نفسه. ولكنه يظهر فوراً عندما تقرأ أو تكتب بايتات خام: ملف ثنائي مثل صورة أو ملف Excel، أو بروتوكول شبكة، أو بيانات من جهاز آخر. فإذا قرأت 4 بايتات من رأس حزمة شبكة بالترتيب الخاطئ فسوف تحصل على 2018915346 بدلاً من 305419896، ولذلك تملك كل لغة دوال تحدد الترتيب صراحة، مثل BinaryPrimitives.ReadInt32BigEndian في C#، وreadUInt32BE وreadUInt32LE في Buffer في Node.js، والحرفين > و< في صيغ struct في Python. والقاعدة: كلما قرأت أو كتبت بايتات، حدد الترتيب بنفسك ولا تعتمد على ترتيب الجهاز.

العمليات على البتات Bitwise Operations والأعلام Flags

العمليات على البتات تعمل على كل بت في الرقم بشكل مستقل، وهي أسرع العمليات على الإطلاق لأن المعالج ينفذها مباشرة، وتجدها بنفس الرموز تقريباً في C# وJava وJavaScript وPython وC:

  • AND (&): الناتج 1 إذا كان البتان 1، وتستخدم لاختبار بت أو لاستخراج جزء من الرقم بقناع Mask، مثل ip & 0xFFFFFF00 للحصول على عنوان الشبكة من عنوان IP بقناع /24.
  • OR (|): الناتج 1 إذا كان أحد البتين 1، وتستخدم لتشغيل بت دون أن تلمس البقية.
  • XOR (^): الناتج 1 إذا اختلف البتان، وتستخدم لقلب بت، وهي العملية التي يقوم عليها كثير من خوارزميات التشفير.
  • NOT (~): تقلب كل البتات، ومعها AND تطفئ بتاً معيناً.
  • الإزاحة Shift (<< و>>): تحرك البتات يساراً أو يميناً، والإزاحة يساراً بمقدار n تساوي الضرب في 2n، فـ 1 << 10 هو 1024.

وأشهر استخدام لها هو الأعلام Flags، أي أن تحفظ عدة خيارات نعم أو لا في رقم واحد، فكل خيار بت له وزن، والقيمة مجموع الأوزان. وأوضح مثال على ذلك صلاحيات الملفات في Linux، فالصلاحيات تسع بتات: القراءة r بوزن 4 والكتابة w بوزن 2 والتنفيذ x بوزن 1، لثلاث فئات هي صاحب الملف والمجموعة والآخرون، وكل فئة خانة ثمانية واحدة لأنها 3 بتات بالضبط، وهذا هو سبب استخدام الثماني في chmod، كما ترى في الثوابت S_IRUSR وأخواتها في صفحة chmod. والصورة التالية تبين الصلاحية 750 والعمليات الثلاث التي تحتاجها مع أي أعلام:

في الأعلى ملف وبجانبه تسع خانات للصلاحيات rwx لصاحب الملف وr-x للمجموعة وثلاث شرطات للآخرين، وتحت كل خانة البت ووزنه، وتجمع كل فئة في دائرة 7 و5 و0 والنتيجة 0750، وفي الأسفل ثلاث بطاقات: التشغيل بـ OR يعطي 110، والاختبار بـ AND مع بت التنفيذ يعطي 000، والحذف بـ AND مع NOT يعطي 100
صلاحيات Linux أعلام من ثلاث بتات لكل فئة، والعمليات نفسها تعمل مع أي أعلام في الكود

وفي C# يوجد دعم مباشر لهذه الفكرة بالخاصية [Flags] على الـ enum، حيث تعطي كل قيمة قوة للعدد 2، ثم تجمعها بـ | وتختبرها بـ HasFlag أو &، كما سوف ترى في التجربة. أما تفاصيل الصلاحيات نفسها وأخطاؤها الشائعة على السيرفر، مثل chmod 777 وumask والصلاحيات الخاصة، فلها مقال مستقل في المرحلة نفسها هو صلاحيات الملفات في Linux وأخطاؤها.

⚠️
الإزاحة لها حدود تختلف بين اللغات، ففي C# تؤخذ أقل 5 بتات فقط من مقدار الإزاحة في النوع int كما في توثيق عمليات الإزاحة، ولذلك أعاد 1 << 32 في التجربة القيمة 1 وليس صفراً، بينما أعاد 1 << 31 رقماً سالباً لأنه وضع 1 في بت الإشارة. وفي JavaScript تحول العمليات على البتات الرقم إلى 32 بت كما في توثيق MDN، فلا تستخدمها على أرقام أكبر من ذلك إلا مع BigInt.

تجربة عملية: كل ما سبق في C# وNode.js وPython

الآن سوف نجمع كل ما سبق في ثلاثة برامج صغيرة، وشغلناها على جهاز Windows 11 بمعالج x86-64، باستخدام .NET SDK 10.0.400 مع Runtime 10.0.11، وNode.js 22.14.0، وPython 3.13.3. والبرنامج الأول ملف C# واحد يعمل دون مشروع بالأمر dotnet run numbers.cs، وهي ميزة التطبيقات المكونة من ملف واحد File-based Apps في .NET 10:

using System;
using System.Buffers.Binary;

// 1) positional systems: the same number in four bases
int n = 2026;
Console.WriteLine($"{n} = 0b{Convert.ToString(n, 2)} = 0o{Convert.ToString(n, 8)} = 0x{n:X}");
Console.WriteLine($"{Convert.ToInt32("11111101010", 2)} {Convert.ToInt32("7EA", 16)} {0b0111_1110_1010} {0x7EA}");

// 2) two's complement: the bits of small negative numbers
foreach (sbyte v in new sbyte[] { 1, 0, -1, -2, -128, 127 })
    Console.WriteLine($"{v,5} -> {Convert.ToString((byte)v, 2).PadLeft(8, '0')}");

// 3) overflow: silent wrap by default, exception with checked
int max = int.MaxValue;
int wrapped = unchecked(max + 1);
Console.WriteLine($"int.MaxValue = {max}, +1 = {wrapped}");
try { int boom = checked(max + 1); }
catch (OverflowException e) { Console.WriteLine($"checked: {e.GetType().Name}: {e.Message}"); }
Console.WriteLine($"long: {(long)max + 1}");

// 4) floating point
double a = 0.1 + 0.2;
Console.WriteLine($"0.1 + 0.2 = {a:R}  equal to 0.3? {a == 0.3}");
Console.WriteLine($"bits of 0.1 = 0x{BitConverter.DoubleToInt64Bits(0.1):X16}");
Console.WriteLine($"float 16777217f = {16777217f:R}");
decimal d = 0.1m + 0.2m;
Console.WriteLine($"decimal 0.1 + 0.2 = {d}  equal to 0.3? {d == 0.3m}");
double total = 0; for (int i = 0; i < 10; i++) total += 0.1;
decimal totalM = 0; for (int i = 0; i < 10; i++) totalM += 0.1m;
Console.WriteLine($"ten times 0.1: double {total:R}, decimal {totalM}");

// 5) endianness
Console.WriteLine($"BitConverter.IsLittleEndian = {BitConverter.IsLittleEndian}");
Console.WriteLine($"0x12345678 in memory: {Convert.ToHexString(BitConverter.GetBytes(0x12345678))}");
var buf = new byte[4]; BinaryPrimitives.WriteInt32BigEndian(buf, 0x12345678);
Console.WriteLine($"big-endian (network order): {Convert.ToHexString(buf)}");

// 6) bitwise ops and flags
var p = Perm.Read | Perm.Write;
Console.WriteLine($"{p} = {(int)p}, has Write? {p.HasFlag(Perm.Write)}, has Execute? {(p & Perm.Execute) != 0}");
p &= ~Perm.Write; Console.WriteLine($"after removing Write: {p}");
Console.WriteLine($"0750 octal = {Convert.ToInt32("750", 8)} = 0b{Convert.ToString(Convert.ToInt32("750", 8), 2)}");
Console.WriteLine($"1 << 10 = {1 << 10}, 1 << 31 = {1 << 31}, 1 << 32 = {1 << 32}");

[Flags] enum Perm { None = 0, Execute = 1, Write = 2, Read = 4 }

والمخرج سوف يكون كما يلي:

2026 = 0b11111101010 = 0o3752 = 0x7EA
2026 2026 2026 2026
    1 -> 00000001
    0 -> 00000000
   -1 -> 11111111
   -2 -> 11111110
 -128 -> 10000000
  127 -> 01111111
int.MaxValue = 2147483647, +1 = -2147483648
checked: OverflowException: Arithmetic operation resulted in an overflow.
long: 2147483648
0.1 + 0.2 = 0.30000000000000004  equal to 0.3? False
bits of 0.1 = 0x3FB999999999999A
float 16777217f = 16777216
decimal 0.1 + 0.2 = 0.3  equal to 0.3? True
ten times 0.1: double 0.9999999999999999, decimal 1.0
BitConverter.IsLittleEndian = True
0x12345678 in memory: 78563412
big-endian (network order): 12345678
Write, Read = 6, has Write? True, has Execute? False
after removing Write: Read
0750 octal = 488 = 0b111101000
1 << 10 = 1024, 1 << 31 = -2147483648, 1 << 32 = 1

في المخرج أعلاه لاحظ التالي:

  • السطر الأول هو الرقم نفسه بأربعة أنظمة، والسطر الثاني يحول النصوص "11111101010" و"7EA" والقيم المكتوبة بالبادئات 0b و0x إلى الرقم 2026 نفسه.
  • -1 كل بتاته 1، و-128 هو بت الإشارة وحده، و127 كل البتات ما عدا بت الإشارة، كما في العجلة.
  • int.MaxValue + 1 أعطى -2147483648 بصمت، ومع checked رمى OverflowException، والحل في هذه الحالة أن تحول إلى long قبل الجمع وليس بعده.
  • 0.1 + 0.2 لم يساو 0.3، وبتات 0.1 هي 3FB999999999999A كما في الصورة، وجمع 0.1 عشر مرات أعطى 0.9999999999999999 في double و1.0 في decimal.
  • الجهاز Little-endian، فطبع 0x12345678 بالترتيب 78563412، وبعد WriteInt32BigEndian أصبح بترتيب الشبكة 12345678.
  • الأعلام Read | Write تساوي 6، والصلاحية 750 الثمانية تساوي 488 بالعشري و111101000 بالثنائي، أي تسع بتات هي rwxr-x---.

والبرنامج الثاني في Node.js، ويركز على ما يختلف في JavaScript:

console.log((2026).toString(2), (2026).toString(16), parseInt('7ea', 16), 0b11111101010, 0o3752);
console.log('0.1 + 0.2 =', 0.1 + 0.2, '=== 0.3 ?', 0.1 + 0.2 === 0.3);
console.log('MAX_SAFE_INTEGER =', Number.MAX_SAFE_INTEGER);
console.log('2**53 + 1 =', 2 ** 53 + 1, ' same as 2**53 ?', 2 ** 53 + 1 === 2 ** 53);
console.log('BigInt: 2n**53n + 1n =', 2n ** 53n + 1n);
console.log('JSON.parse id:', JSON.parse('{"id": 9007199254740993}').id);
console.log('2147483647 | 0 =', 2147483647 | 0, ' 2147483648 | 0 =', 2147483648 | 0, ' 1 << 31 =', 1 << 31);
console.log('-1 >>> 0 =', -1 >>> 0, ' (-1 >>> 0).toString(2) =', (-1 >>> 0).toString(2));
const b = Buffer.alloc(4); b.writeUInt32LE(0x12345678); console.log('LE', b.toString('hex'));
b.writeUInt32BE(0x12345678); console.log('BE', b.toString('hex'));
const f = Buffer.alloc(8); f.writeDoubleBE(0.1); console.log('0.1 as IEEE 754 double (BE hex):', f.toString('hex'));
console.log('(0.1).toFixed(20) =', (0.1).toFixed(20));
console.log('Math.round(1.005 * 100) / 100 =', Math.round(1.005 * 100) / 100, ' 1.005 * 100 =', 1.005 * 100);

والمخرج سوف يكون كما يلي:

11111101010 7ea 2026 2026 2026
0.1 + 0.2 = 0.30000000000000004 === 0.3 ? false
MAX_SAFE_INTEGER = 9007199254740991
2**53 + 1 = 9007199254740992  same as 2**53 ? true
BigInt: 2n**53n + 1n = 9007199254740993n
JSON.parse id: 9007199254740992
2147483647 | 0 = 2147483647  2147483648 | 0 = -2147483648  1 << 31 = -2147483648
-1 >>> 0 = 4294967295  (-1 >>> 0).toString(2) = 11111111111111111111111111111111
LE 78563412
BE 12345678
0.1 as IEEE 754 double (BE hex): 3fb999999999999a
(0.1).toFixed(20) = 0.10000000000000000555
Math.round(1.005 * 100) / 100 = 1  1.005 * 100 = 100.49999999999999

في المخرج أعلاه لاحظ التالي:

  • 2**53 + 1 يساوي 2**53، بينما أعطى BigInt الرقم الصحيح 9007199254740993n.
  • JSON.parse حول المعرف 9007199254740993 إلى 9007199254740992 دون أي خطأ، وهذا ما يحدث لمعرفات 64 بت في المتصفح إذا أرسلها السيرفر كرقم وليس كنص.
  • 2147483648 | 0 أصبح سالباً لأن العملية تحول الرقم إلى 32 بت بإشارة، و-1 >>> 0 هو الطريقة المعروفة لرؤية الرقم كـ 32 بت بدون إشارة، أي 32 واحداً.
  • writeDoubleBE(0.1) أعطى البايتات 3fb999999999999a نفسها التي طبعتها C#، لأن المعيار واحد، وtoFixed(20) كشف أن 0.1 هي في الحقيقة 0.10000000000000000555.

والبرنامج الثالث في Python، واسمه bits.py، ولا تسمه numbers.py كما فعلنا في المحاولة الأولى، لأن Python وجدت ملفنا قبل الوحدة القياسية numbers التي تحتاجها decimal، ففشل السطر from decimal import Decimal بالخطأ ImportError: cannot import name 'Decimal' from partially initialized module 'decimal'، وهذا خطأ شائع مع أي ملف يحمل اسم وحدة قياسية:

import struct, sys
from decimal import Decimal
n = 2026
print(bin(n), oct(n), hex(n), int("11111101010", 2), int("7ea", 16))
print("2**64 =", 2**64, " 2**100 =", 2**100)
print("int.bit_length of 2**100:", (2**100).bit_length())
print("-1 as 8 bits:", format(-1 & 0xFF, "08b"), " -2:", format(-2 & 0xFF, "08b"), " -128:", format(-128 & 0xFF, "08b"))
print("(-2).to_bytes(2, 'big', signed=True) =", (-2).to_bytes(2, "big", signed=True).hex())
print("struct int32 wrap:", struct.unpack("<i", struct.pack("<I", 2**31))[0])
try:
    struct.pack("<i", 2**31)
except struct.error as e:
    print("struct.error:", e)
print("0.1 + 0.2 =", 0.1 + 0.2, " == 0.3 ?", 0.1 + 0.2 == 0.3)
print("Decimal(0.1) =", Decimal(0.1))
print("Decimal('0.1') + Decimal('0.2') =", Decimal("0.1") + Decimal("0.2"))
print("(0.1).hex() =", (0.1).hex(), " struct >d:", struct.pack(">d", 0.1).hex())
print("round(2.675, 2) =", round(2.675, 2))
print("sys.float_info.epsilon =", sys.float_info.epsilon, " max =", sys.float_info.max)
print("sys.byteorder =", sys.byteorder)
print("0o750 =", 0o750, bin(0o750), " 0o750 & 0o100 =", oct(0o750 & 0o100), " 0o640 & 0o111 =", oct(0o640 & 0o111))

والمخرج سوف يكون كما يلي:

0b11111101010 0o3752 0x7ea 2026 2026
2**64 = 18446744073709551616  2**100 = 1267650600228229401496703205376
int.bit_length of 2**100: 101
-1 as 8 bits: 11111111  -2: 11111110  -128: 10000000
(-2).to_bytes(2, 'big', signed=True) = fffe
struct int32 wrap: -2147483648
struct.error: 'i' format requires -2147483648 <= number <= 2147483647
0.1 + 0.2 = 0.30000000000000004  == 0.3 ? False
Decimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
Decimal('0.1') + Decimal('0.2') = 0.3
(0.1).hex() = 0x1.999999999999ap-4  struct >d: 3fb999999999999a
round(2.675, 2) = 2.67
sys.float_info.epsilon = 2.220446049250313e-16  max = 1.7976931348623157e+308
sys.byteorder = little
0o750 = 488 0b111101000  0o750 & 0o100 = 0o100  0o640 & 0o111 = 0o0

في المخرج أعلاه لاحظ التالي:

  • 2**100 طبع كاملاً بـ 31 خانة دون فيضان، لأن int في Python يكبر كما يحتاج، ويحتاج 101 بت كما أعادت bit_length.
  • لرؤية بتات رقم سالب في Python نستخدم القناع & 0xFF، فنحصل على المتمم الثنائي في 8 بتات نفسه الذي طبعته C#، وto_bytes مع signed=True أعطت fffe للرقم -2 في بايتين.
  • الفيضان ظهر فقط عندما خرج الرقم من Python إلى حجم ثابت، فـ struct.pack("<i", 2**31) رمى struct.error لأن i تعني 32 بت بإشارة.
  • (0.1).hex() هي الطريقة التي تكتب بها Python الفاصلة العائمة بالست عشري: الكسر 1.999999999999a ثم p-4 أي مضروباً في 2-4، وهي القيمة نفسها في الصورة.
  • sys.float_info.epsilon هي المسافة بين 1.0 وأقرب double بعده، أي 2-52، وهي مقياس جيد لحجم خطأ التقريب حول الرقم 1.

أخطاء شائعة

  1. الصفر في بداية الرقم: في C وJava يصبح الرقم 0750 ثمانياً قيمته 488، وفي JavaScript بالوضع العادي يحدث نفس الشيء، فإذا كتبت رمز منطقة أو رقم هاتف يبدأ بصفر كرقم في الكود فسوف تحصل على قيمة مختلفة تماماً. احفظ هذه القيم كنصوص، واكتب 0o صراحة عندما تقصد الثماني.
  2. الجمع ثم التحويل: long total = a * b; حيث a وb من النوع int يفيض قبل التحويل إلى long، لأن الضرب يتم بحجم int. حول أحد الطرفين أولاً: (long)a * b.
  3. double للمال: وقد رأينا لماذا، فاستخدم decimal أو BigDecimal أو numeric، أو عدداً صحيحاً بالهللات.
  4. المقارنة بـ == للأرقام العشرية: قارن بقيمة سماح، ولا تستخدم الرقم العشري كمفتاح في Dictionary أو كشرط لنهاية حلقة.
  5. معرفات 64 بت كأرقام في JSON: ترسلها كنص إذا كان الطرف الآخر JavaScript، وإلا ضاعت آخر خاناتها بصمت.
  6. KB أو MB دون تحديد: في حدود الذاكرة والأقراص والتقارير، اكتب Mi أو Gi عندما تقصد القوى الثنائية، واقرأ توثيق الأداة قبل أن تفترض.
  7. ترتيب البايتات الافتراضي: عند قراءة ملفات ثنائية أو بروتوكولات، حدد Big-endian أو Little-endian في كل قراءة وكتابة.
  8. عداد لا ينتهي: أي عداد يزيد مع الوقت أو مع كل طلب، احسب متى يمتلئ بحجمه الحالي، واختر long أو bigint إذا كانت الإجابة سنوات قليلة.

تدريب عملي

  • حول بيدك الرقم 255 إلى الثنائي والست عشري والثماني، ثم تأكد بـ Convert.ToString(255, 2) في C# أو bin(255) في Python، وفسر لماذا يظهر الرقم FF كثيراً في عناوين الشبكة والألوان.
  • احسب كم يوماً يحتاج عداد من النوع int يزيد مرة في كل ميلي ثانية حتى يفيض، ثم كم سنة يحتاج إذا كان من النوع long.
  • اكتب دالة تجمع سلة مشتريات بأسعار مثل 19.99 و0.10، مرة بـ double ومرة بنوع عشري أو بالهللات، وقارن الناتجين بعد ألف عملية.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • كل الأنظمة العددية موضعية، والقيمة مجموع الخانات مضروبة في قوى الأساس، والـ Hex والثماني مجرد طريقة مختصرة لكتابة البتات، 4 بتات و3 بتات لكل خانة.
  • n بت تحمل 2n قيمة، وKB بالنظام الدولي 1000 بايت بينما KiB هي 1024، وهذا الفرق هو سبب القرص «الناقص» واختلاف M وMi.
  • المعالجات تمثل الأرقام السالبة بالمتمم الثنائي، حيث يعمل الجمع العادي على كل الأرقام، وبعد أكبر رقم موجب يأتي أصغر رقم سالب مباشرة.
  • الفيضان صامت في C# وJava افتراضياً، وغير موجود في int بلغة Python، ويظهر في JavaScript كفقدان للدقة بعد 253، فاعرف سلوك لغتك واستخدم checked أو Math.addExact أو BigInt عند الحاجة.
  • الفاصلة العائمة تقريبية بطبيعتها لأن أغلب الكسور العشرية لا تنتهي بالثنائي، فلا تقارنها بـ == ولا تستخدمها للمال.
  • Endianness لا يهمك حتى تقرأ بايتات خام، وعندها حدد الترتيب صراحة، والعمليات على البتات هي الطريقة الأسرع والأوضح لحفظ الأعلام والصلاحيات.

وفي مرحلة أساسيات علوم الحاسب نفسها سوف نتحدث بإذن الله عن هياكل البيانات Data Structures والخوارزميات وتعقيدها Big-O والتعبيرات النمطية Regex، والمقال التالي مباشرة في هذا الموضوع هو مقال الذاكرة، حيث نرى أين تعيش هذه الأرقام عندما يعمل برنامجك.

دليلالذاكرة للمبرمجين: كيف تعمل ال Stack وال Heapلماذا تتغير قائمة لم تطلب تغييرها، وكيف تتسرب الذاكرة في لغة فيها جامع قمامة Garbage Collector، بأمثلة حقيقية من عدة لغات.دليلالترميز Encoding للمطورين: Base64 وأخواتهلماذا نحول البايتات إلى حروف حتى تمر في البريد والروابط، وكيف تفك أي سلسلة منها، ولماذا لا يحمي Base64 أي سر.دليلماذا يحدث عندما تشغل برنامج Hello World؟نتتبع برنامج C صغيراً من المحرر إلى الشاشة، فترى كل طبقة يمر بها في الجهاز حتى تظهر الجملة أمامك.دليلالتاريخ والوقت للمطورين ومديري الأنظمةلماذا يتأخر التقرير ثلاث ساعات ولا تعمل المهمة المجدولة ليلة تغيير التوقيت، وكيف تضبط الوقت في السيرفر والكود.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتنفيذ الأمثلة على Windows 11 (x86-64) مع .NET SDK 10.0.400 (Runtime 10.0.11) وNode.js 22.14.0 وPython 3.13.3، ومثال Java من مواصفة اللغة دون تشغيل.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.