تعلم سي شارب الواجهات (Interfaces) والمجموعات (Collections) في لغة سي شارب #C


حسام برهان

يتحدّث هذا الدرس عن موضوعين مهمّين في سي شارب ألا وهما الواجهات Interfaces والمجموعات Collections. تُعتبر المجموعات collections من المزايا القويّة والمهمّة في سي شارب، وهي وسيلة لتخزين العناصر ضمن بنية قابلة للتوسّع تلقائيًّا وسهلة التعامل. إذًا فهي تشبه المصفوفات، باستثناء أنّ المصفوفات ذات حجم ثابت وهي تبقى كذلك منذ لحظة إنشائها، أمّا المجموعات فهي على النقيض من ذلك، فهي بنية قابلة للتوسّع بقدر ما نرغب. بالإضافة إلى أنّ للمجموعات أشكال عديدة مفيدة جدًّا، فهناك المكدّس stack والرتل queue والقاموس dictionary وغيرها من البنى المهمّة.

learn-csharp-interfaces-collections.png

سنتناول في هذا الدرس المجموعات العاديّة، والمجموعات العموميّة 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 المماثلة لها في المصفوفات).

بعد تنفيذ البرنامج وإدخال بيانات ستة طلّاب، ستحصل على شكل شبيه بما يلي:

fig01.png

يوجد تابع اسمه 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. من النادر أن يخلو أيّ برنامج فعليّ من استخدام المجموعات أو الواجهات، وفي الحقيقة هناك العديد من بنى المجموعات المفيدة التي لم نتناولها في هذا الدرس. سنحاول أن نتوسّع في المزايا القويّة والرّائعة للمجموعات في سلسلة قادمة.





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن