Skip to content

4. کلاس‌ها، ساختارها، رابط‌ها

4.1. شیء در مثال

4.1.1. عمومی

اکنون برنامه‌نویسی شیءگرا را از طریق مثال‌ها بررسی خواهیم کرد. یک شیء یک موجودیت است که شامل داده‌هایی است که وضعیت آن را تعریف می‌کنند (که به آن‌ها فیلدها، ویژگی‌ها و غیره گفته می‌شود) و توابع (که به آن‌ها متدها گفته می‌شود). یک شیء بر اساس قالبی به نام کلاس ایجاد می‌شود:

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

از کلاس قبلی C1 می‌توانیم اشیاء متعددی مانند O1، O2، … ایجاد کنیم. همهٔ این‌ها دارای فیلدهای p1، p2، … و متدهای m3، m4، … خواهند بود. با این حال، آن‌ها مقادیر متفاوتی برای فیلد pi خواهند داشت و بنابراین هر یک وضعیت خاص خود را خواهند داشت. اگر o1 یک شیء از نوع C1 باشد، o1.p1 به خصوصیت p1 از o1 اشاره دارد و o1.m1 به متد m1 از O1 اشاره دارد.512ZQX.

بیایید یک مدل شیء اولیه را در نظر بگیریم: کلاس Personne.

4.1.2. ایجاد پروژهٔ C#

در مثال‌های قبلی، ما تنها یک فایل منبع در یک پروژه داشتیم: Program.cs. از این پس، می‌توانیم چندین فایل منبع در یک پروژه داشته باشیم. ما به شما نشان خواهیم داد که چگونه این کار را انجام دهید.

در [1]، یک پروژه جدید ایجاد کنید. در [2]، «Console Application» را انتخاب کنید. در [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]، مشخص کنید که می‌خواهید یک راه‌حل (solution) ایجاد کنید. یک راه‌حل مجموعه‌ای از پروژه‌ها است. در [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

تعریف کلاس Personne در فایل منبع [Personne.cs] به شرح زیر خواهد بود:


using System;

namespace Chap2 {
    public class Personne {
        // ویژگی‌ها
        private string prenom;
        private string nom;
        private int age;

        // روش
        public void Initialise(string P, string N, int age) {
            this.prenom = P;
            this.nom = N;
            this.age = age;
        }

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

}

این تعریف یک کلاس و در نتیجه یک نوع داده است. هنگامی که متغیرهایی از این نوع ایجاد می‌کنیم، به آنها اشیاء یا نمونه‌های کلاس می‌گوییم. بنابراین یک کلاس قالبی است که اشیاء از روی آن ساخته می‌شوند.

اعضای یا فیلدهای یک کلاس می‌توانند داده (ویژگی‌ها)، متدها (تابع‌ها) یا خواص باشند. خواص متدهای ویژه‌ای هستند که برای بازیابی یا تعیین مقدار ویژگی‌های یک شیء استفاده می‌شوند. این فیلدها ممکن است با یکی از سه کلیدواژه زیر همراه باشند:

privé
یک فیلد خصوصی فقط توسط متدهای داخلی کلاس قابل دسترسی است
public
یک فیلد عمومی از طریق هر متدی قابل دسترسی است، چه در داخل کلاس تعریف شده باشد چه نباشد
protégé
یک فیلد محافظت‌شده تنها از طریق متدهای داخلی کلاس یا توسط یک شیء مشتق‌شده قابل دسترسی است (مفهوم ارث‌بری را بعداً ببینید).

به‌طور کلی، داده‌های یک کلاس به‌صورت خصوصی اعلام می‌شوند، در حالی که متدها و ویژگی‌های آن به‌صورت عمومی اعلام می‌شوند. این بدان معناست که کاربر یک شیء (برنامه‌نویس)

  • دسترسی مستقیم به داده‌های خصوصی شیء نخواهد داشت
  • می‌تواند متدهای عمومی شیء را فراخوانی کند، به‌ویژه آنهایی که دسترسی به داده‌های خصوصی آن را فراهم می‌کنند.

سینتکس برای تعریف یک کلاس 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;
}

ترتیب اعلام ویژگی‌های خصوصی، محافظت‌شده و عمومی دلخواه است.

4.1.4. متد Initialise

بیایید به کلاس Person خود بازگردیم که به صورت زیر تعریف شده است:


using System;

namespace Chap2 {
    public class Personne {
        // ویژگی‌ها
        private string prenom;
        private string nom;
        private int age;

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

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

}

هدف روش Initialise چیست؟ چون nom، prenom و age داده‌های خصوصی کلاس Personne هستند، دستورات:

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

نامعتبر هستند. ما باید یک شیء از نوع Personne را از طریق یک متد عمومی مقداردهی اولیه کنیم. این وظیفه متد Initialise است. خواهیم نوشت:

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

نوتاسیون p1.Initialise معتبر است زیرا Initialise به‌صورت عمومی قابل دسترسی است.

4.1.5. اپراتور جدید

ترتیب دستورالعمل‌ها

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

نادرست است. دستور

    Personne p1;

p1 را به‌عنوان مرجعی به یک شیء از نوع Personne اعلام می‌کند. این شیء هنوز وجود ندارد و بنابراین p1 مقداردهی اولیه نمی‌شود. گویی که کسی بنویسد:

Personne p1=null;

که در آن کلمه کلیدی null صراحتاً نشان می‌دهد که متغیر p1 هنوز به هیچ شیئی ارجاع نمی‌دهد. وقتی سپس می‌نویسیم

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

ما در حال فراخوانی متد Initialise از شی‌ای هستیم که توسط p1 ارجاع شده است. با این حال، این شی هنوز وجود ندارد و کامپایلر خطا را گزارش خواهد کرد. برای اینکه p1 به یک شی ارجاع دهد، باید بنویسیم:

Personne p1=new Personne();

این کار یک شی از نوع Personne ایجاد می‌کند که هنوز مقداردهی اولیه نشده است: ویژگی‌های nom و prenom، که ارجاع به اشیایی از نوع String هستند، مقدار null را خواهند داشت، و age مقدار 0 را خواهد داشت. بنابراین یک مقداردهی پیش‌فرض وجود دارد. اکنون که p1 به یک شیء ارجاع می‌دهد، عبارت مقداردهی اولیه برای آن شیء

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

صحیح است.

4.1.6. کلمهٔ کلیدی 'this'

بیایید کد متد initialise را بررسی کنیم:


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

دستور this.prenom=p به این معنی است که ویژگی prenom از شیء فعلی (this) مقدار p را دریافت می‌کند. کلمه کلیدی this به شیء جاری اشاره دارد: شیئی که متدی که در حال اجراست در آن قرار دارد. چگونه این را می‌دانیم؟ بیایید ببینیم که شیء مرجع‌شده توسط p1 در برنامه فراخوانی‌کننده چگونه مقداردهی اولیه می‌شود:

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

این متد Initialise از شیء p1 است که فراخوانی می‌شود. هنگامی که در داخل این متد به شیء this ارجاع داده می‌شود، در واقع شیء p1 است که ارجاع داده می‌شود. روش Initialise می‌توانست به شکل زیر نیز نوشته شود:


        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 دیگر

بیایید دوباره کلاس Personne را در نظر بگیریم و متد زیر را به آن اضافه کنیم:


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

اکنون ما دو متد با نام Initialise داریم: این امر تا زمانی که پارامترهای متفاوتی را بپذیرند مجاز است. در اینجا نیز همین‌طور است. پارامتر اکنون یک مرجع (p) به یک شخص است. ویژگی‌های شخص (p) سپس به شیء فعلی (this) تخصیص داده می‌شوند. توجه داشته باشید که متد Initialise به طور مستقیم به ویژگی‌های شیء p دسترسی دارد، هرچند این ویژگی‌ها از نوع private هستند. این امر همیشه صادق است: یک شیء o1 از کلاس C همیشه به ویژگی‌های اشیاء از همان کلاس، C، دسترسی دارد.

در اینجا آزمایش کلاس جدید Personne آمده است:


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

سازنده (Constructor) متدی است که نام کلاس را حمل می‌کند و زمانی که شیء ایجاد می‌شود، فراخوانی می‌گردد. این متد عموماً برای مقداردهی اولیه شیء استفاده می‌شود. این یک متد است که می‌تواند آرگومان بپذیرد اما هیچ نتیجه‌ای را بازنمی‌گرداند. پروتوتایپ یا تعریف آن با هیچ نوع (حتی 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();

بیایید دو سازنده برای کلاس خود Personne ایجاد کنیم:


using System;

namespace Chap2 {
    public class Personne {
        // ویژگی‌ها
        private string prenom;
        private string nom;
        private int age;

        // سازنده‌ها
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Initialise(P);
        }

        // روش
        public void Initialise(string p, string n, int age) {
...
        }

        public void Initialise(Personne p) {
...
        }

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

}

هر دو کانستراکتور ما به سادگی متدهای Initialise که قبلاً در موردشان بحث شد را فراخوانی می‌کنند. به یاد داشته باشید که وقتی در داخل یک کانستراکتور، با نماد Initialise(p) مواجه می‌شویم، برای مثال، کامپایلر این را به this.Initialise(p) تبدیل می‌کند. بنابراین در داخل سازنده، متد Initialise برای عمل بر روی شی‌ای که توسط 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. اشاره‌های شیء

ما همیشه از همان کلاس Personne استفاده می‌کنیم. برنامهٔ آزمایشی به شرح زیر است:


using System;

namespace Chap2 {
    class Program2 {
        static void Main() {
            // p1
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Console.Write("p1="); p1.Identifie();
            // p2 به همان شیء اشاره می‌کند که p1 به آن اشاره می‌کند
            Personne p2 = p1;
            Console.Write("p2="); p2.Identifie();
            // p3 به شی‌ای اشاره می‌کند که کپی‌ای از شی مورد اشاره p1 خواهد بود
            Personne p3 = new Personne(p1);
            Console.Write("p3="); p3.Identifie();
            // وضعیت شیء مرجع p1 تغییر می‌کند
            p1.Initialise("Micheline", "Benoît", 67);
            Console.Write("p1="); p1.Identifie();
            // از آنجا که p2 = p1، حالت شی ارجاع‌شده توسط p2 باید تغییر کرده باشد
            Console.Write("p2="); p2.Identifie();
            // از آنجا که p3 به همان شیء p1 اشاره نمی‌کند، شیء مورد اشاره p3 نباید تغییر کرده باشد
            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 به شیء Personne("Jean","Dupont",30) اشاره می‌کند اما خود آن شیء نیست. در زبان C می‌گوییم که این یک اشارهگر (pointer) به نشانی شیء ایجادشده است. اگر سپس بنویسیم:

    p1=null;

این شیء Personne("Jean","Dupont",30) نیست که تغییر می‌کند؛ بلکه مرجع p1 است که مقدار آن تغییر می‌کند. شیء Personne("Jean","Dupont",30) «گم» خواهد شد اگر توسط هیچ متغیر دیگری ارجاع داده نشود.

وقتی می‌نویسید:

Personne p2=p1;

اشاره‌گر p2 مقداردهی اولیه می‌شود: این اشاره‌گر به همان شیئی اشاره می‌کند (یا به همان شیئی ارجاع می‌دهد) که اشاره‌گر p1 به آن اشاره می‌کند. بنابراین، اگر ما شیء «اشاره‌شده» (یا مرجع) توسط p1 را تغییر دهیم، در واقع شیء مرجع توسط p2 را نیز تغییر داده‌ایم.

وقتی می‌نویسیم:

Personne p3=new Personne(p1);

یک شیء جدید، Personne، ایجاد می‌شود. این شیء جدید توسط p3 ارجاع داده خواهد شد. اگر شیء «اشاره‌شده» (یا مرجع‌شده) توسط p1 را تغییر دهید، به هیچ وجه شیء مرجع‌شده توسط p3 را تغییر نمی‌دهید. این موضوع توسط نتایج به‌دست‌آمده نشان داده می‌شود.

4.1.11. گذراندن پارامترهای نوع مرجع شیء

در فصل قبل، بررسی کردیم که پارامترهای تابع چگونه منتقل می‌شوند وقتی که یک نوع سادهٔ C# را که توسط یک ساختار نشان داده می‌شود، نشان می‌دهند. NET. بیایید ببینیم وقتی پارامتر یک مرجع شیء است چه اتفاقی می‌افتد:


using System;
using System.Text;

namespace Chap1 {
    class P12 {
        public static void Main() {
            //مثال ۴
            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);
        }
    }
}
  • خط ۸: ۳ شیء از نوع StringBuilder را تعریف می‌کند. یک شیء از نوع StringBuilder در مجاورت یک شیء از نوع string. قرار دارد. هنگامی که یک شیء از نوع string دستکاری می‌شود، یک شیء جدید از نوع string بازگردانده می‌شود. بنابراین، در توالی کد:
string s="une chaîne";
s=s.ToUpperCase();

خط ۱ یک شیء string را در حافظه ایجاد می‌کند و s نشانی آن است. در خط ۲، s.ToUpperCase() یک شیء string دیگر را در حافظه ایجاد می‌کند. بنابراین، بین خطوط ۱ و ۲، مقدار s تغییر کرده است (اکنون به شیء جدید اشاره می‌کند). از سوی دیگر، کلاس StringBuilder اجازه می‌دهد یک رشته بدون ایجاد شیء دوم تبدیل شود. این همان مثالی است که در بالا داده شده است:

  • خط ۸: ۴ ارجاع به [sb0, sb1, sb2, sb3] که به اشیایی از نوع StringBuilder اشاره می‌کنند
  • خط ۱۰: با حالت‌های مختلف به متد ChangeStringBuilder پاس داده می‌شوند: sb0، sb1 با حالت پیش‌فرض، sb2 با کلیدواژه ref، sb3 با کلیدواژه out.
  • خطوط ۱۵–۲۲: یک متد با پارامترهای فرمال [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 – خط ۳ اصلاح شده است. این اصلاح را می‌توان از طریق sb0 – خط ۴ مشاهده کرد.
  • sb1 و sbf1 دو مرجع متمایز به یک شیء واحد هستند. مقدار sbf1 در داخل متد تغییر یافته و اکنون به یک شیء جدید اشاره می‌کند – خط ۳. این تغییر بر مقدار sb1 تأثیری ندارد، که همچنان به همان شیء اشاره می‌کند – خط ۴.
  • sb2 و sbf2 یک مرجع واحد به یک شی واحد هستند. مقدار sbf2 در داخل متد تغییر کرده و اکنون به یک شیء جدید اشاره می‌کند – خط ۳. از آنجایی که sbf2 و sb2 یک و همان موجودیت هستند، مقدار sb2 نیز تغییر کرده است، و sb2 به همان شیئی اشاره می‌کند که sbf2 به آن اشاره دارد – خطوط ۳ و ۴.
  • قبل از فراخوانی متد، sb3 هیچ ارزشی نداشت. پس از متد، sb3 مقدار sbf3 را دریافت می‌کند. بنابراین ما دو ارجاع به یک شیء واحد داریم – خطوط ۳ و ۴

4.1.12. ابجکت‌های موقت

در یک عبارت، می‌توانید صراحتاً سازنده (constructor) یک شیء را فراخوانی کنید: شیء ایجاد می‌شود، اما شما به آن دسترسی ندارید (برای مثال برای تغییر آن). این شیء موقت برای هدف ارزیابی عبارت ایجاد می‌شود و سپس دور ریخته می‌شود. فضای حافظه‌ای که اشغال کرده بود، بعداً به‌طور خودکار توسط برنامه‌ای به نام «جمع‌کننده زباله» بازیابی می‌شود که وظیفه‌اش بازیابی فضای حافظه‌ای است که توسط اشیایی که دیگر توسط داده‌ها در برنامه ارجاع داده نمی‌شوند، اشغال شده است.

بیایید برنامه آزمایشی جدید زیر را در نظر بگیریم:


using System;

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

و بیایید سازنده‌های کلاس Personne را طوری تغییر دهیم که پیامی را نمایش دهند:


        // سازنده‌ها
        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. متدهای خواندن و نوشتن ویژگی‌های خصوصی

ما متدهای لازم را به کلاس Personne برای خواندن یا تغییر وضعیت ویژگی‌های اشیاء اضافه می‌کنیم:


using System;

namespace Chap2 {
    public class Personne {
        // ویژگی‌ها
        private string prenom;
        private string nom;
        private int age;

        // سازنده‌ها
        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);
        }

        // روش
        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;
        }

        // دسترس‌کننده‌ها
        public String GetPrenom() {
            return prenom;
        }
        public String GetNom() {
            return nom;
        }
        public int GetAge() {
            return age;
        }

        //اصلاح‌کننده‌ها
        public void SetPrenom(String P) {
            this.prenom = P;
        }
        public void SetNom(String N) {
            this.nom = N;
        }
        public void SetAge(int age) {
            this.age = age;
        }

        // روش
        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. ویژگی‌ها

راه دیگری برای دسترسی به ویژگی‌های یک کلاس وجود دارد که ایجاد پراپرتی‌ها است. این‌ها به ما اجازه می‌دهند ویژگی‌های خصوصی را طوری دستکاری کنیم که گویی عمومی هستند.

کلاس زیر Personne را در نظر بگیرید که در آن گترها و سترهای قبلی با ویژگی‌های قابل‌خواندن/نوشتن جایگزین شده‌اند:


using System;

namespace Chap2 {
    public class Personne {
        // ویژگی‌ها
        private string prenom;
        private string nom;
        private int age;

        // سازنده‌ها
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Initialise(p);
        }

        // روش
        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;
        }

         // ویژگی‌ها
        public string Prenom {
            get { return prenom; }
            set {
                //آیا نام اول معتبر است؟
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//اگر
        }//نام

        public string Nom {
            get { return nom; }
            set {
                //آیا نام خانوادگی معتبر است؟
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
            }//اگر
        }//نام خانوادگی


        public int Age {
            get { return age; }
            set {
                //آیا سن معتبر است؟
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
            }//اگر
        }//سن

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

}

یک خاصیت به شما امکان می‌دهد مقدار یک ویژگی را بخوانید (get) یا آن را تنظیم (set) کنید. یک خاصیت به صورت زیر تعریف می‌شود:

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

که در آن Type باید نوع صفتی باشد که توسط این ویژگی مدیریت می‌شود. این ویژگی ممکن است دو متد به نام‌های get و set داشته باشد. متد get معمولاً مسئول بازگرداندن مقدار صفتی است که مدیریت می‌کند (می‌تواند مقدار دیگری بازگرداند؛ هیچ چیزی مانع از این کار نمی‌شود). روش set پارامتری به نام value می‌گیرد که معمولاً آن را به صفتی که مدیریت می‌کند اختصاص می‌دهد. این روش می‌تواند از این فرصت استفاده کند تا اعتبار مقدار دریافتی را بررسی کند و در صورت لزوم، اگر مقدار نامعتبر باشد، یک استثنا پرتاب کند. این کاری است که در اینجا انجام شده است.

این متدهای 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 + ")");

ما در پی به دست آوردن مقادیر ویژگی‌های Prenom، Nom و Age برای شخص p هستیم. این متد get این ویژگی‌ها است که سپس فراخوانی می‌شود و مقدار ویژگی‌ای را که مدیریت می‌کند، برمی‌گرداند.

در عبارت

        p.Age=56;

ما می‌خواهیم مقدار ویژگی Age را تنظیم کنیم. سپس متد set** این ویژگی فراخوانی می‌شود. این متد مقدار 56 را به عنوان پارامتر value** خود دریافت خواهد کرد.

یک ویژگی P از کلاس C که فقط متد get را تعریف می‌کند، ویژگی فقط-خواندنی (read-only) نامیده می‌شود. اگر c یک شیء از کلاس C باشد، عملیات c.P=value توسط کامپایلر رد خواهد شد.

اجرای برنامه آزمایشی قبلی نتایج زیر را تولید می‌کند:

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;
...

ترتیب ویژگی‌ها اهمیتی ندارد. در اینجا یک مثال آمده است.

یک سازندهٔ جدید بدون پارامتر به کلاس Personne اضافه می‌شود:


        public Personne() {
}

سازنده اعضا را مقداردهی اولیه نمی‌کند. این به عنوان سازنده پیش‌فرض شناخته می‌شود. از آن زمانی استفاده می‌شود که کلاس هیچ سازنده‌ای تعریف نکرده باشد.

کد زیر با استفاده از سینتکسی که در بالا نشان داده شده، یک شیء جدید از نوع Personne را ایجاد و مقداردهی اولیه می‌کند (خط ۶):


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);
        }
    }
}

در خط ۶ بالا، سازنده بدون پارامتر Personne() استفاده شده است. در این مورد خاص، می‌توانستیم به این صورت نیز بنویسیم


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

اما پرانتزها برای سازنده بدون پارامتر Personne() در این نحوی اجباری نیستند.

نتایج اجرای کد به شرح زیر است:

p2=(Arthur,Martin,7)

در بسیاری از موارد، متدهای get و set یک خاصیت صرفاً از یک فیلد خصوصی می‌خوانند و به آن می‌نویسند بدون هیچ پردازش بیشتری. در این سناریو، بنابراین می‌توان از یک خاصیت خودکار که به صورت زیر تعریف شده است استفاده کرد:

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

فیلد خصوصی مرتبط با خاصیت تعریف نمی‌شود. این فیلد به‌طور خودکار توسط کامپایلر ایجاد می‌شود. تنها از طریق خاصیت آن می‌توان به آن دسترسی داشت. بنابراین، به جای نوشتن:


    private string prenom;
...
     // ویژگی مرتبط
        public string Prenom {
            get { return prenom; }
            set {
                //آیا نام اول معتبر است؟
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//اگر
        }//نام

می‌توانیم بنویسیم:

public string Prenom {get; set;}

بدون اعلام میدان خصوصی prenom. تفاوت بین این دو ویژگی پیشین در این است که اولی اعتبار نام اول در set را بررسی می‌کند، در حالی که دومی چنین بررسی‌ای انجام نمی‌دهد.

استفاده از خاصیت خودکار Prenom معادل اعلام یک فیلد Prenom به صورت عمومی است:

public string Prenom;

ممکن است این سؤال پیش بیاید که آیا بین این دو اعلان تفاوتی وجود دارد یا خیر. اعلان public به عنوان یک فیلد کلاس توصیه نمی‌شود. این کار با مفهوم پنهان‌سازی وضعیت یک شیء، که باید خصوصی باشد و از طریق متدهای عمومی در دسترس قرار گیرد، در تضاد است.

اگر ویژگی خودکار به صورت virtuelle, اعلام شود، آنگاه می‌توان آن را در یک کلاس فرزند مجدداً تعریف کرد:


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

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

خط ۲ بالا: کلاس فرزند Class2 می‌تواند کدی را در set, شامل شود که اعتبار مقدار تخصیص‌یافته به ویژگی خودکار base.Prop از کلاس والد Class1 را بررسی می‌کند.

4.1.15. متدها و ویژگی‌های کلاس

فرض کنید بخواهیم تعداد اشیاء Personne ایجادشده در یک برنامه را بشماریم. می‌توانیم خودمان یک شمارنده را مدیریت کنیم، اما این خطر وجود دارد که اشیاء موقتی که اینجا و آنجا ایجاد می‌شوند را نادیده بگیریم. به نظر می‌رسد امن‌تر باشد که یک عبارت در سازندهای کلاس Personne قرار دهیم که شمارنده را افزایش دهد. مشکل، ارسال یک مرجع به این شمارنده است تا سازنده بتواند آن را افزایش دهد: باید یک پارامتر جدید به آن ارسال شود. همچنین می‌توان شمارنده را در تعریف کلاس گنجاند. از آنجا که این یک ویژگی خود کلاس است و نه یک نمونه خاص از آن، به شیوه‌ای متفاوت با استفاده از کلمه کلیدی static اعلام می‌شود:


        private static long nbPersonnes;

برای ارجاع به آن، Personne.nbPersonnes را می‌نویسیم تا نشان دهیم که این یک ویژگی از خود کلاس Personne است. در اینجا، ما یک ویژگی خصوصی ایجاد کرده‌ایم که از خارج کلاس به آن دسترسی مستقیم وجود ندارد. بنابراین یک ویژگی عمومی ایجاد می‌کنیم تا دسترسی به ویژگی کلاس nbPersonnes را فراهم کنیم. برای تعیین مقدار nbPersonnes، متد get این ویژگی نیازی به یک شیء خاص Personne ندارد: در واقع، nbPersonnes یک ویژگی از کل کلاس است. بنابراین، ما به یک ویژگی نیاز داریم که همچنین به عنوان static اعلام شده است:


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

که وقتی از خارج فراخوانی شود، از نحو Personne.NbPersonnes استفاده خواهد کرد. در اینجا یک مثال آمده است.

کلاس Personne به شکل زیر درمی‌آید:


using System;

namespace Chap2 {
    public class Personne {

        // ویژگی‌های کلاس
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }

        // ویژگی‌های نمونه
        private string prenom;
        private string nom;
        private int age;

        // سازنده‌ها
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
            nbPersonnes++;
        }
        public Personne(Personne p) {
            Initialise(p);
            nbPersonnes++;
        }

...
}

در خطوط ۲۰ و ۲۴، سازنده‌ها مقدار فیلد ایستا را در خط ۷ افزایش می‌دهند.

با برنامه زیر:


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) {
            // یک آرایه از افراد
            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);
            // نمایش
            foreach (Personne ami in amis) {
                ami.Identifie();
            }
        }
    }
}
  • خط ۷: یک آرایه با ۳ عنصر از نوع Personne ایجاد می‌کند. این ۳ عنصر در اینجا با مقادیر null و c.a.d مقداردهی اولیه شده‌اند، زیرا به هیچ شیئی ارجاع نمی‌دهند. بار دیگر، در معنایی کلی، وقتی به یک «آرایه از اشیاء» اشاره می‌کنیم، در واقع صرفاً منظورمان یک آرایه از ارجاعات به اشیاء است. ایجاد آرایهٔ اشیاء — که خود یک شیء است (همان‌طور که از حضور new پیداست) — هیچ شیء از نوع عناصر خود را ایجاد نمی‌کند: این کار باید به صورت جداگانه انجام شود.
  • خطوط ۸–۱۰: ایجاد ۳ شیء از نوع Personne
  • خطوط ۱۲–۱۴: نمایش محتویات آرایه amis

نتایج زیر به دست می‌آیند:

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

4.2. ارث‌بری به روش مثال

4.2.1. اطلاعات عمومی

در اینجا به مفهوم ارث‌بری می‌پردازیم. هدف از ارث‌بری «سفارشی‌سازی» یک کلاس موجود است تا نیازهای ما را برآورده کند. فرض کنید می‌خواهیم کلاسی به نام Enseignant ایجاد کنیم: معلم یک نوع خاص از افراد است. آن‌ها ویژگی‌هایی دارند که دیگران ندارند: برای مثال موضوعی که تدریس می‌کنند. اما آن‌ها همچنین ویژگی‌های مشترک همه افراد را دارند: نام، نام خانوادگی و سن. بنابراین معلم عضوی تمام‌عیار از کلاس Personne است اما ویژگی‌های اضافی نیز دارد. به جای ایجاد یک کلاس Enseignant از صفر، ترجیح می‌دهیم بر روی ساختار موجود کلاس Personne بنا کنیم و آن را با ماهیت خاص معلمان تطبیق دهیم. این مفهوم ارث‌بری است که به ما این امکان را می‌دهد.

برای بیان اینکه کلاس Enseignant از ویژگی‌های کلاس Personne ارث می‌برد، می‌نویسیم:

    public class Enseignant : Personne

کلاس Personne را کلاس والد (یا مادر) و کلاس Enseignant را کلاس مشتق (یا دختر) می‌نامند. یک شیء Enseignant تمام ویژگی‌های یک شیء Personne را داراست: دارای همان ویژگی‌ها و متدها است. این ویژگی‌ها و متدهای کلاس والد در تعریف کلاس فرزند تکرار نمی‌شوند؛ ما صرفاً ویژگی‌ها و متدهای افزوده‌شده توسط کلاس فرزند را مشخص می‌کنیم:

ما فرض می‌کنیم که کلاس Personne به صورت زیر تعریف شده است:


using System;

namespace Chap2 {
    public class Personne {

        // ویژگی‌های کلاس
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }

        //ویژگی‌های نمونه
        private string prenom;
        private string nom;
        private int age;

        // سازنده‌ها
        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)");
        }

        // ویژگی‌ها
        public string Prenom {
            get { return prenom; }
            set {
                //آیا نام اول معتبر است؟
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//اگر
        }//نام

        public string Nom {
            get { return nom; }
            set {
                //آیا نام خانوادگی معتبر است؟
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
            }//اگر
        }//نام خانوادگی

        public int Age {
            get { return age; }
            set {
                // سن معتبر است؟
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
            }//اگر
        }//سن

        // ویژگی
        public string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
        }
    }

}

متد Identifie با ویژگی فقط-خواندنی Identite که شخص را شناسایی می‌کند، جایگزین شده است. ما یک کلاس Enseignant را از کلاس Personne ارث می‌بریم:


using System;

namespace Chap2 {
    class Enseignant : Personne {
        // ویژگی‌ها
        private int section;

        // سازنده
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // بخش از طریق خاصیت Section ذخیره می‌شود
            Section = section;
            // ردیابی
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
        }//سازنده

        // خواص بخش
        public int Section {
            get { return section; }
            set { section = value; }
        }// بخش

    }
}

کلاس Enseignant به متدها و ویژگی‌های کلاس Personne اضافه می‌کند:

  • خط ۴: کلاس Enseignant از کلاس Personne ارث می‌برد
  • خط ۶: یک ویژگی section که شماره بخشی است که معلم در میان اعضای هیئت‌علمی به آن تعلق دارد (تقریباً یک بخش برای هر درس). این ویژگی خصوصی از طریق خاصیت عمومی Section در خطوط ۱۸–۲۱ قابل دسترسی است.
  • خط ۹: یک سازندهٔ جدید برای مقداردهی اولیهٔ تمام ویژگی‌های یک معلم

4.2.2. ایجاد یک شیء معلم

یک کلاس فرزند، سازندها را از کلاس والد خود به ارث نمی‌برد. بنابراین باید سازندهای خود را تعریف کند. سازند برای کلاس Enseignant به شرح زیر است:


        // سازنده
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            //بخش ذخیره شد
            Section = section;
            // ردیابی
            Console.WriteLine("Construction enseignant(string, string, int, int)");
}//سازنده

اعلام


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

مشخص می‌کند که سازنده چهار پارامتر می‌گیرد: prenom, nom, age، section و سه پارامتر (prenom,nom,age) را به کلاس پایه خود، در این مورد کلاس Personne، ارسال می‌کند. ما می‌دانیم که این کلاس دارای یک سازنده Person(string, string, int) است که امکان ساخت یک شخص را با استفاده از پارامترهای ورودی فراهم می‌کند، (prenom,nom,age). پس از تکمیل ساخت کلاس پایه، ساخت شی Enseignant با اجرای بدنه سازنده ادامه می‌یابد:

            //بخش ذخیره می‌شود
            Section = section;

توجه باید داشت که در سمت چپ علامت =، از ویژگی section شیء استفاده نشده، بلکه از ویژگی مرتبط Section استفاده شده است. این امر به سازنده (constructor) اجازه می‌دهد تا از هرگونه بررسی اعتبار که این متد ممکن است انجام دهد، بهره‌مند شود. این کار از قرار دادن این بررسی‌ها در دو مکان مختلف جلوگیری می‌کند: سازنده و ویژگی.

خلاصه اینکه، سازنده یک کلاس مشتق:

  • پارامترهایی را که برای ساخت خود نیاز دارد به کلاس پایه خود پاس می‌دهد
  • از پارامترهای دیگر برای مقداردهی اولیه ویژگی‌های خود استفاده می‌کند

ممکن بود ترجیح داده شود که به این صورت بنویسیم:


// سازنده
  public Enseignant(string prenom, string nom, int age, int section){
    this.prenom=prenom;
        this.nom=nom;
        this.age=age;
      this.section=section;
  }

این امکان‌پذیر نیست. کلاس Personne سه فیلد خود prenom، nom و age را به‌عنوان خصوصی (private) اعلام کرده است. فقط اشیاء همان کلاس به این فیلدها دسترسی مستقیم دارند. تمام اشیاء دیگر، از جمله اشیاء فرزند در این مورد، باید برای دسترسی به آن‌ها از متدهای عمومی استفاده کنند. این وضعیت متفاوت بود اگر کلاس Personne سه فیلد را به صورت محافظت‌شده (protected) اعلام می‌کرد (protected): در آن صورت به کلاس‌های مشتق اجازه دسترسی مستقیم به این سه فیلد داده می‌شد. در مثال ما، استفاده از سازنده کلاس والد بنابراین راه‌حل صحیح بود و این رویکرد استاندارد است: هنگام ساخت یک شیء فرزند، ابتدا سازنده شیء والد را فراخوانی می‌کنیم و سپس مراحل اولیه را که مختص شیء فرزند است (section در مثال ما) تکمیل می‌کنیم.

بیایید یک برنامه آزمایشی اول را امتحان کنیم، [Program.cs]:


using System;

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

این برنامه به سادگی یک شیء Enseignant (new) ایجاد می‌کند و آن را شناسایی می‌کند. کلاس Enseignant متد Identite را ندارد، اما کلاس والد آن این متد را دارد که آن نیز عمومی است: از طریق ارث‌بری، این متد به یک متد عمومی در کلاس Enseignant تبدیل می‌شود.

پروژه کامل به شرح زیر است:

نتایج حاصل به شرح زیر است:

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

می‌توانیم ببینیم که:

  • یک شیء Personne (خط ۱) قبل از شیء Enseignant (خط ۲) ایجاد شده است
  • هویت به‌دست‌آمده، هویت شیء Personne است

4.2.3. تعریف مجدد یک متد یا خاصیت

در مثال قبلی، ما شناسه بخش Personne معلم را داشتیم، اما جزئیات خاصی که مربوط به کلاس Enseignant (بخش) است، وجود ندارد. بنابراین برای شناسایی معلم، باید یک ویژگی تعریف کنیم:


using System;

namespace Chap2 {
    class Enseignant : Personne {
        // ویژگی‌ها
        private int section;

        // سازنده
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // بخش از طریق خاصیت Section ذخیره می‌شود
            Section = section;
            //ردیابی
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
        }//سازنده

        // خاصیت Section
        public int Section {
            get { return section; }
            set { section = value; }
        }//بخش

        // خواص هویت
        public new string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
        }
    }
}

خطوط ۲۴–۲۶: ویژگی Identite از کلاس Enseignant بر اساس ویژگی Identite از کلاس والد آن (base.Identite) است. (خط ۲۵) برای نمایش بخش «Personne» آن، سپس این بخش را با فیلد section که مختص کلاس Enseignant است، تکمیل می‌کند. توجه داشته باشید اعلان خاصیت Identite:


    public new string Identite{

بیایید یک شیء E از نوع enseignant را در نظر بگیریم. این شیء شامل یک شیء Personne است:

ویژگی Identite هم در کلاس Enseignant و هم در کلاس والد آن، Personne، تعریف شده است. در کلاس فرزند Enseignant، ویژگی Identite باید با کلمه کلیدی 'new' پیش‌فرض قرار گیرد تا نشان دهد که یک ویژگی جدید، Identite، برای کلاس Enseignant. مجدداً تعریف می‌شود.


    public new string Identite{

کلاس Enseignant اکنون دارای دو ویژگی Identite است:

  • یکی ارث‌برده از کلاس والد Personne
  • خودش

اگر E یک شیء از نوع Enseignant باشد، آنگاه E.Identite به ویژگی Identite از کلاس Enseignant اشاره می‌کند. گفته می‌شود که ویژگی Identite در کلاس فرزند، ویژگی Identite در کلاس والد را بازنویسی یا مخفی می‌کند. به‌طور کلی، اگر O یک شیء و M یک متد باشد، برای اجرای متد O.M، سیستم به دنبال متد M به ترتیب زیر می‌گردد:

  • در کلاس شیء O
  • در کلاس والد آن، اگر وجود داشته باشد
  • در کلاس والد کلاس والد آن، اگر وجود داشته باشد
  • و غیره…

وراثت بنابراین اجازه می‌دهد که متدها و ویژگی‌هایی با همان نامِ متدها و ویژگی‌های کلاس والد، در کلاس فرزند دوباره تعریف شوند. این امر است که کلاس فرزند را قادر می‌سازد تا با نیازهای خود سازگار شود. ترکیب این قابلیت با چندریختی (polymorphism) که کمی بعد به آن خواهیم پرداخت، اصلی‌ترین مزیت وراثت است.

بیایید همان برنامه آزمایشی قبلی را در نظر بگیریم:


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 جای دارد.

این واقعیت که یک متغیر Oi از کلاس Ci در واقع می‌تواند نه تنها به یک شیء از کلاس Ci بلکه به هر شیئی که از کلاس Ci, مشتق شده است، اشاره کند، به عنوان چندریختی (polymorphism) شناخته می‌شود: توانایی یک متغیر برای اشاره به انواع مختلف اشیاء.

بیایید یک مثال بزنیم و تابع زیر را در نظر بگیریم که به هیچ کلاسی وابسته نیست (static):

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

ما می‌توانیم به همان آسانی بنویسیم

    Personne p;
    ...
    Affiche(p);

به عنوان

    Enseignant e;
    ...
    Affiche(e);

در مورد دوم، پارامتر رسمی p از نوع Personne در متد استاتیک Affiche مقدار از نوع Enseignant به آن اختصاص داده خواهد شد. از آنجا که نوع Enseignant از نوع Personne مشتق شده است، این امر معتبر است.

4.2.5. تعریف مجدد و چندریختی

بیایید متد Affiche خود را تکمیل کنیم:


        public static void Affiche(Personne p) {
            // شناسایی p را نمایش می‌دهد
            Console.WriteLine(p.Identite);
}//نمایش‌ها

ویژگی p.Identite رشته‌ای را بازمی‌گرداند که شیء Person با نام p را شناسایی می‌کند. در مثال قبلی چه اتفاقی می‌افتد اگر پارامتر ارسال‌شده به متد Affiche شئ‌ای از نوع Enseignant باشد:


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

بیایید به مثال زیر نگاه کنیم:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // یک معلم
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
            // یک شخص
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }

        // نمایش می‌دهد
        public static void Affiche(Personne p) {
            // هویت p را نمایش می‌دهد
            Console.WriteLine(p.Identite);
        }//نمایش می‌دهد
    }
}

نتایج به‌دست‌آمده به شرح زیر است:

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) هر بار ابتدا خاصیت Identite از یک Personne را اجرا کرد. (خط ۷) شخص موجود در e از Enseignant، و سپس (خط ۱۰) خود p از Personne. این به شیء ارسال‌شده به‌عنوان پارامتر به Affiche تطبیق نیافت. ما ترجیح می‌دادیم که هویت کامل Enseignant e را داشته باشیم. برای اینکه این اتفاق می‌افتاد، باید نشانه p.Identite به ویژگی Identite شیئی که در واقع توسط p به آن اشاره می‌شود، ارجاع می‌داد، نه به ویژگی Identite بخشی "Personne" از شیء‌ای که در واقع توسط p به آن اشاره می‌شود.

این نتیجه را می‌توان با اعلام Identite به‌عنوان یک ویژگی مجازی در کلاس پایه Personne به‌دست آورد:


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

کلمه کلیدی virtual باعث می‌شود Identite یک ویژگی مجازی باشد. این کلمه کلیدی همچنین می‌تواند به متدها نیز اعمال شود. کلاس‌های فرزند که یک ویژگی یا متد مجازی را بازتعریف می‌کنند، باید به جای **new از کلمه کلیدی override** برای مشخص کردن ویژگی یا متد بازتعریف‌شده خود استفاده کنند. بنابراین، در کلاس Enseignant، ویژگی Identite به شرح زیر بازتعریف می‌شود:


        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]

این بار، در خط ۳، ما در واقع هویت کامل معلم را به دست آورده‌ایم. اکنون بیایید به جای یک خاصیت، یک متد را مجدداً تعریف کنیم. کلاس object (نام مستعار C# برای System.Object) کلاس «پدر» تمام کلاس‌های C# است. بنابراین، هنگامی که می‌نویسیم:

    public class Personne

ما به‌طور ضمنی در حال نوشتن این هستیم:

    public class Personne : System.Object

کلاس System.Object یک متد مجازی ToString را تعریف می‌کند:

متد ToString نام کلاسی را که شیء به آن تعلق دارد بازمی‌گرداند، همان‌طور که در مثال زیر نشان داده شده است:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // یک معلم
            Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
            // یک شخص
            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 را در کلاس‌های Personne و Enseignant مجدداً تعریف نکرده‌ایم، با این حال می‌توانیم ببینیم که متد ToString در کلاس Object توانست نام واقعی کلاس شیء را نمایش دهد.

بیایید متد ToString را در کلاس‌های Personne و Enseignant مجدداً تعریف کنیم:


        // روش ToString
        public override string ToString() {
            return Identite;
}

تعریف در هر دو کلاس یکسان است. برنامهٔ آزمون زیر را در نظر بگیرید:


using System;
namespace Chap2 {
    class Program3 {
        public static void Main() {
            // یک معلم
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
            // یک شخص
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
        // پوستر
        public static void Affiche(Personne p) {
            // پوستر هویت
            Console.WriteLine(p);
        }//پوستر
    }
}

بیایید بر متد Affiche تمرکز کنیم که یک شخص p را به عنوان پارامتر می‌پذیرد. در خط ۱۵، متد WriteLine از کلاس Console هیچ واریانتی ندارد که پارامتری از نوع Personne را بپذیرد. در میان گونه‌های مختلف Writeline، یکی وجود دارد که پارامتری از نوع Object را می‌پذیرد. کامپایلر از این متد، WriteLine(Object o)، استفاده خواهد کرد، زیرا این امضا به این معناست که پارامتر o می‌تواند از نوع Object یا یک نوع مشتق باشد. از آنجایی که Object سوپرکلاس (superclass) تمام کلاس‌ها است، هر ابجکتی را می‌توان به عنوان پارامتر به WriteLine ارسال کرد، و بنابراین یک ابجکت از نوع Personne یا Enseignant. متد WriteLine(Object o) توابع o.ToString() را در جریان خروجی Out می‌نویسد. از آنجایی که متد ToString مجازی است، اگر شیء o (از نوع Object یا یک نوع مشتق شده) متد ToString را بازنویسی (override) کرده باشد، متد دوم مورد استفاده قرار خواهد گرفت. این مورد در اینجا برای کلاس‌های Personne و Enseignant صدق می‌کند.

این موضوع توسط نتایج اجرای کد نشان داده شده است:

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) ترجمه می‌کند. نوع بازگشتی متد operator مهم است. در واقع، عملیات 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 یک شیء Personne خواهد بود.
  • ویژگی L.Count که تعداد عناصر لیست L را نشان می‌دهد
  • نشانه‌گذاری L[i] که به عنصر i-ام لیست L اشاره دارد

کلاس ListeDePersonnes تمام ویژگی‌ها، متدها و خواص کلاس ArrayList را به ارث خواهد برد. تعریف آن به شرح زیر است:


using System;
using System.Collections;
using System.Text;

namespace Chap2 {
    class ListeDePersonnes : ArrayList{
        // تعریف مجدد عملگر «+»، برای افزودن یک شخص به فهرست
        public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
            // فرد p به فهرست ListeDePersonnes اضافه می‌شود
            l.Add(p);
            // فهرست ListeDePersonnes ایجاد می‌شود
            return l;
        }// عملیات +

        //ToString
        public override string ToString() {
            // مقدار بازگشتی (element1, element2, ..., elementn) است
            // پرانتز باز
            StringBuilder listeToString = new StringBuilder("(");
            // در میان فهرست افراد (این) تکرار کنید
            for (int i = 0; i < Count - 1; i++) {
                listeToString.Append(this[i]).Append(",");
            }//برای
            // آخرین عنصر
            if (Count != 0) {
                listeToString.Append(this[Count-1]);
            }
            // کروشه بسته‌شده
            listeToString.Append(")");
            // ما باید یک رشته را بازگردانیم
            return listeToString.ToString();
        }//ToString
    }
}
  • خط ۶: کلاس ListeDePersonnes از کلاس ArrayList ارث می‌برد
  • خطوط ۸–۱۳: تعریف عملگر + برای عملیات l + p، که در آن l از نوع ListeDePersonnes و p از نوع Personne یا یک نوع مشتق است.
  • خط ۱۰: شخص p به لیست l اضافه می‌شود. در اینجا از متد Add کلاس والد ArrayList استفاده شده است.
  • خط ۱۲: ارجاع به لیست l بازگردانده می‌شود تا بتوان عملگرهای + را پشت سر هم قرار داد، مانند l + p1 + p2. عملیات l+p1+p2 (طبق اولویت‌بندی عملگرها) به صورت (l+p1)+p2 تفسیر خواهد شد. عملیات l+p1 ارجاع l را بازخواهد گرداند. سپس عملیات (l+p1)+p2 به l+p2 تبدیل می‌شود که شخص p2 را به لیست افراد l اضافه می‌کند.
  • خط ۱۶: ما متد ToString را مجدداً تعریف می‌کنیم تا فهرستی از افراد را به شکل (فرد۱، فرد۲، ..) که در آن personnei خود نتیجه متد ToString از کلاس Personne است.
  • خط ۱۹: ما از یک شیء از نوع StringBuilder استفاده می‌کنیم. این کلاس در مواقعی که نیاز به انجام عملیات متعدد روی رشتهٔ کاراکتری باشد، مناسب‌تر از کلاس string است، در این مورد الحاق. این به آن دلیل است که هر عملیاتی روی یک شیء string یک شیء جدید string را بازمی‌گرداند، در حالی که همان عملیات روی یک شیء StringBuilder، شیء را بدون ایجاد یک شیء جدید، تغییر می‌دهد. ما از متد Append برای الحاق رشته‌ها استفاده می‌کنیم.
  • خط ۲۱: ما روی عناصر لیست افراد حلقه می‌زنیم. این لیست در اینجا با `this` اشاره می‌شود. این شیء فعلی است که متد `ToString` روی آن اجرا می‌شود. ویژگی `Count` یک ویژگی از کلاس والد `ArrayList` است.
  • خط ۲۲: عنصر شمارهٔ i از لیست فعلی this را می‌توان با استفاده از نگاشت this[i] دسترسی پیدا کرد. باز هم این یک ویژگی کلاس ArrayList است. از آنجا که ما در حال الحاق رشته‌ها هستیم، متد this[i].ToString() استفاده خواهد شد. از آنجایی که این متد مجازی است، متد ToString از شیء this، از نوع Personne یا یک نوع مشتق، استفاده خواهد شد.
  • خط ۳۱: باید یک شیء از نوع string (خط ۱۶) بازگردانیم. کلاس StringBuilder دارای متدی به نام ToString است که امکان تبدیل از نوع StringBuilder به نوع string را فراهم می‌کند.

شایان ذکر است که کلاس ListeDePersonnes هیچ سازنده‌ای ندارد. در این مورد، می‌دانیم که سازنده‌ی

public ListeDePersonnes(){
}

استفاده خواهد شد. این سازنده کاری جز فراخوانی سازنده بدون پارامتر کلاس والد خود انجام نمی‌دهد:

public ArrayList(){
...
}

یک کلاس آزمایشی ممکن است به این شکل باشد:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // فهرستی از افراد
            ListeDePersonnes l = new ListeDePersonnes();
            // افزودن افراد
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
            //نمایش
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
        }
    }
}
  • خط ۷: ایجاد یک لیست از افراد
  • خط ۹: افزودن ۲ نفر با استفاده از عملگر +
  • خط ۱۲: افزودن یک معلم
  • خطوط ۱۱ و ۱۳: استفاده از متد بازتعریف‌شده 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، هم برای خواندن (شخص p=l[i]) و در نوشتن (l[i]=new Person(...))

برای اینکه بتوانیم l[i] را بنویسیم، جایی که l[i] به یک شیء Personne اشاره می‌کند، باید متد this زیر را در کلاس ListeDePersonnes تعریف کنیم:


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

ما متد this[int i] را یک ایندکس‌ساز می‌نامیم زیرا به عبارت obj[i] که شبیه نگارش آرایه است، معنا می‌بخشد، هرچند obj یک آرایه نیست بلکه یک شیء است. متد get از متد this ازشیء obj زمانی فراخوانی می‌شود که عبارت variable=obj[i] نوشته شود، و متد set زمانی که obj[i]=value نوشته شود.

کلاس ListeDePersonnes از کلاس ArrayList ارث می‌برد، که خود دارای یک ایندکس‌کننده است:

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

یک تداخل بین متد this از کلاس ListeDePersonnes وجود دارد:


 public Personne this[int i] 

و متد this از کلاس ArrayList


 public object this[int i] 

زیرا نام یکسانی دارند و هر دو از نوع پارامتر یکسان (int) استفاده می‌کنند. برای نشان دادن اینکه متد this از کلاس ListeDePersonnes، متد هم‌نام در کلاس ArrayList را «پنهان» می‌کند، باید کلمه کلیدی new را به اعلان ایندکس‌کننده برای ListeDePersonnes اضافه کنیم. بنابراین خواهیم نوشت:


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

بیایید این متد را تکمیل کنیم. متد this.get زمانی فراخوانی می‌شود، برای مثال، وقتی ما variable=l[i] را می‌نویسیم، که در آن l از نوع ListeDePersonnes است. سپس باید شخص شماره i را از فهرست l بازگردانیم. این کار با استفاده از نشانه base[i] انجام می‌شود که شیء در اندیس i از کلاس ArrayList را به یک کلاس پایه برای کلاس ListeDePersonnes تبدیل می‌کند. از آنجایی که شیء بازگشتی از نوع Object است، نیاز به تبدیل نوع به کلاس Personne وجود دارد.


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

متد set زمانی فراخوانی می‌شود که l[i]=p نوشته شود، جایی که p یک Personne است. این شامل تخصیص فرد 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["nom"], c.a.d را بنویسیم و لیست l را نه با شماره عنصر بلکه با نام شخص ایندکس کنیم. برای این کار، ما یک ایندکس‌کننده جدید تعریف می‌کنیم:


        // جستجو بر اساس نام
        public int this[string nom] {
            get {
                // جستجوی یک شخص
                for (int i = 0; i < Count; i++) {
                    if (((Personne)base[i]).Nom == nom)
                        return i;
                }//برای
                return -1;
            }//دریافت
}

خط اول


public int this[string nom]

نشان می‌دهد که کلاس ListeDePersonnes با رشته کاراکتری nom نمایه شده است و نتیجه l[nom] یک عدد صحیح است. این عدد صحیح، موقعیت فرد با نام nom در لیست خواهد بود، یا در صورتی که آن فرد در لیست نباشد، مقدار -1 را خواهد داشت. ما تنها خاصیت get, را تعریف می‌کنیم و بدین ترتیب از تخصیص l["nom"]=value جلوگیری می‌کنیم، که این کار مستلزم تعریف خاصیت set بود. کلمه کلیدی new در اعلان ایندکس‌کننده لازم نیست زیرا کلاس پایه ArrayList ایندکس‌کننده‌ای به نام this[string] تعریف نمی‌کند.

در بدنه get، لیست افراد برای یافتن نام ارسال‌شده به‌عنوان پارامتر جستجو می‌شود. اگر نام در موقعیت i یافت شود، عدد i بازگردانده می‌شود؛ در غیر این صورت، عدد -1 بازگردانده می‌شود.

برنامه آزمون قبلی به شرح زیر گسترش می‌یابد:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // فهرستی از افراد
            ListeDePersonnes l = new ListeDePersonnes();
            // افزودن افراد
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
            //نمایش
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
            // تغییر آیتم ۱
            l[1] = new Personne("franck", "gallon",5);
            //نمایش آیتم 1
            Console.WriteLine("l[1]=" + l[1]);
            //نمایش لیست l
            Console.WriteLine("l=" + l);
            // جستجوی افراد
            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");
            }//برای
        }
    }
}

اجرای این کد نتایج زیر را تولید می‌کند:

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{
// ویژگی‌ها
    ...
// ویژگی‌ها
...
// سازنده‌ها
...
// روش‌ها
...
}

با وجود شباهت در نحوه تعریف، تفاوت‌های قابل توجهی بین کلاس‌ها و ساختارها وجود دارد. برای مثال، مفهوم ارث‌بری در ساختارها وجود ندارد. اگر در حال نوشتن کلاسی هستیم که قرار نیست از کلاسی دیگر مشتق شود، چه تفاوت‌هایی بین ساختارها و کلاس‌ها وجود دارد که به ما در انتخاب بین این دو کمک کند؟ بیایید برای پی بردن به این موضوع، از مثال زیر استفاده کنیم:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // یک ساختار sp1
            SPersonne sp1;
            sp1.Nom = "paul";
            sp1.Age = 10;
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            // یک ساختار sp2
            SPersonne sp2 = sp1;
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
            // sp2 اصلاح شده است
            sp2.Nom = "nicole";
            sp2.Age = 30;
            // تأیید sp1 و sp2
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");

            //یک شیء op1
            CPersonne op1=new CPersonne();
            op1.Nom = "paul";
            op1.Age = 10;
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            //یک شیء op2
            CPersonne op2=op1;
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
            //op2 اصلاح شده است
            op2.Nom = "nicole";
            op2.Age = 30;
            //بررسی op1 و op2
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
        }
    }
    //ساختار SPersonne
    struct SPersonne {
        public string Nom;
        public int Age;
    }

    //کلاس CPersonne
    class CPersonne {
        public string Nom;
        public int Age;
    }

}
  • خطوط ۳۸–۴۱: یک ساختار با دو فیلد عمومی: Nom, Age
  • خطوط ۴۴–۴۷: یک کلاس با دو فیلد عمومی: Nom, Age

اگر این برنامه اجرا شود، نتایج زیر به دست می‌آید:

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)

جایی که قبلاً از کلاس Personne استفاده می‌کردیم، اکنون از ساختار SPersonne استفاده می‌کنیم:


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

این ساختار در اینجا کَنستراکتور ندارد. می‌تواند یکی داشته باشد، همان‌طور که بعداً نشان خواهیم داد. به‌طور پیش‌فرض، همیشه یک کَنستراکتور بدون پارامتر دارد، در این مورد SPersonne() است.

  • خط ۷ کد: اعلام

    SPersonne sp1;

معادل عبارت زیر است:


    SPersonne sp1=new Spersonne();

یک ساختار (Name,Age) ایجاد می‌شود و مقدار sp1 خودِ همین ساختار است. در مورد یک کلاس، شیء (Name,Age) باید صراحتاً با استفاده از اپراتور new (خط ۲۲) ایجاد شود:


CPersonne op1=new CPersonne();

دستور قبلی یک شیء CPersonne (تقریباً معادل ساختار ما) ایجاد می‌کند و مقدار p1 سپس آدرس (اشاره) این شیء است.

خلاصه

  • در مورد ساختار، مقدار sp1 خودِ ساختار است
  • در مورد کلاس، مقدار op1 نشانی شیء ایجادشده است

وقتی در برنامه، خط ۱۲ می‌خواند:


            SPersonne sp2 = sp1;

یک ساختار جدید sp2(Name, Age) ایجاد شده و با مقدار sp1,، یعنی خود ساختار، مقداردهی اولیه می‌شود.

ساختار در sp1 در sp2 به صورت [1] کپی می‌شود. این یک کپی به صورت مقدار است. اکنون دستور در خط 27 را بررسی کنیم:


CPersonne op2=op1;

در مورد کلاس‌ها، مقدار op1 به op2 کپی می‌شود، اما از آنجا که این مقدار در واقع نشانی شیء است، خود شیء دوپلیکه نمی‌شود: [2].

در مورد ساختار [1]، اگر مقدار sp2 را تغییر دهیم، مقدار sp1 تغییر نمی‌کند، همانطور که در برنامه نشان داده شده است. در مورد شیء [2]، اگر شیئی که توسط op2 به آن اشاره می‌شود تغییر یابد، شیئی که توسط op1 به آن اشاره می‌شود نیز تغییر می‌یابد، زیرا آن‌ها یکی هستند. این موضوع نیز توسط نتایج برنامه نشان داده شده است.

بنابراین می‌توانیم از این توضیحات نتیجه بگیریم که:

  • مقدار یک متغیر از نوع «ساختار» خودِ ساختار است
  • مقدار یک متغیر از نوع 'object' نشانی شیئی است که به آن اشاره می‌کند

هنگامی که این تفاوت اساسی درک شود، ساختار بسیار شبیه به یک کلاس از آب درمی‌آید، همانطور که مثال جدید زیر نشان می‌دهد:


using System;

namespace Chap2 {

    //ساختار SPersonne
    struct SPersonne {
        // ویژگی‌های خصوصی
        private string nom;
        private int age;

        // ویژگی‌ها
        public string Nom {
            get { return nom; }
            set { nom = value; }
        }//نام

        public int Age {
            get { return age; }
            set { age = value; }
        }//سن

        // تولیدکننده
        public SPersonne(string nom, int age) {
            this.nom = nom;
            this.age = age;
        }//سازنده

        // ToString
        public override string ToString() {
            return "SPersonne(" + Nom + "," + Age + ")";
        }//ToString
    }//ساختار
}//فضای نام
  • خطوط ۸–۹: دو فیلد خصوصی
  • خطوط ۱۲–۲۰: ویژگی‌های عمومی مرتبط
  • خطوط ۲۳–۲۶: یک سازنده تعریف شده است. توجه داشته باشید که سازنده بدون پارامتر SPersonne() همیشه موجود است و نیازی به تعریف آن نیست. کامپایلر هرگونه تلاش برای تعریف آن را رد خواهد کرد. در کَنستراکتور در خطوط ۲۳–۲۶، ممکن است وسوسه شوید که فیلدهای خصوصی nom و age را از طریق خواص عمومی آنها Nom و Age مقداردهی اولیه کنید. این توسط کامپایلر رد می‌شود. متدهای ساختار نمی‌توانند در طول ساخت آن استفاده شوند.
  • خطوط ۲۹–۳۱: تعریف مجدد متد ToString.

یک برنامهٔ آزمایشی ممکن است به این شکل باشد:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // یک شخص p1
            SPersonne p1=new SPersonne();
            p1.Nom="paul";
            p1.Age= 10;
            Console.WriteLine("p1={0}",p1);
            // یک شخص p2
            SPersonne p2 = p1;
            Console.WriteLine("p2=" + p2);
            // p2 اصلاح شده است
            p2.Nom = "nicole";
            p2.Age = 30;
            // تأیید p1 و p2
            Console.WriteLine("p1=" + p1);
            Console.WriteLine("p2=" + p2);
            // یک شخص p3
            SPersonne p3 = new SPersonne("amandin", 18);
            Console.WriteLine("p3=" + p3);
            // یک شخص p4
            SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
            Console.WriteLine("p4=" + p4);
        }
    }
}
  • خط ۷: ما مجبوریم صراحتاً از سازنده بدون پارامتر استفاده کنیم، زیرا یک سازنده دیگر در ساختار وجود دارد. اگر ساختار هیچ سازنده‌ای نداشت، عبارت

            SPersonne p1;

برای ایجاد یک ساختار خالی کافی می‌بود.

  • خطوط ۸–۹: ساختار از طریق ویژگی‌های عمومی خود مقداردهی اولیه می‌شود
  • خط ۱۰: متد p1.ToString باید درون WriteLine استفاده شود.
  • خط ۲۱: ایجاد یک ساختار با استفاده از سازنده SPersonne(string, int)
  • خط ۲۴: ایجاد یک ساختار با استفاده از سازنده بدون پارامتر 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 
{

    // ویژگی‌ها
    Object Current { get; }

    // روش‌ها
    bool MoveNext();
    void Reset();
}

ویژگی‌ها و متدهای اینفورسمنت صرفاً توسط امضاهایشان تعریف می‌شوند. آنها پیاده‌سازی نشده‌اند (هیچ کدی ندارند). این کلاس‌هایی که اینفورسمنت را پیاده‌سازی می‌کنند هستند که کد مربوط به متدها و ویژگی‌های اینفورسمنت را فراهم می‌کنند.

1
2
3
4
5
6
public class C : IEnumerator{
    ...
    Object Current{ get {...}}
    bool MoveNext{...}
    void Reset(){...}
}
  • خط ۱: کلاس C کلاس IEnumerator را پیاده‌سازی می‌کند. توجه داشته باشید که دو نقطه (:) که برای پیاده‌سازی یک رابط استفاده می‌شود، همانند دو نقطه‌ای است که برای ارث‌بری کلاس به کار می‌رود.
  • خطوط ۳–۵: پیاده‌سازی متدها و ویژگی‌های رابط IEnumerator.

مثال زیر را در نظر بگیرید:


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

رابط IStats دارای:

  • یک خاصیت فقط-خواندنی Moyenne: برای محاسبه میانگین یک سری مقادیر
  • یک متد EcartType: برای محاسبه انحراف معیار

توجه باید داشت که در هیچ کجا مشخص نشده است که کدام مجموعه مقادیر منظور است. این مجموعه می‌تواند میانگین نمرات یک کلاس، میانگین فروش ماهانه یک محصول خاص، میانگین دما در یک مکان مشخص و غیره باشد. این اصل حاکم بر اینترفیس‌ها است: آن‌ها وجود متدها را در شیء فرض می‌کنند، اما نه وجود داده‌های خاص را.

یک کلاس پیاده‌سازی اولیه برای رابط IStats می‌تواند کلاسی باشد که برای ذخیره نمره‌های دانش‌آموزان یک کلاس در یک درس مشخص استفاده می‌شود. یک دانش‌آموز با ساختار زیر مشخص می‌شود Elève:


    public struct Elève {
        public string Nom { get; set; }
        public string Prénom { get; set; }
}//دانشجو

دانش‌آموز با نام خانوادگی و نامش شناسایی می‌شود. خطوط ۲–۳ شامل ویژگی‌های خودکار برای این دو صفت هستند.

یک نمره با ساختار زیر مشخص می‌شود: Note:


    public struct Note {
        public Elève Elève { get; set; }
        public double Valeur { get; set; }
}//علامت

نمرات با شناسهٔ دانش‌آموز و خودِ نمره مشخص می‌شوند. خطوط ۲–۳ شامل ویژگی‌های خودکار برای این دو صفت هستند.

نمرات همهٔ دانش‌آموزان در یک درس مشخص در کلاس زیرگروه TableauDeNotes قرار می‌گیرند:


using System;
using System.Text;

namespace Chap2 {

    public class TableauDeNotes : IStats {
        // ویژگی‌ها
        public string Matière { get; set; }
        public Note[] Notes { get; set; }
        public double Moyenne { get; private set; }
        private double ecartType;

        // سازنده
        public TableauDeNotes(string matière, Note[] notes) {
            // ذخیره‌سازی از طریق ویژگی‌های عمومی
            Matière = matière;
            Notes = notes;
            //محاسبه میانگین نمرات
            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;
            //انحراف معیار
            double carrés = 0;
            for (int i = 0; i < Notes.Length; i++) {
                carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
            }//برای
            if (Notes.Length != 0)
                ecartType = Math.Sqrt(carrés / Notes.Length);
            else ecartType = -1;
        }//سازنده

        public double EcartType() {
            return ecartType;
        }

        // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
            //ما همه نمره‌ها را به هم متصل می‌کنیم
            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("],");
            };
            //آخرین یادداشت
            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(")");
            //پایان
            return valeur.ToString();
        }//ToString

    }//کلاس
}
  • خط ۶: کلاس TableauDeNotes رابط IStats را پیاده‌سازی می‌کند. بنابراین باید ویژگی Moyenne و متد EcartType را پیاده‌سازی کند. این‌ها در خطوط ۱۰ (Moyenne) و ۳۵–۳۷ (EcartType) پیاده‌سازی شده‌اند
  • خطوط ۸–۱۰: سه خاصیت خودکار
  • خط ۸: موضوعی که شیء نمرات را برای آن ذخیره می‌کند
  • خط ۹: جدول نمرات دانش‌آموزان (دانش‌آموز، نمره)
  • خط ۱۰: میانگین نمرات – خاصیتی که پیاده‌سازی خاصیت Moyenne از رابط IStats است.
  • خط ۱۱: فیلدی که انحراف معیار نمرات را ذخیره می‌کند - متد مرتبط get (EcartType) برای خطوط ۳۵–۳۷، متد EcartType از رابط IStats را پیاده‌سازی می‌کند.
  • خط 9: نمرات در یک آرایه ذخیره می‌شوند. این آرایه هنگام نمونه‌سازی کلاس TableauDeNotes در خطوط 14–33 به سازنده ارسال می‌شود.
  • خطوط 14–33: سازنده. در اینجا فرض می‌شود که نمره‌های ارسال‌شده به سازنده پس از آن تغییر نخواهند کرد. بنابراین، از سازنده برای محاسبه فوری میانگین و انحراف معیار این نمره‌ها و ذخیره‌سازی آن‌ها در فیلدهای خطوط 10–11 استفاده می‌شود. میانگین در فیلد خصوصی زیربناییِ ویژگی خودکار Moyenne در خط ۱۰ ذخیره می‌شود و انحراف معیار در فیلد خصوصی در خط ۱۱ ذخیره می‌شود.
  • خط ۱۰: متد get از ویژگی خودکار Moyenne، فیلد خصوصی زیرین را بازمی‌گرداند.
  • خطوط ۳۵–۳۷: متد EcartType مقدار فیلد خصوصی در خط ۱۱ را بازمی‌گرداند.

چند نکته ظریف در این کد وجود دارد:

  • خط ۲۳: متد set از خصوصیت Moyenne برای انجام تخصیص استفاده می‌شود. این متد در خط ۱۰ به‌صورت خصوصی تعریف شده است تا تخصیص مقدار به خصوصیت Moyenne تنها در داخل کلاس امکان‌پذیر باشد.
  • خطوط ۴۰–۵۴: برای بهبود عملکرد، از یک شیء StringBuilder برای ساختن رشته‌ای که نمایانگر شیء TableauDeNotes است، استفاده می‌شود. باید توجه داشت که این کار خوانایی کد را به طور قابل توجهی کاهش می‌دهد. این یک عیب است.

در کلاس قبلی، نشانه‌ها در یک آرایه ذخیره می‌شدند. پس از ساختن شیء TableauDeNotes، افزودن نشانه‌ی جدید امکان‌پذیر نبود. اکنون ما پیاده‌سازی دوم رابط IStats را به نام ListeDeNotes پیشنهاد می‌کنیم که در آن این بار نمرات در یک لیست ذخیره می‌شوند و امکان افزودن نمره‌ها پس از ایجاد اولیه شیء ListeDeNotes وجود دارد.

کد کلاس ListeDeNotes به شرح زیر است:


using System;
using System.Text;
using System.Collections.Generic;

namespace Chap2 {

    public class ListeDeNotes : IStats {
        // ویژگی‌ها
        public string Matière { get; set; }
        public List<Note> Notes { get; set; }
        public double moyenne = -1;
        public double ecartType = -1;

        // سازنده
        public ListeDeNotes(string matière, List<Note> notes) {
            // ذخیره‌سازی از طریق ویژگی‌های عمومی
            Matière = matière;
            Notes = notes;
        }//سازنده

        // افزودن یادداشت
        public void Ajouter(Note note) {
            // افزودن یادداشت
            Notes.Add(note);
            // بازنشانی میانگین و انحراف معیار
            moyenne = -1;
            ecartType = -1;
        }

        // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
            //تمام یادداشت‌ها به هم متصل می‌شوند
            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("],");
            };
            //نمره نهایی
            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(")");
            // پایان
            return valeur.ToString();
        }//ToString

        //میانگین نمرات
        public double Moyenne {
            get {
                if (moyenne != -1) return moyenne;
                //محاسبه میانگین نمرات
                double somme = 0;
                for (int i = 0; i < Notes.Count; i++) {
                    somme += Notes[i].Valeur;
                }
                //محاسبه میانگین
                if (Notes.Count != 0) moyenne = somme / Notes.Count;
                return moyenne;
            }
        }

        public double EcartType() {
            //انحراف معیار
            if (ecartType != -1) return ecartType;
            //میانگین
            double moyenne = Moyenne;
            double carrés = 0;
            for (int i = 0; i < Notes.Count; i++) {
                carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
            }//برای
            //انحراف معیار را بازمی‌گرداند
            if (Notes.Count != 0)
                ecartType = Math.Sqrt(carrés / Notes.Count);
            return ecartType;
        }
    }//کلاس
}
  • خط ۷: کلاس ListeDeNotes رابط IStats را پیاده‌سازی می‌کند
  • خط ۱۰: یادداشت‌ها اکنون به جای آرایه در یک لیست ذخیره می‌شوند
  • خط ۱۱: ویژگی خودکار Moyenne از کلاس TableauDeNotes در اینجا به نفع یک فیلد خصوصی moyenne منسوخ شده است، خط ۱۱، مرتبط با ویژگی عمومی فقط-خواندنی Moyenne در خطوط ۴۸–۶۰
  • خطوط ۲۲–۲۸: اکنون می‌توان یادداشتی به موارد ذخیره‌شده قبلی اضافه کرد، که قبلاً امکان‌پذیر نبود.
  • خطوط ۱۵–۱۹: در نتیجه، میانگین و انحراف معیار دیگر در کانتراکتور محاسبه نمی‌شوند، بلکه در خود متدهای اینترفیس محاسبه می‌شوند: Moyenne (خطوط ۴۸–۶۰) و EcartType (۶۲–۷۶). با این حال، بازحساب تنها زمانی انجام می‌شود که میانگین و انحراف معیار برابر با -1 نباشند (خطوط 50 و 64).

یک کلاس تست ممکن است به این شکل باشد:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // برخی از دانش‌آموزان و نمرات انگلیسی
            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 } };
            //که در یک شیء TableauDeNotes ذخیره شده‌اند
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
            //نمایش میانگین و انحراف معیار
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
            // دانش‌آموزان و موضوع در یک شیء ListeDeNotes قرار می‌گیرند
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
            // نمایش میانگین و انحراف معیار
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
            // یک علامت اضافه می‌شود
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
            //نمایش میانگین و انحراف معیار
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
        }
    }
}
  • خط ۸: ایجاد یک آرایه از دانش‌آموزان با استفاده از سازنده بدون پارامتر و مقداردهی اولیه از طریق ویژگی‌های عمومی
  • خط ۹: ایجاد یک آرایه از نمرات با استفاده از همان تکنیک
  • خط ۱۱: یک شیء TableauDeNotes که میانگین و انحراف معیار آن در خط ۱۳ محاسبه می‌شود
  • خط ۱۵: یک شیء از نوع ListeDeNotes که میانگین و انحراف معیار آن در خط ۱۷ محاسبه می‌شود. کلاس List<Note> دارای یک سازنده است که یک شیء پیاده‌سازی‌کننده رابط IEnumerable<Note> را می‌پذیرد. آرایه notes1 این رابط را پیاده‌سازی می‌کند و می‌توان از آن برای ساختن شی List<Note> استفاده کرد.
  • خط ۱۹: افزودن یک علامت جدید
  • خط ۲۱: بازحساب میانگین و انحراف معیار

نتایج اجرای برنامه به شرح زیر است:

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) {
            // برخی از دانش‌آموزان و نمرات انگلیسی
            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 } };
            // که در یک شیء TableauDeNotes ذخیره شده‌اند
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
            // نمایش میانگین و انحراف معیار
            AfficheStats(anglais);
            //ما دانش‌آموزان و موضوع را در یک شیء ListeDeNotes قرار می‌دهیم
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
            // نمایش میانگین و انحراف معیار
            AfficheStats(français);
            //یک علامت اضافه می‌شود
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
            //نمایش میانگین و انحراف معیار
            AfficheStats(français);
        }

        //نمایش میانگین و انحراف معیار برای یک نوع IStats
        static void AfficheStats(IStats valeurs) {
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
        }
    }
}
  • خطوط ۲۵–۲۷: متد استاتیک AfficheStats یک نوع IStats را به عنوان پارامتر می‌پذیرد، یعنی یک نوع رابط. این بدان معناست که پارامتر واقعی می‌تواند هر ابجکتی باشد که رابط IStats را پیاده‌سازی می‌کند. هنگام استفاده از یک آیتم داده از نوع رابط، این بدان معناست که تنها متدهای رابطی که توسط آن آیتم داده پیاده‌سازی شده‌اند، استفاده خواهند شد. بقیه نادیده گرفته می‌شوند. این ویژگی شبیه به چندریختی (polymorphism) مشاهده‌شده در کلاس‌ها است. اگر مجموعه‌ای از کلاس‌های Ci که از طریق ارث‌بری با یکدیگر مرتبط نیستند (و بنابراین نمی‌توان از چندریختی ارث‌بری استفاده کرد) مجموعه‌ای از متدها با امضای یکسان را به اشتراک بگذارند، ممکن است مفید باشد که این متدها را در یک رابط (interface) با نام I که همه کلاس‌های مرتبط آن را پیاده‌سازی می‌کنند، گروهبندی کرد. اشیاء این کلاس‌های Ci سپس می‌توانند به‌عنوان پارامترهای واقعی توابعی که یک پارامتر رسمی از نوع I، یعنی c.a.d، را می‌پذیرند، استفاده شوند. این توابع تنها از متدهای اشیاء Ci که در رابط I تعریف شده‌اند استفاده می‌کنند، نه از ویژگی‌ها و متدهای خاص کلاس‌های مختلف Ci.
  • خط ۱۳: متد AfficheStats با نوع TableauDeNotes که رابط IStats را پیاده‌سازی می‌کند، فراخوانی می‌شود.
  • خط ۱۷: همین امر در مورد نوع ListeDeNotes نیز صدق می‌کند.

نتایج اجرای کد با نتایج مثال قبلی یکسان است.

یک متغیر می‌تواند از نوع یک رابط باشد. بنابراین، می‌توانیم بنویسیم:

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

اعلامیه‌ای در خط ۱ نشان می‌دهد که stats1 یک نمونه از کلاسی است که رابط IStats را پیاده‌سازی می‌کند. این اعلان به این معناست که کامپایلر تنها اجازه دسترسی به متدهای این رابط را در داخل stats1 می‌دهد: خاصیت Moyenne و متد EcartType.

در نهایت، باید توجه داشت که رابط‌ها می‌توانند پیاده‌سازی‌های متعددی داشته باشند، مانند c.a.d. این را می‌توان به صورت زیر نوشت:

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;

        // تولیدکننده
        public Utilisateur(string login, string motDePasse) {
            // اطلاعات ذخیره می‌شود
            this.login = login;
            this.motDePasse = motDePasse;
            // کاربر شناسایی شده است
            role=identifie();
            // شناسایی شده؟
            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);
        }

        // شناسایی می‌کند
        abstract public string identifie();
    }
}
  • خطوط ۱۱–۲۱: سازنده کلاس Utilisateur. این کلاس اطلاعات مربوط به کاربر یک برنامه وب را ذخیره می‌کند. این برنامه انواع مختلفی از کاربران را از طریق نام کاربری و رمز عبور (خطوط ۶–۷) احراز هویت می‌کند. این دو مورد اطلاعات برای برخی کاربران در برابر سرویس LDAP، برای برخی دیگر در برابر سرویس SGBD و غیره بررسی می‌شوند...
  • خطوط ۱۳–۱۴: جزئیات احراز هویت ذخیره می‌شوند
  • خط ۱۶: این مورد توسط متد `identifie` تأیید می‌شود. از آنجایی که متد شناسایی مشخص نیست، در خط ۲۹ با استفاده از کلمه کلیدی `abstract` به صورت انتزاعی (abstract) اعلام می‌شود. متد `identifie` یک رشته را برمی‌گرداند که نقش کاربر را مشخص می‌کند (اساساً، آنچه به او اجازه انجام آن داده شده است). اگر این رشته نشانگر null باشد، در خط ۱۹ یک استثنا پرتاب می‌شود.
  • خط ۴: از آنجا که یک متد انتزاعی دارد، خود کلاس با استفاده از کلمه کلیدی `abstract` به صورت انتزاعی اعلام می‌شود.
  • خط ۲۹: متد انتزاعی `identify` هیچ تعریفی ندارد. این کلاس‌های مشتق هستند که آن را تعریف خواهند کرد.
  • خطوط ۲۴–۲۶: متد ToString که یک نمونه از کلاس را شناسایی می‌کند.

در اینجا فرض شده است که توسعه‌دهنده می‌خواهد کنترل ایجاد نمونه‌های کلاس Utilisateur و کلاس‌های مشتق آن را در دست داشته باشد، شاید به این دلیل که می‌خواهد اطمینان حاصل کند اگر کاربر شناسایی نشود، یک استثنا از نوع مشخصی پرتاب شود (خط ۱۹). کلاس‌های مشتق می‌توانند از این سازنده استفاده کنند. برای این کار، آن‌ها باید متد identify را فراهم کنند.

کلاس ExceptionUtilisateurInconnu به شرح زیر است:


using System;

namespace Chap2 {
    class ExceptionUtilisateurInconnu : Exception {
        public ExceptionUtilisateurInconnu(string message) : base(message){
        }
    }
}
  • خط ۳: از کلاس Exception ارث می‌برد
  • خطوط ۴–۶: این کلاس تنها یک سازنده دارد که یک پیام خطا را به‌عنوان پارامتر می‌پذیرد. این پیام به کلاس والد (خط ۵) ارسال می‌شود که همان سازنده را دارد.

اکنون کلاس Utilisateur را از کلاس فرزند Administrateur مشتق می‌کنیم:


namespace Chap2 {
    class Administrateur : Utilisateur {
        // تولیدکننده
        public Administrateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        // شناسایی می‌کند
        public override string identifie() {
            // شناسایی LDAP
            // ...
            return "admin";
        }
    }
}
  • خطوط ۴–۶: سازنده به سادگی پارامترهای دریافتی را به کلاس والد خود منتقل می‌کند
  • خطوط ۹–۱۲: متد `identify` کلاس `Administrateur`. فرض کنیم که یک مدیر توسط سیستمی به نام LDAP شناسایی می‌شود. این متد، متد `identify` کلاس والد خود را بازنویسی می‌کند. از آنجا که این متد یک متد انتزاعی را بازنویسی می‌کند، نیازی به درج کلمه کلیدی override. نیست.

اکنون کلاس Utilisateur را از کلاس فرزند Observateur مشتق می‌کنیم:


namespace Chap2 {
    class Observateur : Utilisateur{
        //تولیدکننده
        public Observateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        //شناسایی می‌کند
        public override string identifie() {
            //شناسایی SGBD
            // ...
            return "observateur";
        }
        
    }
}
  • خطوط ۴–۶: سازنده به سادگی پارامترهای دریافتی را به کلاس والد خود پاس می‌دهد
  • خطوط ۹–۱۳: متد «identify» کلاس Observateur. فرض بر این است که یک ناظر با بررسی داده‌های شناسایی‌اش در برابر یک پایگاه داده شناسایی می‌شود.

در نهایت، اشیاء Administrateur و Observateur توسط همان سازنده، یعنی سازنده کلاس والد Utilisateur.، نمونه سازی می‌شوند. این سازنده از متد identify که توسط این کلاس‌ها ارائه شده است، استفاده خواهد کرد.

یک کلاس سوم، Inconnu، نیز از کلاس Utilisateur ارث می‌برد:


namespace Chap2 {
    class Inconnu : Utilisateur{

        // تولیدکننده
        public Inconnu(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        //شناسایی می‌کند
        public override string identifie() {
            // کاربر ناشناخته
            // ...
            return null;
        }
        
    }
}
  • خط ۱۳: متد identifie نشانگر 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);
            }
        }
    }
}

توجه داشته باشید که در خطوط ۶، ۷ و ۹، متد [Utilisateur].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){
            // مراجع «value1» و «value2» جابجا شده‌اند
            int temp = value2;
            value2 = value1;
            value1 = temp;
}

حال، اگر می‌خواستیم دو مرجع به اشیاء Personne را با هم جابجا کنیم، می‌نوشتیم:


        public static void Echanger2(ref Personne value1, ref Personne value2){
            //ارجاعات «value1» و «value2» جابه‌جا شده‌اند
            Personne temp = value2;
            value2 = value1;
            value1 = temp;
}

آنچه این دو متد را از هم متمایز می‌کند، نوع T پارامترها است: int در Echanger1، Personne در Echanger2. کلاس‌ها و اینترفیس‌های عمومی نیاز به متدهایی را که تنها در نوع برخی از پارامترهایشان با هم تفاوت دارند، برآورده می‌کنند.

با یک کلاس عمومی، متد Echanger می‌تواند به صورت زیر بازنویسی شود:


namespace Chap2 {
    class Generic1<T> {
        public static void Echanger(ref T value1, ref T value2){
            // ارجاعات 'value1' و 'value2' جابه‌جا شده‌اند
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • خط ۲: کلاس Generic1 با نوع T پارامتریزه شده است. می‌توان هر نامی را برای آن در نظر گرفت. این نوع T سپس در خطوط ۳ و ۵ در کلاس مجدداً استفاده می‌شود. به کلاس Generic1 یک کلاس عمومی (generic) گفته می‌شود.
  • خط ۳: دو مرجع به نوع T را که باید با هم جابه‌جا شوند، تعریف می‌کند
  • خط ۵: متغیر موقت 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 s1 = "s1", s2 = "s2";
            Generic1<string>.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // شخص
            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);

        }
    }
}
  • خط ۸: وقتی از یک کلاس عمومی پارامتریزه شده با انواع T1، T2 و غیره استفاده می‌شود، این‌ها باید «نمونه سازی» شوند. خط ۸: متد استاتیک Echanger از نوع Generic1<int> برای نشان دادن اینکه ارجاعات ارسال‌شده به متد Echanger از نوع int هستند، استفاده می‌شود.
  • خط ۱۲: متد استاتیک Echanger از نوع Generic1<string> برای نشان دادن اینکه ارجاعات ارسال‌شده به متد Echanger از نوع string هستند، استفاده می‌شود.
  • خط ۱۶: متد استاتیک Echanger از نوع Generic1<Personne> برای نشان دادن اینکه ارجاعات ارسال‌شده به متد Echanger از نوع Personne هستند، استفاده می‌شود.

نتایج اجرای برنامه به شرح زیر است:

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

متد Echanger می‌توانست به شکل زیر نیز نوشته شود:


namespace Chap2 {
    class Generic2 {
        public static void Echanger<T>(ref T value1, ref T value2){
            // مراجع value1 و value2 با هم جابجا می‌شوند
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • خط ۲: کلاس Generic2 دیگر عمومی نیست
  • خط ۳: متد استاتیک Echanger عمومی است

برنامهٔ آزمون سپس به شکل زیر درمی‌آید:


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);
            // شخص
            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);
        }
    }
}
  • خطوط ۸، ۱۲ و ۱۶: متد Echanger فراخوانی می‌شود، با مشخص کردن نوع پارامترها در داخل <>. در واقع، کامپایلر بر اساس نوع پارامترهای واقعی می‌تواند تشخیص دهد که کدام واریانت از متد Echanger را باید استفاده کند. بنابراین، سینتکس زیر معتبر است:

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

خطوط ۱، ۳ و ۵: واریانت متد Echanger که فراخوانی می‌شود دیگر مشخص نشده است. کامپایلر می‌تواند آن را از ماهیت پارامترهای واقعی مورد استفاده استنتاج کند.

می‌توان محدودیت‌هایی را برای پارامترهای عمومی قرار داد:

Image

متد عمومی جدید زیر را در نظر بگیرید Echanger:


namespace Chap2 {
    class Generic3 {
        public static void Echanger<T>(ref T value1, ref T value2) where T : class {
            // ارجاعات value1 و value2 را با هم جابجا کنید
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • خط ۳: نوع 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);
            // شخص
            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);

        }
    }
}

مترجم در خط ۸ خطا گزارش می‌کند زیرا نوع int یک کلاس یا اینترفیس نیست؛ بلکه یک ساختار است:

Image

روش عمومی جدید زیر را در نظر بگیرید: Echanger:


namespace Chap2 {
    class Generic4 {
        public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
            // مقادیر دو عنصر را بازیابی کنید
            int value1 = element1.Value();
            int value2 = element2.Value();
            // اگر عنصر اول بزرگ‌تر از عنصر دوم باشد، عناصر را با هم جابه‌جا کنید
            if (value1 > value2) {
                T temp = element2;
                element2 = element1;
                element1 = temp;
            }
        }
    }
}
  • خط ۳: نوع T باید رابط Interface1 را پیاده‌سازی کند. این رابط دارای متدی به نام Value, است که در خطوط ۵ و ۶ استفاده شده و مقدار شیء از نوع T را برمی‌گرداند.
  • خطوط ۸–۱۲: دو ارجاع element1 و element2 تنها در صورتی با هم مبادله می‌شوند که مقدار element1 از مقدار element2 بزرگ‌تر باشد.

رابط Interface1 به شرح زیر است:


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

این توسط کلاس زیر، Class1، پیاده‌سازی شده است:


using System;
using System.Threading;

namespace Chap2 {
    class Class1 : Interface1 {
        //مقدار شیء
        private int value;

        // سازنده
        public Class1() {
            // منتظر بمانید ۱ میلی‌ثانیه
            Thread.Sleep(1);
            // مقدار تصادفی بین 0 و 99
            value = new Random(DateTime.Now.Millisecond).Next(100);
        }

        // گیرنده (getter) برای فیلد خصوصی 'value'
        public int Value() {
            return value;
        }

        //وضعیت نمونه
        public override string ToString() {
            return value.ToString();
        }
    }
}
  • خط ۵: Class1 رابط Interface1 را پیاده‌سازی می‌کند
  • خط ۷: مقدار یک نمونه از Class1
  • خطوط ۱۰–۱۴: فیلد value با مقدار تصادفی بین ۰ و ۹۹ مقداردهی اولیه می‌شود
  • خطوط ۱۸–۲۰: متد Value از رابط Interface1
  • خطوط 23–25: متد ToString از کلاس

رابط Interface1 نیز توسط کلاس Class2 پیاده‌سازی شده است:


using System;

namespace Chap2 {
    class Class2 : Interface1 {
        // مقادیر شیء
        private int value;
        private String s;

        // سازنده
        public Class2(String s) {
            this.s = s;
            value = s.Length;
        }

        // دسترس‌گر مقدار فیلد خصوصی
        public int Value() {
            return value;
        }

        // وضعیت نمونه
        public override string ToString() {
            return s;
        }
    }
}
  • خط ۴: Class2 رابط Interface1 را پیاده‌سازی می‌کند
  • خط ۶: مقدار یک نمونه از Class2
  • خطوط ۱۰–۱۳: فیلد value با طول رشته کاراکتری ارسال‌شده به سازنده مقداردهی اولیه می‌شود
  • خطوط 16–18: متد Value از رابط Interface1
  • خطوط 21–22: متد ToString از کلاس

یک برنامهٔ آزمایشی ممکن است به این شکل باشد:


using System;

namespace Chap2 {
    class Program5 {
        static void Main(string[] args) {
            // تبادل نمونه‌های از نوع 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);
            }
            // تبادل نمونه‌های از نوع Class2
            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);
        }
    }
}
  • خطوط ۸–۱۴: نمونه‌هایی از نوع 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

برای تشریح مفهوم یک رابط عمومی ( )، یک آرایه از افراد را ابتدا بر اساس نام‌هایشان و سپس بر اساس سن‌شان مرتب خواهیم کرد. متدی که به ما امکان مرتب‌سازی یک آرایه را می‌دهد، متد ایستا Sort کلاس Array است:

Image

به یاد داشته باشید که یک متد استاتیک با پیش‌افزودن نام کلاس به جای نام یک نمونه از کلاس فراخوانی می‌شود. متد Sort امضاهای مختلفی دارد (این متد اوری‌لود شده است). ما از امضای زیر استفاده خواهیم کرد:

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

Sort یک متد عمومی (generic) است که در آن T نمایانگر هر نوع (type) است. این متد دو پارامتر می‌گیرد:

  • T[] array: آرایهٔ عناصر از نوع 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 را به‌عنوان پارامتر می‌پذیرد
  • اگر t1 > t2 باشد مقدار 1، اگر t1 == t2 باشد مقدار 0 و اگر t1 < t2 باشد مقدار -1 را برمی‌گرداند. این وظیفه توسعه‌دهنده است که معنای عملگرهای <، == و > را تعریف کند. برای مثال، اگر p1 و p2 دو شیء Personne باشند، می‌توان گفت که 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) {
            // یک آرایه از افراد
            Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
            // نمایش
            Affiche("Tableau à trier", personnes1);
            // سنجاق‌کردن بر اساس نام
            Array.Sort(personnes1, new CompareNoms());
            //نمایش
            Affiche("Tableau après le tri selon les nom et prénom", personnes1);
            // سنجاق‌گذاری بر اساس سن
            Array.Sort(personnes1, new CompareAges());
            // نمایش
            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);
            }
        }
    }

    //کلاس مقایسه برای نام خانوادگی و نام افراد
    class CompareNoms : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
            // مقایسهٔ نام‌های خانوادگی
            int i = p1.Nom.CompareTo(p2.Nom);
            if (i != 0)
                return i;
            //تطابق نام‌ها – نام‌های کوچک مقایسه می‌شوند
            return p1.Prenom.CompareTo(p2.Prenom);
        }
    }

    //کلاس مقایسه برای سنین افراد
    class CompareAges : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
            //مقایسه سنین
            if (p1.Age > p2.Age)
                return 1;
            else if (p1.Age == p2.Age)
                return 0;
            else
                return -1;
        }
    }

}
  • خط ۸: آرایه افراد
  • خط ۱۲: آرایه مرتب‌شده افراد بر اساس نام خانوادگی و نام. پارامتر دوم متد عمومی Sort یک نمونه از کلاس CompareNoms است که رابط عمومی IComparer<Person> را پیاده‌سازی می‌کند.
  • خطوط ۳۰–۳۹: کلاس CompareNoms که رابط عمومی IComparer<Person> را پیاده‌سازی می‌کند.
  • خطوط ۳۱–۳۸: پیاده‌سازی متد عمومی int CompareTo(T, T) از رابط IComparer<T>. این متد از متد String.CompareTo که در بخش 3.3.5.4 توصیف شده است، برای مقایسه دو رشتهٔ کاراکتری استفاده می‌کند.
  • خط ۱۶: مرتب‌سازی جدول افراد بر اساس سن. پارامتر دوم متد عمومی Sort یک نمونه از کلاس 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 در کدام «اسمبلی» قرار دارد. هنگامی که خارج از ویژوال استودیو کامپایل می‌کنید و نیاز دارید ارجاع به فایل‌های مختلف dll را که حاوی کلاس‌های مورد نیاز شما هستند، مشخص کنید، این اطلاعات می‌تواند مفید باشد. برای ارجاع به فایل‌های dll مورد نیاز برای کامپایل یک کلاس، می‌نویسید:

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

که در آن csc کامپایلر C# است. هنگام ایجاد یک کلاس، می‌توانید آن را در داخل یک فضای نام ایجاد کنید. هدف از این نام‌فضه‌ها جلوگیری از تداخل نام بین کلاس‌ها هنگام فروش آن‌ها است. برای مثال، دو شرکت E1 و E2 را در نظر بگیرید که به ترتیب کلاس‌هایی را در dll بسته‌بندی و توزیع می‌کنند، e1.dll و e2.dll به‌ترتیب. فرض کنیم مشتری C این دو مجموعه کلاس را خریداری می‌کند که در هر دو شرکت کلاسی به نام Personne تعریف شده است. مشتری C برنامه‌ای را به صورت زیر کامپایل می‌کند:

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

اگر سورس prog.cs از کلاس Personne استفاده کند، کامپایلر نخواهد دانست که کلاس Personne را از e1.dll بگیرد یا آن را از e2.dll. این امر منجر به خطا خواهد شد. اگر شرکت E1 دقت کند که کلاس‌های خود را در یک فضای نام به نام E1 ایجاد کند و شرکت E2 در یک فضای نام به نام E2، دو کلاس Personne سپس با نام‌های E1.Personne و E2.Personne نام‌گذاری خواهند شد. کلاینت باید در کلاس‌های خود از E1.Personne یا E2.Personne استفاده کند، اما نه از Personne. فضای نام این ابهام را برطرف می‌کند.

برای ایجاد یک کلاس در یک فضای نام، بنویسید:

namespace EspaceDeNoms{
     // تعریف کلاس
}

4.10. مثال کاربردی – V2

ما به محاسبه مالیات که پیش‌تر در فصل ۳.۶ پوشش داده شده بود بازمی‌گردیم و اکنون آن را با استفاده از کلاس‌ها و اینترفیس‌ها پی می‌گیریم. بیایید مسئله را مرور کنیم:

هدف ما نوشتن برنامه‌ای برای محاسبه بدهی مالیاتی یک مؤدی است. ما مورد ساده‌شده‌ای را در نظر می‌گیریم که مؤدی تنها حقوق خود را برای اظهار دارد (اعداد ۲۰۰۴ برای درآمد ۲۰۰۳):

  • تعداد سهام برای کارمند به صورت nbParts = nbEnfants/2 + 1 در صورت مجرد بودن، محاسبه می‌شود، nbEnfants/2 + 2 اگر متأهل باشند، که در آن nbEnfants تعداد فرزندانشان است.
  • اگر او حداقل سه فرزند داشته باشد، مستحق دریافت نیم سهم اضافی است.
  • درآمد مشمول مالیات او به صورت R = 0.72 × S محاسبه می‌شود، که در آن S حقوق سالانه او است.
  • ضریب خانوادگی او به صورت QF = R / nbParts محاسبه می‌شود
  • ما مالیات او را محاسبه می‌کنیم. جدول زیر را در نظر بگیرید:
۴۲۶۲
0
0
8382
0.0683
291.09
۱۴٬۷۵۳
0.1914
۱٬۳۲۲٫۹۲
۲۳٬۸۸۸
0.2826
۲٬۶۶۸٫۳۹
۳۸٬۸۶۸
0.3738
۴٬۸۴۶٫۹۸
۴۷٬۹۳۲
0.4262
۶٬۸۸۳٫۶۶
0
0.4809
9505.54

هر سطر دارای ۳ فیلد است. برای محاسبه مالیات I، به دنبال اولین سطری می‌گردیم که QFfield1 باشد. برای مثال، اگر QF = 5000 باشد، سطر را پیدا خواهیم کرد

    8382        0.0683        291.09

در این صورت مالیات I برابر با 0.0683*R - 291.09*nbParts خواهد بود. اگر QF به گونه‌ای باشد که شرط QF <= field1 هرگز برقرار نشود، آنگاه ضرایب مربوط به آخرین خط استفاده می‌شوند. در این صورت:

    0                0.4809    9505.54

که مالیات I = 0.4809 × R – 9505.54 × nbParts را می‌دهد.

ابتدا، ساختاری را تعریف می‌کنیم که قادر به دربرگرفتن یک سطر از جدول قبلی باشد:


namespace Chap2 {
    // یک رده مالیاتی
    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);
    }
}
  • خط ۳: متدی برای محاسبه مالیات بر اساس سه داده: وضعیت تأهل مودی، تعداد فرزندان او و حقوقش

سپس، یک کلاس انتزاعی را تعریف می‌کنیم که این رابط را پیاده‌سازی می‌کند:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {

        // محدوده‌های مالیاتی مورد نیاز برای محاسبه مالیات
        // از یک منبع خارجی تأمین می‌شوند

        protected TrancheImpot[] tranchesImpot;

        //محاسبه مالیات
        public int calculer(bool marié, int nbEnfants, int salaire) {
            //محاسبه تعداد سهام
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
            // محاسبه درآمد مشمول مالیات و ضریب خانوادگی
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // محاسبه مالیات
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
            // نتیجه اظهارنامه
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//محاسبه
    }//دسته‌بندی

}
  • خط ۲: کلاس AbstractImpot رابط IImpot را پیاده‌سازی می‌کند.
  • خط ۷: داده‌های سالانه برای محاسبه مالیات به صورت یک فیلد محافظت‌شده. کلاس AbstractImpot نمی‌داند که این فیلد چگونه مقداردهی اولیه خواهد شد. این کار را به کلاس‌های مشتق‌شده واگذار می‌کند. به همین دلیل است که به عنوان انتزاعی (خط ۲) اعلام شده است تا از هرگونه نمونه‌سازی جلوگیری کند.
  • خطوط ۱۰–۲۵: پیاده‌سازی متد calculer از رابط IImpot. کلاس‌های مشتق نیازی به بازنویسی این متد نخواهند داشت. بنابراین کلاس AbstractImpot به عنوان یک کلاس عاملیت‌بخش (factorisation class) برای کلاس‌های مشتق عمل می‌کند. این کلاس شامل موارد مشترکی است که در تمام کلاس‌های مشتق وجود دارد.

می‌توان یک کلاس که رابط IImpot را پیاده‌سازی می‌کند، با ارث‌بری از کلاس AbstractImpot ایجاد کرد. کاری که اکنون انجام می‌دهیم همین است:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

        // جدول‌های داده‌ای مورد نیاز برای محاسبه مالیات
        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() {
                // ایجاد جدول رده‌های مالیاتی
            tranchesImpot = new TrancheImpot[limites.Length];
                // پر کردن
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
    }//کلاس
}// فضای نام

کلاس HardwiredImpot در خطوط ۷–۹، داده‌های مورد نیاز برای محاسبه مالیات را به‌صورت کد سخت (hard-code) درج می‌کند. سازندهٔ آن (خطوط ۱۱–۱۸) از این داده‌ها برای مقداردهی اولیهٔ فیلد محافظت‌شدهٔ tranchesImpot در کلاس والد AbstractImpot استفاده می‌کند.

یک برنامهٔ آزمایشی ممکن است به این شکل باشد:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            // برنامه تعاملی محاسبه مالیات
            // کاربر سه مورد داده را از طریق صفحه‌کلید وارد می‌کند: متأهل nbEnfants حقوق
            // سپس برنامه مالیات قابل پرداخت را نمایش می‌دهد

            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";

            // ایجاد یک شیء IImpot
            IImpot impot = new HardwiredImpot();

            //حلقه بی‌نهایت
            while (true) {
                // درخواست پارامترهای محاسبه مالیات
                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();
                // کاری هست که باید انجام شود؟
                if (paramètres == null || paramètres == "") break;
                //بررسی تعداد آرگومان‌ها در خط واردشده
                string[] args = paramètres.Split(null);
                int nbParamètres = args.Length;
                if (nbParamètres != 3) {
                    Console.WriteLine(syntaxe);
                    continue;
                }//اگر
                // بررسی اعتبار پارامترها
                // متأهل
                string marié = args[0].ToLower();
                if (marié != "o" && marié != "n") {
                    Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
                    continue;
                }//اگر
                //nbEnfants
                int nbEnfants = 0;
                bool dataOk = false;
                try {
                    nbEnfants = int.Parse(args[1]);
                    dataOk = nbEnfants >= 0;
                } catch {
                }//اگر
                //آیا این داده‌ها صحیح هستند؟
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }
                // حقوق
                int salaire = 0;
                dataOk = false;
                try {
                    salaire = int.Parse(args[2]);
                    dataOk = salaire >= 0;
                } catch {
                }//try-catch
                //آیا داده‌ها صحیح هستند؟
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }
                //پارامترها صحیح هستند – محاسبه مالیات
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                // مالیات‌دهنده بعدی
            }//در حالی که
        }
    }
}

برنامهٔ بالا به کاربر امکان می‌دهد شبیه‌سازی‌های مکرر محاسبهٔ مالیات را انجام دهد.

  • خط ۱۶: ایجاد یک شیء از نوع impot که رابط IImpot را پیاده‌سازی می‌کند. این شیء با نمونه‌سازی نوع HardwiredImpot، نوعی که رابط IImpot را پیاده‌سازی می‌کند، ایجاد می‌شود. توجه کنید که متغیر impot از نوع HardwiredImpot نیست، بلکه از نوع IImpot است. با نوشتن این، نشان می‌دهیم که تنها به متد calculer از شیء impot علاقه‌مندیم و نه به بقیه.
  • خطوط ۱۹–۶۸: حلقه شبیه‌سازی محاسبه مالیات
  • خط ۲۲: سه پارامتر مورد نیاز برای متد calculer در یک خط واحد وارد می‌شوند.
  • خط ۲۶: متد [chaine].Split(null) رشته [chaine] را به کلمات تقسیم می‌کند. این کلمات در یک آرایه args ذخیره می‌شوند.
  • خط ۶۶: متد calculer روی شی impot فراخوانی می‌شود که رابط 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