المحتوى عن 'تعلم سي شارب'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • نصائح وإرشادات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • مقالات عامة
  • التجارة الإلكترونية

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • Node.js
    • jQuery
    • AngularJS
    • Cordova
  • HTML5
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • سي شارب #C
    • منصة Xamarin
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة Swift
  • لغة R
  • سير العمل
    • Git
  • صناعة الألعاب
    • Unity3D
  • مقالات عامّة

التصنيفات

  • تجربة المستخدم
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
    • كوريل درو
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • مقالات عامّة

التصنيفات

  • خواديم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • مقالات عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 18 نتائج

  1. يمكنك تنفيذ البرامج الموجودة ضمن هذه السلسلة (كما وسبق أن ذكرنا في المقدّمة) بطريقتين مختلفتين: الأولى هي تحميل وتنصيب بيئة التطوير المجّانيّة Visual Studio 2015 Community من مايكروسوفت. توفّر هذه البيئة خدمات عظيمة للمطوّر وتسهّل عمليّة كتابة البرامج إلى حدّ كبير. ويمكنك الاستفادة من مزايا تنقيح الأخطاء debugging المتقدّمة التي يوفّرها المنقّح debugger المرفق ضمن هذه البيئة. يمكنك تحميل نسختك المجّانيّة من هنا. الطريقة الثانية هي في استخدام الموقع NET Fiddle. الذي يوفّر مزيّة تنفيذ البرامج التي تكتبها على خادم خاص به، ومن ثمّ يعرض لك خرج البرنامج، بدون أن تمتلك نظام تشغيل ويندوز حتى. البرنامج الأول سنبدأ بمثال عمليّ لنسبر سريعًا أغوار هذه اللغة. شغّل برنامج Visual Studio 2015 Community ثم من القائمة File اختر الأمر New > Project. من نافذة مشروع جديد New Project، اختر من القسم الأيسر #Visual C ومن القسم الأيمن اختر Console Application. اكتب HelloWorld ضمن حقل الاسم Name في القسم السفلي، ثم اضغط زر OK. انظر الشكل التوضيحي التالي: قد تبدو الصورة مختلفة بعض الشيء لديك بحسب إعدادات الإظهار التي اخترتها. سيعمل Visual Studio على إنشاء هذا التطبيق وفتح ملف مُجهّز خصيصًا لك اسمه Program.cs. امسح محتويات هذا الملف بالكامل ثم انسخ الشيفرة التالية ضمنه: 1 using System; 2 3 namespace HelloWorld 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Console.WriteLine("Hello World!"); 10 } 11 } 12 } اضغط المفتاحين Ctrl+F5 معًا لتبدأ عملية بناء build البرنامج وتنفيذه لتحصل على العبارة Hello World! في خرج البرنامج (أو من القائمة Debug > Start Without Debugging). رغم أنّ البرنامج السابق بسيط جدًّا إلًا أنّه يحتوي على الكثير من المفاهيم الجديدة التي سنتناولها تباعًا في هذه السلسلة. يبدأ هذا البرنامج بتعريف نطاق اسم namespace (السطر 3) اسمه HelloWorld وهو نفس الاسم الذي زوّدناه للبرنامج، سنتكلّم عن نطاقات الأسماء في درس لاحق، ولكن يكفيك أن تعرف الآن أنّ نطاقات الأسماء هي وسيلة لتنظيم الأصناف ضمن مجموعات مترابطة منطقيًّا. يأتي بعد ذلك تعريف صنف class جديد اسمه Program (السطر 5). يحتاج أيّ برنامج مكتوب بالسي شارب إلى نقطة دخول entry point لكي يبدأ تنفيذه. نقطة الدخول يجب أن تكون عبارة عن تابع method اسمه Main (السطر 7)، تكون التوابع عادةً ضمن الأصناف، يكفيك الآن أن تفهم التابع على أنّه شبيه بالدّالة function في لغات البرمجة الأخرى. أي هو جزء من الشيفرة يمكن استدعاؤه لتنفيذ ناحية وظيفيّة مُحدّدة في البرنامج وقد يُرجع قيمة ما أو لا يُرجع أيّ شيء. العبارة الموجودة في السطر 9 هي عبارة برمجّية قياسيّة في لغة سي شارب. وظيفة هذه العبارة استدعاء التابع WriteLine من الصنف Console وتمرير النص "!Hello World" له لكي يُظهر النص !Hello World في خرج البرنامج. أيّ عبارة برمجيّة في سي شارب يجب أن تنتهي بفاصلة منقوطة (;)، وقد تكون العبارة البرمجيّة مجرّد استدعاء تابع أو أن تكون عمليّة إسناد إلى متغيّر، أو قد تكون مزيجًا بينهما. إذا كانت لديك معرفة سابقة بلغات برمجة مثل C أو ++C أو Java ستلاحظ أنّ الصيغة النحويّة syntax للغة سي شارب تشبه إلى حدٍّ كبير الصيغة النحويّة لهذه اللّغات. حيث تُستخدم الحاضنات { } مثلًا لتحديد البداية والنهاية للتابع method وللصنف class ولنطاق الاسم namespace. وحتى أنّهما يشكّلان حدود أيّ بنية برمجيّة في لغة سي شارب مثل العبارات التكراريّة. انظر على سبيل المثال إلى السطر 6 لتجد الحاضنة "{" الخاصّة بالصنف Program وإلى السطر 12 لتجد حاضنة الإغلاق "}" له. كما ينبغي التنبّه أيضًا إلى كون لغة سي شارب حسّاسة لحالة الأحرف كما هو الحال في لغات البرمجة C و ++C و Java. ملاحظة يمكن استخدام المفتاح F6 في بيئة Visual Studio (أو من القائمة Build > Build Solution) لبناء البرنامج دون تشغيله (تنفيذه) وذلك اعتبارًا من الشيفرة والحصول على ملف تنفيذي منه له الامتداد exe في حال كان البرنامج لا يحتوي على أيّ خطأ. برنامج بسيط لجمع عددين صحيحين لنعمل الآن على برنامج عمليّ أكثر. سنكتب برنامج يعمل على جمع عددين صحيحين وإظهار النتيجة للمستخدم. اتبع نفس الخطوات التي أجريناها في البرنامج السابق لإنشاء برنامج جديد باسم SumTwoNumbers، انسخ محتويات الشيفرة التالية إلى الملف Program.cs: 1 using System; 2 3 namespace SumTwoNumbers 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int a; 10 int b; 11 int c; 12 13 a = 3; 14 b = 4; 15 16 c = a + b; 17 18 Console.WriteLine("3 plus 4 equals: " + c.ToString()); 19 20 } 21 } 22 } يُقدّم هذا البرنامج البسيط مفهوم التصريح عن المتغيّرات والتعامل معها. صرّحنا في الأسطر من 9 إلى 11 عن ثلاثة متغيّرات a و b و c من النوع int. يجب التصريح في لغة سي شارب عن كل متغيّر قبل استخدامه في البرنامج. لاحظ أنّ التصريح عن متغيّر يتمّ بذكر نوعه ومن ثمّ اسمه. يستطيع المتغيّر من النوع int استيعاب أي عدد صحيح (دون فاصلة عشريّة) يقع بين 2,147,483,648- و 2,147,483,647. لاحظ أنّه قد أسندنا القيمتين 3 و 4 إلى المتغيّرين a و b على الترتيب، وذلك في السطرين 13 و14. نجري عملية الجمع والإسناد إلى المتغيّر c في السطر 16. وفي السطر 18 نعرض رسالة للمستخدم. يمكن إسناد قيمة للمتغيّر مباشرةً عند التصريح عنه. فمن الممكن مثلًا إسناد القيمتين 3 و 4 للمتغيّرين a و b على الترتيب عند التصريح عنهما وذلك بالشكل التالي: int a = 3; int b = 4; كما يمكن استخدام عبارة تصريح واحدة للتصريح عن عدّة متغيّرات بنفس الوقت. فمثلًا كان من الممكن التصريح عن المتغيّرات a و b و c السابقة بعبارة برمجيّة واحدة على الشكل التالي: int a, b, c; في الواقع هناك شكلان من أنواع المتغيّرات تدعمهما منصّة دوت نت. أنواع القيمة value types والأنواع المرجعيّة reference types. سنتحدّث عنهما لاحقًا. يُعتبر النوع int نوع قيمة. يجب إجراء عمليّة إسناد واحدة على الأقل إلى متغيّر قيمة قبل القراءة منه. وإلّا سنحصل على خطأ. جرّب حذف العبارة البرمجيّة الموجودة في السطر 16 والمسؤولة عن إسناد قيمة المجموع إلى المتغيّر c. نفّذ البرنامج وستحصل على الخطأ التالي: Use of unassigned local variable 'c' سبب ذلك أنّنا حاولنا قراءة المتغيّر c في السطر 18 دون أن نُسند أيّ قيمة له. العبارة البرمجيّة الموجودة في السطر 18 مسؤولة عن عرض الرسالة إلى المستخدم كما أسلفنا. ستلاحظ أنّنا مرّرنا التعبير expression التالي إلى التابع WriteLine: "3 plus 4 equals: " + c.ToString() التعبير البرمجي هو مفهوم موجود في جميع لغات البرمجة تقريبًا، وهو ببساطة ناتج عمليّة برمجيّة باستخدام عامل operator واحد أو أكثر، أو استدعاء إلى تابع أو مزيج بينهما. نسمّي عمليّة معالجة التعبير بتقييم التعبير expression evaluation. العامل المُستخدم هنا هو عامل ضمّ النصوص (+)، ولعلّك تستغرب لماذا أدعوه بعامل ضمّ النصوص رغم أنّه يشبه عامل الجمع العادي الذي يجمع عددين مع بعضهما (انظر السطر 16). يعود السبب في ذلك إلى نوع المُعامِلَين operands الموجودين على طرفيه. يمكنك أن تلاحظ بسهولة أنّ المعامل الأيسر هو النص: "3 plus 4 equals: " أمّا المعامل الأيمن فهو: c.ToString() وهو أيضًا نص ويعود سبب ذلك إلى استدعاء التابع ToString من المتغيّر c المعرّف أصلًا أنّه متغيّر من نوع int. ولكنّ استدعاء هذا التابع من المتغيّر c يؤدّي إلى الحصول على التمثيل النصّي للقيمة العددية الموجودة ضمنه أصلًا. فإذا كان المتغيّر c يحمل القيمة العددية 7، فإنّ التابع ToString سيُرجع النص "7"، الذي يعمل عامل الضم + على ضمّه مع النص الذي يسبقه لتكون نتيجة التعبير ككل هي: "3 plus 4 equals: 7" سيُمرّر هذا النص إلى التابع WriteLine لعرضه للمستخدم. أعتقد أنّك قد بدأت بفهم طريقة الوصول إلى التوابع واستدعائها في لغة سي شارب. فنحن نستخدم اسم الصنف (أو الكائن object كما سنرى لاحقًا) الذي يحوي التابع المراد استدعاؤه متبوعًا بنقطة ثم باسم التابع المطلوب، وبعد ذلك قوسين هلاليّين نمرّر بينهما الوسائط التي يطلبها التابع إذا اقتضت الضرورة لذلك. برنامج محسن أكثر لجمع عددين لقد تعلّمنا العديد من المفاهيم الجديدة من خلال البرنامجين السابقين. ولكن لعلّك قد لاحظت من برنامج جمع العددين السابق أنّ البرنامج جامد بعض الشيء. فهو يجمع عدّدين مُحدّدين سلفاً. سنعمل في هذه النسخة المطوّرة من البرنامج على استقبال العددين المراد جمعهما من المستخدم ومن ثمّ إجراء عمليّة الجمع عليهما وعرض النتيجة على المستخدم، مع بعض التحسينات الإضافيّة الأخرى. أنشئ مشروعًا جديدًا باسم EnhancedSumTwoNumbers ثمّ استبدل محتويات الملف Program.cs بالشيفرة التالية: 1 using System; 2 3 namespace EnhancedSumTwoNumbers 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 string str1, str2, result; 10 double num1, num2, sum; 11 12 //prompt user to get input for first value. 13 Console.Write("Input first number: "); 14 str1 = Console.ReadLine(); 15 16 //prompt user to get input for second value. 17 Console.Write("Input second number: "); 18 str2 = Console.ReadLine(); 19 20 //convert the input values to double numbers. 21 num1 = double.Parse(str1); 22 num2 = double.Parse(str2); 23 24 //perform sum operation. 25 sum = num1 + num2; 26 27 /*concatenate strings to form output 28 message which contains the result.*/ 29 result = num1.ToString() + " + " + num2.ToString() + " = " + sum.ToString(); 30 31 Console.WriteLine(result); 32 33 } 34 } 35 } نفّذ البرنامج بضغط المفتاحين Ctrl+F5 معًا. سيطلب منك البرنامج في البداية إدخال قيمة العدد الأوّل. أدخل القيمة المرغوبة ثم اضغط مفتاح الإدخال Enter. بعد ذلك سيطلب منك البرنامج إدخال قيمة العدد الثاني. أدخلها واضغط Enter. سيعرض البرنامج بعد ذلك النتيجة المطلوبة على شكل رسالة مناسبة. تحتوي هذه النسخة من برنامج جمع الأعداد على عدّة تحسينات: أصبح برنامجنا يدعم جمع أعداد تقبل فاصلة عشرية من خلال التصريح عن متغيّرات من النوع double (انظر السطر 10). والنوع double هو النوع الذي يقبل أعدادًا ذات فاصلة عائمة مزدوجة الدقّة. مجال الأعداد التي يقبلها يقع بين ±5.0*10-324 حتى ±1.7*10308. أصبح بإمكان مستخدم البرنامج أن يُدخل الأعداد المراد جمعها مباشرة من لوحة المفاتيح، وذلك من خلال التابع ReadLine من الصنف Console (انظر السطرين 13 و 16) يُوقف هذا التابع تنفيذ البرنامج وينتظر المستخدم أن يُدخل قيمة ما ويضغط مفتاح الإدخال Enter ليتابع البرنامج التنفيذ. أضفنا تعليقات توضيحيّة ضمن البرنامج. هذه التعليقات لا يكترث بها المترجم، ووظيفتها هي جعل الشيفرة البرمجيّة مقروءةً وسهلة الفهم والتعديل لاحقًا. في الحقيقة تُعتبر عمليّة كتابة التعليقات البرمجيّة فنًّا بحد ذاته، وأنصح أن يمارسها كلّ مبرمج بأيّ لغة برمجة كانت. في الواقع ليس مطلوبًا كتابة التعليقات البرمجيّة قبل كلّ عبارة برمجيّة، فعلى المرء أن يكون حكيمًا في استخدامها بالشكل الذي يحافظ فيه على التوازن بين جعل الشيفرة واضحة ومقروءة، وعدم الإفراط في كتابة التعليقات بدون ضرورة. تدعم لغة سي شارب نوعين من التعليقات: التعليقات على مستوى السطر، حيث يتجاهل المترجم compiler كلّ ما يقع على يمين الرمزين//. والتعليقات التي تمتد على عدّة أسطر، حيث يتجاهل المترجم المحتوى الموجود بين الرمزين /* والرمزين */. انظر الأسطر 12 و 16 و 20 و 24 من أجل التعليقات على مستوى السطر، والسطرين 27 و 28 من أجل التعليقات التي تمتدّ على عدّة أسطر. استخدمنا التابع Write بدلًا من التابع WriteLine (انظر السطرين 13 و 17)، والسبب في ذلك هو أنّنا نريد أن يطلب البرنامج من المستخدم إدخال القيمة على نفس السطر الذي تُعرَض فيه الرسالة وليس على سطرٍ منفصل. فالتابع Write يعرض النص المُمرّر إليه ولا يُحدِث سطرًا جديدًا. في حين يسلك التابع WriteLine نفس سلوك التابع Write ولكن ينتقل إلى سطر جديد بعد عرض النص. يمكنك أن تجرّب استبدال التابع WriteLine بالتابع Write لترى الفرق. جعلنا عمليّة تشكيل النص الذي سيُعرض على المستخدم ضمن سطر منفصل (السطر 29) وأسندنا هذا النص إلى المتغيّر result من النوع string. الهدف من هذا الأمر هو جعل الشيفرة نظيفة وواضحة وسهلة للقراءة. النوع string هو من الأنواع المرجعيّة reference types ويُستخدم للتعبير عن النصوص. ولكن تبقى هناك بعض العيوب التي لم نعالجها والتي قد تسبّب توقّف البرنامج عن العمل: تُعتبر القيم التي يدخلها المستخدم بواسطة التابع ReadLine أنّها قيم نصيّة. وحتى نستطيع التعامل معها كأعداد تقبل فاصلة عشريّة يجب تحويلها إلى قيم عددية من النوع double. نستطيع ذلك بسهولة من خلال التابع Parse من الصنف double. يقبل هذا التابع أن نُمرّر إليه قيمة نصيّة ليعيد إلينا التمثيل العددي لها من النوع double. ولكنّ السؤال هنا أنّه ماذا لو أدخل المستخدم بشكل غير مقصود (أو مقصود) القيمة النصيّة التالية "abc" للعدد الأوّل؟ عندما يصل تنفيذ البرنامج إلى السطر 21 سيعمل التابع Parse على تحويل القيمة "abc" إلى التمثيل العددي من النوع double وهذا ما لا يمكن حدوثه بالطبع، لذلك فسيرمي التابع Parse استثناءً Exception سيؤدّي إلى توقّف البرنامج عن العمل فورًا! سنتحدّث عن الاستثناءات في درس لاحق. وعلى أيّة حال يمكن حلّ هذه المشكلة بطريقتين مختلفتين سنتحدّث عنهما لاحقًا في هذه السلسلة. ولكنّ المغزى هنا هو أنّه لا تثق بمدخلات المستخدم مطلقًا. تُعتبر عملية ضم النصوص التي أجريناها في السطر 29 غير عمليّة وعادة برمجيّة غير جيّدة. يتعلّق هذا الأمر بالحقيقة طريقة تعامل سي شارب مع النصوص، سأترك الحديث عن هذه المشكلة وطرق حلّها عندما نتحدّث عن النصوص والتعامل معها في الدرس السادس. تمارين داعمة تمرين 1 اكتب برنامجًا يطبع العبارات التالية كما يلي على الشاشة: Today is Sunday. Tomorrow is Monday. Yesterday is Saturday. تمرين 2 اكتب برنامجًا يطلب من المستخدم إدخال قيمتين عدديتين، ومن ثمّ يوجد حاصل الضرب لهما (استخدام العامل *) وبعرض النتيجة على الشاشة. يجب أن يدعم البرنامج الأعداد ذات الفاصلة العشرية. الخلاصة تعرّفنا في هذا الدرس على الشكل الأساسيّ لأيّ تطبيق مكتوب بلغة سي شارب. كما تعاملنا مع ثلاثة برامج بسيطة للغاية وضّحت مبادئ كتابة برنامج باستخدام سي شارب. سنتناول في الدرس القادم موضوع المتغيّرات وأنواعها والعوامل والتعابير expressions بتفصيل أكبر.
  2. سنتحدّث في هذا الدرس عن كيفيّة تطبيق مبادئ البرمجة كائنيّة التوجّه في سي شارب وذلك من خلال إنشاء واستخدام الأصناف والكائنات في هذه اللغة. يمكن التصريح عن صنف في سي شارب باستخدام الكلمة المحجوزة class يليها اسم الصنف وهو يتبع لنفس قواعد التسمية للمتغيّرات، علمًا أنّه يفضّل أن يكون الحرف الأوّل من اسم الصنف حرفًا طباعيًّا كبيرًا. انظر إلى الشكل التالي حيث نرى الصنف البسيط Employee والذي يُعبّر عن موظّف في إحدى الشركات: يحتوي هذا الصنف على ثلاثة حقول بيانات data fields هي: الاسم FirstName الكنية LastName الراتب Salary تستطيع اعتبارها حاليًّا أنّها تمثّل خصائص للصنف Employee، كما يحتوي هذا الصنف على تابع وحيد اسمه DisplayInfo الهدف منه هو الحصول على تمثيل نصيّ لكلّ كائن ننشئه من هذا الصنف كما سنرى بعد قليل، يشبه التابع إلى حدٍّ كبير الدّالة function في لغات البرمجة الأخرى. لا يتطلّب هذا التابع أيّ وسائط في حين أنّه يُرجع قيمة نصيّة من النوع string. هذه الحقول بالإضافة إلى التابع السابق تُعتبر أعضاء ضمن الصنف Employee كما ذكرنا ذلك مسبقًا. تقع أعضاء أيّ صنف ضمن حاضنتيه. لاحظ الكلمة المحجوزة public والموجودة قبل كلّ تصريح لحقل أو تابع ضمن الصنف Employee. هذه الكلمة عبارة عن مُحدّد وصول access modifier. تتحكّم محدّدات الوصول بقابلية الوصول إلى أعضاء الصنف من خارجه، سنتعامل مع نوعين آخرين من محدّدات الوصول وهما private و protected. يكفي أن تعلم الآن أنّ أي عضو في الصنف يمتلك محدّد وصول public يمكن الوصول إليه سواءً من داخل الصنف (أو بشكل أدق من داخل الكائن) أو من خارجه. كما من المفيد أن نعلم أنّه من الممكن استخدام محدّدات الوصول مع الأصناف أيضًا كما سنرى في درس لاحق. إذا أردنا إنشاء كائن جديد من الصنف Employee فعلينا التصريح عن متغيّر مناسب من النوع Employee وذلك على الشكل التالي: Employee empObject; صرّحنا عن المتغيّر empObject على أنّه من النوع Employee. لاحظ التشابه في التصريح عن المتغيّرات بين أنواع موجودة ضمن سي شارب وبين أنواع ننشئها بأنفسنا. التصريح السابق غير كافي لإنشاء الكائن. لإنشاء كائن من النوع Employee علينا استخدام العامل new الذي يعمل على إنشاء كائن من أيّ صنف نرغبه ويعمل على إعادة المرجع (العنوان) لذلك الكائن في الذاكرة. استخدام العامل new سهل حيث يمكننا كتابة ما يلي بعد عبارة التصريح السابقة: empObject = new Employee(); يقوم العامل new بإنشاء كائن جديد من الصنف Employee ثمّ يُسند مرجع (عنوان) هذا الكائن ضمن المتغيّر empObject. لاحظ القوسين الموجودين بعد اسم الصنف Employee. في الحقيقة يُعبّر هذين القوسين عن استدعاء لبانية constructor الصنف Employee عند إنشاء الكائن. ولكن أين هذه البانية؟ هذا ما سنراه بعد قليل. يمكن الآن الوصول إلى الحقول والتوابع الموجودة ضمن الكائن عن طريق كتابة المتغيّر الذي يحوي العنوان إلى الكائن (أي المتغيّر empObject) ثم نضع نقطة وبعدها اسم الحقل أو التابع الذي نريد الوصول إليه. في العبارة التالية سنسند القيمة "Mohammad" إلى الحقل FirstName من الكائن empObject (الكائن الذي يشير إليه empObject): empObject.FirstName = "Mohammad"; حان الآن وقت التنفيذ العمليّ. انظر إلى البرنامج Lesson06_01 الذي يوضّح كيفية إنشاء الصنف Employee وكيفيّة إنشاء كائنين منه: 1 using System; 2 3 namespace Lesson06_01 4 { 5 6 class Employee 7 { 8 public string FirstName; 9 public string LastName; 10 public double Salary; 11 12 public string DisplayInfo() 13 { 14 string result = string.Format("{0} {1} - Salary: {2:N0}", 15 this.FirstName, this.LastName, this.Salary); 16 17 return result; 18 } 19 } 20 21 class Program 22 { 23 static void Main(string[] args) 24 { 25 Employee employee1, employee2; 26 27 employee1 = new Employee(); 28 employee1.FirstName = "Mohammad"; 29 employee1.LastName = "Mansoor"; 30 employee1.Salary = 1000; 31 32 employee2 = new Employee(); 33 employee2.FirstName = "Saleh"; 34 employee2.LastName = "Mahmoud"; 35 employee2.Salary = 2500; 36 37 Console.WriteLine("First Employee: {0}", employee1.DisplayInfo()); 38 Console.WriteLine("Second Employee: {0}", employee2.DisplayInfo()); 39 } 40 } 41 } عند تنفيذ البرنامج سنحصل على الخرج التالي: First Employee: Mohammad Mansoor - Salary: 1,000.00 Second Employee: Saleh Mahmoud - Salary: 2,500.00 نلاحظ من النظرة الأولى للبرنامج السابق أنّه لدينا صنفان ضمن نطاق الاسم Lesson06_01 وهما Employee و Program. يقع التصريح عن الصنف Employee في الأسطر بين 6 و 19 ويحتوي هذا الصنف كما رأينا قبل قليل على أربعة أعضاء وهي عبارة عن ثلاثة حقول FirstName و LastName و Salary بالإضافة إلى التابع DisplayInfo الموجود بين السطرين 12 و18. تنحصر وظيفة هذا التابع في الحصول على التمثيل النصيّ لأيّ كائن ننشئه من الصنف Employee. يحتوي التابع DisplayInfo على أسلوب جميل لتنسيق النصوص يشبه ذلك الأسلوب الذي كنّا نستخدمه مع التابع WriteLine. يحتوي الصنف string على تابع اسمه Format يقبل عدّة وسائط (السطر 14) أولها نصّ تنسيقي، أمّا الوسائط التالية فهي القيم التي ستجد لها أمكنةً ضمن النص التنسيقي، كما كنّا نستخدم التابع WriteLine بالضبط. يُرجع التابع Format نصًّا منسّقًا بحسب القيم الممرّرة له. الشيء الوحيد المختلف هو كيفيّة تنسيق قيمة الراتب Salary باستخدام مُحدّد التنسيق :N0 الموجود ضمن {2:N0}. يخبر هذا المحدّد التابع Format أنّ القيمة التي ستوضع في هذا المكان (وهي قيمة Salary) يجب أن تُنسّق على شكل رقم ذي فاصلة آلاف وبدون فاصلة عشريّة. يفيد مثل هذا التنسيق في الحصول على أرقام منسّقة بشكل محترف تُعبّر عن الراتب الذي يحصل عليه الموظّف وهي تبدو مثل 1,000 أو 2,500. جرّب استخدام التنسيق {2:N1} و {2:N2} ولاحظ الفرق. لاحظ أنّني قد استخدمت الكلمة المحجوزة this متبوعةً بنقطة قبل اسم كل حقل. في الحقيقة تُشير هذه الكلمة إلى الكائن الحالي الذي يتمّ منه استدعاء التابع DisplayInfo كما سنرى ذلك بعد قليل. أمّا لإرجاع القيمة النصيّة من التابع DisplayInfo فإنّنا ببساطة نستخدم الكلمة المحجوزة return ونضع بعدها القيمة المراد إرجاعها. الصنف Program المصرّح عنه في الأسطر بين 21 و 40 هو الصنف الذي تعاملنا معه في جميع البرامج التي كتبناها حتى الآن. يحتوي هذا الصنف على التابع Main الذي يمثّل نقطة الدخول للبرنامج كما نعلم. يبدأ التابع Main بالتصريح عن متغيرين من النوع Employee وهما employee1 و employee2 ثمّ ينشئ كائنًا من النوع Employee باستخدام العامل new (السطر 27) ويسنده إلى المتغيّر employee1. بعد ذلك يمكن استخدام أيّ حقل أو تابع معرّف ضمن الصنف Employee عن طريق المتغيّر employee1 بشرط أن يكون له محدّد وصول public كما هو واضح في الأسطر من 28 حتى 30. يتكرّر نفس الأمر بالنسبة للمتغيّر employee2 الذي سيحمل كائنًا مختلفًا عن الكائن الموجود ضمن employee1. أخيرًا وفي السطرين 37 و38 يتم طباعة التمثيل النصيّ لكلّ من الكائنين باستخدام التابع DisplayInfo. تجدر الإشارة إلى أنّه عند وصول تنفيذ البرنامج إلى السطر 37 وإلى الاستدعاء ()employee1.DisplayInfo تحديدًا سيؤدّي ذلك إلى انتقال التنفيذ إلى السطر 14 ضمن هذا التابع لتنفيذ التعليمات البرمجيّة ضمنه ومن ثمّ الحصول على التمثيل النصيّ للكائن employee1 وإرجاعه إلى السطر 37 مرّة أخرى ليعمل البرنامج على تمرير هذه القيمة النصيّة للتابع WriteLine ومن ثمّ العرض على الشاشة، وبالطبع يتكرّر نفس الأمر تمامًا بالنسبة للكائن ضمن employee2 في السطر 38. إذا كنت تستخدم Visual Studio 2015 بأيّ إصدار فأنصحك أن تنفّذ هذا البرنامج بشكل خُطَوي لكي تتعرّف على آلية عمل هذا البرنامج بشمل عمليّ. اضغط على المفتاح F11 (أو من القائمة Debug > Step Into) لتنفيذ البرنامج باستخدام منقّح الأخطاء debugger. ستلاحظ ظهور مستطيل أصفر يُشير إلى مكان التنفيذ الحالي، وكلما ضغطت المفتاح F11 سينتقل تنفيذ البرنامج إلى العبارة البرمجيّة التالية خطوة بخطوة. البانية constructor ضمن الصنف البانية constructor هي تابع من نوع خاص يجب أن تكون موجودة ضمن أيّ صنف في سي شارب. في حال تمّ إغفالها سيعمل المترجم على توليد واحدة افتراضيّة من أجلنا. في الحقيقة وظيفة البانية هي بناء الكائن وحجز مكان مناسب له في الذاكرة، حيث يتم استدعاء البانية عند إنشاء الكائن باستخدام العامل new. لا يمكن للبواني إرجاع قيمة مخصّصة كما نفعل مع التوابع الأخرى عادةً، في الحقيقة هي تُرجع كائنًا من الصنف الموجودة ضمنه. ولكن يمكن أن تقبل وسائط نمرّرها إليها. استبدل الصنف Employee التالي بذلك الموجود ضمن البرنامج Lesson06_01: 1 class Employee 2 { 3 public string FirstName; 4 public string LastName; 5 public double Salary; 6 7 public Employee() 8 { 9 Console.WriteLine("Hello, I'm in Employee's constructor!"); 10 } 11 12 public string DisplayInfo() 13 { 14 string result = string.Format("{0} {1} - Salary: {2:N0}", 15 this.FirstName, this.LastName, this.Salary); 16 17 return result; 18 } 19 } لقد أضفنا في هذه النسخة البانية ()Employee للصنف Employee. نفّذ البرنامج لتحصل على الخرج التالي: *** Hello, I'm in Employee's constructor! *** *** Hello, I'm in Employee's constructor! *** First Employee: Mohammad Mansoor - Salary: 1,000 Second Employee: Saleh Mahmoud - Salary: 2,500 لاحظ أنّ العبارة: *** Hello, I'm in Employee's constructor! *** قد ظهرت مرّتين في الخرج، وذلك بسبب أنّنا أنشأنا كائنين حيث تُنفّذ هذه البانية من أجل كلّ عملية إنشاء. ولكن السؤال المطروح هنا، ماذا سنستفيد من هذه البانية؟ تُستخدم البواني عمومًا عندما نريد تهيئة الكائن ببعض القيم الضرورية لجعل حالته مستقرّة وذلك أثناء إنشائه وقبل محاولة الوصول إليه من أيّ مصدر خارجيّ. انظر الآن إلى الصنف Employee المعدّل الذي يحوي بانية تقوم ببعض الأعمال المفيدة: 1 class Employee 2 { 3 public string FirstName; 4 public string LastName; 5 public double Salary; 6 7 public Employee(string firstName, string lastName, double salary) 8 { 9 this.FirstName = firstName; 10 this.LastName = lastName; 11 this.Salary = salary; 12 } 13 14 public string DisplayInfo() 15 { 16 string result = string.Format("{0} {1} - Salary: {2:N0}", 17 this.FirstName, this.LastName, this.Salary); 18 19 return result; 20 } 21 22 } تتطلّب البانية هذه المرّة ثلاثة وسائط، تمثّل قيمًا سيتمّ إسنادها إلى الحقول. هذه الوسائط هي: firstName و lastName و salary (لاحظ أنّ اسم كلّ منها يبدأ بحرف طباعي صغير لتمييزها عن حقول الصنف). إذا استبدلت هذا الصنف الجديد بالصنف القديم الموجود ضمن البرنامج Lesson06_01 وحاولت تنفيذ البرنامج فستحصل على خطأ. السبب في ذلك بسيط، وهو أنّ العبارتين في السطرين 27 و 32 من البرنامج Lesson06_01 تحاولان إنشاء كائنين من الصنف Employee عن طريق بانية لا تتطلّب أيّة وسائط وهذا ما لا يتوفّر في الصنف Employee الجديد. فعندما يلاحظ مترجم سي شارب وجود بانية واحدة على الأقل بصرف النظر عن عدد الوسائط التي تتطلّبها فإنّه يمتنع عن توليد بانية افتراضية بشكل تلقائي مثلما كان يفعل من قبل. يوجد حلّ سريع لهذه المشكلة يتمثّل في توفير بانية لا تحتاج لأيّة وسائط كما كان الوضع السابق. انظر إلى النسخة الأخيرة للصنف Employee: 1 class Employee 2 { 3 public string FirstName; 4 public string LastName; 5 public double Salary; 6 7 public Employee(string firstName, string lastName, double salary) 8 { 9 this.FirstName = firstName; 10 this.LastName = lastName; 11 this.Salary = salary; 12 } 13 14 public Employee() 15 { 16 17 } 18 public string DisplayInfo() 19 { 20 string result = string.Format("{0} {1} - Salary: {2:N0}", 21 this.FirstName, this.LastName, this.Salary); 22 23 return result; 24 } 25 26 } بعد اعتماد هذا الصنف ضمن البرنامج Lesson06_01، سيعمل البرنامج الآن بشكل طبيعي ويظهر الخرج كما هو متوقّع. ولكن تأمّل معي هذا الصنف قليلًا، ألا تلاحظ وجود بانيتين له؟ هذا أمر طبيعي ووارد جدًّا في سي شارب حيث يمكن كتابة أكثر من تابع بنفس الاسم طالما اختلف عدد أو أنواع الوسائط الممرّرة لكلّ منهما. نسمي هذه الميزة بزيادة التحميل overloading للتوابع. فعند وجود استدعاء للتابع المزاد تحميله يتمّ اختيار الشكل المناسب بناءً على عدد وأنواع الوسائط الممرّرة. لاحظ أنّ البانية عديمة الوسائط فارغة ولا بأس في ذلك. ولكنّ السؤال هنا كيف يمكن الاستفادة من البانية ذات الوسائط الثلاثة. الأمر بسيط، استبدل محتويات التابع Main في البرنامج Lesson06_01 بالشيفرة البسيطة المكافئة التالية: 1 Employee employee1, employee2; 2 3 employee1 = new Employee("Mohammad", "Mansoor", 1000); 4 employee2 = new Employee("Saleh", "Mahmoud", 2500); 5 6 Console.WriteLine("First Employee: {0}", employee1.DisplayInfo()); 7 Console.WriteLine("Second Employee: {0}", employee2.DisplayInfo()); انظر كم أصبحت الشيفرة نظيفة وقصيرة ومريحة للعين. إليك الآن البرنامج Lesson06_02 كاملًا بعد التعديل: 1 using System; 2 3 namespace Lesson06_02 4 { 5 6 class Employee 7 { 8 public string FirstName; 9 public string LastName; 10 public double Salary; 11 12 public Employee(string firstName, string lastName, double salary) 13 { 14 this.FirstName = firstName; 15 this.LastName = lastName; 16 this.Salary = salary; 17 } 18 19 public Employee() 20 { 21 22 } 23 24 public string DisplayInfo() 25 { 26 string result = string.Format("{0} {1} - Salary: {2:N0}", 27 this.FirstName, this.LastName, this.Salary); 28 29 return result; 30 } 31 32 33 } 34 35 36 class Program 37 { 38 static void Main(string[] args) 39 { 40 Employee employee1, employee2; 41 42 employee1 = new Employee("Mohammad", "Mansoor", 1000); 43 employee2 = new Employee("Saleh", "Mahmoud", 2500); 44 45 Console.WriteLine("First Employee: {0}", employee1.DisplayInfo()); 46 Console.WriteLine("Second Employee: {0}", employee2.DisplayInfo()); 47 } 48 } 49 } تمارين داعمة تمرين 1 أضف تابعًا جديدًا إلى الصنف Employee الموجود في البرنامج Lesson06_02 السابق وسمّه GetSalaryAfterTax. وظيفة هذا التابع هي الحصول على قيمة الراتب للموظّف بعد تطبيق الضريبة Tax عليه. اعتبر نسبة الضريبة 2%. تلميح: اضرب قيمة الراتب Salary بالعدد 0.98 للحصول على قيمة الراتب بعد خصم الضريبة. فإذا كان الراتب 1500 مثلًا، يجب أن يُرجع التابع GetSalaryAfterTax القيمة 1470. تمرين 2 أنشئ صنفًا جديدًا سمّه MyRectangle والذي يُعبّر عن مستطيل في المستوي، بحيث يحتوي على الحقلين Width و Height (من النوع double لكلّ منهما)، بالإضافة إلى التابع GetArea لحساب مساحة المستطيل. ثمّ اكتب برنامجًا بسيطًا يوضّح استخدام هذا الصنف من خلال إنشاء كائنين منه. احسب مساحة كل مستطيل (كائن) واعرض النتيجة على الشاشة. الخلاصة تعلّمنا في هذا الدرس أساسيّات إنشاء الأصناف والكائنات، وكيفية التعامل مع الحقول والتوابع والبواني الموجودة ضمن الصنف. كما أخذنا لمحة سريعة حول محدّدات الوصول وكيفية التعامل مع محدّد الوصول public، علمًا أنّنا ستوضّح كيفيّة التعامل مع باقي المحدّدات في الدرس التالي الذي سنتحدّث فيه عن المزيد حول هذا الموضوع المهم والأساسي لتطوير التطبيقات باستخدام سي شارب.
  3. يُعتبر إطار العمل دوت نت NET Framework. من شركة مايكروسوفت من أُطر العمل المشهورة جدًّا، فمنذ الإصدار التجريبي الأوّل أواخر عام 2000 وحتى الإصدار 4.6 حاليًّا شهد تحسينات كبيرة جعلت من لغات البرمجة التي تعمل بالاعتماد عليه لغات برمجة غنيّة ومعاصرة. يُعتبر إطار العمل دوت نت الكيان الأساسيّ التي تعتمد عليه التطبيقات في تنفيذ المهام المطلوبة منها. فهو يوفّر الوسائل اللازمة لوصول مثل هذه التطبيقات إلى الملفات والتعامل مع HTTP والوصول والتعامل مع قواعد البيانات وغيرها من المهام الأساسيّة التي قد يحتاجها أيّ تطبيق حاسوبيّ. صُمّم إطار العمل دوت نت منذ البداية لكي يعمل على أيّ نظام تشغيل أو أيّ عتاد صلب متاح، وعلى الرغم من أنّ مايكروسوفت لم تدعم تشغيل إطار العمل هذا سوى على أنظمة تشغيل ويندوز، إلّا أنّه جرت محاولات مستقلة لنقله إلى لينكس عن طريق مشروع mono مفتوح المصدر. وقد نجحت هذه المحاولة بصورة لا بأس بها واستمر المشروع لعدّة سنوات، ثمّ توّج بنقله إلى أنظمة تشغيل الأجهزة الذكيّة وهذا ما قامت به شركة Xamarin التي استحوذت عليها شركة مايكروسوفت قبل أقل من شهر من الآن، حيث تمكّنت Xamarin من ذلك بالاعتماد على مشروع mono مفتوح المصدر. تُعتبر لغة سي شارب #C لغة البرمجة الأساسيّة ضمن إطار العمل دوت نت، وقد واكبْتُ تطوّرها منذ الإصدار الأوّل وحتى اليوم. ومما لا شكّ فيه أنّها قد شهدت تحسينات كبيرة ومزايا مفيدة تجمع بين القوّة والمرونة لتطوير طيف واسع من التطبيقات البرمجيّة تشمل تطبيقات سطح المكتب Desktop Applications وتطبيقات ويب باستخدام ASP.NET وتطبيقات خدمات ويب مثل WCF (اختصار لـ Windows Communication Foundation) وحتى تطبيقات الأجهزة الذكيّة التي تعمل على نظام تشغيل Windows Phone أو التي تعمل على باقي الأنظمة مثل Android و iOS من خلال منصّة Xamarin. لن ندخل في التفاصيل الدقيقة لمعمارية إطار العمل دوت نت، وذلك لجعل هذه السلسلة مبسّطة قدر المستطاع. يكفيك أن تعلم أنّ إطار العمل هذا يتكوّن من قسمين رئيسيّين: بيئة التنفيذ المشتركة Common Language Runtime وتُدعى اختصارًا CLR. مكتبة أصناف إطار العمل Framework Class Library وتُدعى اختصارًا FCL. حول هذه السلسلة سنتعلّم في هذه السلسلة كتابة تطبيقات باستخدام لغة سي شارب #C بأسلوب مبسّط وسلس وذلك من خلال تطبيقات موجّه الأوامر Console Applications، وسبب تفضيلي لهذه التطبيقات عن سواها، هو أنّه تسمح لك بالتركيز على تعلّم اللغة نفسها دون تشتيت الانتباه إلى العديد من الجوانب التي تتطلّبها أنواع التطبيقات الأخرى كتطبيقات سطح المكتب لويندوز على سبيل المثال، رغم أنّنا سنتحدّث القليل عنها في الدرس الأخير. ما الذي أحتاجه لكي أستفيد من هذه السلسلة؟ تفترض هذه السلسلة أنّك تمتلك معرفة عامّة بأساسيّات البرمجة. ستحتاج أيضًا إلى حاسوب ذو نظام تشغيل ويندوز 7.1 أو أعلى. بالإضافة إلى تحميل وتنصيب بيئة التطوير Visual Studio 2015 Community المجّانيّة. كما يمكنك تجريب معظم البرامج الموجودة في هذه السلسلة حتى ولو لم يكن لديك نظام تشغيل ويندوز أصلًا عن طريق موقع NET Fiddle. ولكن لن تستفيد بهذه الطريقة من المزايا الهامّة التي يوفّرها لك Visual Studio. الأقسام الأساسية لإطار العمل دوت نت سنتحدّث قليلًا عن القسمين الأساسيّين لإطار العمل دوت نت بالإضافة إلى مترجم سي شارب C# Compiler، لكي تتكوّن لدينا الصورة الواضحة لآليّة عمل تطبيقات دوت نت. بيئة التنفيذ المشتركة CLR هي البيئة التي تنفّذ البرامج المكتوبة بلغات دوت نت، وتشكّل حاضنةً لهذه البرامج من خلال عزلها عن العتاد الصلب hardware للجهاز التي تعمل عليه. تستطيع تشبيهها بالآلة الافتراضيّة Virtual Machine الخاصّة بلغة Java. وهي مسؤولة عن إدارة المصادر التي يحتاجها البرنامج في عمله، وفي مقدّمتها الذاكرة. حيث تعمل هذه البيئة على إدارة عمليّات إنشاء الكائنات التي يحتاجها البرنامج، ومن ثمّ التخلّص منها عند انتفاء الحاجة إليها. تتوفّر CLR بشكل افتراضيّ على معظم أنظمة تشغيل ويندوز (مع الاختلاف بالإصدارات) مثل Windows XP و Windows 7 و Windows 8 و Windows 10 و Windows Phone و Windows Server (بمعظم إصداراته). مكتبة أصناف إطار العمل FCL تحتوي هذه المكتبة الضخمة على آلاف الأصناف classes التي تسمح لنا بإنجاز أيّ مهمّة تخطر ببالنا. فمن التعامل مع النصوص والتعابير النظاميّة regular expressions إلى التعامل مع الاتصالات الشبكيّة network communications والتعامل مع بروتوكولات الانترنت الشهيرة مثل HTTP وFTP وغيرها، والوصول إلى قواعد البيانات والتعامل معها. تتكوّن FCL بدورها من عدّة مكتبات فرعيّة، فهناك مكتبة تساعدنا على تطوير تطبيقات سطح المكتب لويندوز، وأخرى تسمح لنا بتطوير تطبيقات ويب باستخدام تقنيّة ASP.NET، ومكتبة تسمح بإنشاء تطبيقات WCF وهكذا. بمعنى أنّه لن تحتاج إلى التعامل مع كامل مكتبة FCL بل ستنتقي منها ما تحتاجه بحسب متطلّبات عملك، فإذا كنت مطوّر ويب مثلًا فستهتم بالمكتبة الفرعيّة التي تسمح بإنشاء تطبيقات ويب باستخدام ASP.NET. ومن الجدير ذكره أنّ مكتبة FCL تحتوي أيضًا على مكتبة فرعيّة اسمها مكتبة الأصناف الأساسيّة Base Class Library أو اختصارًا BCL، تحتوي هذه المكتبة على الأصناف الأساسيّة التي تعمل مع جميع التطبيقات المنشأة باستخدام المكتبات الأخرى بصرف النظر عن نوعها. مترجم سي شارب C# Compiler لكي نتمكّن من تنفيذ البرامج التي نكتبها باستخدام سي شارب، نحتاج إلى مترجم سي شارب C# Compiler. يكون هذا المترجم مضمّنًا بشكل افتراضيّ مع إطار العمل دون نت. يعمل المترجم على تحويل التعليمات البرمجيّة الموجودة في الشيفرة والمكتوبة بلغة سي شارب إلى تعليمات برمجيّة مكتوبة بلغة تُعرف بلغة مايكروسوفت الوسيطيّة Microsoft Intermediate Language أو اختصارًا MSIL. تُعتبر هذه اللغة الوسيطيّة منخفضة المستوى، وهي شبيهة بلغة Assembly الشهيرة. في الواقع ستعمل بيئة التنفيذ المشتركة CLR على تنفيذ التعليمات المكتوبة بلغة MSIL. فبيئة CLR لا تحتاج أن تعرف لغة البرمجة التي كُتب بها البرنامج أصلًا وهي الميزة الأساسيّة التي تسمح للمبرمج الكتابة بأي لغة برمجة تدعم الدوت نت. فطالما أنّ لغة البرمجة تمتلك مترجمًا متوافقًا مع MSIL فلا مشكلة. وهذا ما جرى مع لغات برمجة أخرى مثل Visual Basic.NET ولغة #F وحتى لغة البايثون Python من خلال IronPython. انظر الشكل التوضيحي التالي: كيف ينفذ البرنامج باستخدام CLR؟ توضع الشيفرة البرمجيّة المكتوبة بلغة سي شارب عادةً في ملفات منفصلة لكلٍّ منها الامتداد cs، يعمل المترجم بعد ذلك على قراءة هذه الملفات التي تسمّى بملفّات النص المصدريّ source code files وتحويل التعليمات البرمجيّة المكتوبة بسي شارب إلى لغة MSIL وجعلها ضمن ملف تنفيذي (الملف MyApp.exe في الشكل السابق). عند تنفيذ البرنامج تقرأ CLR التعليمات البرمجيّة الموجودة ضمن الملف التنفيذي والتي تكون بلغة MSIL وتعمل على تنفيذها عن طريق تحويلها إلى تعليمات برمجيّة أصليّة native code مخصّصة لمعالج الحاسوب الذي يعمل عليه البرنامج حاليًّا. يجري كلّ هذا العمل بسرعة كبيرة جدًّا فعملية التحويل إلى native code لا تجري سوى أوّل مرّة فقط، وبعد ذلك يستفيد البرنامج من السرعة الكبيرة التي توفّرها هذه التعليمات الأصليّة عند التنفيذ. يُفسّر السيناريو السابق سبب قابليّة عمل برامج دوت نت على أيّ نظام تشغيل أو عتاد حاسوبي. فعمليّة كتابة الشيفرة البرمجيّة بأي لغة دوت نت ومن ثمّ تحويلها إلى لغة مايكروسوفت الوسيطيّة MSIL منفصلة تمامًا عن نظام التشغيل وعن العتاد الحاسوبي الذي سيعمل عليه البرنامج. ولعلّ من أبرز الأمثلة على ذلك البرامج المكتوبة بلغة سي شارب والموجّهة إلى أنظمة تشغيل الأجهزة الذكيّة عن طريق منصة Xamarin التي ذكرناها قبل قليل. فالعمل الأساسي الذي قامت به هذه الشركة هو انتاج بيئة CLR منفصلة لكلّ نظام تشغيل مثل Android و iOS مما يسمح للبرامج المكتوبة بلغة سي شارب الخروج من "قمقم" ويندوز إن صحّ التعبير. وأكثر من ذلك، فقد أنتجت مايكروسوفت مؤخّرًا نسخة خاصّة وخفيفة من نظام التشغيل Windows 10 أسمته Windows 10 IoT وهو مخصّص لإنترنت الأشياء. يمكن تنصيب نظام التشغيل هذا بكلّ سلاسة ويُسر على جهاز Raspberry Pi المشهور والموجّه لتطبيقات إنترنت الأشياء، وهذا يعني فتح المجال أمام التطبيقات المكتوبة باستخدام لغة سي شارب للعمل على هذا الجهاز وبالتالي الدخول في هذا المجال المهم والواعد. الخلاصة لغة سي شارب هي لغة عصريّة ولها مستقبل واعد في قطاع الأعمال. فهي اللغة الأساسيّة المستخدمة لكتابة مختلف أنواع التطبيقات الحاسوبيّة ضمن إطار العمل دوت نت. بالإضافة إلى أنّها سلسة وذات بنية مألوفة، ولكن تحتاج إلى قليل من الصبر في تعلّمها. سنتناول في الدروس اللّاحقة كيفيّة كتابة برامج بسيطة ولكن متدرّجة في الصعوبة حتى تكون ملمًّا بأساسيّات هذه اللغة الرائعة.
  4. تعتبر تقنيّة Linq من التقنيّات الجديدة نسبيًّا في سي شارب ولغات دوت نت عمومًا. تسمح هذه التقنيّة بإجراء عمليّات استعلام معقّدة لاستخلاص البيانات بشكل سلس وسهل بسبب شكلها المألوف كما سنرى لاحقًا في هذا الدرس. لتقنيّة Linq أشكال متعدّدة: Linq to Objects: للتعامل مع البيانات الموجودة في ذاكرة البرنامج. Linq To XML: للتعامل مع البيانات النصيّة الموجودة بتنسيق XML. Linq To SQL: وهي تقنيّة خاصّة بالحصول على البيانات من خادم SQL Server. في الحقيقة تمّ التخلّي عن هذه التقنيّة رغم حداثتها، وذلك لصالح تقنيّة أحدث وأكثر تطوّرًا وهي Entity Framework. كما يتحدّث هذا الدرس عن تعابير Lambda وهي من المزايا المفيدة والتي تسهّل عمل المبرمجين إلى حدٍّ كبير. ستناول في هذا الدرس الشكل الأوّل من Linq، وهو استخدام Linq مع الكائنات Objects. ولكن قبل ذلك لنتحدّث قليلًا عن تعابير Lambda. تعابير Lambda تستطيع تخيّل تعابير Lambda على أنّها دوال functions صغيرة ليس لها اسم، تعمل على إجراء عمليّات حسابيّة بسيطة، ومن الممكن أن ترجع نتيجة. في الواقع يمكن للنوّاب Delegates أن تغلّف تعابير Lambda ضمن شروط محدّدة. سنتناول مثالًا بسيطًا يوضّح كيفيّة التعامل معها. انظر إلى الشيفرة التالية: class Program { delegate int Square(int x); static void Main(string[] args) { Square square = (x) => x * x; int result = square(5); } } صرّحنا في الشيفرة السابقة عن النائب Square الذي يتطلّب وسيطًا واحدًا من النوع int ويُرجع قيمة من نفس النوع. يُفترض بهذا النائب بأن يُغلّف التوابع التي تعمل على إيجاد مربّع عدد صحيح. إذا نظرت الآن إلى التابع Main ستجد أنّنا في السطر الأوّل منه نصرّح عن المتغيّر square من نوع النائب Square، حيث نُسند إليه ما يلي: (x) => x * x التعبير السابق هو تعبير Lambda بسبب وجود السهم <= ضمنه. فهم هذا التعبير بسيط، فهو يطلب وسيطًا وحيدًا (x) على يسار السهم، ويضرب قيمة هذا الوسيط بنفسها: x * x على يمين السهم، سيُرجع هذا التعبير قيمة x مضروبةً بنفسها. ولكن الملفت في الأمر أنّنا قد أسندنا هذا التعبير إلى متغيّر من نوع النائب Square. السبب في ذلك أنّ تعبير Lambda السابق يتوافق مع النائب Square في أنّه يحتاج إلى وسيط وحيد من النوع int ويُرجع قيمة من نفس النوع. ولكنّنا لم نوضّح في تعبير Lambda نوع الوسيط أو نوع القيمة المُعادة! لا مشكلة في ذلك، فسيتم استخلاص النوع من الوسيط المُمرّر وذلك بشكل تلقائي. كمثال آخر على استخدام تعبير Lambda يمكن كتابة تعبير يتطلّب وسيطين ولكن لا يُرجع أي قيمة. انظر إلى الشيفرة التالية: class Program { delegate void SumAndPrinting(int a, int b); static void Main(string[] args) { SumAndPrinting sumAndPrnt = (a, b) => Console.WriteLine(a + b); sumAndPrnt(3, 4); } } النائب SumAndPrinting يقبل الآن وسيطين من النوع int لكنّه لا يُرجع أي قيمة (void). انظر إلى تعبير Lambda كيف أصبح: (a, b) => Console.WriteLine(a + b) لاحظ كيف أنّ الوسائط الموجودة بين قوسين تفصل بينها فواصل عاديّة دون تحديد الأنواع. كما يمكنك أن تلاحظ أيضًا بأنّ هذا التعبير مهمّته جمع قيمتي الوسيطين والطبع إلى الشاشة دون إرجاع أيّ قيمة. ملاحظة: يمكننا الاستغناء عن القوسين في تعبير Lambda، إذا أردنا تمرير وسيط واحد فقط. مثل: x => x * x استعلاماتLinq للاستفادة من Linq يجب إضافة نطاق الاسم System.Linq باستخدام الكلمة المحجوزة using. أفضل وسيلة لفهم Linq هي من خلال مثال تطبيقي بسيط، في البرنامج Lesson15_01 سنستخدم الصنف Student الذي استخدمناه في درس سابق ولكن سنجري فيه بعض التعديلات البسيطة، حيث أصبحنا نفصل اسم الطالب FirstName عن كنيته LastName، بالإضافة إلى إضافة حقل جديد اسمه Id من النوع int، والذي يُعبّر عن رقم الطالب: class Student { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Mark { get; set; } } سننشئ 10 كائنات من الصنف Student ونخزّنها ضمن مجموعة عموميّة <List<Student ثم نُجري على هذه المجموعة بعض "الحيل" باستخدام Linq. الهدف من هذا البرنامج هو إجراء عمليّة تصفية على هؤلاء الطلّاب بحيث نحصل على الطلّاب الذين تكون درجاتهم أكبر تمامًا من 60. سيحتوي البرنامج Lesson15_01 على أفكار جديدة ولكن مفيدة فكن مستعدًّا: 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 5 namespace Lesson15_01 6 { 7 class Student 8 { 9 public int Id { get; set; } 10 public string FirstName { get; set; } 11 public string LastName { get; set; } 12 public int Mark { get; set; } 13 } 14 15 class Program 16 { 17 static void Main(string[] args) 18 { 19 List<Student> studentsList = new List<Student>() 20 { 21 new Student {Id = 1, FirstName = "Ahmad", LastName = "Morad" , Mark = 80}, 22 new Student {Id = 2, FirstName = "Husam", LastName = "Sayed" , Mark = 75}, 23 new Student {Id = 3, FirstName = "Nour", LastName = "Hasan" , Mark = 65}, 24 new Student {Id = 4, FirstName = "Bssel", LastName = "Shamma" , Mark = 30}, 25 new Student {Id = 5, FirstName = "Ahmad", LastName = "Khatib" , Mark = 90}, 26 new Student {Id = 6, FirstName = "Maryam", LastName = "Burhan" , Mark = 95}, 27 new Student {Id = 7, FirstName = "Sarah", LastName = "Burhan" , Mark = 100}, 28 new Student {Id = 8, FirstName = "Mansour", LastName = "Khalid" , Mark = 50}, 29 new Student {Id = 9, FirstName = "Omran", LastName = "Barrak" , Mark = 45}, 30 new Student {Id = 10, FirstName = "Hasan", LastName = "Anis" , Mark = 56}, 31 }; 32 33 Console.WriteLine("Full List:"); 34 Console.WriteLine("----------"); 35 PrintList(studentsList); 36 37 38 IEnumerable<Student> students = from student in studentsList 39 where student.Mark > 60 40 select student; 41 42 Console.WriteLine(); 43 Console.WriteLine("After applying Linq:"); 44 Console.WriteLine("----------"); 45 PrintList(students); 46 } 47 48 private static void PrintList(IEnumerable<Student> students) 49 { 50 Console.WriteLine("{0,-5}{1,-15}{2,-15}{3,-10}", "Id", "First Name", "Last Name", "Mark"); 51 52 foreach (Student s in students) 53 { 54 Console.WriteLine("{0,-5}{1,-15}{2,-15}{3,-10}", s.Id, s.FirstName, s.LastName, s.Mark); 55 } 56 } 57 } 58 } نفّذ البرنامج السابق لتحصل على شكل شبيه بما يلي: لاحظ كيف استثنى البرنامج في القائمة الثانية الطلاب الذين تقل درجاتهم عن 60 أو تساويها. قارن بين القائمتين واكتشف العناصر المستثناة. يبدأ البرنامج في التابع Main بإنشاء 10 كائنات من النوع Student تمثّل بيانات عشرة طلّاب وإسناد هذه الكائنات فورًا إلى المجموعة القائمة studentsList بشكل مختصر (الأسطر من 19 إلى 31). الملفت هنا هو طريقة إنشاء كل من هذه الكائنات. انظر إلى السطر 21 مثلًا: new Student {Id = 1, FirstName = "Ahmad", LastName = "Morad" , Mark = 80} هذا شكل مختصر لإنشاء كائن من النوع Student حيث استخدمنا حاضنة {} بعد اسم الصنف Student مباشرةً وكتبنا أسماء الخصائص التي نريد تهيئتها ضمن هذه الحاضنة. بالنسبة لبانيّة الصنف Student فستُستدعى بكلّ تأكيد. يعتبر هذا الشكل من الإنشاء والإسناد المباشر للخصائص مفيدًا للغاية (أستخدمه بكثرة في برامجي الخاصّة) حيث يقلّل من أسطر الشيفرة البرمجيّة إلى حدٍّ كبير. سيطبع البرنامج بعد ذلك القائمة التي أنشأناها قبل قليل من باب التوضيح، وذلك من خلال استدعاء التابع الساكن PrintList (السطر 35) بالشكل التالي: PrintList(studentsList); مرّرنا لهذا التابع القائمة الكاملة studentsList. التصريح عن التابع الساكن موجود في الأسطر بين 48 و 56 وسنتكلّم عنه بعد قليل. يحتوي السطر 38 على عبارة برمجيّة تستخدم استعلام Linq: IEnumerable<Student> students = from student in studentsList where student.Mark > 60 select student; يقع استعلام Linq على يمين عامل الإسناد (=)، وفي الحقيقة إذا كان لديك اطّلاع على لغة SQL فسيكون هذا الاستعلام مألوفًا بالنسبة إليك. لنركّز الآن على هذا الاستعلام فحسب: from student in studentsList where student.Mark > 60 select student يبدأ الاستعلام بالكلمة المحجوزة from يتبعه اسم متغيّر جديد يمكنك تسميّته بأيّ اسم ترغبه. اخترت الاسم student لأنّني وجدتّه معبّرًا. بعد اسم المتغيّر الجديد نجد الكلمة المحجوزة in وبعدها اسم المجموعة التي نريد تطبيق الاستعلام عليها. إذًا أصبح بإمكاننا قراءة السطر الأوّل من الاستعلام على الشكل التالي: يبدأ السطر الثاني بالكلمة المحجوزة where وهي اختياريّة ومن الممكن عدم كتابتها، وهي تسمح بكتابة شرط من ممكن تطبيقه على عناصر المجموعة studentsList. بالنسبة لمثالنا هذا، اخترت تطبيق الشرط: where student.Mark > 60 أي أنّني أريد أن تكون درجة كل طالب (student) أكبر تمامًا من 60. أمّا السطر الثالث select student فهو يخبر Linq عن شكل البيانات التي نريد الحصول عليها بنتيجة تنفيذ الاستعلام. في مثالنا هذا نريد الحصول على مجموعة كل عنصر من عناصرها هو كائن من النوع Student. بنتيجة تنفيذ الاستعلام السابق سيحتوي المتغيّر students على مرجع لكائن مجموعة يحقّق الواجهة <IEnumerable<Student ولا يهمّك في الحقيقة ما هو النوع الفعليّ لهذه المجموعة. يمكن لاستعلام Linq أن يُنتج مرجعًا لكائن مجموعة يحقّق الواجهة <IQueryable<Student ولكنّ الحديث عن هذا الموضوع هو خارج مجال الدرس. في الواقع يمكن استخدام شروط أكثر تعقيدًا كأن نرغب بالحصول على جميع الطلّاب الذين تتراوح درجاتهم بين 60 و90 ضمنًا على سبيل المثال، وذلك باستخدام العامل && بالشكل التالي: from student in studentsList where student.Mark >= 60 && student.Mark <= 90 select student كما من الممكن أنّ نرتّب البيانات حسب رقم الطالب id، أو بحسب اسمه FirstName أو كنيته LastName أو بمزيج منها، وذلك باستخدام الكلمة المحجوزة orderby الخاصّة بـ Linq: from student in studentsList where student.Mark >= 60 && student.Mark <= 90 orderby student.FirstName, student.LastName select student سيقوم الاستعلام السابق بترتيب العناصر التي توافق الشرط الموجود في القسم where حسب الاسم ثمّ حسب الكنيّة. توجد في الحقيقة الكثير من المزايا القويّة التي تتمتّع بها استعلامات Linq والتي لا يتّسع هذا الدرس لذكرها. بالنسبة للتابع PrintList (الأسطر من 50 حتى 58) فيقتصر دوره على طباعة جدول للقائمة التي نمرّرها كوسيط إليه. لاحظ أنّ الوسيط الوحيد الذي يقبله يحقّق الواجهة <IEnumerable<Student لذلك فيمكننا تمرير أي وسيط إليه يحمل مرجعًا إلى كائن من أيّ صنف يحقّق هذه الواجهة بما فيه بالطبع الصنف <List<Student. الأمر الوحيد الجديد في هذا التابع هو استخدامه لتنسيق مختلف في إظهار البيانات بشكل جدوليّ. انظر السطر 52. ستجد النص التنسيقي: "{0,-5}{1,-15}{2,-15}{3,-10}" يسمح هذا النص التنسيقي بعرض البيانات بشكل جدوليّ أنيق على الشاشة، حيث يسمح التنسيق التالي {0, -5} بعرض الوسيط ذو الموقع 0 (من التابع WriteLine) ضمن حقل عرضه 5 فراغات بحيث تكون المحاذاة نحو اليسار. أمّا التنسيق {1, -15} فيسمح بعرض الوسيط ذو الموقع 1 ضمن حقل عرضه 15 فراغ بحيث تكون المحاذاة نحو اليسار أيضًا. بإزالة إشارة السالب (-) من التنسيقين السابقين ستصبح المحاذاة نحو اليمين. هل تريد المزيد من الإثارة؟ أضف السطر التالي إلى السطر 43 من البرنامج السابق (أي بعد العبارة التي تستخدم استعلام Linq): double average = students.Average(s => s.Mark); تستخدم هذه العبارة التابع Average من المتغيّر students الذي يحتوي على قائمة الطلّاب بعد التصفيّة كما نعلم. وكما يُوحي اسمه يعمل هذا التابع على حساب معدّل الطلاب (كائنات Student) الموجودين ضمن students. ولكن كيف سيعرف التابع Average الحقل الذي سيتمّ بموجبه حساب المعدّل؟ يتمثّل الحل في استخدام تعبير Lambda يتطلّب وسيطًا واحدًا (الوسيط s) الذي سيمثّل كائن Student، ويُرجع قيمة الخاصيّة Mark له: s => s.Mark سيستخدم التابع هذا التعبير للمرور على جميع العناصر الموجودة ضمن المجموعة students ليحصل على درجة كلّ منها باستخدام تعبير Lambda السابق ثمّ يحسب المعدّل، ليعمل البرنامج على إسناده إلى المتغيّر average وهو من النوع double كما هو واضح. بعد تنفيذ العبارة السابقة وعلى فرض أنّ نسخة البرنامج Lesson15_01 الأساسيّة هي التي استُخدمت، ستكون قيمة average تساوي 77.5، ويمثّل هذا الرقم معدّل الطلاب الذين تكون درجاتهم أكبر تمامًا من 60. قد يبدو كلّ ما قدّمناه جميلًا وممتعًا، لكنّك لن تستمتع بشكل فعليّ بهذه المزايا الرائعة التي توفّرها Linq و تعابير Lambda ما لم تستخدمها في برامجك الخاصّة. أستخدم مثل هذه المزايا في برامجي التي أطوّرها، ولن تتصوّر مدى سعادتي عندما أستخلص بيانات من قاعدة بيانات أو من خدمة ويب web service قد تحتوي على المئات أو الآلاف من البيانات الخام، ثمّ أُطبّق عليها وصفات Linq السحريّة فأحصل على ما أريده بكتابة عبارة برمجيّة واحدة فقط! تمارين داعمة تمرين 1 أجرِ تعديلًا على البرنامج Lesson15_01 بحيث نحصل على جميع الطلّاب الذين تكون درجاتهم أقل تمامًا من 50. تمرين 2 أجرِ تعديلًا آخرًا على البرنامج Lesson15_01 بحيث نحصل على جميع الطلّاب الذين يكون الحرف الأوّل من اسمهم هو "A". (تلميح: أحد الحلول المقترحة هو استخدام التابع StartWith من الخاصيّة النصية FirstName للكائن student أي على الشكل التالي: student.FirstName.StartsWith("A") وذلك بعد الكلمة where في استعلام Linq ). الخلاصة تعرّفنا في هذا الدرس على تعابير Lambda واستعلامات Linq. حيث صمّمنا عدّة برامج توضّح هاتين التقنيّتين المهمّتين في البرمجة باستخدام سي شارب. ستصادف كلًّا منهما كثيرًا في حياتك البرمجيّة، وستكون سعيدًا باستخدامها نظرًا للاختصار الكبير الذي ستحصل عليه في الشيفرة البرمجيّة، هذا فضلًا عن الأداء عالي المستوى الذي لن تستطيع مجاراته باستخدامك للشيفرة التقليديّة.
  5. تعتبر المصفوفات من بنى المعطيات المهمّة في أيّ لغة برمجة. سيفترض هذا الدرس أنّه لديك خبرة مسبقة عن مفهوم المصفوفة. المصفوفات في سي شارب هي عبارة عن نوع مرجعيّ reference type، وهي ترث من الصنف الأب System.Array. تقدّم لنا سي شارب المصفوفات بأسلوب مبسّط وواضح. فلتعريف مصفوفة يمكنها استيعاب 10 عناصر من النوع int مثلًا يكفي أن نكتب ما يلي: int[] arrIntegers = new int[10]; للعبارة السابقة في الواقع وظيفتان: الأولى هي التصريح عن المتغيّر arrIntegers على أنّه مصفوفة عناصرها من النوع int وذلك عن طريق كتابة []int أوّل العبارة. والثانية هي إنشاء كائن المصفوفة وحجز 10 أماكن في الذاكرة بحيث يستطيع كلّ مكان منها استيعاب قيمة من النوع int وذلك عن طريق التعبير [new int[10 ومن ثمّ إسناد المرجع لهذا الكائن إلى المتغير arrIntegers. ويمكن كما نعلم أن نجري هذه العمليّة على شكل عبارتين منفصلتين. يمكننا إنشاء أيّ نوع من المصفوفات نرغبه. فيمكننا إنشاء مصفوفات عناصرها نصوص []string، ومصفوفات عناصرها أعداد ذوات فاصلة عائمة مثل []float، وحتى يمكننا إنشاء مصفوفات عناصرها كائنات من أصناف ننشئها نحن بأنفسنا. فمثلًا إذا أنشأنا الصنف Car فيمكننا إنشاء مصفوفة من العناصر التي يقبل كل عنصر منها أن يخزّن مرجع لكائن من الصنف Car وذلك على الشكل التالي: Car[] arrCars = new Car[5]; تُنشئ العبارة السابقة المصفوفة arrCars والتي تحوي 5 عناصر يمكنها تخزين مراجع لكائنات من الصنف Car. استخدام المصفوفات مع أنواع مضمنة لكلّ عنصر في مصفوفة دليل index، ويُعبّر عن ترتيب هذا العنصر ضمن المصفوفة. يبدأ ترقيم الأدلّة في أيّ مصفوفة بالصفر. أي أنّ دليل العنصر الأوّل هو الصفر. فالمصفوفة arrCars التي صرّحنا عنها قبل قليل تحتوي على خمسة عناصر، دليل العنصر الأوّل هو 0، أمّا دليل العنصر الأخير فهو 4 كما هو واضح. يمكن المرور على عناصر أيّ مصفوفة باستخدام الدليل. فمثلًا يمكننا الوصول إلى العنصر الثاني في المصفوفة arrCars عن طريق كتابة [arrCars[1. يطلب البرنامج Lesson09_01 التالي من المستخدم إدخال درجات 5 طلاب في إحدى المواد الدراسيّة ومن ثمّ يحسب معدّل هؤلاء الطلبة في هذه المادّة، على افتراض أنّ الدرجة العظمى هي 100. ومن ثمّ يطبع المعدّل مع أسماء الطلاب ودرجاتهم على الشاشة: 1 using System; 2 3 namespace Lesson09_01 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int[] arrMarks = new int[5]; 10 string[] arrNames = new string[5]; 11 int sum = 0; 12 13 Console.WriteLine("Input Students Marks"); 14 Console.WriteLine("====================="); 15 16 //input loop. 17 for(int i = 0; i < arrMarks.Length; i++) 18 { 19 Console.Write("Input student {0} th name: ", i + 1); 20 arrNames[i] = Console.ReadLine(); 21 22 Console.Write("Input student {0} th mark: ", i + 1); 23 string tmpMark = Console.ReadLine(); 24 arrMarks[i] = int.Parse(tmpMark); 25 26 Console.WriteLine(); 27 } 28 29 Console.WriteLine(); 30 Console.WriteLine("Students Marks Table"); 31 Console.WriteLine("===================="); 32 Console.WriteLine("No\tName\tMark"); 33 34 //calculating sum and display output loop. 35 for (int i = 0; i < arrMarks.Length; i++) 36 { 37 sum += arrMarks[i]; 38 Console.WriteLine("{0}\t{1}\t{2}", i + 1, arrNames[i], arrMarks[i]); 39 } 40 41 Console.WriteLine("-------------------"); 42 Console.WriteLine("Sum\t\t{0}", sum); 43 Console.WriteLine("Average\t\t{0}", sum/(double)arrMarks.Length); 44 Console.WriteLine(); 45 } 46 } 47 } يبدأ البرنامج السابق بالتصريح عن المصفوفتين arrMarks لتخزين علامات الطلّاب و arrNames لتخزين أسمائهم. كما يصرّح عن المتغيّر sum لتخزين مجموع الدرجات. يعرض البرنامج عبارتين توضيحيّتين في السطرين 13 و 14، ثمّ تبدأ حلقة for في السطر 17 بجمع أسماء ودرجات الطلّاب في هذه المادّة. لاحظ كيف أنّنا وضعنا شرط استمرار الحلقة i < arrMarks.Length (السطر 17). تعطينا الخاصيّة Length للمصفوفة arrMarks عدد العناصر ضمن هذه المصفوفة (عددها 5 في مثالنا). سيضمن ذلك تنفيذ حلقة for لخمسة مرّات فقط. نبدأ اعتبارًا من السطر 29 بالتجهيز لعرض النتائج، حيث سنظهرها على شكل جدول يضم ثلاثة أعمدة الرقم المتسلسل للطالب No والاسم Name والدرجة Mark. يطبع السطر 32 ترويسة هذا الجدول من خلال النص "No\tName\tMark" نستخدم المحرف t\ في النص السابق لترك مسافة جدولة tab تفصل بين كل عمودين. يدخل البرنامج بعد ذلك إلى حلقة إظهار النتائج اعتبارًا من السطر 35. لاحظ النص التنسيقيّ "{0}\t{1}\t{2}" في السطر 38، وظيفته أيضًا ترك مسافة جدولة بين كل عمودين. بعد الانتهاء من الحلقة نُظهر المجموع Sum والمعدّل Average بقسمة المجموع Sum على عدد الطلاب. أمر أخير تجدر ملاحظته، في السطر 43 عند حساب المعدّل استخدمنا التعبير التالي: sum/(double)arrMarks.Length، ويعود سبب وجود عامل التحويل (double) أمام arrMarks.Length إلى جعل القسمة تجري بين قيمة من نوع int (قيمة sum) وقيمة من نوع double لكي يصبح الناتج من نوع double. لأنّه بدون عامل التحويل هذا، ستجري عمليّة القسمة بين قيمتين من نوع int (الخاصيّة Length هي من نوع int) وبالتالي سيكون الناتج من نوع int وتُهمل أي أجزاء عشريّة وهذا أمر غير مرغوب. لقد نفّذت البرنامج وقمت بتزويده ببعض البيانات، وحصلت عل الخرج التالي: استخدام المصفوفات مع أنواع من إنشاءنا لا تختلف طريقة التعامل مع المصفوفات عناصرها من أنواع مضمّنة مع مصفوفات عناصرها من أصناف موجودة في مكتبة FCL أو حتى من أصناف ننشئها نحن، باستثناء أمرٍ مهمٍ واحد سنتعرّض له. سننشئ لهذا الغرض صنف جديد اسمه Student، يحتوي هذا الصنف على خاصّتين: الاسم Name والدرجة Mark. سنصرّح بعد ذلك عن المتغيّر arrStudents ليكون مصفوفة من النوع []Student. سيسلك هذا البرنامج نفس سلوك البرنامج Lesson09_01 تمامًا، أي سيطلب درجات خمسة طلاب ليعرضهم ويحسب مجموع درجاتهم ومعدّلهم. انظر البرنامج Lesson09_02: 1 using System; 2 3 namespace Lesson09_02 4 { 5 class Student 6 { 7 public string Name { get; set; } 8 public int Mark { get; set; } 9 } 10 11 class Program 12 { 13 static void Main(string[] args) 14 { 15 Student[] arrStudents = new Student[5]; 16 int sum = 0; 17 18 Console.WriteLine("Input Students Marks"); 19 Console.WriteLine("====================="); 20 21 //input loop. 22 for (int i = 0; i < arrStudents.Length; i++) 23 { 24 arrStudents[i] = new Student(); 25 26 Console.Write("Input student {0} th name: ", i + 1); 27 arrStudents[i].Name = Console.ReadLine(); 28 29 Console.Write("Input student {0} th mark: ", i + 1); 30 string tmpMark = Console.ReadLine(); 31 arrStudents[i].Mark = int.Parse(tmpMark); 32 33 Console.WriteLine(); 34 } 35 36 Console.WriteLine(); 37 Console.WriteLine("Students Marks Table"); 38 Console.WriteLine("===================="); 39 Console.WriteLine("No\tName\tMark"); 40 41 //calculating sum and display output loop. 42 for (int i = 0; i < arrStudents.Length; i++) 43 { 44 sum += arrStudents[i].Mark; 45 Console.WriteLine("{0}\t{1}\t{2}", i + 1, arrStudents[i].Name, arrStudents[i].Mark); 46 } 47 48 Console.WriteLine("-------------------"); 49 Console.WriteLine("Sum\t\t{0}", sum); 50 Console.WriteLine("Average\t\t{0}", sum / (double)arrStudents.Length); 51 Console.WriteLine(); 52 } 53 } 54 } كلّ من البرنامجين Lesson09_01 و Lesson09_02 متشابهان من حيث الخرج. ولكن يتعامل البرنامج Lesson09_02 مع المصفوفة arrStudents التي عناصرها من النوع Student. الصنف Student مصرّح عنه في الأسطر من 5 إلى 9. والمصفوفة arrStudents مصرّح عنها في السطر 15. يُعتبر السطر 24 مهمًا جدًا وفيه يتم إنشاء كائن جديد من الصنف Student وإسناده إلى كل عنصر من عناصر المصفوفة arrStudents في كل دورة من دورات حلقة for. إذا حاولت إزالة عبارة إنشاء الكائن من الصنف Student في السطر 24 فسيعمل البرنامج ولكنّه سيتوقّف عن التنفيذ ويصدر خطأ عندما يصل التنفيذ إلى السطر 27. لأنّه عندما نصرّح عن مصفوفة عناصرها من أنواع ليست مضمّنة، فنحن في الحقيقة نصرّح عن متغيّرات فقط دون إنشاء كائنات ضمن هذه العناصر (المتغيّرات). وبالتالي لا يحق لنا الوصول إلى أعضاء كائن غير مُنشَأ أصلًا، فالتعبير arrStudents.Name سيولّد خطأً مالم يكن هناك كائن فعلي ضمن العنصر [arrStudents[i (أي عنصر المصفوفة ذو الدليل i). حلقة foreach التكرارية حلقة foreach من الحلقات التكراريّة المفيدة والتي تتسم بأسلوب عمل أقرب إلى المألوف. يمكن استخدام حلقة foreach على المصفوفات والمجموعات كما سنرى لاحقًا. طريقة استخدام foreach بسيطة سنتناولها من خلال البرنامج Lesson09_03 التالي: 1 using System; 2 3 namespace Lesson09_03 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int[] arrNumbers = new int[10]; 10 Random random = new Random(); 11 12 for(int i = 0; i < arrNumbers.Length; i++) 13 { 14 arrNumbers[i] = random.Next(100); 15 } 16 17 foreach(int n in arrNumbers) 18 { 19 Console.WriteLine(n); 20 } 21 } 22 } 23 } ملاحظة: البرنامج السابق بسيط، حيث يصرّح عن المصفوفة arrNumbers بعشرة عناصر، ويعمل على تعبئة عناصرها بقيم عشوائيّة يحصل عليها من كائن من النوع Random. صرّحنا عن المتغيّر random وأسندنا إليه مرجع لكائن من الصنف Random وذلك في السطر 10. في حلقة for (الأسطر من 12 إلى 15) قمنا بتوليد أرقام عشوائيّة عن الطريق التابع (Next(100 من الكائن random والذي يولّد أرقامًا عشوائيّة صحيحة غير سالبة أقل من 100. بعد ذلك سنطبع هذه الأرقام العشوائيّة على الشاشة عن طريق حلقة foreach. مبدأ هذه الحلقة بسيط، منطق عملها يتلخّص على النحو التالي: "من أجل كل عنصر n موجود ضمن المصفوفة arrNumbers نفّذ الشيفرة الموجودة في حاضنة foreach". في مثالنا هذا تحتوي الحاضنة على عبارة برمجيّة واحدة (السطر 19). في البرنامج Lesson09_03 السابق المتغيّر n المصرّح عنه في السطر 17 والذي سيحمل قيمة مختلفة من عناصر المصفوفة arrNumbers في كل دورة للحلقة، هو متغيّر للقراءة فقط read-only لا يمكن تغيير قيمته ضمن حاضنة foreach. تمارين داعمة تمرين 1 اكتب برنامجًا يعرّف مصفوفة من نوع int بعشرة عناصر. ثم يطلب من المستخدم إدخال قيم لهذه العناصر. بعد ذلك يعمل البرنامج على ترتيب عناصر هذه المصفوفة تصاعديًّا. تلميح: تمتلك سي شارب أساليب جاهزة وسريعة لمثل عمليّات الترتيب هذه، ولكنّنا نريد في هذا المثال التدرُّب على الحلقات والمصفوفات. تمرين 2 عدّل البرنامج Lesson09_02 السابق لكي يسمح للمستخدم بإدخال بيانات عدد كيفي من الطلّاب. تلميح: ستحتاج لأن تطلب من المستخدم إدخال عدد الطلاب المراد إدخال بياناتهم أولًا، ومن ثمّ تصرّح عن المصفوفة arrStudents بالعدد المطلوب. الخلاصة تعرّفنا في هذا الدرس على المصفوفات وطرق استخدامها. توجد في الحقيقة مزايا أخرى تتمتّع بها المصفوفات مثل إمكانيّة نسخ مصفوفة إلى مصفوفة أخرى، وغيرها من المزايا الأخرى التي سنتناولها في الدروس والسلاسل القادمة. كما تعرّفنا أيضًا على الحلقة التكراريّة foreach التي يُعتبر أسلوب عملها مألوفًا، وهي مفيدة جدًا عند استخدامها مع المجموعات كما سنرى لاحقًا.
  6. تشبه البنية structure الصنف class إلى حدٍّ كبير باستثناء أنّها تُعتبر نوع قيمة value type وليس نوعًا مرجعيًّا كما هو الحال بالنسبة للأصناف. أمّا بالنسبة للمعدودة enum فهي تقنيّة تسمح لنا بتعيين مجموعة من الثوابت الرقميّة التي يمكن تسميّتها، وينحصر دورها في تنظيم البرنامج وجعل الشيفرة أكثر قابليّة للفهم والتعديل. البنى Structures تستطيع اعتبار البنية على أنّها نسخة مخفّفة من الصنف class. يكمن الفرق الأساسي بينهما في أنّ البنية هي نوع قيمة كما ذكرنا، فعند إنشاء بنية سيتم حجز المكان المخصّص لها في المكدّس stack وليس في الكومة heap كما هو الحال مع الأنواع المرجعيّة reference types. ستُنشئ الشيفرة التالية بنية اسمها Student على الشكل التالي: struct Student { public string FirstName { get; set; } public string LastName { get; set; } public int Mark { get; set; } public override string ToString() { return string.Format("{0} {1} ({2})",FirstName,LastName,Mark); } } لاحظ مدى الشبه بينها وبين الأصناف. الآن إذا أردنا استخدام هذه البنية في الشيفرة يمكننا ذلك من خلال العبارة التالية: Student student = new Student(); علينا أن نتذكّر دائمًا أنّ الكائن الذي يقوم العامل new بإنشائه سيكون في المكدّس stack. يمكننا أن نتابع ونستخدم المتغيّر student من نوع البنية Student كما كنّا نفعل من قبل مع متغيّرات الأصناف: student.FirstName = "Maher"; student.LastName = "Rajab"; student.Mark = 100; Console.WriteLine(student.ToString()); بعد تنفيذ الشيفرة السابقة ستحصل على الخرج: Maher Rajab (100) توجد الكثير من البنى المهمّة والمفيدة. فمثلًا هناك البنية DateTime التي تسمح لنا بالتعامل مع الزمن والتاريخ، وهي بنية مهمّة للغاية. انظر إلى البرنامج Lesson17_01 الذي يوضّح بعض المزايا التي توفّرها. 1 using System; 2 using System.Globalization; 3 4 namespace Lesson17_01 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 DateTime myBirthDate = new DateTime(1979, 12, 25); 11 12 //display birth date in simple form: 13 Console.WriteLine(myBirthDate.ToString("dd/MM/yyyy")); 14 15 //display birth date in more elegent way: 16 Console.WriteLine(myBirthDate.ToString("D", CultureInfo.InvariantCulture)); 17 18 //add time info: 19 myBirthDate = myBirthDate.AddHours(4); 20 myBirthDate = myBirthDate.AddMinutes(30); 21 22 //display completed birth date info: 23 Console.WriteLine(myBirthDate.ToString("dddd, dd MMMM yyyy - hh:mm:ss tt", CultureInfo.InvariantCulture)); 24 } 25 } 26 } بعد تنفيذ البرنامج السابق ستحصل على الخرج التالي: أنشأنا في السطر 10 كائنًا جديدًا من البنية DateTime حيث مرّرنا ثلاثة وسائط للبانية هي على الترتيب من اليسار إلى اليمين: العام والشهر واليوم (الميلادي). في الحقيقة تمتلك بانية البنية DateTime اثنا عشرة شكلًا مختلفًا، نستطيع من خلال بعض هذه الأشكال ضبط الوقت أيضًا. يؤدّي الاستدعاء في السطر 13: myBirthDate.ToString("dd/MM/yyyy") إلى الحصول على تمثيل نصّي مختصر من التاريخ الذي مرّرناه إلى البانية قبل قليل. حيث ترمز dd إلى اليوم، و MM (حرف كبير) إلى الشهر، وyyyy إلى العام، ويمكنك بالتأكيد تغيير هذا الترتيب إذا أحببت (انظر إلى السطر الأوّل من الخرج في الشكل السابق). أمّا الاستدعاء في السطر 16: myBirthDate.ToString("D", CultureInfo.InvariantCulture) فيؤدّي إلى الحصول على نفس التاريخ ولكن بشكل أنيق. حيث سنحصل على القيم الكتابيّة للتاريخ (انظر السطر الثاني من الخرج في الشكل السابق). ولكن لاحظ أنّنا نمرّر إلى التابع ToString وسيطين: الأوّل هو النص التنسيقي "D" الذي يُخبر التابع ToString أنّنا نريد التنسيق على شكل تاريخ ولكن بصورة تفصيليّة أنيقة، أمّا الوسيط الثاني CultureInfo.InvariantCulture فهو يُرجع كائن من النوع CultureInfo. الهدف منه هو اعتماد الإعدادات الإقليميّة العالميّة في تنسيق التاريخ. وسبب ذلك أنّ كل بلد أو ثقافة culture لها أسلوب معيّن في تنسيق التاريخ (وكذلك الأمر بالنسبة للأرقام والعملات). لقد اخترت التنسيق العالميّ InvariantCulture لأنّني لا أريد من البرنامج استخدام الإعدادات الإقليميّة الخاصّة بنظام التشغيل في حاسوبي الشخصي الذي يعمل عليه البرنامج، لأنّ الخرج سيظهر مشوّهًا نتيجة لذلك. يكمن السبب في أنّ نافذة موجّه الأوامر console window لا تدعم اللغة العربية بشكل افتراضيّ، لذلك آثرت استخدام الإعدادات العالميّة (تشبه إلى حدٍّ كبير الإعدادات الخاصّة بالولايات المتحدّة). أخيرًا الاستدعاء في السطر 23: Console.WriteLine(myBirthDate.ToString("dddd, dd MMMM yyyy - hh:mm:ss tt", CultureInfo.InvariantCulture)); سيؤدّي هذا الاستدعاء إلى الحصول على النص الذي يظهر في السطر الثالث من الخرج، وهو مزيج بين التاريخ والوقت. لاحظ أنّنا قد أسبقنا هذا الاستدعاء باستدعاءين في السطرين 19 و20 للتابعين AddHours وAddMinutes على الترتيب. أتصوّر أنّ وظيفتهما واضحة، وهي إضافة قيمة ساعات ودقائق محدّدة إلى المتغيّر myBirthDate، مع الانتباه إلى أنّ كل منهما ستُرجع كائن DateTime جديد يمثّل التعديل الذي حصل. أي أنّ المتغيّر myBirthDate الأصلي لن يُمس في كلتا الحالتين. إلّا أنّنا أعدنا إسناد القيم المُعادة إلى المتغيّر myBirthDate لتجنّب إنشاء متغيّرات جديدة كما هو واضح من السطرين 19 و20. بقي أن أخبرك ما يلي: ترمز dddd إلى اليوم ولكن بصيغة كتابيّة. ترمز dd إلى اليوم ولكن بصيغة رقميّة. ترمز MMMM إلى الشهر ولكن بصيغة كتابيّة. ترمز yyyy إلى العام بصيغة رقميّة. ترمز hh إلى الساعات. ترمز mm (أحرف صغيرة) إلى الدقائق. ترمز ss إلى الثواني. ترمز tt إلى AM أو PM. ملاحظة هناك العديد من التوابع الأخرى التي لم نتناولها بالنسبة للبنية DateTime. كما أنّ هناك بنية أخرى اسمها TimeSpan متعلّقة بالبنية DateTime ويمكن من خلالها تمثيل فترة زمنيّة بدلًا من تاريخ محدّد. ملاحظة توجد خاصيّة ضمن البنية DateTime مفيدة كثيرًا وهي الخاصيّة Now. تُرجع هذه الخاصيّة التوقيت الحالي الذي يمثّل التاريخ والوقت الحاليين من ساعة الحاسوب. طريقة استخدامها بسيطة: DateTime current = DateTime.Now; المعدودات Enums المعدودة هي نوع قيمة value type خاص يسمح لنا بتعيين مجموعة من الثوابت الرقميّة ذات تسميّة. انظر مثلًا إلى تصريح المعدودة BorderSide التالي: public enum BorderSide { Left, Right, Top, Bottom } يمكن لهذا التصريح أن يوضع ضمن صنف ما، أو خارج أيّ صنف. عندها من الممكن استخدام هذه المعدودة بالشكل التالي: BorderSide topSide = BorderSide.Top; في الحقيقة إنّ لكلّ ثابت من ثوابت المعدودة (مثل Left أو Right أو غيرها) قيمة رقميّة ضمنيّة. تأخذ هذه الثوابت القيم الرقميّة الافتراضيّة 0، 1، 2، ... الخ بنفس الترتيب الذي تكون فيه هذه الثوابت ضمن تصريح المعدودة. فمثلًا في المعدودة BorderSide السابقة، يأخذ الثابت Left القيمة 0، والثابت Rigth القيمة 1، وهكذا. كما ويمكننا أيضًا أن نسند لثوابت المعدودة قيمًا رقميّة مباشرةً أثناء التصريح عنها. انظر إلى المعدودة BorderSide بشكلها المعدّل: public enum BorderSide { Left = 1, Right = 5, Top = 7, Bottom = 8 } أي ليس من الضروري أن تكون القيم الرقميّة متسلسلة أو تبدأ من الصفر. المهم فقط أن تكون صحيحة (بدون فاصلة عشريّة). يمكن الحصول على القيمة الرقميّة المُسندة إلى أيّ ثابت ضمن المعدودة بالشكل البسيط التالي: int i = (int)BorderSide.Left; بعد تنفيذ العبارة السابقة ستصبح قيمة المتغيّر i تساوي 1. تمارين داعمة تمرين 1 عرّف متغيّرين من البنية DateTime، سمّ الأوّل first وأسند إليه التاريخ 01/01/2016، وسمّ الثاني second وأسند إليه التاريخ 13/05/2016. وبعد ذلك نفّذ العبارة التالية وحاول تفسير الخرج: Console.WriteLine(second - first); تمرين 2 أنشئ برنامجًا يستخدم معدودةً اسمها LevelEnum تحتوي على الثوابت: Weak، Middle، Good، VeryGood. أضف هذه المعدودة إلى الصنف Student التالي: class Student { public string FirstName { get; set; } public string LastName { get; set; } public int Mark { get; set; } } ثمّ أضف لنفس الصنف السابق الخاصيّة Level من نوع المعدودة LevelEnum، حيث تُعبّر هذه الخاصيّة عن مستوى الطالب Student. الخلاصة تعرّفنا في هذا الدرس على البنى Structures والمعدودات Enums وكيفيّة استخدامهما. ستصادف في برامجك المستقبليّة هاتين التقنيّتين بشكل متكرّر. فإمّا أن تنشئهما بنفسك أو أن تفرض عليك أصناف دوت نت أو غيرها استخدامهما. تُعتبر البنى خفيفة، حيث أنّها سريعة في الإنشاء والمعالجة أكثر من الأصناف، كونها تتوضّح في مكدّس البرنامج كما أسلفنا. وتُعتبر المعدودات أداةً رائعة لتنظيم برامجك في حال كانت تتطلّب تعريف ثوابت عدديّة ضمن مجموعات منطقيّة.
  7. لا تخلو أيّ لغة برمجة محترمة من وسيلة لمعالجة الأخطاء. تحتوي لغة سي شارب على آليّة قويّة لمعالجة الأخطاء. تشبه هذه الآلية إلى حدّ ما تلك المستخدمة في لغة Java. هناك نوعان أساسيّان من الأخطاء التي قد تنتج عن البرنامج: النوع الأوّل هي أخطاء أثناء التصميم، أي أثناء كتابة الشيفرة، وهي أخطاء يمكن اكتشافها بسهولة من خلال مترجم سي شارب الذي يتعرّف عليها ويعطيك وصفًا عنها، وقد يزوّدك أيضًا ببعض المقترحات للتخلّص منها. بالنسبة للنوع الثاني من الأخطاء فهي التي تنتج أثناء تنفيذ البرنامج runtime errors. توجد الكثير من الأسباب لحدوث مثل هذه الأخطاء. فمن القسمة على صفر إلى القراءة من ملف غير موجود أو حتى استخدام متغيّر مصرّح على أنّه من نوع مرجعيّ ولكن لم تتم تهيئته بعد بمرجع إلى كائن. عند حدوث مثل هذه الأخطاء ترمي بيئة التنفيذ المشتركة CLR استثناءً exception يحتوي على معلومات بخصوص الخطأ الذي حدث، فإذا كنت مستعدًّا لالتقاط هذا الاستثناء فستنجو، وإلّا سيتوقّف برنامجك عن العمل بصورة مفاجئة. التقاط استثناء من خلال عبارة try-catch إذا صادفتك عبارة برمجيّة تحتوي على عمليّة حسابيّة أو على استدعاء لتابع آخر، أو أيّ شيء قد يثير الريبة في نفسك، فمن الممكن مراقبتها أثناء تنفيذ البرنامج باستخدام عبارة try-catch. تتألّف هذه العبارة من قسمين: القسم الأوّل هو قسم المراقبة try، والقسم الثاني هو قسم الالتقاط catch. الشكل "الأبسط" لهذه العبارة هو التالي: try { //عبارة برمجيّة مريبة } catch(Exception exp) { //هنا يُلتقط الاستثناء وتتمّ معالجته } يمكن أن يحوي قسم try على عبارات برمجيّة بقدر ما ترغب. عندما يصادف البرنامج أثناء التنفيذ خطأً ما، سيتوقّف التنفيذ عند العبارة التي سبّبت الخطأ، ثم ينتقل فورًا إلى قسم الالتقاط catch. لاحظ معي أنّ قسم catch سيُمرَّر إليه وسيط من الصنف Exception. الصنف Exception موجود ضمن نطاق الاسم System، وهو الصنف الأب لجميع الاستثناءات. إذ أنّ أي استثناء مهما كانت صفته (بما فيها الاستثناءات التي يمكنك أن تكتبها أنت) يجب أن ترث من هذا الصنف. بعد حدوث الاستثناء والانتقال إلى قسم catch، سيحتوي الوسيط exp على معلومات حول مكان حدوث الاستثناء وسبب حدوثه، وغيرها من المعلومات التي قد تكون مفيدة لمستخدم البرنامج. تجدر الملاحظة بأنّ البرنامج لن يدخل إلى القسم catch أبدًا ما لم يحدث استثناء ضمن القسم try. لنرى الآن البرنامج Lesson16_01 الذي سيعرّفنا على الاستثناءات بشكل عمليّ. يحتوي هذا البرنامج البسيط على عبارة try-catch وحيدة سنعمل من خلالها على توليد خطأ بشكل مقصود أثناء التنفيذ، وسنتعلّم كيفيّة المعالجة. 1 using System; 2 3 namespace Lesson16_01 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int x = 5; 10 int y = 0; 11 int result; 12 13 try 14 { 15 result = x / y; 16 } 17 catch(Exception exp) 18 { 19 Console.WriteLine("The following error has occurred:"); 20 Console.WriteLine(exp.Message); 21 } 22 23 Console.WriteLine("Good Bye!"); 24 } 25 } 26 } من الواضح أنّ هذا البرنامج يتجّه لأن يجري عمليّة قسمة على صفر في السطر 15 ضمن القسم try. عند وصول البرنامج إلى السطر 15 وإجراء عمليّة القسمة هذه، سيتولّد استثناء يؤدّي إلى انتقال التنفيذ مباشرةً إلى قسم catch في السطر 17. في قسم catch يعرض البرنامج معلومات عن هذا الخطأ باستخدام الخاصيّة Message لكائن الحدث exp. في النهاية يعرض البرنامج في السطر 23 رسالة توديعيّة للمستخدم. نفّذ البرنامج لتحصل على الخرج التالي: The following error has occurred: Attempted to divide by zero. Good Bye! جرّب الآن تغيير قيمة المتغيّر y لتصبح 1 مثلًا وأعد تنفيذ البرنامج. ستلاحظ ظهور الرسالة التوديعيّة فقط على الشاشة، أي أنّه لم يحدث أي استثناء هذه المرّة. على كلّ الأحوال لا ينصح باستخدام معالجة الاستثناءات من أجل حالة القسمة على صفر في البرنامج السابق. ملاحظة: في الواقع يمكن الاستغناء عن الوسيط الذي يمرّر إلى قسم catch بشكل كامل، وفي هذه الحالة لن يكون بإمكانك الحصول على معلومات حول الاستثناء المُلتقط. سيبدو شكل عبارة try-catch على الشكل التالي: try { //عبارة برمجيّة مريبة } catch { //هنا يُلتقط الاستثناء وتتمّ معالجته } من الممكن استخدام هذا الأسلوب إذا كنّا على يقين حول طبيعة الخطأ الذي سيحدث. عبارة try-catch أكثر تطورا تصادفنا في بعض الأحيان حالات يكون من الضروري معها مراقبة أكثر من عبارة برمجيّة مريبة ضمن قسم try. لقد اتفقنا أنّه عند حدوث أيّ استثناء ضمن قسم try سينتقل التنفيذ إلى قسم catch. ولكن كيف سنميّز العبارة التي سبّبت هذا الاستثناء في قسم try؟ توجد العديد من الأصناف التي ترث من الصنف Exception والتي يُعتبر كلّ منها استثناءً مخصّصًا أكثر للمشكلة التي قد تحدث. فمثًلا كان من الممكن في البرنامج Lesson16_01 السابق أن نستخدم الصنف DivideByZeroException بدلًا من الصنف Exception في عبارة catch، وذلك لأنّه يرث (بشكل غير مباشر) من الصنف Exception، وسيعمل البرنامج كما هو متوقّع. ولكن في هذه الحالة لن تستطيع catch التقاط سوى الاستثناءات التي تنتج عن القسمة على صفر. في كثير من الحالات قد تتسبّب العبارات البرمجيّة الموجودة في قسم try باستثناءات متنوّعة لا توجد علاقة فيما بينها. مما يفرض علينا استخدام الصنف Exception لكي نلتقط بشكل مؤكّد أي استثناء قد يصدر عنها، أو أن تساعدنا سي شارب في هذا الخصوص! في الحقيقة الخيار الثاني هو الأفضل وهو جاهز. يمكننا في الواقع إضافة أقسام catch أخرى بقدر ما نرغب بعد قسم try. سيوضّح البرنامج Lesson16_02 هذه الفكرة من خلال فتح ملف نصي ومحاولة قراءة محتوياته. لاحظ أنّنا سنستخدم هنا نطاق الاسم System.IO. 1 using System; 2 using System.IO; 3 4 namespace Lesson16_02 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string contents; 11 12 Try 13 { 14 using (StreamReader sr = new StreamReader("myfile.txt")) 15 { 16 contents = sr.ReadLine(); 17 } 18 19 Console.WriteLine("This file contains {0} characters.", contents.Length); 20 } 21 catch(NullReferenceException nullExp) 22 { 23 Console.WriteLine("The file does not contain any data."); 24 } 25 catch(FileNotFoundException notFoundExp) 26 { 27 Console.WriteLine("File: {0} not found!", notFoundExp.FileName); 28 } 29 } 30 } 31 } يستخدم هذا البرنامج قسميّ catch. القسم الأوّل (الأسطر من 21 إلى 24) يلتقط استثناءً من النوع NullReferenceException وهذا يحدث عند محاولة استدعاء تابع أو خاصيّة من متغيّر يحتوي على null بدلًا من مرجع لكائن حقيقي. أمّا القسم الثاني (الأسطر من 25 إلى 28) فهو يلتقط استثناءً من النوع FileNotFoundException والذي يحدث عند محاولة القراءة من ملف غير موجود. نفّذ البرنامج السابق وستحصل على الرسالة التالية في الخرج: File: C:\\Users\Husam\documents\visual studio 2015\Projects\Lesson16_02\Lesson16_02\bin\Debug\myfile.txt not found! وهذا طبيعي تمامًا لأنّني لم أنشئ الملف myFile.txt في هذا المسار. لاحظ كيف يضيف الصنف FileNotFoundException خاصيّة جديدة له وهي FileName (السطر 27) من النوع string التي تحوي مسار الملف مع اسمه. أنشئ الآن الملف myFile.txt واتركه فارغًا، ثمّ ضعه ضمن نفس المجلّد الذي يحوي الملف التنفيذي للبرنامج (موجود ضمن bin\Debug\). أعد تنفيذ البرنامج وستحصل على الرسالة التالي: The file does not contain any data. السبب في ظهور هذه الرسالة هو الاستثناء NullReferenceException وذلك لأنّنا حاولنا الوصول إلى الخاصيّة Length (تعطينا عدد المحارف الموجودة ضمن متغيّر نصي) من المتغيّر النصي contents رغم أنّه يحتوي على null (تذكّر بأنّنا تركنا الملف myFile.txt فارغًا). اذهب إلى الملف myFile.txt الذي أنشأناه قبل قليل، واكتب بعض الكلمات ضمنه واحفظ الملف، ثمّ أعد تنفيذ البرنامج Lesson16_02 من جديد. يجب الآن أن تحصل على رسالة تخبرك بعدد الأحرف التي كتبتها ضمن الملف. ملاحظة: يجب الانتباه إلى ترتيب أقسام catch. فلو كان مثلًا أوّل قسم catch موجود بعد قسم try يلتقط استثناءً من النوع Exception فعندها لن يستطيع أي قسم لاحق التقاط أي استثناء، لأنّ جميع الاستثناءات سيلتقطها هذا القسم الأوّل. السبب في ذلك أنّ الصنف Exception هو الأب العام لجميع أصناف الاستثناءات الأخرى، فيمكن له التقاطها. عبارة try-catch-final يمكن إضافة قسم أخير لعبارة try-catch اسمه final. وكما يوحي اسمه، فهذا القسم يمكن له أن يحتوي على عبارات برمجيّة سيتمّ تنفيذها بعد أن يدخل البرنامج إلى القسم try العائد له. وذلك سواءً أحدث استثناء ضمن try أم لم يحدث. تكمن فائدة وجود هذا القسم، في أنّه قد نواجه أحيانًا بعض الحالات التي تتطلّب إجراء بعض المهام عندما نفرغ من قسم try مثل إغلاق بعض المصادر المفتوحة، أو تحرير الذاكرة بشكل فوري وغيرها. الشرط الوحيد لاستخدام هذا القسم الاختياري هو أن يكون آخر قسم في عبارة try-catch. سنعدّل البرنامج Lesson16_02 ليدعم القسم final. انظر البرنامج Lesson16_03. 1 using System; 2 using System.IO; 3 4 namespace Lesson16_03 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string contents; 11 12 try 13 { 14 using (StreamReader sr = new StreamReader("myfile.txt")) 15 { 16 contents = sr.ReadLine(); 17 } 18 Console.WriteLine("This file contains {0} characters.", contents.Length); 19 } 20 catch(NullReferenceException nullExp) 21 { 22 Console.WriteLine("The file does not contain any data."); 23 } 24 catch(FileNotFoundException notFoundExp) 25 { 26 Console.WriteLine("File: {0} not found!", notFoundExp.FileName); 27 } 28 finally 29 { 30 Console.WriteLine("Good Bye!"); 31 } 32 } 33 } 34 } سواءً كان الملف myFile.txt موجودًا أم غير موجود، أو كان يحتوي على بيانات أم فارغاً، ستظهر العبارة !Good Bye على الشاشة. ملاحظة: من الأفضل دومًا أن تحاول عدم استخدام عبارة try-catch وأن تستخدم عبارة if لاختبار الحالات التي تواجهك قبل تنفيذها. استخدم try-catch إذا كان ذلك ضروريًّا. والسبب في ذلك أنّ عمليّة معالجة الأخطاء بشكل عام تتطلّب المزيد من الموارد المخصّصة للبرنامج، مما قد يؤثّر على أداء البرنامج في حال تمّ استخدامها بشكل غير مدروس. تمارين داعمة تمرين 1 عدّل البرنامج Lesson16_02 بحيث يمكن الاستغناء عن عبارة try-catch تمامًا. (تلميح: ستحتاج إلى استخدام عبارتيّ if في هذه الحالة). تمرين 2 لتكن لدينا الشيفرة التالية: string input = Console.ReadLine(); int t = int.Parse(input); تطلب الشيفرة السابقة من المستخدم أن يدخل عددًا على شكل نص لتعمل على تحويله إلى قيمة عدديّة باستخدام التابع int.Parse. المطلوب هو إضافة عبارة try-catch إلى الشيفرة السابقة لمعالجة استثناء ممكن الحدوث في حال أدخل المستخدم قيمة مثل "u" وهي لا يمكن تحويلها إلى قيمة عدديّة كما هو واضح. (تلميح: استخدام الاستثناء FormatException الذي يُعبّر عن هذه الحالة). الخلاصة تعرّفنا في هذا الدرس على كيفيّة معالجة الأخطاء التي تظهر أثناء تنفيذ البرنامج. في الحقيقة هذا الموضوع ذو شجون! وهناك الكثير ليقال، ولكن يكفي الآن أن تتعرّف على المبادئ الأساسيّة لالتقاط الاستثناءات، وكيفيّة معالجتها. يمكنك استخدام هذا الأسلوب في جميع التطبيقات التي تُنشئها باستخدام سي شارب، مثل تطبيقات الويب بأنواعها، وتطبيقات سطح المكتب، وحتى تطبيقات الأجهزة الذكيّة باستخدام تقنيّة Xamarin.
  8. المتغيرات Variables سبق وأن ذكرنا في الدرس الأوّل أنّه يوجد نمطان أساسيّان لأنواع المتغيّرات في سي شارب، وهما: أنواع قيمة value types وأنواع مرجعيّة reference types. تشتمل أنواع القيمة على الأنواع المُدمجة built-in في اللغة مثل int و float و decimal و double و bool وجميع الأنواع المُعرّفة كبنية struct. سنتحدّث عن البنى في درس لاحق. في حين تشتمل الأنواع المرجعيّة على أيّ نوع آخر وهذا يشتمل على عدد لا يحصى من الأنواع، فيكفيك أن تعرف مثلًا أنّ جميع الأنواع الموجودة في مكتبة FCL هي أنواع مرجعيّة، بالإضافة إلى أنّ أي نوع جديد (على شكل صنف class) ينشئه المستخدم يُعتبر نوعًا مرجعيًّا. ومن المفيد أن تعلم أنّ النوع المُضمّن string هو نوع مرجعيّ أيضًا. يكمن الفرق الأساسي بين أنواع القيمة والأنواع المرجعيّة في مكان وطريقة تخزين قيم المتغيّرات المصرّح عنها بواسطتها. فعند التصريح عن متغيّر من نوع قيمة، تُخزّن أي قيمة يتمّ إسنادها إليه ضمن المتغيّر نفسه أو بمعنى أدق تُخزّن في ذاكرة المُكدّس Stack Memory، أمّا المتغيّر المصرّح عنه على أنّه نوع مرجعيّ فالّذي يُخزّن ضمنه هو العنوان إلى موقع في الذاكرة. هذا الموقع موجود ضمن ما يُسمّى بذاكرة الكَوْمَة Heap Memory. انظر إلى الشكل التوضيحي التالي. حيث صرّحنا عن المتغيّر x من النوع int وخزّنّا القيمة 5 ضمنه. وبما أنّ int هو نوع قيمة، لذلك سيكون المتغيّر مع القيمة المخزّنة فيه ضمن المكدّس Stack. أمّا بالنسبة للمتغيّر s فهو من النوع string وقد أسندنا إليه النص "!Hello" وبما أنّ النوع string هو نوع مرجعيّ كما أسلفنا لذلك فالقيمة التي ستكون مخزّنة ضمن المتغيّر s في الحقيقة ليست النص "!Hello" إنّما العنوان address الذي يُشير إلى موقع ضمن الكومة Heap موجود ضمنه النص "!Hello"، وهذا بالمناسبة ليس سلوكًا تنفرد به سي شارب، بل هو موجود في لغات أخرى مثل ++C و C. سنتوسّع في هذا الموضوع قليلًا عندما نتحدّث عن الأصناف والكائنات لاحقًا في هذه السلسلة. يمكن استخدام أيّ مزيج من الحروف والأرقام عند تسمية المتغيّرات، بشرط أن يكون أوّل محرف في اسم المتغيّر حرفًا وليس رقمًا. كما لا يجوز أن يحتوي اسم المتغيّر على فراغات ولا يجوز أيضًا أن يكون مماثلًا لكلمة محجوزة في سي شارب مثل new أو class أو غيرها، ولا يجوز أن يحتوي على رموزًا خاصّة مثل & و$ و#، ولكن يُعتبر الرمز (_) underscore حرفًا ويجوز الابتداء به. الأنواع المضمنة ومجالاتها الأنواع المُضمّنة هي الأنواع الموجودة ضمنًا في لغة سي شارب وفي إطار عمل دوت نت عمومًا. سنستعرض في الجدول التالي هذه الأنواع. لاحظ أنّ عمود "الاسم" يحتوي على أسماء الأنواع المستخدمة في سي شارب، في حين يحتوي العمود الذي يليه مباشرةً على اسم نفس النوع ولكن ضمن منصّة دوت نت. في الحقيقة يمكننا استخدام أي تسمية نرغبها ولكنّ الأسهل والأفضل هي في استخدام أسماء الأنواع في سي شارب. يعود سبب وجود أسماء أنواع مختلفة في إطار عمل دوت نت هو أنّه بإمكان أي لغة برمجة ضمن منصة دوت نت استخدام نفس هذه الأنواع ضمنها. الاسم النوع الموافق في منصّة دوت نت القيم التي يقبلها الحجم في الذاكرة 1 bool System.Boolean true أو false 2 sbyte System.SByte من128- حتى 127 8 bits 3 byte System.Byte من 0 حتى 255 8 bits 4 short System.Int16 من 32,768- حتى 32,767 16 bits 5 ushort System.UInt16 من 0 حتى 65,535 16 bits 6 int System.Int32 من 2,147,483,648- حتى 2,147,483,647 32 bits 7 uint System.UInt32 من 0 حتى 4,294,967,295 32 bits 8 long System.Int64 من 9,223,372,036,854,775,808- حتى 9,223,372,036,854,775,807 64 bits 9 ulong System.UInt64 من 0 حتى 18,446,744,073,709,551,615 64 bits 10 char System.Char من U+0000 حتى U+ffff 16 bits 11 float System.Single من 3.4*1038- حتى +3.4*1038 32 bits 12 double System.Double من ±5.0*10-324 حتى ±1.7*10308 64 bits 13 decimal System.Decimal (-7.9*1028 to 7.9*1028)/(100 to 28) 128 bits 14 string System.String حسب حدود الذاكرة 15 object System.Object يمكن تخزين بيانات من أيّ نوع ضمن المتغيّرات من النوع object الأنواع من 2 حتى 9 هي أنواع صحيحة لا تقبل أعدادًا ذات فاصلة عشريّة. أما الأنواع من 11 حتى 13 فهي أنواع تقبل أعداد ذات فاصلة عشريّة وتختلف فيما بينها في مجالات الأعداد التي تقبلها ودقّة تلك الأعداد بالنسبة لعدد الخانات على يمين الفاصلة العشريّة. النوع char مخصّص للمتغيّرات التي تسمح بتخزين محرف character واحد فقط، وهو نوع يدعم ترميز Unicode، يُعتبر أي محرف موضوع ضمن علامتي اقتباس مفردتين مثل 'a' من نوع char. في الحقيقة أنّ النوع string يُعبّر عن سلسلة من المحارف من نوع char. النوع object هو الأب العام لجميع الأنواع في إطار العمل دوت نت ومنه تنحدر جميع الأنواع الأخرى مهما كانت، سنصادفه في هذه السلسلة مرّةً أخرى. العوامل Operators تدعم سي شارب نوعين من العوامل بشكل أساسيّ: عوامل أحاديّة unary operators وعوامل ثنائيّة binary operators. سنتحدّث في هذا الدرس عن أكثر العوامل استخدامًا في سي شارب. العوامل الأحادية لهذه العوامل أسبقيّة أعلى في الحساب من العوامل الثنائيّة، وهي تشمل العديد من العوامل يلخّص الجدول التالي أهمّها، انظر إلى الأمثلة العمليّة التي ستأتي بعد الجدول لمعرفة كيفيّة استخدامها: العامل الوصف الاستخدام ! عامل النفي المنطقي وهو عامل يُطبّق على القيم المنطقيّة من النوع bool. x! ~ عامل المتمّم الثنائي bitwise complement وهو عبارة عن عامل نفي ولكن على مستوى البتّات bits. x~ ++ لهذا العامل شكلان يعمل كلّ منها على زيادة قيمة متغيّر عددي بمقدار 1، ويختلفان فقط في توقيت هذه الزيادة. x++ عامل زيادة بادئ. ++x عامل زيادة لاحق. -- لهذا العامل شكلان أيضًا، يعمل كلّ منها على إنقاص قيمة متغيّر عددي بمقدار 1، ويختلفان فقط في توقيت هذا الإنقاص. x-- عامل إنقاص بادئ. --x عامل إنقاص لاحق. (T) وهو عامل التحويل بين الأنواع casting. وهو عامل مهم جدًّا سنصادفه مرارًا في هذه السلسلة. يمكن استبدال الحرف T باسم أيّ نوع يخطر على بالك مثل int وdouble وstring وغيرها. طريقة استخدامه هو في وضع النوع المراد التحويل إليه بين قوسين ونضعها جميعًا قبل القيمة التي نريد تحويلها مثل (int(x لتحويل قيمة x إلى قيمة من النوع int. فهم عاملي الزيادة والإنقاص شغّل برنامج Visual Studio 2015 Community وأنشئ مشروعًا جديدًا من النوع Console Application سمّه UnaryOperatorsTest1 ثم استبدل محتويات الملف Program.cs بالشيفرة التالية: 1 using System; 2 3 4 namespace UnaryOperatorsTest 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 int i = 1; 11 12 Console.WriteLine("Using of pre-increment operator (++i):"); 13 Console.WriteLine("Current value of i is {0}, and after applying ++i, the value of i becomes {1}", i, ++i); 14 Console.WriteLine(new string('-', 40)); 15 16 Console.WriteLine(); 17 i = 1; 18 19 Console.WriteLine("Using of post-increment operator (i++):"); 20 Console.WriteLine("Current value of i is {0}, and after applying i++, the value of i becomes {1}", i, i++); 21 Console.WriteLine(new string('-', 40)); 22 } 23 } 24 } نفّذ البرنامج باستخدام Ctrl+F5 (أو من القائمة Debug > Start Without Debugging) ستحصل على الخرج التالي: sing of pre-increment operator (++i): Current value of i is 1, and after applying ++i, the value of i becomes 2 ---------------------------------------- Using of post-increment operator (i++): Current value of i is 1, and after applying i++, the value of i becomes 1 ---------------------------------------- يوضّح هذا البرنامج البسيط استخدام عامل الزيادة البادئ وعامل الزيادة اللاحق. يبدأ البرنامج بالتصريح عن المتغيّر i من النوع int وإسناد القيمة 1 إليه. تعمل العبارة في السطر 13 على إظهار قيمتين، الأولى هي القيمة الحاليّة للمتغيّر i وتساوي 1، والقيمة الثانيّة هي قيمة المتغيّر i مضافًا إليها 1 باستخدام عامل الزيادة البادئ ++i أي هي القيمة 2، إذًا يقوم هذا العامل بزيادة قيمة المتغيّر i بمقدار 1 قبل تمرير القيمة النهائيّة إلى التابع WriteLine لذلك نحصل على الخرج: Current value of i is 1, and after applying ++i, the value of i becomes 2 ولكن على النقيض من ذلك، نلاحظ أنّ العبارة الموجودة في السطر 20 تعمل على إظهار قيمتين أيضًا، الأولى هي القيمة الحالية للمتغيّر i وتساوي 1 (أعدنا إسناد القيمة 1 للمتغيّر i في السطر 17)، والقيمة الثانيّة هي قيمة المتغيّر i مضافًا إليها 1 باستخدام الزيادة اللاحق i++ ولكن لن تمرَّر القيمة 2 هذه المرّة إلى التابع WriteLine. والسبب في ذلك أنّ البرنامج سيعمل على تمرير قيمة i الأصلية (القيمة 1) ثمّ يطبّق بعد ذلك عامل الزيادة اللاحق. وهذا هو سبب الحصول على الخرج التالي: Current value of i is 1, and after applying i++, the value of i becomes 1 لعلّ هذا السلوك يُسبّب بعض الإرباك للمبرمجين الجدد في سي شارب، وعلى أيّة حال أنصح بتجنّب تمرير القيمة إلى التوابع عمومًا بهذا الأسلوب. إذا احتجت لزيادة (أو إنقاص) قيمة متغيّر ما قبل تمرير لأحد التوابع فاعمل على ذلك ضمن سطر منفصل قبل استدعاء هذا التابع وأرح نفسك. في الحقيقة يُطبّق نفس المفهوم السابق بالنسبة لعامليّ الإنقاص البادئ والإنقاص اللاحق. ملاحظة: انظر إلى طريقة التنسيق الجديدة التي استخدمتها في السطر 13: Console.WriteLine("Current value of i is {0}, and after applying ++i, the value of i becomes {1}", i, ++i); مرّرت إلى التابع WriteLine ثلاثة وسائط: الأوّل هو النص التنسيقي وقد حُجز ضمنه مكانين مخصّصين لقيمتين سأمرّرهما لاحقًا لهذا التابع، هذان المكانان على الشكل {0} و {1}. الوسيط الثاني هو المتغيّر i، والوسيط الثالث هو ++i. سيعمل البرنامج على وضع القيمة الممرّرة للتابع WriteLine والتي تلي النص التنسيقي مباشرةً (في حالتنا هذه قيمة i) في المكان {0}، أمّا المكان {1} فسيُوضع ضمنه القيمة التالية وهي ++i. ينطبق نفس الكلام تمامًا على العبارة الموجودة في السطر 20. كما يحتوي السطران 14 و21 على أسلوب جميل لطباعة سطر فاصل في خرج البرنامج بغرض توضيحه. أنشأنا كائنًا من النوع string باستخدام العامل new ومرّرنا لبانيته وسيطين: الأوّل المحرف '-' من نوع char والثاني القيمة 40: new string('-', 40) سيولّد ذلك نصّا يحتوي على 40 محرف '-' مكرّر (لاحظ علامتي الاقتباس المفردتين ' ')، يمرَّر هذا النص بعد ذلك إلى التابع WriteLine. لا تقلق إن بدا هذا الكلام غير مفهومًا الآن، فسنتحدّث عن الكائنات فيما بعد. فهم عامل النفي المنطقي وعامل التحويل بين الأنواع أنشئ مشروعًا جديدًا من النوع Console Application سمّه UnaryOperatorsTest2 ثم استبدل محتويات الملف Program.cs بالشيفرة التالية: 1 using System; 2 3 4 namespace UnaryOperatorsTest2 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 bool b = true; 11 double d = 8.9; 12 int i; 13 14 Console.WriteLine("b = {0}, !b = {1}", b, !b); 15 16 i = (int)d; 17 Console.WriteLine("d = {0}, after applying casting to (int), i = {1}", d, i); 18 } 19 } 20 } نفّذ البرنامج باستخدام Ctrl+F5 لتحصل على الخرج التالي: b = True, !b = False d = 8.9, after applying casting to (int), i = 8 استخدمنا في هذا البرنامج المتغير b من النوع bool وهو نوع منطقيّ تحمل المتغيّرات المصرّح عنها بواسطته إحدى قيمتين true أو false. أسندنا للمتغيّر b القيمة true عند التصريح عنه في السطر 10، ثمّ عرضنا للمستخدم قيمة b الأصليّة وقيمته بعد تطبيق عامل النفي المنطقي عليه b! لنحصل على الخرج التالي: b = True, !b = False يعكس هذا العامل الحالة المنطقيّة، فإذا كانت true تصبح false، أمّا إذا كانت false فتصبح true. ولكن إذا لاحظت أنّ الخرج يُظهر القيمتين المنطقيتين true و false بحرفين كبيرين في بداية كل منهما: True و False. السبب في ذلك أن التابع WriteLine في السطر 14 يعمل بشكل ضمني على استدعاء التابع ToString لكل من الوسيطين الممرّرين له، أي الوسيطين b و b! فيحصل بذلك على التمثيل النصّي للقيمة المنطقيّة الموجودة في كلّ منهما، والذي يبدأ بحرف طباعي كبير. جرّب استبدال العبارة البرمجيّة في السطر 14 بالعبارة التالية: Console.WriteLine("b = {0}, !b = {1}", b.ToString(), (!b).ToString()); التعديل الذي أجريناه في السطر السابق هو استدعاء التابع ToString بشكل صريح لكلّ وسيط قبل تمريره إلى التابع WriteLine. ستحصل بذلك على نفس الخرج دون أيّ تغيير. بالنسبة لعمليّة التحويل بين الأنواع فقد أجريناها بين المتغيّر d من النوع double (السطر 11) والمتغيّر i من النوع int (السطر 12)، حيث سنحوّل القيمة ذات الفاصلة العشرية 8.9 الموجودة في d إلى قيمة صحيحة بدون فاصلة ونخزّنها ضمن i. تجري عملية التحويل هذه في السطر 16 على الشكل التالي: i = (int)d; لاحظ أنّ القوسين المحيطين بـ int ضروريين. إذا حاولت إزالة عامل التحويل (int) من العبارة السابقة وحاولت تنفيذ البرنامج فستحصل على الخطأ التالي: CS0266 Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?) يُشير هذا الخطأ إلى عدم إمكانيّة إسناد قيمة متغيّر من النوع double إلى متغيّر من النوع int مباشرةً بدون تحويل لأنّ ذلك سيؤدّي إلى ضياع في البيانات (ستضيع القيمة 0.9). يقترح عليك هذا الخطأ استخدام التحويل بين الأنواع cast في الجزء الأخير من الرسالة. أعد وضع عامل التحويل (int) أمام المتغيّر d ونفّذ البرنامج لتحصل في الخرج على ما يلي: d = 8.9, after applying casting to (int), i = 8 انظر كيف أصبحت قيمة i تساوي 8. في الواقع سيصادفنا عامل التحويل كثيرًا في هذه السلسلة. العوامل الثنائية تشتمل هذه العوامل على معظم العوامل الموجودة في سي شارب ولها العديد من الأصناف، تحتاج هذه العوامل إلى وجود مُعاملَين operands على طرفيها لكل تعمل، يلخّص الجدول التالي أهم هذه العوامل مع التصنيف الذي تقع ضمنه. العامل الوصف الاستخدام التصنيف + عملية الجمع العددي x + y عوامل - عملية الطرح العددي x - y حسابيّة * عملية الضرب العددي x * y / عملية القسمة العددية (إذا كان كل من المعاملين من نوع صحيح فسيكون ناتج القسمة صحيحًا بدون فاصلة، حيث تُهمل الأجزاء العشرية في حال وجودها). x / y % عمليّة باقي القسمة x % y > عامل اختبار "أصغر من" يُرجع القيمة true إذا كان المُعامل الأيسر أصغر من الأيمن، وإلّا يُرجع false. x < y عوامل مقارنة < عامل اختبار "أكبر من" يُرجع القيمة true إذا كان المُعامل الأيسر أكبر من الأيمن، وإلّا يُرجع false. x > y => عامل اختبار "أصغر من أو يساوي" يُرجع القيمة true إذا كان المُعامل الأيسر أصغر من أو يساوي الأيمن، وإلّا يُرجع false. x <= y =< عامل اختبار "أكبر من أو يساوي" يُرجع القيمة true إذا كان المُعامل الأيسر أكبر من أو يساوي الأيمن، وإلّا يُرجع false. x >= y == عامل اختبار "المساواة" بين قيمتين، يُرجع true إذا كانت القيمتين متساويتين وإلّا يُرجع false. x == y عوامل اختبار المساواة =! عامل اختبار "عدم المساواة" بين قيمتين، يُرجع true إذا كانت القيمتين غير متساويتين وإلّا يُرجع false. x != y && تطبيق منطق AND على قيمتين (أو تعبيرين) منطقيين. x && y العوامل || تطبيق منطق OR على قيمتين (أو تعبيرين) منطقيين. x || y الشرطية = عامل الإسناد للقيمة (أو التعبير) الموجودة في اليمين إلى المتغيّر الموجود في اليسار. x = y عوامل إسناد =+ عامل الجمع ثم الإسناد. x += y =- عامل الطرح ثم الإسناد. x -= y =* عامل الضرب ثم الإسناد. x *= y =/ عامل القسمة ثم الإسناد. x /= y =% عامل باقي القسمة ثم الإسناد. x %= y فهم العوامل الحسابية تُعتبر هذه العوامل بسيطة وواضحة وهي مشتركة بين جميع لغات البرمجة. على أيّة حال إليك برنامجًا بسيطًا يتعامل معها ويوضّح وظائفها. 1 using System; 2 3 namespace ArithmeticOperators 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int x, y; 10 string str_x, str_y; 11 12 //input operands. 13 Console.Write("Input left operand (x) : "); 14 str_x = Console.ReadLine(); 15 16 Console.Write("Input right operand (y) : "); 17 str_y = Console.ReadLine(); 18 19 //convert each operand to integer representative. 20 x = int.Parse(str_x); 21 y = int.Parse(str_y); 22 23 24 Console.WriteLine(); 25 26 //perform arithmetic calculations and display results. 27 Console.WriteLine("x + y = {0}", x + y); 28 Console.WriteLine("x - y = {0}", x - y); 29 Console.WriteLine("x * y = {0}", x * y); 30 Console.WriteLine("x / y = {0}", x / y); 31 Console.WriteLine("x % y = {0}", x % y); 32 33 } 34 } 35 } نفّذ البرنامج باستخدام Ctrl+F5. سيطلب منك البرنامج إدخال المُعامل الأيسر left operand، ثم المُعامل الأيمن right operand، وبعدها ينفّذ العمليّات الحسابيّة الأربع عليهما. جرّب إدخال القيمتين 9 و 2 على الترتيب لتحصل على الخرج التالي: Input left operand (x) : 9 Input right operand (y) : 2 x + y = 11 x - y = 7 x * y = 18 x / y = 4 x % y = 1 العمليّات الثلاث الأولى واضحة. بالنسبة لعمليّة القسمة يجب أن يكون الناتج 4.5، ولكن بما أنّ عملية القسمة تجري بين قيمتين صحيحتين فإنّ النتيجة يجب أن تكون صحيحة، وبالتالي يُهمل الجزء العشري 0.5 ويكون الناتج 4 فقط. بالنسبة لعمليّة باقي القسمة x % y فإنّ النتيجة 1 هي باقي قسمة 9 على 2. ملاحظة: إذا لم ترغب بحذف الجزء العشري من ناتج عملية القسمة الصحيحة ودون أن تغيّر أنوع المتغيّرات، يمكنك استخدام عامل التحويل بين الأنواع (T). استبدال العبارة الموجودة في السطر 30 بالعبارة التالية: Console.WriteLine("x / y = {0}", x /(double)y); وضعت عامل التحول (double) قبل المتغيّر y لتحويل قيمته العدديّة إلى قيمة من نوع double (دون المسّ بقيمة y الأصليّة بالطبع)، فعندما يرى البرنامج أنّه يُجري عملية القسمة بين قيمة صحيحة (قيمة x) وقيمة من النوع double فسيعطي الناتج على شكل قيمة من نوع double تُمرّر بدورها إلى التابع WriteLine ليعرض القيمة 4.5 بدلًا من 4. ويمكن فعل نفس الأمر مع المتغيّر x بدلًا من y إذا أحببت. فهم عوامل المقارنة سنتناول عوامل المقارنة > و < و => و =< و == و =! في البرنامج التالي: 1 using System; 2 3 4 namespace RelationalOperators 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 int x, y; 11 string str_x, str_y; 12 13 14 //input operands. 15 Console.Write("Input left operand : "); 16 str_x = Console.ReadLine(); 17 18 Console.Write("Input right operand : "); 19 str_y = Console.ReadLine(); 20 21 //convert each operand to integer representative. 22 x = int.Parse(str_x); 23 y = int.Parse(str_y); 24 25 Console.WriteLine(); 26 27 //perform comparing operations and display results. 28 Console.WriteLine("{0} == {1} evaluates to {2}", x, y, x == y); 29 Console.WriteLine("{0} != {1} evaluates to {2}", x, y, x != y); 30 Console.WriteLine("{0} > {1} evaluates to {2}", x, y, x > y); 31 Console.WriteLine("{0} >= {1} evaluates to {2}", x, y, x >= y); 32 Console.WriteLine("{0} < {1} evaluates to {2}", x, y, x < y); 33 Console.WriteLine("{0} <= {1} evaluates to {2}", x, y, x <= y); 34 } 35 } 36 } نفّذ البرنامج وأدخل القيمتين 3 و 4 على الترتيب لتحصل على الخرج التالي: Input left operand : 3 Input right operand : 4 3 == 4 evaluates to False 3 != 4 evaluates to True 3 > 4 evaluates to False 3 >= 4 evaluates to False 3 < 4 evaluates to True 3 <= 4 evaluates to True تكون نتيجة تنفيذ عوامل المقارنة قيمة منطقية true أو false. جرّب إدخال قيم متنوّعة، كما جرّب إدخال قيمتين متساويتين وانظر إلى الخرج. فهم العوامل الشرطية العاملين الشرطيين && (AND) و || (OR) هما عاملان مهمّان جدًّا ويستخدمان بكثرة في بنى القرار في سي شارب. ولهما وجود في جميع لغات البرمجة. يوضّح البرنامج التالي استخدام هذين العاملين بصورة مبسّطة. 1 using System; 2 3 4 namespace RelationalOperators 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 int a, b, c, d; 11 bool and_operator, or_operator; 12 13 a = 1; 14 b = 2; 15 c = 5; 16 d = 9; 17 18 and_operator = (a > b) && (c <= d); 19 Console.WriteLine("({0} > {1}) && ({2} <= {3}) evaluates to {4}", a, b, c, d, and_operator); 20 21 or_operator = (a > b) || (c <= d); 22 Console.WriteLine("({0} > {1}) || ({2} <= {3}) evaluates to {4}", a, b, c, d, or_operator); 23 } 24 } 25 } لا نستخدم العوامل الشرطيّة بهذا الأسلوب في البرامج الحقيقيّة، ولكنّ هذا الأسلوب مفيد في توضيح آلية عمل العوامل الشرطيّة وتفاعلها مع عوامل المقارنة. نفّذ البرنامج لتحصل على الخرج التالي: (1 > 2) && (5 <= 9) evaluates to False (1 > 2) || (5 <= 9) evaluates to True تفسير هذا الخرج يسير للغاية. لنبدأ بالسطر الأوّل، نتيجة حساب التعبير الأول هو false: (1 > 2) && (5 <= 9) وسبب ذلك هو أنّ نتيجة التعبير (2 < 1) هو false، أمّا نتيجة حساب (9 => 5) هو true وبالتالي سيعمل العامل الشرطي && بالنتيجة على حساب التعبير false && true والذي يعطي بكلّ تأكيد القيمة المنطقية false. بالنسبة للسطر الثاني، وهو التعبير: (1 > 2) || (5 <= 9) والذي يعطي true. والسبب هو أنّ العامل الشرطي || سيعمل على حساب التعبير false || true والذي يعطي القيمة المنطقيّة true. لاحظ استخدام الأقواس على أطراف عوامل المقارنة، يمكن الاستغناء عنها، ولكن لا أنصح بذلك، استخدم الأقواس دومًا حتى ولو لم يكن استخدامها ضروريًا لتوضيح منطق البرنامج، ولكن استخدمها بحكمة. السبب في انتفاء الحاجة إلى استخدام الأقواس في هذا البرنامج، هو أنّ عوامل المقارنة لها أسبقيّة تنفيذ أعلى من العوامل الشرطيّة، لذلك فهي تُقيّم قبل تقييم العوامل الشرطيّة. فهم عوامل الإسناد استخدمنا حتى الآن عامل الإسناد (=). توجد عوامل إسناد أخرى تُسهّل البرمجة في سي شارب وهي =+ و =- و =* و =/ و =%. الأمر بسيط، بالنسبة لعامل الإسناد =+ يمكن توضيح عمله بالشيفرة التالية: int x = 3; x += 5; بعد تنفيذ الشيفرة السابقة ستصبح قيمة x تساوي 8. لأنّ العبارة x += 5 تكافئ تمامًا العبارة x = x + 5 ويمكننا استبدالها بها. يُطبّق نفس الأسلوب تمامًا على العوامل الباقية. فمثلًا انظر إلى الشيفرة التالية: int x = 21; x /= 4; x %= 3; هل تستطيع تخمين قيمة x بعد تنفيذ هذه الشيفرة؟ إذا كانت النتيجة 2 فقد أصبت. السبب في ذلك بسيط. فقد بدأنا بقيمة x تساوي 21 ثم نفّذنا العبارة x /= 4 التي تكافئ العبارة x = x / 4 وهي قسمة صحيحة، لذلك سيحمل x القيمة 5 (بدون فاصلة عشرية). بعد تنفيذ العبارة الأخيرة x %= 3 التي تكافئ العبارة x = x % 3 ستصبح قيمة x تساوي 2 لأنّ باقي قسمة 5 على 3 يساوي 2. وهذا كلّ ما في الأمر. تمارين داعمة تمرين 1 حاول تخمين القيمة المنطقيّة التي ستُطبع على الشاشة باستخدام القلم والورقة فقط: int a = 30; a /= 3; a %= 3; Console.WriteLine(a == 1); تمرين 2 حاول تخمين قيمة f التي ستُطبع على الشاشة باستخدام القلم والورقة فقط: int x; double f; x = 9; f = (double)x / 2; f *= 10; Console.WriteLine("f = {0}", f); الخلاصة لفد تعرّفنا في هذا الدرس على الأنواع المُضمّنة في سي شارب وعلى مجالات كلٍّ منها، وعلى الفرق الأساسي بين أنواع القيمة value types والأنواع المرجعيّة reference types. كما تحدّثنا عن معظم العوامل التي تدعمها سي شارب وتصنيفاتها. وتناولنا بعض الأمثلة التوضيحيّة على استخدامها. سنتحدّث في الدرس التالي عن بنى القرار وتغيير مسار البرنامج وهو موضوع مهم في جميع لغات البرمجة.
  9. مقدمة إلى المفهوم الكائني تُعتبر لغة سي شارب لغة برمجة كائنيّة صرفة pure object oriented programming language فكلّ ما تشاهده أمامك في سي شارب عبارة عن كائن. سيكون هذا الدّرس نظريًّا بعض الشيء ولكن فهمه بالشكل الصحيح يُعدّ أمرًا حيويًّا للبرمجة باستخدام سي شارب. ولنكن واقعيين، فإنّ هذا الدرس يُعتبر مدخلًا مبسّطًا للغاية إلى هذا الموضوع المهم والضخم ولا يمكن اعتباره بأيّ حال من الأحوال مرجعًا للبرمجة كائنيّة التوجّه. ستحصل -بعد قراءتك لهذا الدرس- على المعرفة الضروريّة للتمييز بين الأصناف classes والكائنات objects وفهم العلاقة بينهما. بالإضافة إلى فهم المبادئ الأوليّة للوراثة والتغليف. لكي نفهم ما هو الصنف وما هو الكائن اسمع منّي هذه القصّة: نتبع نحن البشر إلى ما يسمّى بالصنف الإنساني. يُعرّف هذا الصنف المزايا التي يجب أن يتمتّع بها كلّ إنسان. فمثلًا لكلّ إنسان اسم وطول ووزن ولون عينان وبصمة إبهام مميّزة تميّزه عن أيّ إنسان آخر. يُعرّف الصنف class الإنسانيّ هذه الصفات السابقة، بحيث أنّ كلّ كائن object إنسانيّ من هذا الصنف تكون له مثل هذه الصفات ولكنّ مع مجموعة خاصّة من القيم لها. فمثلًا الكائن من الصنف الإنساني هو إنسان قد يكون اسمه سعيد وطوله 180 سم ولون عينيه أسود وله بصمة إبهام مميّزة، وهذا الإنسان يختلف عن كائن إنسانيّ آخر، اسمه عمّار وطوله 175 سم ولون عينيه بنيّ وله أيضًا بصمة إبهام مميّزة خاصّة به، وهكذا. ندعو الصفات السابقة بالخصائص Properties، فالصنف Class يعرّف الخصائص، أمّا الكائن Object فيتمتّع بهذه الخصائص ولكن مع مجموعة قيم لها تميّزه عن كائنٍ آخر. أمر آخر، يُعرّف الصنف الإنساني أيضًا سلوكيّات أو إجراءات معيّنة خاصّة للكائنات التي تعود للصنف الإنسانيّ. فهناك مثلًا سلوكيّات المشي والجري والضحك. وفي الغالب أنّ كل كائن يُعبّر عن هذه السلوكيّات بشكل يراعي خصوصيّته. فلكلّ منّا أسلوب مختلف في الضحك. كما يمتلك كلّ منّا أسلوب مختلف في المشي والجري، فقد تميّز إنسانًا لا ترى وجهه من خلال مشيته فقط وهذا أمر واردٌ جدًّا. مثل هذه السلوكيّات Methods نصطلح عليها في البرمجة بالتوابع. فالصنف الإنسانيّ يُعرّف وجود مثل هذه السلوكيّات ولكلّ كائن إنسانيّ الحريّة في التعبير عن هذه السلوكيّات بالشكل الذي يرغبه. التابع في البرمجة يضم تعليمات برمجية يجري تنفيذها عند استدعائه. يعالج ويتعامل هذا التابع عادةً مع الخصائص والتوابع الأخرى الموجودة ضمن نفس الكائن. نسمي التوابع والخصائص بأعضاء الصنف class members وهناك أعضاء أخرى سنتناولها في الدروس التالية. المبادئ العامة للمفهوم كائني التوجه هناك المئات من المقالات والكتب التي تتحدّث عن المفهوم الكائنيّ من منظورات مختلفة، وهناك أساليب متعدّدة تسمح بتحليل المسألة المطروحة وتصميمها وفق أسلوب كائنيّ أو ما يُعرف بالتصميم والتحليل كائنيّ التوجّه OOAD. ولكن يكفيك أن تعرف الآن أنّ هناك مبدآن أساسيّان ينبغي أن تتمتّع بها أيّ لغة برمجة تدعم المفهوم كائنيّ التوجّه وهما: التغليف Encapsulation والوراثة Inheritance. وهناك مفهوم مهم آخر يستند إلى الوراثة وهو التعدّديّة الشكلية Polymorphism. التغليف Encapsulation وهو مبدأ جوهري في البرمجة كائنيّة التوجّه، وهو أحد أسباب ظهور هذا المفهوم. يُقرّر هذا المبدأ أنّه ليس من المفترض أن نطّلع على آلية العمل الداخلية للكائن. ما يهمنا هو استخدام الكائن وتحقيق الغرض المطلوب بصرف النظر عن التفاصيل الداخليّة له. تأمّل المثال البسيط التالي: عندما نقود السيّارة ونريد زيادة سرعتها فإنّنا بكلّ بساطة نضغط على مدوسة الوقود. لا أعتقد أنّ أحدًا يهتمّ بالآلية الميكانيكيّة التي تقف وراء الضغط على مدوسة الوقود. فالمطلوب هو زيادة سرعة السيّارة فحسب دون الاهتمام بالتفاصيل الداخليّة. فالسيّارة تُغلّف encapsulate التفاصيل الميكانيكيّة الداخليّة التي تقف وراء زيادة سرعة السيّارة. السيّارة في هذا المثال هو كائن Object. وعمليّة زيادة السرعة هي سلوكيّة (تابع) Method من كائن السيّارة. هناك مثال آخر كثيرًا ما نراه أثناء تجوّلنا في الشوارع ومحطّات القطار وصالات الانتظار، وهو آلات تحضير المشروبات الساخنة. نقف أمام الآلة نُدخل النقود ثمّ نضغط على زرّ محدّد لنحصل على المشروب الساخن الذي نرغب به. لا نهتمّ عادةً بالتفاصيل الداخليّة التي تحدث ضمن الآلة عندما نضغط أحد الأزرار للحصول على كوب من القهوة. فالآلة هنا تُعتبر كائنًا، وعمليّة الحصول على كوب من القهوة هي سلوكيّة Method من هذا الكائن. فهذه الآلة تعمل على تغليف encapsulate التفاصيل الداخليّة لعمليّة التحضير، فكلّ ما نفعله هو ضغط الزر ومن ثمّ نحصل على الناتج المطلوب. فإذا ما أُجري تعديل في الآلة بحيث تتغيّر طريقة تحضير مشروب ساخن لجعله أفضل وأكثر لذّة، فإنّ ذلك لن يؤثّر مطلقًا على أسلوب التعامل مع الآلة للحصول على نفس المشروب، ولن نلاحظ هذا التعديل إلّا بعد تذوّقنا للمشروب وملاحظة الفرق في المذاق. الوراثة Inheritance تُعتبر الوراثة من أهم أشكال إعادة الاستخدام للمكوّنات البرمجيّة، حيث يعمل الصنف الجديد على الاستفادة من المكوّنات الموجودة مسبقًا ضمن الصنف الذي "يرث" منه ويجري عليها بعض التعديلات التي تناسبه على نحو مخصّص. فبدلًا من إنشاء صنف جديد من الصفر، يمكننا إنشاء صنف يعتمد على صنف آخر ويستفيد من خصائصه وسلوكيّاته (توابعه) الموجودة مسبقًا ثمّ يكيّفها أو يضيف عليها. نسمّي الصنف الأساسي الذي نرث منه بالصنف الأب. أمّا الصنف الذي يقوم بعمليّة الوراثة فنسمّيه بالصنف الابن أو بالصنف المشتق. لتثبيت الفكرة لنتناول المثال التالي. في المدارس هناك ثلاثة أنواع أساسيّة من الأشخاص المتواجدين فيها: الطلاب والمدرّسون والإداريّون. يمكننا بناء صنف عام يُمثّل أي شخص يعمل في المدرسة وليكن SchoolMember يحتوي هذا الصنف على خصائص مثل: الاسم والكنية واسم الأب واسم الأم وتاريخ الميلاد ورقم الهاتف. يمكننا البناء على هذا الصنف عندما نريد إنشاء أصناف أكثر "تخصّصًا" منه. مثل الصنف الذي يُعبّر عن الطلاب Student والصنف الذي يُعبّر عن المدرّسين Teacher، والصنف المُعبّر عن الإداريين Staff. يرث كلّ صنف منها من الصنف الأب SchoolMember فيصبح لكلّ منها نفس الخصائص الموجودة ضمن الصنف SchoolMember بشكل تلقائيّ. من الواضح أنّ الصنف Student مخصّص أكثر من الصنف SchoolMember فهو يحتوي بالإضافة إلى الخصائص الموجودة في SchoolMember خصائص فريدة خاصّة به. فمثلًا من الممكن أن يحتوي على الخاصيّة التي تعبّر عن الصفّ الحالي Grade وعن السلوك العام Behavior للطالب، أمّا صنف المدرّس Teacher فمن الممكن أن يحتوي (بالإضافة إلى الخصائص الموجودة ضمن SchoolMember) على خاصيّة Course التي تُعبّر عن المقرّر الذي يدرّسه (رياضيّات، فيزياء ...الخ) والخاصيّة WeeklyHours التي تعبّر عن عدد الساعات التدريسيّة الأسبوعيّة المكلّف بها. وينطبق نفس المفهوم تمامًا على الصنف Staff الذي يعبّر عن الموظّفين الإداريين في المدرسة. فالوراثة تنتقل بنا من الشكل الأكثر عموميّةً SchoolMember إلى الشكل الأكثر تخصيصًا مثل Student. وفي الحقيقة كان من الممكن أن نتابع عمليّة الوراثة اعتبارًا من الصنف Staff فهناك قسم التوجيّه وهناك أمانة السر والإدارة وغيرها، وكلّ منها يمكن أن يرث من الصنف Staff. التعددية الشكلية Polymorphism بفرض أنّنا نريد بناء برنامج يحاكي الحركة الانتقاليّة لعدّة أنواع من الحيوانات لدراسة حيويّة. كلّ من أصناف السمكة Fish والطائر Bird والضفدع Frog ترث من الصنف Animal الذي يمثّل أيّ حيوان. بفرض أنّ الصنف Animal يحتوي على سلوكيّة (تابع) اسمها Move (تُعبّر عن الانتقال)، فكما نعلم أنّ هذه السلوكيّة ستصبح وبشكل تلقائي موجودة ضمن أيّ صنف يرث من الصنف Animal، وهنا تكمن التعدديّة الشكليّة. فكل صنف من الأصناف Fish وBird وFrog يُعبّر عن عملية الانتقال Move بشكل مختلف. فالسمكة ربما تنتقل عن طريق السباحة مترًا واحدًا عند استدعاء التابع Move. أمّأ الطائر Bird فمن الممكن أي يطير مسافة 10 متر عند كل استدعاء للتابع Move، وأخيرًا فإنّه من الممكن للضفدع أن يقفز مسافة 20 سنتيمتر كلّما استدعي التابع Move. فالتابع Move المعرّف ضمن الصنف Animal يمكن التعبير عنه بأشكال متعدّدة ضمن الأصناف الأبناء Fish وBird وFrog كلٌّ بحسب حاجته. الخلاصة تعرّفنا في هذا الدرس على المفهوم العام للبرمجة كائنيّة التوجّه وتعاملنا مع التغليف حيث لا تهمّنا التفاصيل الداخلية لآلية العمل. والوراثة التي تتعلّق بمفهوم إعادة الاستخدام والانتقال من العام (الأب) إلى المخصّص (الابن). بالإضافة إلى التعدديّة الشكليّة التي تسمح لنا بإكساب سلوكيّات مخصّصة للأصناف الأبناء تنسجم مع طبيعتها. سنتناول في الدروس التالية هذه المفاهيم بشكل تطبيقي في سي شارب.
  10. عندما تعمل على حاسوبك الشخصي، أو حتى على هاتفك الذكيّ، أو ربّما ساعتك الذكيّة. فأنت تستخدم الأحداث آلاف المرّات. الحدث هو وسيلة جميلة للتعبير عن أمر طارئ حدث لكائن برمجيّ. قد يكون هذا الأمر الطارئ عبارة عن نقرة زر فأرة، أو عن لمسة على شاشة جهازك الذكي. أو ضغطة مفتاح من لوحة المفاتيح الخاصّة بي وأنا أكتب هذا المقال، أو أن يكون أمرًا طارئًا يُعبّر عن حالة داخليّة ضمن نظام التشغيل. باختصار، هناك عدد كبير جدًّا من المصادر المختلفة أو المحتملة للأحداث. العلاقة بين الأحداث Events والنوّاب Delegates تعتمد الأحداث بشكل كليّ على النوّاب. وفي الحقيقة هي وسيلة لتهذيبها! لنستعير الصنف Car من البرنامج Lesson13_02 من الدرس السابق: 1 public class Car 2 { 3 public delegate void SpeedNotificatoinDelegate(string message); 4 5 private SpeedNotificatoinDelegate speedNotificationHandler; 6 7 public void RegisterWithSpeedNotification(SpeedNotificatoinDelegate handler) 8 { 9 this.speedNotificationHandler = handler; 10 } 11 12 public int CurrentSpeed { get; set; } 13 public int MaxSpeed { get; set; } 14 15 public Car() 16 { 17 CurrentSpeed = 0; 18 MaxSpeed = 100; 19 } 20 21 public Car(int maxSpeed, int currentSpeed) 22 { 23 CurrentSpeed = currentSpeed; 24 MaxSpeed = maxSpeed; 25 } 26 27 public void Accelerate(int delta) 28 { 29 CurrentSpeed += delta; 30 31 if (CurrentSpeed > MaxSpeed) 32 { 33 if(this.speedNotificationHandler != null) 34 { 35 string msg = string.Format("You exceed the maximum speed! (Current = {0}, Max = {1})", 36 CurrentSpeed, MaxSpeed); 37 38 speedNotificationHandler(msg); 39 } 40 } 41 } 42 } كنت قد طلبت منك في التمرين الداعم رقم 2 من الدرس السابق أن تجري تعديلًا على الصنف Car بحيث تستغني عن التابع RegisterWithSpeedNotification، ولمّحت بأن تجعل الحقل speedNotificationHandler ذي محدّد وصول public. بعد إجراء هذا التعديل سيصبح الصنف Car على الشكل التالي: public class Car { public delegate void SpeedNotificatoinDelegate(string message); public SpeedNotificatoinDelegate speedNotificationHandler; public int CurrentSpeed { get; set; } public int MaxSpeed { get; set; } public Car() { CurrentSpeed = 0; MaxSpeed = 100; } public Car(int maxSpeed, int currentSpeed) { CurrentSpeed = currentSpeed; MaxSpeed = maxSpeed; } public void Accelerate(int delta) { CurrentSpeed += delta; if (CurrentSpeed > MaxSpeed) { if (this.speedNotificationHandler != null) { string msg = string.Format("You exceed the maximum speed! (Current = {0}, Max = {1})", CurrentSpeed, MaxSpeed); speedNotificationHandler(msg); } } } } سيعمل هذا الصنف بشكل ممتاز، حيث من الممكن أن نُسند نائبًا للحقل speedNotificationHandler من خارج الصنف Car بالشكل التالي: Car car = new Car(100, 0); car.speedNotificationHandler = new Car.SpeedNotificatoinDelegate(OnExceedMaxSpeedHandler); المشكلة هنا أنّ الحقل speedNotificationHandler أصبح مكشوفًا تمامًا، حتى أنّه من الممكن استدعاء التابع الذي يغلّفه هذا النائب، من خارج الصنف Car، وهذا يعدّ خرقًا لمبدأ مهم في البرمجة كائنيّة التوجّه ألا وهو التغليف Encapsulation. لا ينبغي أن يتمكّن أيّ أحدٍ من استدعاء التابع الذي يغلّفه الحقل السابق إلّا من داخل الصنف Car حصرًا، لأنّ النائب المُسند لذلك الحقل يُعبّر عن حالة داخليّة ضمن الصنف Car وهي تجاوز السرعة القصوى. توفّر لنا سي شارب حلًا عمليًّا وأنيقًا لهذه المشكلة تتمثّل في استخدام الأحداث. بالنسبة للصنف Car السابق (بعد التعديل) يكفيك أن تَسِم الحقل speedNotificationHandler بالكلمة المحجوزة event ليتحوّل إلى حدث لا يمكن استدعاؤه إلّا من داخل الصنف Car. لكن سيكون هناك اختلاف صغير في طريقة إسناد النوّاب إلى هذا الحقل. يحتوي البرنامج Lesson14_01 على النسخة الجديدة للصنف Car مع تعديل بسيط ضمن التابع Main لندعم استخدام الأحداث: 1 using System; 2 3 namespace Lesson14_01 4 { 5 public class Car 6 { 7 public delegate void SpeedNotificatoinDelegate(string message); 8 9 public event SpeedNotificatoinDelegate speedNotificationHandler; 10 11 public int CurrentSpeed { get; set; } 12 public int MaxSpeed { get; set; } 13 14 public Car() 15 { 16 CurrentSpeed = 0; 17 MaxSpeed = 100; 18 } 19 20 public Car(int maxSpeed, int currentSpeed) 21 { 22 CurrentSpeed = currentSpeed; 23 MaxSpeed = maxSpeed; 24 } 25 26 public void Accelerate(int delta) 27 { 28 CurrentSpeed += delta; 29 30 if (CurrentSpeed > MaxSpeed) 31 { 32 if (this.speedNotificationHandler != null) 33 { 34 string msg = string.Format("You exceed the maximum speed! (Current = {0}, Max = {1})", 35 CurrentSpeed, MaxSpeed); 36 37 speedNotificationHandler(msg); 38 } 39 } 40 } 41 } 42 43 class Program 44 { 45 static void Main(string[] args) 46 { 47 Car car = new Car(100, 0); 48 49 car.speedNotificationHandler += new Car.SpeedNotificatoinDelegate(OnExceedMaxSpeedHandler); 50 51 for (int i = 0; i < 5; i++) 52 { 53 Console.WriteLine("Increasing speed by 30"); 54 car.Accelerate(30); 55 } 56 } 57 58 static void OnExceedMaxSpeedHandler(string message) 59 { 60 Console.WriteLine(message); 61 } 62 } 63 } انظر كيف وضعنا الكلمة المحجوزة event بعد كلمة public في التصريح عن الحقل speedNotificationHandler في السطر 9. كما أرجو أن تلاحظ أيضًا التعديل الذي طرأ في السطر 49 على كيفيّة إسناد النائب الجديد إلى الحقل speedNotificationHandler: car.speedNotificationHandler += new Car.SpeedNotificatoinDelegate(OnExceedMaxSpeedHandler); لاحظ كيف استخدمنا العامل (=+) بدلًا من العامل (=). في الحقيقة سيؤدي استخدام العامل (=) في هذه الحالة إلى حدوث خطأ أثناء ترجمة البرنامج. ولكن ماهي الفائدة من العامل (=+)؟ لهذا العمل فائدة كبيرة، فمن خلاله يمكن تسجيل عدّة نوّاب (وبالتالي عدّة توابع) ضمن الحدث speedNotificationHandler بنفس الوقت. مما يعني أنّ عبارة مثل تلك الموجودة في السطر 37: speedNotificationHandler(msg); ستؤدّي إلى استدعاء أي نائب (وبالتالي أي تابع) مسجّل في الحدث speedNotificationHandler بشكل متسلسل يراعي الترتيب الذي سُجّلت ضمنه هذه النوّاب ضمن الحدث speedNotificationHandler باستخدام العامل =+. نسمّي التابع OnExceedMaxSpeedHandler اصطلاحًا بمعالج الحدث. الآن سنذهب أبعد من ذلك ونجري تغييرًا على عبارة التسجيل في الحدث في السطر 49 لتصبح على الشكل التالي: car.speedNotificationHandler += OnExceedMaxSpeedHandler; لاحظ هنا أنّنا قد أزلنا التعبير الذي ينشئ كائن من النائب SpeedNotificatoinDelegate، ووضعنا بدلًا من ذلك اسم التابع OnExceedMaxSpeedHandler مباشرةً بعد العامل (=+). في الحقيقة إنّ مترجم سي شارب ذكيّ كفاية ليعرف أنّه ينبغي عليه أن يُنشئ كائنًا جديدًا من النائب SpeedNotificatoinDelegate بشكل تلقائيّ يغلّف التابع OnExceedMaxSpeedHandler. إذا نفّذت البرنامج ستحصل على نفس الخرج المتوقّع. ملاحظة: تتمتّع النوّاب أيضًا بميّزة التسجيل المتعدّد باستخدام العامل =+. ولكن هذه الميّزة تُستخدم مع الأحداث بشكل أكبر. التوابع مجهولة الاسم Anonymous Methods التوابع مجهولة الاسم هي من المزايا التي تسمح باختصار الشيفرة إلى حدٍّ كبير. فمن اسمها، يظهر أنَّه لا يوجد لمثل هذه التوابع اسم، وإنّما جسم فقط يحوي الشيفرة المطلوب تنفيذها. كمثال بسيط على التوابع مجهولة الاسم سنجري تعديلًا على البرنامج Lesson14_01 السابق ليستخدم تابعًا عديم الاسم بدلًا من التابع OnExceedMaxSpeedHandler. سيكون هذا التعديل في السطر 49 ليصبح على الشكل التالي: car.speedNotificationHandler += delegate(string message) { Console.WriteLine(message); }; لاحظ هنا أنّنا قد استخدمنا الكلمة المحجوزة delegate بعد العامل =+ بعد ذلك الوسائط التي يقبلها هذا التابع ثم جسم التابع المحاط بالحاضنة. الشيء الوحيد الناقص هو اسم التابع. من الواضح أنّ تعريف هذا التابع عديم الاسم يجب أن يتطابق مع تعريف النائب الذي صُرّح الحدث speedNotificationHandler بناءً عليه. الآن يمكن التخلّص من التابع OnExceedMaxSpeedHandler. انظر إلى البرنامج Lesson14_02 الكامل: 1 using System; 2 3 namespace Lesson14_02 4 { 5 public class Car 6 { 7 public delegate void SpeedNotificatoinDelegate(string message); 8 9 public event SpeedNotificatoinDelegate speedNotificationHandler; 10 11 public int CurrentSpeed { get; set; } 12 public int MaxSpeed { get; set; } 13 14 public Car() 15 { 16 CurrentSpeed = 0; 17 MaxSpeed = 100; 18 } 19 20 public Car(int maxSpeed, int currentSpeed) 21 { 22 CurrentSpeed = currentSpeed; 23 MaxSpeed = maxSpeed; 24 } 25 26 public void Accelerate(int delta) 27 { 28 CurrentSpeed += delta; 29 30 if (CurrentSpeed > MaxSpeed) 31 { 32 if (this.speedNotificationHandler != null) 33 { 34 string msg = string.Format("You exceed the maximum speed! (Current = {0}, Max = {1})", 35 CurrentSpeed, MaxSpeed); 36 37 speedNotificationHandler(msg); 38 } 39 } 40 } 41 } 42 43 class Program 44 { 45 static void Main(string[] args) 46 { 47 Car car = new Car(100, 0); 48 49 car.speedNotificationHandler += delegate(string message) 50 { 51 Console.WriteLine(message); 52 }; 53 54 for (int i = 0; i < 5; i++) 55 { 56 Console.WriteLine("Increasing speed by 30"); 57 car.Accelerate(30); 58 } 59 } 60 } 61 } ملاحظة: يوجد نائب جاهز موجود ضمن مكتبة FCL اسمه EventHandler وظيفته توفير الدعم للأحداث الجديدة التي نعرّفها، بحيث لا نضطّر إلى التصريح عن نائب جديد في كلّ مرّة نريد فيها التصريح عن حدث جديد. يغلّف النائب EventHandler أي تابع يتطلّب وسيطين الأوّل من النوع object والذي يمثّل الكائن الذي أصدر الحدث، والثاني من النوع EventArgs وهو كائن يحتوي على بعض المعلومات الإضافيّة عن الحدث. تمارين داعمة تمرين 1 ليكن لدينا الصنف التالي: class Counter { private int currentValue = 0; public void Increase() { currentValue++; } public void Decrease() { currentValue--; } } أجرِ تعديلًا على هذا الصنف بحيث تصرّح عن الحدث Notification الذي يُفعَّل عندما تصبح قيمة الحقل currentValue من مضاعفات العدد 5 فقط. (تلميح: الأعداد السالبة ليست من مضاعفات 5. والعدد 5 هو مضاعف لنفسه). تمرين 2 أجرِ تعديلًا على البرنامج Lesson14_02 السابق لتستغني عن النائب SpeedNotificatoinDelegate تمامًا، بحيث تستخدم النائب الجاهز EventHandler عوضًا عنه. (تلميح: سيتطلّب الأمر تعديل الوسائط الممرّرة إلى معالج الحدث لتتطابق مع الوسائط التي يحتاجها النائب EventHandler) الخلاصة تعرّفنا في هذا الدرس على الأحداث Events. تلك التقنيّة المهمّة التي تعتمد عليها تطبيقات سطح المكتب desktop applications بشكل أساسيّ، فضلًا عن باقي أنواع التطبيقات مثل تطبيقات الويب، وتطبيقات الأجهزة المحمولة، وأي نوع من أنواع التطبيقات التي تتطلّب التفاعل الداخلي مع نظام التشغيل أو الخارجيّ مع المستخدم.
  11. يُعتبر مفهوم النوّاب من المفاهيم الأساسيّة في سي شارب، وستستخدمه بشكل أو بآخر في أيّ برنامج حقيقي مكتوب بهذه اللغة. سنتحدّث في هذا الدرس عن هذا الموضوع المهمّ، وسنتعرّض في درس لاحق إلى مفهوم مهم آخر مرتبط به، ألا وهو مفهوم الأحداث Events. في جميع البرامج التي صادفناها في هذه السلسلة كنّا نعمل على استدعاء توابع وخصائص من كائنات ننشئها من الأصناف المختلفة. في البرمجة العمليّة، يحتاج الكائن في الكثير من الأحيان إلى وسيلة يفعل فيها العكس. أي يحتاج الكائن إلى أسلوب يستطيع من خلاله استدعاء تابع ما لا ينتمي إليه وذلك من تابع أو خاصيّة موجودة داخل الكائن. تُسمّى هذه الظاهرة بالردود callbacks، وهي تقنيّة مهمّة جدًّا مستخدمة حتى في أنظمة التشغيل على نحو واسع. ربما قد تتساءل لماذا يحتاج الكائن لأن يستخدم مثل هذه التقنيّة (الردود). الجواب على ذلك بسيط، فقد يحتاج الكائن إلى التعبير عن حالته الداخليّة، أو عن أمر طارئ قد حدث له، ففي تطبيقات سطح المكتب desktop applications مثلًا، يُعتبر أيّ زر موجود على نافذة البرنامج عبارة عن كائن. فعندما ينقر مستخدم البرنامج هذا الزر، فمن الطبيعي أن يبلّغنا الزر عن "حدث" النقر عليه. فكيف يفعل ذلك؟ يفعل ذلك ببساطة عن الطريق الردود التي نسمّيها في سي شارب بالنوّاب (ستعرف سبب التسمية بعد قليل). التصريح عن نائب يمكنك اعتبار النائب كصنف مستقل بحد ذاته، مع أنّ التصريح عنه يختلف عن التصريح عن الأصناف. تُصرّح الشيفرة التالية عن النائب SumDelegate: public delegate int SumDelegate(int a, int b); النوّاب -وعلى سبيل التبسيط- عبارة عن مغلّفات للتوابع. فالنائب SumDelegate السابق يمكنه أن يغلّف أو يشير إلى أيّ تابع آخر بشرط أن يقبل وسيطين من نوع int ويُرجع قيمة من نوع int أيضًا (لاحظ الشيفرة السابقة). فإذا أزلنا الكلمة المحجوزة delegate من الشيفرة السابقة، سيبدو الأمر "كما لو أنّنا" نصرّح عن التابع SumDelegate الذي يقبل وسيطين من النوع int ويُرجع قيمة من النوع int، وهو بمحدّد وصول public. لفهم الموضوع بشكل أفضل دعنا نستخدم النائب السابق في البرنامج Lesson13_01: 1 using System; 2 3 namespace Lesson13_01 4 { 5 public delegate int SumDelegate(int a, int b); 6 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 SumDelegate handler = new SumDelegate(Sum); 12 13 int result = handler(5, 6); 14 15 Console.WriteLine(result); 16 } 17 18 static int Sum(int a, int b) 19 { 20 return a + b; 21 } 22 } 23 } لاحظ أولًا أنّنا وضعنا التصريح عن النائب SumDelegate في السطر 5 خارج أي صنف، مع أنّه كان ممكنًا أن نصرّح عنه ضمن الصنف Program. صرّحنا في الأسطر من 18 إلى 21 عن تابع ساكن static ضمن الصنف Program اسمه Sum يقبل وسيطين من النوع int ويُرجع قيمة من النوع int أيضًا. العمليّة التي يقوم بها هذا التابع بسيطة، فهو يجمع قيمتي الوسيطين الممرّرين إليه ويُرجع الناتج بسطرٍ واحد. انظر الآن إلى السطر 11 ضمن التابع Main. ستلاحظ أنّنا نصرّح عن المتغيّر handler من النوع (النائب) SumDelegate، ونُسند إليه كائنًا من نفس النوع باستخدام التعبير (new SumDelegate(Sum حيث مرّرنا إلى بانيته الوسيط Sum. ولكن أليس الوسيط Sum هو نفسه اسم التابع الساكن المصرّح عنه ضمن الصنف Main! في الواقع تتطلّب بانية النائب SumDelegate (على اعتباره صنفًا) وسيطًا عبارة عن تابع يقبل وسيطين من النوع int ويرُجع قيمة من النوع int، أي بشكل مماثل لتصريح النائب SumDelegate في السطر 5. انظر إلى العبارة البرمجيّة التالية في السطر 13: int result = handler(5, 6); أصبح بإمكاننا الآن أن نستدعي النائب handler بوسيطين من النوع int، وسيُرجع بالتأكيد قيمة من النوع int أيضًا. إذًا أصبح handler "ينوب" عن التابع Sum في عمليّة الاستدعاء، رغم أنّه في حقيقة الأمر سيستدعي التابع Sum ولكن من وراء الكواليس! يمكن باستخدام هذه التقنيّة وبإجراء بعض التعديلات الطفيفة (كما سنرى في الأحداث Events)، أن ينوب handler عن أكثر من تابع بنفس الوقت بشرط أن يقبل كلّ منها وسيطين من نوع int ويُرجع كلّ منها قيمة من النوع int. استخدام النواب بشكل عملي لنتناول برنامجًا يوضّح استخدام النوّاب بشكل عمليّ أكثر. انظر البرنامج Lesson13_02: 1 using System; 2 3 namespace Lesson13_02 4 { 5 public class Car 6 { 7 public delegate void SpeedNotificatoinDelegate(string message); 8 9 private SpeedNotificatoinDelegate speedNotificationHandler; 10 11 public void RegisterWithSpeedNotification(SpeedNotificatoinDelegate handler) 12 { 13 this.speedNotificationHandler = handler; 14 } 15 16 public int CurrentSpeed { get; set; } 17 public int MaxSpeed { get; set; } 18 19 public Car() 20 { 21 CurrentSpeed = 0; 22 MaxSpeed = 100; 23 } 24 25 public Car(int maxSpeed, int currentSpeed) 26 { 27 CurrentSpeed = currentSpeed; 28 MaxSpeed = maxSpeed; 29 } 30 31 public void Accelerate(int delta) 32 { 33 CurrentSpeed += delta; 34 35 if (CurrentSpeed > MaxSpeed) 36 { 37 if(this.speedNotificationHandler != null) 38 { 39 string msg = string.Format("You exceed the maximum speed! (Current = {0}, Max = {1})", 40 CurrentSpeed, MaxSpeed); 41 42 speedNotificationHandler(msg); 43 } 44 } 45 } 46 } 47 48 class Program 49 { 50 static void Main(string[] args) 51 { 52 Car car = new Car(100, 0); 53 54 car.RegisterWithSpeedNotification(new Car.SpeedNotificatoinDelegate(OnExceedMaxSpeedHandler)); 55 56 for(int i = 0; i < 5; i++) 57 { 58 Console.WriteLine("Increasing speed by 30"); 59 car.Accelerate(30); 60 } 61 } 62 63 static void OnExceedMaxSpeedHandler(string message) 64 { 65 Console.WriteLine(message); 66 } 67 } 68 } نفّذ هذا البرنامج لتحصل في الخرج على ما يلي: Increasing speed by 30 Increasing speed by 30 Increasing speed by 30 Increasing speed by 30 You exceed the maximum speed! (Current = 120, Max = 100) Increasing speed by 30 You exceed the maximum speed! (Current = 150, Max = 100) خذ نفسًا عميقًا، وجهّز كوبًا من الشاي (شرابي المفضّل) لندخل في تفاصيل البرنامج. ينقسم هذا البرنامج إلى صنفين: الصنف Car (من السطر 5 حتى السطر 46) والصنف Program (من السطر 48 حتى السطر 67). يمثّل الصنف Car القالب العام لسيّارة، ويحتوي على خاصيّتين CurrentSpeed و MaxSpeed وتعبّران عن السرعة الحالية والسرعة القصوى على الترتيب. كما يحتوي على بانيتين، إحداهما عديمة الوسائط، والثانية ذات وسيطين من نوع int لسهولة الإنشاء والإسناد للخصائص. يضم الصنف Car أيضًا النائب SpeedNotificatoinDelegate (السطر 7). تذكّر أنّه من الممكن أن تكون النوّاب ضمن الأصناف. يمكن لهذا النائب أن يغلّف أي تابع يقبل وسيطًا واحدًا من النوع string ولا يرجع أي قيمة (void). نعرّف أيضًا ضمن الصنف Car الحقل speedNotificationHandler (السطر 9) من النوع SpeedNotificatoinDelegate وذو محدّد وصول private، وأخيرًا يحتوي الصنف Car على التابعين Accelerate الذي يعبّر عن إكساب السيّارة المزيد من السرعة، وهو يتطلّب وسيطًا واحدًا من النوع int ويمثّل مقدار الزيادة في السرعة. والتابع RegisterWithSpeedNotification الذي يتطلّب وسيطًا واحدًا من النوع SpeedNotificatoinDelegate. وظيفة هذا التابع هو استقبال وسيط من النوع (النائب) SpeedNotificatoinDelegate وإسناده إلى الحقل الخاص speedNotificationHandler. سيغلّف النائب الممرّر لهذا التابع تابعًا آخر يقبل وسيطًا واحدًا من النوع string ولا يُرجع أي قيمة (void). يحتوي الصنف Program على تابعين: الأوّل هو Main (من السطر 50 حتى السطر 61) وهو غنيّ عن التعريف. والثاني هو OnExceedMaxSpeedHandler (من السطر 63 حتى السطر 66) الذي يحتاج إلى وسيط نصي واحد، ولا يرجع شيء (void)، وهو يحتوي على عبارة برمجيّة وحيدة وظيفتها عرض قيمة الوسيط النصيّ msg على الشّاشة. يبدأ البرنامج في التابع Main بالتصريح عن المتغيّر car وإسناد كائن جديد إليه من النوع Car، حيث نمرّر 100 كسرعة قصوى، والقيمة 0 كسرعة حاليّة إلى بانية الصنف Car. بعد ذلك سيعمل البرنامج على إنشاء كائن نائب يغلّف التابع OnExceedMaxSpeedHandler من خلال التعبير: new Car.SpeedNotificatoinDelegate(OnExceedMaxSpeedHandler) سيمرَّر هذا الكائن الجديد إلى التابع RegisterWithSpeedNotification كما هو واضح من السطر 54. يدخل البرنامج بعد ذلك في حلقة for (الأسطر من 56 حتى 60) التي ستتكرّر 5 مرّات، وبعد أن يتم استدعاء التابع Accelerate أربعة مرّات (السطر 59)، تصبح عندها السرعة الحالية تساوي 120 وهي أكبر من السرعة القصوى، لذلك يُستدعى التابع الذي يغلّفه النائب speedNotificationHandler (السطر 42) بالشكل: speedNotificationHandler(msg); والذي هو في حالتنا التابع OnExceedMaxSpeedHandler ليمرّر إليه رسالة نصيّة ضمن المتغيّر msg توضّح بأنّه قد تمّ تجاوز السرعة القصوى المحدّدة. وستتكرّر نفس العمليّة من أجل الدورة الخامسة والأخيرة للحلقة for. لاحظ الاختبار الذي أجريناه في السطر 37 من التابع Accelerate. يتأكّد هذا السطر من أنّ الحقل speedNotificationHandler ليس فارغًا (يحوي null). لأنّه إذا كان يحوي null فلا ينبغي تنفيذ عبارة الاستدعاء في السطر 42. أنصح بأن تستخدم تطبيق Visual Studio من أجل هذا البرنامج، حيث يمكن أن تستفيد من المنقّح Debugger الخاص به لتنفيذ البرنامج خطوة بخطوة (استخدم المفتاح F11 لتنفيذ البرنامج بشكل خُطَويّ) لفهم أفضل له. ملاحظة: أيّ حقل مصرّح عنه ضمن صنف ما ويكون من نوع مرجعيّ reference type تكون القيمة الافتراضيّة له هي null في حال لم نُسند إليه أي قيمة عند التصريح عنه. وتعني null أنّ هذا الحقل لا يحتوي على مرجع لأيّ كائن. ويسري نفس الأمر على المتغيّرات المحليّة التي نصرّح عنها ضمن التوابع والتي تكون أيضًا من أنواع مرجعيّة. ملاحظة: يمكن لأي نائب أن يغلّف توابع عاديّة أو توابع ساكنة static. تمارين داعمة تمرين 1 أجرِ تعديلًا على البرنامج Lesson13_02 بحيث يستخدم البرنامج النائب speedNotificationHandler لإرسال تنبيه إلى التابع الذي يُغلّفه هذا النائب، في حال وصلت السرعة الحالية إلى منتصف السرعة القصوى أو تجاوزتها (التنبيه يجب أن يصدر لمرّة واحدة). (تلميح: ستحتاج إلى التصريح عن حقل خاص ضمن الصنف Car من نوع bool مثلًا لكي يعرف البرنامج أنّه قد أصدر التنبيه الخاص بالوصول إلى منتصف السرعة القصوى، لكيلا يعيد إصدار مثل هذا التنبيه مرّة أخرى). تمرين 2 أجرِ تعديلًا آخرًا على البرنامج Lesson13_02 بحيث تستغني فيه عن استخدام التابع RegisterWithSpeedNotification. دون التغيير في أسلوب عمل البرنامج. (تلميح: ستحتاج إلى جعل الحقل speedNotificationHandler ذو محدّد وصول public بدلًا من private، ثم تتعامل مع هذا الحقل مباشرةً من الصنف Main). الخلاصة تعرّفنا في هذا الدرس على مفهوم جديد لكنّه أساسيّ وهو النوّاب Delegates. حيث اكتشفنا كيف أنّ النوّاب هي وسائل لتحقيق مبدأ الردود callbacks المهم في عالم البرمجة، والمستخدم على نطاق واسع في أنظمة التشغيل في تبادل الرسائل بين الكائنات المختلفة. تعلّمنا كيف يُغلّف النائب تابعًا يوافق معايير معيّنة تكون محدّدة عند التصريح عن النائب. في الحقيقة تُعتبر النوّاب الركن الأساسيّ للأحداث Events، ذلك المفهوم المهم في البرمجة كائنيّة التوجّه، والذي سنتحدّث عنه في الدرس التالي.
  12. يمكن القراءة والكتابة من وإلى الملفات النصيّة في سي شارب بعدّة طرق. سنتناول أسلوبًا بسيطًا وذلك من خلال الصنفين StreamWriter و StreamReader. يسمح الصنف StreamWriter بالكتابة فقط، أمّا الصنف StreamReader فهو يسمح بالقراءة فقط. كما يمكن استخدام كلا الصنفين في نفس البرنامج. وكلّ منهما موجود ضمن نطاق الاسم System.IO. يرث الصنف StreamWriter من الصنف TextWriter في حين يرث الصنف StreamReader من الصنف TextReader. الكتابة إلى ملف نصي سنعمل في البرنامج Lesson12_01 على إنشاء الملف data.txt وكتابة بعض الأسطر ضمنه: 1 using System; 2 using System.IO; 3 4 namespace Lesson12_01 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string[] lines = { "First Line", "Second Line", "Third Line" }; 11 12 StreamWriter fileWriter = new StreamWriter("data.txt"); 13 14 foreach (string line in lines) 15 { 16 fileWriter.WriteLine(line); 17 } 18 19 fileWriter.Close(); 20 } 21 } 22 } صرّحنا في السطر 10 عن المصفوفة lines التي عناصرها نصوص، لاحظ أنّنا استخدمنا الطريقة المختصرة لإنشاء كائن مصفوفة وإسناد العناصر الموجودة ضمن الحاضنة {} إلى عناصره مباشرةً. نصرّح في السطر 12 عن المتغيّر fileWriter حيث نسند إليه كائن من الصنف StreamWriter. عند إنشاء هذا الكائن، مرّرنا اسم الملف "data.txt" إلى بانية الصنف StreamWriter. في الحقيقة تخضع هذه البانية إلى زيادة التحميل overloading، حيث تمتلك ثمانية أشكال مختلفة اخترنا أبسطها، وهو مسار واسم الملف المراد إنشاؤه. وبما أنّنا مرّرنا الاسم فقط دون المسار، فسيتم إنشاء هذا الملف في نفس المجلّد الموجود ضمنه الملف التنفيذي للبرنامج. بعد ذلك نستخدم التابع WriteLine من المتغيّر fileWriter لكتابة عناصر المصفوفة lines على أسطر منفصلة ضمن الملف data.txt. العبارة البرمجيّة في السطر 19 ضرورية لإغلاق الملف باستدعاء التابع Close وتحرير المصدر الذي يحجزه في نظام التشغيل. جرّب تنفيذ البرنامج، لن تحصل على شيء على الشاشة، ولكن إذا فتحت الملف data.txt (ستجده غالبًا ضمن bin\debug ضمن مجلّد المشروع) ستجد الأسطر الثلاثة موجودةً ضمنه. ملاحظة: يوجد شكل آخر لبانية الصنف StreamWriter يقبل بالإضافة إلى اسم الملف ومساره قيمة منطقيّة (من نوع bool) تُدعى append. إذا مرّرت true مكانها فسيعمل البرنامج إلى الإضافة إلى محتويات الملف data.txt، أمّا إذا مرّرت false فسيعمل على الكتابة عليه. أمّا إذا أهملت هذا الشكل تمامًا كما هو الحال في مثالنا فسيعمل البرنامج على الكتابة على الملف، أي استبدال محتوياته، في كلّ مرّة ننفّذ فيها البرنامج. في الحقيقة ليس هذا هو الاستخدام الأمثل للصنف StreamWriter والسبب في ذلك أنّ مصادر نظام التشغيل محدودة، حيث يؤدّي التعامل مع الملفات إلى حجز بعض من هذه المصادر، لذلك ينبغي تحرير هذه المصادر فورًا عندما تنتفي الحاجة إليها. قد يبدو أنّنا قد فعلنا ذلك باستخدام التابع Close وهذا صحيح تمامًا، ولكن ليس بالسرعة القصوى الممكنة! هناك أسلوب آخر يسمح بتحرير المصادر بشكل أكثر فعاليّة وسرعة باستخدام الكلمة المحجوزة using. سأعدّل البرنامج Lesson12_01 ليستخدم هذا الأسلوب الجديد. انظر البرنامج Lesson12_02 بعد التعديل: 1 using System; 2 using System.IO; 3 4 namespace Lesson12_02 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string[] lines = { "First Line", "Second Line", "Third Line" }; 11 12 using (StreamWriter fileWriter = new StreamWriter("data.txt")) 13 { 14 foreach (string line in lines) 15 { 16 fileWriter.WriteLine(line); 17 } 18 } 19 } 20 } 21 } لاحظ السطر 12 كيف وضعنا عبارة التصريح عن المتغيّر fileWriter والإسناد إليه ضمن عبارة using. في الواقع لن يكون المتغيّر fileWriter مرئيًّا خارج حاضنة using (من السطر 13 حتى السطر 18)، وبمجرّد وصول تنفيذ البرنامج إلى السطر 19 سيتم إغلاق الملف فورًا وتحرير المصدر الذي يحجزه. يظهر من البرنامج السابق أنّنا لم نعد نحتاج إلى استخدام التابع Close. القراءة من ملف نصي سنستخدم الصنف StreamReader لهذا الغرض. سيعمل البرنامج Lesson12_03 على قراءة محتويات الملف data.txt السابق وعرضها على الشاشة: 1 using System; 2 using System.IO; 3 4 namespace Lesson12_03 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 using (StreamReader fileReader = new StreamReader("data.txt")) 11 { 12 while(!fileReader.EndOfStream) 13 { 14 string line = fileReader.ReadLine(); 15 Console.WriteLine(line); 16 } 17 } 18 } 19 } 20 } تخضع بانية الصنف StringReader أيضًا لزيادة التحميل، حيث تمتلك 11 شكلًا مختلفًا تسمح للمبرمج بالتحكّم الكامل بكيفيّة القراءة من الملف. أبسط هذه الأشكال هو الشكل الذي استخدمناه في البرنامج Lesson12_03 حيث سنمرّر لهذه البانية اسم الملف data.txt الذي أنشأناه في البرنامج Lesson12_02 السابق. استخدمنا في هذا البرنامج أيضًا العبارة using (من السطر 10 حتى السطر 17) لتحرير المصدر الذي يحجزه الملف عند الانتهاء من القراءة. ننشئ كائن من الصنف StreamReader ونسنده إلى المتغيّر fileReader ضمن عبارة using في السطر 10. ثمّ نستخدم حلقة while لقراءة محتويات الملف، وذلك لأنّنا من الناحية النظريّة لا نعلم بالتحديد كم سطرًا يحوي الملف. لاحظ شرط استمرار حلقة while حيث تُرجع الخاصيّة EndOfStream للمتغيّر fileReader القيمة true إذا وصلنا إلى نهاية الملف أثناء عمليّة القراءة، وإلّا فإنّها تُرجع false. إذًا، في حال لم نصل بعد إلى نهاية الملف ستُرجع الخاصيّة القيمة EndOfStream القيمة false وبسبب وجود عامل النفي المنطقي (!) قبل هذه الخاصيّة مباشرةً، ستكون القيمة النهائيّة لهذا التعبير هو true مما يسمح لحلقة while بالاستمرار. أمّا عند الوصول إلى نهاية الملف سيحدث العكس تماماً مما يجعل شرط استمرار الحلقة false وينتهي تنفيذ الحلقة. نقرأ في السطر 14 سطرًا من الملف data.txt ونسنده إلى المتغيّر النصي line في كل دورة، ثمّ نطبع محتويات هذا المتغيّر إلى الشاشة في السطر 15. استخدمنا في عمليّة قراءة سطر من الملف data.txt التابع ReadLine من المتغيّر fileReader الذي يعمل على قراءة سطر واحد في كلّ مرّة من الملف data.txt. تمارين داعمة تمرين 1 اكتب برنامجًا يطلب من المستخدم إدخال خمس جُمل، ثم يعمل على تخزين هذه الجُمل على اعتبار أنّ كل جملة يُدخلها المستخدم تمثّل سطرًا نصيًّا. تخزّن كل جملة مع رقم سطرها بحيث يفصل بينهما محرف الجدولة (\t). تمرين 2 استفد من الصنف Student التالي: class Student { public string Name { get; set; } public int Mark { get; set; } } في كتابة برنامج يطلب من المستخدم إدخال بيانات خمسة طلّاب (اسم الطالب Name، والدرجة Mark) مستخدمًا المجموعة العموميّة <List<Student في تخزين بيانات هؤلاء الطلّاب (ستحتاج إلى استخدام نطاق الاسم System.Collections.Generic في بداية البرنامج). بعد الانتهاء من الإدخال، يجب على البرنامج حفظ بيانات هؤلاء الطلّاب ضمن الملف students.txt على شكل جدول بسيط، بحيث تصبح محتويات الملف مشابهة للشكل التالي: Amjad 50 Mohammad 80 Mazen 90 Nour 88 Anwar 40 الخلاصة تعرّفنا في هذا الدرس على المبادئ الأساسيّة في التعامل مع الملفات النصيّة. حيث تعلّمنا كيفيّة القراءة من الملف النصيّ وكيفيّة الكتابة إليه. وتعلّمنا أساليب مهمّة عند فتح الملف للقراءة أو الكتابة تتمثّل في استخدام العبارة using لكي نحرّر المصدر الذي يحجزه الملف فور الانتهاء من التعامل معه.
  13. يُعتبر التعامل مع النصوص من المواضيع المهمّة التي يحتاجها أيّ مبرمج. توفّر سي شارب أساليب متقدّمة للتعامل مع النصوص، سنتحدّث في هذا الدرس عن بعض هذه الأساليب. حيث سنتعلّم كيفيّة البحث عن نص محدّد ضمن نص آخر، واستبدال نص بآخر، واستخلاص جزء محدّد من نص، وضمّ النصوص. النص في سي شارب هو سلسلة من المحارف char الواقعة بين علامتي الاقتباس المزدوج " ". لقد تعاملنا في العديد من المرّات مع النصوص في الدروس السابقة، وتعلّمنا كيف أنّ النوع string هو الذي يمثّل أي نص في سي شارب. البحث والاستبدال ضمن نص البحث ضمن نص للمحارف في أيّ نص ترتيب رقمي يبدأ من الصفر ويسمى الدليل index. إذا أردنا أن نبحث عن نص محدّد ضمن نص آخر فيمكن ذلك باستخدام التابع IndexOf الذي نستدعيه من النص المراد البحث ضمنه، ونمرّر إليه النص المراد البحث عنه، فيُرجع دليل أوّل محرف للنص المطابق في حال وجوده وإلّا فيرجع 1-. كمثال على ذلك انظر الشيفرة التالية: string text = "My friend Mohammad is a developer, Mohammad likes C#."; int pos = text.IndexOf("Mohammad"); بعد تنفيذ الشيفرة السابقة سيحمل المتغيّر pos القيمة 10 والتي تمثّل دليل الحرف M ضمن النص text. ربما تكون قد لاحظت أنّ النص Mohammad موجود في النص text مرّتين، فأيّ دليل أعاد التابع IndexOf؟ تجري عملية البحث باستخدام التابع IndexOf حسب ترتيب إدخال المحارف في النص text، لذلك فأوّل كلمة Mohammad مطابقة يصادفها تُنهي عمليّة البحث. ففي مثالنا السابق حدثت المطابقة عند كلمة Mohammad الموجودة بعد "My friend". أمّا إذا استخدمنا كلمة غير موجودة فسنحصل على القيمة 1- كما أسلفنا. انظر الشيفرة التالية: string text = "My friend Mohammad is a developer, Mohammad likes C#."; int pos = text.IndexOf("Amjad"); سيحمل المتغيّر pos القيمة 1- لأنّ النص الذي نبحث عنه غير موجود في النص text. يخضع التابع IndexOf في الواقع لزيادة التحميل، حيث أنّ هناك 9 أشكال مختلفة له. تؤمّن هذه الأشكال المزيد من مزايا البحث، حيث يمكننا مثلًا البحث عن نص ضمن جزء محدّد من النص الموجود في text، أو ابتداءً من دليل محدّد حتى آخر النص، وهكذا. هناك تابع آخر يسمح لنا بأن نبحث بشكل معكوس، وهو التابع LastIndexOf لفهم عمله بشكل جيّد سأعيد كتابة الشيفرة السابقة ولكن باستخدام التابع LastIndexOf: string text = "My friend Mohammad is a developer, Mohammad likes C#."; int pos = text.LastIndexOf("Mohammad"); بعد التنفيذ سيحمل المتغيّر pos القيمة 35، وذلك لأنّ المطابقة حدثت هذه المرّة مع كلمة Mohammad الثانية (تأتي قبل كلمة likes). حيث تمثّل القيمة 35 دليل الحرف M لهذه الكلمة بالنسبة للنص text. ملاحظة يمكنك أن تستخدم التابع ToUpper من أي متغيّر نصي والذي يعيد حالة الأحرف الطباعية الكبيرة للنص الموجود ضمن المتغيّر النصي دون أن يؤثّر ذلك على محتوى النص الأساسي ضمن المتغيّر. ونفس الأمر ينطبق على التابع ToLower والذي يعيد الحالة الطباعية الصغيرة. يفيد كل من التابعين السابقين أحيانًا في البحث إذا أردنا عدم التمييز بين الأحرف الطباعية الكبيرة والصغيرة عند عملية المطابقة بين الكلمات. انظر لكيفيّة استخدام هذين التابعين: string text = "Hello!"; string upper = text.ToUpper(); //upper will contains "HELLO!" string lower = text.ToLower(); //lower will contains "hello!" مع الانتباه إلى أنّ قيمة المتغيّر text تبقى كما هي. الاستبدال ضمن نص بالنسبة للاستبدال فالأمر يسير أيضًا، حيث يمكن استخدام التابع Replace لهذا الغرض. يُستدعى هذا التابع من النص الذي نريد إجراء عمليّة الاستبدال ضمنه، حيث يتطلّب هذا التابع وسيطين: الأوّل oldValue والذي يمثّل القيمة النصيّة المراد استبدالها، والوسيط الثاني newValue الذي يمثّل القيمة النصيّة الجديدة. يُرجع هذا التابع قيمة نصيّة تمثّل النص الجديد بعد إجراء عمليّة الاستبدال ضمنه، أي أنّ النص الأساسي يبقى دون تغيير. انظر الشيفرة التالية: string text = "My friend Mohammad is a developer, Mohammad likes C#."; string newText = text.Replace("Mohammad", "Amjad"); بعد التنفيذ سيحمل المتغيّر newText القيمة النصيّة التالية: "My friend Amjad is a developer, Amjad likes C#." وسيبقى النص الأصلي text على حاله دون تغيير. استخلاص النصوص وضمها استخلاص النصوص يمكننا استخلاص جزء من نص باستخدام التابع SubString والذي يعني نصًّا فرعيًّا أو جزئيًّا. لهذا التابع شكلان. يسمح الشكل الأوّل باستخلاص نص جزئي ابتداءً من دليل محدّد حتى آخر النص. أمّا الشكل الثاني فيسمح أيضًا باستخلاص نص جزئي ابتداءً من دليل محدّد ولكن بطول محدّد أيضًا. للتمييز بين هاتين الحالتين لنكتب بعض الشيفرة: string text = "My friend Mohammad is a developer, Mohammad likes C#."; string text1 = text.Substring(24); بعد التنفيذ سيحتوي المتغيّر text1 على النص: ".#developer, Mohammad likes C". أمّا إذا استخدمنا الشيفرة التالية: string text = "My friend Mohammad is a developer, Mohammad likes C#."; string text1 = text.Substring(24, 9); فسيحتوي المتغيّر text1 على النص: "developer" فقط. والسبب في ذلك أنّنا بدأنا عمليّة الاستخلاص من الدليل 24 (دليل الحرف d) وبطول 9 محارف فقط مما يعني استخلاص الكلمة developer فحسب. ضم النصوص يمكن ضمّ النصوص اعتبارًا من نصوص أصغر باستخدام عامل الضم + حيث أنّ استخدامه بسيط. انظر الشيفرة البسيطة التالية: string text = "My name is " + "Husam"; بعد تنفيذ هذه العبارة سيحتوي المتغيّر text على النص: "My name is Husam" هناك أمرٌ بسيط لكنّه مهم جرى وراء الكواليس! لقد أنشأ مترجم سي شارب كائنًا نصيًّا جديدًا ليستوعب النص الجديد، ثم وضع مرجعًا لهذا الكائن ضمن المتغيّر text. قد لا يبدو الأمر مقلقًا في حالة مثالنا البسيط هذا، ولكن تخيّل معي ماذا سيحدث في برنامج يتطلّب ضمّ مئات من النصوص مع بعضها (وهذا أمر وارد جدًّا)؟ ستحدث بالتأكيد مشاكل في الأداة، وستغص الذاكرة بمئات الكائنات النصيّة التي لا لزوم لها، والتي تنتظر دورها في التنظيف من قبل جامع النفايات GC الذي يعمل على تحرير ذاكرة تطبيقات دون نت من الكائنات غير المستخدمة في البرنامج. الحل الأمثل في هذه الحالة هو تجنّب استخدام عامل الضم (+)، واستخدام الصنف StringBuilder الموجود ضمن نطاق الاسم System.Text الذي يحل هذه المشكلة بكفاءة عالية. لنستعرض البرنامج Lesson11_01 لهذا الغرض: 1 using System; 2 using System.Text; 3 4 namespace Lesson11_01 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 StringBuilder sb = new StringBuilder(); 11 string[] arrText = { 12 "C# is a powerful language.", 13 "It contains advanced features.", 14 "It makes programming tasks more easier.", 15 "C# is the main programming language in .NET world." 16 }; 17 18 foreach(string text in arrText) 19 { 20 sb.Append(text); 21 } 22 23 Console.WriteLine(sb.ToString()); 24 } 25 } 26 } أنشأنا المتغيّر sb من النوع StringBuilder وأسندنا إليه كائنًا من هذا النوع في السطر 10. بعد ذلك أنشأنا المصفوفة arrText بطريقة مختصرة في السطر 11. ثمّ استخدمنا حلقة foreach للمرور على جميع عناصر المصفوفة arrText وإضافتها واحدًا تلو الآخر إلى المتغيّر sb من خلال التابع Append الذي يتطلّب وسيطًا نصيًّا واحدًا (السطر 20). أخيرًا يعمل التابع WriteLine في السطر 23 على طباعة المحتوى النصيّ الناتج من ضمّ جميع النصوص التي أضفناها باستخدام التابع Append وذلك من خلال استدعاء التابع ToString من المتغيّر sb، ستلاحظ بعد تنفيذ البرنامج أنّ النص المعروض سيكون مضمومًا كما لو استخدمنا عامل الضم (+) على الشكل التالي: C# is a powerful language.It contains advanced features.It makes programming tasks more easier.C# is the main programming language in .NET world. كان من الممكن استخدام تابع آخر من المتغيّر sb في السطر 20 وهو التابع AppendLine. يشبه هذا التابع Append تمامًا باستثناء أنّه يضيف محرف سطر جديد n\ تلقائيًّا إلى كلّ نص جديد تتم إضافته. ملاحظة استخدمنا في السطر 11 أسلوبًا مختصرًا في إنشاء المصفوفات. يمكن استخدام هذا الأسلوب عندما تكون عناصر المصفوفة معروفة ومحدّدة. للأسلوب المختصر الشكل التالي بالنسبة للمصفوفات النصيّة: string[] arrText = {"Text_1", "Text_2", "Text_3", … , "Text_n"}; ويمكن بالطبع استخدام هذا الأسلوب المختصر مع أيّ نوع مصفوفة آخر. تمارين داعمة تمرين 1 ليكن لدينا النص التالي: "black cat and white dog are friends. black cat always brings food to dog, and white dog thanks it." المطلوب كتابة برنامج يحسب عدد الكلمات black وعدد الكلمات dog الموجودة في النص. ثمّ يستبدل كل كلمة white في النص بكلمة brown ويعرض النص المعدّل على الشاشة. تمرين 2 هذا التمرين هو تحدٍّ من نوع آخر! اكتب برنامجًا يطلب من المستخدم نص كيفيّ. ثم أعدّ تقريرًا يتضمّن إحصائيّات حول عدد كل كلمة أدخلها المستخدم، واعرض هذا التقرير على الشاشة. (تلميح: ربما تحتاج إلى شكل مختلف للتابع IndexOf يتطلّب وسيطين: الأوّل هو النص المراد البحث عنه، والثاني هو دليل المحرف الذي سيبدأ البحث اعتبارًا منه. في أمثلتنا السابقة كنّا نبحث افتراضيًّا من المحرف ذو الدليل 0). الخلاصة تعرّفنا في هذا الدرس على الخطوات الأساسيّة في التعامل مع النصوص. حيث تعلّمنا كيف نبحث عن نص محدّد ضمن نص آخر وذلك بأسلوبين مختلفين، بالإضافة إلى إجراء عمليّات استبدال داخل نص. كما تعلّمنا كيف نستخلص جزءًا محدّدًا من نص، وكيف نضمّ النصوص باستخدام الصنف StringBuilder. يُعتبر العمل مع النصوص أساسيًّا ومهمًّا لكلّ مبرمج وخصوصًا في مثل هذه الأيّام، فالبيانات التي يتمّ تبادلها بين التطبيقات المختلفة عبر شبكة الإنترنت تكون على شكل نصوص. سنتناول في الدرس القادم كيفيّة التعامل مع الملفات النصيّة من خلال القراءة والكتابة منها وإليها.
  14. يتحدّث هذا الدرس عن موضوعين مهمّين في سي شارب ألا وهما الواجهات Interfaces والمجموعات Collections. تُعتبر المجموعات collections من المزايا القويّة والمهمّة في سي شارب، وهي وسيلة لتخزين العناصر ضمن بنية قابلة للتوسّع تلقائيًّا وسهلة التعامل. إذًا فهي تشبه المصفوفات، باستثناء أنّ المصفوفات ذات حجم ثابت وهي تبقى كذلك منذ لحظة إنشائها، أمّا المجموعات فهي على النقيض من ذلك، فهي بنية قابلة للتوسّع بقدر ما نرغب. بالإضافة إلى أنّ للمجموعات أشكال عديدة مفيدة جدًّا، فهناك المكدّس stack والرتل queue والقاموس dictionary وغيرها من البنى المهمّة. سنتناول في هذا الدرس المجموعات العاديّة، والمجموعات العموميّة generics collections. سنبدأ هذا الدرس بالحديث المختصر عن الواجهات، وهي تشبه الأصناف ولكن بصورة مجرّدة، حيث تصف سمات عامّة ينبغي أن تتحلّى بها الأصناف التي تحقّقها. كلمة عن الواجهات ليست فكرة الواجهات Interfaces محصورةً بسي شارب، فهي موجودة أيضًا في لغات برمجة أخرى مثل جافا Java. تشبه الواجهة Interface الصنف Class بشكل كبير، فهي تضم خصائص وتوابع، ولكن كتصاريح فقط، بدون شيفرة تحوي عبارات برمجيّة، وبدون محدّدات وصول. كما لا تحتوي على حقول fields ولا على بواني. تستطيع القول أنّ الواجهة تعرّف الهيكل العام بشكل مجرّد فحسب. يمكن الاستفادة من الواجهة عندما نحقّقها implement. فعندما يرث صنف ما من واجهة، فيجب عليه أن يعيد التصريح عن جميع الخصائص والتوابع الموجودة ضمن هذه الواجهة ولكن مع تزويدها بحواضن تحوي الشيفرة المطلوب تنفيذها. فكأنّ الواجهة تحقّق مبدأ التعدديّة الشكليّة بشكل ممتاز، فهي تصرّح عن تابع أو خاصيّة، وتترك للصنف الذي يحقّقها (يرث منها) حريّة التعبير عن هذا التابع أو هذه الخاصيّة بالشكل الملائم. تُستخدم الواجهات على نحو واسع جدًّا في سي شارب، وتتبع تسميتها لنفس أسلوب تسمية الأصناف، مع الاصطلاح على أن يكون الحرف الأوّل من اسم أيّ واجهة هو الحرف الطباعي الكبير I وهو الحرف الأوّل من كلمة Interface. في الحقيقة لقد حلّت الواجهات مشكلة الوراثة المتعدّدة (الوراثة من أكثر من صنف بنفس الوقت) والتي كانت تتمتّع بها لغة C++ فقط. الآن أصبح بإمكان أي صنف أن يرث من صنف آخر واحد فقط، بالإضافة إلى تحقيقه لأي عدد يرغبه من الواجهات. يُصرّح عن الواجهة بالكلمة المحجوزة interface. انظر البرنامج Lesson10_1 الذي يوضّح استخدام الواجهات. هل تذكر الأصناف Animal و Bird و Frog و Fish؟ لقد استعرت الصنفين Animal و Frog لتوضيح فكرة استخدام الواجهات، تذكّر أنّ الصنف Frog كان يرث من الصنف Animal. في الحقيقة لقد استفدت من فكرة أنّ الكائنات الحيّة تتنفّس Breathing. لذلك أنشئت واجهة اسمها IBreathing لتعبّر عن عمليّة التنفّس، وبما أنّ الضفدع Frog هو كائن حيّ، فمن الطبيعي أن يحقّق هذه الواجهة. تحتوي الواجهة IBreathing على تابع وحيد اسمه TakeBreath (السطر 9) يقبل وسيطًا واحدًا من النوع double يُعبّر عن كميّة الأكسجين التي سيحصل عليها الكائن الحيّ عند التنفّس، كما يُرجع هذا التابع قيمة من النوع double أيضًا تمثّل كميّة الأكسجين التي بقيت بعد عمليّة التنفّس. يرث الصنف Frog هذه الواجهة في السطر 20، ويحقّقها من خلال إعادة تعريف التابع TakeBreath في الأسطر من 27 حتى 30 حيث يُعبّر عن عمليّة التنفّس بالشكل الذي يناسبه (سيستهلك في مثالنا هذا 20% من كميّة الأكسجين التي يحصل عليها). في الحقيقة يمكن استخدام الواجهة IBreathing مع أيّ "كائن حيّ" بصرف النظر عن كونه يرث من الصنف Animal (الذي يمثّل الصنف الحيواني) أو من الصنف Mammal (الثديّيات) مثلًا أو غيره، وذلك لأنّ جميع الكائنات الحيّة تشترك معًا بخاصيّة التنفّس، وهنا تمكن قوّة الواجهات. 1 using System; 2 3 namespace Lesson10_01 4 { 5 class Program 6 { 7 interface IBreathing 8 { 9 double TakeBreath(double oxygen_amount); 10 } 11 12 class Animal 13 { 14 public virtual void Move() 15 { 16 Console.WriteLine("Animal: Move General Method"); 17 } 18 } 19 20 class Frog : Animal, IBreathing 21 { 22 public override void Move() 23 { 24 Console.WriteLine("Frog - Move: jumping 20 cm"); 25 } 26 27 public double TakeBreath(double oxygen_amount) 28 { 29 return oxygen_amount * 0.8; 30 } 31 32 } 33 34 static void Main(string[] args) 35 { 36 double oxygent_to_breath = 10; 37 38 IBreathing frog = new Frog(); 39 40 Console.WriteLine("Oxygen amount before breath: {0}", oxygent_to_breath); 41 Console.WriteLine("Oxygen amount after breath: {0}", frog.TakeBreath(oxygent_to_breath)); 42 } 43 } 44 } هناك أمر آخر جدير بالملاحظة. انظر إلى السطر 38 ستجد أنّنا قد صرّحنا عن المتغيّر frog من النوع IBreathing ثمّ أسندنا إليه مرجعًا لكائن من النوع Frog، وهذا أمر صحيح تمامًا وشائع جدًّا لأنّ الصنف Frog يرث من الواجهة IBreathing. عند تنفيذ البرنامج ستحصل على الخرج التالي: Oxygen amount before breath: 10 Oxygen amount after breath: 8 المجموعات في سي شارب المجموعات العادية توجد الأصناف المعبّرة عن هذه المجموعات ضمن نطاق الاسم System.Collection. تلعب نطاقات الأسماء دورًا تنظيميًّا للأصناف، وسنتحدّث عنها بشكل أكبر في درس لاحق. من أبرز المجموعات في نطاق الاسم هذا هو الصنف ArrayList. يحقّق الصنف ArrayList كلّ من الواجهات IList و ICollection و IEnumerable و ICloneable. جميع هذه الواجهات تقع في مكتبة FCL في إطار عمل دوت نت، حيث تعرّف هذه الواجهات العمليّات الأساسيّة التي ينبغي أن يتمتّع بها الصنف ArrayList. تسمح الكائنات من هذه المجموعة بإضافة أي نوع من العناصر لها، حيث من الممكن أن نضيف عناصر من النوع object. يمكن إضافة العناصر إلى هذه المجموعة باستخدام التابع Add الذي يقبل وسيطًا من النوع object. أي أنّنا فعليًّا نستطيع أن نضيف عناصر من أنواع مختلفة لنفس المجموعة. أيّ عنصر تتمّ إضافته سيوضع آخر المجموعة التي هي ذات حجم مرن، فمن الممكن إضافة أي عدد نرغبه من العناصر. أمّا إذا أردنا إضافة عنصر إلى مكان محدّد ضمن القائمة، فعلينا استخدام التابع Insert الذي يحتاج إلى وسيطين، الأوّل هو الدليل المراد إدراج العنصر الجديد ضمنه، والثاني هو العنصر نفسه. انظر البرنامج Lesson10_02 البسيط التالي: 1 using System; 2 using System.Collections; 3 4 namespace Lesson10_02 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 ArrayList values = new ArrayList(); 11 12 values.Add("My "); 13 values.Add("age: "); 14 values.Add(36); 15 16 foreach(object item in values) 17 { 18 Console.Write(item); 19 } 20 21 Console.WriteLine(); 22 } 23 } 24 } استطعنا الحصول على عناصر هذه المجموعة باستخدام حلقة foreach. ولكن إذا أردنا الوصول إلى عنصر محدّد فحسب، ولنقل أنّه العنصر الثالث (القيمة 36) في مثالنا السابق، فيمكن ذلك من خلال الشكل التالي: values[2] تذكّر دومًا أنّ دليل العنصر الأوّل هو 0. في الواقع ستكون القيمة التي سنحصل عليها من [values[2 هي قيمة من نوع object رغم أنّها في حقيقة الأمر تحوي القيمة 36 وهي قيمة من نوع int بطبيعة الحال. السبب في ذلك منطقيّ وهو أنّنا عندما أضفنا القيمة 36 إلى المجموعة كان ذلك باستخدام التابع Add الذي يقبل وسيطًا من النوع object. إذا أردنا الاستفادة من القيمة الفعليّة المخزّنة ضمن [values[2 فعلينا هنا أن نستخدم عامل التحويل (int) على الشكل التالي: int age = (int) values[2]; ملاحظة: عند تمرير القيمة 36 في المثال السابق إلى التابع Add الذي يتوقّع وسيط من نوع object تحدث ظاهرة نسميها التعليب boxing. حيث تُعلَّب القيمة 36 ليصبح بالإمكان تمريرها مكان وسيط يتطلّب النوع object (يبقى هذا الأمر صحيحًا من أجل أي قيمة value type). أمّا عندما نريد استرجاع القيمة الفعليّة فإنّنا نقوم بعمليّة معاكسة تدعى بإلغاء التعليب unboxing باستخدام عامل التحويل بين الأنواع كما فعلنا بالعبارة البرمجيّة الأخيرة: int age = (int) values[2]; هناك العديد من المجموعات الأخرى الموجودة ضمن نطاق الاسم System.Collection، ولكن لن أتحدّث عنها هنا. في الحقيقة إذا أردت نصيحتي حاول ألّا تستخدم المجموعات العاديّة أبدًا! يكمن السبب في ذلك في الفقرة التالية عندما نتحدّث عن المجموعات العموميّة generic collection، حيث سنطّلع على مجموعات تشبه إلى حدٍّ بعيد المجموعات العاديّة الموجودة هنا، ولكنّها عمليّة وأكثر أمانًا. المجموعات العمومية تشبه المجموعات العموميّة generic collections من حيث المبدأ المجموعات العاديّة باستثناء أنّها أكثر أمنًا وأفضل أداءً. حيث ينبغي تعيين نوع العناصر التي ستتعامل معها المجموعة عند التصريح عنها، فتتعامل المجموعة في هذه الحالة مع نوع مُحدّد. من أشهر المجموعات العموميّة هي المجموعة <List<T وهي تعبّر عن القائمة list. قد يبدو الشكل السابق غريبًا قليلًا، ولكنّه في الحقيقة بسيط. استبدل الحرف T بأيّ نوع (صنف) ترغبه وستقبل المجموعة نتيجة لذلك أن يكون عناصرها من هذا النوع. تقع المجموعات العموميّة في نطاق الاسم System.Collections.Generic. سنعدّل البرنامج Lesson09_02 من الدرس السابق الذي كان يسمح بإدخال أسماء ودرجات خمسة طلاب فقط، ويخزّنها على شكل كائنات Student ضمن مصفوفة من النوع []Student وذلك لإيجاد مجموع الدرجات والمعدّل. سنجعل هذا البرنامج يستخدم المجموعة العموميّة <List<Student (مجموعة يمكن لعناصرها تخزين مراجع لكائنات من النوع Student)، سننشئ البرنامج Lesson10_03 لهذا الغرض. 1 using System; 2 using System.Collections.Generic; 3 4 namespace Lesson10_03 5 { 6 class Student 7 { 8 public string Name { get; set; } 9 public int Mark { get; set; } 10 } 11 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 List<Student> listStudents = new List<Student>(); 17 int sum = 0; 18 bool continueCondition = true; 19 int counter = 0; 20 string response; 21 22 Console.WriteLine("Input Students Marks"); 23 Console.WriteLine("====================="); 24 25 //input loop. 26 while(continueCondition) 27 { 28 Student student = new Student(); 29 30 Console.Write("Input student {0} th name: ", counter + 1); 31 student.Name = Console.ReadLine(); 32 33 Console.Write("Input student {0} th mark: ", counter + 1); 34 string tmpMark = Console.ReadLine(); 35 student.Mark = int.Parse(tmpMark); 36 37 listStudents.Add(student); 38 39 Console.WriteLine(); 40 Console.Write("Add another student? (y/n) : "); 41 response = Console.ReadLine(); 42 43 if(response=="n" || response == "N") 44 { 45 continueCondition = false; 46 } 47 48 counter++; 49 } 50 51 Console.WriteLine(); 52 Console.WriteLine("Students Marks Table"); 53 Console.WriteLine("===================="); 54 Console.WriteLine("No\tName\tMark"); 55 56 //calculating sum and display output loop. 57 for (int i = 0; i < listStudents.Count; i++) 58 { 59 sum += listStudents[i].Mark; 60 Console.WriteLine("{0}\t{1}\t{2}", i + 1, listStudents[i].Name, listStudents[i].Mark); 61 } 62 63 Console.WriteLine("-------------------"); 64 Console.WriteLine("Sum\t\t{0}", sum); 65 Console.WriteLine("Average\t\t{0}", sum / (double)listStudents.Count); 66 } 67 } 68 } لقد أجرينا هنا بعض التحسينات. بدأنا البرنامج في السطر 16 بالتصريح عن المتغيّر listStudents من النوع <List<Student وإنشاء كائن من هذا النوع وإسناده لهذا المتغيّر. تقبل المجموعة العموميّة <List<Student بتخزين كائنات من النوع Student ضمنها. لاحظ أنّنا لم نحدّد عدد الكائنات مسبقًا (مع أنّه يمكن ذلك بهدف تحسين الأداء لا غير). وضعنا حلقة while في السطر 26 بدلًا من حلقة for القديمة وذلك لأنّنا لا نعرف على وجه التحديد عدد الطلّاب الذين يرغب المستخدم بإدخال بياناتهم. لاحظ شرط استمرار الحلقة continueCondition الذي يحمل القيمة true بشكل افتراضيّ. أصبح البرنامج غير مقيّدٍ بعدد محدّد من الطلاب، فبعد إدخال بيانات كل طالب، سيعرض البرنامج رسالة يخيّر فيها المستخدم في إضافة المزيد أم التوقّف (السطر 40) فإذا اختار المستخدم التوقّف بإدخاله النص "N" أو "n" عندها سيسند البرنامج القيمة false للمتغيّر continueCondition مما يؤدّي إلى الخروج من حلقة while عند بدء الدورة التالية. تنحصر وظيفة المتغيّر counter الذي صرّحنا عنه في السطر 19 في إظهار ترتيب الطالب الحالي على الشاشة. نستخدم الخاصيّة Count للمجموعة listStudents لمعرفة عدد العناصر الفعليّة المخزّنة ضمنها (تذكّر الخاصيّة Length المماثلة لها في المصفوفات). بعد تنفيذ البرنامج وإدخال بيانات ستة طلّاب، ستحصل على شكل شبيه بما يلي: يوجد تابع اسمه RemoveAt ضمن هذه المجموعة يسمح بإزالة عنصر من القائمة، حيث نمرّر لهذا التابع دليل index العنصر المراد إزالته (دليل العنصر الأوّل هو 0) ليعمل هذا التابع على إزالته وإعادة تعيين أدلّة جميع العناصر بعد إزالة العنصر المطلوب. انظر الشيفرة التالية: List<string> listStrings = new List<string>(); listStrings.Add("Bird"); listStrings.Add("Fish"); listStrings.Add("Frog"); listStrings.RemoveAt(1); أنشأنا في الشيفرة السابقة مجموعة من النوع <List<string (عناصرها من النوع string)، ثمّ أضفنا إليها ثلاثة عناصر. يؤدّي استدعاء التابع (RemoveAt(1 إلى إزالة العنصر ذو الدليل 1 من هذه المجموعة، أي أنّ العنصر ذو القيمة Fish سيُزال من هذه القائمة. يوجد تابع مشابه لهذا التابع اسمه Remove يتطلّب أن تمرّر إليه مرجعًا لكائن موجود في هذه المجموعة لتتم إزالته. فإذا كان النوع العمومي لهذه المجموعة عبارة عن نوع قيمة مثل <List<int أو <List<double فعندها يكفي تمرير القيمة المراد إزالتها للتابع Remove فحسب. علمًا أنّ هذا التابع يزيل أوّل نتيجة تطابق يصادفها في هذه المجموعة. يوجد أيضًا التابع Reverse الذي يعمل على عكس ترتيب العناصر الموجودة في المجموعة، حيث يصبح العنصر الأوّل هو الأخير، والعنصر الأخير هو الأوّل. كما يوجد التابع Sort الذي يعمل على ترتيب العناصر ضمن المجموعة وفق الترتيب الافتراضي (بالنسبة للأنواع المضمّنة) أو وفق ترتيب كيفيّ يمكن للمبرمج أن يختاره. وهناك تابع مفيد آخر وهو BinarySearch الذي يجري خوارزمية البحث الشهيرة على عناصر المجموعة، حيث نمرّر إليه القيمة المراد البحث عنها (أو مرجع الكائن الذي نريد البحث عنه) ويُرجع هذا التابع دليل العنصر ضمن المجموعة في حال وجده. مع الانتباه إلى أنّ هذا الدليل يمثّل دليل العنصر ضمن المجموعة على اعتبارها مرتّبة. إذ أنّه يقوم بترتيبها بشكل داخليّ قبل أن يجري عملية البحث. إذا أردت الحصول على نتائج منطقيّة، فاعمل على ترتيب مجموعتك باستخدام التابع Sort قبل استدعاء BinarySearch. يمكننا الوصول إلى عنصر محدّد ضمن مجموعة عموميّة بنفس الأسلوب التي تحدّثنا عنه في المجموعات العاديّة، مع ملاحظة أنّنا لن نحتاج إلى عامل التحويل بين الأنواع، وبالتالي التخلّص من عمليتيّ التعليب boxing وإلغاء التعليب unboxing. كما يمكن الاستفادة أيضًا من التابع Insert للإدراج ضمن موقع مُحدّد، والذي تحدّثنا عنه أيضًا في المجموعات العاديّة. توجد توابع أخرى مفيدة ضمن المجموعة <List<T ولكنّنا لن نستطيع الخوض فيها قبل أن نتحدّث عن النوّاب delegates في درس لاحق. ملاحظة: توجد طريقة سريعة وفعّالة لإنشاء مجموعة <List<T وإسناد عناصر إليها مباشرةً في حال كان عدد العناصر محدّد ومعروف سلفًا. فإذا أردنا إنشاء مجموعة من النوع <List<int تحوي العناصر 1، 2، 5، 10 يمكنك كتابة العبارة التالية لهذا الغرض: List<int> listNumbers = new List<int>() { 10, 5, 2, 1 }; تنشئ هذه العبارة مجموعة من النوع <List<int وتضيف إليها العناصر 10 و5 و2 و1 ثمّ تسند هذه المجموعة إلى المتغيّر listNumbers. تمارين داعمة تمرين 1 اكتب برنامجًا يطلب من المستخدم إدخال خمس قيم نصيّة، ويخزّنها ضمن مجموعة من النوع <List<string. ثمّ استخدم التابع Reverse لعكس ترتيب العناصر ضمن هذه المجموعة، ثمّ اطبع النتائج على الشاشة. تمرين 2 اكتب برنامجًا يطلب من المستخدم إدخال قيم عدديّة من النوع double بقدر ما يريد، وبعد أن يفرغ من الإدخال، احسب المتوسّط الحسابي (المعدّل) لهذه الأعداد، ورتّبها باستخدام التابع Sort، ثم اطبعها على الشاشة، مع المتوسّط الحسابي لها. (تلميح: استفد من البرنامج Lesson10_03 لسؤال المستخدم هل يريد إضافة عدد جديد أم لا) الخلاصة تعرّفنا في هذا الدرس على الواجهات Interfaces والمجموعات Collections. من النادر أن يخلو أيّ برنامج فعليّ من استخدام المجموعات أو الواجهات، وفي الحقيقة هناك العديد من بنى المجموعات المفيدة التي لم نتناولها في هذا الدرس. سنحاول أن نتوسّع في المزايا القويّة والرّائعة للمجموعات في سلسلة قادمة.
  15. تحدثنا في الدرسين السابقين عن المبادئ الأوليّة لتطبيق مفاهيم البرمجة الكائنيّة التوجّه في سي شارب. سننهي في هذا الدرس تطبيق هذه المبادئ، حيث سنتناول موضوع الوراثة Inheritance والتعدديّة الشكليّة Polymorphism، كما سنتعرّف على محدّد الوصول protected الذي يُستخدم في الوراثة. الوراثة Inheritance سبق وأن قدّمنا للوراثة، واتفقنا على أنّها من أهمّ المفاهيم التي يمكن أن تدعمها لغات البرمجة كائنيّة التوجّه. في الحقيقة يرث أيّ صنف موجود في إطار عمل دوت نت أو أيّ صنف تنشئه بنفسك بشكل مباشر أو غير مباشر من الصنف Object حتى ولو لم نخبر مترجم سي شارب بذلك، حيث سيعمل المترجم على الوراثة منه بشكل ضمنيّ. هذا الصنف ذو دلالة عامّة، وهو غير مفيد كثيرًا كاستخدام بحدّ ذاته. يحتوي الصنف Object على عدد قليل من التوابع كأعضاء ضمنه مثل Equals و GetHashCode و GetType و ToString. التابع الأكثر استخدامًا هو التابع ToString، وهو يُستخدَم عادةً للحصول على التمثيل النصيّ لأيّ كائن. لكي نفهم الوراثة بشكل عمليّ لا بدّ لنا من مثال تمهيديّ. سننشئ لهذا الغرض صنف أب سأسمّيه Father. سيكون هذا الصنف هو الأساس الذي نرث منه. لهذا الصنف الشكل التالي: class Father { public Father() { Console.WriteLine("Father: In Constructor"); } public void MyMethod() { Console.WriteLine("Father: In MyMethod"); } } يحتوي هذا الصنف على التابع MyMethod الذي يحوي الكلمة void قبل اسم التابع مباشرةً. تعني هذه الكلمة أنّ التابع MyMethod لن يُرجع أي قيمة للشيفرة التي استدعته. كما يحتوي الصنف Father على البانية عديمة الوسائط Father. وضعت عبارتي Writeline في كلّ تابع من باب التوضيح. الآن سنعرّف صنفًا جديدًا لنسمّه Child يرث من الصنف Father على الشكل التالي: class Child : Father { } قد يبدو الصنف Child فارغًا إلّا أنّه ليس كذلك. لاحظ من السطر الأوّل لتصريح هذا الصنف كيف وضعنا النقطتان الرأسيّتان (:) ومن ثمّ اسم الصنف Father. يخبر ذلك مترجم سي شارب أنّنا نريد من الصنف Child أن يرث من الصنف Father. في الحقيقة جميع الأعضاء المعرّفة ضمن الصنف Father ستصبح موجودة تلقائيًّا ضمن الصنف Child، بل ويمكن إضافة المزيد من الأعضاء إلى الصنف Child بحسب الحاجة. أنشئ مشروعًا جديدًا وسمّه Lesson08_01، ضع الصنفين السابقين بجوار الصنف Program ضمن فضاء الاسم Lesson08_01 ثم اكتب الشيفرة التالية ضمن التابع Main: Child c = new Child(); c.MyMethod(); نفّذ البرنامج لتحصل على الخرج التالي: Father: In Constructor Father: In MyMethod من الواضح أنّ التنفيذ سيدخل إلى بانية الصنف Father (تذكّر أنّ البانية هي أوّل تابع يُستدعى عند إنشاء الكائن) وإلى التابع MyMethod وكلاهما موجودان ضمن الصنف الأب Father. لنضيف بعض التعديلات على الصنف Child. عدّل الصنف Child ليصبح كما يلي: class Child : Father { public Child() { Console.WriteLine("Child: In Constructor"); } } لاحظ أنّنا قد أضفنا بانية عديمة الوسائط للصنف Child وبداخلها التابع WriteLine لطباعة جملة توضيحيّة. أعد تنفيذ البرنامج السابق لتحصل على الخرج التالي: Father: In Constructor Child: In Constructor Father: In MyMethod الخرج السابق منطقيّ تمامًا. عند إنشاء كائن من الصنف Child باستخدام العبارة: Child c = new Child(); فإنّ بانية الصنف Child سُتستدعى نتيجة لذلك، وبما أنّ الصنف Child يرث من الصنف Father لذلك فإنّ بانية الصنف Father هي من ستُنفّذ أولًا ومن ثمّ بانية الصنف Child. أمّا عند استدعاء التابع MyMethod من الكائن الموجود ضمن المتغيّر c فسنحصل على رسالة الخرج الثالثة كما هو متوقّع. لنجرّب الآن شيئًا آخر. ماذا لو أردنا استبدال محتوى التابع MyMethod بمحتوى خاص بالابن، بمعنى آخر نريد "تجاوز" تعريف التابع MyMethod الموجود في الصنف الأب Father إلى تعريف آخر للتابع MyMethod ولكنّه خاص بالصنف Child. أضف التابع التالي إلى الصنف Child: public new void MyMethod() { Console.WriteLine("Child: In MyMethod"); } لاحظ وجود الكلمة المحجوزة new بعد مُحدّد الوصول public. وظيفة هذه الكلمة في هذا المكان هي إخفاء التابع MyMethod الموجود في الصنف Father واستبداله بالتابع MyMethod الموجود في الصنف Child. الآن بعد تنفيذ البرنامج ستحصل على الخرج التالي: Father: In Constructor Child: In Constructor Child: In MyMethod تمّ المطلوب، لقد أُخفي التابع MyMethod الموجود ضمن الصنف الأب Father لصالح التابع MyMethod الموجود ضمن الصنف الابن Child. يجب أن يبدو البرنامج Lesson08_01 بعد التعديلات الأخيرة شبيهًا بما يلي: 1 using System; 2 3 namespace Lesson08_01 4 { 5 class Father 6 { 7 public Father() 8 { 9 Console.WriteLine("Father: In Constructor"); 10 } 11 12 public void MyMethod() 13 { 14 Console.WriteLine("Father: In MyMethod"); 15 } 16 } 17 18 class Child : Father 19 { 20 public Child() 21 { 22 Console.WriteLine("Child: In Constructor"); 23 } 24 25 public new void MyMethod() 26 { 27 Console.WriteLine("Child: In MyMethod"); 28 } 29 } 30 31 class Program 32 { 33 static void Main(string[] args) 34 { 35 Child c = new Child(); 36 37 c.MyMethod(); 38 39 } 40 } 41 } محدد الوصول protected يُستخدم محدّد الوصول protected في الوراثة. فعندما نُعرّف أحد أعضاء الصنف الأب باستخدام protected فهذا يعني أنّه لا يمكن الوصول إليه مطلقًا إلّا من خلال أعضاء الصنف الأب نفسه، أو من خلال أعضاء الصنف الابن (أو الأحفاد). 1 using System; 2 3 namespace Lesson08_02 4 { 5 class Car 6 { 7 protected string manufacturer; 8 9 public Car() 10 { 11 this.manufacturer = "Car"; 12 } 13 14 public string Manufacturer 15 { 16 Get 17 { 18 return this.manufacturer; 19 } 20 } 21 } 22 23 class Toyota : Car 24 { 25 public Toyota() 26 { 27 this.manufacturer = "Toyota"; 28 } 29 } 30 31 class Program 32 { 33 static void Main(string[] args) 34 { 35 Toyota toyota = new Toyota(); 36 37 Console.WriteLine(toyota.Manufacturer); 38 } 39 } 40 } عند تنفيذ البرنامج ستحصل على الكلمة Toyota في الخرج. السبب في ذلك أنّ بانية الصنف Toyota تصل إلى الحقل manufacturer في السطر 27، رغم أنّه مصرّح عنه في الصنف الأب Car، وذلك لأنّه ذو محدّد وصول protected. من الواضح أنّ الأعضاء المصرّح عنها باستخدام محدّد الوصول private في الأصناف الآباء تبقى مرئيّةً فقط ضمن أعضاء الصنف الأب فحسب. التعددية الشكلية Polymorphism سبق وأن تحدّثنا عن التعدديّة الشكليّة، وكيف أنّها مفهوم أساسيّ في البرمجة كائنيّة التوجّه. يتمحور مفهوم التعدديّة الشكليّة حول أنّه يحق للصنف الابن إعادة صياغة تابع (أو خاصيّة) موجود في صنف أب بصورةٍ تناسبه أكثر. لقد طبّقنا هذا المفهوم قبل قليل وذلك عندما "تجاوز" التابع MyMethod في الصنف الابن Child، التابع MyMethod الموجود في الصنف الأب Father، فأصبح التابع الموجود في الابن يُعبّر عن نفسه بشكل أكثر تخصّصًا. ولكن هذه ليست هي الطريقة المثلى لتنفيذ فكرة التعدديّة الشكلية، تزوّدنا سي شارب في الواقع بأسلوب أفضل بكثير لتحقيق هذا المفهوم. هل تذكر مثال الضفدع Frog والسمكة Fish والطائر Bird وسلوكيّة الانتقال Move التي يرثونها من الصنف Animal؟ تناولنا هذا المثال البسيط في درس سابق. وقد ذكرنا أنّ الصنف Animal هو الصنف الأب للأصناف Frog و Fish و Bird وهو يحتوي على التابع Move الذي يُعبّر عن سلوكيّة الانتقال. وبما أنّ كلًّا من الأصناف الأبناء الثلاثة تُعبّر بشكل مختلف عن عمليّة الانتقال، لذلك فنحن أمام التعدديّة الشكليّة. يحتوي البرنامج Lesson08_03 على صنف أب Animal يحوي تابعًا وحيدًا اسمه Moveـ موسوم بالكلمة المحجوزة virtual التي تجعل منه تابعًا ظاهريًّا يسمح للتوابع الأخرى بتجاوزه. بالإضافة إلى وجود ثلاثة أصناف أبناء للصنف Animal وهي Frog و Fish و Bird. يحتوي كل صنف من الأصناف الأبناء على التابع Move مع وسم خاص هو override. تسمح هذه الكلمة للتابع في الصنف الابن أن "يتجاوز" تعريف نفس التابع في الصنف الأب (موسوم بالكلمة virtual). أعني بكلمة "تجاوز" إعادة تعريف التابع بالشكل الذي يناسب الصنف الابن. فعند الحديث عن الانتقال، فالذي يناسب الضفدع Frog هو القفز، والذي يناسب السمكة Fish هو السباحة، والذي يناسب الطائر Bird بالطبع هو الطيران. وبالمناسبة فإنّ التابع ToString الموجود في الصنف Object هو تابع ظاهريّ (موسوم بالكلمة virtual) ليسمح لأي صنف آخر بتجاوزه. إليك الآن البرنامج Lesson08_03: 1 using System; 2 3 namespace Lesson08_03 4 { 5 class Animal 6 { 7 public virtual void Move() 8 { 9 Console.WriteLine("Animal: Move General Method"); 10 } 11 } 12 13 class Frog : Animal 14 { 15 public override void Move() 16 { 17 Console.WriteLine("Frog - Move: jumping 20 cm"); 18 } 19 } 20 21 class Bird : Animal 22 { 23 public override void Move() 24 { 25 Console.WriteLine("Brid - Move: flying 10 m"); 26 } 27 28 } 29 30 class Fish : Animal 31 { 32 public override void Move() 33 { 34 Console.WriteLine("Fish - Move: swimming 1 m"); 35 } 36 } 37 class Program 38 { 39 static void Main(string[] args) 40 { 41 Frog frog = new Frog(); 42 Fish fish = new Fish(); 43 Bird bird = new Bird(); 44 45 frog.Move(); 46 fish.Move(); 47 bird.Move(); 48 } 49 } 50 } نفّذ البرنامج السابق لتحصل على الخرج التالي: Frog - Move: jumping 20 cm Fish - Move: swimming 1 m Brid - Move: flying 10 m لاحظ كيف يُعبّر كلّ كائن من الأصناف الأبناء عن التابع Move بالشكل الذي يناسبه. وواضح أنّ التابع Move الموجود في الصنف الأب Animal لا يُستدعى مطلقًا. ولكن في بعض الحالات قد نرغب أن يُستدعى التابع المُتجاوَز الموجود في الصنف الأب لإنجاز بعض المهام ومن ثمّ نتابع العمل ضمن التابع المُتجاوِز. يمكننا ذلك ببساطة من خلال استخدام الكلمة المحجوزة base التي تُشير إلى الصنف الأب الذي يرث منه الابن. لاستدعاء التابع Move الموجود في الصنف الأب Animal وذلك من خلال التابع Move الموجود في الصنف Frog أضف العبارة التالية بعد السطر 16 مباشرةً قبل أي عبارة أخرى، ليصبح هذا التابع على الشكل: public override void Move() { base.Move(); Console.WriteLine("Frog - Move: jumping 20 cm"); } أعد تنفيذ البرنامج لتحصل على الخرج التالي: Animal: Move General Method Frog - Move: jumping 20 cm Fish - Move: swimming 1 m Brid - Move: flying 10 m انظر كيف استُدعي التابع Move الموجود في الصنف الأب Animal ومن ثمّ استُدعي التابع Move الموجود في الصنف الابن Frog. التحويل بين الأنواع سنتناول في هذه الفقرة سلوكًا قد يبدو غريبًا بعض الشيء، ولكنّه مهم وأساسيّ وسيصادفك في معظم البرامج التي تكتبها باستخدام سي شارب. أعد البرنامج Lesson08_03 إلى حالته الأصلية (أي أزل العبارة ()base.Move). امسح محتويات التابع Main واستبدلها بالشيفرة التالية: Animal animal = new Frog(); animal.Move(); العبارة الأولى غريبة قليلًا أليس كذلك؟ في الحقيقة الوضع طبيعي تمامًا، فبما أنّ الصنف Animal هو صنف أب للصنف Frog لذلك فيستطيع أيّ متغيّر مصرّح عنه على أنّه من النوع Animal (في مثالنا هذا هو المتغيّر animal) أن يخزّن مرجع إلى كائن من الصنف Frog (تذكّر أنّ التعبير ()new Frog يولّد مرجع لكائن من الصنف Frog). نفّذ البرنامج الآن لتحصل على الخرج التالي: Frog - Move: jumping 20 cm يبدو أنّ برنامجنا ذكيّ كفاية لكي يعرف أنّ الكائن الذي يشير إليه المتغيّر animal هو كائن من الصنف Frog. في الحقيقة يحصل هنا تحويل ضمني بين الكائنات، ولكن إذا فعلنا العكس، أي أسندنا مرجع لكائن من الصنف Animal إلى متغيّر من النوع Frog فسنحصل على خطأ أثناء ترجمة البرنامج. امسح محتويات التابع Main واستبدلها بالشيفرة التالية: Animal animal = new Frog(); Frog frog = animal; نحاول في السطر الثاني أن نُسند المتغيّر animal من النوع Animal إلى المتغيّر frog من النوع Frog، فنحصل على الخطأ التالي عند محاولة تنفيذ البرنامج: Cannot implicitly convert type 'Lesson08_03.Animal' to 'Lesson08_03.Frog'. An explicit conversion exists (are you missing a cast?) يخبرنا هذا الخطأ أنّه لا يمكن التحويل بشكل ضمنيّ من النوع Animal إلى النوع Frog ويقترح علينا استخدام عامل التحويل بين الأنواع casting (هل تذكره؟). رغم أنّ المتغيّر animal يحمل مرجع إل كائن من الصنف Frog في حقيقة الأمر (انظر السطر الأوّل من الشيفرة السابقة) إلّا أنّنا عند محاولتنا إسناد المتغيّر animal إلى المتغيّر frog حصلنا على خطأ. السبب في ذلك هو أنّه لا يحدث تحويل ضمنيّ بين الأنواع implicit conversion وإنّما يتطلّب الأمر إجراء تحويل صريح باستخدام عامل التحويل بين الأنواع. إذا استبدلت السطر الثاني من الشيفرة السابقة بالسطر التالي، ستكون الأمور على ما يرام: Frog frog = (Frog)animal; لاحظ كيف وضعنا عامل التحويل (Frog) أمام المتغيّر animal. سيضمن ذلك حدوث التحويل المطلوب دون أيّ مشاكل. لكي تريح نفسك من التفكير متى يحدث التحويل الضمنيّ ومتى يجب استخدام التحويل الصريح، تذكّر منّي القاعدة التالية: "في حياتنا اليوميّة، كثيرًا ما يحتوي الأب ابنه، ولكنّ العكس ليس صحيحًا". أعلم أنّ لهذه القاعدة شواذ في واقعنا، ولكنّها في البرمجة لا تخيب! فالمتغيّر من النوع الأب يستطيع استقبال أي مرجع لكائن من صنف ابن، ولكنّ العكس ليس صحيح ما لم نستخدم التحويل الصريح بين الأنواع. تحدث ظاهرة التحويل بين الأنواع بالنسبة للأنواع المضمّنة built-in أيضًا. فهناك تحويلات تحدث ضمنيًّا، وأخرى تحدث بتدخّل من المبرمج باستخدام عامل التحويل بين الأنواع، ولكن مع فرق جوهريّ. ففي هذه الحالة ليس بالضرورة أن يكون بين الأنواع التي تجري عمليّة التحويل فيما بينها أي علاقة وراثة. فمثلًا يمكن التحويل ضمنيًّا بين متغيّر من النوع int إلى آخر من النوع double: int i = 6; double d = i; أمّا إذا حاولنا فعل العكس: double d = 6; int i = d; فسنحصل على نفس الخطأ السابق الذي يخبرنا بوجوب استخدام التحويل الصريح بين الأنواع. يمكن حل هذه المشكلة ببساطة باستخدام عامل التحويل (int) ووضعه أمام المتغيّر d في السطر الثاني: double d = 6; int i = (int)d; نخبر المترجم هنا أنّنا نريد التحويل فعليًّا من double إلى int. ستحتاج إلى مثل هذه التقنيّة دومًا إذا كانت عمليّة التحويل ستؤدّي إلى ضياع في البيانات. فالتحويل من double إلى int سيؤدّي إلى ضياع القيمة على يمين الفاصلة العشريّة لأنّ المتغيّرات من النوع int لا تقبلها. وكذلك الأمر عند التحويل من float إلى double لأنّ المتغيّرات من النوع float ذات دقّة أقل من المتغيّرات من النوع double، وهكذا. تمارين داعمة تمرين 1 عدّل البرنامج Lesson08_03 ليعمل كل صنف من الأصناف Frog و Bird و Fish على تجاوز التابع ToString (الموجود في الصنف الأب Object). بحيث عند استدعاء التابع ToString من كائن من الصنف Frog نحصل على النص "I am Frog"، وهكذا بالنسبة للصنفين الباقيين كلّ حسب اسمه. تمرين 2 استفد من البرنامج Lesson08_02 في إنشاء صنف جديد اسمه Corolla يرث من الصنف Toyota. وأضف إلى الصنف الجديد الخاصيّة ProductionYear من النوع int. بعد ذلك أنشئ كائنًا من الصنف Corolla وحاول إسناد قيم لهذه الخاصيّة، وقرائتها منها. الخلاصة تعرّفنا في هذا الدرس على كيفيّة تطبيق الوراثة في سي شارب، كما تعرّفنا على مبادئ التعدديّة الشكليّة Polymorphism وأهميّتها وكيفيّة استثمارها في هذه اللغة. وتعاملنا أيضًا مع التحويل بين الأنواع ورأينا كيف يمكن لمتغيّر من صنف أب أن يحمل مراجع لكائنات من أصناف أبناء. تُستَخدم هذه الأساليب على نحو واسع جدًّا في مكتبة الأصناف الأساسيّة، وستحتاجها في العديد من التطبيقات التي تُنشئها.
  16. تحدثنا في الدرس السابق عن المبادئ الأوليّة لتطبيق مفاهيم البرمجة كائنيّة التوجّه في سي شارب، حيث تعلّمنا كيفيّة إنشاء الأصناف وأعضائها، وإنشاء الكائنات من الأصناف والتعامل معها. سنتابع في هذا الدرس الحديث عن تطبيق مبادئ البرمجة كائنيّة التوجّه في سي شارب حيث سنتناول مُحدّد الوصول private (تناولنا محدّد الوصول public في الدرس السابق). وسنتحدّث أيضًا عن الخصائص Properties كنوع جديد من أعضاء الصنف، والفرق بينها وبين حقول البيانات Data Fields. وسنختم هذا الدرس بالحديث عن الأعضاء الساكنة Static Members في الصنف. الخصائص Properties للخصائص ولحقول البيانات المعنى المنطقيّ نفسه في البرمجة كائنيّة التوجّه، إلّا أنّ سي شارب تميّز بينهما من الناحية العمليّة. للخصائص في سي شارب مرونة أكبر، حيث من الممكن تنفيذ عبارات برمجيّة عند إسناد قيمة إلى خاصيّة أو حتى عند القراءة منها. كما من الممكن أن نجعل إحدى الخواص قابلة للقراءة فقط أو حتى قابلة للكتابة فقط (ولو أنّه أمر نادر الحدوث). لفهم هذه المزايا بشكل جيّد سأستعير البرنامج Lesson06_02 من الدرس السابق، وأجري عليه بعض التعديلات لإدخال مفهوم الخصائص. 1 using System; 2 3 namespace Lesson07_01 4 { 5 6 class Employee 7 { 8 private string firstName; 9 private string lastName; 10 private double salary; 11 12 public string FirstName 13 { 14 get 15 { 16 return this.firstName; 17 } 18 set 19 { 20 this.firstName = value; 21 } 22 } 23 24 public string LastName 25 { 26 get 17 { 28 return this.lastName; 29 } 30 set 31 { 32 this.lastName = value; 33 } 34 } 35 36 37 public double Salary 38 { 39 get 40 { 41 return this.salary; 42 } 43 set 44 { 45 this.salary = value; 46 } 47 } 48 49 public string DisplayInfo() 50 { 51 string result = string.Format("{0} {1} - Salary: {2:N0}", 52 this.FirstName, this.LastName, this.Salary); 53 54 return result; 55 } 56 57 public Employee(string firstName, string lastName, double salary) 58 { 59 this.FirstName = firstName; 60 this.LastName = lastName; 61 this.Salary = salary; 62 } 63 64 public Employee() 65 { 66 67 } 68 } 69 70 71 class Program 72 { 73 static void Main(string[] args) 74 { 75 Employee employee1, employee2; 76 77 employee1 = new Employee("Mohammad", "Mansoor", 1000); 78 employee2 = new Employee("Saleh", "Mahmoud", 2500); 79 80 Console.WriteLine("First Employee: {0}", employee1.DisplayInfo()); 81 Console.WriteLine("Second Employee: {0}", employee2.DisplayInfo()); 82 } 83 } 84 } عند تنفيذ البرنامج Lesson07_01 سنحصل على نفس الخرج الذي حصلنا عليه في البرنامج Lesson06_02. لقد أجرينا في الحقيقة بعض التعديلات التي تبدو للوهلة الأولى أنّها ليست ذات مغزى. لقد استبدلنا محدّد الوصول للحقول في الأسطر من 8 حتى 10 ليصبح private بدلًا من public (كما كان الوضع في البرنامج Lesson06_02). يفيد مُحدّد الوصول هذا في جعل الحقل خاصًّا بالصنف ولا يمكن الوصول إليه من خارج الكائن المُنشَأ من هذا الصنف، وبالتالي لا يمكن لأحد أن يُعدّل عليه إلّا التوابع الموجودة ضمن نفس الصنف حصرًا. الأمر الآخر أنّنا قد جعلنا أسماء الحقول تبدأ بحرف طباعي صغير وذلك لتمييزها عن الخصائص التي ستأتي بعدها والتي تحمل نفس الاسم ولكن بحرف طباعي كبير. يبدأ التصريح عن الخاصيّة FirstName في السطر 12 ويمتدّ حتى السطر 22. للخصائص في سي شارب فوائد عظيمة سنختبرها بالتدريج في هذا الدرس وفي الدروس اللّاحقة. لاحظ وجود النوع string قبل اسم الخاصيّة في السطر 12، يُشير ذلك إلى أنّ هذه الخاصيّة تقبل وتعطي قيمًا نصيّة فحسب. في الحقيقة يمكن استبدال string بأيّ نوع نحتاجه. من الواضح أنّ التصريح عن الخاصيّة FirstName يتألّف من قسمين: قسم القراءة get (بين السطرين 14 و 17) وقسم الإسناد set (بين السطرين 18 و 21). في الواقع لا تتعدّى الخاصيّة كونها وسيلة للوصول إلى الحقول الخاصّة private fields الموجودة ضمن الكائن سواءً بالقراءة أو الإسناد، ولكن مع إمكانيّة معالجة القيم سواءً قبل إسنادها إلى هذه الحقول أو بعد القراءة منها. عندما نحاول إسناد قيمة إلى الخاصيّة FirstName ستُنفّذ العبارات البرمجيّة الموجودة في قسم set، وهي عبارة برمجيّة واحدة فقط في مثالنا هذا: this.firstName = value; الكلمة value هي كلمة محجوزة تحتوي على القيمة المُسندة إلى الخاصيّة FirstName. العبارة السابقة واضحة للغاية فهي تعمل على إسناد القيمة المخزّنة ضمن value إلى الحقل firstName. لاحظ كيف يمكننا الوصول إلى هذا الحقل من الكلمة this، كما ويمكننا إغفالها. نستطيع الوصول إلى الحقل firstName رغم أنّه ذو محدّد وصول private لأنّنا نصل إليه من تابع يقع في نفس الصنف. أمّا عندما نحاول قراءة الخاصيّة FirstName فسيتمّ تنفيذ العبارات البرمجيّة الموجودة ضمن القسم get. في هذا المثال يحتوي القسم get على عبارة برمجيّة واحدة وهي: return this.firstName; حيث تعمل على إرجاع القيمة المخزّنة ضمن الحقل firstName إلى الشيفرة التي طلبت قراءة الخاصيّة FirstName. يطبّق نفس الأمر تمامًا على الخاصيّتين LastName و Salary. الخصائص المطبقة تلقائيا يبدو البرنامج السابق طويلًا بلا مبرّر، فنحن لم نقم بأيّ عمل ضمن الخصائص سوى الإسناد أو القراءة. إذا كان الأمر كذلك في برامجك الحقيقيّة فيمكنك الاستغناء عن هذا الشكل من الخصائص واللجوء إلى شكل أكثر حداثةً وعصريّة، والذي يتمثّل بالخصائص المطبّقة تلقائيًّا auto implemented properties. انظر الشكل العام لها فيما يتعلّق بالخاصيّة FirstName: public string FirstName { get; set; } لم تعد العبارات البرمجيّة في قسميّ get و set موجودة. ينحصر دور هذه الخاصيّة في شكلها الحالي في تخزين القيم ضمن الخاصيّة FirstName والقراءة منها فقط. ولكن يأتي السؤال هنا، أين ستخزّن الخاصيّة FirstName قيمها، فأنا لا أرى حقلًا للتخزين! يعمل المترجم في هذه الحالة على إنشاء حقل خاص غير مُشاهد في شيفرة MSIL وظيفته الاحتفاظ بقيمة الخاصيّة FirstName. وهنا قد يجول بخاطرك سؤال آخر: لماذا كلّ هذا التعقيد، لماذا لا نستخدم الحقول كما كنّا نفعل في الدرس السابق وحسب؟ الإجابة بسيطة على هذا التساؤل المشروع. فنحن نستخدم الخصائص بهذا الشكل لغايات تصميميّة فحسب. فكلّما كنت بحاجة لأن تُضيف خاصيّة لأحد الأصناف يمكن الوصول إليها من خارجه فافعل ذلك عن طريق الخصائص (وليس الحقول) ولن تندم، وإن بدا ذلك يتطلّب المزيد من العمل. سنطبّق هذه الخصائص الفريدة على برنامجنا المعدّل Lesson07_02 بحيث تستغني تمامًا عن الحقول firstName و lastName و salary. 1 using System; 2 3 namespace Lesson07_02 4 { 5 6 class Employee 7 { 8 public string FirstName { get; set; } 9 public string LastName { get; set; } 10 public double Salary { get; set; } 11 12 public string DisplayInfo() 13 { 14 string result = string.Format("{0} {1} - Salary: {2:N0}", 15 this.FirstName, this.LastName, this.Salary); 16 17 return result; 18 } 19 20 public Employee(string firstName, string lastName, double salary) 21 { 22 this.FirstName = firstName; 23 this.LastName = lastName; 24 this.Salary = salary; 25 } 26 27 public Employee() 28 { 29 30 } 31 } 32 33 34 class Program 35 { 36 static void Main(string[] args) 37 { 38 Employee employee1, employee2; 39 40 employee1 = new Employee("Mohammad", "Mansoor", 1000); 41 employee2 = new Employee("Saleh", "Mahmoud", 2500); 42 43 Console.WriteLine("First Employee: {0}", employee1.DisplayInfo()); 44 Console.WriteLine("Second Employee: {0}", employee2.DisplayInfo()); 45 } 46 } 47 } أصبح هذا البرنامج الآن يُشبه البرنامج Lesson06_02 من الدرس السابق إلى حدّ كبير، باستثناء أنّنا نستخدم هنا الخصائص بدلًا من الحقول. لاحظ فقط أنّه يمكننا كتابة التصريح عن أيّ خاصيّة على نفس السطر مثل الأسطر 8 و 9 و 10. الخصائص ذات إمكانية القراءة فقط هل تذكُر التمرين الداعم الأوّل من الدرس السابق؟ كان يطلب ذلك التمرين إضافة تابع جديد اسمه GetSalaryAfterTax للحصول على قيمة الراتب بعد خصم الضريبة. واتفقنا وقتها أن تكون هذه الضريبة 2%. سنضيف خاصيّةً لتقوم بهذه المهمّة بدلًا من هذا التابع، ولكنّنا سنجعلها للقراءة فقط read only. أي لا يمكن إسناد أي قيم لها. ستكون الخاصيّة SalaryAfterTax الجديدة على الشكل التالي: public double SalaryAfterTax { get { return 0.98 * this.Salary; } } من الواضح أنّه قد أزلنا القسم set المسؤول عن الإسناد من تصريح الخاصيّة SalaryAfterTax وبذلك تتحوّل للقراءة فقط. يحتوي القسم get على عمليّة حسابيّة بسيطة تطبّق عملية حسم الضريبة على الراتب Salary. سنجري تعديلًا طفيفًا على التابع DisplayInfo لكي يُرفق قيمة الراتب بعد حسم الضريبة ضمن النصّ المنسّق الذي يرجعه. سنحصل في النتيجة على البرنامج Lesson07_03 المعدّل: 1 using System; 2 3 namespace Lesson07_03 4 { 5 6 class Employee 7 { 8 public string FirstName { get; set; } 9 public string LastName { get; set; } 10 public double Salary { get; set; } 11 public double SalaryAfterTax 12 { 13 get 14 { 15 return 0.98 * this.Salary; 16 } 17 } 18 19 public string DisplayInfo() 20 { 21 string result = string.Format("{0} {1} \n Salary: {2:N0} \n Salary after tax: {3:N0}", 22 this.FirstName, this.LastName, this.Salary, this.SalaryAfterTax); 23 24 return result; 25 } 26 27 public Employee(string firstName, string lastName, double salary) 28 { 29 this.FirstName = firstName; 30 this.LastName = lastName; 31 this.Salary = salary; 32 } 33 34 public Employee() 35 { 36 37 } 38 } 39 40 41 class Program 42 { 43 static void Main(string[] args) 44 { 45 Employee employee1, employee2; 46 47 employee1 = new Employee("Mohammad", "Mansoor", 1000); 48 employee2 = new Employee("Saleh", "Mahmoud", 2500); 49 50 Console.WriteLine("First Employee: {0}", employee1.DisplayInfo()); 51 Console.WriteLine("Second Employee: {0}", employee2.DisplayInfo()); 52 } 53 } 54 } أضفت الخاصيّة SalaryAfterTax (الأسطر من 11 حتى 17). وأجريت تعديلًا طفيفًا ضمن التابع DisplayInfo في السطر 21 حيث أضفت المحرف n\ والذي يُستخدم ضمن النص للإشارة إلى وجوب الانتقال إلى سطر جديد لأغراض تنسيقية فقط، كما أضفت مكانًا في النصّ التنسيقيّ {3:N0} لإدراج قيمة الراتب بعد خصم الضريبة this.SalaryAfterTax، وهذا كلّ ما في الأمر. إذا حاولت في هذا البرنامج أن تُسند أيّ قيمة إلى الخاصيّة SalaryAfterTax ستحصل على خطأ يفيد أنّها للقراءة فقط read only. الأعضاء الساكنة Static Members هي أعضاء يمكن استدعاؤها مباشرةً من الصنف الذي صُرّحت ضمنه، وليس من كائن مُنشَأ من هذا الصنف. يمكن أن نجعل أيّ عضو ساكن وذلك بوسمه بالكلمة المحجوزة static. يوضّح البرنامج Lesson07_04 استخدام التوابع الساكنة. لاحظ وجود الكلمة المحجوزة static بعد محّدد الوصول public: 1 using System; 2 3 namespace Lesson07_04 4 { 5 class Calculator 6 { 7 public static double Addition(double x, double y) 8 { 9 return x + y; 10 } 11 12 public static double Minus(double x, double y) 13 { 14 return x - y; 15 } 16 17 public static double Division(double x, double y) 18 { 19 if (y == 0) 20 { 21 return double.NaN; 22 } 23 else 24 { 25 return x / y; 26 } 27 } 28 29 public static double Multiplication(double x, double y) 30 { 31 return x * y; 32 } 33 } 34 35 class Program 36 { 37 static void Main(string[] args) 38 { 39 double x = 5; 40 double y = 9; 41 42 double addition = Calculator.Addition(x, y); 43 double minus = Calculator.Minus(x, y); 44 double multiplication = Calculator.Multiplication(x, y); 45 double division = Calculator.Division(x, y); 46 47 Console.WriteLine("{0} + {1} = {2}", x, y, addition); 48 Console.WriteLine("{0} - {1} = {2}", x, y, minus); 49 Console.WriteLine("{0} * {1} = {2}", x, y, multiplication); 50 Console.WriteLine("{0} / {1} = {2}", x, y, division); 51 } 52 } 53 } نفّذ البرنامج السابق لتحصل على الخرج التالي: 5 + 9 = 14 5 - 9 = -4 5 * 9 = 45 5 / 9 = 0.555555555555556 أنشأنا الصنف Calculator الذي يحتوي على التوابع الساكنة Addition و Minus و Multiplication و Division. إذا انتقلنا إلى التابع Main (الذي هو بالمناسبة تابع ساكن بسبب وجود الكلمة static) انظر إلى السطر 42 كيف استدعينا التابع Addition من الصنف Calculator مباشرةً بدون إنشاء أي كائن من هذا الصنف. سنكرّر نفس العمليّة من أجل التوابع Minus و Multiplication و Division. أمرٌ أخير. انظر إلى محتوى التابع الساكن Division، ستجد أنّنا نختبر قيمة y فيما إذا كانت تساوي الصفر أم لا. فإذا كانت قيمة y تساوي الصفر فإنّه لا يجوز القسمة على صفر. لذلك فنرجع double.NaN وهو عبارة عن ثابت يُعبّر عن عدم وجود قيمة عدديّة. وهذا أمر طبيعي لأنّ القسمة على صفر لن تعطينا عدد. إذا استبدلت قيمة y في السطر 40 من البرنامج السابق بالقيمة 0 سنحصل على الخرج التالي: 5 + 0 = 5 5 - 0 = 5 5 * 0 = 0 5 / 0 = NaN لاحظ السطر الأخير من البرنامج كيف يبدو منطقيًّا تمامًا. يمكننا تعميم نفس المفهوم السابق بالنسبة للخصائص والحقول ضمن الصنف بجعلها ساكنة وذلك بإضافة الكلمة المحجوزة static بعد محدّد الوصول مباشرةً. بقي أن نشير إلى أنّه من غير الممكن استخدام الكلمة المحجوزة this ضمن أي عضو ساكن والسبب كما أعتقد واضح. يُشير this إلى الكائن الذي يحدث من ضمنه الاستدعاء. ولكن في الأعضاء الساكنة فإنّنا نجري الاستدعاءات للتوابع أو الخصائص من الصنف المصرّحة ضمنه مباشرةً، لذلك فاستخدام this لن يكون له أيّ معنى. تمارين داعمة تمرين 1 أجرِ تعديلًا على البرنامج Lesson07_03 بحيث يُضيف الخاصيّة Tel (قابلة للقراءة وللكتابة) التي تمثّل رقم الهاتف للموظّف Employee، ثمّ أجرِ التعديل المناسب على التابع DisplayInfo لكي يعرض قيمة هذه الخاصيّة على سطر منفصل كما يفعل مع بقيّة الخصائص. تمرين 2 أنشئ صنفًا سمّه StaticDemo بحيث يحتوي على خاصيّة ساكنة اسمها Counter من النوع int. عند كل إنشاء لكائن من هذا الصنف يجب زيادة قيمة هذه الخاصيّة بمقدار 1 وبشكل تلقائيّ. (تلميح: يمكنك كتابة العبارة البرمجيّة المسؤولة عن زيادة قيمة الخاصيّة Counter ضمن بانيّة الصنف StaticDemo التي ليس لها وسائط.) الخلاصة تعرّفنا في هذا الدرس على كيفيّة التعامل مع الخصائص، والتي تمثّل أسلوبًا أكثر تطوّرًا من الحقول لتشكّل مزايا الصنف. كما اتفقنا أنّه ينبغي على الحقول أن تكون داخليّة بالنسبة للصنف باستخدام محدّد الوصول private، وتعرّفنا أيضًا على الأعضاء الساكنة static members وكيفيّة التعامل معها.
  17. تُعتبر الحلقات التكراريّة من البنى المهمّة في لغات البرمجة، حيث نستطيع من خلالها تنفيذ عبارة أو عدّة عبارات برمجيّة لعدد من المرّات. تدعم سي شارب مثل باقي لغات البرمجة نوعين من الحلقات التكراريّة من حيث عدد التكرار، فهناك الحلقات ذات العدد المحدّد من المرّات (حلقة for) والتي نعلم فيها عدد مرّات التكرار بشكل مسبق، والحلقات ذات العدد غير المحدّد من المرّات (حلقة do-while وحلقة while) التي يكون فيها عدد مرّات التكرار غير مُحدّدًا. حلقة for التكرارية يمكن من خلال هذه الحلقة تكرار تنفيذ عبارة برمجيّة أو أكثر عددًا محدّدًا من المرّات، ولهذه الحلقة الشكل العام التالي: for ([init_counter]; [loop_condition]; [counter_expression]) { statement1; statement2; ... } القسم [init_counter] هو قسم التهيئة الذي يتمّ من خلاله تهيئة متغيّر الحلقة بقيمة ابتدائيّة (وغالبًا ما يتمّ التصريح عنه في هذا القسم أيضًا)، يسمّى متغيّر الحلقة أيضًا بعدّاد الحلقة loop counter، أمّا القسم [loop_condition] فيمثّل شرط التكرار أو الاستمرار للحلقة، فهو تعبير مقارنة يعطي true أو false بحيث تستمرّ الحلقة بالتكرار طالما كان هذا الشرط محقّقًا (يعطي true). القسم الأخير [counter_expression] ويتمّ فيه عادةً إجراء عملية حسابية على متغيّر الحلقة وغالبًا ما تكون هذه العمليّة هي زيادة متغيّر الحلقة بمقدار واحد. انظر الشيفرة البسيطة التالية التي تعمل على تنفيذ العبارة التي تحوي التابع WriteLine ثلاث مرّات: for (int i = 0; i < 3; i++) { Console.WriteLine("i = {0}", i); } عند تنفيذ الشيفرة السابقة ستحصل على الخرج التالي: i = 0 i = 1 i = 2 لاحظ أنّنا صرّحنا عن المتغيّر i من النوع int وأسندنا إليها القيمة 0 في قسم التهيئة، وبالنسبة لشرط الاستمرار للحلقة i < 3 فمن الواضح أنّ الحلقة ستستمرّ بالتكرار طالما كانت قيمة i أصغر تمامًا من 3. أمّا بالنسبة للقسم الأخير فنعمل على زيادة قيمة متغيّر الحلقة i بمقدار 1 في كلّ دورة عن طريق عامل الزيادة اللاحق ++i. آلية عمل هذه الحلقة بسيطة: عندما يصل تنفيذ البرنامج إلى حلقة for يتمّ التصريح عن المتغيّر i وإسناد القيمة 0 إليه. يختبر البرنامج شرط استمرار الحلقة i < 3 فإذا كان true يبدأ بتنفيذ العبارات البرمجيّة الموجودة ضمن حاضنة for. وإلّا يخرج فورًا من الحلقة. بعد الانتهاء من تنفيذ العبارات ضمن حاضنة for، ينتقل البرنامج إلى التعبير ++i ليزيد قيمة i بمقدار 1. تتكرّر نفس الخطوتين 2 و 3. سيتكرّر في هذا المثال البسيط تنفيذ العبارة الموجودة في الحاضنة ثلاث مرّات لأنّ العدّ يبدأ من الصفر (قيمة i الابتدائيّة تساوي الصفر). لنتناول الآن برنامجًا عمليًّا وظيفته إيجاد مجموع سلسلة من الأعداد المتتالية. أنشئ برنامجًا جديدًا اسمه Lesson04_1 ثمّ استبدل محتويات الملف Program.cs بالشيفرة التالية: 1 using System; 2 3 namespace Lesson04_1 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int n, sum = 0; 10 string str_n; 11 12 Console.WriteLine("This Program calculates the series: sum = 1 + 2 + 3 + ... + n"); 13 Console.Write("Input 'n' please: "); 14 str_n = Console.ReadLine(); 15 16 n = int.Parse(str_n); 17 18 for (int i = 1; i <= n; i++) 19 { 20 sum += i; 21 } 22 23 Console.WriteLine("sum = {0}", sum); 24 } 25 } 26 } يعمل البرنامج السابق على جمع الأعداد من 1 حتى القيمة المدخلة n. أي سيحسب برنامجنا مجموع السلسلة: 1 + 2 + 3 + … + n. لاحظ القيمة الابتدائيّة للمتغيّر i (تساوي 1) وشرط استمرار الحلقة i <= n في السطر 18. تذكّر أنّ العبارة الموجودة في السطر 20 تُكافئ العبارة sum = sum + i. نفّذ البرنامج وجرّب إدخال قيم مختلفة للمتغيّر n لتحصل على المجاميع الموافقة. ملاحظة: المتغيّر i في البرنامج السابق مرئي فقط ضمن الحلقة التكراريّة ولا وجود له خارجها، يعرف هذا بمجال الرؤية للمتغيّر variable scope. ستؤدّي محاولة الوصول للمتغيّر i خارج الحلقة إلى خطأ أثناء بناء البرنامج. سنكتب الآن برنامجًا آخرًا لحساب مجموع السلسلة: 2 + 4 + 6 + 8 + … + n. لن يختلف البرنامج في هذا المثال عن البرنامج Lesson04_1 باستثناء أنّنا سنجمع الأعداد الزوجية فقط. إليك البرنامج كما سيبدو: 1 using System; 2 3 namespace Lesson04_2 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int n, sum = 0; 10 string str_n; 11 12 Console.WriteLine("This Program calculates the series: sum = 2 + 4 + 6 + ... + n"); 13 Console.Write("Input 'n' please: "); 14 str_n = Console.ReadLine(); 15 16 n = int.Parse(str_n); 17 18 for(int i = 0; i <= n; i += 2) 19 { 20 sum += i; 21 } 22 23 Console.WriteLine("sum = {0}", sum); 24 } 25 } 26 } لاحظ كيف نزيد قيمة المتغيّر i في كلّ تكرار للحلقة for بمقدار 2 باستخدام التعبير i += 2 (السطر 18)، وبالتالي نتفادى جمع الأعداد الفردية (لاحظ أنّ قيمة i بدأت من الصفر). فيما عدا ذلك يبدو هذا البرنامج مطابقًا لبنية البرنامج Lesson04_1. حلقة while التكرارية لهذه الحلقة التكراريّة الشكل العام التالي: while (loop_condition) { statement1; statement2; ... } ستتكرّر العبارات البرمجيّة الموجودة ضمن حاضنة while طالما كان الشرط loop_condition محقّقًا (أي true) وبمجرّد أن يصبح الشرط loop_condition غير محقّق تتوقّف الحلقة عن التكرار. سنعدّل البرنامج Lesson04_1 السابق لكي يسمح باستخدام الحلقة while. أنشئ مشروعًا جديدًا وسمّه Lesson04_3 ثم استبدل محتويات Program.cs بما يلي: 1 using System; 2 3 namespace Lesson04_3 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int n, sum = 0, i = 1; 10 string str_n; 11 12 Console.WriteLine("This Program calculates the series: sum = 1 + 2 + 3 + ... + n"); 13 Console.Write("Input 'n' please: "); 14 str_n = Console.ReadLine(); 15 16 n = int.Parse(str_n); 17 18 while (i <= n) 19 { 20 sum += i; 21 22 i++; 23 } 24 25 Console.WriteLine("sum = {0}", sum); 26 } 27 } 28 } صرّحنا عن المتغيّر i وأسندنا له القيمة 1 في السطر 9 والذي سيمثّل متغيّر حلقة while. استبدلنا حلقة for بحلقة while في السطر 18 مع ملاحظة أنّ شرط استمرار الحلقة i <= 5 بقي دون تعديل. لاحظ العبارة المهمّة في السطر 22 والتي تحوي التعبير ++i الذي سيزيد قيمة i بمقدار واحد في كلّ دورة. إنّ إغفال هذه العبارة سيؤدّي إلى الدخول في حلقة لا نهائيّة، لأنّ شرط الاستمرار في هذه الحالة لن يعطي false أبدًا لأنّ قيمة i لن تتغيّر. نفّذ البرنامج وأدخل قيم مختلفة للمتغيّر n لاختبار البرنامج. جرّب الآن إدخال القيمة 0 للمتغيّر n ستحصل في الخرج على المجموع sum = 0 وهذا منطقيّ. إذ أنّنا نخبر البرنامج بأنّنا لا نريد جمع أي عدد. سبب الحصول على هذا الخرج في الواقع هو أنّ البرنامج أثناء التنفيذ لن يدخل إلى حلقة while مطلقًا لأنّ شرط الاستمرار i <= n سيكون غير محقّقًا منذ البداية (تذكّر أنّ قيمة i الابتدائيّة هي 1). حلقة do-while التكرارية لهذه الحلقة الشكل العام التالي: do { statement1; statement2; ... } while (loop_condition) وهي تشبه الحلقة while باستثناء أنّ شرط استمرار الحلقة loop_condition يجري اختباره في نهايتها وليس في بدايتها كما هو الحال مع حلقة while. قد لا يبدو هذا الأمر مهمًّا في البداية ولكنّه في الحقيقة عكس ذلك تمامًا. لفهم الفرق أنشئ مشروعًا جديدًا وسمّه Lesson04_04 ثمّ استبدل الشيفرة الموجودة في Program.cs بالشيفرة التالية: 1 using System; 2 3 namespace Lesson04_4 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 int n, sum = 0, i = 1; 10 string str_n; 11 12 Console.WriteLine("This Program calculates the series: sum = 1 + 2 + 3 + ... + n"); 13 Console.Write("Input 'n' please: "); 14 str_n = Console.ReadLine(); 15 16 n = int.Parse(str_n); 17 18 Do 19 { 20 sum += i; 21 22 i++; 23 } 24 while (i <= n); 25 26 Console.WriteLine("sum = {0}", sum); 27 } 28 } 29 } لا يختلف هذا البرنامج عن سابقيه في حساب مجموع السلسلة 1 + 2 + 3 + … + n، نفّذ البرنامج وأدخل القيمة 0 للمتغيّر n ستحصل في الخرج على المجموع sum = 1 وهذا خطأ بالطبع! السبب في ذلك أنّ اختبار شرط الاستمرار في حلقة do-while يجري بعد انتهاء الحلقة من تنفيذ أوّل دورة لها، حيث تؤدّي هذه الدورة إلى جعل قيمة المتغيّر sum تساوي 1 وقيمة i تساوي 2، وبعد ذلك يأتي اختبار الشرط i <= n والذي سيعطي false بالطبع وتتوقف الحلقة عن التكرار ولكن بعد فوات الأوان. في حلقة while (وحتى في حلقة for) لم نواجه هذه المشكلة لأنّ شرط استمرارها يجري اختباره في بداية الحلقة وقبل تنفيذ أي دورة تكراريّة. تمارين داعمة تمرين 1 اكتب برنامجًا يطبع الأعداد من 1 حتى 100 على الشاشة باستثناء الأعداد من مضاعفات العدد 5 أي على الشكل التالي: 1, 2, 3, 4, 6, 7, 8, 9, 11, … , 14, 16, … تلميح: ستحتاج في هذا التمرين إلى استخدام بنية if ضمن حلقة for واختبار قيمة التعبير المنطقي i % 5 == 0 على افتراض أنّ i هو عدّاد الحلقة. تمرين 2 اكتب برنامجًا يطلب من المستخدم إدخال عدد صحيح موجب ثمّ يوجد مضروب هذا عدد (قيمة العاملي له). فإذا أدخل عددًا سالبًا يجب على البرنامج أن ينبّه المستخدم على ذلك ويُنهي التنفيذ. تلميح: تذكّر أنّ مضروب عدد يُعبّر عن الجداءات للقيم المتناقصة لهذا العدد فمثلًا لإيجاد مضروب 5 (!5) نكتب: 5! = 5 * 4 * 3 * 2 * 1 تذكّر أيضًا أنّ !1 =1 و !0 = 1. الخلاصة تحدثنا في هذا الدرس عن الحلقات التكراريّة بأنواعها المختلفة. تدعم سي شارب عدة حلقات تكراريّة تُعتبر حلقة for من أهمّها. في الحقيقة توجد حلقة تكراريّة أخرى لم نتحدّث عنها في هذا الدرس، وهي حلقة foreach، وهي حلقة مفيدة جدًّا أجلّت الحديث عنها إلى أن نتعرّف على المجموعات Collections بأنواعها ونتعلّم التعامل معها.
  18. تعتبر العبارات الشرطية في البرنامج من الأمور الأساسيّة في البرمجة كما هو معلوم. تمتلك لغة سي شارب نوعين من العبارات الشرطية وهما: بنية if-else وبنية switch-case. العبارة الشرطية if-else وهي بنية مألوفة في معظم لغات البرمجة، تشبه هذه البنية في تشكيلها تلك الموجودة في لغات أخرى مثل ++C و Java. تمتلك هذه البنية ثلاثة أشكال سنتحدّث عنها تباعًا. الشكل الأول لبنية if الشكل الأبسط لبنية if هي: if ([condition]) { statement1; statement2; ... } إذا كان تقييم evaluate الشرط [condition] يعطينا true (أي تحقّق الشرط) عندها ستُفّذ العبارات البرمجيّة الموجودة ضمن الحاضنة {}، وإلّا (أي لم يتحقّق الشرط) فلن يُنفّذ أيّ منها. أنشئ مشروعًا جديدًا سمّه Lesson03_1 واستبدل محتويات الملف Program.cs بالبرنامج البسيط التالي الذي يعمل على مقارنة القيمة المدخلة من المستخدم مع العدد 5 ويُظهر الخرج المناسب: 1 using System; 2 3 namespace Lesson03_1 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 double x; 10 string str_x; 11 12 Console.Write("Input a number: "); 13 str_x = Console.ReadLine(); 14 15 x = double.Parse(str_x); 16 17 if(x > 5) 18 { 19 Console.WriteLine("The value {0} is greater than 5", x); 20 } 21 22 Console.WriteLine("Goodbye!"); 23 } 24 } 25 } جرّب تنفيذ هذا البرنامج باستخدام Ctrl+F5 (أو من القائمة Debug > Start Without Debugging). سيطلب منك البرنامج إدخال قيمة عدديّة، أدخل العدد 6، سيعرض البرنامج الخرج التالي: The value 6 is greater than 5 Goodbye! أعد تنفيذ البرنامج وأدخل هذه المرّة القيمة 3 لتحصل على الخرج التالي: Goodbye! لاحظ بأنّ خرج البرنامج قد اختلف باختلاف القيم المدخلة، أي أنّ هناك اختلاف في العبارات البرمجيّة التي تمّ تنفيذها في كلّ مرّة. يعود سبب ذلك إلى البنية if الموجودة بين السطرين 17 و 20. يختبر الشرط الموجود بعد كلمة if في السطر 17 فيما إذا كانت قيمة المتغيّر x أكبر تمامًا من 5. فإذا كانت نتيجة تقييم التعبير x > 5 تساوي true فهذا يعني أنّ الشرط قد تحقّق وبالتالي تنفّذ جميع العبارات البرمجيّة الموجودة في الحاضنة (بين السطرين 18 و 20). أمّا إذا كانت نتيجة تقييم التعبير x > 5 تساوي false فعندها سيتجاوز تنفيذ البرنامج البنية if إلى العبارات التي تأتي بعد السطر 20. الشكل الثاني لبنية if هذا الشكل للعبارة الشرطية if مفيد أيضًا، ويُستخدم عندما نريد الاختيار بين مجموعتين من العبارات البرمجيّة، والشكل العام له: if ([condition]) { statement1; statement2; ... } else { Statement3; Statement4; ... } لقد أضفنا القسم else مع حاضنته. المنطق هنا بسيط يمكننا قراءته بالشكل التالي: "إذا تحقق الشرط [condition] عندها تنفّذ الحاضنة الموجودة بعد if مباشرةً، وإلّا يتم تنفيذ الحاضنة الموجودة بعد else مباشرةً" لكي نتعرّف على كيفيّة التعامل مع هذا الشكل، أنشئ مشروعًا جديدًا سمّه Lesson03_2 وانسخ الشيفرة التالية إلى الملف Program.cs: 1 using System; 2 3 namespace Lesson03_2 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 double x; 10 string str_x; 11 12 Console.Write("Input a number: "); 13 str_x = Console.ReadLine(); 14 15 x = double.Parse(str_x); 16 17 if (x > 5) 18 { 19 Console.WriteLine("The value {0} is greater than 5", x); 20 } 21 else 22 { 23 Console.WriteLine("The value {0} is smaller than or equals 5", x); 24 } 25 26 Console.WriteLine("Goodbye!"); 27 } 28 } 29 } هذا البرنامج مطابق للبرنامج الذي رأيناه قبل قليل باستثناء القسم else مع حاضنته. يسلك هذا البرنامج نفس السلوك الذي يسلكه البرنامج السابق باستثناء أنّه لو أدخل المستخدم قيمة مثل 3 سيعمل البرنامج على طباعة الخرج التالي: The value 3 is smaller than or equals 5 Goodbye! لاحظ أنّ البرنامج Lesson03_1 كان يطبع العبارة !Goodbye فقط عند إدخال القيمة 3. السبب في ظهور الخرج الجديد هو وجود القسم else في بنية if السابقة، فعندما يُقيّم الشرط x > 5 في السطر 17 وتكون نتيجة تقييمه false سينتقل البرنامج فورًا إلى تنفيذ العبارات البرمجيّة الموجودة ضمن حاضنة else وهذا هو سبب ظهور هذا الخرج. العيب الوحيد في هذا البرنامج أنّه لا يستطيع التمييز بين الحالة التي تكون فيها القيمة المدخلة تساوي 5 وبين الحالة التي تكون فيها أصغر تمامًا من 5، ففي كلّ من هاتين الحالتين يعرض البرنامج نفس الخرج عن طريق تنفيذ العبارة الموجودة في السطر 23. ملاحظة: في حال كانت أيّة حاضنة تحوي عبارة برمجيّة واحد فقط، فعندها يمكن عدم استخدام قوسي الحاضنة {} مع أنّني أفضّل استخدامهما لجعل البرنامج أكثر وضوحًا. الشكل الثالث لبنية if وهو الشكل الأكثر شمولًا وفيه نستخدم القسم else if على الصورة التالية: if ([condition]) { statement1; statement2; ... } else if ([condition1]) { Statement3; Statement4; ... } else if ([condition2]) { Statement3; Statement4; ... } ... else { Statement3; Statement4; ... } يمكننا قراءة المنطق هنا على الشكل التالي: "إذا تحقّق الشرط [condition] عندها تنفّذ الحاضنة الموجودة بعد if مباشرةً، وإلّا إذا (else if) تحقّق الشرط [condition1] يتم تنفيذ الحاضنة الموجودة بعد else if الأولى مباشرةً، وإلّا إذا تحقّق الشرط [condition2] يتم تنفيذ الحاضنة الموجودة بعد else if الثانية مباشرةً، وإلّا (else) يتم تنفيذ الحاضنة الموجودة بعد else مباشرةً" نلاحظ أنّه يمكننا استخدام أقسام else if بقدر ما نريد، ولكن يمكن استخدام قسم else وحيد. ونلاحظ أيضًا أنّه بالنتيجة ستنفّذ مجموعة واحدة فقط ضمن حاضنة ما. وواضح أيضًا أنّ أقسام else if و else هي أقسام اختياريّة ووجودها غير مرتبط ببعضها، ولكن إذا حوت بنية if قسم else if فيجب أي يكون القسم else (في حال وجوده) هو القسم الأخير. لكي نثبّت هذا المفهوم بشكل جيّد انظر البرنامج Lesson03_3 التالي: 1 using System; 2 3 namespace Lesson03_3 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 double x; 10 string str_x; 11 12 Console.Write("Input a number: "); 13 str_x = Console.ReadLine(); 14 15 x = double.Parse(str_x); 16 17 if (x > 5) 18 { 19 Console.WriteLine("The value {0} is greater than 5", x); 20 } 21 else if (x == 5) 22 { 23 Console.WriteLine("The value {0} is equals 5", x); 24 } 25 else 26 { 27 Console.WriteLine("The value {0} is smaller than 5", x); 28 } 29 30 Console.WriteLine("Goodbye!"); 31 } 32 } 33 } يشبه هذا البرنامج سابقيه إلى حدٍّ بعيد، فهو يقارن القيمة المدخلة مع العدد 5 ويعرض رسالة مناسبة نتيجة عملية المقارنة. الشيء الجديد هنا هو التمييز بين الحالة التي تكون فيها القيمة المدخلة تساوي العدد 5 والحالة التي تكون فيها أصغر من العدد 5. قمنا بذلك من خلال إضافة القسم else if جديد يختبر حالة المساواة مع العدد 5. الآن أصبح منطق البرنامج كالتالي: "إذا كانت القيمة المدخلة أكبر تمامًا من 5 (السطر 17) عندها تُنفّذ العبارة الموجودة في السطر 19، وإلّا إذا كانت القيمة المدخلة تساوي 5 (السطر 21) عندها تُنفّذ العبارة الموجودة في السطر 23، وإلّا ستكون القيمة المدخلة أصغر من 5 حتمًا، وتُنفَّذ العبارة الموجودة في السطر 27." العبارة الشرطية switch-case تفيد هذه البنية في الاختيار من بين عدّة حالات منفصلة. لهذه البنية الشكل العام التالي: switch(expression) { case [A]: [statements] break; case [B]: [statements] break; ... [default:] [statements] break; } القسم الأخير default هو قسم اختياري، كما يجب أن يكون هناك قسم case واحد على الأقل. إليك الآن البرنامج Lesson03_4 لفهم كيفيّة استخدام هذه البنية: 1 using System; 2 3 namespace Lesson03_4 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 double x, y; 10 string str_x, str_y, operation; 11 12 Console.Write("Input first number: "); 13 str_x = Console.ReadLine(); 14 15 Console.Write("Input second number: "); 16 str_y = Console.ReadLine(); 17 18 Console.Write("Choose operation (+, -, *, /): "); 19 operation = Console.ReadLine(); 20 21 x = double.Parse(str_x); 22 y = double.Parse(str_y); 23 24 switch (operation) 25 { 26 case "+": 27 Console.WriteLine("{0} + {1} = {2}", x, y, x + y); 28 break; 29 case "-": 30 Console.WriteLine("{0} - {1} = {2}", x, y, x - y); 31 break; 32 case "*": 33 Console.WriteLine("{0} * {1} = {2}", x, y, x * y); 34 break; 35 case "/": 36 Console.WriteLine("{0} / {1} = {2}", x, y, x / y); 37 break; 38 default: 39 Console.WriteLine("Unsupported operation."); 40 break; 41 } 42 } 43 } 44 } البرنامج السابق عبارة عن برنامج آلة حاسبة بسيطة تدعم العمليات الحسابية الأربع: الجمع والطرح والضرب والقسمة. يطلب البرنامج من المستخدم إدخال قيمتين عدديّتين، بعد ذلك يطلب اختيار العمليّة الحسابيّة المراد إجراؤها على هاتين القيمتين (+ ، - ، * ، /) وتخزين العمليّة المختارة ضمن المتغيّر النصّي operation وذلك في السطر 19. تعمل البنية switch في السطر 24 على مقارنة قيمة المتغيّر النصيّ operation مع القيم الموجودة في أقسام case (الأسطر 26 و 29 و 32 و 35) فإذا طابقت القيمة الموجودة في operation إحدى تلك القيم، فإنّ العبارات البرمجيّة الموجودة ضمن هذا القسم سيتمّ تنفيذها. أمّا إذا لم يحدث مثل هذا التطابق، فستنفّذ العبارات البرمجيّة الموجودة في القسم الاختياري default والتي ستخبر المستخدم (في هذا المثال) بأنّ العمليّة الحسابيّة التي يرغبها لا يدعمها البرنامج. نستفيد من القسم default في تنفيذ عبارات برمجيّة في حال لم يحدث التطابق مع أيّ قسم case سابق. كما نلاحظ أنّ العبارة break الموجودة في كلّ قسم من أقسام case بالإضافة إلى قسم default هي عبارة ضرورية وتؤدّي إلى انتقال تنفيذ البرنامج إلى خارج بنية switch أي إلى السطر 42. جرّب تنفيذ البرنامج وإدخال قيم متنوّعة بالإضافة إلى تجريب العمليات الحسابيّة الأربع. جرّب إدخال عامل باقي القسمة مثلًا (%) وانظر كيف سيجيب البرنامج بالرسالة Unsupported operation. تمارين داعمة تمرين 1 في البرنامج Lesson03_4 السابق إذا أدخل المستخدم القيمة 0 للعدد الثاني، ثم اختار عمليّة القسمة ( / ) سيؤدّي ذلك إلى القسمة على صفر، وهذا يسبّب خطًأ أثناء التنفيذ runtime error يؤدّي إلى رمي استثناء وتوقّف البرنامج عن العمل. أجرِ تعديلًا على البرنامج ليأخذ هذا الأمر بالحسبان. (تلميح: أضف شرط if ضمن قسم case الموافق للعمليّة ( / ) لاختبار قيمة المتغيّر y فيما إذا كانت تساوي الصفر أم لا). تمرين 2 اكتب برنامجًا يطلب من المستخدم إدخال درجة الحرارة الحاليّة. فإذا كانت درجة الحرارة أقل من 4 مئوية يعرض البرنامج الرسالة "Very Cold". أمّا إذا كانت درجة الحرارة بين 4 وأقل من 10 مئويّة يعرض الرسالة "Cold". وفي حال كانت درجة الحرارة بين 10 وأقل من 30 مئويّة يعرض الرسالة "Normal". أمّا إذا كانت درجة الحرارة 30 فما فوق فيعرض البرنامج الرسالة "Hot". الخلاصة تعلّمنا في هذا الدرس مبادئ التعامل مع العبارات الشرطية والحاجة الماسّة إليها في اتخاذ القرارات المناسبة في البرنامج. تعرّفنا على العبارة الشرطية if-else وأشكالها المفيدة، كما تعرّفنا أيضًا على بنية الاختيار swicth-case. في مجال البرمجة من غير الممكن في الواقع أن يخلو أيّ برنامج فعليّ من وجود عبارة شرطية if واحدة على الأقل.