البحث في الموقع
المحتوى عن 'c sharp'.
-
تعتبر المصفوفات من بنى المعطيات المهمّة في أيّ لغة برمجة. سيفترض هذا الدرس أنّه لديك خبرة مسبقة عن مفهوم المصفوفة. المصفوفات في سي شارب هي عبارة عن نوع مرجعيّ 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. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن استخدام المصفوفات مع أنواع مضمنة لكلّ عنصر في مصفوفة دليل 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 التي يُعتبر أسلوب عملها مألوفًا، وهي مفيدة جدًا عند استخدامها مع المجموعات كما سنرى لاحقًا.
-
مقدمة إلى المفهوم الكائني تُعتبر لغة سي شارب لغة برمجة كائنيّة صرفة 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 كلٌّ بحسب حاجته. الخلاصة تعرّفنا في هذا الدرس على المفهوم العام للبرمجة كائنيّة التوجّه وتعاملنا مع التغليف حيث لا تهمّنا التفاصيل الداخلية لآلية العمل. والوراثة التي تتعلّق بمفهوم إعادة الاستخدام والانتقال من العام (الأب) إلى المخصّص (الابن). بالإضافة إلى التعدديّة الشكليّة التي تسمح لنا بإكساب سلوكيّات مخصّصة للأصناف الأبناء تنسجم مع طبيعتها. سنتناول في الدروس التالية هذه المفاهيم بشكل تطبيقي في سي شارب.
-
سنتحدّث في هذا الدرس عن كيفيّة تطبيق مبادئ البرمجة كائنيّة التوجّه في سي شارب وذلك من خلال إنشاء واستخدام الأصناف والكائنات في هذه اللغة. يمكن التصريح عن صنف في سي شارب باستخدام الكلمة المحجوزة 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، علمًا أنّنا ستوضّح كيفيّة التعامل مع باقي المحدّدات في الدرس التالي الذي سنتحدّث فيه عن المزيد حول هذا الموضوع المهم والأساسي لتطوير التطبيقات باستخدام سي شارب.
-
تُعتبر الحلقات التكراريّة من البنى المهمّة في لغات البرمجة، حيث نستطيع من خلالها تنفيذ عبارة أو عدّة عبارات برمجيّة لعدد من المرّات. تدعم سي شارب مثل باقي لغات البرمجة نوعين من الحلقات التكراريّة من حيث عدد التكرار، فهناك الحلقات ذات العدد المحدّد من المرّات (حلقة 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 بأنواعها ونتعلّم التعامل معها.
-
تعتبر العبارات الشرطية في البرنامج من الأمور الأساسيّة في البرمجة كما هو معلوم. تمتلك لغة سي شارب نوعين من العبارات الشرطية وهما: بنية 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 واحدة على الأقل.
-
يمكنك تنفيذ البرامج الموجودة ضمن هذه السلسلة (كما وسبق أن ذكرنا في المقدّمة) بطريقتين مختلفتين: الأولى هي تحميل وتنصيب بيئة التطوير المجّانيّة 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 بتفصيل أكبر.
-
يُعتبر التعامل مع النصوص من المواضيع المهمّة التي يحتاجها أيّ مبرمج. توفّر سي شارب أساليب متقدّمة للتعامل مع النصوص، سنتحدّث في هذا الدرس عن بعض هذه الأساليب. حيث سنتعلّم كيفيّة البحث عن نص محدّد ضمن نص آخر، واستبدال نص بآخر، واستخلاص جزء محدّد من نص، وضمّ النصوص. النص في سي شارب هو سلسلة من المحارف 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. يُعتبر العمل مع النصوص أساسيًّا ومهمًّا لكلّ مبرمج وخصوصًا في مثل هذه الأيّام، فالبيانات التي يتمّ تبادلها بين التطبيقات المختلفة عبر شبكة الإنترنت تكون على شكل نصوص. سنتناول في الدرس القادم كيفيّة التعامل مع الملفات النصيّة من خلال القراءة والكتابة منها وإليها.
-
لغة C# هي لغة برمجة أنيقة، كائنيّة التوجه Object-oriented بأنواع بيانات سليمة Type-safe تمكّن المطورين من بناء تطبيقات آمنة ومتينة تعمل على إطار العمل NET. // تبدأ التعليقات وحيدة السطر بشريطين مائلين هكذا // /* توضع التعليقات متعدّدة الأسطر بين العلامة أعلاه والعلامة أسفله */ هذا تعليق xml يُستخدَم لتوليد توثيق خارجي أو لتقديم مساعدة حسب السياق في بيئة تطوير مندمجة IDE. /// <param name="firstParam"> لتوثيق الدالة Parameter الذي هو معامل firstParam هذا تعليق</param> /// <returns>معلومات عن القيمة المُرجَعة للدالة/returns> //public void MethodOrClassOrOtherWithParsableHelp(string firstParam) {} يحدّد فضاءات اﻷسماء Namespaces التي ستستخدمها هذه الشفرة فضاءات الأسماء أدناه هي كلّها جزء من مكتبة الأصناف Classes المعيارية في إطار العمل NET. Framework Class Library using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Net; using System.Threading.Tasks; using System.IO; فضاء الأسماء هذا ليس مُتضمّنا في المكتبة المعيارية: using System.Data.Entity; لكي تتمكّن من استخدام المكتبة أعلاه فستحتاج لإضافة مرجع إلى ملف dll وهو ما يمكن لمدير الحزم NuGet فعلُه: Install-Package EntityFramework تعرّف فضاءات الأسماء مجالات لتنظيم الشفرات ضمن حزم Packages أو وِحْدات Modules لاستخدام فضاء الأسماء المُعرّف أدناه في شفرة أخرى نضيف العبارة Learning.CSharp إلى فضاءات الأسماء المستخدمة namespace Learning.CSharp { يجب أن يحوي كل ملف cs. على الأقل على صنف Class له نفس اسم الملف. يمكنك لك عدم التقيّد بهذا الشرط، إلا أنه أفضل لصحة الشفرة المصدرية public class LearnCSharp { صياغة أساسية: يمكنك التجاوز إلى “ميزات مثيرة للاهتمام” إن سبق لك كتابة شفرات بجافا أو سي++ public static void Syntax() { // للكتابة في سطر جديد Console.WriteLine استخدم Console.WriteLine("Hello World"); Console.WriteLine( "Integer: " + 10 + " Double: " + 3.14 + " Boolean: " + true); // لكتابة عبارات على نفس السطر Console.Write استخدم Console.Write("Hello "); Console.Write("World"); أنواع البيانات Types والمتغيّرات Variables دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن عرّف المتغيّرات على النحو التالي <type> <name> Sbyte - عدد صحيح (سالب أو موجب) على 8 بتات (محصور بين 128- و127) sbyte fooSbyte = 100; Byte - عدد طبيعي (موجب فقط) على 8 بتات (محصور بين 0 و255) byte fooByte = 100; Short - عدد صحيح أو طبيعي طوله 16 بتات صحيح short محصور بين -32,768 و32,767 طبيعي ushort محصور بين 0 و65,535 short fooShort = 10000; ushort fooUshort = 10000; عدد صحيح fooInt أو طبيعي fooUint طوله 32 بت int fooInt = 1; // (-2,147,483,648 <= int <= 2,147,483,647) uint fooUint = 1; // (0 <= uint <= 4,294,967,295) Long عدد صحيح fooLong أو طبيعي fooUlong طوله 64 بت long fooLong = 100000L; // (-9,223,372,036,854,775,808 <= long <= 9,223,372,036,854,775,807) ulong fooUlong = 100000L; // (0 <= ulong <= 18,446,744,073,709,551,615) النوع المبدئي default للأعداد هو int أو uint حسب طول العدد. والحرف L وراء العدد يشير إلى أن نوع العدد هو long أو ulong Double - فاصلة عائمة مزدوجة الدقة حسب المعيار 64-bit IEEE 754 double fooDouble = 123.4; // الدقة: 15-16 رقما Float - فاصلة عائمة وحيدة الدقة 32-bit IEEE 754 Floating Point float fooFloat = 234.5f; // الدقة: 7 أرقام يشير الحرف f وراء العدد إلى أن نوع العدد هو Float Decimal - نوع بيانات بطول 128 بت، ودقّة أعلى من بقية أنواع البيانات ذات الفاصلة العائمة مناسب للحسابات المالية والنقدية decimal fooDecimal = 150.3m; // Boolean - true & false bool fooBoolean = true; // or false Char - نوع بيانات بطول 16 بت يرمز لمحرف يونيكود `char fooChar = 'A'; Strings – على النقيض من جميع أنواع البيانات السابقة التي هي أنواع لقيم البيانات فإن النوع String - سلسلة محارف - هو نوع لمرجع Reference بمعنى أنه يمكنه أخذ القيمة null string fooString = "\"escape\" quotes and add \n (new lines) and \t (tabs)"; Console.WriteLine(fooString); يمكن الوصول إلى كل محرف من سلسلة المحارف عن طريق ترتيبه في السلسلة char charFromString = fooString[1]; // => 'e' لا يمكن التعديل على سلاسل المحارف؛ التعليمة fooString[1] = X خاطئة مقارنة سلاسل محارف مع قيمة الخاصيّة CurrentCulture المعرّفة في المكتبة المعيارية لتمثيل اللغة المستخدمة في النظام، مع تجاهل حالة الأحرف IgnoreCase string.Compare(fooString, "x", StringComparison.CurrentCultureIgnoreCase); تهيئة سلسلة المحارف اعتمادا على sprintf string fooFs = string.Format("Check Check, {0} {1}, {0} {1:0.0}", 1, 2); التاريخ والتهيئة DateTime fooDate = DateTime.Now; Console.WriteLine(fooDate.ToString("hh:mm, dd MMM yyyy")); سلاسل المحارف الأصلية Verbatim String يمكنك استخدام العلامة @ أمام سلسلة محارف لتخليص جميع المحارف الموجودة في السلسلة string path = "C:\\Users\\User\\Desktop"; string verbatimPath = @"C:\Users\User\Desktop"; Console.WriteLine(path == verbatimPath); // => true يمكنك توزيع سلسلة محارف على أكثر من سطر بالرمز @ لتخليص العلامة " ضع مكانها "" (" مرتين) string bazString = @"Here's some stuff on a new line! ""Wow!"", the masses cried"; استخدم الكلمة المفتاحية const لجعل المتغيّر ثابتًا غير قابل للتعديل وتُحسب القيم الثابتة أثناء تصريف البرنامج compile time const int HoursWorkPerWeek = 9001; بنى البيانات المصفوفات - يبدأ العنصر الأول عند الترتيب 0 ويجب تحديد قياس المصفوفة عند تعريفها صيغة تعريف المصفوفة هي كالتالي: ;<datatype>[] <var name> = new <datatype>[<array size>] المصفوفة intArray في المثال التالي تحوي 10 أعداد int[] intArray = new int[10]; طريقة أخرى لتعريف مصفوفة وتهيئتها int[] y = { 9000, 1000, 1337 }; ترتيب عناصر المصفوفة - الوصول إلى عنصر Console.WriteLine("intArray @ 0: " + intArray[0]); المصفوفات قابلة للتعديل. intArray[1] = 1; القوائم تُستخدَم القوائم أكثر من المصفوفات لما توفّره من مرونة وصيغة تعريف قائمة هي على النحو التالي: ;List<datatype> <var name> = new List<datatype>() List<int> intList = new List<int>(); List<string> stringList = new List<string>(); List<int> z = new List<int> { 9000, 1000, 1337 }; // تحديد القيم الابتدائية لعناصر القائمة تُستخدَم الإشارتان <> للأنواع العميمة Generics - راجع فقرة ميزات رائعة ليست للقوائم قيم مبدئية ويجب أولا إضافة قيمة قبل إمكانية الوصول إلى العنصر intList.Add(1); Console.WriteLine("intList @ 0: " + intList[0]); بنى تحتية أخرى يجدر بك مراجعتها: قوائم الانتظار Queues/ الرصوص Stacks القواميس Dictionaries HashSet تجميعاـ القراءة فقط Read-only collections الأزواج المُرتّبة Tuples (الإصدار 4 من .NET. فما فوق) العوامل Console.WriteLine("\n→Operators"); int i1 = 1, i2 = 2; // اختصار لتعريف متغيّرات عدة في آن واحد العمليات الحسابية واضحة Console.WriteLine(i1 + i2 - i1 * 3 / 7); // => 3 المقياس Modulo Console.WriteLine("11%3 = " + (11 % 3)); // => 2 عوامل المقارنة Console.WriteLine("3 == 2? " + (3 == 2)); // => false Console.WriteLine("3 != 2? " + (3 != 2)); // => true Console.WriteLine("3 > 2? " + (3 > 2)); // => true Console.WriteLine("3 < 2? " + (3 < 2)); // => false Console.WriteLine("2 <= 2? " + (2 <= 2)); // => true Console.WriteLine("2 >= 2? " + (2 >= 2)); // => true عوامل المقارنة البتّية Bitwise ~ عامل التكملة الأحادي (إن كان البت يحوي 0 يحوله إلى 1، وإن كان يحوي واحد يحوّله إلى صفر) >> إزاحة البتات إلى اليسار << إزاحة البتات إلى اليمين & عامل “و” المنطقي ^ عامل “أو” المنطقي غير الشامل exclusive OR | عامل “أو” المنطقي الشامل inclusive OR التزايد Incrementation int i = 0; Console.WriteLine("\n->Inc/Dec-rementation"); Console.WriteLine(i++); //Prints "0", i = 1. تزاد بعدي Console.WriteLine(++i); //Prints "2", i = 2. تزايد قبلي Console.WriteLine(i--); //Prints "2", i = 1. تناقص بعدي Console.WriteLine(--i); //Prints "0", i = 0. تناقص قبلي بنى التحكّم Console.WriteLine("\n->Control Structures"); تتبع بنية التحكم if else طريقة كتابة بنى التحكم في C int j = 10; if (j == 10) { Console.WriteLine("I get printed"); } else if (j > 10) { Console.WriteLine("I don't"); } else { Console.WriteLine("I also don't"); } العوامل الثلاثية بنية تحكّم if else بسيطة تمكن كتابتها على النحو التالي: <condition> ? <true> : <false> int toCompare = 17; string isTrue = toCompare == 17 ? "True" : "False"; حلقة While التكرارية int fooWhile = 0; while (fooWhile < 100) { //تتكرّر الحلقة مئة مرة، من القيمة 0 إلى القيمة 99 fooWhile++; } حلقة Do.. While التكرارية int fooDoWhile = 0; do { الحلقة معدّة للتكرار مئة مرة، من القيمة 0 إلى القيمة 99 Start iteration 100 times, fooDoWhile 0->99 if (false) continue; // تجاوز التكريرة الحالية fooDoWhile++; if (fooDoWhile == 50) break; // توقيف الحلقة تماما، والخروج منها } while (fooDoWhile < 100); حلقة for التكرارية ذات الصيغة: (<for(<start_statement>; <conditional>; <step for (int fooFor = 0; fooFor < 10; fooFor++) { // تتكرّر الحلقة عشر مرات، من القيمة 0 إلى القيمة 9 } حلقة For Each يمكن استخدام حلقة التكرار foreach للمرور عبر أي كائن Object يُنفّذ الصنف IEnumerable أو <IEnumerable<T تنفّذ جميع الأنواع التجميعية (المصفوفات، القوائم، القواميس…) في إطار العمل .Net واجهة أو أكثر من الأصناف المذكورة (يمكن حذف ()ToCharArray من التعليمة أدناه، لأن String تنفّذ الواجهة IEnumerable) foreach (char character in "Hello World".ToCharArray()) { // تمرّ على جميع المحارف في السلسلة } تعليمة Switch تعمل Switch مع أنواع البيانات byte, short, char, وint تعمل كذلك مع أنواع البيانات Enum (نتعرّض لها أدناه)، الصنف String وبضعة أصناف خاصّة تغلّف أنواع بيانات أساسية: Character,Byte,Short, و Integer. int month = 3; string monthString; switch (month) { case 1: monthString = "January"; break; case 2: monthString = "February"; break; case 3: monthString = "March"; break; يمكن تنفيذ أكثر من إجراء في كل حالة case، إلا أنه لا يمكن إضافة إجراء ضمن حالة دون إضافة تعليمة توقيف break; قبل الحالة الموالية (إن أردت فعل هذا الأمر، فستحتاج لإضافة تعليمة goto case x بعد الإجراء) case 6: case 7: case 8: monthString = "Summer time!!"; break; default: monthString = "Some other month"; break; } التحويل بين أنواع البيانات وجبْر الأنواع Typecasting تحويل البيانات تحويل سلسلة محارف String إلى عدد Integer سيظهر استثناء Exception في حالة إخفاق عملية التحويل int.Parse("123");// نحصُل على النسخة العددية من سلسلة المحارف "123" عند استخدام الدالة TryParse لتحويل نوع البيانات فإن قيمة التحويل ستكون القيمة المبدئية لنوع البيانات وفي حالة الأعداد فإن القيمة المبدئية هي 0 int tryInt; if (int.TryParse("123", out tryInt)) // ترجع الدالة قيمة منطقية Console.WriteLine(tryInt); // 123 تحويل الأعداد إلى سلاسل محارف String يتضمّن الصنف Convert عددا من التوابع Methods لتسهيل التحويل Convert.ToString(123); أو tryInt.ToString(); جبر أنواع البيانات جبر العدد العشري 15 للحصول على قيمة من النوع int ثم جبر القيمة المُتحصَّل عليها ضمنيا لنحصُل على النوع long long x = (int) 15M; } الأصناف راجع التعريفات في آخر الملف public static void Classes() { انظر تعريف الكائنات في آخر الملف استخدم الكلمة المفتاحية new لاستهلال صنف Bicycle trek = new Bicycle(); استدعاء توابع الكائن trek.SpeedUp(3); // يجب دائما المرور عبر المعدّلات والمسترجعات Setter and getter methods trek.Cadence = 100; يُستخدم التابع ToString لعرض قيمة الكائن Console.WriteLine("trek info: " + trek.ToString()); استهلال كائن جديد من الصنف PennyFarthing PennyFarthing funbike = new PennyFarthing(1, 10); Console.WriteLine("funbike info: " + funbike.ToString()); Console.Read(); } // نهاية التابع الرئيس Main method مَدخل الكونسول Console entry. التطبيقات التي تعمل عبر الطرفية يجب أن يكون لديها مدخل عبارة عن تابع رئيس public static void Main(string[] args) { OtherInterestingFeatures(); } ميزات مثيرة للاهتمام التوقيعات المبدئية للتوابع public // مجال الرؤية static // يسمح بالاستدعاء المباشر من الصنف دون المرور بكائنات int // نوع البيانات المُرجَعة, MethodSignatures( int maxCount, // المتغيّر الأول عددي int count = 0, // القيمة المبدئية هي 0، تُستخدَم إن لم يُمرَّر متغير إلى التابع int another = 3, params string[] otherParams // يستقبل بقية المتغيّرات المُمررة إلى التابع جميعا ) { return -1; } يمكن أن تكون أسماء التوابع متطابقة، ما دامت التوقيعات مختلفة وكل تابع لا يختلف عن آخر سوى في نوع البيانات المُرجَع ليس وحيدا public static void MethodSignatures( ref int maxCount, // تمرير المعاملات حسب المرجع، وليس القيمة out int count) { المعامل المُمرر في المتغيّر count سيحوي القيمة 15 خارج هذه الدالة count = 15; // معامل الخروج out يجب أن يُسنَد قبل الانتهاء من التابع } أنواع البيانات العميمة Generics الأصناف TKey وTValue يحدّدها المستخدم الذي يستدعي هذه الدالة ويحاكي هذا التابع عمل SetDefault في بايثون public static TValue SetDefault<TKey, TValue>( IDictionary<TKey, TValue> dictionary, TKey key, TValue defaultItem) { TValue result; if (!dictionary.TryGetValue(key, out result)) return dictionary[key] = defaultItem; return result; } يمكنك تقييد الكائنات التي يمكن تمريرها إلى الدالة public static void IterateAndPrint<T>(T toPrint) where T: IEnumerable<int> { بما أن الصنف T ينفّذ IEnumerable فإنه يمكننا المرور على عناصره باستخدام foreach foreach (var item in toPrint) // العنصر هو من النوع int Console.WriteLine(item.ToString()); } الكلمة المفتاحية “yield” يدلّ استخدام yield أن التابع الذي تظهر فيه هذه الكلمة المفتاحية لديه خاصيّة التكرار (أي أنه يمكن استخدام التابع مع الحلقة foreach) public static IEnumerable<int> YieldCounter(int limit = 10) { for (var i = 0; i < limit; i++) yield return i; } نستطيع استدعاء التابع أعلاه على النحو التالي public static void PrintYieldCounterToConsole() { foreach (var counter in YieldCounter()) Console.WriteLine(counter); } يمكن استخدام yield return أكثر من مرّة في نفس التابع public static IEnumerable<int> ManyYieldCounter() { yield return 0; yield return 1; yield return 2; yield return 3; } كما يمكنك استخدام “yield break” لتوقيف التكرار التابع التالي يُرجِع نصف القيم الموجودة بين 0 وlimit public static IEnumerable<int> YieldCounterWithBreak(int limit = 10) { for (var i = 0; i < limit; i++) { if (i > limit/2) yield break; yield return i; } } public static void OtherInterestingFeatures() { المعاملات الاختيارية MethodSignatures(3, 1, 3, "Some", "Extra", "Strings"); MethodSignatures(3, another: 3); // تعيين قيمة المعامل مباشرة، مع تجاوز المعاملات الاختيارية تمرير المعاملات بالمرجع By reference، والقيمة المُرجعة Out parameter BY REF AND OUT PARAMETERS int maxCount = 0, count; // المعاملات المُمررة بالمرجع يجب أن تحوي قيمة MethodSignatures(ref maxCount, out count); توابع التمديد Extension methods int i = 3; i.Print(); // مُعرَّفة أدناه الأنواع التي تقبل قيمة فارغة Nullable types، مناسبة للتخاطب مع قواعد البيانات والقيم المُرجَعة وأي نوع بيانات قيمي (أي ليس صنفا) يمكن جعله يقبل قيما فارغة بكتابة ? بعده <type>? <var name> = <value> int? nullable = null; // اختصار لـ Nullable<int> Console.WriteLine("Nullable variable: " + nullable); bool hasValue = nullable.HasValue; // قيمة منطقية صحيحة true إن لم يكن يساوي null علامتا الاستفهام المتلاصقتان ?? هما اختصار لتحدد قيمة مبدئية في حال كان المتغيّر فارغا نعطيه 0 قيمة مبدئية int notNullable = nullable ?? 0; // 0 ?. هذه العلامة عي عامل للتحقّق من القيمة الفارغة null nullable?.Print(); // استخدم تابع التمديد Print() إذا كان المتغيّر nullable مختلفا عن null المتغيّرات ضمنية النوع - يمكنك ترك المُصرّف Compiler يحدّد نوع المتغيّر: var magic = "magic is a string, at compile time, so you still get type safety"; ;magic = 9 لن تُسنَد القيمة 9 إلى المتغيّر magic لأنه يحوي سلسلة محارف الأنواع العميمة var phonebook = new Dictionary<string, string>() { {"Sarah", "212 555 5555"} // إضافة عنوان إلى دفتر العناوين }; استدعاء الدالة SetDefault المُعرَّفة أعلاه Console.WriteLine(SetDefault<string,string>(phonebook, "Shaun", "No Phone")); // No Phone يمكنك عدم تحديد TKey و TValue بما أنه يمكن استنتاجهما تلقائيا Console.WriteLine(SetDefault(phonebook, "Sarah", "No Phone")); // 212 555 5555 الدوال مجهولة الاسم Lambda expressions - تتيح كتابة شفرات على نفس السطر Func<int, int> square = (x) => x * x; // آخر عنصر من T هو القيمة المُرجعة Console.WriteLine(square(3)); // 9 التعامل مع الأخطاء try { var funBike = PennyFarthing.CreateWithGears(6); لن تُنفَّذ لأن CreateWithGears تتسبّب في استثناء Exception string some = ""; if (true) some = null; some.ToLower(); // تتسبّب في الاستثناء NullReferenceException } catch (NotSupportedException) { Console.WriteLine("Not so much fun now!"); } catch (Exception ex) // التقاط جميع الاستثناءات الأخرى { throw new ApplicationException("It hit the fan", ex); // throw; // التقاط آخر يحافظ على ركام النداء callstack } // catch { } // التقاط كل شيء دون التعامل مع الاستثناءات finally { // try أو catch تُنفّذ بعد } إدارة الموارد يمكنك إدارة الموارد المتوفّرة بسهولة حيث تنفّذ أغلب الكائنات التي تستعمل الموارد غير المستغلة (الملفات، سياق الأجهزة الطرفية، …إلخ) تُنفّذ الواجهة IDisposable تتولّى التعليمة using التخلّص من كائنات IDisposable using (StreamWriter writer = new StreamWriter("log.txt")) { writer.WriteLine("Nothing suspicious here"); } يُتخلَّص من جميع الموارد بعد الانتهاء من تنفيذ هذه الشفرة حتى ولو تسبّبت في استثناء البرمجة المتوازية var words = new List<string> {"dog", "cat", "horse", "pony"}; Parallel.ForEach(words, new ParallelOptions() { MaxDegreeOfParallelism = 4 }, word => { Console.WriteLine(word); } ); تشغيل هذه الشفرة سينتُج عنه مُخرجات متعدّدة لأن كلّ تشعّب thread يُكمل في وقت مختلف عن الآخر. أدناه أمثلة على المُخرجات cat dog horse pony dog horse pony cat الكائنات الديناميكية (رائعة للعمل مع لغات برمجة أخرى) dynamic student = new ExpandoObject(); student.FirstName = "First Name"; لا تحتاج لتعريف صنف أولا بل إنه يمكنك إضافة توابع (يُرجع التابع أدناه سلسلة محارف ويتلقّى سلسلة محارف) student.Introduce = new Func<string, string>( (introduceTo) => string.Format("Hey {0}, this is {1}", student.FirstName, introduceTo)); Console.WriteLine(student.Introduce("Beth")); تنفّذ أغلب التجميعات Collections الواجهة <IQUERYABLE<T التي توفّر الكثير من التوابع المفيدة var bikes = new List<Bicycle>(); // دراجات هوائية bikes.Sort(); // يرتّب القائمة bikes.Sort((b1, b2) => b1.Wheels.CompareTo(b2.Wheels)); // يرتّب القائمة بناءً على عدد العجلات var result = bikes .Where(b => b.Wheels > 3) // الترشيج والفلترة .Where(b => b.IsBroken && b.HasTassles) .Select(b => b.ToString()); // var sum = bikes.Sum(b => b.Wheels); يجمع عدد العجلات في كامل القائمة وينشئ قائمة من الكائنات الضمنية Implicit objects بالاعتماد على بعض خواص الدراجة الهوائية var bikeSummaries = bikes.Select(b=>new { Name = b.Name, IsAwesome = !b.IsBroken && b.HasTassles }); من الصعب توضيح الأمر هنا، إلا أنك تحصُل على نوع البيانات قبل الانتهاء من التعليمات، إذ أن المصرّف يمكنه العمل ضمنا على الأنواع أعلاه foreach (var bikeSummary in bikeSummaries.Where(b => b.IsAwesome)) Console.WriteLine(bikeSummary.Name); التوازي مع ASPARALLEL نخلط عمليّات LINQ والعمليّات المتوازية var threeWheelers = bikes.AsParallel().Where(b => b.Wheels == 3).Select(b => b.Name); سيحدث الأمر بالتوازي. تُنشَأ التشعبات تلقائيا وستُقسَّم النتائج بينها طريقة رائعة للعمل مع مجموعة بيانات ضخمة إن كانت لديك الكثير من الأنوية Cores LINQ تربط بين مخزن بيانات وكائنات من الصنف <IQueryable<T مثلا: LinqToSql تربط الكائنات مع قاعدة بيانات، LinqToXml تربط الكائنات مع مستند XML var db = new BikeRepository(); يؤجَّل التنفيذ، وهو أمر جيّد عند التعامل مع قواعد البيانات var filter = db.Bikes.Where(b => b.HasTassles); // no query run if (42 > 6) // يمكنك الاستمرار في إضافة المرشحات، حتى تلك المشروطة؛ مناسبة لميزة "البحث المتقدّم" filter = filter.Where(b => b.IsBroken); // no query run var query = filter .OrderBy(b => b.Wheels) .ThenBy(b => b.Name) .Select(b => b.Name); // still no query run يعمل الاستعلام الآن، إلا أنك لا تحصُل على نتائج الاستعلام إلا عند المرور عليها foreach (string bike in query) Console.WriteLine(result); } } // نهاية الصنف LearnCSharp يمكنك إضافة أصناف أخرى في ملف cs. public static class Extensions { توابع الصنف Extensions public static void Print(this object obj) { Console.WriteLine(obj.ToString()); } } التفويض والأحداث public class DelegateTest { public static int count = 0; public static int Increment() { // زيادة العدّاد ثم إرجاع النتيجة return ++count; } التفويض delegate هو مرجع لتابع لجعل مرجع على التابع Increment نبدأ بتعريف تفويض بنفس التوقيع أي أنه لا يأخذ أية معطيات ويُرجع عددا من النوع int public delegate int IncrementDelegate(); يمكن أيضا استخدام حدث Event لتحريك التفويض أنشئ حدثا بنوع التفويض public static event IncrementDelegate MyEvent; static void Main(string[] args) { نحيل إلى التابع Increment باستهلال التفويض وتمرير معطى هو التابع نفسه IncrementDelegate inc = new IncrementDelegate(Increment); Console.WriteLine(inc()); // => 1 يمكن تركيب التفويضات بالعامل + IncrementDelegate composedInc = inc; composedInc += inc; composedInc += inc; سينفّذ التفويض composedInc التابع Increment ثلاث مرات Console.WriteLine(composedInc()); // => 4 الاشتراك في الحدث باستخدام التفويض MyEvent += new IncrementDelegate(Increment); MyEvent += new IncrementDelegate(Increment); تحريك الحدث، أي تنفيذ كل التفويضات المشترِكة في هذا الحدث Console.WriteLine(MyEvent()); // => 6 } } صيغة تعريف صنف: <public/private/protected/internal> class <class name>{` // حقول البيانات، المشيّدات، الدوالّ.. كلّها في الداخل //تُستدعى الدوال بنفس طريقة استدعاء التوابع في جافا } public class Bicycle { // حقول/متغيّرات صنف الدراجات الهوائية Bicycle public int Cadence // عمومي public : يمكن استدعاء من أي مكان { get // مسترجع - نعرّف تابعا للوصول إلى قيمة خاصيّة من الكائن { return _cadence; } set // معدّل - نعرّف تابعا لتعيين قيمة خاصيّة { _cadence = value; // القيمة value هي المعطى المُمرّر إلى المعدّل } } private int _cadence; protected virtual int Gear // يمكن استدعاءه فقط من هذا الصنف أو الأصناف المتفرّعة منه Protected:محميّ { get; // تُنشأ خاصيّة تلقائية بحيث لا تحتاج لإضافة حقل بيانات set; } internal int Wheels // داخليّ Internal: يُمكن الوصول إليه من نفس الملف التنفيذي { get; private set; // يمكن تغيير مجال المسترجعات والمعدّلات } int _speed; // ولا يمكن الوصول إليها إلا من داخل الصنف Private كل الخاصيّات هي مبدئيا خاصّة // يمكن أيضا استخدام الكلمة المفتاحية private public string Name { get; set; } للخاصيّات صياغة استثنائية عندما نريد خاصية متاحة للقراءة فقط بمعنى أنها تعيد فقط نتيجة عبارة public string LongName => Name + " " + _speed + " speed"; النوع enum هو نوع بيانات قيمية يتمثّل في مجموعة من المتغيّرات ثابتة القيمة هذا النوع هو في الواقع مجرّد ربط اسم بقيمة (عددية، إن لم يحدد نوع آخر) أنواع البيانات الموثوقة في قيم الثوابت هي byte, sbyte, short, ushort, int, uint, long, و ulong ولا يمكن أن توجد نفس القيمة مرتين في متغيّر من النوع enum public enum BikeBrand { AIST, BMC, Electra = 42, // مباشرة enum يمكن تعيين قيمة المتغيّر في Gitane // 43 } عرّفنا هذا النوع داخل الصنف Bicycle لذا فهو نوع داخلي وعندما نريد استخدامه خارج الصنف فسيتوجّب أن نكتُب Bicycle.Brand public BikeBrand Brand; بعد تعريف نوع enum يصبح بإمكاننا تعريف متغيّر من هذا النوع تستطيع التعليم على وجود قيم عدّة يمكن الاختيار بينها بإضافة الصنف FlagsAttribute قبل تعريف النوع enum يمكن استخدام أي صنف متفرّع عن الصنف Attribute لتعليم أنواع البيانات، التوابع والمعاملات…إلخ يمكن استخدام العوامل المنطقية & و | لإجراء عمليّات منطقية داخل القيمة [Flags] public enum BikeAccessories { None = 0, Bell = 1, MudGuards = 2, // نحتاج لتعيين القيم يدويا Racks = 4, Lights = 8, FullPackage = Bell | MudGuards | Racks | Lights } الاستخدام: aBike.Accessories.HasFlag(Bicycle.BikeAccessories.Bell) في الإصدارات السابقة على الإصدار الرابع من إطار العمل NET (aBike.Accessories & Bicycle.BikeAccessories.Bell) == Bicycle.BikeAccessories.Bell public BikeAccessories Accessories { get; set; } تنتمي الخاصيّات المُعلمة بالكلمة المفتاحية static للصنف نفسه، وليس لكائن عكس بقية الخاصيّات يمكن الوصول إلى هذه الخاصيّات دون الرجوع إلى كائن محدّد ;Console.WriteLine("Bicycles created: " + Bicycle.bicyclesCreated) public static int BicyclesCreated { get; set; } عيّن القيم غير القابلة للتعديل أثناء التشغيل ولا يمكن إسناده إلا عند تعريفها أو داخل مشيّد readonly bool _hasCardsInSpokes = false; // خاصيّة خاصّة وللقراءة فقط المشيّدات Constructors هي طريقة لإنشاء الأصناف أدناه المشيّد المبدئي public Bicycle() { this.Gear = 1; // يمكن الوصول إلى خاصيّات الصنف بالكلمة المفتاحية this Cadence = 50; // إلا أنك لا تحتاجها في كل الحالات _speed = 5; Name = "Bontrager"; Brand = BikeBrand.AIST; BicyclesCreated++; } هذا مشيّد مُعيّن (يحوي معطيات) public Bicycle(int startCadence, int startSpeed, int startGear, string name, bool hasCardsInSpokes, BikeBrand brand) : base() // أولا base يستدعي { Gear = startGear; Cadence = startCadence; _speed = startSpeed; Name = name; _hasCardsInSpokes = hasCardsInSpokes; Brand = brand; } يمكن وضع المشيّدات بالتسلسل public Bicycle(int startCadence, int startSpeed, BikeBrand brand) : this(startCadence, startSpeed, 0, "big wheels", true, brand) { } صيغة كتابة الدوال (<public/private/protected> <return type> <function name> <args>) يمكن للأصناف أن تعيّن مسترجعات ومعدّلات لحقولها كما يمكنها تنفيذ الخاصيّات عبر دوال (وهي الطريقة المفضّلة في Csharp) ويمكن لمعاملات التابع أن تحوي قيما مبدئية، في هذه الحالة، يمكن استدعاء التوابع بدون تمرير معطيات عن هذه العوامل public void SpeedUp(int increment = 1) { _speed += increment; } public void SlowDown(int decrement = 1) { _speed -= decrement; } تعيّن الخاصيّات القيم وتسترجعها. إن كان غرضك الوصول إلى البيانات فقط دون تعديلها فالخاصيّات أنسب. يمكن أن يكون للخاصيّة مسترجع أو معدّل أو كلاهما private bool _hasTassles; // متغيّر خاص public bool HasTassles // مسترجع عام { get { return _hasTassles; } set { _hasTassles = value; } } كما يمكنك تعريف خاصيّة تلقائية في سطر واحد ستُنشئ هذه الصيغة حقلا داعما تلقائيا يمكنك الحد من مجال الرؤية على المسترجع أو المعدّل أو كليهما public bool IsBroken { get; private set; } يمكن للخاصيّات أن تكون تلقائية التنفيذ public int FrameSize { get; // يمكنك الحد من مجال الرؤية على المسترجع أو المعدّل //Framesize يمكنه استدعاء معدّل Bicycle يعني هذا أن الصنف private set; } يمكن تعريف فهرس على الكائنات يمكنك مثلا كتابة bicycle[0] التي ترجع القيمة “chris” للحصول على أول راكب أو كتابة “bicycle[1] = "lisa لتعيين الراكب الثاني (دراجة رباعية المقاعد!) private string[] passengers = { "chris", "phil", "darren", "regina" }; public string this[int i] { get { return passengers[i]; } set { passengers[i] = value; } } تابع لعرض قيم حقول الكائن public virtual string Info() { return "Gear: " + Gear + " Cadence: " + Cadence + " Speed: " + _speed + " Name: " + Name + " Cards in Spokes: " + (_hasCardsInSpokes ? "yes" : "no") + "\n------------------------------\n" ; } يمكن للتوابع أن تكون ثابتة (الكلمة المفتاحية static). مناسبة للدوال المساعدة public static bool DidWeCreateEnoughBicycles() { داخل التابع الثابت لا يمكن إجراء عمليات سوى على الحقول الثابتة return BicyclesCreated > 9000; } إن كان الصنف لا يحتاج إلا إلى حقول ثابتة فربما يكون من الأفضل أن يكون الصنف نفسه ثابتا } // نهاية الصنف Bicycle PennyFarthing هو صنف متفرّع من الصنف Bicycle class PennyFarthing : Bicycle { (يمثّل هذا الصنف تلك الدراجات الهوائية التي لديها عجلة أمامية كبيرة جدا، وليست لديها مسنّنات Gears لتعديل السرعة) . نستدعي مشيّد الصنف الأب public PennyFarthing(int startCadence, int startSpeed) : base(startCadence, startSpeed, 0, "PennyFarthing", true, BikeBrand.Electra) { } protected override int Gear { get { return 0; } set { throw new InvalidOperationException("You can't change gears on a PennyFarthing"); } } public static PennyFarthing CreateWithGears(int gears) { var penny = new PennyFarthing(1, 1); // عمليا لا توجد دراجة من نوع PennyFarthing بمسنّنات penny.Gear = gears; return penny; } public override string Info() { string result = "PennyFarthing bicycle "; result += base.ToString(); // نستدعي التابع الأصلي الموجود في الصنف الأب return result; } } تحتوي الواجهات على التوقيعات فقط interface IJumpable { void Jump(int meters); // جميع الأعضاء في الواجهة هي مبدئيا عمومية } interface IBreakable { bool Broken { get; } // يمكن للواجهات أن تحوي خاصيّات كما يمكنها أن تتضمّن واجهات وأحداثا } يمكن للأصناف أن ترث Inherit من صنف واحد آخر على الأكثر، إلا أنه يمكنها تنفيذ أي عدد من الواجهات ويجب أن يكون الصنف الأب الأول في لائحة الأصناف تليه الواجهات كلّها class MountainBike : Bicycle, IJumpable, IBreakable { int damage = 0; public void Jump(int meters) { damage += meters; } public bool Broken { get { return damage > 100; } } } صنف للاتصال بقاعدة البيانات، نستخدمه مثالا لعمل LinqToSql يعمل إطار العمل EntityFramework Code First لربط الكائنات بسجلات جداول البيانات (بنفس طريقة ActiveRecord في روبي، إلا أنه ثنائي الاتجاه) public class BikeRepository : DbContext { public BikeRepository() : base() { } public DbSet<Bicycle> Bikes { get; set; } } يمكن تقسيم الأصناف على ملفات cs. عدّة A1.cs public partial class A { public static void A1() { Console.WriteLine("Method A1 in class A"); } } A2.cs public partial class A { public static void A2() { Console.WriteLine("Method A2 in class A"); } } يستخدم الصنف Program أدناه الصنف A المُقسّم على ملفي cs. Program using the partial class "A" public class Program { static void Main() { A.A1(); A.A2(); } } يمكن الإداراج في سلاسل المحارف String interpolation بكتابة $ أمام السلسلة ثم إحاطة المتغيّر المُدرج بقوسين معكوفين { }. يمكنك أيضا تجميع السلسلتين، الأصلية والمُعدّلة، بالعلامة @ public class Rectangle { public int Length { get; set; } public int Width { get; set; } } class Program { static void Main(string[] args) { Rectangle rect = new Rectangle { Length = 5, Width = 3 }; Console.WriteLine($"The length is {rect.Length} and the width is {rect.Width}"); string username = "User"; Console.WriteLine([email protected]"C:\Users\{username}\Desktop"); } } ميزات جديدة في الإصدار C# 6 class GlassBall : IJumpable, IBreakable { تمهيد الخاصيّات التلقائية public int Damage { get; private set; } = 0; تمهيد الخاصيّات التلقائية المقتصرة على المسترجعات public string Name { get; } = "Glass ball"; تمهيد الخاصيّات التلقائية المقتصرة على المسترجعات في المشيّد public string GenieName { get; } public GlassBall(string genieName = null) { GenieName = genieName; } public void Jump(int meters) { if (meters < 0) العبارة nameof() مستحدثة وينتُج عنها التحقّق من وجود المعرّف "nameof(x) == "x تحول على سبيل المثال دون بقاء أسماء المتغيّرات القديمة في رسائل الخطأ بعد تحديثها throw new ArgumentException("Cannot jump negative amount!", nameof(meters)); Damage += meters; } الخاصيّات المعرَّفة ضمن هيكل العبارة public bool Broken => Damage > 100; نفس الشيء بالنسبة للتوابع public override string ToString() // سلسلة محارف تُدرج ضمنها متغيّرات => $"{Name}. Damage taken: {Damage}"; public string SummonGenie() العوامل المشترطة بالقيمة الفارغة null ترجع العبارة x?.y القيمة null بمجرد كون x مساوية ل null، بدون تقييم y => GenieName?.ToUpper(); } static class MagicService { private static bool LogException(Exception ex) { /* سجّل الاستثناءات في مكان ما */ log exception somewhere */ return false; } public static bool CastSpell(string spell) { try { // API نفترض هنا أننا نستدعي واجهة تطبيقات برمجية throw new MagicServiceException("Spell failed", 42); // نجح الاستدعاء return true; } يلتقط استثناء في حالة إخفاق استدعاء واجهة التطبيقات، أي أن قيمة Code تساوي 42 Only catch if Code is 42 i.e. spell failed catch(MagicServiceException ex) when (ex.Code == 42) { // أخفق الاستدعاء return false; } استثماءات أخرى أو الاستثناء MagicServiceException عندما تكون قيمة المتغير Code مختلفة عن 42 catch(Exception ex) when (LogException(ex)) { // لا يصل التنفيذ إلى هذه الكتلة } return false; } لاحظ أن التقاط الاستثناء MagicServiceException وإعادة إطلاقه عندما يكون المتغير Code لا يساوي القيمة 42 أو 117 هو أمر مختلف، إذ أن كتلة catch-all الأخيرة لن تلتقط الاستثناء المُعاد public class MagicServiceException : Exception { public int Code { get; } public MagicServiceException(string message, int code) : base(message) { Code = code; } } الخاصية Obsolete public static class PragmaWarning { [Obsolete("Use NewMethod instead", false)] public static void ObsoleteMethod() { /*شفرة برمجية قديمة هنا */ } public static void NewMethod() { /* شفرة برمجية جديدة */ } public static void Main() { ObsoleteMethod(); تحذير يظهر عند استخدام شفرة برمجية قديمة، ناتج عن الوسم Obsolete أعلاه CS0618: 'ObsoleteMethod is obsolete: Use NewMethod instead' تعطّل التعليمة التالية إظهار التحذير السابق #pragma warning disable CS0618 ObsoleteMethod(); // لا تحذير #pragma warning restore CS0618 ObsoleteMethod(); // CS0618: 'ObsoleteMethod is obsolete: Use NewMethod instead' } } } // نهاية فضاء الأسماء using System; ميزة في C# 6: إمكانية استخدام static مع using using static System.Math; namespace Learning.More.CSharp { class StaticUsing { static void Main() { // using مع static بدون استخدام Console.WriteLine("The square root of 4 is {}.", Math.Sqrt(4)); // using مع static باستخدام Console.WriteLine("The square root of 4 is {}.", Sqrt(4)); } } } ميزة جديدة في C# 7 ثبّت آخر إصدار من Microsoft.Net.Compilers باستخدام Nuget ثبّت آخر إصدار من System.ValueTuple باستخدام Nuget using System; namespace Csharp7 { الأزواج المرتبة Tuples، التفكيك DECONSTRUCTION والإلغاءات Discards class TuplesTest { public (string, string) GetName() { // Item1، Item2 .... تُسمى الحقول في الأزواج المرتبة مبدئيا بـ var names1 = ("Peter", "Parker"); Console.WriteLine(names1.Item2); // => Parker يمكن تخصيص أسماء الحقول تعريف النوع الأول (string FirstName, string LastName) names2 = ("Peter", "Parker"); تعريف النوع الثاني var names3 = (First:"Peter", Last:"Parker"); Console.WriteLine(names2.FirstName); // => Peter Console.WriteLine(names3.Last); // => Parker return names3; } public string GetLastName() { var fullName = GetName(); يمكن تفكيك الأزواج المرتبة (string firstName, string lastName) = fullName; يمكن إلغاء حقول من الزوج المرتب بعد تفكيكه بالعلامة _ Fields in a deconstructed tuple can be discarded by using _ var (_, last) = fullName; return last; } يمكن تفكيك أي نوع بيانات على نفس المنوال باستخدام التابع Deconstruct public int randomNumber = 4; public int anotherRandomNumber = 10; public void Deconstruct(out int randomNumber, out int anotherRandomNumber) { randomNumber = this.randomNumber; anotherRandomNumber = this.anotherRandomNumber; } static void Main(string[] args) { var tt = new TuplesTest(); (int num1, int num2) = tt; Console.WriteLine($"num1: {num1}, num2: {num2}"); // => num1: 4, num2: 10 Console.WriteLine(tt.GetLastName()); } } مطابقة الأنماط Pattern matching class PatternMatchingTest { public static (string, int)? CreateLogMessage(object data) { switch(data) { // when ترشيح إضافي باستخدام case System.Net.Http.HttpRequestException h when h.Message.Contains("404"): return (h.Message, 404); case System.Net.Http.HttpRequestException h when h.Message.Contains("400"): return (h.Message, 400); case Exception e: return (e.Message, 500); case string s: return (s, s.Contains("Error") ? 500 : 200); case null: return null; default: return (data.ToString(), 500); } } } الإحالة إلى الموارد المحلية Reference locals تعطيك إمكانية إرجاع مرجع Reference كائن بدلا من قيمته class RefLocalsTest { لاحظ الكلمة المفتاحية ref في تعليمة الإرجاع return public static ref string FindItem(string[] arr, string el) { for(int i=0; i<arr.Length; i++) { if(arr[i] == el) { // إرجاع المرجع return ref arr[i]; } } throw new Exception("Item not found"); } public static void SomeMethod() { string[] arr = {"this", "is", "an", "array"}; //في كل مكان ref لاحظ ref string item = ref FindItem(arr, "array"); item = "apple"; Console.WriteLine(arr[3]); // => apple } } الدوال المحليّة Local functions class LocalFunctionTest { private static int _id = 0; public int id; public LocalFunctionTest() { id = generateId(); لا يمكن الوصول إلى الدالة المحلية خارج هذا المجال int generateId() { return _id++; } } public static void AnotherMethod() { var lf1 = new LocalFunctionTest(); var lf2 = new LocalFunctionTest(); Console.WriteLine($"{lf1.id}, {lf2.id}"); // => 0, 1 int id = generateId(); // خطأ // error CS0103: The name 'generateId' does not exist in the current context } } } ترجمة -وبتصرّف- للمقال Learn C# in Y Minutes
-
تُعتبر منصّة Xamarin في الوقت الحالي، من أهمّ منصّات تطوير تطبيقات الأجهزة المحمولة المتوفّرة، والتي يبدو أنّها ستبقى على الواجهة لوقت ليس بالقليل، خصوصًا بعد استحواذ شركة مايكروسوفت على الشركة المنتجة لها والتي تحمل أيضًا نفس الاسم (Xamarin). قد تكون متحفّزًا للدخول في عالم تطوير تطبيقات الأجهزة المحمولة (كما كنت عندما تعرّفت للمرّة الأولى على Xamarin) ومستعدًّا لكتابة الشيفرة مباشرةً، ولكن لنصبر قليلًا، ولنفهم بدايات الأمور، ومبدأ عمل Xamarin بشكل أفضل ضمن هذا المقال، الذي يُعدّ الأوّل في سلسلة مقالات سترشدك إلى كيفيّة تطوير تطبيقات باستخدام منصّة Xamarin مع إنشاء تطبيقات عمليّة من خلالها. مقدمة تمّ تأسيس شركة Xamarin (وتُقرأ زامارين) في عام 2011، من قِبَل نفس المهندسين الذين صمّموا مشروع Mono. ومشروع Mono وإخوانه مثل Mono for android و Mono Touch، هي عبارة عن منصّات لتشغيل تطبيقات مكتوبة بلغة سي شارب C# على أنظمة التشغيل: Linux و Android و iOS على الترتيب. يمكن باستخدام Xamarin إنشاء تطبيقات أصليّة Native Apps لأجهزة Android و iOS و Mac وويندوز بلغة برمجة ليست معتمدة رسميًا بالنسبة إليها. سنركّز في هذه السلسلة على بناء تطبيقات تعمل على نظام التشغيل Android. لمن هذه السلسلة؟ إذا كان لديك إلمام جيّد بلغة سي شارب ومكتبة الأصناف الأساسيّة BCL ضمن إطار العمل دوت نت بالإضافة إلى معرفة أوليّة بطريقة استخدام بيئة التطوير Visual Studio 2015، وتمتلك الشغف والصبر لتعلّم كيفيّة برمجة تطبيقات للأجهزة المحمولة باستخدام Xamarin فإنّ هذه السلسلة هي بالتأكيد لك. لدينا هنا في أكاديميّة حسوب سلسلة تعليميّة خاصّة بلغة سي شارب يمكن الاستفادة منها. الحاجة إلى Xamarin برزت الحاجة إلى Xamarin بسبب طبيعة تطبيقات الأجهزة المحمولة وعملها على أنظمة تشغيل مختلفة تعود إلى شركات متنافسة. توجد في الوقت الحالي ثلاثة أنظمة تشغيل مسيطرة على السوق بصورة متفاوتة وهي: iOS لشركة Apple وAndroid لشركة Google وWindows Phone لشركة Microsoft. تختلف هذه الأنظمة في العديد من النواحي، التي سنناقشها فيما يلي. تجربة المستخدم بالنسبة إلى تجربة المستخدم يوجد تشابه في هذه الأنظمة من ناحية تقديم الواجهات الرسوميّة للمستخدم والتفاعل مع الجهاز من خلال اللمس أو اللمس المتعدّد، ولكنّ هناك اختلافات في التفاصيل. فلكلّ نظام تشغيل وسائل مختلفة في التنقّل بين صفحات التطبيق، وفي تقديم البيانات، وفي العمل مع القوائم، وغيرها من التفاصيل الأخرى التي تتطلّب أن يسلك المطوّر developer منحىً مختلفًا من أجل كلّ نظام تشغيل. بيئات تطوير ولغات برمجة مختلفة أمّا بالنسبة للغات البرمجة وبيئات التطوير، فهذا أمر آخر. فلكلّ نظام تشغيل متطلّباته الخاصّة التي ألخّصها بشكل سريع فيما يلي: لإنشاء تطبيقات على أنظمة iOS فأنت تحتاج إلى إتقان لغة البرمجة Objective-C أو لغة البرمجة Swift. وأن تمتلك حاسوب MacBook (أي إصدار) مع بيئة التطوير Xcode. لإنشاء تطبيقات على أنظمة Android فستحتاج إلى لغة جافا Java مع بيئة التطوير Android Studio التي تعمل على العديد من أنظمة التشغيل. أمّا لإنشاء تطبيقات تعمل على Windows Phone أو Windows 10 Mobile فأنت بحاجة إلى لغة البرمجة سي شارب C# مع حاسوب يشغّل ويندوز، وإلى بيئة التطوير Visual Studio. واجهات برمجية مختلفة تعتمد جميع أنظمة التشغيل السابقة على واجهات تطبيق برمجيّة API مختلفة. مع أنّه يوجد بعض الشبه بالنسبة للكائنات المتعلّقة بواجهة المستخدم user-interface. فعلى سبيل المثال، جميع الأنظمة السابقة توفّر وسيلة للمستخدم لاختيار حالة منطقيّة Boolean والتي يمكن تمثيلها بـ True أو False ففي iOS يصنّف هذا الكائن على أنّه view اسمه UISwitch، أمّا في أندرويد فهو widget اسمها Switch، وفي ويندوز فهو control ويسمّى ToggleSwitch. الحل الذي توفره Xamarin يمكن تجاوز جميع النقاط السابقة من خلال Xamarin لأنّها توفّر لغة برمجة وحيدة وهي سي شارب يمكن استخدامها لكتابة تطبيقات على أيّ نظام تشغيل. كما أنّها توفّر بيئة تطوير متقدّمة ووحيدة وهي Visual Studio 2015 لكتابة هذه التطبيقات (يمكن استخدام بيئة التطوير Xamarin Studio أيضًا بالنسبة لنظام Mac). بالإضافة إلى أنّها وحّدت الواجهات البرمجيّة المختلفة API ضمن واجهة برمجيّة وحيدة يتعامل معها المطوّر. لغة سي شارب غنيّة عن التعريف. فهي لغة قويّة ومرنة وغنيّة جدًا ودائمة التطوير. لقد أصبحت بحق من أكثر لغات البرمجة تطوّرًا وحداثةً. ولسنا هنا بصدد تفضيل لغة برمجة على أخرى، ولكن بحسب خبرتي الشخصيّة، واطّلاعي على العديد من لغات البرمجة الأخرى، تستطيع القول بأنّ سي شارب تحتلّ موقعًا مرموقًا بينها. يستطيع المطوّرون من أجل تطبيق محدّد، كتابة شيفرة واحدة مشتركة لجميع أنظمة التشغيل السابقة بدون أيّة تعديلات تُذكر عليها فيما يتعلّق بمنطق العمل business logic داخل التطبيق وبما يتضمنّه من عمليّات برمجيّة لا تتعلّق بنوع الجهاز المحمول أو نظام التشغيل الذي يعمل عليه. نسمّي هذه الشيفرة بالشيفرة المستقلة عن نظام التشغيل platform independent. أمّا إذا تطلّب الأمر من التطبيق أن يتعامل مع العتاد الصلب للجهاز الذي يعمل عليه (مثل الكاميرا أو حسّاس GPS مثلًا)، فيمكن عندها كتابة أجزاء من الشيفرة التي تراعي خصوصيّة كل نظام تشغيل، نسمّي مثل هذه الشيفرة بالشيفرة المرتبطة بنظام التشغيل platform dependent. المكونات الرئيسية لمنصة Xamarin ركّزت شركة Xamarin منذ نشأتها على التكنولوجيا الخاصّة بالمترجمات Compilers. وقد أصدرت الشركة ثلاث مجموعات أساسيّة من مكتبات دوت نت .NET وهي: Xamarin.Mac و Xamarin.iOS و Xamarin.Andorid التي تشكّل بمجموعها منصّة Xamarin. تسمح هذه المكتبات للمطوّرين بكتابة تطبيقات أصليّة native apps لكلّ من أنظمة التشغيل الموافقة. ذكرنا قبل قليل أنّ هناك جزء من شيفرة التطبيق البرمجيّة يتكرّر عادةً من أجل كل نظام تشغيل، يمكن عزل هذا الجزء ووضعه ضمن مشروع منفصل في برنامج Visual Studio. لهذا المشروع نوعين: مشروع الأصول المشتركة Shared Asset Project أو اختصارًا SAP. وهو عبارة عن مجموعة من ملفات الشيفرة بالإضافة إلى ملفّات الأصول assets مثل الصور وغيرها الموجودة ضمن المشروع والتي يمكن مشاركتها مع باقي المشاريع الأخرى ضمن نفس الحل Solution في Visual Studio. مكتبة الأصناف المحمولة Portable Class Library أو اختصارًا PCL. وهي تضمّ الشيفرة التي نرغب بمشاركتها وذلك ضمن مكتبة ربط ديناميكي dynamic link library أو اختصارًا DLL. سنركّز في هذه السلسلة على هذا النوع. أمّا بالنسبة للشيفرة التي تتعلّق بنظام التشغيل، فتوضع ضمن مشروع منفصل ضمن Visual Studio لكلّ نظام تشغيل نرغب بأن ندعمه. ففي الحالة العامّة، يمكن أن يكون لدينا ثلاثة مشاريع منفصلة لكل من Android و iOS و Windows Phone. تستفيد جميعها من الشيفرة المشتركة والمستقلّة عن نظام التشغيل الموجودة في PCL أو SAP. وبالمجمل تكون جميع تلك المشاريع ضمن نفس الحل Solution ضمن Visual Studio. انظر إلى الشكل التالي الذي يوضّح كيفيّة تفاعل التطبيق المكتوب لأنظمة التشغيل المختلفة مع مكوّنات Xamarin: يمثّل الشكل السابق تطبيقًا واحدًا. نلاحظ في السطر الثاني الأشكال المختلفة لهذا التطبيق على أنظمة التشغيل المختلفة. انظر كيف تتفاعل هذه الأشكال المختلفة مع مشروع PCL أو SAP للاستفادة من الشيفرة المشتركة التي لا تتعلّق بنظام تشغيل محدّد. انظر إلى تطبيق iOS مثلًا كيف يتفاعل أيضًا مع المكتبة Xamarin.iOS التي توفّر وسيلة للربط bindings بينه وبين واجهة التطبيقات البرمجيّة API لنظام التشغيل iOS. ينطبق نفس الأمر تماماً بالنسبة لتطبيق أندرويد. الاستثناء الوحيد هو بالنسبة لتطبيق ويندوز الذي لا يحتاج إلى وسيط (وسيلة للربط) في هذه الحالة، حيث يمكنه الاتصال مباشرةً مع واجهة API لنظام التشغيل ويندوز (بصورة أدق Windows Phone). نماذج Xamarin ابتكرت شركة Xamarin في عام 2014 ما يُعرف بنماذج Xamarin أو Xamarin Forms. تسمح هذه المنصّة للمطوّرين بأن يكتبوا شيفرة برمجيّة لواجهة المستخدم user interface بحيث يمكن تحويل هذه الشيفرة مباشرةً إلى تطبيقات تعمل على أجهزة أندرويد و iOS وويندوز. في الحقيقة أصبحت Xamarin Forms تدعم إنشاء تطبيقات عامّة Universal Windows Platform على الأجهزة التي تشغّل نسخ ويندوز المختلفة مثل Windows Phone و Windows 10 و Windows 10 Mobile و Windows 8.1 و Windows 8.1 Phone. بالنسبة لبنية الحل Solution في Visual Studio فلن يتغيّر كثيرًا، باستثناء أنّ المشاريع المنفصلة الخاصّة بأنظمة التشغيل السابقة ستكون صغيرة على نحو ملحوظ بسبب وجود كميّة قليلة من الشيفرة البرمجيّة ضمنها. سيتضمّن مشروع PCL أو SAP في هذه الحالة الشيفرة المشتركة والمستقلّة عن أنظمة التشغيل كما اتفقنا على ذلك من قبل، بالإضافة إلى الشيفرة البرمجيّة المسؤولة عن الإظهار والتعامل مع واجهة المستخدم. أي أنّ منصّة Xamarin Forms تسمح لنا بكتابة شيفرة برمجيّة واحدة تعمل مباشرةً على أنظمة التشغيل المختلفة. انظر الشكل التالي لفهم آليّة عمل التطبيقات التي تعتمد Xamarin Forms. تعتمد التطبيقات المكتوبة لأنظمة التشغيل المختلفة في هذه الحالة على مشروع PCL أو SAP بشكل كليّ في التواصل مع الواجهات البرمجيّة API. وهكذا من الممكن في الكثير من التطبيقات كتابة شيفرة واحدة فقط تعمل على جميع الأجهزة! باستثناء الحالات التي يكون من الضروري فيها كتابة شيفرة مخصّصة لنظام تشغيل محدّد. في المستقبل قد يتغيّر هذا الأمر، فقد يكون من الممكن كتابة شيفرة واحدة فقط تعمل على جميع الأجهزة مهما كان نوع هذه الشيفرة. سنتحدّث في هذه السلسلة عن Xamarin Forms بشكل أساسيّ. كيفية عمل Xamarin بالنسبة لتطبيقات iOS فيعمل مترجم سي شارب الخاص بـ Xamarin على ترجمة الشيفرة البرمجيّة إلى لغة مايكروسوفت الوسيطيّة MSIL، ثمّ يُستخدم مترجم Apple على نظام تشغيل Mac لتوليد رُماز أصليّ native code يعمل على iOS كما لو أنّه تطبيق مكتوب بلغة Objective-C تمامًا. أمّا بالنسبة لتطبيقات Android، فسيولّد المترجم أيضًا لغة MSIL التي ستعمل في هذه الحالة على بيئة تنفيذ مشتركة CLR مخصّصة للعمل على أندرويد. وستكون التطبيقات الناتجة في هذه الحالة تشبه أيضًا إلى حدّ كبير تلك المنشأة باستخدام لغة جافا وبيئة التطوير Android Studio الخاصّة بأندرويد. وأخيرًا بالنسبة للتطبيقات التي تعمل على Windows Phone و Windows 10 Mobile، فالتطبيقات مدعومة بشكل واضح، وستعمل كالتطبيقات الأصليّة المنشأة باستخدام Visual Studio بدون استخدام Xamarin. الخلاصة منصّة Xamarin واعدة، ولها تاريخ عريق وتجارب غنيّة من قبل أن تظهر شركة Xamarin إلى الوجود. ولعلّ مايكروسوفت قد أدركت الأهميّة الكبيرة لها، فتمّت عمليّة الاستحواذ التي كانت متوقعّة. أنصحك بأن تبادر إلى تعلّم Xamarin وخصوصًا في حال كنت مبرمج سي شارب، أو لديك معلومات أوليّة عنها. ولعلّ الأيّام القادمة قد تحمل المزيد من الدعم والمفاجآت لمنصّة Xamarin التي كان أوّلها هو جعلها مجّانيّة للاستخدام الشخصيّ أو للفرق البرمجيّة الصغيرة. سنبدأ اعتبارًا من الدرس القادم في هذه السلسلة التعليميّة، تنصيب برنامج Visual Studio 2015 وبدء العمل مع Xamarin.
-
يُعتبر إطار العمل دوت نت 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 المشهور والموجّه لتطبيقات إنترنت الأشياء، وهذا يعني فتح المجال أمام التطبيقات المكتوبة باستخدام لغة سي شارب للعمل على هذا الجهاز وبالتالي الدخول في هذا المجال المهم والواعد. الخلاصة لغة سي شارب هي لغة عصريّة ولها مستقبل واعد في قطاع الأعمال. فهي اللغة الأساسيّة المستخدمة لكتابة مختلف أنواع التطبيقات الحاسوبيّة ضمن إطار العمل دوت نت. بالإضافة إلى أنّها سلسة وذات بنية مألوفة، ولكن تحتاج إلى قليل من الصبر في تعلّمها. سنتناول في الدروس اللّاحقة كيفيّة كتابة برامج بسيطة ولكن متدرّجة في الصعوبة حتى تكون ملمًّا بأساسيّات هذه اللغة الرائعة.
- 7 تعليقات
-
- 10
-
- visual studio
- dot net
-
(و 5 أكثر)
موسوم في:
-
تحدثنا في الدرسين السابقين عن المبادئ الأوليّة لتطبيق مفاهيم البرمجة الكائنيّة التوجّه في سي شارب. سننهي في هذا الدرس تطبيق هذه المبادئ، حيث سنتناول موضوع الوراثة 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 وأهميّتها وكيفيّة استثمارها في هذه اللغة. وتعاملنا أيضًا مع التحويل بين الأنواع ورأينا كيف يمكن لمتغيّر من صنف أب أن يحمل مراجع لكائنات من أصناف أبناء. تُستَخدم هذه الأساليب على نحو واسع جدًّا في مكتبة الأصناف الأساسيّة، وستحتاجها في العديد من التطبيقات التي تُنشئها.
- 3 تعليقات
-
- 2
-
- polymorphism
- inheritance
- (و 7 أكثر)
-
تحدثنا في الدرس السابق عن المبادئ الأوليّة لتطبيق مفاهيم البرمجة كائنيّة التوجّه في سي شارب، حيث تعلّمنا كيفيّة إنشاء الأصناف وأعضائها، وإنشاء الكائنات من الأصناف والتعامل معها. سنتابع في هذا الدرس الحديث عن تطبيق مبادئ البرمجة كائنيّة التوجّه في سي شارب حيث سنتناول مُحدّد الوصول 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 وكيفيّة التعامل معها.
-
يُعتبر التعامل مع النصوص من المهام البرمجيّة الأساسيّة في جميع أنواع التطبيقات بما فيها تطبيقات الأجهزة المحمولة. توفّر Xamarin تقنيّات جيّدة للتعامل مع النصوص، سنتناول العديد منها من خلال هذا الدرس، وأيضًا من خلال دروس لاحقة في هذه السلسلة. سنعالج في هذا الدرس بعضًا من الحالات التي قد تواجه المطوّر عند التعامل مع النصوص في التطبيقات التي يُنشئها. التعامل مع المقاطع النصية نحتاج في الكثير من الأحيان أن نعرض نصًّا طويلًا بعض الشيء على الشاشة. قد يأتي هذا النص من مستند أو من خدمة ويب web service أو من غيرها من المصادر. المتنوّعة، وينبغي أن يتمكّن التطبيق من التعامل مع نصوص بمثل هذا الحجم. كما فعلنا من الدرس السابق، أنشأ مشروعًا جديدًا من النوع (Blank App (Xamarin.Forms Portable وسمّه TextManipulationApp. أبق فقط على المشروعين (TextManipulationApp (Portable و TextManipulationApp.Droid. سنضيف الآن صفحة محتوى content page جديدة ولكن بطريقة مختلفة ومختصرة عما فعلناه في الدرس السابق. انقر بزر الفأرة الأيمن على المشروع (TextManipulationApp (Portable واختر Add ثم من القائمة الفرعية التي ستظهر اختر New Item. اختر من الجهة اليسرى للنافذة التي ستظهر العقدة Cross-Platform، وبعد تحديث محتويات القسم الأوسط من النافذة، اختر نوع الملف Forms ContentPage وامنحه الاسم ParagraphPage من مريع الاسم Name في الأسفل، وبعد ذلك انقر Add. كما في الشكل التالي: بعد أن يضيف Visual Studio هذا الملف، ستكون محتوياته على الشكل التالي: using System; using System.Collections.Generic; using System.Linq; using System.Reflection.Emit; using System.Text; using Xamarin.Forms; namespace TextManipulationApp { public class ParagraphPage : ContentPage { public ParagraphPage() { Content = new StackLayout { Children = { new Label { Text = "Hello ContentPage" } } }; } } } توجد نطاقات أسماء غير ضرورية هنا ولكن لا بأس بها حاليًا. لاحظ أنّ الصنف ParagraphPage يرث من الصنف ContentPage بشكل افتراضي، كما لاحظ أنّ بانية الصنف ParagraphPage جاهزة، وتحتوي على مثال بسيط جاهز. احذف محتويات هذه البانية، لنبدأ العمل على تطبيقنا. سننشئ لصيقة Label تحتوي على مقطع نصيّ مكتوب باللغة العربية. اكتب الشيفرة التالية ضمن بانية الصنف ParagraphPage: Content = new Label { VerticalOptions = LayoutOptions.Center, Text = "تُعتبر منصّة Xamarin في الوقت الحالي، من أهمّ منصّات تطوير تطبيقات الأجهزة المحمولة المتوفّرة،" + " والتي يبدو أنّها ستبقى على الواجهة لوقت ليس بالقليل، " + "خصوصًا بعد استحواذ شركة مايكروسوفت على الشركة المنتجة لها والتي تحمل أيضًا نفس الاسم." + "سنتعلّم من خلال هذه السلسلة كتابة تطبيقات عمليّة من خلال استخدام هذه التقنيّة الواعدة." }; انتقل إلى الملف App.cs ضمن المشروع (TextManipulationApp (Portal واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new ParagraphPage(); } نفّذ البرنامج باستخدام F5 لتحصل على شكل شبيه بما يلي: تجدر الملاحظة أنّه تتم مُحاذاة كامل النص رأسيًّا في منتصف الشاشة. سبب ذلك هو إسناد القيمة LayoutOptions.Center إلى الخاصيّة VerticalOptions (خيارات التموضع الرأسيّة) للصيقة Label. لاحظ أيضًأ أنّ النص في الشكل السابق تمّت مُحاذاته نحو اليسار، وليس إلى اليمين كما هو متوقّع بالنسبة للنصوص العربيّة. يعود سبب ذلك إلى أنّ الخاصيّة HorizontalTextAlignment من الصنف Label والمسؤولة عن المحاذاة الأفقيّة للنص ضمن اللصيقة سيكون لها القيمة الافتراضيّة TextAlignment.Start عند تشغيل البرنامج. و TextAlignment عبارة عن معدودة enum تحتوي على ثلاثة قيم هي: Start و Center و End. وبما أنّ شاشة الجهاز المحمول تُقسم إلى ثلاثة مناطق أساسيّة أفقيًا: Start للمنطقة اليسرى و Center للمنطقة الوسطى (المركزية) و End للمنطقة اليمنى من الشاشة لذلك ستتم محاذاة النص إلى اليسار بدلًا من اليمين. إذا أردنا أن تمم مُحاذاة النص إلى اليمين، فكل ما عليك فعله هو إسناد القيمة TextAlignment.End للخاصية HorizontalTextAlignment، وهذا ما سنفعله بعد قليل. وهناك أمر آخر نلاحظه أيضًا من الشكل السابق، وهو أنّ النص يقترب كثيرًا من حواف الشاشة حتى يكاد يلتصق بها. حلّ هذا الموضوع بسيط ويتمثّل في إضافة حشوة padding على حواف الشاشة، وهي عبارة عن مساحة صغيرة يمكن تعيينها برمجيًا للشاشة، نستطيع من خلالها التحكم بمساحة الهامش على الأطراف الأربعة لها. انظر الآن إلى بانية الصنف ParagraphPage بعد التعديلات المطلوبة: public ParagraphPage() { Content = new Label { VerticalOptions = LayoutOptions.Center, Text = "تُعتبر منصّة Xamarin في الوقت الحالي، من أهمّ منصّات تطوير تطبيقات الأجهزة المحمولة المتوفّرة،" + " والتي يبدو أنّها ستبقى على الواجهة لوقت ليس بالقليل، " + "خصوصًا بعد استحواذ شركة مايكروسوفت على الشركة المنتجة لها والتي تحمل أيضًا نفس الاسم." + "سنتعلّم من خلال هذه السلسلة كتابة تطبيقات عمليّة من خلال استخدام هذه التقنيّة الواعدة.", HorizontalTextAlignment = TextAlignment.End }; Padding = new Thickness(5, 5, 5, 5); } أضفنا أولًا الخاصيّة HorizontalTextAlignment إلى اللصيقة Label وأسندنا إليها القيمة TextAlignment.End لمحاذاة النص ضمن اللصيقة نحو اليمين (لاحظ أنّ الفاصلة العادية تفصل بين الخصائص). كما أضفنا الخاصية Padding (من الصنف ParagraphPage) وأسندنا إليها كائنًا جديدًا من الصنف Thickness (السماكة). تخضع بانية الصنف Thickness لزيادة التحميل حيث لها أكثر من شكل. استخدمت الشكل الأخير الذي يقبل أربعة وسائط تمثّل مقادير الحشوة padding من اليسار left والأعلى top واليمين right والأسفل bottom على الترتيب. قد تتساءل عن واحدة القياس المستخدمة هنا، ولكنني سنؤجل النقاش حولها إلى درس لاحق. عند تنفيذ البرنامج ستحصل على شكل شبيه بما يلي: لاحظ الآن كيف تمّت مُحاذاة النص نحو اليمين (ضمن اللصيقة Label)، ولاحظ أيضًا كيف ابتعد النص عن حواف الشاشة بعد تعيين الخاصية Padding لصفحة المحتوى ParagraphPage. ملاحظة: أرجو التمييز بين الخاصيتين HorizontalOptions و HorizontalTextAlignment للصيقة Label. فالأولى تمثّل خيارات التموضع الأفقية للصيقة ضمن الشاشة. أمّا الثانية فتمثّل محاذاة النص الأفقية ضمن اللصيقة. وبنفس الأسلوب، يجب التمييز بين الخاصيتين VerticalOptions و VerticalTextAlignment للصيقة Label ولكن من الناحية الرأسية بدلًا من الأفقية. تنسيق النص ضمن اللصيقة يمكن تنسيق النص الموجود ضمن لصيقة Label بالشكل الذي نرغبه عن طريق استخدام خاصيّة أخرى من خصائص اللصيقة وهي FormattedText. الخاصية FormattedText هي من الصنف FormattedString، والذي يحتوي بدوره على خاصية اسمها Spans من النوع <IList<Span فهي عبارة عن مجموعة (قائمة) من كائنات من النوع Span. أيُّ كائن من الصنف Span ينسّق جزءًا محدّدًا من النص الكلي، ويتم التحكم بهذا التنسيق من خلال ست خصائص من الصنف Span هي: جزء النص المراد تنسيقه Text واسم الخط FontFamily وحجم الخط FontSize وسمات الخط FontAttributes ولون النص ForegroundColor ولون الخلفية BackgroundColor. إذا شعرت ببعض الارتباك من الكلام السابق فلا بأس! سنوضّح هذه الأمور من خلال مثال بسيط يعمل على إضافة بعض التأثيرات على النص الذي يظهر على الشاشة. انظر أولًا إلى الشكل الذي أود الحصول عليه ثم لنوضّح كيفيّة فعل ذلك برمجيًّا: قد لا يبدو التنسيق السابق جميلًا، ولكنّه كفيل بتوضيح الفكرة. لاحظ في البداية أنّ كلمة "المجد" قد لوّنت باللون الأزرق مع خلفية صفراء. أمّا عبارة "لن تبلغ المجد حتى تلعق الصبر" فتم تلوينها باللون Aqua مع ملاحظة أنّ هذه العبارة ذات حجم نص أكبر من حجم النص للعبارة التي قبلها. لقد أضفت صنف محتوى جديد Forms ContentPage سميته FormattedParagraphPage إلى نفس المشروع (TextManipulationApp (Portable السابق، وبأسلوب مماثل للأسلوب الذي أضفنا فيه الصنف ParagraphPage في بداية هذا الدرس. انظر إلى محتويات الملف FormattedParagraphPage.cs: 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Reflection.Emit; 5 using System.Text; 6 7 using Xamarin.Forms; 8 9 namespace TextManipulationApp 10 { 11 public class FormattedParagraphPage : ContentPage 12 { 13 public FormattedParagraphPage() 14 { 15 FormattedString formattedString = new FormattedString(); 16 17 formattedString.Spans.Add(new Span 18 { 19 Text = "لا تحسبنّ ", 20 }); 21 22 formattedString.Spans.Add(new Span 23 { 24 Text = "المجد ", 25 BackgroundColor = Color.Yellow, 26 ForegroundColor = Color.Blue, 27 }); 28 29 formattedString.Spans.Add(new Span 30 { 31 Text = "تمرًا أنت آكله ", 32 }); 33 34 formattedString.Spans.Add(new Span 35 { 36 Text = "لن تبلغ المجد حتى تلعق الصبر.", 37 ForegroundColor = Color.Aqua, 38 FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) 39 }); 40 41 Content = new Label 42 { 43 VerticalOptions = LayoutOptions.Center, 44 FormattedText = formattedString, 45 HorizontalTextAlignment = TextAlignment.End, 46 FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)) 47 }; 48 49 Padding = new Thickness(5, 5, 5, 5); 50 } 51 } 52 } بدأنا في البانية (السطر 15) بإنشاء كائن من الصنف FormattedString وإسناده إلى المتغيّر formattedString. بعد ذلك بدأنا بإضافة النصوص المنسّقة إلى القائمة Spans باعتماد الأسلوب البسيط التالي (الأسطر من 17 حتى 20): formattedString.Spans.Add(new Span { Text = "لا تحسبنّ " }); استخدمنا التابع Add من القائمة Spans لإضافة كائن من النوع Span حيث أنشأنا هذا الكائن وأسندنا الخاصية Text له مباشرة عند الإنشاء. النص في الشيفرة السابقة لا يحمل أي تنسيق خاص. في الحقيقة لقد كرّرنا هذا الأسلوب من أجل كل جزء من النص الكامل. ولكنّنا اعتمدنا بعض التنسيقات النصيّة المختلفة أحيانًا. انظر مثلًا إلى الشيفرة الموجودة في الأسطر من 22 حتى 27: formattedString.Spans.Add(new Span { Text = "المجد ", BackgroundColor = Color.Yellow, ForegroundColor = Color.Blue, }); هذه المرّة نسّقنا النص "المجد" بحيث أسندنا اللون الأصفر كلون للخلفية BackgroundColor، واللون الأزرق للون النص ForegroundColor. انظر أيضًا إلى جزء النص الذي أضفناه في الأسطر من 34 حتى 39: formattedString.Spans.Add(new Span { Text = "لن تبلغ المجد حتى تلعق الصبر.", ForegroundColor = Color.Aqua, FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) }); هذه المرّة سيكون لون الخط Aqua وحجم الخط كبير Large. لاحظ كيف أسندنا قيمة حجم الخط إلى الخاصية FontSize. استخدمت لهذا الغرض التعبير التالي: Device.GetNamedSize(NamedSize.Large, typeof(Label)) الصنف Device يمثّل الجهاز الحالي الذي يعمل عليه تطبيقنا، ويحتوي على العديد من التوابع الساكنة المفيدة. من هذه التوابع استخدمنا التابع GetNamedSize الذي يُرجع حجم الخط المطلوب حسب الوسائط الممرّرة له. الوسيط الأول هو NamedSize.Large أي أنّنا نريد حجم كبير للخط (NamedSize هي معدودة)، والوسيط الثاني هو نوع العنصر المراد تطبيق هذا الخط عليه. في حالتنا نريد تطبيق هذا الخط على لصيقة Label لذلك مرّرنا (typeof(Label كوسيط ثانٍ حيث يُرجع النوع الخاص Type باللصيقة Label. هذا هو الأسلوب المفضّل في تعيين القياسات المناسبة لأيّ شيء يخطر ببالك، وذلك لأنّ الأجهزة التي نعمل عليها ستكون شاشاتها مختلفة القياسات بكلّ تأكيد، لذلك فنرغب أن نترك للتابع GetNamedSize مهمة القيام بالعمليات الحسابية المناسبة لإرجاع حجم الخط المناسب بالنسبة لحجم الشاشة التي يعمل عليها البرنامج حاليًا، وللحجم المراد الحصول عليه (في مثالنا هذا أردنا الحصول على حجم خط كبير NamedSize.Large وتوجد بالتأكيد قياسات أخرى). ما تبقى من البرنامج سهل للغاية، حيث نقوم في السطر 44 بإسناد المتغيّر formattedString إلى الخاصية FormattedText للصيقة. يكفل ذلك بعرض النص منسّقًا على الشاشة، مع الانتباه إلى أنّنا لم نستخدم في هذه الحالة الخاصية Text للصيقة. هناك أمر أخير يجب الانتباه إليه. لاحظ أنّنا في السطر 46 نُسند حجم خط جديد إلى الخاصية FontSize الخاصة باللصيقة. سيكون هذا الخط متوسّط الحجم (لاحظ الوسيط الأوّل NamedSize.Medium). الفكرة هنا هو أنّ هذا الخط سيطبّق على كل جزء من النص (موجود ضمن كائن Span) في حال لم تُعيّن الخاصية FontSize الخاصة بهذا الجزء. فمثلًا لن يطبّق هذا الحجم على جزء النص المعيّن في الأسطر بين 34 و 39. وذلك لأنّ هذا الجزء قد عيّن حجم خط خاص به (NamedSize.Large) كما رأينا قبل قليل. ملاحظة: ستحتاج لتجربة هذا البرنامج إلى تعديل بسيط ضمن بانية الصنف App ضمن الملف App.cs. احرص على أن تكون البانية على الشكل التالي لتستخدم صنفنا الجديد FormattedParagraphPage: public App() { // The root page of your application MainPage = new FormattedParagraphPage(); } الخلاصة تعرّفنا في هذا الدرس على مبادئ التعامل مع النصوص في Xamarin وبنينا للمرّة الأولى تطبيقين بسيطين يعرضان بعض المعلومات على المستخدم. تعلّمنا كيفيّة محاذاة النصوص وإجراء بعض عمليّات التنسيق عليها مثل تغيير لون النص ولون الخلفية لأيّ كلمة أو عبارة. لم يتناول هذا الدرس كيفيّة معالجة الحالة التي يكون فيها النص كبيرًا ويحتاج إلى وسيلة لتمريره لعرض محتوياته كاملة. سنعالج هذه المسألة في درس لاحق.
-
- xamarin
- أجهزة ذكية
-
(و 2 أكثر)
موسوم في:
-
سنبدأ في هذا الدرس من سلسلة برمجة تطبيقات الأجهزة المحمولة باستخدام Xamarin بالتعرّف على بنية التطبيق ضمن الحل Solution في Visual Studio 2015، بالإضافة إلى إنشائنا لتطبيق بسيط نتعرّف من خلاله على آلية عمل التطبيقات المبنية بواسطة Xamarin والتي تشبه في العديد من الجوانب مثيلاتها المنشأة باستخدام التقنيّات الأساسيّة المعتمدة في إنشاء تطبيقات أندرويد (لغة جافا مع Android Studio). بنية التطبيق المنشأ باستخدام Xamarin شغّل برنامج Visual Studio 2015، وبعد ظهور النافذة الرئيسيّة انتقل إلى القائمة File ومنها إلى New ثم اختر من القائمة الفرعيّة التي ستظهر الخيار Project. وكما فعلنا في الدرس السابق، اختر من الجهة اليسرى للنافذة التي ستظهر Cross-Platform، ثمّ اختر المشروع (Blank App (Xamarin.Forms Portable من القسم الأوسط للنافذة. امنح هذا المشروع الاسم SimpleTextApp ثم انقر الزر OK. سنبني تطبيقًا بسيطًا يوضّح كيفيّة استخدام لُصيقة Label وحيدة في إظهار النص في التطبيقات المبنيّة باستخدام Xamarin. من مستكشف الحل Solution Explorer الذي يوجد عادةً في الجهة اليمنى لنافذة بيئة التطوير Visual Studio، أبقِ فقط على المشروعين SimpleTextApp.Droid و (SimpleTextApp (Portable لأنّنا سنتعامل مع تطبيقات أندرويد فحسب (كما اتفقنا أيضًا من الدرس السابق). احذف باقي المشاريع الإضافيّة، يجب أن تحصل على شكل شبيه بما يلي: يحتوي المشروع (SimpleTextApp (Portable على معظم الشيفرة البرمجيّة، وهذا سلوك عام في المشاريع ذات النمط PCL. حيث تُعتبر الشيفرة الموجودة ضمن المشروع (الذي يحمل المقطع Portable بالإضافة لاسمه) مشتركة لجميع أنواع التطبيقات العاملة على أيّ نظام تشغيل بما فيه أندرويد. نلاحظ الملف App.cs الأساسي الذي سيكون موجودًا في أيّ تطبيق Xamarin وقد تحدثنا عنه في الدرس السابق ورأينا كيف يحتوي على الصنف App الذي يرث من الصنف Application، وكيف أنّ بانية هذا الصنف هي المسؤولة عن إنشاء الصفحة الرئيسيّة للتطبيق. انشر العقدة References لترى المراجع الخاصّة بهذا المشروع. ستلاحظ وجود عدّة مكتبات يهمّنا منها: Xamarin.Forms.Core و Xamarin.Forms.Platform و Xamarin.Forms.Xaml هذه هي المكتبات الرئيسيّة المشكّلة لـ Xamarin.Forms. بالنسبة للمشروع SimpleTextApp.Droid فهو يحتوي على مجلّدين هما: Assets و Resources. بالنسبة للمجلّد Resources فيحتوي على ملفات الصور وملفات معلومات التخطيطات layouts descriptions وغيرها من الملفات التي تُعتبر بمثابة المصادر resources لتطبيقك. انشر المجلّد Resources لترى بنيته. أمّا بالنسبة للمجلّد Assets فتُوضَع ضمنه الملفات العامّة الأخرى التي ترغب بتضمينها مع تطبيقك. يحتوي هذا المشروع أيضًا على الملف MainActivity.cs الذي يحوي ضمنه صنف له نفس الاسم MainActivity وهو يمثّل الفعاليّة Activity الأساسيّة لتطبيق أندرويد. افتح هذا الملف لتجد أنّ هذا الصنف يحتوي على التابع OnCreate الذي يُنفَّذ عند بدء تشغيل تطبيق أندرويد. لاحظ العبارة البرمجيّة التالية ضمن هذا التابع: LoadApplication(new App()); من الواضح أنّها تنشئ كائنًا جديدًا من الصنف App الموجود ضمن المشروع (SimpleTextApp (Portable، ثم تمرّره إلى التابع LoadApplication مباشرةً ليعمل على تحميل التطبيق وإظهاره للمستخدم. إذًا فعند تشغيل تطبيق أندرويد، سينفّذ التابع OnCreate الموجود في الصنف MainActivity أولًا، حيث يعمل هذا التابع على إنشاء كائن جديد من الصنف App فينفّذ بانيته وهي المسؤولة عن إنشاء الصفحة الرئيسيّة للتطبيق كما أشرنا. على أيّة حال ستتوضّح الفكرة بشكل جيّد بعد أن نبدأ بكتابة الشيفرة البرمجيّة. كتابة الشيفرة البرمجية انقر بزر الفأرة الأيمن على اسم المشروع (SimpleTextApp (Portable في مستكشف الحل Solution Explorer لتظهر قائمة سياق، اختر منها Add ثمّ من القائمة الفرعيّة اختر Class. ستظهر النافذة الخاصّة بإضافة صنف جديد. من حقل الاسم Name من الأسفل اكتب الاسم التالي لهذا الصنف الجديد وهو MainPage، ثم انقر Add. سيعمل Visual Studio على إضافة ملف جديد إلى المشروع اسمه MainPage.cs يحتوي على صنف بنفس اسمه ومحتواه شبيه بما يلي: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SimpleTextApp { class MainPage { } } لن نحتاج إلى معظم نطاقات الأسماء الموجودة في الشيفرة السابقة في هذا البرنامج، سنحتاج حاليًا إلى نطاق الاسم Xamarin.Forms. وسنجعل الصنف MainPage يرث من الصنف ContentPage كما سنضيف البانية إليه. عدّل الشيفرة السابقة لتصبح على الشكل التالي: using Xamarin.Forms; namespace SimpleTextApp { class MainPage : ContentPage { public MainPage() { } } } لاحظ أنّني تخلصت من جميع نطاقات الأسماء غير الضرورية (على أية حال سيظهر أي نطاق اسم غير مُستَخدَم في الشيفرة البرمجيّة بلون باهت في بيئة التطوير Visual Studio 2015). سيلعب الصنف MainPage دور صفحة محتوى content page رئيسيّة للتطبيق SimpleTextApp. سنضع ضمن بانية هذا الصنف الشيفرة البرمجيّة اللازمة لإنشاء لُصيقة واحدة Label تحوي نصًّا بسيطًا. ثم سنجري بعض التجارب البسيطة على هذه اللُّصيقة. يُعتبر هذا البرنامج بسيطًا في الواقع، ومهمته جعلك أكثر تآلفًا مع Xamarin. سنبدأ بإنشاء لُصيقة من الصنف Label وذلك ضمن بانية الصنف MainPage. تُستخدم اللُصيقات عادةً لعرض النصوص إلى المستخدم. انظر إلى الشيفرة البسيطة التالية: Label lblMain = new Label(); lblMain.Text = "Welcome to Hsoub Academy!"; ننشئ لُصيقة كما ننشئ أيّ كائن من صنف ما. أنشأنا في الشيفرة السابقة كائنًا جديدًا من الصنف Label وأسندناه إلى المتغيّر lblMain. ثم أسندنا النص "!Welcome to Hsoub Academy" إلى الخاصيّة Text من المتغيّر lblMain. تُستَخدم الخاصية Text لضبط وقراءة المحتوى النصيّ للُّصيقة. كي نستطيع عرض اللُّصيقة السابقة على الصفحة الرئيسيّة للتطبيق، علينا إسناد المتغيّر lblMain إلى الخاصيّة Content من الصنف ContentPage. وبما أنّ الصنف MainPage يرث من ContentPage فستكون الخاصيّة Content موجودة تلقائيًّا ضمنه أيضًا. ستكون عمليّة الإسناد على الشكل التالي: this.Content = lblMain; تكون عمليّة الإسناد السابقة ممكنة لأنّ الصنف Label يرث من الصنف View الذي يمثّل واجهة عرض view بمفهومها العام. وبما أنّ الخاصيّة Content هي من النوع View أيضًا فتكون عمليّة الإسناد هذه ممكنة. ستكون محتويات الملف MainPage.cs شبيهة بما يلي: using Xamarin.Forms; namespace SimpleTextApp { class MainPage:ContentPage { public MainPage() { Label lblMain = new Label(); lblMain.Text = "Welcome to Hsoub Academy!"; this.Content = lblMain; } } } هذا كلّ شيء! علينا الآن القيام بخطوة صغيرة أخيرة، وهي في جعل تطبيقنا يُنشئ كائن من صنفنا الجديد MainPage وذلك ضمن الملف App.cs. انتقل إلى هذا الملف (موجود ضمن المشروع (SimpleTextApp (Portable واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new MainPage(); } اضغط الآن المفتاح F5 لتنفيذ التطبيق، لتحصل على شكل شبيه بما يلي: ملاحظة يمكنك وصل جهازك المحمول الذي يعمل بنظام أندرويد إلى الحاسوب لتجريب تشغيل التطبيق عليه مباشرةً. ولكن في هذه الحالة ينبغي أن تتوفّر واجهة برمجيّة API منصّبة على حاسوبك مناسبة لنسخة أندرويد الموجودة على الجهاز المحمول. على أية حال يمكنك تنصيب هذه الواجهة في حال عدم وجودها من خلال Android SDK Manager كما أشرنا في الدرس السابق. كما أنصح بوصل الجوال قبل تشغيل بيئة التطوير Visual Studio، أو أن تعيد تشغيلها إذا كانت تعمل من قبل. تنسيق الشيفرة البرمجية بشكل أفضل في الحقيقة لا تُعتبر الشيفرة البرمجيّة الموجودة ضمن الملف MainPage.cs مثاليّةً رغم بساطتها. ربما لا تشعر بمشكلة الآن، ولكن عندما تصبح برامجك أكبر، ستقع في مشاكل حقيقيّة لأنّ الأسلوب السابق بكتابة الشيفرة يجعلها كبيرة جدًّا. سأستخدم ميّزة الإسناد المباشر للخصائص أثناء إنشاء الكائنات. تفيد هذه الميزة في الاستغناء عن معظم المتغيّرات في البرنامج، التي ينحصر دورها في لعب دور الوسيط المؤقّت. سنعيد صياغة الشيفرة الموجودة في الملف MainPage.cs بشكل أنيق وعمليّ أكثر: using Xamarin.Forms; namespace SimpleTextApp { class MainPage : ContentPage { public MainPage() { this.Content = new Label{ Text = "Welcome to Hsoub Academy!" }; } } } لاحظ أنّني استغنيت عن المتغيّر lblMain لأنّنا أسندنا كائن اللُّصيقة مباشرةً إلى الخاصيّة Content. مزايا إضافية على التطبيق ستلاحظ من التطبيق السابق أنّ اللُّصيقة تظهر في الزاوية اليسرى العليا من النافذة. سنجعلها تظهر في مركز الشاشة. ينبغي ضبط خاصيّتين من خصائص اللُّصيقة لهذا الغرض، وهما HorizontalOptions (لخيارات المحاذاة الأفقيّة) و VerticalOptions (لخيارات المحاذاة الرأسيّة). كل من هاتين الخاصيّتين من النوع LayoutOptions، وهي بنية struct موجودة ضمن نطاق الاسم Xamarin.Forms. تحتوي هذه البنية على عدّة حقول جاهزة تُفيد في خيارات التموضع الأفقيّة والرأسيّة للُّصيقة، وهي خيارات مهمّة للغاية كما سنرى لاحقًا في هذه السلسلة. لنجري الآن التعديل المطلوب على اللُّصيقة الموجودة ضمن بانية الصنف MainPage. ستصبح محتويات الملف MainPage.cs على الشكل التالي: using Xamarin.Forms; namespace SimpleTextApp { class MainPage : ContentPage { public MainPage() { this.Content = new Label{ Text = "Welcome to Hsoub Academy!", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; } } } لاحظ إسناد القيمة LayoutOptions.Center لكل من الخاصّتين HorizontalOptions و VerticalOptions وذلك للتوسيط الأفقي والرأسي على الترتيب. نفّذ البرنامج مرّة أخرى ستلاحظ أنّ اللُّصيقة أصبحت في مركز الشاشة كما هو واضح من الشكل التالي: أمّا إذا أحببت أن يكون النص ضمن اللُّصيقة بلون آخر، وليكن الأحمر مثلًا، فعليك في هذه الحالة إسناد القيمة Color.Red للخاصيّة TextColor لكائن اللُّصيقة، علمًا أنّ Color هو بنية موجودة ضمن نطاق الاسم Xamarin.Forms أيضًا. انظر كيف نفعل ذلك في الشيفرة: this.Content = new Label{ Text = "Welcome to Hsoub Academy!", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, TextColor = Color.Red }; نفّذ البرنامج مرّة أخرى، لترى النص وقد أصبح لونه أحمر. الخلاصة تعرّفنا في هذا الدرس على البنية العامة لتطبيق Xamarin ضمن بيئة التطوير Visual Studio 2015. كما قمنا ببناء تطبيق بسيط للغاية أسميناه SimpleTextApp. تنحصر وظيفة هذا التطبيق في التّعرف أكثر على Xamarin. سنبدأ اعتبارًا من الدرس القادم ببناء تطبيقات متدرّجة في الصعوبة، وذات مزايا إضافيّة من الممكن أن تستفيد منها كمبرمج تطبيقات محمولة في حياتك العمليّة.
-
تعتبر تقنيّة 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. حيث صمّمنا عدّة برامج توضّح هاتين التقنيّتين المهمّتين في البرمجة باستخدام سي شارب. ستصادف كلًّا منهما كثيرًا في حياتك البرمجيّة، وستكون سعيدًا باستخدامها نظرًا للاختصار الكبير الذي ستحصل عليه في الشيفرة البرمجيّة، هذا فضلًا عن الأداء عالي المستوى الذي لن تستطيع مجاراته باستخدامك للشيفرة التقليديّة.
-
سنتابع العمل في سلسلة برمجة تطبيقات الأجهزة المحمولة باستخدام Xamarin، حيث سنعمل في هذا الدرس على تنزيل وتنصيب برنامج Visual Studio 2015 Community الذي يأتي بشكل مجّاني من مايكروسوفت. حيث أنّ منصّة Xamarin أصبحت تأتي مع Visual Studio كميّزة من المزايا الاختياريّة التي من الممكن اختيارها أثناء تنصيب Visual Studio، وذلك بعد استحواذ مايكروسوفت على شركة Xamarin. تنزيل وتنصيب Visual Studio 2015 Community أنصح أن يكون التنصيب على ويندوز 10 أو ويندوز 8.1. انتقل إلى صفحة تنزيل بيئة التطوير Visual Studio 2015. كما في الشكل التالي: سنختار الإصدار Community من اليسار، لذلك فانقر الزر Download Community Free. سيعمل المتصفّح على تحميل ملف تنفيذي صغير اسمه vs_community.exe وهو برنامج الإعداد الذي سيعمل على تنصيب Visual Studio Community. سيشّغل المتصفّح هذا البرنامج بعد تنزيله فورًا، لذلك سيعطيك ويندوز تحذير أمان أنّه برنامج تنفيذي، ويعرض عليك تشغيله أو إلغاء العمليّة. اقبل تشغيله من خلال نقر الزر Run كما في الشكل التالي: سيعمل برنامج الإعداد ويبدأ بجلب بيانات التنصيب من الإنترنت. قد يستغرق ذلك القليل من الوقت قبل أن تحصل على النافذة في الشكل التالي، التي تخيّرنا بين خيارين للتنصيب: افتراضي Default ومخصّص Custom: انقر الخيار المخصّص Custom ثمّ انقر Next. لينتقل برنامج الإعداد إلى نافذة المزايا المطلوب تنصيبها. انتقل إلى العقدة Cross Platform Mobile Development وانشرها لتصل إلى الميّزة (C#/.NET (Xamarin v4.0.4 وهو الإصدار الحالي لمنصّة Xamarin حين كتابة هذا الدرس. قد يختلف هذا الإصدار بالنسبة إليك. انقر صندوق الاختيار بجوار هذه الميّزة، سيؤدّي ذلك إلى اختيار مزايا أخرى بشكل تلقائي. في الحقيقة سيكون حجم حزمة البيانات التي ستُنزّل من الإنترنت كبيرة نسبيًّا. بالنسبة لهذه السلسلة لن نحتاج إلى جميع هذه المزايا، لذلك انتقل إلى العقدة Programming Languages وانشرها، وأزل الإشارة من صندوق الاختيار الموجود بجانب ++Visual C. انقر الزر Next لنصل إلى المرحلة النهائيّة قبل البدء بعمليّة التنصيب، وهي المرحلة التي تلخّص ما سيقوم به برنامج الإعداد. انقر الزر Install لتبدأ عمليّة التنصيب التي ستأخذ بعض الوقت بحسب سرعة الإنترنت لديك. ستتضمّن عملية التنصيب تحميل حزمة التطوير البرمجيّة SDK الخاصة بأندرويد، بالإضافة إلى تثبيت واجهتين برمجيّتين أو أكثر افتراضيًّا مثل API 19 و API 21. على العموم يمكن تثبيت الواجهات البرمجيّة التي ترغبها بعد انتهاء التنصيب وذلك من خلال مدير الحزم والواجهات في أندرويد Android SDK Manager الذي يأتي مع حزمة التطوير البرمجيّة SDK. اقرأ هذا المقال هنا على أكاديميّة حسّوب للمزيد من المعلومات حول الواجهات البرمجيّة API ودعمها للأجهزة المشغّلة لأندرويد. التشغيل الأول لبيئة التطوير Visual Studio عند تشغيل بيئة التطوير Visual Studio 2015 للمرّة الأولى، سيطلب منك Visual Studio تسجيل الدخول باستخدام حساب بريد إلكتروني من مايكروسوفت (كحساب بريد إلكتروني على Hotmail مثلًا) كما في الشكل التالي: انقر الزر Sign in لتسجيل الدخول، لتحصل على النافذة الخاصّة بإدخال البريد الإلكتروني. أدخل البريد الإلكتروني ثم انقر Continue وإذا طلب منك Visual Studio أن تحدّد نوع البريد الإلكتروني، فاختر شخصيّ Personal Email. بعد ذلك ستصل إلى نافذة تطلب منك كلمة المرور لحساب البريد الإلكتروني الذي أدخلته قبل قليل. أدخل كلمة المرور، ثم انقر Sign in، لتصل إلى النافذة الرئيسيّة لتطبيق Visual Studio كما في الشكل التالي: إنشاء مشروع جديد من النافذة الرئيسيّة لبيئة التطوير Visual Studio، انقر القائمة File من الأعلى، واختر منها New ثم Project لإنشاء مشروع جديد، ستحصل على النافذة التالية: اختر من الشجرة التي تظهر على الجانب الأيسر الخيار: Cross-Platform (قد تحتاج لنشر بعض العقد لتصل إليه). ثم اختر نوع المشروع Blank App Xamarin.Forms) Portable) من القسم الأوسط للنافذة وذلك من أجل اختيار مكتبة الأصناف المحمولة PCL مع هذا التطبيق. ثمّ أدخل الاسم HelloWorld في حقل الاسم Name من الأسفل، ثم انقر OK. سيستغرق الأمر وقتًا قليلًا ليعمل Visual Studio على إنشاء عدّة مشاريع ضمن الحل Solution الحالي. بالنسبة إليّ (أستخدم نظام التشغيل Windows 10) فقد حصلت على خمسة مشاريع ضمن هذا الحل وهي: (HelloWorld (Portable HelloWorld.Droid HelloWorld.iOS (HelloWorld.Windows (Windows 8.1 (HelloWorld.WinPhone (Windows Phone 8.1 تظهر هذه المشاريع ضمن مستكشف الحل Solution Explorer في الجانب الأيمن من النافذة (إذا لم يكن ظاهرًا فيمكنك إظهاره من القائمة View ثم اختيار Solution Explorer). انظر الشكل التالي: في الحقيقة لن نهتم في هذه السلسلة سوى بالتطبيقات التي تعمل على أندرويد، لذلك سنحتفظ بالمشروعين (HellowWorld (Portable و HellowWorld.Droid ونحذف باقي المشاريع. انقر بزر الفأرة الأيمن على المشروع HelloWorld.iOS ثم اختر Remove لإزالته. كرّر نفس العمليّة بالنسبة للمشروعين (HelloWorld.Windows (Windows 8.1 و (HelloWorld.WinPhone (Windows Phone 8.1. ملاحظة: قد تختلف المشاريع التي تظهر عندك بشكل طفيف. على أيّة حال احرص على وجود مشروعين فقط وهما HelloWorld.Droid و (HelloWorld (Portable. تشغيل تطبيق أندرويد الأول لتشغيل تطبيقات أندرويد فإنّنا نحتاج إلى جهاز ذكي يشغّل أندرويد (بصرف النظر عن الإصدار) أو أن يتوفّر لدينا محاكي Emulator يعمل على محاكاة عمل هذا الجهاز ولكن على حاسوبنا الشخصي. توفّر مايكروسوفت محاكٍ Emulator خاص بها: Visual Studio Emulator For Android، وذلك لمحاكاة عمل تطبيقات أندرويد على جهاز الحاسوب بدون الحاجة إلى وجود جهاز فيزيائي متصل بالحاسوب. يُعتبر هذا المحاكي برأيي أفضل من المحاكي الافتراضي الذي يأتي مع حزمة التطوير الخاصّة بأندرويد من حيث الأداء. وعلى أية حال، فستحتاج إلى تفعيل ميزة HAXM التي تأتي مع معالجات Intel، والتي يحتاجها المحاكي الافتراضي لتسريع أدائه. انظر إلى هذا الرابط لتعرف المزيد عن هذا الموضوع. لنشغّل تطبيقنا الأوّل ضمن المحاكي الخاص بمايكروسوفت، سيظهر لك أعلى نافذة بيئة التطوير شريط صغير يحتوي على اسم المحاكي الذي سيتم تشغيله. انظر إلى الشكل التالي: لاحظ بأنّ المحاكي الذي لديّ يدعم الواجهة البرمجيّة API 19 ولا بأس في ذلك. يمكنك دعم أي واجهة برمجيّة ترغبها، باستخدام Android SDK Manager. كلّ ما فعلته أنّني تركت الإعدادات الافتراضيّة كما هي. انقر السهم الأخضر الصغير للتشغيل في وضع التنقيح debugging mode (أو يمكنك اختيار الأمر Start Debugging من القائمة Debug أو اضغط F5). سيستغرق الأمر قليلًا من الوقت حتى تحصل على شكل شبيه بما يلي: لاحظ الرسالة الترحيبيّة !Welcome to Xamarin Forms في وسط الشاشة. مبروك لقد حصلت على برنامجك الأوّل! أوقف الآن تشغيل البرنامج باختيار الأمر Stop Debugging من القائمة Debug (أو اضغط Shift+F5). انتقل إلى مستكشف الحل Solution Explorer وانشر المشروع (HelloWorld (Portable ثمّ انقر على الملف App.cs وهو الملف الأساسيّ في أيّ تطبيق من تطبيقات Xamarin. ستجد ضمنه الشيفرة التالية: 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 using Xamarin.Forms; 7 8 namespace HelloWorld 9 { 10 public class App : Application 11 { 12 public App() 13 { 14 // The root page of your application 15 MainPage = new ContentPage 16 { 17 Content = new StackLayout 18 { 19 VerticalOptions = LayoutOptions.Center, 20 Children = { 21 new Label { 22 XAlign = TextAlignment.Center, 23 Text = "Welcome to Xamarin Forms!" 24 } 25 } 26 } 27 }; 28 } 29 30 protected override void OnStart() 31 { 32 // Handle when your app starts 33 } 34 35 protected override void OnSleep() 36 { 37 // Handle when your app sleeps 38 } 39 40 protected override void OnResume() 41 { 42 // Handle when your app resumes 43 } 44 } 45 } لاحظ الصنف App في السطر 10. يرث هذا الصنف من الصنف Application الذي يمثّل التطبيق في Xamarin.Forms. أيّ تطبيق ننشئه في Xamarin.Forms يجب أن يرث من هذا الصنف. تقع بانية الصنف App في الأسطر من 12 حتى 28 وفيها يتمّ تعيين الصفحة الرئيسيّة التي سيظهرها التطبيق عند تشغيله. أيّ صفحة من صفحات التطبيق ستكون عبارة عن كائن من صنف يرث من الصنف ContentPage (صفحة محتوى). يمكن أن تحتوي هذه الصفحة على أيّ شيء يخطر ببالك من مربّعات النص والأزرار وأشرطة التمرير وغيرها من الأدوات. لاحظ أنّنا في السطر 15 (ضمن بانية الصنف App) قد أسندنا كائنًا جديدًا من الصنف ContentPage إلى الخاصيّة MainPage وهي خاصيّة تتبع الصنف Application (الذي يرث منه الصنف App). تستخدم الشيفرة الموجود هنا أسلوب الإنشاء المختصر للكائنات، فكلّ شيء يتمّ هنا من خلال عبارة برمجيّة واحدة. حيث نسند الخصائص للكائنات الجديدة المُنشأة مباشرةً عند إنشاء هذه الكائنات. فمن خلال الشيفرة الموجودة في الأسطر بين 15 و 27 كرّرنا هذا الأسلوب ثلاث مرّات، وذلك من أجل كائنات جديدة من الأصناف ContentPage و StackLayout و Children. ملاحظة يُعتبر الإسناد المختصر للخصائص عند إنشاء كائنات جديدة، من التقنيّات المهمّة التي تبسّط الشيفرة البرمجيّة إلى حدّ كبير. وكيف تنعش ذاكرتك، إليك المثال البسيط التالي. بفرض أنّه لدينا الصنف Student الذي يحتوي على الخصائص التالية: FirstName و LastName و Age. عند إنشاء كائن جديد من الصنف Student يمكننا استخدام العبارة البرمجيّة التالية: Student student = new Student { FirstName = "Ahmad", LastName = "Shareef", Age = 16 }; ففي هذه الحالة ننشئ كائن جديد من الصنف Student وبنفس الوقت نُسند القيم المناسبة لخصائصه. سنؤجّل الحديث عن التوابع OnStart و OnSleep و OnResume لنتحدّث عنها لاحقًا في هذه السلسلة، رغم أنّ أسمائها توحي بوظائفها التي تُعتبر مفيدة ومهمّة في عمل التطبيق. الخلاصة تعلّمنا في هذا الدرس كيفيّة تنزيل وتنصيب Visual Studio 2015 Community مع إضافة قابليّة تطوير تطبيقات لأندرويد باستخدام Xamarin. لاحظ أنّني قد تركت الأمور تسير بشكلها الافتراضيّ قدر المستطاع، لأنّه كما هو واضح هناك العديد من الإجراءات كي تصبح بيئة التطوير جاهزة للعمل، وللبدء بتطوير تطبيقات تعمل على أندرويد وغيره من أنظمة التشغيل باستخدام سي شارب #C و Xamarin.
- 3 تعليقات
-
- 2
-
- xamarin
- visual studio
- (و 5 أكثر)
-
لا تخلو أيّ لغة برمجة محترمة من وسيلة لمعالجة الأخطاء. تحتوي لغة سي شارب على آليّة قويّة لمعالجة الأخطاء. تشبه هذه الآلية إلى حدّ ما تلك المستخدمة في لغة 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.
-
المتغيرات 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 حرفًا ويجوز الابتداء به. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن الأنواع المضمنة ومجالاتها الأنواع المُضمّنة هي الأنواع الموجودة ضمنًا في لغة سي شارب وفي إطار عمل دوت نت عمومًا. سنستعرض في الجدول التالي هذه الأنواع. لاحظ أنّ عمود "الاسم" يحتوي على أسماء الأنواع المستخدمة في سي شارب، في حين يحتوي العمود الذي يليه مباشرةً على اسم نفس النوع ولكن ضمن منصّة دوت نت. في الحقيقة يمكننا استخدام أي تسمية نرغبها ولكنّ الأسهل والأفضل هي في استخدام أسماء الأنواع في سي شارب. يعود سبب وجود أسماء أنواع مختلفة في إطار عمل دوت نت هو أنّه بإمكان أي لغة برمجة ضمن منصة دوت نت استخدام نفس هذه الأنواع ضمنها. الاسم النوع الموافق في منصّة دوت نت القيم التي يقبلها الحجم في الذاكرة 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. كما تحدّثنا عن معظم العوامل التي تدعمها سي شارب وتصنيفاتها. وتناولنا بعض الأمثلة التوضيحيّة على استخدامها. سنتحدّث في الدرس التالي عن بنى القرار وتغيير مسار البرنامج وهو موضوع مهم في جميع لغات البرمجة.
-
عندما تعمل على حاسوبك الشخصي، أو حتى على هاتفك الذكيّ، أو ربّما ساعتك الذكيّة. فأنت تستخدم الأحداث آلاف المرّات. الحدث هو وسيلة جميلة للتعبير عن أمر طارئ حدث لكائن برمجيّ. قد يكون هذا الأمر الطارئ عبارة عن نقرة زر فأرة، أو عن لمسة على شاشة جهازك الذكي. أو ضغطة مفتاح من لوحة المفاتيح الخاصّة بي وأنا أكتب هذا المقال، أو أن يكون أمرًا طارئًا يُعبّر عن حالة داخليّة ضمن نظام التشغيل. باختصار، هناك عدد كبير جدًّا من المصادر المختلفة أو المحتملة للأحداث. العلاقة بين الأحداث 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 بشكل أساسيّ، فضلًا عن باقي أنواع التطبيقات مثل تطبيقات الويب، وتطبيقات الأجهزة المحمولة، وأي نوع من أنواع التطبيقات التي تتطلّب التفاعل الداخلي مع نظام التشغيل أو الخارجيّ مع المستخدم.
-
يُعتبر مفهوم النوّاب من المفاهيم الأساسيّة في سي شارب، وستستخدمه بشكل أو بآخر في أيّ برنامج حقيقي مكتوب بهذه اللغة. سنتحدّث في هذا الدرس عن هذا الموضوع المهمّ، وسنتعرّض في درس لاحق إلى مفهوم مهم آخر مرتبط به، ألا وهو مفهوم الأحداث 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، ذلك المفهوم المهم في البرمجة كائنيّة التوجّه، والذي سنتحدّث عنه في الدرس التالي.
-
يمكن القراءة والكتابة من وإلى الملفات النصيّة في سي شارب بعدّة طرق. سنتناول أسلوبًا بسيطًا وذلك من خلال الصنفين 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 لكي نحرّر المصدر الذي يحجزه الملف فور الانتهاء من التعامل معه.
-
- streamwriter
- streamreader
-
(و 4 أكثر)
موسوم في:
-
يتحدّث هذا الدرس عن موضوعين مهمّين في سي شارب ألا وهما الواجهات 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. من النادر أن يخلو أيّ برنامج فعليّ من استخدام المجموعات أو الواجهات، وفي الحقيقة هناك العديد من بنى المجموعات المفيدة التي لم نتناولها في هذا الدرس. سنحاول أن نتوسّع في المزايا القويّة والرّائعة للمجموعات في سلسلة قادمة.