Skip to content

4. الفئات، الهياكل، الواجهات

4.1. الكائن من خلال المثال

4.1.1. عام

ننتقل الآن إلى البرمجة الكائنية من خلال مثال. الكائن هو كيان يحتوي على بيانات تحدد حالته (تسمى الحقول، والسمات، ...) ووظائفه (تسمى الطرق). يتم إنشاء الكائن وفقًا لنموذج يسمى الفئة:

public class C1{
     Type1 p1        ; // field p1
     Type2 p2        ; // p2 field
    
     Type3 m3(        ) { // m3 method
        
    }
     Type4 m4(        ) { // m4 method
        
    }
    
}

من الفئة C1 أعلاه، يمكنك إنشاء العديد من الكائنات O1، O2،... وستحتوي جميعها على الحقول p1، p2،... والطرق m3، m4،... ولكنها ستحتوي على قيم مختلفة لحقولها pi، لكل منها حالتها الخاصة. إذا كان o1 كائنًا من النوع C1، فإن o1.p1 يشير إلى الخاصية p1 من o1، وo1.m1 يشير إلى الطريقة m1 من O1.

لننظر إلى نموذج الكائن الأول: Person.

4.1.2. إنشاء مشروع C#

في الأمثلة السابقة، كان لدينا ملف واحد فقط: Program.cs. من الآن فصاعدًا، سنتمكن من الحصول على عدة ملفات مصدر في مشروع واحد. سنوضح لك كيفية القيام بذلك.

في [1]، قم بإنشاء مشروع جديد. في [2]، اختر "Application Console". في [3]، اترك القيمة الافتراضية. في [4]، قم بالتأكيد. في [5]، المشروع الذي تم إنشاؤه. محتويات ملف Program.cs هي كما يلي:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

لنحفظ المشروع الذي تم إنشاؤه:

في [1]، حدد الخيار المراد حفظه. في [2]، حدد المجلد الذي تريد حفظ المشروع فيه. في [3]، قم بتسمية المشروع. في [5]، حدد أنك تريد إنشاء حل. الحل هو مجموعة من المشاريع. في [4]، قم بتسمية الحل. في [6]، قم بتأكيد الحفظ.

في [1]، المشروع المحفوظ. في [2]، أضف عنصرًا جديدًا إلى المشروع.

في [1]، حدد أنك تريد إضافة فئة. في [2]، أدخل اسم الفئة. في [3]، تحقق من صحة المعلومات. في [4]، أصبح لدى المشروع [01] ملف مصدر جديد باسم Personne.cs:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1 {
    class Personne {
    }
}

قم بتغيير مساحة الاسم لكل ملف مصدر إلى Chap2 وتخلص من الحاجة إلى استيراد مساحات الأسماء غير الضرورية:


using System;
 
namespace Chap2 {
    class Personne {
    }
}

using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

4.1.3. تعريف فئة Person

سيكون تعريف فئة Person في ملف المصدر [Personne.cs] كما يلي:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // method
        public void Initialise(string P, string N, int age) {
            this.prenom = P;
            this.nom = N;
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

لدينا هنا تعريف لفئة، أي نوع من أنواع البيانات. عندما ننشئ متغيرات من هذا النوع، نسميها كائنات أو عناصر الفئة. وبالتالي، فإن الفئة هي قالب تُبنى منه الكائنات.

يمكن أن تكون عناصر أو حقول الفئة بيانات (سمات) أو طرق (دوال) أو خصائص. الخصائص هي طرق خاصة تُستخدم لمعرفة أو تعيين قيمة سمات الكائن. يمكن أن تكون هذه الحقول مصحوبة بإحدى الكلمات الرئيسية الثلاث التالية:

privé
لا يمكن الوصول إلى الحقل الخاص إلا من خلال الطرق الداخلية للفئة
public
يمكن الوصول إلى الحقل العام بواسطة أي دالة، سواء كانت محددة أم لا داخل
المحمي
لا يمكن الوصول إلى الحقل المحمي (protected) إلا من خلال الطرق الداخلية للفئة أو كائن مشتق (انظر مفهوم الوراثة لاحقًا).

بشكل عام، تُعلن بيانات الفئة على أنها خاصة بينما تُعلن طرقها وخصائصها على أنها عامة. وهذا يعني أن مستخدم الكائن (المبرمج)

  • لن يكون لديه وصول مباشر إلى البيانات الخاصة للكائن
  • سيكون قادرًا على استدعاء الطرق العامة للكائن، وخاصة تلك التي توفر الوصول إلى بياناته الخاصة.

فيما يلي صيغة إعلان فئة C:


public class C{
    private  donnée ou méthode ou propriété privée;
    public  donnée ou méthode ou propriété publique;
    protected  donnée ou méthode ou propriété protégée;
}

ترتيب إعلان السمات private و protected و public تعسفي.

4.1.4. طريقة Initialize

نعود إلى فئة Person المُعلنة على النحو التالي:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

ما هو دور Initializes؟ لأن الاسم واللقب والعمر هي بيانات خاصة بفئة Person، التعليمات:

Personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

غير صالحة. نحتاج إلى تهيئة كائن من نوع Person عبر طريقة عامة. هذا هو دور Initializes. نكتب:

Personne p1;
p1.Initialise("Jean","Dupont",30);

كتابة p1.Initialise صحيحة لأن Initializes متاحة للجمهور.

4.1.5. المشغل الجديد

تسلسل التعليمات

Personne p1;
p1.Initialise("Jean","Dupont",30);

غير صحيح. التعليمات

    Personne p1;

تشير إلى p1 كمرجع إلى كائن من نوع Person. هذا الكائن غير موجود بعد، لذا لم يتم تهيئة p1. الأمر يشبه كتابة:

Personne p1=null;

حيث تشير الكلمة الرئيسية null إلى أن المتغير p1 لا يشير بعد إلى أي كائن. وعندما تكتب بعد ذلك

p1.Initialise("Jean","Dupont",30);

نستخدم Initializes للكائن الذي يشير إليه p1. أو أن هذا الكائن غير موجود بعد وسيقوم المُجمع بالإشارة إلى الخطأ. للتأكد من أن p1 يشير إلى كائن، اكتب:

Personne p1=new Personne();

يؤدي هذا إلى إنشاء كائن من نوع Person لم يتم تهيئته بعد: ستكون قيمة السمتين name و first name — اللتين تشيران إلى كائنين من نوع Stringهي null، وستكون قيمة السمة age هي 0. أي أن هناك تهيئة افتراضية. والآن بعد أن أصبح p1 يشير إلى كائن، فإن تعليمة التهيئة لهذا الكائن

p1.Initialise("Jean","Dupont",30);

صحيحة.

4.1.6. الكلمة الرئيسية this

دعونا نلقي نظرة على كود initialize :


        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
}

تعني التعليمات this.prenom=p أن الاسم الأول للكائن الحالي (this) يأخذ القيمة p. تشير الكلمة الرئيسية this إلى الكائن الحالي: الكائن الذي توجد فيه الطريقة المنفذة. كيف نعرف ذلك؟ لنلقِ نظرة على تهيئة الكائن المشار إليه بواسطة p1 في البرنامج المستدعي:

p1.Initialise("Jean","Dupont",30);

هذه هي طريقة Initializes للكائن p1. عندما تشير هذه الطريقة إلى this، فإنها تشير في الواقع إلى الكائن p1. كان من الممكن أيضًا كتابة الطريقة Initializes على النحو التالي:


        public void Initialise(string p, string n, int age) {
            prenom = p;
            nom = n;
            this.age = age;
}

عندما تشير طريقة كائن إلى سمة A لهذا الكائن، فإن كتابة this.A تكون ضمنية. ويجب استخدامها صراحةً عند تعارض المعرفات. وهذا هو الحال مع:


this.age=age;

حيث يشير «age» إلى سمة الكائن الحالي والمعلمة «age» التي تستقبلها الطريقة. ويجب عندئذٍ حل هذا الغموض عن طريق الإشارة إلى السمة «age» باستخدام «this.age».

4.1.7. برنامج اختبار

إليك برنامج اختبار قصير. وهو مكتوب في ملف المصدر [Program.cs]:


using System;
 
namespace Chap2 {
    class P01 {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
        }
    }
}

قبل تنفيذ المشروع [01]، قد تحتاج إلى تحديد ملف المصدر المراد تنفيذه:

في خصائص المشروع [01]، يتم تحديد الفئة المراد تنفيذها في [1].

النتائج التي تم الحصول عليها عند الانتهاء هي كما يلي:

[Jean, Dupont, 30]

4.1.8. طريقة أخرى Initialise

لننظر إلى Person ونضيف الطريقة التالية:


        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
}

لدينا الآن طريقتان باسم Initializes : وهذا أمر مقبول طالما أنهما تقبلان معلمات مختلفة. وهذا هو الحال هنا. المعلمة الآن هي مرجع p إلى شخص. ثم يتم تعيين سمات الشخص p إلى الكائن الحالي (this). لاحظ أن Initializes لها وصول مباشر إلى سمات الكائن p على الرغم من أنها خاصة. وهذا صحيح دائمًا: الكائن o1 من الفئة C لديه دائمًا وصول إلى سمات الكائنات من نفس الفئة C.

فيما يلي اختبار للفئة الجديدة P person:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne();
            p2.Initialise(p1);
            p2.Identifie();
        }
    }
}

ونتيجتها:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.9. منشئات فئة Person

منشئ هو طريقة تحمل اسم الفئة ويتم استدعاؤها عند إنشاء الكائن. وتستخدم عادةً لتهيئة الكائن. ويمكنها قبول معلمات، لكنها لا تُرجع أي نتائج. ولا يسبق نموذجها الأولي أو تعريفها أي نوع (ولا حتى void).

إذا كانت فئة C تحتوي على منشئ يقبل n معلمات argi، فيمكن إجراء إعلان وتهيئة كائن من هذه الفئة بالشكل التالي:

        C objet =new C(arg1,arg2, ... argn);

أو

        C objet;
        objet=new C(arg1,arg2, ... argn);

عندما يكون لـ C منشئ واحد أو أكثر، يجب استخدام أحد هذه المنشئات لإنشاء كائن من هذه الفئة. إذا لم يكن لـ C منشئ، فإنه يحتوي على منشئ افتراضي وهو المنشئ بدون معلمات: public C(). ثم يتم تهيئة سمات الكائن بالقيم الافتراضية. هذا ما حدث في البرامج السابقة، حيث:

    Personne p1;
    p1=new Personne();

لنقم بإنشاء منشئين لفئة Person الخاصة بنا:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Initialise(P);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
...
        }
 
        public void Initialise(Personne p) {
...
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

يستخدم منشئانا ببساطة Initializes التي درسناها سابقًا. تذكر أنه عندما يستخدم المصمم الترميز Initialise(p) على سبيل المثال، يقوم المُترجم بترجمة this.Initialise(p). في المنشئ، يتم استدعاء Initializes للعمل على الكائن المشار إليه بواسطة this، أي الكائن الحالي، الذي يجري إنشاؤه.

فيما يلي برنامج اختبار قصير:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne(p1);
            p2.Identifie();
        }
    }
}
 

والنتائج التي تم الحصول عليها:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.10. مراجع الكائنات

نستخدم دائمًا نفس الشخص. يصبح برنامج الاختبار كما يلي:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main() {
             // p1
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Console.Write("p1="); p1.Identifie();
             // p2 references the same object as p1
            Personne p2 = p1;
            Console.Write("p2="); p2.Identifie();
             // p3 references an object that will be a copy of the object referenced by p1
            Personne p3 = new Personne(p1);
            Console.Write("p3="); p3.Identifie();
             // change the state of the object referenced by p1
            p1.Initialise("Micheline", "Benoît", 67);
            Console.Write("p1="); p1.Identifie();
             // as p2=p1, the object referenced by p2 must have changed state
            Console.Write("p2="); p2.Identifie();
             // as p3 does not reference the same object as p1, the object referenced by p3 must not have changed
            Console.Write("p3="); p3.Identifie();
        }
    }
}

النتائج هي كما يلي:

1
2
3
4
5
6
p1=[Jean, Dupont, 30]
p2=[Jean, Dupont, 30]
p3=[Jean, Dupont, 30]
p1=[Micheline, Benoît, 67]
p2=[Micheline, Benoît, 67]
p3=[Jean, Dupont, 30]

عند تعريف المتغير p1 بواسطة

Personne p1=new Personne("Jean","Dupont",30);

فإن p1 يشير إلى الكائن Pperson("John", "Smith",30) ولكنه ليس الكائن نفسه. في لغة C، نقول إنه مؤشر، أي عنوان الكائن الذي تم إنشاؤه. إذا كتبت بعد ذلك:

    p1=null;

فليس الكائن Person("John", "Smith",30) هو الذي يتم تعديله، بل المرجع p1 هو الذي يتغير قيمته. وسيُفقد الكائن Person("John", "Smith",30) إذا لم تتم الإشارة إليه بواسطة أي متغير آخر.

عندما نكتب:

Personne p2=p1;

نقوم بتهيئة المؤشر p2: فهو "يشير" إلى نفس الكائن (يحدد نفس الكائن) الذي يشير إليه المؤشر p1. لذا، إذا قمنا بتعديل الكائن الذي "يشير إليه" (أو المشار إليه) p1، فإننا نقوم أيضًا بتعديل الكائن المشار إليه بواسطة p2.

عندما نكتب:

Personne p3=new Personne(p1);

يتم إنشاء كائن جديد Person. سيتم الإشارة إلى هذا الكائن الجديد بواسطة p3. إذا قمت بتعديل الكائن الذي "يشير إليه" (أو المشار إليه) بواسطة p1، فإن الكائن المشار إليه بواسطة p3 سيتغير أيضًا. وهذا ما تظهره النتائج.

4.1.11. تمرير معلمات مرجعية للكائنات

في الفصل السابق، تناولنا كيفية تمرير معلمات الدالة عندما تمثل نوعًا بسيطًا في C# ممثلاً ببنية .NET. لنرى ما يحدث عندما تكون المعلمة:


using System;
using System.Text;
 
namespace Chap1 {
    class P12 {
        public static void Main() {
             // example 4
            StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
            Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
            ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
            Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
 
        }
 
        private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
            Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
            sbf0.Append("*****");
            sbf1 = new StringBuilder("essai1*****");
            sbf2 = new StringBuilder("essai2*****");
            sbf3 = new StringBuilder("essai3*****");
            Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
        }
    }
}
  • السطر 8: يحدد 3 StringBuilder. كائن StringBuilder قريب من كائن string. عند التعامل مع كائن string، تحصل على كائن جديد في string. لذا في تسلسل الكود:
string s="une chaîne";
s=s.ToUpperCase();

السطر 1 ينشئ سلسلة نصية، و s هو عنوانها. في السطر 2، تؤدي s.ToUpperCase() إلى إنشاء كائن سلسلة نصية آخر في الذاكرة. وبالتالي، بين السطرين 1 و 2، تغيرت قيمة s (أصبحت تشير إلى الكائن الجديد). تسمح لك فئة StringBuilder بتعديل سلسلة نصية دون إنشاء كائن ثانٍ. وهذا هو المثال المذكور أعلاه:

  • السطر 8: 4 مراجع [sb0، sb1، sb2، sb3] إلى كائنات من نوع StringBuilder
  • السطر 10: يتم تمريرها إلى ChangeStringBuilder بأوضاع مختلفة: sb0 و sb1 بالوضع الافتراضي، و sb2 بالكلمة الرئيسية ref، و sb3 بالكلمة الرئيسية out.
  • الأسطر 15-22: طريقة ذات معلمات رسمية [sbf0، sbf1، sbf2، sbf3]. العلاقات بين المعلمات الرسمية sbfi والقوى العاملة sbi هي كما يلي:
  • sbf0 و sb0 هما، في بداية الدالة، مرجعان منفصلان يشيران إلى نفس الكائن (تمرير قيمة العنوان)
  • وينطبق الأمر نفسه على sbf1 و sb1
  • sbf2 و sb2 هما، في بداية الطريقة، مرجع واحد لنفس الكائن (الكلمة الرئيسية ref)
  • sbf3 و sb3 هما، بعد تنفيذ الطريقة، مرجع واحد لنفس الكائن (الكلمة الرئيسية out)

والنتائج هي كما يلي:

1
2
3
4
Dans fonction appelante avant appel : sb0=essai0, sb1=essai1, sb2=essai2
Début fonction appelée : sbf0=essai0, sbf1=essai1, sbf2=essai2
Fin fonction appelée : sbf0=essai0*****, sbf1=essai1*****, sbf2=essai2*****, sbf3=essai3*****
Dans fonction appelante après appel : sb0=essai0*****, sb1=essai1, sb2=essai2*****, sb3=essai3*****

التفسيرات:

  • sb0 و sbf0 هما مرجعان منفصلان لنفس الكائن. تم تعديل هذا الكائن عبر sbf0 - السطر 3. يمكن رؤية هذا التعديل عبر sb0 - السطر 4.
  • sb1 و sbf1 هما مرجعان متميزان لنفس الكائن. طريقة sbf1 وتشير الآن إلى كائن جديد - السطر 3. هذا لا يغير قيمة sb1 التي تستمر في الإشارة إلى نفس الكائن - السطر 4.
  • sb2 و sbf2 هما نفس المرجع لنفس الكائن. تم تعديل sbf2 في الطريقة ويشير الآن إلى كائن جديد - السطر 3. وبما أن sbf2 و sb2 كيان واحد، فقد تم تعديل قيمة sb2 أيضًا ويشير sb2 إلى نفس الكائن الذي يشير إليه sbf2 - السطران 3 و 4.
  • قبل استدعاء الطريقة، كان sb3 عديم القيمة. بعد الطريقة، يتلقى sb3 قيمة sbf3. لذلك لدينا مرجعان لنفس الكائن - السطران 3 و 4

4.1.12. الكائنات المؤقتة

في أحد التعبيرات، يمكننا استدعاء منشئ كائن بشكل صريح: يتم إنشاء هذا الكائن، لكن لا يمكننا الوصول إليه (لتعديله، على سبيل المثال). يتم إنشاء هذا الكائن المؤقت لغرض تقييم التعبير، ثم يتم التخلي عنه. ويتم استرداد مساحة الذاكرة التي يشغلها تلقائيًا لاحقًا بواسطة برنامج يُسمى «جامع القمامة»، وتتمثل مهمته في استرداد مساحة الذاكرة التي تشغلها الكائنات التي لم تعد مرجعية لها في بيانات البرنامج.

لننظر إلى برنامج الاختبار الجديد التالي:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
        }
    }
}

وقم بتعديل منشئات Person لعرض رسالة:


         // manufacturers
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(P);
}

نحصل على النتائج التالية:

1
2
3
Constructeur Personne(string, string, int)
Constructeur Personne(Personne)
[Jean, Dupont, 30]

يُظهر البناء المتتالي للكائنين المؤقتين.

4.1.13. طرق قراءة وكتابة السمات الخاصة

نضيف إلى Person طرقًا لقراءة أو تعديل حالة سمات الكائن:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(p);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }
 
         // accessors
        public String GetPrenom() {
            return prenom;
        }
        public String GetNom() {
            return nom;
        }
        public int GetAge() {
            return age;
        }
 
         //modifiers
        public void SetPrenom(String P) {
            this.prenom = P;
        }
        public void SetNom(String N) {
            this.nom = N;
        }
        public void SetAge(int age) {
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

نختبر الفئة الجديدة باستخدام البرنامج التالي:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p = new Personne("Jean", "Michelin", 34);
            Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
            p.SetAge(56);
            Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
        }
    }
}

ونحصل على النتائج:

1
2
3
Constructeur Personne(string, string, int)
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)

4.1.14. الخصائص

هناك طريقة أخرى للوصول إلى سمات الفئة وهي إنشاء الخصائص. تسمح لنا هذه الخصائص بالتعامل مع السمات الخاصة كما لو كانت عامة.

لننظر إلى فئة P حيث تم استبدال أدوات الوصول والمعدلات السابقة بخصائص للقراءة والكتابة:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Initialise(p);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }
 
         // properties
        public string Prenom {
            get { return prenom; }
            set {
                 // valid first name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
             }//if
         }//first name
 
        public string Nom {
            get { return nom; }
            set {
                 // valid name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
             }//if
         }//name
 
 
        public int Age {
            get { return age; }
            set {
                 // valid age?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
             }//if
         }//age

         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

تسمح لك الخاصية بقراءة (الحصول على) أو تعديل (تعيين) قيمة سمة ما. يتم إعلان الخاصية على النحو التالي:

public Type Propriété{
    get {...}
    set {...}
}

حيث يجب أن يكون «Type» هو نوع السمة التي تديرها الخاصية. ويمكن أن تحتوي على طريقتين تسميان «get» و«set». عادةً ما تكون الطريقة «get» مسؤولة عن عرض قيمة السمة التي تديرها (ويمكنها عرض شيء آخر، فلا شيء يمنعها من ذلك). تستقبل الطريقة set معلمة تسمى value والتي عادةً ما تخصصها للسمة التي تديرها. يمكنها الاستفادة من ذلك للتحقق من صحة القيمة المستلمة، وإذا لزم الأمر، إلقاء استثناء إذا تبين أن القيمة غير صالحة. هذا ما تفعله ici.

كيف يتم استدعاء هاتين الطريقتين get و set؟ انظر إلى برنامج الاختبار التالي:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p = new Personne("Jean", "Michelin", 34);
            Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
            p.Age = 56;
            Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
            try {
                p.Age = -4;
            } catch (Exception ex) {
                Console.Error.WriteLine(ex.Message);
             }//try-catch
        }
    }
}

في


    Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");

نحن نبحث عن قيم "الاسم" و"اللقب" و"العمر" للشخص p. هذه هي طريقة الحصول على هذه الخصائص، والتي يتم استدعاؤها بعد ذلك لتُرجع قيمة السمة التي تديرها.

في

        p.Age=56;

نريد تعيين قيمة الخاصية Age. هذه هي مجموعة set التي يتم استدعاؤها بعد ذلك. وستتلقى 56 في قيمة معلمتها.

يُقال إن الخاصية P لفئة C التي تحدد فقط الدالة get هي خاصية للقراءة فقط. إذا كان c كائنًا من فئة C، فسيتم رفض العملية c.P=valeur من قبل المُترجم.

يؤدي تنفيذ برنامج الاختبار السابق إلى النتائج التالية:

1
2
3
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)
âge (-4) invalide

تسمح لنا الخصائص بمعالجة السمات الخاصة كما لو كانت عامة. ومن الميزات الأخرى للخصائص أنه يمكن استخدامها بالاقتران مع منشئ باستخدام الصيغة التالية:

Classe objet=new Classe (...) {Propriété1=val1, Propriété2=val2, ...}

هذه الصيغة تعادل الكود التالي:

1
2
3
4
Classe objet=new Classe(...);
objet.Propriété1=val1;
objet.Propriété2=val2;
...

ترتيب الخصائص لا يهم. إليك مثال على ذلك.

تضيف فئة Person منشئًا جديدًا بدون معلمات:


        public Personne() {
}

لا يقوم المنشئ بتهيئة أعضاء الكائن. يُعرف هذا بالمنشئ الافتراضي. ويُستخدم عندما لا تحدد الفئة منشئًا.

يُنشئ الكود التالي ويُهيئ (السطر 6) كائن Person جديد باستخدام الصيغة المذكورة أعلاه:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
            Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
        }
    }
}

السطر 6 أعلاه، يتم استخدام المنشئ بدون معلمات Person(). في هذه الحالة بالذات، كان بإمكاننا أيضًا كتابة


            Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };

لكن الأقواس المحيطة بالمنشئ Person() بدون معلمات ليست إلزامية في هذه الصيغة.

والنتائج هي كما يلي:

p2=(Arthur,Martin,7)

في كثير من الحالات، تقوم خاصية get و set ببساطة بقراءة وكتابة حقل خاص دون أي معالجة إضافية. في هذا السيناريو، يمكننا استخدام إعلان تلقائي على النحو التالي:

public Type Propriété{ get ; set ; }

لا يتم إعلان الحقل الخاص المرتبط بالخاصية. يتم إنشاؤه تلقائيًا بواسطة المُجمع. ولا يمكن الوصول إليه إلا من خلال خاصيته. لذا، بدلاً من كتابة:


    private string prenom;
...
    // propriété associée
        public string Prenom {
            get { return prenom; }
            set {
                // prénom valide ?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//if
        }//prenom

يمكننا كتابة:

public string Prenom {get; set;}

دون إعلان الحقل الخاص first name. الفرق بين الخاصيتين السابقتين هو أن الأولى تتحقق من صحة الاسم الأول في مجموعة set، بينما الثانية لا تفعل ذلك.

استخدم الخاصية التلقائية «First name» لإعلان الحقل «First name» كحقل عام:

public string Prenom;

نتساءل عما إذا كان هناك أي فرق بين الإعلانين. لا يُنصح بإعلان حقل فئة كعام. فهذا يتعارض مع مفهوم تغليف حالة الكائن، التي يجب أن تكون خاصة ويتم الكشف عنها بواسطة طرق عامة.

إذا تم إعلان الخاصية التلقائية على أنها افتراضية، فيمكن إعادة تعريفها في فئة فرعية:


    class Class1 {
        public virtual string Prop { get; set; }
}

    class Class2 : Class1 {
        public override string Prop { get { return base.Prop; } set {... } }
}

في السطر 2 أعلاه، يمكن للفئة الفرعية Class2 إدراج كود في set للتحقق من صحة القيمة المعينة للخاصية التلقائية base.Prop للفئة الأصلية Class1.

4.1.15. طرق الفئة وسماتها

لنفترض أننا نريد حساب عدد الكائنات Person التي تم إنشاؤها في أحد التطبيقات. يمكنك إدارة العداد بنفسك، ولكنك تخاطر بنسيان الكائنات المؤقتة التي يتم إنشاؤها هنا أو هناك. يبدو أنه من الأكثر أمانًا تضمين تعليمات في منشئات الفئة Person تعمل على زيادة العداد. تكمن المشكلة في تمرير مرجع إلى هذا العداد حتى يتمكن منشئ الفئة من زيادته: يجب تمرير معلمة جديدة. بدلاً من ذلك، يمكنك تضمين العداد في تعريف الفئة. نظرًا لأنه سمة للفئة نفسها وليس لمثيل معين من تلك الفئة، فإننا نعلنه بشكل مختلف باستخدام الكلمة الرئيسية static :


        private static long nbPersonnes;

للإشارة إليه، نكتب Personne.nbPersonnes لإظهار أنه سمة من سمات فئة Person نفسها. هنا، أنشأنا سمة خاصة لن يتم الوصول إليها مباشرة خارج الفئة. لذلك، نقوم بإنشاء خاصية عامة لإتاحة الوصول إلى سمة الفئة nbPersonnes. لإرجاع قيمة nbPersonnes، لا تحتاج طريقة get لهذه الخاصية إلى كائن Person معين: في الواقع، nbPersonnes هي سمة لفئة بأكملها. لذلك نحتاج إلى خاصية مُعلنة أيضًا بـ static :


        public static long NbPersonnes {
            get { return nbPersonnes; }
}

والتي سيتم استدعاؤها من الخارج باستخدام الصيغة Personne.NbPersonnes. إليك مثال على ذلك.

تصبح فئة P كما يلي:


using System;
 
namespace Chap2 {
    public class Personne {
 
         // class attributes
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }
 
         // instance attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
            nbPersonnes++;
        }
        public Personne(Personne p) {
            Initialise(p);
            nbPersonnes++;
        }
 
...
}

في السطرين 20 و24، يقوم المُنشئون بزيادة الحقل الثابت الموجود في السطر 7.

باستخدام البرنامج التالي:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Personne p2 = new Personne(p1);
            new Personne(p1);
            Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
        }
    }
}

نحصل على النتائج التالية:

    Nombre de personnes créées : 3

4.1.16. جدول بالأشخاص

الكائن هو بيانات مثل أي شيء آخر، وبالتالي يمكن تجميع عدة كائنات في جدول:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
             // a table of people
            Personne[] amis = new Personne[3];
            amis[0] = new Personne("Jean", "Dupont", 30);
            amis[1] = new Personne("Sylvie", "Vartan", 52);
            amis[2] = new Personne("Neil", "Armstrong", 66);
             // display
            foreach (Personne ami in amis) {
                ami.Identifie();
            }
        }
    }
}
  • السطر 7: ينشئ مصفوفة من 3 عناصر من نوع Person. يتم تهيئة هذه العناصر الثلاثة هنا بالقيمة null، أي أنها لا تشير إلى أي كائنات. مرة أخرى، هذا استخدام خاطئ لمصطلح "مصفوفة من الكائنات"، في حين أنها في الواقع مجرد مصفوفة من مراجع الكائنات. إن إنشاء مصفوفة الكائنات، التي هي كائن بحد ذاتها (وجود new)، لا يؤدي إلى إنشاء أي كائنات من نفس نوع عناصرها.
  • الأسطر 8-10: إنشاء 3 كائنات من نوع Person
  • الأسطر 12-14: عرض محتويات الجدول friends

نحصل على النتائج التالية:

1
2
3
[Jean, Dupont, 30]
[Sylvie, Vartan, 52]
[Neil, Armstrong, 66]

4.2. الوراثة بالقدوة

4.2.1. عام

نستعرض هنا مفهوم الميراث. الهدف من الميراث هو "تخصيص" فئة موجودة لتناسب احتياجاتنا. لنفترض أننا نريد إنشاء فئة Enstructor: المعلم هو شخص مميز. لديه سمات لا يمتلكها أي شخص آخر: المادة التي يدرسها، على سبيل المثال. لكنه يمتلك أيضًا سمات أي شخص آخر: الاسم الأول ، الاسم الأخير والعمر. وبالتالي، فإن المعلم هو جزء كامل من فئة Person ولكنه يمتلك سمات إضافية. بدلاً من كتابة Enstructor من الصفر، نفضل أن نأخذ ما تعلمناه في فئة Person ونكيفه مع الطابع الخاص للمعلمين. هذا هو مفهوم التوريث الذي يجعل ذلك ممكنًا.

للتعبير عن أن فئة Teacher ترث خصائص من Person، نكتب:

    public class Enseignant : Personne

تسمى Person بالفئة الأم، وEnseignant بالفئة المشتقة (أو الفرعية). يمتلك الكائن Enstructor جميع خصائص الكائن Person: فهو يمتلك نفس السمات والطرق. لا تتكرر هذه السمات والطرق الخاصة بالفئة الأم في تعريف الفئة الفرعية: نحن نكتفي بالإشارة إلى السمات والطرق التي أضافتها الفئة الفرعية:

نفترض أن Person مُعرَّفة على النحو التالي:


using System;
 
namespace Chap2 {
    public class Personne {
 
         // class attributes
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }
 
         // instance attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String prenom, String nom, int age) {
            Nom = nom;
            Prenom = prenom;
            Age = age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(string, string, int)");
        }
        public Personne(Personne p) {
            Nom = p.Nom;
            Prenom = p.Prenom;
            Age = p.Age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(Personne)");
        }
 
         // properties
        public string Prenom {
            get { return prenom; }
            set {
                 // valid first name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
             }//if
         }//first name
 
        public string Nom {
            get { return nom; }
            set {
                 // valid name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
             }//if
         }//name
 
        public int Age {
            get { return age; }
            set {
                 // valid age?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
             }//if
         }//age
 
         // property
        public string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
        }
    }
 
}

تم استبدال الطريقة Identifies بـ Identity التي تحدد هوية الشخص. نقوم بإنشاء فئة Enstructor ترث من فئة Person :


using System;
 
namespace Chap2 {
    class Enseignant : Personne {
         // attributes
        private int section;
 
         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // the section is saved using the Section property
            Section = section;
             // follow-up
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
         }//manufacturer
 
         // property Section
        public int Section {
            get { return section; }
            set { section = value; }
         }// Section
 
    }
}

تضيف فئة Teacher إلى أساليب وسمات فئة Person :

  • السطر 4: اشتقاق فئة Teacher من فئة Person
  • السطر 6: سمة Section وهي رقم القسم الذي ينتمي إليه المعلم في هيئة التدريس (تقريبًا قسم واحد لكل تخصص). يمكن الوصول إلى هذه السمة الخاصة عبر الخاصية العامة Section في الأسطر 18-21
  • السطر 9: منشئ جديد لتهيئة جميع سمات المعلم

4.2.2. إنشاء كائن Teacher

لا ترث فئة girls منشئات فئة Parent. لذا يجب عليها تعريف منشئاتها الخاصة. ومنشئ Enstructor هو كما يلي:


         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // section is memorized
            Section = section;
             // follow-up
            Console.WriteLine("Construction enseignant(string, string, int, int)");
}//manufacturer

الإعلان


        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {

يُشير إلى أن مُنشئ الفئة يتلقى أربعة معلمات هي الاسم الأول، والاسم، والعمر، والقسم، بالإضافة إلى ثلاث معلمات أخرى (firstname، lastname، age) من فئته الأساسية، وهي هنا فئة Person. ونعلم أن هذه الفئة تحتوي على مُنشئ Person(string، string، int) الذي سيقوم بإنشاء شخص باستخدام المعلمات المُرسلة (firstname، lastname، age). وبمجرد اكتمال إنشاء الفئة الأساسية، يستمر إنشاء فئة Teacher بتنفيذ نص مُنشئ الفئة:


            // on mémorise la section
            Section = section;

لاحظ أنه على يسار علامة =، ليست خاصية section للكائن المستخدم، بل Section المرتبطة به. وهذا يسمح للمنشئ بالاستفادة من أي فحوصات صحة قد يتم إجراؤها بواسطة هذه الطريقة. وهذا يتجنب الحاجة إلى وضعها في مكانين مختلفين: المنشئ والخاصية.

باختصار، منشئ الكائن المشتق :

  • المعلمات التي يحتاجها لبناء نفسه إلى فئته الأساسية
  • يستخدم المعلمات الأخرى لتهيئة سماته الخاصة

ربما كنا نفضل كتابة:


// constructeur
  public Enseignant(string prenom, string nom, int age, int section){
    this.prenom=prenom;
        this.nom=nom;
        this.age=age;
      this.section=section;
  }

لا يمكن القيام بذلك. فقد أعلنت الفئة Person أن حقولها الثلاثة first name و name و age هي حقول خاصة (private). ولا يمكن الوصول المباشر إلى هذه الحقول إلا من خلال كائنات من نفس الفئة. أما جميع الكائنات الأخرى، بما في ذلك الكائنات الفرعية مثل ici، فيجب عليها استخدام الطرق العامة للوصول إليها. وكان الأمر سيختلف لو أن Person أعلنت أن الحقول الثلاثة هي حقول محمية (protected): فهذا يسمح للفئات المشتقة بالوصول المباشر إلى الحقول الثلاثة. في مثالنا، كان استخدام منشئ الفئة الأصلية هو الحل الصحيح، وهذه هي الطريقة المعتادة: عند إنشاء كائن فرعي، نستدعي أولاً منشئ الكائن الأصلي، ثم نكمل عمليات التهيئة الخاصة بالكائن الفرعي (section في مثالنا).

دعونا نجرب برنامج اختبار أول [Program.cs] :


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
        }
    }
}

يقوم هذا البرنامج ببساطة بإنشاء كائن Enstructor (جديد) وتحديد هويته. لا تحتوي فئة Enstructor على طريقة Identite، لكن فئتها الأم تحتوي على طريقة من هذا النوع وهي عامة أيضًا: وبموجب الوراثة، تصبح هذه الطريقة طريقة عامة لفئة Enstructor.

المشروع الكامل هو كما يلي:

والنتائج هي كما يلي:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Jean, Dupont, 30]

يمكننا أن نلاحظ أن:

  • تم إنشاء كائن Person (السطر 1) قبل كائن Enstructor (السطر 2)
  • الهوية التي تم الحصول عليها هي هوية الكائن Person

4.2.3. إعادة تعريف طريقة أو خاصية

في المثال السابق، حصلنا على هوية الجزء Person ولكن بعض المعلومات الخاصة بالفئة Enstructor (القسم) مفقودة. وهذا يقودنا إلى كتابة خاصية تحدد هوية المعلم:


using System;
 
namespace Chap2 {
    class Enseignant : Personne {
         // attributes
        private int section;
 
         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // the section is saved using the Section property
            Section = section;
             // follow-up
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
         }//manufacturer
 
         // property Section
        public int Section {
            get { return section; }
            set { section = value; }
         }// section
 
         // property Identity
        public new string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
        }
    }
}

الأسطر 24-26، تستند الخاصية Identite لفئة Enstructor إلى Identite لفئتها الأم (baseidentity) (السطر 25) لعرض "Person" ثم تكملها بالقسم الخاص بـ Enstructor. لاحظ إعلان الخاصية Identity :


    public new string Identite{

لنفترض وجود كائن teacher E. يحتوي هذا الكائن على Person :

يتم تعريف الخاصية Identity في كل من فئة Teacher وفئتها الأم Person. في فئة Teacher الخاصة بالفتيات، يجب أن تسبق الخاصية Identity الكلمة الرئيسية new للإشارة إلى أنه يتم إعادة تعريف خاصية Identity جديدة لفئة Teacher.


    public new string Identite{

أصبح لدى فئة Enstructor الآن خاصيتان Identite :

  • الواحدة الموروثة من الفئة الأم Person
  • والأخرى خاصة بها

إذا كان E هو فئة Enstructor، فإن E.Identite تشير إلى الخاصية Identite الخاصة بفئة Enstructor. ونقول إن الخاصية Identite الخاصة بفئة Enstructor تعيد تعريف أو تخفي الخاصية Identite الخاصة بالفئة الأم. وبشكل عام، إذا كان O كائنًا وM طريقة، فإن النظام يبحث عن الطريقة M بالترتيب التالي لتنفيذ O.M:

  • في O
  • في فئته الأصلية إذا كان له واحدة
  • في الفئة الأم لفئتها الأم إن وجدت
  • إلخ.

يسمح لك التوريث بإعادة تعريف الطرق/الخصائص التي تحمل الاسم نفسه في الفئة الأم داخل الفئة الفرعية. وهذا يتيح لك تكييف الفئة الفرعية وفقًا لاحتياجاتك الخاصة. وبالاقتران مع تعدد الأشكال، الذي سنناقشه بعد قليل، فإن إعادة تعريف الطرق/الخصائص هي الميزة الرئيسية للتوريث.

لنأخذ نفس برنامج الاختبار المذكور أعلاه:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
        }
    }
}

النتائج التي تم الحصول عليها هذه المرة هي كما يلي:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Jean, Dupont, 30],27]

4.2.4. التعدد

لنفترض سلسلة من الفئات: C0 C1 C2 … ←Cn

حيث تشير Ci Cj إلى أن Cj مشتقة من Ci. وهذا يعني أن Cj تمتلك جميع خصائص Ci بالإضافة إلى خصائص أخرى. لنفترض أن الكائنات Oi من النوع Ci. من الصحيح كتابة:

    Oi=Oj avec j>i

في الواقع، من خلال التوريث، يمتلك Cj جميع خصائص الفئة Ci بالإضافة إلى خصائص أخرى. لذا فإن Oj من النوع Cj يحتوي على كائن من النوع Ci. العملية

    Oi=Oj

تعني أن Oi هو مرجع إلى الكائن من النوع Ci الموجود في الكائن Oj.

إن قدرة متغير من فئة Ci على الإشارة ليس فقط إلى كائن من فئة Ci، بل إلى أي كائن مشتق من فئة Ci، تُسمى بالتعدد الشكلي: وهي قدرة المتغير على الإشارة إلى أنواع مختلفة من الكائنات.

لنأخذ مثالاً وننظر إلى الدالة التالية المستقلة عن الفئة (ثابتة):

    public static void Affiche(Personne p){
        ….
    }

يمكننا أيضًا كتابة

    Personne p;
    ...
    Affiche(p);

ذلك

    Enseignant e;
    ...
    Affiche(e);

في الحالة الأخيرة، ستتلقى المعلمة الرسمية p من النوع Person في الطريقة الثابتة Affiche قيمة من النوع Enstructor. وبما أن نوع المعلم ينحرف عن Person، فإن ذلك يعتبر صحيحًا.

4.2.5. إعادة التعريف والتعدد الشكلي

دعونا نكمل طريقة Affiche الخاصة بنا:


        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p.Identite);
}//poster

تُرجع الخاصية p.Identite سلسلة تحدد هوية الكائن Person p. ماذا يحدث في المثال السابق إذا كانت المعلمة التي تم تمريرها إلى Poster كائنًا من نوع Teacher :


            Enseignant e = new Enseignant(...);
            Affiche(e);

لنلقِ نظرة على المثال التالي:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a teacher
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
             // a person
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
 
         // poster
        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p.Identite);
         }//poster
    }
}

النتائج هي كما يلي:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Lucile, Dumas, 56]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

يُظهر التنفيذ أن p.Identite (السطر 17) قد نفذت هوية شخص، أولاً (السطر 7) الشخص الموجود في المعلم e، ثم (السطر 10) الشخص p نفسه. لم تتكيف مع الكائن الذي تم تمريره فعليًا كمعلمة إلى Poster. كنا نفضل الحصول على الهوية الكاملة لـ theTeacher e. وكان هذا سيتطلب استخدام صيغة p.Identite للإشارة إلى خاصية Identity للكائن الذي يشير إليه p فعليًا بدلاً من خاصية Identity الخاصة بـ "Person" للكائن الذي يشير إليه p فعليًا.

يمكن الحصول على هذه النتيجة من خلال إعلان Identity كخاصية افتراضية (virtual) في الفئة الأساسية Person :


public virtual string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
        }

جعلت الكلمة الرئيسية virtual من Identity خاصية افتراضية. يمكن أيضًا تطبيق هذه الكلمة الرئيسية على الطرق. يجب على الفئات الفرعية التي تعيد تعريف خاصية أو طريقة افتراضية أن تستخدم الكلمة الرئيسية override بدلاً من new لتأهيل الخاصية/الطريقة المعاد تعريفها. وبالتالي، في فئة Teacher، يتم إعادة تعريف الخاصية Identity على النحو التالي:


        public override string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}

ثم ينتج عن البرنامج السابق النتائج التالية:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

هذه المرة، في السطر 3، لدينا الهوية الكاملة للمعلم. الآن دعونا نعيد تعريف طريقة بدلاً من خاصية. كائن الفئة (الاسم المستعار لـ System.Object في C#) هو الفئة "الأم" لجميع فئات C#. لذا عندما تكتب:

    public class Personne

فإننا نكتب ضمناً:

    public class Personne : System.Object

تُعرّف الفئة System.Object طريقة افتراضية ToString:

تُرجع الطريقة ToString اسم الفئة التي ينتمي إليها الكائن، كما هو موضح في المثال التالي:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a teacher
            Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
             // a person
            Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
        }
    }
}

النتائج هي كما يلي:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Chap2.Enseignant
Constructeur Personne(string, string, int)
Chap2.Personne

لاحظ أنه على الرغم من أننا لم نعد تعريف ToString في فئتي Person و Teacher، إلا أننا نرى أن كائن فئة ToString تمكن من عرض اسم الفئة الحقيقي للكائن.

دعونا نعيد تعريف ToString في فئتي Person و Teacher:


        // méthode ToString
        public override string ToString() {
            return Identite;
}

التعريف هو نفسه في كلا الفئتين. انظر إلى برنامج الاختبار التالي:


using System;
namespace Chap2 {
    class Program3 {
        public static void Main() {
             // a teacher
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
             // a person
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
         // poster
        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p);
         }//Poster
    }
}

لنلقِ نظرة على الطريقة Poster التي يكون معلمتها شخصًا p. في السطر 15، لا تحتوي فئة Console على أي متغير يقبل معلمة من نوع Person. من بين طرق WriteLine المختلفة، هناك طريقة تقبل كائنًا. سيستخدم المُجمِّع هذه الطريقة، WriteLine(Object o لأن هذا التوقيع يعني أن o يمكن أن يكون من نوع Object أو مشتقًا منه. نظرًا لأن Object هي الفئة الأم لجميع الفئات، يمكن تمرير أي كائن كمعلمة إلى WriteLine وبالتالي كائن من نوع Person أو Teacher. تكتب الطريقة WriteLine(Object o) o.ToString() في تدفق الكتابة Out. نظرًا لأن الطريقة ToString افتراضية، إذا كان الكائن o (من نوع Object أو مشتق) قد أعاد تعريف ToString، فسيتم استخدام هذا الأخير. هذا هو الحال هنا مع Person و Teacher.

وهذا ما تظهره نتائج الأداء:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

4.3. إعادة تعريف معنى عامل لفئة

4.3.1. مقدمة

لننظر إلى التعليمات التالية

op1 + op2

حيث op1 و op2 هما معاملان. من الممكن إعادة تعريف معنى عامل +. إذا كان المعامل op1 كائنًا من فئة C1، فيجب تعريف طريقة ثابتة في C1 بالتوقيع التالي:

public static [type] operator +(C1 opérande1, C2 opérande2);

عندما يصادف المُترجم

op1 + op2

ثم يقوم بترجمتها إلى C1.operator+(op1,op2). ويُعد النوع الذي يحدده المشغل أمرًا مهمًا. لنأخذ العملية op1+op2+op3 على سبيل المثال. يقوم المُترجم بترجمتها إلى (op1+op2)+op3. لنفترض أن res12 هي نتيجة op1+op2. العملية التالية هي res12+op3. إذا كان نوع res12 هو C1، فسيتم ترجمتها أيضًا إلى C1.operator+(res12,op3). وهذا يجعل من الممكن ربط العمليات معًا.

يمكن أيضًا إعادة تعريف العوامل الأحادية ذات المعامل الواحد. على سبيل المثال، إذا كان op1 كائنًا من النوع C1، فيمكن إعادة تعريف العملية op1++ بواسطة طريقة ثابتة لـ C1 :

public static [type] operator ++(C1 opérande1);

ما قيل هنا ينطبق على معظم العوامل مع بعض الاستثناءات:

  • يجب إعادة تعريف العوامل == و != في نفس الوقت
  • لا يمكن إعادة تعريف العوامل && و|| و[] و() و+= و-= و...

4.3.2. مثال

نقوم بإنشاء ListeDePersonnes مشتقة من ArrayList. تنفذ هذه الفئة قائمة ديناميكية ويتم عرضها في الفصل التالي. نستخدم فقط العناصر التالية من هذه الفئة:

  • الطريقة L.Add(Object o) لإضافة كائن o إلى L. هنا سيكون الكائن o كائنًا من فئة Person.
  • الخاصية L.Count التي تعطي عدد العناصر في القائمة L
  • الترميز L[i] الذي يعطي العنصر i من القائمة L

سترث فئة ListeDePersonnes جميع سمات ووظائف وخصائص ArrayList. وتعريفها كما يلي:


using System;
using System.Collections;
using System.Text;
 
namespace Chap2 {
    class ListeDePersonnes : ArrayList{
         // redefine + operator, to add a person to the list
        public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
             // person p is added to the ListeDePersonnes l
            l.Add(p);
             // we return the ListeDePersonnes l
            return l;
         }// operator +
 
         // ToString
        public override string ToString() {
             // render (él1, él2, ..., éln)
             // opening parenthesis
            StringBuilder listeToString = new StringBuilder("(");
             // browse the list of people (this)
            for (int i = 0; i < Count - 1; i++) {
                listeToString.Append(this[i]).Append(",");
            }//for
             // last element
            if (Count != 0) {
                listeToString.Append(this[Count-1]);
            }
             // closing parenthesis
            listeToString.Append(")");
             // you must return a string
            return listeToString.ToString();
         }//ToString
    }
}
  • السطر 6: اشتقاق فئة ListeDePersonnes من فئة ArrayList
  • الأسطر 8-13: تعريف عامل + لعملية l + p، حيث l من نوع ListeDePersonnes و p من نوع Person أو مشتق.
  • السطر 10: تتم إضافة الشخص p إلى القائمة l. هذه هي طريقة Add للفئة الأصلية ArrayList المستخدمة هنا.
  • السطر 12: يتم عرض الإشارة إلى القائمة l بحيث يمكن ربط عوامل +، كما في l + p1 + p2. سيتم تفسير العملية l+p1+p2 (أولوية العامل) على أنها (l+p1)+p2. تؤدي العملية l+p1 إلى إنشاء المرجع l. ثم تصبح العملية (l+p1)+p2 هي l+p2 التي تضيف الشخص p2 إلى قائمة الأشخاص l.
  • السطر 16: نعيد تعريف ToString لعرض قائمة بالأشخاص على النحو التالي (person1, person2, ..) حيث personi هي نفسها نتيجة فئة ToString Person.
  • السطر 19: نستخدم كائنًا من نوع StringBuilder. تُعد هذه الفئة أكثر ملاءمة من نوع string عندما تتطلب العملية إجراء العديد من العمليات على السلاسل، مثل عمليات الإضافة هنا. في الواقع، تؤدي كل عملية تُجرى على سلسلة إلى إنشاء كائن string جديد، بينما تؤدي نفس العمليات على StringBuilder إلى تعديل الكائن الحالي دون إنشاء كائن جديد. نستخدم الطريقة Append لربط السلاسل.
  • السطر 21: التمرير عبر عناصر قائمة الأشخاص. يتم هنا تحديد هذه القائمة بواسطة this. هذا هو الكائن الحالي الذي يتم تطبيق ToString عليه. الخاصية Count هي خاصية للفئة الأصلية ArrayList.
  • السطر 22: العنصر رقم i في القائمة الحالية this يمكن الوصول إليه عبر الترميز this[i]. مرة أخرى، هذه خاصية من ArrayList. وبما أن الأمر يتعلق بإضافة سلاسل، فسيتم استخدام this[i].ToString(). وبما أن هذه طريقة افتراضية، فسيتم استخدام كائن ToString this، من النوع Person أو مشتق منه.
  • السطر 31: نحتاج إلى إرجاع كائن من نوع string (السطر 16). تحتوي الفئة StringBuilder على طريقة ToString التي تسمح لك بالتحويل من StringBuilder إلى نوع string.

لاحظ أن ListeDePersonnes لا تحتوي على منشئ. في هذه الحالة، نعلم أن

public ListeDePersonnes(){
}

. لا يقوم هذا المنشئ بأي شيء سوى استدعاء المنشئ الخالي من المعلمات لفئة الأصل:

public ArrayList(){
...
}

قد تبدو فئة الاختبار كما يلي:


using System;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // a list of people
            ListeDePersonnes l = new ListeDePersonnes();
             // add people
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
             // display
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
        }
    }
}
  • السطر 7: إنشاء قائمة بالأشخاص l
  • السطر 9: إضافة شخصين باستخدام علامة +
  • السطر 12: إضافة معلم
  • السطران 11 و 13: استخدام الطريقة المعاد تعريفها ListeDePersonnes.ToString().

النتائج:

l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])

4.4. تحديد مفهرس لفئة

نستمر هنا في استخدام الفئة ListeDePersonnes. إذا كان l كائنًا من فئة ListeDePersonnes، فإننا نريد أن نتمكن من استخدام l[i] للإشارة إلى الشخص رقم i في القائمة l سواء في القراءة (Person p=l[i]) أو في الكتابة (l[i]=new Person(...)).

لنتمكن من كتابة l[i] حيث يشير l[i] إلى كائن Person، الفئة، نحتاج إلى تعريف ListeDePersonnes بالطريقة التالية:


        public Personne this[int i] {
            get { ... }
            set { ... }
}

تسمى هذه الطريقة this[int i]، وهي عبارة عن مؤشر لأنها تعطي معنى للتعبير obj[i] الذي يذكرنا بترميز المصفوفات، في حين أن obj ليس مصفوفة بل كائن. يتم استدعاء طريقة get لهذا الكائن obj عند كتابة variable=obj[i]، بينما يتم استدعاء طريقة set عند كتابة obj[i]=value.

تشتق فئة ListeDePersonnes من فئة ArrayList التي تحتوي هي نفسها على مؤشر:

    public object this[int i] { ... }

يوجد تعارض بين هذه الفئة ListeDePersonnes:


 public Personne this[int i] 

وفئة ArrayList


 public object this[int i] 

لأنهما يحملان نفس الاسم ونفس نوع المعلمة (int). وللإشارة إلى أن هذه الفئة ListeDePersonnes "تخزن مؤقتًا" الطريقة التي تحمل نفس الاسم في فئة ArrayList، يتعين علينا إضافة الكلمة الرئيسية new عند إعلان ListeDePersonnes. لذلك نكتب:


    public new Personne this[int i]{
        get { ... }
        set { ... }
    }

دعونا نكمل هذه الطريقة. يتم استدعاء الطريقة this.get عندما تكون المتغير=l[i] على سبيل المثال، حيث نوع l هو ListeDePersonnes. يجب علينا عندئذٍ إرجاع الشخص رقم i من القائمة l. يتم ذلك باستخدام الترميز base[i]، الذي يجعل الكائن رقم i من فئة ArrayList التي تشكل الأساس لـ ListeDePersonnes. الكائن المرجع هو من نوع Object، لذا يلزم تحويله إلى فئة Person.


    public new Personne this[int i]{
        get { return (Personne) base[i]; }
        set { ... }
    }

يتم استدعاء الطريقة set عندما يكون l[i]=p حيث p هو Person. والهدف هو ربط الشخص p بالعنصر i في l.


    public new Personne this[int i]{
        get { ... }
        set { base[i]=value; }
    }

هنا، يتم تعيين الشخص p الذي تمثله الكلمة الرئيسية value إلى العنصر رقم i من الفئة الأساسية ArrayList.

وبالتالي، سيكون مؤشر الفئة ListeDePersonnes كما يلي:


    public new Personne this[int i]{
        get { return (Personne) base[i]; }
        set { base[i]=value; }
    }

الآن نريد أن نتمكن من كتابة Person p=l["name"], أي فهرسة القائمة l ليس برقم العنصر، بل باسم الشخص. للقيام بذلك، نحدد مؤشرًا جديدًا:


        // indexeur via un nom
        public int this[string nom] {
            get {
                // on recherche la personne
                for (int i = 0; i < Count; i++) {
                    if (((Personne)base[i]).Nom == nom)
                        return i;
                }//for
                return -1;
            }//get
}

السطر الأول


public int this[string nom]

يشير إلى أن قائمة ListeDePersonnes تحتوي على اسم سلسلة نصية، وأن نتيجة l[name] هي عدد صحيح. وسيكون هذا العدد الصحيح هو الموضع الذي يشغله الشخص الذي يحمل الاسم "name" في القائمة، أو -1 إذا لم يكن هذا الشخص موجودًا في القائمة. ويقتصر الأمر على القراءة فقط، مما يمنع الكتابة l["name"]=value، الأمر الذي كان سيتطلب تعريف فئة set. الكلمة الرئيسية new غير مطلوبة في إعلان المؤشر، حيث أن الفئة الأساسية ArrayList لا تحدد مؤشرًا this[string].

في نص get، يتم مسح قائمة الأشخاص بحثًا عن الاسم الذي تم تمريره في المعلمة. إذا تم العثور عليه في الموضع i، يتم إرجاع i، وإلا يتم إرجاع -1.

يتم إكمال برنامج الاختبار السابق على النحو التالي:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a list of people
            ListeDePersonnes l = new ListeDePersonnes();
             // add people
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
             // display
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
             // change item 1
            l[1] = new Personne("franck", "gallon",5);
             // display element 1
            Console.WriteLine("l[1]=" + l[1]);
             // display list l
            Console.WriteLine("l=" + l);
             // people search
            string[] noms = { "martin", "germain", "xx" };
            for (int i = 0; i < noms.Length; i++) {
                int inom = l[noms[i]];
                if (inom != -1)
                    Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
                else
                    Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
            }//for
        }
    }
}

يؤدي تنفيذها إلى النتائج التالية:

1
2
3
4
5
6
7
l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])
l[1]=[franck, gallon, 5]
l=([jean, martin, 10],[franck, gallon, 5],Enseignant[[camille, germain, 27],60])
Personne(martin)=[jean, martin, 10]
Personne(germain)=Enseignant[[camille, germain, 27],60]
Personne(xx) n'existe pas

4.5. الهياكل

تشبه بنية C# بنية لغة C وهي قريبة جدًا من مفهوم الفئة. تُعرَّف البنية على النحو التالي:

struct NomStructure{
// attributs
    ...
// propriétés
...
// constructeurs
...
// méthodes
...
}

على الرغم من وجود إعلانات مشابهة، هناك اختلافات كبيرة بين الفئة والبنية. على سبيل المثال، مفهوم الوراثة غير موجود في البنى. إذا كنا نكتب فئة لا تحتاج إلى أن تكون مشتقة، فما هي الاختلافات بين البنية والفئة التي ستساعدنا في الاختيار بينهما؟ دعونا نستخدم المثال التالي لمعرفة ذلك:


using System;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // a sp1 structure
            SPersonne sp1;
            sp1.Nom = "paul";
            sp1.Age = 10;
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
             // a sp2 structure
            SPersonne sp2 = sp1;
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
             // sp2 is modified
            sp2.Nom = "nicole";
            sp2.Age = 30;
             // checking sp1 and sp2
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
 
             // an op1 object
            CPersonne op1=new CPersonne();
            op1.Nom = "paul";
            op1.Age = 10;
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
             // an op2 object
            CPersonne op2=op1;
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
             // op2 is modified
            op2.Nom = "nicole";
            op2.Age = 30;
             // op1 and op2 verification
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
        }
    }
     // structure SPersonne
    struct SPersonne {
        public string Nom;
        public int Age;
    }
 
     // class CPersonne
    class CPersonne {
        public string Nom;
        public int Age;
    }
 
}
  • الأسطر 38-41: بنية تحتوي على حقلين عامين: Nom، Age
  • الأسطر 44-47: فئة تحتوي على حقلين عامين: الاسم، العمر

إذا قمنا بتشغيل هذا البرنامج، فسنحصل على النتائج التالية:

1
2
3
4
5
6
7
8
sp1=SPersonne(paul,10)
sp2=SPersonne(paul,10)
sp1=SPersonne(paul,10)
sp2=SPersonne(nicole,30)
op1=CPersonne(paul,10)
op2=CPersonne(paul,10)
op1=CPersonne(nicole,30)
op2=CPersonne(nicole,30)

حيث كنا نستخدم سابقًا Person، نستخدم الآن SPersonne :


    struct SPersonne {
        public string Nom;
        public int Age;
}

لا تحتوي البنية هنا على منشئ. يمكن أن تحتوي على منشئ، كما سنوضح لاحقًا. بشكل افتراضي، تحتوي دائمًا على منشئ بدون معلمات، هنا SPersonne().

  • السطر 7 من الكود: الإعلان

    SPersonne sp1;

يعادل الأمر:


    SPersonne sp1=new Spersonne();

يتم إنشاء بنية (Name,Age) وقيمة sp1 هي هذه البنية نفسها. في حالة الفئة، يجب أن يتم إنشاء الكائن (Name,Age) بشكل صريح بواسطة المشغل new (السطر 22):


CPersonne op1=new CPersonne();

تنشئ التعليمات السابقة كائن CPersonne (مما يعادل تقريبًا البنية الخاصة بنا) وتكون قيمة p1 عندئذٍ هي عنوان (مرجع) هذا الكائن.

للتلخيص

  • في حالة البنية، تكون قيمة sp1 هي البنية نفسها
  • في حالة الفئة، تكون قيمة op1 هي عنوان الكائن الذي تم إنشاؤه

عندما نكتب في البرنامج السطر 12:


            SPersonne sp2 = sp1;

يتم إنشاء بنية sp2(Name,Age) جديدة وتهيئتها بقيمة sp1، وهي البنية نفسها.

يتم تكرار بنية sp1 في sp2 [1]. هذه نسخة من قيمة. الآن انظر إلى التعليمات في السطر 27:


CPersonne op2=op1;

في حالة الفئات، يتم نسخ قيمة op1 إلى op2، ولكن بما أن هذه القيمة هي في الواقع عنوان الكائن، فإنها لا يتم تكرارها [2].

في حالة البنية [1]، إذا قمنا بتغيير قيمة sp2، فإن قيمة sp1 تتغير، كما هو موضح في البرنامج. في حالة الكائن [2]، إذا قمنا بتعديل الكائن الذي يشير إليه op2، فإن الكائن الذي يشير إليه op1 يتغير أيضًا لأنهما متطابقان. وهذا ما تظهره نتائج البرنامج أيضًا.

توضح هذه التفسيرات ما يلي:

  • قيمة متغير البنية هي البنية نفسها
  • قيمة متغير الكائن هي عنوان الكائن المشار إليه

بمجرد فهم هذا الاختلاف الأساسي، تصبح البنية قريبة جدًا من الفئة، كما هو موضح في المثال الجديد التالي:


using System;
 
namespace Chap2 {
 
     // structure SPersonne
    struct SPersonne {
         // private attributes
        private string nom;
        private int age;
 
         // properties
        public string Nom {
            get { return nom; }
            set { nom = value; }
         }//name
 
        public int Age {
            get { return age; }
            set { age = value; }
         }//age
 
         // Manufacturer
        public SPersonne(string nom, int age) {
            this.nom = nom;
            this.age = age;
         }//manufacturer
 
         // ToString
        public override string ToString() {
            return "SPersonne(" + Nom + "," + Age + ")";
         }//ToString
     }//structure
}//namespace
  • السطران 8-9: حقلان خاصان
  • السطور 12-20: خصائص عامة مرتبطة
  • الأسطر 23-26: تعريف مُنشئ. لاحظ أن المُنشئ الخالي من المعلمات SPersonne() موجود دائمًا ولا يحتاج إلى إعلان. فالإعلان عنه يُرفض من قِبل المُترجم. في المُنشئ الموجود في الأسطر 23-26، قد تميل إلى تهيئة الحقول الخاصة name و age عبر خصائصهما العامة Name و Age. لكن المُترجم يرفض ذلك. لا يمكن استخدام أساليب البنية أثناء إنشاء البنية.
  • الأسطر 29-31: إعادة تعريف الأسلوب ToString.

قد يبدو برنامج الاختبار كما يلي:


using System;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // one person p1
            SPersonne p1=new SPersonne();
            p1.Nom="paul";
            p1.Age= 10;
            Console.WriteLine("p1={0}",p1);
             // one person p2
            SPersonne p2 = p1;
            Console.WriteLine("p2=" + p2);
             // p2 is modified
            p2.Nom = "nicole";
            p2.Age = 30;
             // checking p1 and p2
            Console.WriteLine("p1=" + p1);
            Console.WriteLine("p2=" + p2);
             // one person p3
            SPersonne p3 = new SPersonne("amandin", 18);
            Console.WriteLine("p3=" + p3);
             // one person p4
            SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
            Console.WriteLine("p4=" + p4);
        }
    }
}
  • السطر 7: نحن مضطرون إلى استخدام المنشئ بدون معلمات بشكل صريح، لأن هناك منشئًا آخر في البنية. إذا لم تكن البنية تحتوي على منشئ، فإن التعليمات

            SPersonne p1;

كانت كافية لإنشاء بنية فارغة.

  • السطران 8-9: يتم تهيئة البنية عبر خصائصها العامة
  • السطر 10: سيتم استخدام الطريقة p1.ToString في WriteLine.
  • السطر 21: إنشاء بنية باستخدام المنشئ SPersonne(string,int)
  • السطر 24: إنشاء بنية باستخدام المنشئ بدون معلمات SPersonne() مع تهيئة الحقول الخاصة بين الأقواس باستخدام خصائصها العامة.

يتم الحصول على النتائج التالية:

1
2
3
4
5
6
p1=SPersonne(paul,10)
p2=SPersonne(paul,10)
p1=SPersonne(paul,10)
p2=SPersonne(nicole,30)
p3=SPersonne(amandin,18)
p4=SPersonne(x,10)

الفرق الوحيد الملحوظ هنا بين البنية والفئة هو أنه في حالة الفئة، كان الكائنان p1 و p2 سيشيران إلى نفس الكائن في نهاية البرنامج.

4.6. الواجهات

الواجهة هي مجموعة من الأساليب أو الخصائص النموذجية التي تشكل عقدًا. تتعهد الفئة التي تقرر تنفيذ واجهة بتوفير تنفيذ لجميع الأساليب المحددة في الواجهة. يتحقق المُترجم من هذا التنفيذ.

على سبيل المثال، إليك تعريف الواجهة System.Collections.IEnumerator :

public interface System.Collections.IEnumerator 


{    // Prop
e   rties Object Curren

t    { get; } 
     // Methods 
     bool MoveNe
xt(); void Reset(); }

يتم تعريف خصائص وأساليب الواجهة فقط من خلال توقيعاتها. وهي غير مطبقة (لا تحتوي على كود). إن الفئات التي تطبق الواجهة هي التي توفر الكود لأساليب وخصائص الواجهة.

1
2
3
4
5
6
public class C : IEnumerator{
    ...
    Object Current{ get {...}}
    bool MoveNext{...}
    void Reset(){...}
}
  • السطر 1: الفئة C تنفذ الفئة IEnumerator. لاحظ أن علامة : المستخدمة لتنفيذ واجهة هي نفسها المستخدمة لاشتقاق فئة.
  • الأسطر 3-5: تنفيذ طرق وخصائص واجهة IEnumerator.

انظر إلى الواجهة التالية:


namespace Chap2 {
    public interface IStats {
        double Moyenne { get; }
        double EcartType();
    }
}

تقدم الواجهة IStats:

  • خاصية للقراءة فقط Average : لحساب متوسط سلسلة من القيم
  • طريقة EcartType: لحساب الانحراف المعياري

لاحظ أنه لم يتم تحديد سلسلة القيم المعنية في أي مكان. قد يكون متوسط درجات فصل دراسي، أو متوسط المبيعات الشهرية لمنتج معين، أو متوسط درجة الحرارة في موقع معين، إلخ. هذا هو مبدأ الواجهات: نفترض وجود طرق في الكائن، ولكن لا نفترض وجود بيانات محددة.

قد يكون أحد تطبيقات IStats من الدرجة الأولى فئة تُستخدم لتخزين درجات الطلاب في فصل دراسي في مادة معينة. ويتم تمييز الطالب بالبنية Student التالية:


    public struct Elève {
        public string Nom { get; set; }
        public string Prénom { get; set; }
}//Student

سيتم تحديد هوية الطالب من خلال الاسم الأول واسم العائلة. توضح السطران 2 و3 الخصائص التلقائية لهاتين السمتين.

سيتم تمييز الملاحظة بالبنية Note التالية:


    public struct Note {
        public Elève Elève { get; set; }
        public double Valeur { get; set; }
}//Note

سيتم تحديد الدرجة من خلال الطالب الذي تم تقييمه والدرجة نفسها. توضح السطور 2-3 الخصائص التلقائية لهاتين السمتين.

يتم تجميع درجات جميع الطلاب في مادة معينة في الفئة TableauDeNotes التالية:


using System;
using System.Text;
 
namespace Chap2 {
 
    public class TableauDeNotes : IStats {
         // attributes
        public string Matière { get; set; }
        public Note[] Notes { get; set; }
        public double Moyenne { get; private set; }
        private double ecartType;
 
         // manufacturer
        public TableauDeNotes(string matière, Note[] notes) {
             // saving via public properties
            Matière = matière;
            Notes = notes;
             // calculating the average score
            double somme = 0;
            for (int i = 0; i < Notes.Length; i++) {
                somme += Notes[i].Valeur;
            }
            if (Notes.Length != 0) Moyenne = somme / Notes.Length;
            else Moyenne = -1;
             // standard deviation
            double carrés = 0;
            for (int i = 0; i < Notes.Length; i++) {
                carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
            }//for
            if (Notes.Length != 0)
                ecartType = Math.Sqrt(carrés / Notes.Length);
            else ecartType = -1;
         }//manufacturer
 
        public double EcartType() {
            return ecartType;
        }
 
         // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
             // concatenate all the notes
            for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
             //final note
            if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
             // end
            return valeur.ToString();
         }//ToString
 
     }//class
}
  • السطر 6: تنفذ الفئة TableauDeNotes واجهة IStats. لذلك يجب أن تنفذ Average و EcartType. يتم تنفيذ هذين في السطر 10 (Average) و 35-37 (EcartType)
  • الأسطر 8-10: ثلاث خصائص تلقائية
  • السطر 8: المادة التي يحفظ الكائن ملاحظاتها
  • السطر 9: جدول درجات الطلاب (Student، Grade)
  • السطر 10: متوسط الدرجات - خاصية تنفذ واجهة Average IStats.
  • السطر 11: الحقل الذي يحفظ الانحراف المعياري للدرجات - الطريقة get المرتبطة بـ EcartType في الأسطر 35-37 تنفذ واجهة EcartType IStats.
  • السطر 9: يتم تخزين الدرجات في جدول. ويتم إرسال هذا الجدول عند إنشاء فئة TableauDeNotes إلى مُنشئ الأسطر 14-33.
  • السطور 14-33: المنشئ. يُفترض هنا أن الدرجات التي يتم إرسالها إلى المنشئ لن تتغير في المستقبل. لذلك نستخدم المنشئ لحساب المتوسط والانحراف المعياري لهذه الدرجات على الفور وتخزينهما في الحقول الموجودة في السطور 10-11. يتم تخزين المتوسط في الحقل الخاص الذي يكمن وراء الخاصية التلقائية Average في السطر 10، ويتم تخزين الانحراف المعياري في الحقل الخاص في السطر 11.
  • السطر 10: ستقوم الطريقة get التي تمتلكها الخاصية التلقائية Average بعرض الحقل الخاص الأساسي.
  • الأسطر 35-37: تعرض الطريقة EcartType قيمة الحقل الخاص في السطر 11.

هناك بعض التفاصيل الدقيقة في هذا الكود:

  • السطر 23: تُستخدم الطريقة set property Average لإجراء التعيين. تم إعلان هذه الطريقة على أنها خاصة في السطر 10، بحيث لا يمكن تعيين قيمة لـ Average إلا داخل الفصل الدراسي.
  • الأسطر 40-54: استخدم كائن StringBuilder لإنشاء السلسلة التي تمثل TableauDeNotes لتحسين الأداء. ومع ذلك، تجدر الإشارة إلى أن قابلية قراءة الكود تتأثر بشكل كبير. وهذا هو الجانب الآخر من العملة.

في الفصل السابق، تم تخزين الملاحظات في جدول. لم يكن من الممكن إضافة ملاحظة جديدة بمجرد إنشاء TableauDeNotes. نقترح الآن تطبيقًا ثانيًا لـ IStats، يسمى ListeDeNotes، حيث يتم هذه المرة حفظ الملاحظات في قائمة، مع إمكانية إضافة ملاحظات بعد الإنشاء الأولي للكائن ListeDeNotes.

فيما يلي كود فئة ListeDeNotes:


using System;
using System.Text;
using System.Collections.Generic;
 
namespace Chap2 {
 
    public class ListeDeNotes : IStats {
         // attributes
        public string Matière { get; set; }
        public List<Note> Notes { get; set; }
        public double moyenne = -1;
        public double ecartType = -1;
 
         // manufacturer
        public ListeDeNotes(string matière, List<Note> notes) {
             // saving via public properties
            Matière = matière;
            Notes = notes;
         }//manufacturer
 
         // add a note
        public void Ajouter(Note note) {
             // add note
            Notes.Add(note);
             // mean and standard deviation reset
            moyenne = -1;
            ecartType = -1;
        }
 
         // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
             // concatenate all the notes
            for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
             //final note
            if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
             // end
            return valeur.ToString();
         }//ToString
 
         // average score
        public double Moyenne {
            get {
                if (moyenne != -1) return moyenne;
                 // calculating the average score
                double somme = 0;
                for (int i = 0; i < Notes.Count; i++) {
                    somme += Notes[i].Valeur;
                }
                 // we return the average
                if (Notes.Count != 0) moyenne = somme / Notes.Count;
                return moyenne;
            }
        }
 
        public double EcartType() {
             // standard deviation
            if (ecartType != -1) return ecartType;
             // average
            double moyenne = Moyenne;
            double carrés = 0;
            for (int i = 0; i < Notes.Count; i++) {
                carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
            }//for
             // we return the standard deviation
            if (Notes.Count != 0)
                ecartType = Math.Sqrt(carrés / Notes.Count);
            return ecartType;
        }
     }//class
}
  • السطر 7: الفئة ListeDeNotes تنفذ واجهة IStats
  • السطر 10: يتم الآن عرض الملاحظات في قائمة بدلاً من جدول
  • السطر 11: تم التخلي هنا عن الملكية التلقائية لـ«Average» في فئة TableauDeNotes لصالح حقل خاص باسم «average» في السطر 11، مرتبط بملكية عامة للقراءة فقط لـ«Average» في الأسطر 48-60
  • الأسطر 22-28: يمكنك الآن إضافة ملاحظة إلى تلك التي تم حفظها بالفعل، وهو ما كان مستحيلاً في السابق.
  • الأسطر 15-19: نتيجة لذلك، لم يعد يتم حساب المتوسط والانحراف المعياري في المنشئ، بل في طرق الواجهة نفسها: Average (الأسطر 48-60) و EcartType (62-76). ومع ذلك، لا يتم إعادة الحساب إلا إذا كان المتوسط والانحراف المعياري مختلفين عن -1 (السطران 50 و 64).

قد تبدو فئة الاختبار كما يلي:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // some students & english notes
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
             // which we save in a TableauDeNotes object
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
             // we put the students and the material in a ListeDeNotes object
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
             // we add a note
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
        }
    }
}
  • السطر 8: إنشاء مصفوفة من الطلاب باستخدام المنشئ بدون معلمات والتهيئة عبر الخصائص العامة
  • السطر 9: إنشاء جدول للملاحظات باستخدام نفس التقنية
  • السطر 11: كائن TableauDeNotes يتم حساب متوسطه وانحرافه المعياري في السطر 13
  • السطر 15: كائن ListeDeNotes يتم حساب متوسطه وانحرافه المعياري في السطر 17. تحتوي الفئة List<Note> على منشئ يقبل كائنًا ينفذ IEnumerable<Note>. ينفذ الجدول notes1 هذه الواجهة ويمكن استخدامه لإنشاء List<Note>.
  • السطر 19: إضافة ملاحظة جديدة
  • السطر 21: إعادة حساب المتوسط والانحراف المعياري

النتائج هي كما يلي:

1
2
3
matière=anglais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18],[Jérôme,Jaric,10]), Moyenne=14,5, Ecart-type=2,95803989154981

في المثال السابق، تقوم فئتان بتنفيذ IStats. ومع ذلك، لا يوضح المثال مدى فائدة واجهة IStats. دعونا نعيد كتابة برنامج الاختبار على النحو التالي:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // some students & english notes
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
             // which we save in a TableauDeNotes object
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
             // average and standard deviation display
            AfficheStats(anglais);
             // we put the students and the material in a ListeDeNotes object
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
             // average and standard deviation display
            AfficheStats(français);
             // we add a note
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
             // average and standard deviation display
            AfficheStats(français);
        }
 
         // display mean and standard deviation of a type IStats
        static void AfficheStats(IStats valeurs) {
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
        }
    }
}
  • الأسطر 25-27: تتلقى الطريقة الثابتة AfficheStats كائنًا من نوع IStats، وهو واجهة. وهذا يعني أن المعلمة الفعلية يمكن أن تكون أي كائن ينفذ واجهة IStats. عند استخدام بيانات ذات نوع واجهة، فهذا يعني أنك تستخدم فقط طرق الواجهة التي تنفذها البيانات. يتم تجاهل الباقي. هذه خاصية مشابهة للتعدد الشكلي الذي نراه في الفئات. إذا كانت مجموعة من Ci غير مرتبطة بالوراثة (لذلك لا يمكنك استخدام تعدد أشكال الوراثة) تقدم مجموعة من الطرق بنفس التوقيع، فقد يكون من المثير للاهتمام تجميع هذه الطرق في واجهة I تنفذها جميع الفئات المعنية. يمكن بعد ذلك استخدام مثيلات هذه الفئات Ci كمعلمات فعالة للوظائف التي تقبل معلمة رسمية من النوع I، أي الوظائف التي تستخدم فقط طرق الكائنات Ci المحددة في I وليس سمات وطرق الفئات الفردية Ci.
  • السطر 13: يتم استدعاء الطريقة AfficheStats باستخدام TableauDeNotes التي تنفذ IStats
  • السطر 17: نفس الشيء مع نوع ListeDeNotes

نتائج هذا التشغيل مطابقة لتلك التي تم الحصول عليها في التشغيل السابق.

يمكن أن يكون المتغير من نوع الواجهة. وبالتالي، يمكننا كتابة:

1
2
3
IStats stats1=new TableauDeNotes(...);
...
stats1=new ListeDeNotes(...);

تشير العبارة الموجودة في السطر 1 إلى أن stats1 هي مثيل لفئة تنفذ IStats. وتشير هذه العبارة إلى أن المُجمع لن يسمح بالوصول إلا إلى طرق واجهة stats1: Average و EcartType.

أخيرًا، تجدر الإشارة إلى أنه يمكن تنفيذ الواجهات بطرق متعددة، أي يمكن كتابتها على النحو التالي

public class ClasseDérivée:ClasseDeBase,I1,I2,..,In{
...
}

حيث Ij هي واجهات.

4.7. الفئات المجردة

الفئة المجردة هي فئة لا يمكن إنشاء مثيل لها. تحتاج إلى إنشاء فئات مشتقة يمكن إنشاء مثيلات لها.

يمكن استخدام الفئات المجردة لتقسيم كود مجموعة من الفئات. لنفكر في الحالة التالية:


using System;
 
namespace Chap2 {
    abstract class Utilisateur {
         // fields
        private string login;
        private string motDePasse;
        private string role;
 
         // manufacturer
        public Utilisateur(string login, string motDePasse) {
             // information is recorded
            this.login = login;
            this.motDePasse = motDePasse;
            // on identifie l'utilisateur
            role=identifie();
             // identified?
            if (role == null) {
                throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
            }
        }
 
         // toString
        public override string ToString() {
            return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
        }
 
         // identifies
        abstract public string identifie();
    }
}
  • الأسطر 11-21: منشئ الفئة User. تخزن هذه الفئة معلومات عن مستخدم تطبيق ويب. يحتوي هذا التطبيق على أنواع مختلفة من المستخدمين الذين يتم توثيقهم بواسطة اسم المستخدم/كلمة المرور (الأسطر 6-7). يتم التحقق من هاتين المعلومتين باستخدام خدمة LDAP لبعض المستخدمين، وباستخدام SGBD لآخرين، إلخ...
  • السطور 13-14: يتم تخزين معلومات المصادقة في الذاكرة
  • السطر 16: يتم التحقق منها بواسطة method identifies. ونظرًا لأن طريقة التعريف غير معروفة، يتم إعلانها على أنها مجردة في السطر 29 باستخدام الكلمة المفتاحية abstract. تُرجع الطريقة identifies سلسلة تحدد دور المستخدم (بشكل أساسي، ما يُسمح له بفعله). إذا كانت هذه السلسلة فارغة، يتم إلقاء استثناء في السطر 19.
  • السطر 4: نظرًا لوجود طريقة مجردة، يتم إعلان الفئة نفسها على أنها مجردة باستخدام الكلمة المفتاحية abstract.
  • السطر 29: الطريقة المجردة identifies ليس لها تعريف. ستقوم الفئات المشتقة بتعريفها.
  • الأسطر 24-26: الطريقة ToString التي تحدد مثيلًا للفئة.

يُفترض هنا أن المطور يرغب في التحكم في إنشاء مثيلات لفئة User والفئات المشتقة، ربما لأنه يريد التأكد من إلقاء استثناء من نوع معين إذا لم يتم التعرف على المستخدم (السطر 19). يمكن للفئات المشتقة الاعتماد على هذا المنشئ. للقيام بذلك، يجب أن توفر الفئات المشتقة الأسلوب identifies.

فيما يلي فئة ExceptionUtilisateurInconnu:


using System;
 
namespace Chap2 {
    class ExceptionUtilisateurInconnu : Exception {
        public ExceptionUtilisateurInconnu(string message) : base(message){
        }
    }
}
  • السطر 3: مشتق من Exception
  • الأسطر 4-6: يحتوي على منشئ واحد يقبل رسالة خطأ كمعلمة. يتم تمرير هذه الرسالة إلى الفئة الأصلية (السطر 5)، التي تحتوي على نفس المنشئ.

نقوم الآن بتشتيت User في فئة Director الخاصة بالفتيات:


namespace Chap2 {
    class Administrateur : Utilisateur {
         // manufacturer
        public Administrateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         // identifies
        public override string identifie() {
             // identification LDAP
            // ...
            return "admin";
        }
    }
}
  • الأسطر 4-6: يقوم المنشئ ببساطة بتمرير المعلمات التي يتلقاها إلى فئته الأصلية
  • الأسطر 9-12: تحدد الطريقة الفئة Director. يُفترض أن يتم تحديد هوية المسؤول بواسطة نظام LDAP. تعيد هذه الطريقة تعريف هوية الفئة الأم. ونظرًا لأنها تعيد تعريف فئة مجردة، فلا فائدة من وضع الكلمة الرئيسية override.

نستمد الآن User في فئة Observer الخاصة بالفتيات:


namespace Chap2 {
    class Observateur : Utilisateur{
         // manufacturer
        public Observateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         //identifies
        public override string identifie() {
             // identification SGBD
            // ...
            return "observateur";
        }
 
    }
}
  • الأسطر 4-6: يقوم المنشئ ببساطة بتمرير المعلمات التي يتلقاها إلى فئته الأم
  • الأسطر 9-13: تحدد الطريقة فئة Observer. يُفترض أن المراقب يتم تحديده عن طريق التحقق من بيانات هويته في قاعدة البيانات.

في النهاية، يتم إنشاء مثيلات الكائنين Director و Observer بواسطة نفس المنشئ الخاص بالفئة الأصلية User. سيستخدم هذا المنشئ التعريفات التي توفرها هذه الفئات.

هناك فئة ثالثة هي Unknown تنحدر أيضًا من User :


namespace Chap2 {
    class Inconnu : Utilisateur{
 
         // manufacturer
        public Inconnu(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         //identifies
        public override string identifie() {
             // unknown user
            // ...
            return null;
        }
 
    }
}
  • السطر 13: تقوم الطريقة بتعيين المؤشر على القيمة "null" للإشارة إلى عدم التعرف على المستخدم.

قد يبدو برنامج الاختبار كما يلي:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Observateur("observer","mdp1"));
            Console.WriteLine(new Administrateur("admin", "mdp2"));
            try {
                Console.WriteLine(new Inconnu("xx", "yy"));
            } catch (ExceptionUtilisateurInconnu e) {
                Console.WriteLine("Utilisateur non connu : "+ e.Message);
            }
        }
    }
}

لاحظ أن الأسطر 6 و7 و9 تستخدم [User].ToString() التي ستستخدمها WriteLine.

والنتائج هي كما يلي:

1
2
3
Utilisateur[observer,mdp1,observateur]
Utilisateur[admin,mdp2,admin]
Utilisateur non connu : [xx,yy]

4.8. الفئات والواجهات والأساليب العامة

لنفترض أننا نريد كتابة طريقة لتبديل ترتيب عددين صحيحين. يمكن أن تكون هذه الطريقة كما يلي:


        public static void Echanger1(ref int value1, ref int value2){
            // on échange les références value1 et value2
            int temp = value2;
            value2 = value1;
            value1 = temp;
}

الآن، إذا أردنا تبديل مرجعين لكائنين من نوع Person، فسنكتب:


        public static void Echanger2(ref Personne value1, ref Personne value2){
            // on échange les références value1 et value2
            Personne temp = value2;
            value2 = value1;
            value1 = temp;
}

الفرق بين الطريقتين هو نوع T للمعلمات: int في Exchange1، Person في Exchange2. تلبي الفئات والواجهات العامة الحاجة إلى طرق لا تختلف إلا في نوع بعض معلماتها.

باستخدام فئة عامة، يمكن إعادة كتابة Exchange على النحو التالي:


namespace Chap2 {
    class Generic1<T> {
        public static void Echanger(ref T value1, ref T value2){
             // exchange the value1 and value2 references
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • السطر 2: يتم تحديد معلمات الفئة Generic1 بواسطة نوع يُشار إليه بـ T. يمكنك تسميته بأي اسم تريده. ثم يتم إعادة استخدام هذا النوع T في الفئة في السطرين 3 و 5. نقول إن Generic1 هي فئة عامة.
  • السطر 3: يحدد المرجعين من النوع T المراد تبديلهما
  • السطر 5: المتغير المؤقت temp له النوع T.

قد يكون برنامج الاختبار للفئة كما يلي:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic1<int>.Echanger(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic1<string>.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic1<Personne>.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
 
        }
    }
}
  • السطر 8: عند استخدام فئة عامة معلمة بالأنواع T1، T2، ... يجب "إنشاء مثيلات" لهذه الأنواع. السطر 8: استخدم الطريقة الثابتة Exchange من النوع Generic1<int> للإشارة إلى أن المراجع التي تم تمريرها إلى Exchange هي من النوع int.
  • السطر 12: يتم استخدام الطريقة الثابتة Exchange من النوع Generic1<string> للإشارة إلى أن المراجع التي تم تمريرها إلى Exchange هي من النوع string.
  • السطر 16: يتم استخدام الطريقة الثابتة Exchange من النوع Generic1<Person> للإشارة إلى أن المراجع التي تم تمريرها إلى Exchange هي من النوع Person.

النتائج هي كما يلي:

1
2
3
i1=2,i2=1
s1=s2,s2=s1
p1=[pauline, dard, 55],p2=[jean, clu, 20]

كان من الممكن أيضًا كتابة طريقة Exchange على النحو التالي:


namespace Chap2 {
    class Generic2 {
        public static void Echanger<T>(ref T value1, ref T value2){
             // exchange the value1 and value2 references
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • السطر 2: لم تعد الفئة Generic2 عامة
  • السطر 3: الطريقة الثابتة Exchange عامة

يصبح برنامج الاختبار كما يلي:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic2.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic2.Echanger<string>(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic2.Echanger<Personne>(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
        }
    }
}
  • الأسطر 8 و 12 و 16: استدعاء Exchange عن طريق تحديد نوع المعلمة في <>. في الواقع، يمكن للمترجم استنتاج متغير Exchange الذي يجب استخدامه. وبالتالي، فإن الإدخال التالي صحيح:

            Generic2.Echanger(ref i1, ref i2);
...
            Generic2.Echanger(ref s1, ref s2);
...
            Generic2.Echanger(ref p1, ref p2);

الأسطر 1 و 3 و 5: لم يعد متغير الأسلوب Exchange محددًا. يمكن للمترجم استنتاجه من طبيعة المعلمات الفعلية المستخدمة.

يمكن وضع قيود على المعلمات العامة:

Image

لننظر إلى الطريقة العامة الجديدة Exchange التالية:


namespace Chap2 {
    class Generic3 {
        public static void Echanger<T>(ref T value1, ref T value2) where T : class {
             // exchange the value1 and value2 references
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • السطر 3: يجب أن يكون النوع T مرجعًا (فئة، واجهة)

انظر إلى برنامج الاختبار التالي:


using System;
 
namespace Chap2 {
    class Program4 {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic3.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic3.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic3.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
 
        }
    }
}

يعلن المُترجم عن وجود خطأ في السطر 8 لأن النوع int ليس فئة أو واجهة، بل هو بنية:

Image

انظر إلى الطريقة العامة الجديدة Exchange التالية:


namespace Chap2 {
    class Generic4 {
        public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
             // retrieve the value of the 2 elements
            int value1 = element1.Value();
            int value2 = element2.Value();
             // if 1st element > 2nd element, exchange elements
            if (value1 > value2) {
                T temp = element2;
                element2 = element1;
                element1 = temp;
            }
        }
    }
}
  • السطر 3: يجب أن ينفذ النوع T الواجهة Interface1. ويحتوي على طريقة Value، المستخدمة في السطرين 5 و6، والتي تعطي قيمة الكائن من النوع T.
  • الأسطر 8-12: يتم تبادل المرجعين element1 و element2 فقط إذا كانت قيمة element1 أكبر من قيمة element2.

الواجهة Interface1 هي كما يلي:


namespace Chap2 {
    interface Interface1 {
        int Value();
    }
}

يتم تنفيذها بواسطة Class1 التالية:


using System;
using System.Threading;
 
namespace Chap2 {
    class Class1 : Interface1 {
         // object value
        private int value;
 
         // manufacturer
        public Class1() {
             // wait 1 ms
            Thread.Sleep(1);
             // random value between 0 and 99
            value = new Random(DateTime.Now.Millisecond).Next(100);
        }
 
         // accessor private field value
        public int Value() {
            return value;
        }
 
         // instance status
        public override string ToString() {
            return value.ToString();
        }
    }
}
  • السطر 5: Class1 تنفذ Interface1
  • السطر 7: قيمة مثيل Class1
  • الأسطر 10-14: يتم تهيئة قيمة الحقل بقيمة عشوائية بين 0 و 99
  • الأسطر 18-20: الطريقة Value للواجهة Interface1
  • الأسطر 23-25: الطريقة ToString للفئة

يتم أيضًا تنفيذ الواجهة Interface1 بواسطة Class2 :


using System;
 
namespace Chap2 {
    class Class2 : Interface1 {
         // object values
        private int value;
        private String s;
 
         // manufacturer
        public Class2(String s) {
            this.s = s;
            value = s.Length;
        }
 
         // accessor private field value
        public int Value() {
            return value;
        }
 
         // instance status
        public override string ToString() {
            return s;
        }
    }
}
  • السطر 4: Class2 تنفذ Interface1
  • السطر 6: قيمة مثيل Class2
  • الأسطر 10-13: يتم تهيئة قيمة الحقل بطول السلسلة التي تم تمريرها إلى المنشئ
  • الأسطر 16-18: الطريقة Value للواجهة Interface1
  • السطران 21-22: الطريقة ToString للفئة

قد يبدو برنامج الاختبار كما يلي:


using System;
 
namespace Chap2 {
    class Program5 {
        static void Main(string[] args) {
             // exchange instances of type Class1
            Class1 c1, c2;
            for (int i = 0; i < 5; i++) {
                c1 = new Class1();
                c2 = new Class1();
                Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
                Generic4.Echanger(ref c1, ref c2);
                Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
            }
             // exchange Class2 instances
            Class2 c3, c4;
            c3 = new Class2("xxxxxxxxxxxxxx");
            c4 = new Class2("xx");
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
            Generic4.Echanger(ref c3, ref c4);
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
        }
    }
}
  • الأسطر 8-14: يتم تبادل مثيلات Class1
  • الأسطر 16-22: يتم تبادل مثيلات النوع Class2

والنتائج هي كما يلي:

Avant échange --> c1=43,c2=79
Après échange --> c1=43,c2=79
Avant échange --> c1=72,c2=56
Après échange --> c1=56,c2=72
Avant échange --> c1=92,c2=75
Après échange --> c1=75,c2=92
Avant échange --> c1=11,c2=47
Après échange --> c1=11,c2=47
Avant échange --> c1=31,c2=67
Après échange --> c1=31,c2=67
Avant échange --> c3=xxxxxxxxxxxxxx,c4=xx
Après échange --> c3=xx,c4=xxxxxxxxxxxxxx

لتوضيح مفهوم واجهة عامة ( )، سنقوم بفرز مصفوفة من الأشخاص أولاً حسب أسمائهم، ثم حسب أعمارهم. الطريقة التي نستخدمها لفرز المصفوفة هي الطريقة الثابتة Spell في فئة Array:

Image

تذكر أن الطريقة الثابتة تُستخدم عن طريق إضافة اسم الفئة إلى بداية اسم الطريقة وليس اسم مثيل الفئة. تحتوي Spell على توقيعات مختلفة (وهي مُثقلة). سنستخدم التوقيع التالي:

public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

Spell هي طريقة عامة حيث يشير T إلى أي نوع. تتلقى الطريقة معلمتين:

  • T[] table : مصفوفة عناصر T المراد فرزها
  • IComparer<T> comparator : مرجع كائن ينفذ واجهة IComparer<T>.

IComparer<T> هي واجهة عامة تُعرَّف على النحو التالي:

1
2
3
public interface IComparer<T>{
    int Compare(T t1, T t2);
}

تحتوي الواجهة IComparer<T> على طريقة واحدة فقط. الطريقة Compare :

  • تستقبل عنصرين كمعلمات t1 و t2 من النوع T
  • تُرجع 1 إذا كان t1>t2، و0 إذا كان t1==t2، و-1 إذا كان t1<t2. يعود الأمر للمطور لإعطاء معنى لمشغلات < و== و>. على سبيل المثال، إذا كان p1 وp2 كائنين من نوع Person، فيمكننا القول أن p1>p2 إذا كان اسم p1 يسبق اسم p2 بالترتيب الأبجدي. سنقوم بعد ذلك بالفرز بترتيب تصاعدي حسب الاسم. إذا كنت تريد الفرز حسب العمر، فقل p1>p2 إذا كان عمر p1 أكبر من عمر p2.
  • للترتيب بترتيب تنازلي، ما عليك سوى عكس نتائج +1 و -1

لدينا ما يكفي لفرز جدول الأشخاص. البرنامج كالتالي:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program6 {
        static void Main(string[] args) {
             // a table of people
            Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
             // display
            Affiche("Tableau à trier", personnes1);
             // sort by name
            Array.Sort(personnes1, new CompareNoms());
             // display
            Affiche("Tableau après le tri selon les nom et prénom", personnes1);
             // sorted by age
            Array.Sort(personnes1, new CompareAges());
             // display
            Affiche("Tableau après le tri selon l'âge", personnes1);
        }
 
        static void Affiche(string texte, Personne[] personnes) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (Personne p in personnes) {
                Console.WriteLine(p);
            }
        }
    }
 
     // first and last name comparison class
    class CompareNoms : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
             // compare names
            int i = p1.Nom.CompareTo(p2.Nom);
            if (i != 0)
                return i;
             // equal names - first names are compared
            return p1.Prenom.CompareTo(p2.Prenom);
        }
    }
 
     // age comparison class
    class CompareAges : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
             // comparing ages
            if (p1.Age > p2.Age)
                return 1;
            else if (p1.Age == p2.Age)
                return 0;
            else
                return -1;
        }
    }
 
}
  • السطر 8: جدول الأشخاص
  • السطر 12: فرز جدول الأشخاص حسب الاسم الأول واسم العائلة. المعلمة الثانية للطريقة العامة Spell هي مثيل لـ CompareNoms الذي ينفذ IComparer<Person> العام.
  • الأسطر 30-39: فئة CompareNoms التي تنفذ الواجهة العامة IComparer<Person>.
  • الأسطر 31-38: تنفيذ الطريقة العامة int CompareTo(T,T) واجهة IComparer<T>. تستخدم الطريقة String.CompareTo، المقدمة في clipboard 3.3.5.4، لمقارنة سلسلتين.
  • السطر 16: فرز جدول الأشخاص حسب العمر. المعلمة الثانية للطريقة العامة Spell هي مثيل لـ CompareAges الذي ينفذ IComparer<Person> العام والمُعرَّف في الأسطر 42-51.

النتائج هي كما يلي:

Tableau à trier-----------------------------------
[claude, pollon, 25]
[valentine, germain, 35]
[paul, germain, 32]
Tableau après le tri selon les nom et prénom------
[paul, germain, 32]
[valentine, germain, 35]
[claude, pollon, 25]
Tableau après le tri selon l'âge------------------
[claude, pollon, 25]
[paul, germain, 32]
[valentine, germain, 35]

4.9. مساحات الأسماء

لكتابة سطر على الشاشة، نستخدم الأمر

Console.WriteLine(...)

إذا نظرنا إلى تعريف Console


Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)

نكتشف أنه جزء من System. وهذا يعني أنه يجب تعيين Console بواسطة System.Console ويجب أن نكتب في الواقع:

System.Console.WriteLine(...)

يمكن تجنب ذلك باستخدام using:

using System;
...
Console.WriteLine(...)

نقول إننا نستورد مساحة الاسم System باستخدام الجملة using. عندما يصادف المُترجم اسم فئة (هنا Console)، سيحاول العثور عليها في مساحات الأسماء المختلفة التي تم استيرادها بواسطة using. هنا سيجد الفئة Console في مساحة الاسم System. الآن دعونا نلاحظ المعلومة الثانية المرتبطة بالفئة Console:

Assembly: Mscorlib (in Mscorlib.dll)

يشير هذا السطر إلى "التجميع" الذي يوجد فيه تعريف الفئة Console. عند التحويل البرمجي خارج Visual Studio وتحتاج إلى إعطاء مراجع إلى ملفات dll المختلفة التي تحتوي على الفئات المراد استخدامها، يمكن أن تكون هذه المعلومات مفيدة. للإشارة إلى ملف dll المطلوب لتحويل فئة برمجياً، نكتب:

csc /r:fic1.dll /r:fic2.dll ... prog.cs

حيث يُعرف CSC بأنه مُترجم لغة C#. وعندما نقوم بإنشاء فئة، يمكننا إنشاؤها ضمن مساحة اسم. والغرض من مساحات الأسماء هذه هو تجنب تضارب الأسماء بين الفئات عند بيعها، على سبيل المثال. لنفترض أن هناك شركتين، E1 و E2، توزعان فئات مجمعة على التوالي في ملفات dll، e1.dll و e2.dll. لنفترض أن عميلاً C يشتري هاتين المجموعتين من الفئات التي حددت فيهما الشركتان فئة Person. يقوم العميل C بترجمة برنامج على النحو التالي:

csc /r:e1.dll /r:e2.dll prog.cs

إذا استخدم الملف المصدري prog.cs فئة Person، فلن يعرف المُجمِّع ما إذا كان عليه أخذ Person من e1.dll أم من e2.dll. وسيُشير إلى وجود خطأ. إذا حرصت الشركة E1 على إنشاء فئاتها في مساحة أسماء تسمى E1 والشركة E2 في مساحة أسماء تسمى E2، فستُسمى فئتا Person عندئذٍ E1.Person و E2.Personne. يجب على العميل استخدام E1.Personne أو E2.Personne وليس Person. يزيل مساحة الاسم أي غموض.

لإنشاء فئة في مساحة اسم، اكتب:

namespace EspaceDeNoms{
     // class definition
}

4.10. تطبيق نموذجي - الإصدار 2

نكرر حساب الضريبة الذي درسناه بالفعل في الفقرة 3.6 من الفصل السابق، ونعالجه الآن باستخدام الفئات والواجهات. دعونا نستذكر المشكلة:

نقترح كتابة برنامج لحساب ضريبة الدخل للمكلف. الحالة المبسطة هي حالة مكلف لا يعلن سوى عن راتبه (أرقام عام 2004 لدخل عام 2003):

  • يُحسب عدد حصص الموظف nbParts=nbEnfants/2 +1 إذا كان غير متزوج، و nbEnfants/2+2 إذا كان متزوجًا، حيث nbEnfants هو عدد أطفاله.
  • إذا كان لديه ثلاثة أطفال على الأقل، يحصل على نصف حصة إضافية
  • احسب دخلك الخاضع للضريبة R=0.72*S حيث S هو راتبه السنوي
  • احسب معامل الأسرة QF=R/nbParts
  • احسب ضريبتك I. انظر الجدول التالي:
4262
0
0
8382
0.0683
291.09
14753
0.1914
1322.92
23888
0.2826
2668.39
38868
0.3738
4846.98
47932
0.4262
6883.66
0
0.4809
9505.54

يحتوي كل سطر على 3 حقول. لحساب الضريبة I، ابحث عن السطر الأول حيث QF<=champ1. على سبيل المثال، إذا كان QF=5000، نجد السطر

    8382        0.0683        291.09

وبذلك، فإن الضريبة I تساوي 0.0683*R - 291.09*عدد_القطع. وإذا كان QF بحيث لا يتم التحقق أبدًا من صحة العلاقة QF<=champ1، يتم عندئذٍ استخدام معاملات السطر الأخير. وهنا:

    0                0.4809    9505.54

مما يعطي الضريبة I=0.4809*R - 9505.54*nbParts.

أولاً، نحدد بنية قادرة على تغليف صف من المصفوفة السابقة:


namespace Chap2 {
     // a tax bracket
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}
 

ثم نُعرّف واجهة IImpot قادرة على حساب الضريبة:


namespace Chap2 {
    interface IImpot {
        int calculer(bool marié, int nbEnfants, int salaire);
    }
}
  • السطر 3: طريقة حساب الضريبة بناءً على ثلاثة بيانات: ما إذا كان دافع الضرائب متزوجًا أم لا، وعدد الأطفال، والراتب

بعد ذلك، نحدد فئة مجردة تنفذ هذه الواجهة:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {
 
         // tax brackets required to calculate tax
         // come from an external source
 
        protected TrancheImpot[] tranchesImpot;
 
         // tAX CALCULATION
        public int calculer(bool marié, int nbEnfants, int salaire) {
             // calculating the number of shares
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
             // calculation of taxable income & family quota
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
             // tAX CALCULATION
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
             // return result
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
         }//calculate
     }//class
 
}
  • السطر 2: تنفذ الفئة AbstractImpot واجهة IImpot.
  • السطر 7: بيانات حساب الضريبة السنوية في شكل حقل محمي. لا تعرف الفئة AbstractImpot كيف سيتم تهيئة هذا الحقل. فهي تترك ذلك للفئات المشتقة. ولهذا السبب تم إعلانها على أنها مجردة (السطر 2)، لمنع أي إنشاء مثيل.
  • الأسطر 10-25: تنفيذ واجهة الحساب IImpot. لن تضطر الفئات المشتقة إلى إعادة كتابة هذه الطريقة. تعمل AbstractImpot كفئة تحليلية للفئات المشتقة. هذا هو المكان الذي نضع فيه ما هو مشترك بين جميع الفئات المشتقة.

يمكن إنشاء فئة تنفذ فئة IImpot عن طريق اشتقاق AbstractImpot. وهذا ما نقوم به الآن:


using System;
 
namespace Chap2 {
    class HardwiredImpot : AbstractImpot {
 
         // data tables for tax calculations
        decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
 
        public HardwiredImpot() {
                 // creation of tax bracket table
            tranchesImpot = new TrancheImpot[limites.Length];
                 // filling
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
     }// class
}// namespace

تُعرّف الفئة HardwiredImpot، في الأسطر 7-9، البيانات الثابتة المطلوبة لحساب الضريبة. ويستخدم مُنشئها (الأسطر 11-18) هذه البيانات لتهيئة الحقل المحمي tranchesImpot الخاص بالفئة الأم AbstractImpot.

قد يكون برنامج الاختبار كما يلي:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
             // interactive Tax calculation program
            // l'user types three data into keyboard: married nbEnfants salary
             // the program then displays Tax payable
 
            const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
                            + "Marié : o pour marié, n pour non marié\n"
                            + "NbEnfants : nombre d'enfants\n"
                            + "Salaire : salaire annuel en F";
 
             // creation of a IImpot object
            IImpot impot = new HardwiredImpot();
 
             // infinite loop
            while (true) {
                 // tax calculation parameters are requested
                Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
                string paramètres = Console.ReadLine().Trim();
                 // anything to do?
                if (paramètres == null || paramètres == "") break;
                 // check number of arguments in the input line
                string[] args = paramètres.Split(null);
                int nbParamètres = args.Length;
                if (nbParamètres != 3) {
                    Console.WriteLine(syntaxe);
                    continue;
                 }//if
                 // checking the validity of parameters
                 // married
                string marié = args[0].ToLower();
                if (marié != "o" && marié != "n") {
                    Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
                    continue;
                 }//if
                 // nbEnfants
                int nbEnfants = 0;
                bool dataOk = false;
                try {
                    nbEnfants = int.Parse(args[1]);
                    dataOk = nbEnfants >= 0;
                } catch {
                 }//if
                 // correct data?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }
                 // salary
                int salaire = 0;
                dataOk = false;
                try {
                    salaire = int.Parse(args[2]);
                    dataOk = salaire >= 0;
                } catch {
                 }//try-catch
                 // correct data?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }
                 // parameters are correct - Tax is calculated
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                 // next taxpayer
             }//while
        }
    }
}

يتيح البرنامج أعلاه للمستخدم إجراء محاكاة متكررة لحساب الضرائب.

  • السطر 16: إنشاء كائن tax الذي ينفذ واجهة IImpot. يتم الحصول على هذا الكائن عن طريق إنشاء مثيل لـ HardwiredImpot، وهو نوع ينفذ واجهة IImpot. لاحظ أننا لم نعط المتغير tax النوع HardwiredImpot بل واجهة IImpot. هذا يشير إلى أننا مهتمون فقط بكائن calculate وليس بالباقي.
  • الأسطر 19-68: حلقة محاكاة حساب الضريبة
  • السطر 22: يتم طلب المعلمات الثلاثة المطلوبة للطريقة calculate في سطر واحد يتم كتابته على لوحة المفاتيح.
  • السطر 26: تقوم الطريقة [string].Split(null) بتقسيم [string] إلى كلمات. يتم تخزين هذه الكلمات في مصفوفة args.
  • السطر 66: يستدعي كائن calculate tax الذي ينفذ IImpot.

فيما يلي مثال على كيفية تشغيل البرنامج:

Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d f
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros