چندریختی یا Polymorphism در سی شارپ
آشنایی با مفهوم چندریختی polymorphism در C#
چندریختی یا Polymorphism چیست؟
اکنون با شرح و توضیح مفهوم «چند ریختی» با سلسله مرتبههای ارثبری، بحث خود در خصوص برنامهنویسی شیءگرا (OOP) را ادامه میدهیم. چندریختی ما را قادر میسازد تا به جای «برنامهنویسی جزئی» به «برنامهنویسی کلی» بپردازیم. بویژه، چندریختی ما را قادر میسازد تا برنامههایی را بنویسیم که این برنامهها اشیائی را پردازش میکنند که کلاس مبنای یکسانی را در یک سلسله مراتب کلاسی به اشتراک میگذارند به طوری که انگار همگی اشیاء کلاس مبنا بودهاند.
اجازه دهید یک مثال چندریختی را در نظر بگیریم. فرض کنید برنامهای ایجاد کردهایم که حرکت انواع مختلفی از حیوانات را به خاطر مطالعات بیولوژیکی شبیهسازی میکند. کلاسهای Fish، Frog و Bird بیانگر انواعی از حیوانات تحت تحقیق و بررسیاند. تصور کنید که هر یک از کلاسها کلاس مبنای Animal را بسط میدهند؛ کلاس Animal حاوی یک متد Move بوده و موقعیت جاری یک حیوان را به صورت مختصات x-y-z نگهداری میکند. هر یک از کلاسهای مشتق شده متد Move را پیادهسازی میکنند. برنامهی ما آرایهای از مراجعات به شیءهایی از کلاسهای مشتق شدهی Animal مختلف را نگهداری میکند. برای شبیهسازی حرکات یک حیوان، برنامه به ازای هر ثانیه یک بار پیغام مشابهی به هر شیء ارسال میکند؛ برای مثال، پیغام Move. هر نوع بخصوصی از Animal به روش منحصر به فردی به پیغام Move واکنش نشان میدهد؛ یک Fish میتواند سه فوت شنا کند، یک قورباغه میتواند پنج فوت پرش کند و یک پرنده میتواند ده فوت پرواز کند. برنامه پیغام Move را به طور عام به تک تک اشیاء حیوان صادر میکند، اما هر شیئی مختصات x-y-z خود را به شکل مقتضی برای نوع خاص حرکت خود تغییر میدهد. استناد به هر یک از اشیاء برای آگاهی از نحوهی «انجام یک چیز درست» در واکنش به فراخوان متد یکسان، مفهوم کلیدی چندریختی است. پیغام یکسانی (در این مورد، Move) به گسترهای از اشیاء که دارای اشکال بسیاری از نتایج هستند ارسال شد؛ ازاینرو این مفهوم با اصطلاح چندریختی (یا چند شکلی) شناخته میشود.
توسعهی سیستمها آسان است
به کمک چندریختی میتوانیم سیستمهایی را طراحی و پیادهسازی کنیم که به راحتی قابل توسعه هستند؛ کلاسهای جدید مادامی که بخشی از سلسله مراتب ارثبری باشند که برنامه آن را به طور عام پردازش میکند میتوانند با کمترین و یا بدون هیچ تغییری به بخشهای کلی برنامه اضافه شوند. تنها بخشهایی از برنامه که میبایست تغییر داده شوند تا با کلاسهای جدید تطبیق یابند آنهایی هستند که نیازمند آگاهی مستقیم از کلاسهای جدیدی هستند که شما به سلسله مراتب اضافه کردهاید. برای مثال، اگر کلاس Animal را بسط دهیم تا کلاس Tortoise (که میتواند با یک اینچ خزش به پیغام Move واکنش نشان دهد) را ایجاد کنیم، نیازمند این هستیم تا تنها کلاس Tortoise و بخشی از فرایند شبیهسازی را بنویسیم که یک شیء Tortoise را نمونهسازی میکند. قسمتهایی از فرایند شبیهسازی که هر Animal را به طور عام پردازش میکند میتواند به همان صورت باقی بماند.
این فصل دارای بخشهای متعددی است. نخست، مثالهای رایج چندریختی را مورد بحث و بررسی قرار خواهیم داد. پس از آن یک نمونه کد عینی را ارائه خواهیم کرد که تبیینگر رفتار چندریختی است. همان طور که به زودی خواهید دید، شما مراجعات کلاس مبنا را مورد استفاده قرار خواهید داد تا هم اشیاء کلاس مبنا و هم اشیاء کلاس مشتق شده را به صورت چندریختی دستکاری کنید.
سلسله مراتب ارثبری چندریخت Employee
باردیگر سراغ مطالعهی موردی میرویم که سلسله مراتب استخدامی بخش 11.4.5 را بازبینی میکند. برنامهی پرداخت حقوق سادهای را توسعه میدهیم که به صورت چندریختی پرداخت هفتگی انواع مختلف کارمندان را با استفاده از متد Earnings هر شیء کارمند، محاسبه میکند. با وجود این که درآمد هر کارمند از یک نوع بخصوص به روش معینی محاسبه میشود، چندریختی به ما اجازه میدهد تا کارمندان را «به صورت عام» پردازش کنیم. در مطالعهی موردی، سلسله مراتب را بسط میدهیم تا دو کلاس جدید را در آن جای دهیم: SalariedEmployee (برای اشخاصی که دارای حقوق ثابت هفتگیاند) و HourlyEmployee (برای کسانی که حقوق ساعتی و به اضافه کاری دریافت میکنند). مجموعهی مشترکی از کارکردهای مربوط به تمامی کلاسهای واقع در سلسله مراتب بروز شده را در یک کلاس انتزاعی، Employee، اعان کردیم که از آن کلاسهای SalariedEmployee، HourlyEmployee و CommissionEmployee به طور مستقیم و کلاس BasePlusCommissionEmployee به طور غیرمستقیم ارث میبرند. همان طور که به زودی خواهید دید، هرگاه متد Earnings هر کارمند را ، بدون یک مراجعهی کلاس مبنای Employee احضار کنیم، به سبب قابلیتهای چندریخت C#، محاسبهی درآمد صحیح انجام میپذیرد.
مشخص کردن نوع یک شیء در زمان اجرا
برخی از اوقات، هنگام انجام پردازش چندریخت، لازم است تا برنامهنویسی جزئی (که به یک مورد خاص میپردازد) انجام دهیم. مطالعهی موردی Employee بیانگر این است که یک برنامه میتواند نوع یک شیء را در زمان اجرا مشخص کند و بر طبق آن بر روی آن شیء عمل میکند. در مطالعهی موردی، این قابلیتها را به کار میبریم تا مشخص کنیم که آیا یک شیء کارمند بخصوص یک BasePlusCommissionEmployee است یا خیر. اگر جواب آری باشد، حقوق پایهی آن کارمند را به اندازهی 10 درصد افزایش خواهیم داد.
واسطها
فصل را با معرفی واسطهای C# ادامه میدهیم. یک واسط توصیف کنندهی مجموعهای از متدها و خاصیتهاست که میتوانند بر روی یک شیء فراخوان شوند اما واسط پیادهسازی ملموسی را برای آنها تدارک نمیبیند. شما میتوانید کلاسهایی را اعلان کنید که یک یا چند واسط را پیادهسازی میکنند (یعنی این کلاسها پیادهسازی ملموسی را برای متدها و خاصیتهای آن واسط یا واسطها تدارک میبینند). هر عضو واسط باید برای تمامی کلاسهایی تعریف شود که واسط را پیادهسازی میکنند. به مجرد این که یک کلاس واسطی را پیادهسازی کند، همهی اشیاء آن کلاس دارای یک رابطهی «یک...است» با نوع واسط است، و فراهمسازی کارکردهای توصیف شده توسط واسط بوسیلهی همهی اشیاء کلاس تضمین شده است. این مطلب برای همهی کلاسهای مشتق شده از آن کلاس نیز صدق میکند.
واسطها بویژه برای تخصیص قابلیتهای مشترک به کلاسهای نامربوط مناسباند. این کار به اشیاء کلاسهای نامرتبط اجازه میدهد تا به صورت چندریختی پردازش شوند؛ اشیاء کلاسهایی که واسطهای یکسانی را پیادهسازی میکنند میتوانند به فراخوانهای متد یکسانی عکسالعمل نشان دهند. به منظور تبیین ایجاد و استفاده از واسطها، برنامهی پرداخت حقوق خود را اصلاح میکنیم تا برنامهی پرداخت حقوق کلی را ایجاد کنیم که میتواند پرداختهای ناشی از درآمدهای کارمندان شرکت و فاکتورهای پرداختی برای کالاهای خریداری شده را محاسبه کند. همان طور که خواهید دید، واسطها قابلیتهای چندریختی را همانند قابلیتهای فعال شده توسط ارثبری، امکانپذیر میسازند.
سربارگذاری عملگر
Operator Overloading
این فصل با معرفی سربارگذاری عملگر به انتها میرسد. در فصول قبل، کلاسهای شخصی خود را اعلان کرده و متدها را برای انجام وظایفی بر روی شیءهایی از این کلاسها مورد استفاده قرار دادیم. سربارگذاری متد به ما اجازه میدهد تا رفتار عملگرهای توکاری چون +، - و < را هنگام استفاده شدن بر روی اشیاء کلاسهای شخصیمان، تعریف کنیم. این امر نسبت به فراخوانی متدها، نوشتار سرراستتری را برای انجام وظایف بر روی اشیاء در اختیار میگذارد.
12.2 مثالهای چندریختی
حال به بررسی چندین مثال در خصوص چندریختی میپردازیم.
سلسله مراتب ارثبری Quadrilateral
Quadrilateral Inheritance Hierachy
اگر کلاس Rectangle از کلاس Quadrilateral (یک شکل چهاروجهی) مشتق شود، در این صورت یک Rectangle یک نسخهی اختصاصیتر از یک Quadrilateral است. هر عملیاتی (برای نمونه محاسبهی محیط یا مساحت) که بتواند بر روی یک شیء Quadrilateral انجام پذیرد بر روی یک شیء Rectangle نیز قابل انجام است. این عملیاتها میتوانند بر روی Quadrilateralهای دیگری چون Squareها، Parallelogramsها و Trapezoidها نیز انجام پذیرند. چندریختی زمانی رخ میدهد که یک برنامه متدی را از طریق یک متغیر کلاس پایه احضار کند؛ در زمان اجرا، نسخهی کلاس مشتق شدهی صحیح متد بر اساس نوع شیء مورد مراجعه قرار گرفته فراخوان میشود. در بخش 12.3 کد نمونهی سادهای را خواهید دید که این فرایند را در قالب یک مثال نشان میدهد.
سلسله مراتب بازی ویدیویی SpaceObject
به عنوان یک مثال دیگر، فرض کنید یک بازی ویدیویی طراحی میکنیم که با انواع بسیار متفاوتی از اشیاء سروکار دارد که این اشیاء شامل اشیاء کلاسهای Martin، Venusian، Plutonian، SpaceShip و LaserBeam هستند. تصور کنید که هر کلاسی از کلاس پایهی مشترک SpaceObject ارث میبرد؛ این کلاس حاوی متد Draw است. هر کلاس مشتق شدهای این متد را پیادهسازی میکند. یک برنامهی مدیر صحنه مجموعهای (مثلاً یک آرایهی SpaceObject) از مراجعات به اشیاء چندین کلاس را نگهداری میکند. به منظور بازآرایی صحنه، مدیر صحنه به طور متناوب به هر شیئی پیغام یکسانی را ارسال میکند؛ یعنی Draw. با این وجود، هر شیئی به روش منحصر به فردی واکنش نشان میدهد. برای مثال، یک شیء Martin میتواند خودش را همراه با تعداد مناسبی از شاخکها به رنگ قرمز ترسیم نماید.
یک شیء SpaceShip میتواند خودش را به صورت یک بشقاب پرندهی نقرهای روشن ترسیم نماید. یک شیء LaserBeam میتواند خودش را به صورت یک پرتو قرمز روشن در طول صحنه ترسیم کند. دوباره، پیغام یکسانی (در این مورد، Draw) به طیفی از اشیاء که دارای فرمهای زیادی از نتایج هستند ارسال میشود. یک مدیر صحنه چندریخت میتواند از چندریختی استفاده کند تا افزودن کلاسهای جدید به یک سیستم را با کمترین تغییرات اعمالی به کد سیستم تسهیل سازد. فرض کنید که میخواهیم اشیاء Mercurian را به بازی ویدیویی خودمان اضافه نماییم. برای انجام این کار، باید یک کلاس Mercurian را ایجاد کنیم که این کلاس SpaceObject را بسط داده و پیادهسازی متعلق به خود را از متد Draw در اختیار میگذارد. هرگاه اشیاء کلاس Mercurian در مجموعهی SpaceObject ظاهر شود، کد مدیر صحنه متد Draw را احضار میکند، دقیقاً به همان صورتی که برای هر شیء دیگر واقع در مجموعه علیرغم نوع آن شیء انجام میدهد، ازاینرو اشیاء جدید Mercurian بدون هیچ تغییری در کد مدیر صحنه توسط برنامهنویس به سادگی کار خود را انجام میدهند. بنابراین، بدون تغییر دادن سیستم (غیر از ایجاد کلاسهای جدید و اصلاح کد ایجاد کنندهی اشیاء جدید)، میتوانید چندریختی را به کار ببرید تا نوعهای دیگری را که ممکن است هنگام ایجاد شدن سیستم متصور نشده باشند ضمیمهی سیستم نمایید.
ملاحظاتی در باب مهندسی نرمافزار 12.1
چندریختی قابلیت گسترشپذیری را ترقی میدهد: نرمافزاری که رفتار چندریختی را احضار میکند مستقل از انواع شیئی است که پیغامها به آنها ارسال میشود. انواع شیء جدیدی که میتوانند به فراخوانهای متد موجود واکنش نشان دهند میتوانند بدون نیاز به اصلاح سیستم پایه در سیستم گنجانده شوند. تنها کد سرویس گیرندهای که اشیاء جدید را نمونهسازی میکند میبایست تغییر داده شود تا با نوعهای جدید تطبیق پیدا کند.
12.3 تبیین رفتار چندریختی
Demonstrating Polymorphic Behavior
بخش 11.4 سلسله مراتب کلاسی یک کارمند پیمانی را ایجاد کرد که در آن کلاس BasePlusCommissionEmployee از کلاس CommissionEmployee ارث میبرد. مثالهای موجود در آن بخش اشیاء CommissionEmployee و BasePlusCommissionEmployee را با استفاده از مراجعاتی به آنها برای احضار متدهایشان دستکاری میکردند. ما مراجعات کلاس مبنا را در اشیاء کلاس مبنا و مراجعات کلاس مشتق شده را در اشیاء کلاس مشتق شده ارزیابی کردیم. این تخصیصات طبیعی و سرراست هستند؛ مراجعات کلاس مبنا نامزد مراجعه به اشیاء کلاس مبنا هستند و مراجعات کلاس مشتق شده نازمد مراجعه به اشیاء کلاس مشتق شده هستند. با این وجود، تخصیصات دیگر نیز امکانپذیرند.
در مثال بعد، مراجعهی کلاس مبنا را در یک شیء کلاس مشتق شده ارزیابی خواهیم کرد. پس از آن نحوهی احضار یک متد واقع بر روی یک شیء کلاس کلاس مشتق شده را از طریق یک مراجعهی کلاس مبنا که قادر به احضار کارکردهای کلاس مشتق شده است، نشان میدهیم— نوع شیء مورد مراجعه قرار گرفته واقعی و نه نوع مراجعه مشخص کنندهی متدی است که احضار شده است. این مثال این مفهوم کلیدی را تبیین میکند که با یک شیء از یک کلاس مشتق شده میتوان به صورت یک شیء از کلاس مبنایش برخورد کرد. این کار پیادهسازیهای جذاب و متنوعی را امکانپذیر میسازد. یک برنامه قادر است تا آرایهای از مراجعات کلاس مبنا را ایجاد کند که به شیءهایی از انواع زیادی از کلاس مشتق شده مراجعه میکنند. این کار به این خاطر مجاز شده است که هر شیء کلاس مشتق شده یک شیء از کلاس مبنای خود است. برای نمونه، ما میتوانیم مراجعهی یک شیء BasePlusCommissionEmployee را به یک متغیر کلاس مبنای CommissionEmployee تخصیص دهیم زیرا یک BasePlusCommissionEmployee یک CommissionEmployee است؛ ازاینرو ما میتوانیم با یک BasePlusCommissionEmployee به صورت یک CommissionEmployee برخورد کنیم.
یک شیء کلاس مبنا شیء هیچ یک از کلاسهای مشتق شدهاش نیست. برای مثال، ما نمیتوانیم به طور مستقیم مراجعهی یک شیء CommissionEmployee را به یک متغیر کلاس مشتق شدهی BasePlusCommissionEmployee نسبت دهیم، زیرا یک CommissionEmployee یک BasePlusCommissionEmployee نیست؛ برای نمونه، یک CommissionEmployee دارای متغیر نمونهی baseSalary و خاصیت BaseSalary نیست. رابطهی «یک...است» از یک کلاس مشتق شده به کلاسهای مبنای مستقیم و غیرمستقیماش اعمال میشود اما حالت برعکس امکانپذیر نیست. اگر متغیر کلاس مبنا را به طور صریح به نوع کلاس مشتق شده قالببندی (تبدیل) کنیم، کامپایلر اجازهی تخصیص یک مراجعهی کلاس مبنا به یک متغیر کلاس مشتق شده را خواهد داد؛ این تکنیکی است که با جزئیات بیشتری آن را در بخش 12.5.6 بحث خواهیم کرد. چرا ما همیشه میخواهیم تا چنین تخصیص را انجام دهیم؟ یک مراجعهی کلاس مبنا میتواند تنها برای احضار متدهای اعلان شده در کلاس مبنا به کار گرفته شود؛ هرگونه تلاش برای احضار متدهایی که صرفاً متعلق به کلاس مشتق شده هستند از طریق یک مراجعهی کلاس مبنا منجر به بروز خطاهای زمان کامپایل خواهد شد. اگر برنامهای نیازمند انجام یک عمل مختص کلاس مشتق شده بر روی یک شیء کلاس مشتق شدهای است که توسط یک متغیر کلاس مبنا مورد مراجعه قرار گرفته است، برنامه میبایست قبل از همه چیز مراجعهی کلاس مبنا را از طریق یک تکنیک شناخته شده بنام downcasting به یک مراجعهی کلاس مشتق شده قالببندی (تبدیل نوع) کند. این کار برنامه را قادر میسازد تا متدهای کلاس مشتق شدهای را احضار کند که در کلاس مبنا حضور ندارند. در بخش 12.5.6 مثالی را در خصوص downcasting تقدیم حضورتان خواهیم کرد.
شکل 12.1 سه روش استفاده از متغیرهای کلاس مبنا و کلاسهای مشتق شده برای ذخیرهسازی مراجعات به اشیاء کلاس مبنا و کلاس مشتق شده را تبیین میکند. دوتای اولی سرراستاند؛ همانند بخش 11.4، یک مراجعهی کلاس مبنا را به یک متغیر کلاس مبنا تخصیص میدهیم و یک مراجعهی کلاس مشتق شده را به یک متغیر کلاس مشتق شده نسبت میدهیم. پس از آن با تخصیص یک مراجعهی کلاس مشتق شده به یک متغیر کلاس مبنا، رابطهی مابین کلاسهای مشتق شده و کلاسهای مبنا (یعنی رابطهی «یک...است») را نشان میدهیم. (نکته: این برنامه به ترتیب از کلاسهای CommissionEmployee و BasePlusCommissionEmployee شکل 11.12 و شکل 11.13 استفاده میکند.)
// Fig. 12.1: PolymorphismTest.cs // Assigning base class and derived class references to base class and // derived class variables. using System; public class PolymorphismTest { public static void Main( string[] args ) { // assign base class reference to base class variable CommissionEmployee commissionEmployee = new CommissionEmployee( "Sue", "Jones", "222-22-2222", 10000.00M, .06M ); // assign derived class reference to derived class variable BasePlusCommissionEmployee basePlusCommissionEmployee = new BasePlusCommissionEmployee( "Bob", "Lewis", "333-33-3333", 5000.00M, .04M, 300.00M ); // invoke ToString and Earnings on base class object // using base class variable Console.WriteLine( "{0} {1}:\n\n{2}\n{3}: {4:C}\n", "Call CommissionEmployee's ToString and Earnings methods", "with base class reference to base class object", commissionEmployee.ToString(), "earnings", commissionEmployee.Earnings() ); // invoke ToString and Earnings on derived class object // using derived class variable Console.WriteLine( "{0} {1}:\n\n{2}\n{3}: {4:C}\n", "Call BasePlusCommissionEmployee's ToString and Earnings", "methods with derived class reference to derived class object", basePlusCommissionEmployee.ToString(), "earnings", basePlusCommissionEmployee.Earnings() ); // invoke ToString and Earnings on derived class object // using base class variable CommissionEmployee commissionEmployee2 = basePlusCommissionEmployee; Console.WriteLine( "{0} {1}:\n\n{2}\n{3}: {4:C}", "Call BasePlusCommissionEmployee's ToString and Earnings", "methods with base class reference to derived class object", commissionEmployee2.ToString(), "earnings", commissionEmployee2.Earnings() ); } // end Main } // end class PolymorphismTest Call CommissionEmployee's ToString and Earnings methods with base class reference to base class object: commission employee: Sue Jones social security number: 222-22-2222 gross sales: $10,000.00 commission rate: 0.06 earnings: $600.00 Call BasePlusCommissionEmployee's ToString and Earnings methods with derived class reference to derived class object: base-salaried commission employee: Bob Lewis social security number: 333-33-3333 gross sales: $5,000.00 commission rate: 0.04 base salary: $300.00 earnings: $500.00 Call BasePlusCommissionEmployee's ToString and Earnings methods with base class reference to derived class object: base-salaried commission employee: Bob Lewis social security number: 333-33-3333 gross sales: $5,000.00 commission rate: 0.04 base salary: $300.00 earnings: $500.00
شکل 12.1 | تخصیص مراجعات کلاس مبنا و کلاس مشتق شده به متغیرهای کلاس مبنا و کلاس مشتق شده.
در شکل 12.1، خطوط 11 تا 12 یک شیء جدید CommissionEmployee را ایجاد کرده و مراجعهی آن را به یک متغیر CommissionEmployee تخصیص میدهند. خطوط 15 تا 17 یک شیء جدید BasePlusCommissionEmployee را ایجاد کرده و مراجعهی آن را به یک متغیر BasePlusCommissionEmployee تخصیص میدهند. این تخصیصات عدای هستند؛ برای مثال، هدف اصلی یک متغیر CommissionEmployee نگهداری یک مراجعه به یک شیء CommissionEmployee است. خطوط 21 تا 25 مراجعهی commissionEmployee را برای احضار متدهای ToString و Earnings مورد استفاده قرار میدهند. از آن جایی که commissionEmployee به یک شیء CommissionEmployee اشاره میکند، نسخهی کلاس پایهی CommissionEmployee متدها فراخوان میشوند. به طور مشابه، خطوط 29 تا 33 از مراجعهی basePlusCommissionEmployee استفاده میکنند تا متدهای ToString و Earnings را بر روی شیء basePlusCommissionEmployee احضار کنند. این کار نسخهی کلاس مشتق شدهی BasePlusCommissionEmployee متدها را احضار میکند. پس از آن خطوط 37 تا 38 مراجعه به شیء کلاس مشتق شدهی BasePlusCommissionEmployee را به یک متغیر کلاس پایهی CommissionEmployee نسبت میدهند، که خطوط 39 تا 43 از آن استفاده میکنند تا متدهای ToString و Earnings را احضار نمایند. متغیر کلاس پایهای که حاوی یک مراجعه به یک کلاس مشتق شده است و برای فراخوان یک متد virtual به کار گرفته میشود درواقع نسخهی بازتعریف شدهی کلاس مشتق شده از متد را فرامیخواند. ازاینرو، commissionEmployee2.ToString() در خط 42 در واقع متد ToString کلاس مشتق شدهی BasePlusCommissionEmployee را فرامیخواند. کامپایلر اجازهی این تغییر حالت را میدهد زیرا یک شیء از کلاس مشتق شده یک شیء از کلاس پایه خود است (اما حالت برعکس درست نیست). هرگاه کامپایلر با فراخوان متدی که از طریق یک متغیر انجام شده است مواجه شود، کامپایلر تشخیص میدهد که آیا متد میتواند با بررسی نوع کلاس متغیر فراخوان شود. اگر آن کلاس حاوی اعلان متد مناسب باشد (یا یکی را به ارث ببرد)، کامپایلر به فراخوان اجازه میدهد تا کامپایل شود. در زمان اجرا، نوع شیئی که متغیر به آن اشاره میکند متد واقعی را برای استفاده مشخص میکند.
12.4 کلاسها و متدهای انتزاعی
Abstract Classes and Methods
هنگامی که در مورد نوع یک کلاس فکر میکنیم، فرض ما بر این است که برنامهها اشیاء آن نوع را ایجاد خواهند کرد. با اینحال، در برخی از موارد بهتر است کلاسهایی را اعلان نمود که برای آنها هرگز هیچ شیئی را نمونهسازی نخواهید کرد. چنین کلاسهایی، کلاسهای انتزاعی نامیده میشوند. از آن جایی که این کلاسها در سلسله مراتب ارثبری فقط به عنوان کلاسهای مبنا به کار گرفته میشوند، ما با عنوان کلاسهای مبنای انتزاعی به آنها اشاره خواهیم کرد. این کلاسها نمیتوانند برای نمونهسازی اشیاء به کار برده شوند، زیرا همان طور که به زودی خواهید دید، کلاسهای انتزاعی ناتمام هستند؛ کلاسهای مشتق شده باید «بخشهای مفقود شده» را تعریف نمایند. در بخش 12.5.1 کلاسهای انتزاعی را بررسی خواهیم کرد.
هدف از یک کلاس انتزاعی در اصل تدارک دیدن کلاس مبنای مناسبی است که کلاسهای دیگر میتوانند از آن ارث ببرند و ازاینرو این کلاس طرح مشترکی را به اشتراک میگذارد. برای نمونه در سلسله مراتب Shape شکل 11.3، کلاسهای مشتق شده مفهومی را به ارث میبرند که معنی یک Shape از آن استنباط خواهد شد؛ مشخصات مشترکی چون location، color و borderThickness و رفتارهایی مانند Draw، Move، Resize و ChangeColor. کلاسهایی که بتوانند برای نمونهسازی اشیاء به کار برده شوند کلاسهای مقید نامیده میشوند. چنین کلاسهایی پیادهسازیهای هر متدی را که اعلان میکنند تدارک میبینند (برخی از پیادهسازیها میتوانند موروثی باشند). برای مثال، میتوانیم کلاسهای مقید Circle، Square و Triangle را از کلاس مبنای انتزاعی TwoDimensionalShape مشتق کنیم. به طور مشابه، ما میتوانیم کلاسهای مقید Sphere، Cube و Tetrahedron را از کلاس مبنای انتزاعی ThreeDimensionalShape مشتق کنیم. کلاسهای مبنای انتزاعی برای ایجاد اشیاء واقعی بیش از حد کلی هستند؛ این کلاسها تنها مشخص میکنند که چه چیزهایی مابین کلاسهای مشتق شده مشترک هستند. قبل از این که بتوانیم اشیاء را ایجاد نماییم نیاز داریم تا دقیقتر شویم. برای مثال، اگر شما پیغام Draw را به کلاس انتزاعی TwoDimensionalShape ارسال کنید، کلاس میداند که اشکال دوبعدی باید قابل ترسیم باشند، اما نمیداند که چه شکل خاصی باید ترسیم گردد، ازاینرو نمیتواند یک متد Draw واقعی را پیادهسازی نماید. کلاسهای مقید مشخصاتی را در اختیار میگذارند که نمونهسازی اشیاء را مقرون به صرفه میسازند. همهی سلسله مراتب ارثبری حاوی کلاسهای انتزاعی نیستند. با این وجود، شما اغلب کد سرویس گیرندهای را خواهید نوشت که انواع کلاس مبنای انتزاعی را مورد استفاده قرار میدهد تا نیازمندیهای کد سرویس گیرنده را بر روی محدودهای از انواع کلاس مشتق شدهی خاص کاهش دهد. برای مثال، شما میتوانید متدی را همراه با یک پارامتر از یک نوع کلاس مبنای انتزاعی بنویسید. یک شیء از هر کلاس مقیدی که به طور مستقیم یا غیرمستقیم کلاس مبنای مشخص شده به عنوان نوع پارامتر را بسط میدهد میتواند هنگام فراخوان شدن چنین متدی ارسال گردد.
کلاسهای انتزاعی گاهاً سطوح متعددی از سلسله مراتب را شکل میدهند. برای مثال، سلسله مراتب Shape شکل 11.3 با کلاس انتزاعی Shape شروع میشود. بر روی سطح بعدی سلسله مراتب دو کلاس انتزاعی دیگر قرار دارد: TwoDimensionalShape و ThreeDimensionalShape. سطح بعدی سلسله مراتب کلاسهای مقیدی را برای TwoDimensionalShapeها (یعنی کلاسهای Circle، Square و Triangle) و ThreeDimensionalShapeها (یعنی کلاسهای Sphere، Cube و Tetrahedron) اعلان میکند. شما میتوانید با اعلان یک کلاس با کلمهی کلیدی abstract آن کلاس را انتزاعی سازید. یک کلاس انتزاعی معمولاً حاوی یک یا چند متد انتزاعی است. همان طور که در عبارت زیر نشان داده شده است، متد انتزاعی متدی است که کلمهی کلیدی abstract در اعلان آن وجود دارد:
public abstract void Draw(); // abstract method
متدهای انتزاعی به طور ضمنی virtual بوده و هیچ پیادهسازی را در اختیار نمیگذارند. کلاسی که حاوی متدهای انتزاعی است باید به صورت یک کلاس انتزاعی اعلان شود حتی اگر حاوی چندین متد مقید (غیرانتزاعی) باشد. هر یک از کلاسهای مشتق شدهی مقید یک کلاس مبنای انتزاعی نیز باید پیادهسازیهای مقید متدهای انتزاعی کلاس مبنا را تدارک ببینند. در شکل 12.4 نمونهای از یک کلاس انتزاعی را همراه با یک متد انتزاعی نشان دادهایم.
خاصیت نیز میتوانند یا به صورت abstract و virtual اعلان شوند، سپس در کلاسهای مشتق شده بوسیلهی کلمهی کلیدی overriade بازتعریف شوند، درست مانند متدها. این کار به یک کلاس مبنای abstract اجازه میدهد تا خاصیتهای مشترک کلاسهای مشتق شدهاش را مشخص کند. اعلان خاصیتهای abstract بفرم زیر است:
public abstract PropertyType MyProperty
{
get;
set;
} // end abstract property
نقطه ویرگولی که بعد از کلمات کلیدی get و set قرار دارد نشانگر این است که ما هیچ پیادهسازی را برای این توابع دسترسی آماده نکردهایم. یک خاصیت انتزاعی ممکن است پیادهسازیهای مربوط به توابع دسترسی get یا set را از قلم بیندازد. کلاسهای مشتق شدهی مقید باید پیادهسازیهایی را برای هر تابع دسترسی اعلان شده در خاصیت انتزاعی آماده کنند. هرگاه هردو تابع دسترسی get و set مشخص شده باشند، هر کلاس مشتق شدهی مقید باید هردوی آنها را پیادهسازی کند. اگر یکی از توابع دسترسی کنار گذاشته شود، کلاس مشتق شده مجاز به پیادهسازی آن تابع دسترسی نیست. انجام این کار باعث بروز یک خطای زمان کامپایل میشود. سازندهها و متدهای استاتیک نمیتوانند به صورت abstract اعلان شوند. سازندهها به ارث برده نمیشوند، ازاینرو یک سازندهی abstract هرگز نمیتواند پیادهسازی شود. مشابهاً، کلاسهای مشتق شده نمیتوانند متدهای استاتیک را بازتعریف کنند، ازاینرو یک متد abstract static هرگز نمیتوانست پیادهسازی شود.
ملاحظاتی در باب مهندسی نرمافزار 12.2
یک کلاس abstract تعریف کنندهی مشخصهها و رفتارهای مشترک چندین کلاس است که در یک سلسله مراتب کلاسی، چه به طور مستقیم و یا غیر مستقیم از آن کلاس ارث میبرند. یک کلاس abstract نوعاً حاوی یک یا چند متد یا خاصیت abstract است که کلاسهای مشتق شدهی مقید باید آنها را بازتعریف کنند. متغیرهای نمونه، متدهای مقید و خاصیتهای مقید یک کلاس abstract تحت کنترل قواعد معمول ارثبری قرار دارند.
خطای برنامهنویسی رایج 12.1
هرگونه تلاش برای نمونهسازی یک کلاس انتزاعی، یک خطای زمان کامپایل در خواهد داشت.
خطای برنامهنویسی رایج 12.2
کوتاهی در پیادهسازی متدها و خاصیتهای abstract یک کلاس مبنا در یک کلاس مشتق شده یک خطای زمان کامپایل در پی خواهد داشت مگر این که کلاس مشتق شده نیز به صورت abstract اعلان شده باشد.
با وجود این که ما نمیتوانیم اشیاء کلاسهای مبنای abstract را نمونهسازی کنیم، به زودی خواهید دید که میتوانیم کلاسهای مبنای abstract را مورد استفاده قرار دهیم تا متغیرهایی را اعلان کنیم که این متغیرهای میتوانند مراجعات به اشیاء هر کلاس مقید مشتق شده از آن کلاسهای مبنای abstract را نگهداری کنند. برنامهها به طور معمول چنین متغیرهایی را به کار میبرند تا اشیاء کلاس مشتق شده را به صورت چندریختی دستکاری کنند. درضمن، شما میتوانید اسامی کلاس مبنای انتزاعی را مورد استفاده قرار دهید تا متدهای استاتیک اعلان شده در آن کلاسهای مبنای انتزاعی را احضار کنید.
چندریختی و درایورهای دستگاه
چنریختی مخصوصاً برای پیادهسازی سیستمهای نرمافزاری به اصطلاح طبقهبندی شده مؤثر و کارآمد میباشد. برای نمونه در سیستم عاملها، هر دستگاه فیزیکی میتواند متفاوت از سایرین عمل نماید. با این حال، فرامین مشترکی میتوانند داده را از دستگاهها خوانده و به آنها بنویسند. برای هر دستگاه، سیستم عامل یک قطعهی نرمافزاری بنام درایور دستگاه را برای کنترل تمامی ارتباطات مابین سیستم و دستگاه، مورد استفاده قرار میدهد. نوشتن پیغام ارسالی به یک شیء درایور دستگاه نیازمند تفسیر ویژه در متن آن درایور و نحوهی دستکاری یک دستگاه خاص توسط آن درایور است. با این وجود، خود نوشتن فراخوان واقعاً از نوشتن به هر دستگاه دیگر در سیستم متفاوت نیست: تعدادی از بایتها را از حافظه بر روی آن دستگاه جای دهید. یک سیستم عامل شیءگرا میتواند یک کلاس مبنای انتزاعی را مورد استفاده قرار دهد تا «واسطی» را تدارک ببیند که مناسب تمامی درایورهای دستگاه باشد. پس از آن، از طریق ارثبری از آن کلاس مبنای انتزاعی، کلاسهای مشتق شدهای شکل میگیرند که همگی به شکل مشابهی رفتار میکنند. متدهای درایور دستگاه در کلاس مبنای انتزاعی به صورت متدهای انتزاعی اعلان میشوند. پیادهسازی این متدهای انتزاعی در کلاسهای مشتق شدهای تدارک دیده میشوند که این کلاسها با نوعهای مختص درایورهای دستگاه متناظرند. دستگاهها اغلب مدتها پس از ارائهی سیستم عامل توسعه داده میشوند. هرگاه یک دستگاه جدید را خریداری کنید، این دستگاه همراه با یک درایور دستگاهی که توسط فروشندهی دستگاه فراهم شده است ارائه میشود. بعد از این که دستگاه را به کامپیوتر خود وصل کنید و درایور آن را نصب نمایید این دستگاه بلافاصله قابل استفاده خواهد شد. این مثال نمونهی خوب دیگریست که نشان میدهد چندریختی چگونه سیستمها را قابل توسعه میسازد.
تکرارکنندهها
در برنامهنویسی شیءگرا اعلان یک کلاس تکرارکننده که بتواند تمامی اشیاء موجود در یک کلکسیون مانند یک آرایه (فصل 8) یا یک لیست (فصل 9) را پیمایش کند رایج میباشد. برای مثال، یک برنامه میتواند با ایجاد یک شیء تکرارکننده و استفاده از آن برای بدست آوردن عنصر بعدی لیست در هر بار فراخوان شدن تکرارکننده، لیستی از اشیاء چاپ نماید. تکرار کنندهها اغلب اوقات در برنامهنویسی چندریخت برای پیمایش کلکسیونی به کار گرفته میشوند به کار گرفته میشوند که این کلکسیون حاوی مراجعات به اشیاء کلاسهای مختلف واقع در یک سلسله مراتب ارثبری میباشد. (در فصول 22 و 23 رفتار کامل قابلیتها و تکرارکنندههای «جنریکهای» C# ارائه خواهند شد.) برای نمونه، یک List از مراجعات به اشیاء کلاس TwoDimensionalShape میتواند حاوی مراجعاتی به اشیاء از کلاسهای Square، Circle، Triangle و غیره باشد. فراخوان متد Draw برای هر شیء TwoDimensionalShape از سوی یک متغیر TwoDimensionalShape به شکل چندریختی هر شیئی را به درستی بر روی صفحه ترسیم خواهد کرد.
12.5 مطالعهی موردی: سیستم پرداخت حقوق با استفاده از چندریختی
Case Study: Payroll System Using Polymorphism
این بخش سلسله مراتب CommissionEmployee-BasePlusCommissionEmployee را که در سرتاسر بخش 11.4 مورد کنکاش قرار دادیم بار دیگر مورد بازبینی قرار میدهد. اکنون یک متد انتزاعی و چندریختی را مورد استفاده قرار میدهیم تا محاسبات سیستم پرداخت حقوق را براساس نوع کارمند به انجام رسانیم. برای حل مسألهی زیر یک سلسله مراتب بهبود یافته را ایجاد خواهیم کرد:
شرکتی حقوق کارمندان خود را به صورت هفتگی پرداخت میکند. کارمندان به چهار دسته تقسیم میشوند: کارمندان حقوق بگیری که علیرغم ساعات کار هفتگی دارای یک حقوق هفتگی ثابتاند، کارمندان ساعتی بر اساس کاری حقوق میگیرند و اگر ساعات کاریشان از 40 ساعت بگذرد اضافه حقوق دریافت میکنند، کارمندان پیمانی که درصدی از فروش خود را به عنوان حقوق دریافت میکنند و کارمندان پیمانی با حقوق ثابت که دارای یک دستمزد ثابت بوده و درصدی از فروش خود را دریافت میکنند. برای دورهی پرداخت جاری، شرکت تصمیم گرفته است تا با افزودن 10 درصد به حقوق ثابت کارمندان پیمانی با حقوق ثابت، به آنها پاداش دهد. شرکت قصد دارد تا برنامهی C# را پیادهسازی کند تا محاسبات مربوط به پرداخت حوقوق را به صورت چندریختی انجام دهد.
برای بیان مفهوم کلی یک کارمند کلاس Employee را که به صورت abstract اعلان شده است مورد استفاده قرار میدهیم. کلاسهایی که Employee را بسط و گسترش میدهند شامل کلاسهای SalariedEmployee، CommissionEmployee و HourlyEmployee است. کلاس BasePlusCommissionEmployee که کلاس CommissionEmployee را بسط میدهد بیانگر آخرین نوع کارمند است. دیاگرام UML کلاس واقع در شکل 12.2 نشان دهندهی سلسله مراتب ارثبری مربوط به برنامهی پرداخت حقوق چندریخت کارمند ماست. کلاس انتزاعی Employee مطابق قراردهای UML به صورت ایتالیک (مایل) نوشته شده است.
شکل 12.2 | دیاگرام کلاس UML سلسله مراتب Employee.
کلاس مبنا و انتزاعی Employee واسطی را برای سلسله مراتب اعلان میکند؛ یعنی مجموعهای از متدها که یک برنامه قادر است آنها را بر روی همهی اشیاء Employee احضار کند. در این جا عبارت «واسط» را به صورت یک مفهوم کلی به کار بستهایم تا به روشهای متعددی که برنامهها میتوانند از طریق آنها با اشیاء هر کلاس مشتق شده از Employee ارتباط برقرار کنند اشاره کنیم. مراقب باشید مفهوم کلی یک «واسط» را با مفهوم قراردادی یک واسط C#، که موضوع بخش 12.7 است، اشتباه نگیرید. هر کارمند علیرغم نحوهی محاسبهی درآمدش، دارای یک نام، یک نام خانوادگی و یک شمارهی تأمین اجتماعی است ازاینرو این قطعات داده در یک کلاس مبنا و انتزاعی Employee ظاهر میشوند.
ملاحظاتی در باب مهندسی نرمافزار 12.3
یک کلاس مشتق میتواند «واسط» یا «پیادهسازی» را از یک کلاس مبنا ارث ببرد. سلسله مراتب طراحی شده برای ارثبری پیادهسازی تمایل دارند تا در سلسله مراتب دارای کارکردهای بالاتری باشند؛ هر کلاس مشتق شدهی جدید یک یا چند متد پیادهسازی شده در یک کلاس مبنا را ارث میبرد و کلاس مشتق شده از پیادهسازیهای کلاس مبنا استفاده میکند. سلسله مراتب طراحی شده برای ارثبری واسط تمایل دارند تا دارای پایینترین کارکرد در سلسله مرتبه باشند؛ یک کلاس مبنا یک یا چند متد انتزاعی را مشخص میکند که باید برای هر کلاس مقید واقع در سلسله مراتب اعلان شوند و کلاسهای مشتق شدهی مجزا این متدها را بازتعریف میکنند تا پیادهسازیهای مختص کلاس مشتق شده را تدارک ببینند.
بخشهای آتی سلسله مراتب کلاس Employee را پیادهسازی میکنند. بخش نخست کلاس مبنا و انتزاعی Employee را پیادهسازی میکند. چهار بخش بعدی هر کدام یکی از کلاسهای مقید را پیادهسازی میکنند. بخش ششم یک برنامهی آزمایشی را پیادهسازی میکند که این برنامه شیءهایی را از تمامی این کلاسها ایجاد کرده و آنها را به صورت چندریختی پردازش میکند.
12.5.1 ایجاد کلاس مبنای انتزاعی Employee
کلاس Employee (شکل 12.4) علاوه بر خاصیتهای پیادهسازی شدهی خودکاری که دادههای Employee را دستکاری میکنند، متدهای Earnings و ToString را تدارک دیده است. به طور قطع و یقین متد Earnings به صورت کلی به همهی کارمندان اعمال میشود. اما هر یک از محاسبات درآمدی بستگی به کلاس کارمند دارد. بنابراین متد Earnings را در کلاس مبنای Employee به صورت abstract اعلان میکنیم زیرا پیادهسازی پیش فرض معنی خاصی را برای آن متد تداعی نمیکند؛ در آنجا اطلاعات کافی وجود ندارد تا مشخص کند که متد Earnings چه مبلغی را برگشت دهد. هر یک از کلاسهای مشتق شده متد Earnings را با یک پیادهسازی مناسب بازتعریف میکنند. برای محاسبهی درآمد یک کارمند، برنامه یک مراجعه به شیء کارمند را به یک متغیر کلاس مبنای Employee تخصیص میدهد، پس از آن متد Earnings را روی آن متغیر احضار میکند. ما آرایهای از متغیرهای Employee را نگهداری میکنیم که تک تک این متغیرها یک مراجعه به یک شیء Employee را در خود نگه میدارند (البته، در اینجا هیچ شیء Employee نمیتواند وجود داشته باشد زیرا Employee یک کلاس انتزاعی است؛ اگر چه به خاطر ارثبری، همهی اشیاء تمامی کلاسهای مشتق شده از Employee را هنوز هم میتوان به صورت اشیاء Employee در نظر گرفت). برنامه سرتاسر آرایه را پیمایش کرده و متد Earnings را برای تک تک اشیاء Employee فرامیخواند. C# این فراخوانهای متد را به صورت چندریختی پردازش میکند. جای دادن Earnings در Employee به صورت یک متد انتزاعی هر کلاس مقیدی را که به شکل مستقیم از کلاس Employee مشتق شده باشد وادار میکند تا Earnings را با متدی بازتعریف کند که یک محاسبهی پرداخت مقتضی را آنجام میدهد.
متد ToString در کلاس Employee رشتهای برمیگرداند که حاوی نام، نام خانوادگی و شمارهی تأمین اجتماعی کارمند است. هر کلاس مشتق شده از Employee متد ToString را بازتعریف میکند تا یک نمایش رشتهای از یک شیء آن کلاس را ایجاد کند که حاوی نوع کارمندی (مثلاً، "salaried employee:" باشد که با بقیهی اطلاعات کارمند پی گرفته میشود.
دیاگرام واقع در شکل 12.3 پنج کلاس موجود در سلسله مراتب را در سمت چپ و متدهای Earnings و ToString را سرستونهای بالای جدول نمایش میدهد. برای هر کلاس، دیاگرام نتایج مورد نظر هر متد را نشان میدهد. (نکته: در اینجا خاصیتهای کلاس مبنای Employee را لیست نکردهایم زیرا آنها د رهیچ یک از کلاسهای مشتق شده بازتعریف نمیشوند؛ کلاسهای مشتق شده هر یک از این خاصیتها را به همان صورتی هستند به ارث برده و به کار میبندند.)
شکل 12.3 | واسط چندریختی برای کلاسهای سلسله مراتب Employee.
اجازه دهید اعلان کلاس Employee (شکل 12.4) را در نظر بگیریم. این کلاس حاوی این موارد است: سازندهای که نام، نام خانوادگی و شمارهی تأمین اجتماعی یک کارمند را به عنوان آرگومان دریافت میکند (خطوط 15 تا 20)؛ خاصیتهای فقط خواندنی برای بدست آوردن نام، نام خانوادگی و شمارهی تآمین اجتماعی (به ترتیب خطوط 6، 9 و 12)؛ متد ToString (خطوط 23 تا 27) که خاصیتها را مورد استفاده قرار میدهد تا نمایش رشتهای از Employee را نمایش دهد؛ و متد انتزاعی Earnings (خط 30) که باید توسط کلاسهای مشتق شدهی مقید و ملموس پیادهسازی شود. سازندهی Employee شمارهی تأمین اجتماعی را در این مثال اعتبارسنجی نمیکند. به طور کلی، چنین اعتبارسنجی باید تدارک دیده شود.
// Fig. 12.4: Employee.cs // Employee abstract base class. public abstract class Employee { // read-only property that gets employee's first name public string FirstName { get; private set; } // read-only property that gets employee's last name public string LastName { get; private set; } // read-only property that gets employee's social security number public string SocialSecurityNumber { get; private set; } // three-parameter constructor public Employee( string first, string last, string ssn ) { FirstName = first; LastName = last; SocialSecurityNumber = ssn; } // end three-parameter Employee constructor // return string representation of Employee object, using properties public override string ToString() { return string.Format( "{0} {1}\nsocial security number: {2}", FirstName, LastName, SocialSecurityNumber ); } // end method ToString // abstract method overridden by derived classes public abstract decimal Earnings(); // no implementation here } // end abstract class Employee
شکل 12.4 | کلاس مبنای مجرد Employee.
چرا متد Earnings را به صورت یک متد abstract اعلان کردیم؟ همان طور که پیش از این شرح داده شد، فراهمسازی پیادهسازی این متد در کلاس Employee به نظر بی معنی میرسد. ما نمیتوانیم درآمدهای مربوط به یک Employee به صورت کلی محاسبه کنیم؛ ما باید نخست از نوع بخصوص هر Employee آگاه باشیم تا بتوانیم محاسبهی درآمدی مناسب را مشخص کنیم. با اعلان این متد به صورت abstract، مشخص میکنیم که هر کلاس مشتق شدهی مقید باید یک پیادهسازی Earnings درخور تدارک ببیند که در نتیجهی آن برنامه قادر خواهد بود تا متغیرهای کلاس مبنای Employee را مورد استفاده قرار دهد تا متد Earnings را به صورت چندریختی برای هر یک از انواع Employee احضار کند.
12.5.2 ایجاد کلاس مشتق شده و مقید SalariedEmployee
کلاس SalariedEmployee (شکل 12.5) کلاس Employee را بسط و گسترش میدهد (خط 5) و متد Earnings را بازتعریف میکند (خطوط 34 تا 37) که با انجام این کار SalariedEmployee یک کلاس مقید میشود. کلاس شامل این موارد است: یک سازنده (خطوط 10 تا 14) که یک نام، یک نام خانوادگی، یک شمارهی تأمین اجتماعی و یک دستمزد هفتگی را به عنوان آرگومان دریافت میکند؛ خاصیت WeeklySalary (خطوط 17 تا 31) برای دستکاری متغیر نمونهی weeklySalary، که حاوی یک تابع دسترسی set برای حصول اطمینان از عدم تخصیص مقادیر منفی به weeklySalary است؛ متد Earnings (خطوط 34 تا 37) برای محاسبهی در آمد یک SalariedEmployee؛ و متد ToString (خطوط 40 تا 44) که رشتهای را برمیگرداند که حاوی نوع کارمند، یعنی "salaried employee:" است که با اطلاعات مختص کارمند تولید شده توسط متد ToString کلاس مبنای Employee و خاصیت WeeklySalary کلاس SalariedEmployee پی گرفته میشود. سازندهی SalariedEmployee نام، نام خانوادگی و شمارهی تأمین اجتماعی را از طریق یک مقدارگذار سازنده برای مقداردهی دادهی کلاس مبنا، به سازندهی Employee ارسال میکند (خط 11). متد Earnings، متد Earnings کلاس Employee را که به صورت انتزاعی اعلان شده است بازتعریف میکند تا یک پیادهسازی ملموس را که برگشت دهندهی دستمزد هفتگی SalariedEmployee است، فراهم کند. اگر متد Earnings را پیادهسازی نکنیم، کلاس SalariedEmployee باید به صورت abstract اعلان شود؛ در غیر این صورت، یک خطای زمان کامپایل رخ خواهد داد (و البته خواهان این هستیم که SalariedEmployee یک کلاس مقید گردد).
// Fig. 12.5: SalariedEmployee.cs // SalariedEmployee class that extends Employee. using System; public class SalariedEmployee : Employee { private decimal weeklySalary; // four-parameter constructor public SalariedEmployee( string first, string last, string ssn, decimal salary ) : base( first, last, ssn ) { WeeklySalary = salary; // validate salary via property } // end four-parameter SalariedEmployee constructor // property that gets and sets salaried employee's salary public decimal WeeklySalary { get { return weeklySalary; } // end get set { if ( value >= 0 ) // validation weeklySalary = value; else throw new ArgumentOutOfRangeException( "WeeklySalary", value, "WeeklySalary must be >= 0" ); } // end set } // end property WeeklySalary // calculate earnings; override abstract method Earnings in Employee public override decimal Earnings() { return WeeklySalary; } // end method Earnings // return string representation of SalariedEmployee object public override string ToString() { return string.Format( "salaried employee: {0}\n{1}: {2:C}", base.ToString(), "weekly salary", WeeklySalary ); } // end method ToString } // end class SalariedEmployee
شکل 12.5 | کلاس SalariedEmployee که کلاس Employee را بسط میدهد.
متد ToString کلاس SalariedEmployee (خطوط 40 تا 44) نسخهی Employee این متد را بازتعریف میکند. اگر کلاس SalariedEmployee متد ToString را بازتعریف نکند، کلاس SalariedEmployee نسخهی Employee آن را به ارث خواهد برد. در این حالت، متد ToString کلاس SalariedEmployee صرفاً نام، نام خانوادگی و شمارهی تأمین اجتماعی کارمند را برگشت خواهد که به طور مناسب بیانگر یک SalariedEmployee است. برای تولید یک نمایش رشتهای کامل از یک SalariedEmployee، متد ToString کلاس مشتق شده، رشتهی "salaried employee:" را برگشت میدهد که با اطلاعات مختص کلاس مبنای Employee (یعنی نام، نام خانوادگی و شمارهی تأمین اجتماعی) پی گرفته میشود؛ این اطلاعات با احضار متد ToString کلاس مبنا بدست میآید (خط 43)؛ این مثال یکی نمونههای عالی استفادهی مجدد از کد است. نمایش رشتهای یک SalariedEmployee در ضمن حاوی دستمزد هفتگی کارمند است که با استفاده از خاصیت WeeklySalary کلاس قابل حصول است.
12.5.3 ایجاد کلاس مشتق شده و مقید HourlyEmployee
کلاس HourlyEmployee (شکل 12.6) نیز کلاس Employee را بسط میدهد (خط 5). این کلاس حاوی سازندهای (خطوط 11 تا 17) است که یک نام، یک نام خانوادگی، یک شمارهی تأمین اجتماعی، یک حقوق هفتگی و تعداد ساعات کاری را به عنوان آرگومان دریافت میکند. خطوط 20 تا 34 و 37 تا 51 خاصیتهای Wage و Hours را به ترتیب برای متغیرهای نمونهی wage و hours اعلان میکنند. تابع دسترسی set در خاصیت Wage اطمینان میدهد که مقدارمتغیر نمونهی wage نامنفی است و تابع دسترسی set در خاصیت Hours اطمینان میدهد که مقدار متغیر نمونهی hours در دامنهی صفر تا 168 (تعداد کل ساعات واقع در یک هفته) قرار داشته باشد. این کلاس متد Earnings (خطوط 54 تا 60) را بازتعریف میکند تا درآمد یک HourlyEmployee را محاسبه کند و متد ToString (خطوط 63 تا 68) را بازتعریف میکند تا نمایش رشتهای کارمند را برگرداند. سازندهی HourlyEmployee همانند سازندهی SalariedEmployee، نام، نام خانوادگی و شمارهی تأمین اجتماعی را به سازندهی کلاس مبنای Employee ارسال میکند (خط 13) تا دادهی کلاس مبنا را مقداردهی اولیه کند. درضمن، متد ToString این کلاس متد ToString کلاس مبنا را فرامیخواند (خط 67) تا اطلاعات مختص Employee (یعنی نام، نام خانوادگی و شمارهی تأمین اجتماعی) را بدست آورد.
// Fig. 12.6: HourlyEmployee.cs // HourlyEmployee class that extends Employee. using System; public class HourlyEmployee : Employee { private decimal wage; // wage per hour private decimal hours; // hours worked for the week // five-parameter constructor public HourlyEmployee( string first, string last, string ssn, decimal hourlyWage, decimal hoursWorked ) : base( first, last, ssn ) { Wage = hourlyWage; // validate hourly wage via property Hours = hoursWorked; // validate hours worked via property } // end five-parameter HourlyEmployee constructor // property that gets and sets hourly employee's wage public decimal Wage { get { return wage; } // end get set { if ( value >= 0 ) // validation wage = value; else throw new ArgumentOutOfRangeException( "Wage", value, "Wage must be >= 0" ); } // end set } // end property Wage // property that gets and sets hourly employee's hours public decimal Hours { get { return hours; } // end get set { if ( value >= 0 && value <= 168 ) // validation hours = value; else throw new ArgumentOutOfRangeException( "Hours", value, "Hours must be >= 0 and <= 168" ); } // end set } // end property Hours // calculate earnings; override Employee’s abstract method Earnings public override decimal Earnings() { if ( Hours <= 40 ) // no overtime return Wage * Hours; else return ( 40 * Wage ) + ( ( Hours - 40 ) * Wage * 1.5M ); } // end method Earnings // return string representation of HourlyEmployee object public override string ToString() { return string.Format( "hourly employee: {0}\n{1}: {2:C}; {3}: {4:F2}", base.ToString(), "hourly wage", Wage, "hours worked", Hours ); } // end method ToString } // end class HourlyEmployee
شکل 12.6 | کلاس HourlyEmployee که کلاس Employee را بسط میدهد.
12.5.4 ایجاد کلاس مشتق شده و مقید CommissionEmployee
کلاس CommissionEmployee (شکل 12.7) کلاس Employee را بسط میدهد (خط 5). این کلاس حاوی این موارد است: سازندهای (خطوط 11 تا 16) که یک نام، یک نام خانوادگی، یک شمارهی تأمین اجتماعی، میزان فروش و یک نرخ کمیسیون را به عنوان آرگومان دریافت میکند؛ خاصیتهایی (خطوط 19 تا 33 و 36 تا 50) که به ترتیب مربوط به متغیرهای نمونهی grossSales و commissionRate هستند؛ متد Earnings (خطوط 53 تا 56) برای محاسبهی درآمد یک CommissionEmployee؛ و متد ToString (خطوط 59 تا 64) که نمایش رشتهای شیء را برمیگرداند. سازندهی CommissionEmployee نیز نام، نام خانوادگی و شمارهی تأمین اجتماعی را به سازندهی Employee ارسال میکند (خط 12) تا دادهی Employee را مقداردهی اولیه کند. متد ToString کلاس CommissionEmployee متد ToString کلاس مبنا را فرامیخواند (خط 62) تا اطلاعات مختص Employee را بدست آورد (یعنی نام، نام خانوادگی و شمارهی تأمین اجتماعی).
// Fig. 12.7: CommissionEmployee.cs // CommissionEmployee class that extends Employee. using System; public class CommissionEmployee : Employee { private decimal grossSales; // gross weekly sales private decimal commissionRate; // commission percentage // five-parameter constructor public CommissionEmployee( string first, string last, string ssn, decimal sales, decimal rate ) : base( first, last, ssn ) { GrossSales = sales; // validate gross sales via property CommissionRate = rate; // validate commission rate via property } // end five-parameter CommissionEmployee constructor // property that gets and sets commission employee's gross sales public decimal GrossSales { get { return grossSales; } // end get set { if ( value >= 0 ) grossSales = value; else throw new ArgumentOutOfRangeException( "GrossSales", value, "GrossSales must be >= 0" ); } // end set } // end property GrossSales // property that gets and sets commission employee's commission rate public decimal CommissionRate { get { return commissionRate; } // end get set { if ( value > 0 && value < 1 ) commissionRate = value; else throw new ArgumentOutOfRangeException( "CommissionRate", value, "CommissionRate must be > 0 and < 1" ); } // end set } // end property CommissionRate // calculate earnings; override abstract method Earnings in Employee public override decimal Earnings() { return CommissionRate * GrossSales; } // end method Earnings // return string representation of CommissionEmployee object public override string ToString() { return string.Format( "{0}: {1}\n{2}: {3:C}\n{4}: {5:F2}", "commission employee", base.ToString(), "gross sales", GrossSales, "commission rate", CommissionRate ); } // end method ToString } // end class CommissionEmployee
شکل 12.7 | کلاس CommissionEmployee که کلاس Employee را بسط میدهد.
12.5.5 ایجاد کلاس مشتق شده غیرمستقیم و مقید BasePlusCommissionEmployee
کلاس BasePlusCommissionEmployee (شکل 12.8) کلاس CommissionEmployee را بسط میدهد (خط 5) و ازاینرو یک کلاس مشتق شدهی غیرمستقیم از کلاس Employee است. کلاس BasePlusCommissionEmployee دارای سازندهای است (خطوط 10 تا 15) که یک نام، یک نام خانوادگی، شمارهی تأمین اجتماعی، مقدار فروش، نرخ کمیسیون و یک حقوق پایه را به عنوان آرگومان دریافت میکند. پس از آن نام، نام خانوادگی، شمارهی تأمین اجتماعی، میزان فروش و نرخ کمیسیون را به سازندهی CommissionEmployee ارسال میکند (خط 12) تا دادهی کلاس مبنا را مقداردهی اولیه کند. کلاس BasePlusCommissionEmployee در ضمن حاوی خاصیت BaseSalary است (خطوط 19 تا 33) تا متغیر نمونهی baseSalary را دستکاری کند. متد Earnings (خطوط 36 تا 39) درآمد یک BasePlusCommissionEmployee را محاسبه میکند. خط 38 در متد Earnings متد Earnings کلاس مبنای CommissionEmployee را فرامیخواند تا سهم مبتنی بر کمیسیون درآمد کارمند را محاسبه کند. این امر یک بار دیگر مزایای استفادهی مجدد از کد را نشان میدهد. متد ToString کلاس BasePlusCommissionEmployee (خطوط 42 تا 46) رشتهی را به نمایندگی از یک شیء BasePlusCommissionEmployee ایجاد میکند که حاوی "base-salaried" است که با رشتهی حاصل از فراخوان متد ToString کلاس مبنای CommissionEmployee (نمونهای دیگر از استفادهی مجدد از کد)، و سپس حقوق پایه پی گرفته میشود. نتیجه رشتهای است که با "base-salaried commission employee" شروع شده و با اطلاعات باقیماندهی BasePlusCommissionEmployee پی گرفته میشود. به خاطر بیاورید که متد ToString کلاس CommissionEmployee نام، نام خانوادگی و شمارهی تأمین اجتماعی کارمند را با احضار متد ToString کلاس مبنای آن (یعنی Employee) بدست میآورد؛ دلیل دیگری برای استفادهی مجدد از کد. متد ToString کلاس BasePlusCommissionEmployee زنجیرهای از فراخوانهای متد را راهاندازی میکند که در هر سه سطح سلسله مراتب Employee گسترش مییابد.
// Fig. 12.8: BasePlusCommissionEmployee.cs // BasePlusCommissionEmployee class that extends CommissionEmployee. using System; public class BasePlusCommissionEmployee : CommissionEmployee { private decimal baseSalary; // base salary per week // six-parameter constructor public BasePlusCommissionEmployee( string first, string last, string ssn, decimal sales, decimal rate, decimal salary ) : base( first, last, ssn, sales, rate ) { BaseSalary = salary; // validate base salary via property } // end six-parameter BasePlusCommissionEmployee constructor // property that gets and sets // base-salaried commission employee's base salary public decimal BaseSalary { get { return baseSalary; } // end get set { if ( value >= 0 ) baseSalary = value; else throw new ArgumentOutOfRangeException( "BaseSalary", value, "BaseSalary must be >= 0" ); } // end set } // end property BaseSalary // calculate earnings; override method Earnings in CommissionEmployee public override decimal Earnings() { return BaseSalary + base.Earnings(); } // end method Earnings // return string representation of BasePlusCommissionEmployee object public override string ToString() { return string.Format( "base-salaried {0}; base salary: {1:C}", base.ToString(), BaseSalary ); } // end method ToString } // end class BasePlusCommissionEmployee
شکل 12.8 | کلاس BasePlusCommissionEmployee که کلاس CommissionEmployee را بسط میدهد.
12.5.6 پردازش چندریخت، عملگر is و Downcasting
برای آزمایش سلسله مراتب Employee، برنامهی واقع در شکل 12.9 یک شیء از هر یک از چهار کلاس مقید SalariedEmployee، HourlyEmployee، CommissionEmployee و BasePlusCommissionEmployee ایجاد میکند. برنامه این اشیاء را دستکاری میکند، نخست از طریق متغیرهایی از نوع متعلق به هر شیء و سپس به صورت چندفرمی، استفاده کردن از یک آرایه از متغیرهای Employee. هنگام پردازش اشیاء به شکل چندفرمی، برنامه دستمزد پایه هر BasePlusCommissionEmployee را به اندازهی 10 درصد افزایش میدهد (البته این امر نیازمند تشخیص نوع شیء در زمان اجراست). در نهایت، برنامه به شکل چندریختی نوع هر شیء واقع در آرایهی Employee را تشخیص داده و در خروجی به نمایش میگذارد. خطوط 10 تا 20 اشیاء هر یک از چهار کلاس مشتق شدهی Employee مقید را ایجاد میکنند. خطوط 24 تا 32 نمایش رشتهای و درآمد هر یک از این اشیاء را در خروجی به نمایش میگذارند. هرگاه شیء به صورت یک رشته همراه با آیتمهای فرمت به خروجی ارسال شود، متد ToString هر شیء به طور ضمنی توسط WriteLine فراخوان میگردد.
تخصیص اشیاء کلاس مشتق شده به مراجعات کلاس مبنا
خط 35 employees را اعلان کرده و آن را به آرایهای از چهار متغیر Employee تخصیص میدهد. خطوط 38 تا 41 یک شیء SalariedEmployee، یک شیء HourlyEmployee، یک شیء CommissionEmployee و یک شیء BasePlusCommissionEmployee را به ترتیب به employees[0]، employees[1]، employees[2]و employees[3] تخصیص میدهند. تک تک تخصیصها معتبرند زیرا یک SalariedEmployee یک Employee است، یک HourlyEmployee یک Employee است، یک CommissionEmployee یک Employee است و یک BasePlusCommissionEmployee یک Employee است. بنابراین، میتوانیم مراجعات اشیاء SalariedEmployee، HourlyEmployee، CommissionEmployee و BasePlusCommissionEmployee را به متغیرهای کلاس مبنای Employee تخصیص دهیم حتی اگر Employee یک کلاس انتزاعی باشد.
// Fig. 12.9: PayrollSystemTest.cs // Employee hierarchy test application. using System; public class PayrollSystemTest { public static void Main( string[] args ) { // create derived class objects SalariedEmployee salariedEmployee = new SalariedEmployee( "John", "Smith", "111-11-1111", 800.00M ); HourlyEmployee hourlyEmployee = new HourlyEmployee( "Karen", "Price", "222-22-2222", 16.75M, 40.0M ); CommissionEmployee commissionEmployee = new CommissionEmployee( "Sue", "Jones", "333-33-3333", 10000.00M, .06M ); BasePlusCommissionEmployee basePlusCommissionEmployee = new BasePlusCommissionEmployee( "Bob", "Lewis", "444-44-4444", 5000.00M, .04M, 300.00M ); Console.WriteLine( "Employees processed individually:\n" ); Console.WriteLine( "{0}\nearned: {1:C}\n", salariedEmployee, salariedEmployee.Earnings() ); Console.WriteLine( "{0}\nearned: {1:C}\n", hourlyEmployee, hourlyEmployee.Earnings() ); Console.WriteLine( "{0}\nearned: {1:C}\n", commissionEmployee, commissionEmployee.Earnings() ); Console.WriteLine( "{0}\nearned: {1:C}\n", basePlusCommissionEmployee, basePlusCommissionEmployee.Earnings() ); // create four-element Employee array Employee[] employees = new Employee[ 4 ]; // initialize array with Employees of derived types employees[ 0 ] = salariedEmployee; employees[ 1 ] = hourlyEmployee; employees[ 2 ] = commissionEmployee; employees[ 3 ] = basePlusCommissionEmployee; Console.WriteLine( "Employees processed polymorphically:\n" ); // generically process each element in array employees foreach ( Employee currentEmployee in employees ) { Console.WriteLine( currentEmployee ); // invokes ToString // determine whether element is a BasePlusCommissionEmployee if ( currentEmployee is BasePlusCommissionEmployee ) { // downcast Employee reference to // BasePlusCommissionEmployee reference BasePlusCommissionEmployee employee = ( BasePlusCommissionEmployee ) currentEmployee; employee.BaseSalary *= 1.10M; Console.WriteLine( "new base salary with 10% increase is: {0:C}", employee.BaseSalary ); } // end if Console.WriteLine( "earned {0:C}\n", currentEmployee.Earnings() ); } // end foreach // get type name of each object in employees array for ( int j = 0; j < employees.Length; j++ ) Console.WriteLine( "Employee {0} is a {1}", j, employees[ j ].GetType() ); } // end Main } // end class PayrollSystemTest Employees processed individually: salaried employee: John Smith social security number: 111-11-1111 weekly salary: $800.00 earned: $800.00 hourly employee: Karen Price social security number: 222-22-2222 hourly wage: $16.75; hours worked: 40.00 earned: $670.00 commission employee: Sue Jones social security number: 333-33-3333 gross sales: $10,000.00 commission rate: 0.06 earned: $600.00 base-salaried commission employee: Bob Lewis social security number: 444-44-4444 gross sales: $5,000.00 commission rate: 0.04; base salary: $300.00 earned: $500.00 Employees processed polymorphically: salaried employee: John Smith social security number: 111-11-1111 weekly salary: $800.00 earned $800.00 hourly employee: Karen Price social security number: 222-22-2222 hourly wage: $16.75; hours worked: 40.00 earned $670.00 commission employee: Sue Jones social security number: 333-33-3333 gross sales: $10,000.00 commission rate: 0.06 earned $600.00 base-salaried commission employee: Bob Lewis social security number: 444-44-4444 gross sales: $5,000.00 commission rate: 0.04; base salary: $300.00 new base salary with 10% increase is: $330.00 earned $530.00 Employee 0 is a SalariedEmployee Employee 1 is a HourlyEmployee Employee 2 is a CommissionEmployee Employee 3 is a BasePlusCommissionEmployee
شکل 12.9 | آزمایش سلسله مراتب Employee در یک برنامه.
پردازش Employeeها به شکل چندریختی
خطوط 46 تا 66 روی عناصر آرایهی employees حرکت کرده و متدهای ToString و Earnings را با متغیر currentEmployee (از نوع Employee) احضار میکنند؛ در طی هر یک از تکرارها مراجعه به یک Employee متفاوت به متغیر currentEmployee تخصیص داده میشود. خروجی نشان دهندهی این است که براستی متدهای درخور هر کلاس احضار میشوند. تمامی فراخوانها به متدهای مجازی ToString و Earnings در زمان اجرا بر اساس نوع شیئی که currentEmployee به آن اشاره میکند تجزیه تحلیل میشوند. این فرایند با عنوان اتصال (یا انقیاد) پویا (dynamic binding) و یا اتصال (یا انقیاد) دیرهنگام (late binding) شناخته میشود. برای مثال، برای مثال، خط 48 به طور ضمنی متد ToString شیئی را احضار میکند که currentEmployee به آن اشاره مینماید. تنها متدهای کلاس Employee میتوانند از طریق یک متغیر Employee فراخوان شوند؛ و Employee حاوی متدهای کلاس object مانند متد ToString است. (در بخش 11.7 متدهایی که تمامی کلاسها از کلاس object به ارث میبرند مورد بررسی قرار گرفتند.) یک مراجعهی کلاس مبنا میتواند تنها برای احضار متدهای کلاس مبنا به کار گرفته شود.
اعطای 10 درصد اضافه حقوق به BasePlusCommissionEmployeeها
ما پردازش ویژهای را بر روی اشیاء BasePlusCommissionEmployee به اجرا میگذاریم؛ به محض مواجه شدن با این اشیاء حقوق پایهی آنها را به اندازهی 10 درصد افزایش خواهیم داد. هنگام پردازش چندریختی اشیاء، به طور معمول نیاز نیست تا نگران «مشخصات» باشیم، اما برای تنظیم حقوق پایه، میبایست نوع مختص هر یک از اشیاء Employee را در زمان اجرا تشخیص دهیم. خط 51 عملگر is را مورد استفاده قرار میدهد تا مشخص کند که آیا نوع یک شیء Employee بخصوص، BasePlusCommissionEmployee است یا خیر. شرط واقع در خط 51 در صورتی برقرار است که شیء مورد مراجعه قرار گرفته توسط currentEmployee یک BasePlusCommissionEmployee باشد. این شرط برای هر شیء از یک کلاس مشتق شدهی BasePlusCommissionEmployee نیز برقرار است زیرا یک کلاس مشتق شده دارای یک رابطهی «یک...است» با کلاس مبنای خود است. خطوط 55 تا 56 currentEmployee را از نوع Employee به نوع BasePlusCommissionEmployee تبدیل (از نوع downcast) میکند؛ این تبدیل نوع صریح تنها در صورتی مجاز است که شیء دارای یک رابطهی «یک...است» با BasePlusCommissionEmployee باشد. شرط واقع در خط 51 اطمینان میدهد که این مورد بجا میباشد. در صورتی که بخواهیم خاصیت BaseSalary کلاس BasePlusCommissionEmployee را بر روی شیء Employee فعلی مورد استفاده قرار دهیم این تبدیل نوع صریح لازم خواهد بود؛ هر گونه تلاش برای احضار مستقیم یک متد کلاس مشتق شدهی صرف بر روی یک مراجعهی مبنا یک خطای زمان کامپایل در پی خواهد داشت.
خطای برنامهنویسی رایج 12.3
تخصیص یک متغیر کلاس مبنا به یک متغیر کلاس مشتق شده (بدون یک تبدیل نوع صریح از نوع downcast) یک خطای زمان کامپایل در پی خواهد داشت.
ملاحظاتی در باب مهندسی نرمافزار 12.4
اگر در زمان اجرا مراجعه به یک شیء کلاس مشتق شده به متغیری از یکی از کلاسهای مبنای مستقیم یا غیرمستقیم آن تخصیص داده شود، تبدیل نوع صریح مراجعهی ذخیره شده در آن متغیر کلاس مبنا به یک مراجعه از نوع کلاس مشتق شده قابل قبول خواهد بود. قبل از انجام چنین تبدیل نوع صریحی، عملگر is را مورد استفاده قرار دهید تا مطمئن شوید که شیء براستی یک شیء از یک نوع کلاس مشتق شدهی درخور است.
هنگام تبدیل نوع صریح از نوع downcasting یک شیء، در صورتی که در زمان اجرا شیء دارای یک رابطهی «یک...است» با نوع مشخص شده در عملگر cast نباشد یک استثنای InvalidCastException رخ خواهد داد. یک شیء تنها میتواند به نوع متعلق به خود و یا به نوع یکی از کلاسهای مبنای خود تبدیل گردد. شما میتوانید به جای یک عملگر cast، با استفاده از عملگر as برای انجام یک تبدیل از نوع downcast از بروز یک InvalidCastException بالقوه اجتناب نمایید. برای مثال، در عبارت زیر
BasePlusCommissionEmployee employee =
currentEmployee as BasePlusCommissionEmployee;
به employee یک مراجعه به شیئی تخصیص داده شده است که این شیء یک BasePlusCommissionEmployee است، یا در صورتی که currentEmployee یک BasePlusCommissionEmployee نباشد مقدار null به employee تخصیص داده میشود. شما میتوانید پس از آن employee را با null مقایسه کنید تا مشخص شود که آیا تبدیل نوع موفقیتآمیز بوده است یا خیر. اگر رابطهی is در خط 51 برابر true باشد، عبارت if (خطوط 51 تا 62) پردازش ویژهی مورد نیاز برای شیء BasePlusCommissionEmployee را انجام میدهد. با استفاده از متغیر employee (از نوع BasePlusCommissionEmployee)، خط 58 به خاصیت BaseSalary کلاس مشتق شده صرف دسترسی پیدا میکند تا حقوق پایهی کارمند را بازیابی کرده و با 10 درصد افزایش حقوق بروزرسانی مینماید.
خطوط 64 تا 65 متد Earnings را بر روی currentEmployee احضار میکند که این متغیر متد Earnings شیء کلاس مشتق شده را به صورت چندفرمی فراخوانی میکند. بدست آوردن چندفرمی درآمد SalariedEmployee، HourlyEmployee و CommissionEmployee در خطوط 64 تا 65 همان نتایجی را تولید میکند که بدست آوردن درآمد این کارمندان به شکل مجزا در خطوط 24 تا 29 آنها را تولید میکند. با این حال، میزان درآمد بدست آمده برای BasePlusCommissionEmployee در خطوط 64 تا 65 به خاطر 10 درصد افزایش در حقوق پایهاش، بزرگتر از مقداری است که در خطوط 30 تا 32 بدست آمده است.
هر شیئی از نوع متعلق به خود آگاه است
Every Object Knows Its Own Type
خطوط 69 تا 71 نوع هر یک از کارمندان را به صورت یک رشته نمایش میدهند. هر شیء در C# از نوع خودش آگاه است و میتواند از طریق متدهای GetType به این اطلاعات دسترسی پیدا کند؛ متد GetType متدی است که همهی کلاسها آن را از کلاس object ارث میبرند. متد GetType شیئی از کلاس Type (از فضای نام System) برمیگرداند که حاوی اطلاعاتی دربارهی نوع شیء است؛ این اطلاعات شامل نوع کلاس نوع، اسامی متدهای نوع و اسم کلاس مبنای نوع است. خط 71 متد GetType را بر روی شیء احضار میکند تا کلاس زمان اجرای آن نوع را بدست دهد (یعنی یک شیء Typeکه بیانگر نوع شیء است). پس از آن متد ToString به طور ضمنی بر روی شیء برگردانده شده توسط GetType احضار میشود. متد ToString کلاس Type اسم کلاس را برگشت میدهد.
اجتناب از خطای زمان کامپایل با Downcasting
در مثال پیشین، در خطوط 55 تا 56، با downcasting یک متغیر Employee به یک متغیر BasePlusCommissionEmployee، از بروز چندین خطای زمان کامپایل جلوگیری کردیم. اگر عملگر قالببندی (BasePlusCommissionEmployee) را از خط 56 حذف کرده و تلاش کنیم تا متغیر Employee بنام currentEmployee را مستقیماً به متغیر BasePlusCommissionEmployee بنام employee تخصیص دهیم، خطای زمان کامپایل “Cannot implicitly convert type” را دریافت خواهیم کردو این خطا نشان دهندهی این است که هرگونه تلاش برای تخصیص مراجعهی شیء کلاس مبنای commissionEmployee را به متغیر کلاس مشتق شدهی basePlusCommissionEmployee بدون یک عملگر قالببندی مناسب مجاز نیست. کامپایلر از این تخصیص جلوگیری میکند چون که یک CommissionEmployee یک BasePlusCommissionEmployee نیست؛ بار دیگر خاطر نشان میکنیم که رابطهی «یک...است» تنها مابین کلاس مشتق شده و کلاسهای مبنای آن کلاس اعمال میشود و حالت برعکس برقرار نیست.
مشابهاً، اگر خطوط 58 و 61 به جای متغیر کلاس مشتق شدهی employee از متغیر کلاس مبنای currentEmployee استفاده کنند تا از خاصیت BaseSalary که تنها متعلق به کلاس مشتق شده است استفاده کنند، به سبب هر یک از این خطوط یک خطای زمان کامپایل “'Employee' does not contain a definition for 'BaseSalary'” دریافت خواهیم کرد. تلاش برای احضار متدهایی که تنها متعلق به کلاس مشتق شدهاند بر روی یک مراجعهی کلاس مبنا مجاز نیست. در حالی که خطوط 58 و 61 تنها در صورتی اجرا میشوند که عملگر is واقع در خط 51 مقدار true را برگرداند تا نشان دهد که به currentEmployee یک مراجعه به یک شیء BasePlusCommissionEmployee تخصیص داده شده است، ما نمیتوانیم تلاش کنیم تا خاصیت BaseSalary کلاس مشتق شدهی BasePlusCommissionEmployee را بوسیلهی مراجعهی کلاس مبنای Employee با نام currentEmployee مورد استفاده قرار دهیم. کامپایلر خطاهایی را در خطوط 58 و 61 تولید خواهد کرد، زیرا BaseSalary یکی از اعضای کلاس مبنا نیست و نمیتواند با یک متغیر کلاس مبنا به کار گرفته شود. با وجود این که متدی که فراخوان میشود به نوع زمان اجرای شیء وابسته است، یک متغیر میتواند تنها آن متدهایی را احضار کند که اعضای نوع آن متغیر باشند، که کامپایلر معلومش میکند. با استفاده از یک متغیر کلاس مبنای Employee، میتوانیم تنها متدها و خاصیتهای یافت شده در کلاس Employee— یعنی متدهای Earnings و ToString و خاصیتهای FirstName، LastName و SocialSecurityNumber— و متدهای به ارث رسیده از کلاس object را احضار کنیم.
12.5.7 خلاصهای از تخصیصات مجاز مابین متغیرهای کلاس مبنا و کلاس مشتق شده
Summary of the Allowed Assignments Between Base-Class and Derived-Class Variables
حال که برنامهی کاملی را دیدید که اشیاء کلاس مشتق شدهی مختلف را به شکل چندریختی پردازش میکند، آن چه را که شما میتوانید و نمیتوانید با اشیاء و متغیرهای کلاس مبنا و کلاسهای مشتق شده انجام دهید جمعبندی میکنیم. با وجود این که یک شیء کلاس مشتق شده یک شیء کلاس کلاس مبناست، با اینحال دو چیز متفوت هستند. همان طور که پیش از ین بحث کردیم، با اشیاء کلاس مشتق شده میتوان به گونهای رفتار کرد که انگار اشیاء کلاس مبنا بودهاند. اگرچه، کلاس مشتق شده میتواند دارای اعضایی دیگری باشد که تنها متعلق به کلاس مشتق شدهاند. به همین دلیل، تخصیص یک مراجعهی کلاس مبنا به یک متغیر کلاس مشتق شده بدون یک قالببندی صریح مجاز نیست؛ چنین تخصیصی اعضای کلاس مشتق شده را برای یک شیء کلاس مبنا تعریف نشده رها خواهد کرد.
دربارهی چهار روش تخصیص مراجعات کلاس مبنا و کلاس مشتق شده به متغیرهایی از انواع کلاس مبنا و کلاس مشتق شده بحث کردیم:
تخصیص یک مراجعهی کلاس مبنا به یک متغیر کلاس مبنا کار سرراستی است.
تخصیص یک مراجعهی کلاس مشتق شده به یک متغیر کلاس مشتق شده نیز کار سرراستی است.
تخصیص یک مراجعهی کلاس مشتق شده به یک متغیر کلاس مبنا از انظر ایمنی مشکلی ندارد زیرا شیء کلاس مشتق شده یک شیء از کلاس مبنای خود است. با اینحال، این مراجعه میتواند صرفاً برای مراجعه به اعضای کلاس مبنا به کار گرفته شود. اگر این کد از طریق متغیر کلاس مبنا به اعضایی که تنها متعلق کلاس مشتق شدهاند مراجعه کند، کامپایلر خطاهایی را گزارش خواهد کرد.
هرگونه تلاش برای تخصیص یک مراجعهی کلاس مبنا به یک متغیر کلاس متق شده یک خطای کامپایل در پی خواهد داشت. برای اجتناب از این خطا، مراجعهی کلاس مبنا میبایست به طور صریح به یک نوع کلاس مشتق شده قالببندی شود و یا باید با استفاده از عملگر as تبدیل شود. در زمان اجرا، اگر شیئی که مراجعه به آن اشاره میکند یک شیء کلاس مشتق شده نباشد، یک استثنا اتفاق خواهد افتاد. عملگر is میتواند به کار گرفته شود تا اطمینان حاصل شود که یک چنین قالببندی تنها درصورتی انجام میشود که شیء، یک شیء کلاس مشتق شده باشد.
12.6 متدها و کلاسهای sealed
تنها متدهایی که به صورت virtual، override یا abstract اعلان شدهاند میتوانند در کلاسهای مشتق شده بازتعریف شوند. متدی که در یک کلاس مبنا به صورت sealed اعلان شده است نمیتواند در یک کلاس مشتق شده بازتعریف شود. متدهایی که به صورت private اعلان میشوند به طور ضمنی sealed هستند، زیرا بازتعریف کردن آنها در یک کلاس مشتق شده امکانپذیر است (اگرچه کلاس مشتق شده میتواند یک متد جدید را با همان امضای متد private واقع در کلاس مبنا اعلان کند). متدهایی که به صورت static اعلان میشوند نیز به طور ضمنی sealed هستند، زیرا متدهای static نمیتوانند به هیچ طریقی بازتعریف شوند. یک متد کلاس مشتق شدهای که هم به صورت onerride اعلان شده و هم به صورت sealed، میتواند یک متد کلاس مبنا را بازتعریف کند، اما نمیتواند در کلاسهای مشتق شدهای که در بخشهای پایینی سلسله مراتب ارثبری قرار دارند بازتعریف شود (به خاطر بیاورید که oject بالاترین کلاس در سلسله مراتب ارثبری است).
اعلان یک متد sealed هرگز نمیتواند تغییر داده شود ازاینرو تمامی کلاسهای مشتق شده از پیادهسازی یکسانی استفاده میکنند و فراخوانها به متدهای sealed در زمان کامپایل تجزیه تحلیل میشوند؛ این کار با عنوان مقیدسازی استاتیک شناخته میشود. از آنجا کامپایلر میداند که متدهای sealed نمیتوانند بازتعریف شوند، اغلب اوقات میتواند با حذف فراخوانها به متدهای sealed و جایگزین کردن آنها با کد گسترش یافتهی اعلانشان در هر موقعیت فراخوان متد، کد را بهینهسازی کند؛ این تکنیک با عنوان «درون برنامهای کردن کد» شناخته میشود.
نکتهای برای افزایش کارایی 12.1
کامپایلر میتواند برای درون برنامهای کردن یک متد sealed تصمیم بگیرد و این کار را برای متدهای sealed کوچک و ساده انجام خواهد داد. درون برنامهایسازی کپسولهسازی یا پنهانسازی اطلاعات را نقض نمیکند اما باعث افزایش میزان کارایی میشود زیرا باعث حذف بار اضافهی ناشی از انجام یک فراخوان متد میشود.
کلاسی که به صورت sealed اعلان شده باشد نمیتواند یک کلاس مبنا شود (یعنی، هیچ کلاسی نمیتواند یک کلاس sealed را بسط دهد). تمامی متدهای واقع در یک کلاس sealed به طور ضمنی sealed هستند. کلاس string یک کلاس sealed است. این کلاس نمیتواند بسط داده شود، ازاینرو برنامههایی از stringها استفاده میکنند میتوانند به قابلیتها و کارکردهای اشیاء string به همان صورتی که در کتابخانهی کلاس .NET Framework مشخص شدهاند تکیه کنند.
خطای برنامهنویسی رایج 12.4
هرگونه تلاش برای اعلان یک کلاس مشتق شده از یک کلاس sealed یک خطای زمان کامپایل در پی خواهد داشت.
12.7 مطالعهی موردی: ایجاد کردن و استفاده از واسطها
در مثال بعدیمان (شکلهای 12.11 تا 12.15) سیستم پرداخت حقوق بخش 12.5 را مورد بازبینی قرار خواهیم داد. فرض کنید شرکت مایل است تا چندین عملیات حسابداری را در یک برنامهی پرداخت حقوق و رسیدگی به حساب واحد انجام دهد؛ علاوه بر محاسبهی درآمدهای پرداختی که باید به هر یک از کارمندان پردخت شود، شرکت میبایست پرداختهای ناشی از هر کدام از فاکتورها (یعنی صورتحساب مربوط به کالاهای خریداری شده) را هم محاسبه کند. با وجود اعمال شدن به دو چیز نامربوط (یعنی کارمندان و فاکتورها)، هر دو عملیات میبایست با با محاسبهی نوعی از پرداخت کار کنند. برای یک کارمند، پرداخت اشاره به درآمدهای کارمند دارد. برای یک فاکتور، پرداخت به هزینهی کل کالاهای لیست شده در فاکتور اشاره میکند. آیا میتوانیم چنین چیزهای متفاوتی را به شکل چندریختی، به صورت پرداختهای مقرر مربوط به کارمندان و فاکتورها، در یک برنامهی واحد محاسبه کنیم؟ آیا C# قابلیتی در اختیار میگذارد که مستلزم این باشد که کلاسهای نامربوط مجموعهای از متدهای مشترک (مثلاً، متدی که یک مبلغ پرداختی را محاسبه میکند) را پیادهسازی کنند؟ واسطهای C# دقیقاً این قابلیت را در اختیار میگذارند.
واسطها روشهایی را تعریف و استاندارد میکنند که افراد و سیستمها میتوانند از طریق آنها با یکدیگر تعامل برقرار کنند. برای نمونه، کنترلهای واقع بر روی یک رادیو به صورت یک واسط مابین کاربران رادیو و اجزای داخلی آن عمل میکنند. کنترلها به کاربران اجازه میدهند تا مجموعهی محدودی از عملیاتها را انجام دهند (مثلاً، ایستگاه رادیو را تغییر دهند، تن صدای آن تنظیم کنند، مابین فرکانسهای AM و FM یکی را انتخاب کنند) و رادیوهای مختلف میتوانند کنترلها را به شیوههای مختلفی پیادهسازی کنند (مثلاً، استفاده از دکمههای فشاری، صفحات شمارهدار، فرامین صوتی). واسط مشخص میکند که یک رادیو باید اجازهی انجام چه عملیاتهایی را به کاربران بدهد اما نحوهی انجام آنها را مشخص نمیکند. مشابهاً، واسط مابین یک راننده و یک اتومبیل با سیستم تغییر دندهی دستی شامل فرمان، دسته دنده، پدال کلاچ، پدال گاز و پدال ترمز است. این واسط تقریباً در همهی اتومبیلهایی که دارای سیستم تغییر دندهی دستی هستند یافت میشود که هر کسی را که با راندن یک ماشین با سیستم دندهی دستی آشنا باشد قادر میسازد تا هر اتومبیل دیگری با سیستم مشابه را براند. اجزای هر خودرو ممکن است اندکی متفاوت به نظر برسد، اما هدفی کلی یکسان است: اجازه دادن به افراد برای راندن اتومبیل.
اشیاء نرمافزاری نیز از طریق واسطها با همدیگر ارتباط برقرار میکنند. یک واسط C# توصیف کنندهی مجموعهای از متدها و خاصیتهاست که میتوانند روی یک شیء فراخوان شوند؛ تا برای مثال، به آن شیء بگویند که برخی از کارها را انجام دهد و قطعات معینی از اطلاعات را برگرداند. مثال بعد واسطی به نام IPayable را پیادهسازی میکند که توصیف کنندهی قابلیتهای هر شیئی است که میبایست قادر به پرداخت کردن باشد و ازاینرو باید متدی را برای مشخص کردن مقدار مقرر پرداختی صحیح پیشنهاد کند.
اعلان یک واسط با کلمهی کلیدی interface شروع شده و میتواند تنها حاوی متدها، خاصیتها، مولدهای شاخص و رویدادهای انتزاعی (abstract) باشد (رویدادها در فصل 14، رابط کاربر گرافیکی با Windows Forms: بخش 1، بحث خواهند شد). همهی اعضای واسط به طور ضمنی به صورت public و abstract اعلان میشوند. علاوه بر این، هر واسط میتواند یک یا چند واسط را بسط دهد تا واسط استادانهتری را ایجاد کند که کلاسهای دیگر میتوانند آن را پیادهسازی کنند.
خطای برنامهنویسی رایج 12.5
اعلان صریح یک عضو واسط به صورت public یا abstract یک خطای زمان کامپایل در پی خواهد داشت، زیرا آنها در اعلانات عضو واسط زائدند و نیازی به حضورشان نیست. مشخص کردن هرگونه جزئیات پیادهسازی مانند اعلان ملموس متدها در یک واسط نیز یک خطای کامپایل در پی دارد.
برای به کار بردن یک واسط، یک کلاس باید در اعلان کلاس با لیست کردن واسط بعد از کولن (:) مشخص کند که واسط را پیادهسازی میکند. این ترکیب نوشتاری مشابه ترکیب نوشتاری به کار رفته برای نشان دادن ارثبری از یک کلاس مبناست. کلاس ملموسی (یا مقیدی) که واسط را پیادهسازی میکند باید هر یک از اعضای واسط را با امضای مشخص شده در اعلان واسط اعلان کند. کلاسی که یک واسط را پیادهسازی میکند اما همهی اعضای آن را پیادهسازی نمیکند یک کلاس انتزاعی (abstract) است؛ این کلاس باید به صورت abstract اعلان شود و بایست حاوی یک اعلان abstract برای تک تک اعضای پیادهسازی نشدهی واسط باشد. پیادهسازی یک واسط مثل امضای یک قرارداد با کامپایلر است که میگوید «من برای تمامی اعضای مشخص شده توسط واسط یک پیادهسازی تدارک خواهیم دید و یا آنها را به صورت abstract اعلان خواهم کرد».
خطای برنامهنویسی رایج 12.6
کوتاهی در تعریف یا اعلان هر عضو یک واسط در کلاسی که واسط را پیادهسازی میکند منجر به یک خطای زمان کامپایل خواهد شد.
یک واسط به طور معمول زمانی به کار گرفته میشود که کلاسهای نامربوط نیازمند به اشتراکگذاری متدهای مشترک باشند. این حالت به اشیاء کلاسهای نامربوط اجازه میدهد تا به صورت چندریختی پردازش شوند؛ اشیاء کلاسهایی که واسط یکسانی را پیادهسازی میکنند میتوانند به فراخوانهای متد یکسانی واکنش نشان دهند. شما میتوانید واسطی را برای توصیف قابلیتهای مورد نظر ایجاد کنید، بعد از آن این واسط را در هر کلاسی که نیازمند این قابلیت است پیادهسازی کنید. برای نمونه، در برنامهی پرداخت حقوق و صورتحسابهای توسعه یافته در این بخش، واسط IPayable را در هر کلاسی که میبایست قادر به محاسبهی یک مبلغ پرداختی باشد (مثلاً، Employee، Invoice) پیادهسازی خواهیم کرد.
اغلب اوقات، هنگامی که هیچ پیادهسازی پیش فرضی برای به ارث بردن وجود نداشته باشد (یعنی هیچ فیلد و هیچ پیادهسازی متد پیش فرضی وجود نداشته باشد)، یک واسط به جای یک کلاس abstract به کار گرفته میشود. همانند کلاسهای abstract، واسطهای به طور معمول نوعهای public هستند، ازاینرو معمولاً به تنهایی و در فایلهای خودشان با همان اسم واسط و پسوند فایل .cs اعلان میشوند.
12.7.1 توسعهی سلسله مراتب IPayable
برای ایجاد برنامهای که بتواند پرداختهای مربوط به کارمندان و فاکتورها را به یک جور مشخص مشخص کند، نخست واسطی به نام IPayable را ایجاد خواهیم کرد. واسط IPayable حاوی متد GetPaymentAmount است که یک مقدار decimal را برگشت میدهد که به یک شیء از هر کلاسی که واسط را پیادهسازی کند، پرداخت خواهد شد. متد GetPaymentAmount یک نسخهی همه منظوره از متد Earnings سلسله مراتب Employee است؛ متد Earnings یک مبلغ پرداختی را به طور خاص برای یک Employee محاسبه میکند در حالی که GetPaymentAmount میتواند در محدودهی وسیعتری از اشیاء نامربوط به کار گرفته شود. بعد از اعلان واسط IPayable، کلاس Invoice را معرفی خواهیم کرد؛ این کلاس واسط IPayable را پیادهسازی میکند. پس از آن کلاس Employee را به گونهای اصلاح خواهیم کرد تا این کلاس نیز بتواند واسط IPayable را پیادهسازی کند. در نهایت، کلاس مشتق شدهی SalariedEmployee را برورز میکنیم تا در سلسله مراتب IPayable جای گیرد (یعنی، متد Earnings کلاس SalariedEmployee را به GetPaymentAmount تغییر نام میدهیم).
عادت برنامهنویسی خوب 12.1
اسم یک واسط به طور قراردادی با I شروع میشود. این امر به متمایز شدن واسطها از کلاسها کمک کرده و خوانایی کد را بهبود میبخشد.
عادت برنامهنویسی خوب 12.2
هنگام اعلان یک متد در یک واسط، نامی را انتخاب کنید که هدف متد را در حالت کلی توصیف کند، زیرا متد ممکن است توسط طیف وسیعی از کلاسهای نامرتبط پیادهسازی شود.
کلاسهای Invoice و Employee هردو بیانگر چیزهایی هستند که شرکت میبایست قادر به محاسبهی میزان پرداختی برای آنها باشد. هردو کلاس واسط IPayable را پیادهسازی میکنند، ازاینرو یک برنامه میتواند متد GetPaymentAmount را به طور متساوی بر روی اشیاء Invoice و اشیاء Employee احضار نماید. این امر پردازش چندفرمی Invoiceها و Employeeها را که مورد نیاز برنامهی حسابداری شرکت ماست، امکانپذیر میسازد. دیاگرام کلاس UML در شکل 12.10 سلسله مراتب کلاس و واسط به کار گرفته شده در برنامهی حسابداری مورد نظر ما را نشان میدهد. سلسله مراتب با واسط IPayable شروع میشود. UML با جای دادن کلمهی «interface» در میان یک جفت گیومه (« و ») در بالای اسم واسط یک واسط را از یک کلاس متمایز میکند. UML رابطهی مابین یک کلاس و یک واسط را از طریق یک ادراک یا تحقق تبیین میکند. گفته میشود که یک کلاس یک واسط را «تحقق بخشیده» و یا آن را پیادهسازی کرده است.
دیاگرام کلاس یک تحقق را به صورت یک فلش خطچین با سرفلش توخالی که از کلاس پیادهسازی کننده به واسط اشاره میکند، مدل میکند. دیاگرام واقع در شکل 12.10 نشانگر این است که کلاسهای Invoice و Employee هر کدام واسط IPayable را تحقق میبخشند (یعنی پیادهسازی میکنند). همان طور که در دیاگرام کلاس شکل 12.2 نشان داده شده است، کلاس Employee به صورت ایتالیک ظاهر میشود که نشان دهندهی این است که این کلاس یک کلاس انتزاعی میباشد. کلاس مقید SalariedEmployee کلاس Employee را بسط میدهد و رابطهی تحققبخشی کلاس مبنای آن با واسط IPayable را ارث میبرد.
شکل 12.10 | دیاگرام UML سلسله مراتب کلاسی و واسط IPayable.
12.7.2 اعلان واسط IPayable
اعلان واسط IPayable در شکل 12.11 در خط 3 آغاز میشود. واسط IPayable حاوی متد GetPaymentAmount است که به طورضمنی public abstract است (خط 5). این متد نمیتواند صریحاً به صورت public یا abstract اعلان گردد. واسطها میتوانند به هر تعداد عضو داشته باشند و متدهای واسط میتوانند دارای پارامتر (یا پارامترهایی) باشند.
1 // Fig. 12.11: IPayable.cs
2 // IPayable interface declaration.
3 public interface IPayable
4 {
5 decimal GetPaymentAmount(); // calculate payment; no implementation
6 } // end interface IPayable
شکل 12.11 | اعلان واسط IPayable.
12.7.3 ایجاد کلاس Invoice
حال کلاس Invoice را ایجاد میکنیم (شکل 12.12) که بیانگر فاکتور سادهای است که حاوی اطلاعات صورتحساب برای نوعی قطعه است. این کلاس حاوی خصوصیات PartNumber (خط 11)، PartDescription (خط 14)، Quantity (خطوط 27 تا 41) و PricePerItem (خطوط 44 تا 58) است که نشانگر شمارهی قطعه، توضیحی در ارتباط با قطعه، تعداد قطعهی سفارش شده و قیمت هر آیتم هستند. کلاس Invoice ضمناً حاوی یک سازنده (خطوط 17 تا 24) و متد ToString (خطوط 61 تا 67) است که یک نمایش رشتهای یک شیء Invoice را برگشت میدهد. توابع دسترسی set خاصیتهای Quantity و PricePerItem اطمینان میدهند که تنها مقادیر نامنفی به متغیرهای نمونهی quantity و pricePerItem تخصیص داده میشود.
// Fig. 12.12: Invoice.cs // Invoice class implements IPayable. using System; public class Invoice : IPayable { private int quantity; private decimal pricePerItem; // property that gets and sets the part number on the invoice public string PartNumber { get; set; } // property that gets and sets the part description on the invoice public string PartDescription { get; set; } // four-parameter constructor public Invoice( string part, string description, int count, decimal price ) { PartNumber = part; PartDescription = description; Quantity = count; // validate quantity via property PricePerItem = price; // validate price per item via property } // end four-parameter Invoice constructor // property that gets and sets the quantity on the invoice public int Quantity { get { return quantity; } // end get set { if ( value >= 0 ) // validate quantity quantity = value; else throw new ArgumentOutOfRangeException( "Quantity", value, "Quantity must be >= 0" ); } // end set } // end property Quantity // property that gets and sets the price per item public decimal PricePerItem { get { return pricePerItem; } // end get set { if ( value >= 0 ) // validate price pricePerItem = value; else throw new ArgumentOutOfRangeException( "PricePerItem", value, "PricePerItem must be >= 0" ); } // end set } // end property PricePerItem // return string representation of Invoice object public override string ToString() { return string.Format( "{0}: \n{1}: {2} ({3}) \n{4}: {5} \n{6}: {7:C}", "invoice", "part number", PartNumber, PartDescription, "quantity", Quantity, "price per item", PricePerItem ); } // end method ToString // method required to carry out contract with interface IPayable public decimal GetPaymentAmount() { return Quantity * PricePerItem; // calculate total cost } // end method GetPaymentAmount } // end class Invoice
شکل 12.12 | کلاس Invoice واسط IPayable را پیادهسازی میکند.
خط 5 شکل 12.12 نشانگر این است که کلاس Invoice واسط IPayable را پیادهسازی میکند. همانند تمامی کلاسها، کلاس Invoice نیز به طور ضمنی از کلاس object ارث میبرد. C# به کلاسها اجازه نمیدهد تا از بیش از یک کلاس مبنا ارث ببرند، اما به یک کلاس اجازه میدهد تا از یک کلاس مبنا ارث ببرد و هر تعداد واسطی را (در صورت نیاز) پیادهسازی کند. همهی اشیاء کلاسی که چندین واسط را پیادهسازی میکند، با تک تک واسطهای پیادهسازی شده دارای رابطهی «یک...است» هستند. به منظور پیادهسازی بیش از یک واسط، همان طور که در زیر نشان داده شده است، لیستی از اسامی واسط را که با کاما از یکدیگر جدا شدهاند بعد از کولن (:) در اعلان کلاس مورد استفاده قرار دهید:
public class ClassName : BaseClassName, FirstInterface, SecondInterface, …
هرگاه یک کلاس از یک کلاس مبنا ارث برده و یک یا چند واسط را پیادهسازی کند، اعلان کلاس باید اسم کلاس مبنا را قبل از هر نام واسطی لیست کند. کلاس Invoice یک متد واقع در IPayable را پیادهسازی میکند؛ متد GetPaymentAmount در خطوط 70 تا 73 اعلان شده است. این متد مبلغ مورد نیاز برای پرداخت فاکتور را محاسبه میکند. متد مقادیر quantity و pricePerItem را (که از خاصیتهای مقتضی بدست آمدهاند) در هم ضرب میکند و نتیجه را برگشت میدهد (خط 72). این متد شرایط پیادهسازی مربوط به متد واقع در واسط IPayable را برآورده میکند؛ ما قرارداد واسط با کامپایلر را تکمیل کردیم.
12.7.4 اصلاح کلاس Employee برای پیادهسازی واسط IPayable
حال کلاس Employee را تغییر میدهیم تا واسط IPayable را پیادهسازی کند. شکل 12.13 حاوی کلاس Employee اصلاح شده است. این اعلان کلاس به جز دو استثناء با اعلان کلاس موجود در شکل 12.4 یکی است. نخست، خط 3 شکل 12.13 نشانگر این است که کلاس Employee اکنون واسط IPayable را پیادهسازی میکند. به همین خاطر، ما باید در سرتاسر سلسله مراتب Employee، متد Earnings را به GetPaymentAmount تغییر نام دهیم. با این وجود درست مانند متد Earnings موجود در نسخهی از Employee موجود در شکل 12.4، انجام این کار نشان نمیدهد که متد GetPaymentAmount در کلاس Employee پیادهسازی شده باشد، زیرا ما نمیتوانیم پرداخت حقوق متعلق به یک Employee کلی را محاسبه کنیم؛ در ابتدا میبایست از نوع بخصوصی از Employee آگاه باشیم. در شکل 12.4، به همین علت متد Earnings را به صورت abstract اعلان کردیم و در نتیجهی آن، کلاس Employee میبایست به صورت abstract اعلان شده باشد. این امر هر یک از کلاسهای مشتق شدهی Employee را وادار میکند تا متد Earnings را با یک پیادهسازی ملموس بازتعریف کند.
// Fig. 12.13: Employee.cs // Employee abstract base class. public abstract class Employee : IPayable { // read-only property that gets employee's first name public string FirstName { get; private set; } // read-only property that gets employee's last name public string LastName { get; private set; } // read-only property that gets employee's social security number public string SocialSecurityNumber { get; private set; } // three-parameter constructor public Employee( string first, string last, string ssn ) { FirstName = first; LastName = last; SocialSecurityNumber = ssn; } // end three-parameter Employee constructor // return string representation of Employee object public override string ToString() { return string.Format( "{0} {1}\nsocial security number: {2}", FirstName, LastName, SocialSecurityNumber ); } // end method ToString // Note: We do not implement IPayable method GetPaymentAmount here so // this class must be declared abstract to avoid a compilation error. public abstract decimal GetPaymentAmount(); } // end abstract class Employee
شکل 12.13 | کلاس مبنای مجرد Employee.
در شکل 12.13، این وضعیت را به روش مشابهی مدیریت کردیم. به خاطر بیاورید که زمانی که یک کلاس واسطی را پیادهسازی میکند، کلاس قراردادی با کامپایلر امضا میکند که میگوید کلاس یا تک تک متدهای واقع در واسط را پیادهسازی خواهد کرد و یا آنها را به صورت abstract اعلان خواهد کرد. اگر گزینهی آخری انتخاب گردد، ما نیز باید کلاس را به صورت abstract اعلان کنیم. همان طور که در بخش 12.4 بحث کردیم، هر کلاس مشتق شدهی ملموس از کلاس انتزاعی باید متدهای abstract کلاس مبنا را پیادهسازی کند. اگر کلاس مشتق شده این کار را نکند بایستی به صورت abstract اعلان گردد. همان طور که توسط توضیحات واقع در خطوط 19 تا 30 نشان داده شده است، کلاس Employee شکل 12.13 متد GetPaymentAmount را پیادهسازی نمیکند بنابراین کلاس به صورت abstract اعلان شده است.
12.7.5 اصلاح کلاس SalariedEmployee برای استفاده با IPayable
شکل 12.14 حاوی نسخهی اصلاح شدهای از کلاس SalariedEmployee است که Employee را بسط داده و متد GetPaymentAmount را پیادهسازی میکند. این نسخه از SalariedEmployee با نسخهی موجود در شکل 12.5 یکسان است با این استثنا که نسخهی موجود در اینجا به جای متد Earnings متد GetPaymentAmount را پیادهسازی میکند (خطوط 35 تا 38). هر دو متد حاوی قابلیتها و کارکردهای یکسانی هستند اما اسامی متفاوت دارند. به خاطر بیاورید که نسخهی IPayable متد دارای نام کلیتری است تا بتواند به کلاسهای مجزای محتمل قابل اعمال باشد. مابقی کلاسهای مشتق شده (مثلاً، HourlyEmployee، CommissionEmployee و BasePlusCommissionEmployee) نیز باید ویرایش شوند تا به جای Earnings حاوی متد GetPaymentAmount گردند تا این حقیقت را که Employee اکنون IPayable را پیادهسازی میکند بازتاب دهند. انجام این تغییرات را به عهدهی شما میگذاریم و در این بخش تنها SalariedEmployee را در برنامهی آزمایشی خود مورد استفاده قرار میدهیم.
// Fig. 12.14: SalariedEmployee.cs // SalariedEmployee class that extends Employee. using System; public class SalariedEmployee : Employee { private decimal weeklySalary; // four-parameter constructor public SalariedEmployee( string first, string last, string ssn, decimal salary ) : base( first, last, ssn ) { WeeklySalary = salary; // validate salary via property } // end four-parameter SalariedEmployee constructor // property that gets and sets salaried employee's salary public decimal WeeklySalary { get { return weeklySalary; } // end get set { if ( value >= 0 ) // validation weeklySalary = value; else throw new ArgumentOutOfRangeException( "WeeklySalary", value, "WeeklySalary must be >= 0" ); } // end set } // end property WeeklySalary // calculate earnings; implement interface IPayable method // that was abstract in base class Employee public override decimal GetPaymentAmount() { return WeeklySalary; } // end method GetPaymentAmount // return string representation of SalariedEmployee object public override string ToString() { return string.Format( "salaried employee: {0}\n{1}: {2:C}", base.ToString(), "weekly salary", WeeklySalary ); } // end method ToString } // end class SalariedEmployee
شکل 12.14 | کلاس SalariedEmployee که Employee را بسط میدهد.
زمانی که یک کلاس واسطی را پیادهسازی میکند، همان رابطهی «یک...است» مهیا شده توسط ارثبری قابل اعمال است. کلاس Employee واسط IPayable را پیادهسازی میکند ازاینرو میتوان گفت Employee یک IPayable است، درست مانند هر کلاس دیگری که Employee را بسط میدهد. همین طور، اشیاء SalariedEmployee اشیاء IPayable هستند. یک شیء از کلاسی که یک واسط را پیادهسازی میکند میتواند به عنوان یک شیء از نوع واسط در گرفته شود. ازاینرو همان گونه که میتوانیم مراجعهی یک شیء SalariedEmployee را به یک متغیر کلاس مبنای Employee تخصیص دهیم، میتوانیم مراجعهی یک شیء SalariedEmployee را به یک متغیر واسط IPayable تخصیص دهیم. کلاس Invoice واسط IPayable را پیادهسازی میکند، ازاینرو یک شیء Invoice نیز یک شیء IPayable است و میتوانیم مراجعهی یک شیء Invoice را به یک متغیر IPayable تخصیص دهیم.
ملاحظاتی در باب مهندسی نرمافزار 12.5
ارثبری و واسطها در پیادهسازی خود از رابطهی «یک...است» مشابهند. میتوان یک شیء از کلاسی که یک واسط را پیادهسازی میکند به صورت یک شیء از نوع آن واسط در نظر گرفت. یک شیء از هر یک از کلاسهای مشتق شدهی کلاسی که یک واسط را پیادهسازی میکند نیز میتواند به صورت یک شیء از نوع واسط در نظر گرفته شود.
ملاحظاتی در باب مهندسی نرمافزار 12.6
رابطهی «یک...است» که بین کلاسهای مبنا و کلاسهای مشتق شده و بین واسطها و کلاسهایی که آنها را پیادهسازی میکنند، وجود دارد زمان ارسال یک شیء را به یک متد نگه میدارد. زمانی که یک پارامتر متد یک آرگومان را از یک کلاس مبنا یا نوع واسط دریافت میکند، متد به صورت چندریختی شیء دریافت شده را به صورت یک آرگومان پردازش میکند.
12.7.6 استفاده از واسط IPayable برای پردازش چندریختی اشیاء Invoice و Employee
کلاس PayableInterfaceTest (شکل 12.15) نشان دهندهی این است که واسط IPayable میتواند برای پردازش چندریختی مجموعهای از اشیاء Invoice و Employee در یک برنامهی واحد به کار گرفته شود. خط 10 payableObjects را اعلان کرده و آرایهای از چهار متغیر IPayable را به آن تخصیص میدهد. خطوط 13 تا 14 مراجعات اشیاء Invoice را به دو عنصر نخست payableObjects تخصیص میدهند. خطوط 15 تا 18 مراجعههایی از اشیاء SalariedEmployee را به دو عنصر باقیماندهی payableObjects تخصیص میدهند. این تخصیصات مجازند زیرا یک Invoice یک IPayable است، یک SalariedEmployee یک Employee است و یک Employee یک IPayable است. خطوط 24 تا 29 یک عبارت foreach را به کار میبرند تا هر شیء IPayable واقع در payableObjects را به صورت چندریختی پردازش کنند و شیء را به صورت یک رشته همراه با پرداخت مقرر چاپ کنند. خطوط 27 تا 28 به طور ضمنی متد ToString از روی یک مراجعهی واسط IPayable احضار میکنند، حتی اگر ToString در واسط IPayable اعلان نشده باشد؛ همهی مراجعات (از جمله آنهایی که از نوع واسط هستند) به اشیائی مراجعه میکنند object را بسط میدهند و ازاینرو دارای یک متد ToString هستند. خط 28 متد GetPaymentAmount واسط IPayable را احضار میکند تا علیرغم نوع واقعی شیء، مبلغ پرداختی را برای تک تک اشیاء موجود در payableObjects بدست آورد. خروجی نشان میدهد که فراخوانهای متد واقع در خطوط 27 تا 28 پیادهسازی درخور متدهای ToString و GetPaymentAmount را احضار میکنند. برای نمونه، زمانی که currentPayable در طی اولین تکرار حلقهی foreach به یک Invoice اشاره میکند متدهای ToString و GetPaymentAmount کلاس Invoice اجرا میشوند.
ملاحظاتی در باب مهندسی نرمافزار 12.7
همهی متدهای کلاس object میتوانند با استفاده از یک مراجعه از یک نوع واسط فراخوان شوند؛ مراجعه به یک شیء اشاره میکند و همهی اشیاء متدهای کلاس object را به ارث میبرند.
// Fig. 12.15: PayableInterfaceTest.cs // Tests interface IPayable with disparate classes. using System; public class PayableInterfaceTest { public static void Main( string[] args ) { // create four-element IPayable array IPayable[] payableObjects = new IPayable[ 4 ]; // populate array with objects that implement IPayable payableObjects[ 0 ] = new Invoice( "01234", "seat", 2, 375.00M ); payableObjects[ 1 ] = new Invoice( "56789", "tire", 4, 79.95M ); payableObjects[ 2 ] = new SalariedEmployee( "John", "Smith", "111-11-1111", 800.00M ); payableObjects[ 3 ] = new SalariedEmployee( "Lisa", "Barnes", "888-88-8888", 1200.00M ); Console.WriteLine( "Invoices and Employees processed polymorphically:\n" ); // generically process each element in array payableObjects foreach ( var currentPayable in payableObjects ) { // output currentPayable and its appropriate payment amount Console.WriteLine( "payment due {0}: {1:C}\n", currentPayable, currentPayable.GetPaymentAmount() ); } // end foreach } // end Main } // end class PayableInterfaceTest Invoices and Employees processed polymorphically: invoice: part number: 01234 (seat) quantity: 2 price per item: $375.00 payment due: $750.00 invoice: part number: 56789 (tire) quantity: 4 price per item: $79.95 payment due: $319.80 salaried employee: John Smith social security number: 111-11-1111 weekly salary: $800.00 payment due: $800.00 salaried employee: Lisa Barnes social security number: 888-88-8888 weekly salary: $1,200.00 payment due: $1,200.00
شکل 12.15 | آزمایش واسط IPayable .
12.7.7 واسطهای عمومی کتابخانهی کلاس .NET Framework
در این بخش، نگاه اجمالی به چند واسط عمومی تعریف شده در کتابخانهی کلاس .NET Framework خواهیم انداخت. این واسطها به همان روشی که شما واسطهای خود (مثلاً واسط IPayable در بخش 12.7.2) را پیادهسازی کرده و به کار بستید، پیادهسازی شده و به کار گرفته میشوند. واسطهای کتابخانهی کلاس .NET Framework شما را قادر میسازند تا بسیاری از جنبههای مهم C# را با کلاسهای سفارشیتان بسط و گسترش دهید. شکل 12.16 چند واسط کتابخانهی کلاس .NET Framework را که به شکل رایج به کار گرفته میشوند به طور اجمالی مورد بررسی قرار میدهد.
توضیح واسط
C# حاوی عملگرهای مقایسهی متعددی است (مثلاً، ، >=، ==، !=) که به شما اجازه میدهند تا مقادیر نوعهای ساده را با یکدیگر مقایسه کنید. در بخش 12.8 خواهید دید که این عملگرها میتوانند به گونهای تعریف شوند تا بتوانند دو شیء را با هم مقایسه کنند. واسط IComparable میتواند به کار گرفته شود تا به اشیاء کلاسی که این واسط را پیادهسازی کردهاند اجازه دهد تا با یکدیگر مقایسه شوند. واسط حاوی متدی بنام CompareTo است که شیئی را که متد را فراخوان کرده است با شیئی که به عنوان آرگومان به متد ارسال شده است مقایسه میکند. کلاسها باید CompareTo را پیادهسازی کنند تا مقداری را برگرداند که با استفاده از ضوابطی که تعیین میکنید نشان دهد که آیا شیئی که متد روی آن احضار شده است کمتراز (مقدار برگشتی صحیح منفی)، برابر با (مقدار برگشتی صفر) و یا بزرگتر از (مقدار برگشتی صحیح مثبت) شیئی است که به عنوان آرگومان به متد ارسال شده است. برای مثال، اگر کلاس Employee واسط IComparable را پیادهسازی کند، متد CompareTo آن میتواند اشیاء Employee را به واسطهی میزان درآمدشان مقایسه کند. واسط IComparable عموماً برای مرتبسازی اشیاء در کلکسیونی چون یک آرایه به کار برده میشود. در فصل 22، جنریکها، و فصل 23، کلکسیونها، از واسط IComparable استفاده خواهیم کرد. IComparable
این واسط توسط هر کلاسی که بیانگر یک کامپوننت باشد پیادهسازی میشود، ازجمله کنترلهای رابط کاربر گرافیکی (GUI) مانند دکمهها یا برچسبها. واسط IComponent معرف رفتاری است که کامپوننتها باید پیادهسازی کنند. در فصل 14، رابط کاربر گرافیکی با Windows Forms: بخش 1، و فصل 15، رابط کاربر گرافیکی با Windows Forms: بخش 2، دربارهی IComponent و بسیاری از کنترلهای GUI که این واسط را پیادهسازی میکنند بحث خواهیم کرد. IComponent
این واسط توسط کلاسهایی پیادهسازی میشود که بایست یک مکانیزم صریح را برای آزادسازی منابع تدارک ببینند. برخی از منابع میتوانند در هر لحظه تنها در یک برنامه به کار گرفته شوند. علاوه براین، برخی از منابع مانند فایلهای واقع بر روی دیسک، منابع مدیریت نشدهای هستند که برخلاف حافظه، نمیتوانند توسط جمع کنندهی زباله آزاد شوند. کلاسهایی که واسط IDisposable را پیادهسازی میکنند یک متد Dispose را در اختیار میگذارند که میتواند برای آزادسازی صریح منابع فراخوان گردد. در فصل 13، مدیریت استثنا، واسط IDisposable را به طور مختصر مورد بحث و بررسی قرار خواهیم داد. شما میتوانید با مراجعه به آدرس msdn.microsoft.com/en-us/library/system.idisposable.aspx مطالب بیشتری را پیرامون این واسط یاد بگیرید. مقالهی MSDN با عنوان Implementing a Dispose Method که در آدرس msdn.microsoft.com/en-us/library/fs2xkftw.aspx قرار دارد دربارهی نحوهی پیادهسازی صحیح این واسط در کلاسهای شخصیتان بحث میکند. IDisposable
این واسط برای حرکت در میان (یا به عبارتی پیمایش) عناصر یک کلکسیون (مانند یک آرایه) و به صورت یک عنصر در هر دفعه به کار برده میشود. واسط IEnumerable حاوی متد MoveNext برای حرکت به عنصر بعدی واقع در یک کلکسیون، متد Reset برای حرکت به موقعیت ماقبل اولین عنصر و خاصیت Current برای برگرداندن شیء واقع در موقعیت جاری است. واسط IEnumerable را در فصل 23 مورد استفاده قرار خواهیم داد. IEnumerator
شکل 12.16 | واسطهای عمومی کتابخانهی کلاس .NET Framework
Common Interfaces of the .NET Framework Class Library
12.8 سربارگذاری عملگر
Operator Overloading
دستکاری اشیاء با ارسال پیغامهایی (در قالب فراخوان متدها) به اشیاء صورت میگیرد. این مفهوم فراخوان متد برای برخی از انواع کلاسها، بخصوص کلاسهای محاسباتی سنگین است. برای این کلاسها، استفاده از مجموعهی پرباری از عملگرهای توکار C# برای مشخص کردن دستکاری اشیاء سرراستتر میباشد. در این بخش، نشان خواهیم داد که چگونه میتوان این عملگرها را (از طریق یک فرایند بنام سربارگذاری عملگر) قادر ساخت تا با اشیاء کلاس نیز کار کنند.
C# شما را قادر میسازد تا اغلب عملگرها را سربارگذاری نمایید تا آنها را حساس به متنی سازید که در آن به کار برده میشوند. برخی از عملگرها، بخصوص چندین عملگر محاسباتی نظیر + و -، به کرات بیشتر از سایرین سربارگذاری میشوند، جایی که مفهوم عملگر اغلب اوقات خیلی بدیهی میباشد. شکلهای 12.17 و 12.18 نمونهای از کاربرد سربارگذاری عملگر را با یک کلاس ComplexNumber در اختیار میگذارند. برای مشاهدهی لیست عملگرهای قابل سربارگذاری، به آدرس msdn.microsoft.com/en-us/library/8edha89s.aspx مراجعه نمایید.
کلاس ComplexNumber (شکل 12.17) عملگرهای به اضافه (+)، منها (-) و ضرب (*) را سربارگذاری میکند تا برنامهها را قادر سازد تا نمونههای کلاس ComplexNumber را با استفاده از مفهوم محاسباتی رایج، جمع، تفریق و ضرب نمایند. خطوط 9 و 12 خصوصیات مربوط به اجزای Real (حقیقی) و Imaginary (موهوم) عدد مختلط را تعریف میکنند.
// Fig. 12.17: ComplexNumber.cs // Class that overloads operators for adding, subtracting // and multiplying complex numbers. using System; public class ComplexNumber { // read-only property that gets the real component public double Real { get; private set; } // read-only property that gets the imaginary component public double Imaginary { get; private set; } // constructor public ComplexNumber( double a, double b ) { Real = a; Imaginary = b; } // end constructor // return string representation of ComplexNumber public override string ToString() { return string.Format( "({0} {1} {2}i)", Real, ( Imaginary < 0 ? "-" : "+" ), Math.Abs( Imaginary ) ); } // end method ToString // overload the addition operator public static ComplexNumber operator+ ( ComplexNumber x, ComplexNumber y ) { return new ComplexNumber( x.Real + y.Real, x.Imaginary + y.Imaginary ); } // end operator + // overload the subtraction operator public static ComplexNumber operator- ( ComplexNumber x, ComplexNumber y ) { return new ComplexNumber( x.Real - y.Real, x.Imaginary - y.Imaginary ); } // end operator - // overload the multiplication operator public static ComplexNumber operator* ( ComplexNumber x, ComplexNumber y ) { return new ComplexNumber( x.Real * y.Real - x.Imaginary * y.Imaginary, x.Real * y.Imaginary + y.Real * x.Imaginary ); } // end operator * } // end class ComplexNumber
شکل 12.17 | کلاسی که عملگرها را برای جمع، تفریق و ضرب اعداد مختلط سربارگذاری میکند.
خطوط 29 تا 34 عملگر به اضافه (+) را سربارگذاری میکنند تا عمل جمع ComplexNumberها را انجام دهد. کلمهی کلیدی operator که با یک نماد عملگر پی گرفته میشود نشانگر این است که یک متد عملگر مشخص شده را سربارگذاری میکند. متدهایی که عملگرهایی باینری را سربارگذاری میکنند باید دو آرگومان دریافت کنند. اولین آرگومان عملوند سمت چپی است و دومین آرگومان عملوند سمت راستی میباشد. عملگر به اضافهی سربارگذاری شدهی کلاس ComplexNumber دو مراجعهی ComplexNumber را به عنوان آرگومان گرفته و یک ComplexNumber را برمیگرداند که بیانگر مجموع آرگومانهاست. این متد به صورت public و static علامتگذاری شده است که این تصریح کنندهها مورد نیاز عملگرهای سربارگذاری شده هستند. بدنهی متد (خطوط 32 تا 33) عمل جمع را انجام داده و نتیجه را به صورت یک ComplexNumber جدید برمیگرداند. توجه داشته باشید که ما محتویات هیچ کدام از عملوندهای اصلی را که به صورت آرگومانهای x و y ارسال شدهاند تغییر نمیدهیم. این امر با احساس شهودی ما از نحوهی عملکرد این عملگر مطابقت دارد؛ جمع زدن دو عدد هیچ یک از اعداد اصلی را تغییر نمیدهد. خطوط 37 تا 51 عملگرهای سربارگذاری شدهی مشابهی را برای تفریق و ضرب ComplexNumberها در اختیار میگذارند.
ملاحظاتی در باب مهندسی نرمافزار 12.8
عملگرها را سربارگذاری کنید تا همان عملیات یا عملیات مشابه با آن را بر روی اشیاء کلاس انجام دهند که بر روی اشیائی از نوعهای ساده انجام میدهند.
ملاحظاتی در باب مهندسی نرمافزار 12.9
دست کم یکی از پارامترهای یک عملگر سربارگذاری شده میبایست یک مراجعه به یک شیء از کلاسی باشد که عملگر در آن سربارگذاری شده است. این کار شما را از تغییر دادن نحوهی عملکرد عملگرها بر روی انواع ساده بازمیدارد.
کلاس ComplexTest (شکل 12.18) به تبیین عملگرهای +، - و * که بر روی ComplexNumber سربارگذاری شدهاند، میپردازد. خطوط 14 تا 27 از کاربر درخواست میکنند تا دو عدد مختلط را وارد کند، سپس این ورودیها را برای ایجاد دو شیء ComplexNumber مورد استفاده قرار میدهند و آنها را به متغیرهای x و y را نسبت میدهند.
// Fig 12.18: OperatorOverloading.cs // Overloading operators for complex numbers. using System; public class ComplexTest { public static void Main( string[] args ) { // declare two variables to store complex numbers // to be entered by user ComplexNumber x, y; // prompt the user to enter the first complex number Console.Write( "Enter the real part of complex number x: " ); double realPart = Convert.ToDouble( Console.ReadLine() ); Console.Write( "Enter the imaginary part of complex number x: " ); double imaginaryPart = Convert.ToDouble( Console.ReadLine() ); x = new ComplexNumber( realPart, imaginaryPart ); // prompt the user to enter the second complex number Console.Write( "\nEnter the real part of complex number y: " ); realPart = Convert.ToDouble( Console.ReadLine() ); Console.Write( "Enter the imaginary part of complex number y: " ); imaginaryPart = Convert.ToDouble( Console.ReadLine() ); y = new ComplexNumber( realPart, imaginaryPart ); // display the results of calculations with x and y Console.WriteLine(); Console.WriteLine( "{0} + {1} = {2}", x, y, x + y ); Console.WriteLine( "{0} - {1} = {2}", x, y, x - y ); Console.WriteLine( "{0} * {1} = {2}", x, y, x * y ); } // end method Main } // end class ComplexTest Enter the real part of complex number x: 2 Enter the imaginary part of complex number x: 4 Enter the real part of complex number y: 4 Enter the imaginary part of complex number y: -2 (2 + 4i) + (4 - 2i) = (6 + 2i) (2 + 4i) - (4 - 2i) = (-2 + 6i) (2 + 4i) * (4 - 2i) = (16 + 12i)
شکل 12.18 | عملگرهای سربارگذاری شده برای اعداد مختلط.
خطوط 31 تا 33 متغیرهای x و y را بوسیلهی عملگرهای سربارگذاری شده جمع زده، از هم تفریق کرده و در هم ضرب مینمایند، سپس نتایج را در خروجی به نمایش میگذارند. در خط 31، عمل جمع را با استفاده از عملگر به اضافه (+) همراه با عملوندهای x و y از نوع ComplexNumber انجام میدهد. بدون سربارگذاری عملگر، رابطهی x+y مفهومی نخواهد داشت؛ کامپایلر نمیداند که چگونه دو شیء از کلاس ComplexNumber با هم جمع میشوند. این رابطه در اینجا دارای معناست زیرا در خطوط 29 تا 34 شکل 12.17 عملگر به اضافه را برای دو شیء ComplexNumber تعریف کردهایم. هرگاه دو ComplexNumber در خط 31 شکل 12.18 جمع گردند، این کار ضمن ارسال عملوند سمت چپی به عنوان آرگومان نخست و عملوند سمت راستی به عنوان دوم، اعلان operator+ را احضار میکند.
زمانی که عملگرهای تفریق و ضرب را در خطوط 32 تا 33 مورد استفاده قرار میدهیم، مشابهاً اعلان عملگر سربارگذاری شدهی مربوط به آنها احضار میشوند. نتیجهی هر محاسبه یک مراجعه به یک شیء جدید ComplexNumber است. هرگاه این شیء جدید به متد WriteLine کلاس Console ارسال گردد، متد ToString آن (خطوط 22 تا 26 شکل 12.17) به طور ضمنی احضار میشود. خط 31 شکل 12.18 میتواند به صورت زیر بازنویسی گردد تا به طور صریح متد ToString شیء ایجاد شده توسط عملگر سربارگذاری شدهی جمع را احضار نماید:
Console.WriteLine( "{0} + {1} = {2}", x, y, ( x + y ).ToString() );
12.9 خلاصه فصل
در این فصل به معرفی مفهوم چندریختی (یا چندفرمی) پرداختیم؛ چندریختی توانایی پردازش اشیائی است که کلاس مبنای یکسانی را در سلسله مراتب کلاسی به اشتراک میگذارند به گونهای که انگار همگی آنها اشیاء کلاس مبنا بودهاند. این فصل دربارهی این که چگونه چندریختی سیستمها را قابل توسعه و قابل نگهداری میسازد بحث کرد، سپس نحوهی استفاده از متدهای بازتعریف شده را برای عملی کردن رفتار چندریختی نشان داد. در این فصل مفهوم کلاس انتزاعی را معرفی کردیم؛ کلاس انتزاعی به شما امکان میدهد تا کلاس مبنای مناسبی را که کلاسهای دیگر میتوانند از آن ارثبری کنند، تدارک ببینید. آموختید که یک کلاس انتزاعی میتواند متدهای انتزاعی را اعلان نماید که هر یک از کلاسهای مشتق شده برای این که یک کلاس مقید گردد باید آنها را پیادهسازی نماید، و این که یک برنامه میتواند متغیرهای یک کلاس انتزاعی را برای احضار پیادهسازی کلاس مشتق شده از متدهای انتزاعی به صورت چندفرمی مورد استفاده قرار دهد. در ضمن چگونگی مشخص کردن نوع یک شیء را در زمان اجرا آموختید. ما نحوهی ایجاد متدها و کلاسهای sealed را نشان دادیم. در این فصل دربارهی اعلان و پیادهسازی یک واسط به عنوان یک روش جایگزین برای دستیابی به رفتار چندریختی، اغلب در میان اشیائی از کلاسهای مختلف بحث و بررسی شد. در نهایت، با سربارگذاری عملگر، نحوهی تعریف رفتار عملگرهای توکار را بر روی اشیاء کلاسهای متعلق به خودتان فراگرفتید.
اکنون بایستی با کلاسها، اشیاء، کپسولهسازی، ارثبری، واسطها و چندریختی آشنا شده باشید؛ اینها اصلیترین جنبههای برنامهنویسی شیءگرا هستند. در آینده، نگاه عمیقتری به استفاده از مدیریت استثنا برای رسیدگی به خطاهای زمان اجرا خواهیم انداخت.
نظرات (۰)