تعلم سي شارب استخدام النواب (Delegates) في لغة سي شارب #C


حسام برهان

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

learn-csharp-delegates.png

في جميع البرامج التي صادفناها في هذه السلسلة كنّا نعمل على استدعاء توابع وخصائص من كائنات ننشئها من الأصناف المختلفة. في البرمجة العمليّة، يحتاج الكائن في الكثير من الأحيان إلى وسيلة يفعل فيها العكس. أي يحتاج الكائن إلى أسلوب يستطيع من خلاله استدعاء تابع ما لا ينتمي إليه وذلك من تابع أو خاصيّة موجودة داخل الكائن. تُسمّى هذه الظاهرة بالردود 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، ذلك المفهوم المهم في البرمجة كائنيّة التوجّه، والذي سنتحدّث عنه في الدرس التالي.



1 شخص أعجب بهذا


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


الشرح جميل، ومرتب، ولكني أفضل التعريب المناسب، وهو التفويض، أو المفوض، بدلا من التنويب والنائب

1 شخص أعجب بهذا

شارك هذا التعليق


رابط هذا التعليق
شارك على الشبكات الإجتماعية
بتاريخ On 8/4/2016 at 22:56 قال Khalil Saleem:

الشرح جميل، ومرتب، ولكني أفضل التعريب المناسب، وهو التفويض، أو المفوض، بدلا من التنويب والنائب

شكرًا لك أخ خليل. بالفعل تبدو كلمة المفوّض معبّرة أيضًا. علمًا أنّني لم أستخدم كلمة التنويب.

شارك هذا التعليق


رابط هذا التعليق
شارك على الشبكات الإجتماعية


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

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

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


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

تسجيل الدخول

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


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